Compare commits

...

85 Commits

Author SHA1 Message Date
2dust
5d6c5da9d9 up 7.15.0 2025-09-28 19:12:58 +08:00
2dust
ade2db3903 Code clean 2025-09-28 19:12:17 +08:00
Wydy
7f07279a4c Update pac (#7991) 2025-09-28 19:08:29 +08:00
2dust
b25d4d57bd Fix ProfilesSelectWindow 2025-09-27 19:46:31 +08:00
2dust
46edd8f9a4 Bug fix 2025-09-27 18:07:20 +08:00
JieXu
ebb95b5ee8 Update MsgView.axaml.cs (#8042) 2025-09-27 17:02:49 +08:00
2dust
dc4611a258 Adjust qrcode width 2025-09-26 20:36:27 +08:00
2dust
03d5b7a05b Bug fix 2025-09-26 17:11:48 +08:00
2dust
a652fd879b Added simple highlight function to the message view 2025-09-26 15:29:46 +08:00
2dust
326bf334e7 Optimize and improve MsgView 2025-09-26 15:07:33 +08:00
JieXu
21a773f400 Update MsgView.axaml.cs Plan C (#8035)
* Add avaloniaEdit for test

* Adjust avaloniaEdit

* Optimize and improve message function

* Update build-linux.yml

* Update MsgView.axaml

* Update MsgView.axaml.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2025-09-26 13:55:35 +08:00
2dust
d86003df55 Optimize and improve the Subject 2025-09-25 10:56:10 +08:00
2dust
faff8e4ea2 Remove secret data from mihomo configuration 2025-09-24 18:41:00 +08:00
2dust
6b85aa0b03 Remove Splat.NLog package 2025-09-24 10:57:23 +08:00
2dust
671678724b Optimization and improvement, using event subscribers 2025-09-24 10:57:06 +08:00
2dust
e96a4818c4 Optimization and improvement 2025-09-23 15:31:19 +08:00
2dust
0377e7ce19 Optimization and improvement, using event subscribers 2025-09-23 14:27:42 +08:00
2dust
6929886b3e Optimization and improvement, using event subscribers 2025-09-23 12:08:43 +08:00
2dust
721d70c8c7 Update Directory.Packages.props 2025-09-23 11:39:57 +08:00
2dust
27b45aee83 Optimization and improvement, using event subscribers 2025-09-23 11:39:55 +08:00
2dust
18ac76e683 up 7.14.12 2025-09-21 14:50:01 +08:00
2dust
3e1e23a524 Update Directory.Packages.props 2025-09-21 14:48:54 +08:00
2dust
534c7ab444 Optimize and improve QR code display 2025-09-21 14:35:49 +08:00
2dust
c2c13ad318 Create v2rayN.slnx
https://github.com/2dust/v2rayN/pull/7969
2025-09-21 12:12:24 +08:00
2dust
3a21596d95 Fix node domain resolving in TUN mode
https://github.com/2dust/v2rayN/pull/7989
2025-09-21 12:05:06 +08:00
2dust
ef30d389dc up 7.14.11 2025-09-20 14:06:55 +08:00
2dust
bf8783fed7 Update CheckUpdateViewModel.cs 2025-09-20 14:06:41 +08:00
DHR60
4e042295d2 Add global fakeip and fakeip filter (#7919) 2025-09-13 14:55:30 +08:00
2dust
33d9c5db6c up GlobalUsings 2025-09-13 14:46:35 +08:00
DHR60
cb182125f6 Fix (#7946)
https://github.com/2dust/v2rayN/pull/7937
2025-09-13 11:13:09 +08:00
2dust
ec627bdb82 up 7.14.10 2025-09-13 09:53:03 +08:00
2dust
4606e78570 Update Directory.Packages.props 2025-09-13 09:46:28 +08:00
2dust
f00e968b8f Bug fix
https://github.com/2dust/v2rayN/issues/7944
2025-09-13 09:41:34 +08:00
DHR60
a87a015c03 Fix some minor UI bugs (#7941) 2025-09-12 20:28:24 +08:00
2dust
c559914ff7 Fix
https://github.com/2dust/v2rayN/issues/7938
2025-09-12 17:01:53 +08:00
2dust
436d95576e Optimization and improvement JsonUtils 2025-09-12 16:45:55 +08:00
DHR60
54e83391d0 Pre-resolve to apply hosts (#7937) 2025-09-12 16:28:31 +08:00
JieXu
3e0578f775 Update CheckUpdateViewModel.cs (#7932)
* Update CheckUpdateViewModel.cs

* Update Utils.cs

* Update Utils.cs

* Update Utils.cs

* Update CheckUpdateViewModel.cs

* Update CheckUpdateViewModel.cs

* Update Utils.cs
2025-09-12 16:24:59 +08:00
2dust
29a5abf4d6 Optimization and improvement 2025-09-10 19:43:11 +08:00
2dust
b54c67d6f1 up 7.14.9 2025-09-09 20:18:55 +08:00
2dust
b49486cc23 Update ProfilesSelectWindow.axaml 2025-09-09 20:00:00 +08:00
JieXu
b95830b3d5 Update package-rhel.sh package-debian.sh MainWindowViewModel.cs (#7910)
* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update MainWindowViewModel.cs

* Update package-rhel.sh

* Update package-debian.sh
2025-09-09 19:51:10 +08:00
2dust
8e0c5cb9aa Bug fix
https://github.com/2dust/v2rayN/issues/7914
2025-09-09 17:55:15 +08:00
2dust
6ffb3bd30c up 7.14.8 2025-09-08 18:48:56 +08:00
2dust
2826444ffc Code clean 2025-09-08 18:45:21 +08:00
JieXu
56c3e9c46d Fix package-appimage.sh bugs. (#7904)
* Update package-appimage.sh

* Delete pkg2appimage.yml
2025-09-08 18:02:54 +08:00
th1nker
0770e30034 fix: 修正获取系统hosts (#7903)
- 修复当host的记录存在行尾注释时,无法将其添加到dns.host中

示例hosts
```
127.0.0.1 test1.com
127.0.0.1 test2.com # test
```
在之前仅仅会添加`127.0.0.1 test1.com`这条记录,而忽略另一条
2025-09-08 18:02:44 +08:00
DHR60
04195c2957 Profiles Select Window (#7891)
* Profiles Select Window

* Sort

* wpf

* avalonia

* Allow single select

* Fix

* Add Config Type Filter

* Remove unnecessary
2025-09-07 18:58:59 +08:00
JieXu
d18d74ac1c Update package-debian.sh (#7899) 2025-09-07 16:39:54 +08:00
2dust
6391667c15 up 7.14.7 2025-09-06 16:59:54 +08:00
2dust
7f26445327 Update Directory.Packages.props 2025-09-06 16:59:38 +08:00
2dust
291d4bd8e5 Update Directory.Packages.props 2025-09-06 16:52:57 +08:00
dependabot[bot]
f2f3a7eb5f Bump actions/setup-dotnet from 4.3.1 to 5.0.0 (#7883)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4.3.1 to 5.0.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4.3.1...v5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-06 15:46:34 +08:00
JieXu
e7609619d4 Update package-debian.sh (#7888) 2025-09-06 15:46:20 +08:00
DHR60
84bf9ecfaf Fix DNS Regional Presets (#7885) 2025-09-05 18:11:16 +08:00
2dust
a2917b3ce8 Update Directory.Packages.props 2025-09-04 20:50:25 +08:00
2dust
d094370209 up 7.14.6 2025-09-03 19:05:45 +08:00
2dust
1a6fbf782d Using RxApp replace ViewAction 2025-09-02 17:12:38 +08:00
2dust
3f67a23f8b up 7.14.5 2025-08-31 19:55:17 +08:00
2dust
b8eb7e7b29 Optimization and Improvement. 2025-08-31 15:41:25 +08:00
2dust
1d69916410 Update GlobalHotKeys 2025-08-31 14:21:22 +08:00
2dust
49fa103077 Optimize UI 2025-08-31 14:08:05 +08:00
2dust
e3a63db966 Using RxApp replace ViewAction 2025-08-30 20:36:16 +08:00
DHR60
ef4a1903ec Update mihomo download url (#7852) 2025-08-30 19:44:54 +08:00
2dust
5a3286dad1 Using RxApp replace ViewAction 2025-08-30 19:32:07 +08:00
2dust
058c6e4a85 Use Rx event subscription instead of MessageBus to send information 2025-08-29 15:46:09 +08:00
2dust
ea1d438e40 Use Rx event subscription to replace MessageBus refresh configuration file function 2025-08-29 14:46:08 +08:00
2dust
a108eaf34b Optimization and Improvement.
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 11:53:30 +08:00
2dust
da28c639b3 Optimization and Improvement.
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 11:40:08 +08:00
2dust
8ef68127d4 Optimization and Improvement.
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 10:53:57 +08:00
2dust
f39d966a33 Optimization and Improvement.
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 10:31:09 +08:00
2dust
f83e83de13 Optimization and Improvement
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 09:49:30 +08:00
2dust
abdafc9b3b up 7.14.4 2025-08-27 20:29:16 +08:00
2dust
8f93c50151 Bug fix 2025-08-27 17:22:13 +08:00
2dust
fe7c505cc9 Update subscription using Task.Run 2025-08-27 17:14:24 +08:00
2dust
0d5afa4ff5 Optimizing SQLite performance
https://github.com/2dust/v2rayN/issues/7835
2025-08-26 20:56:28 +08:00
2dust
2ad716a4ad Remove Cursor="Hand" 2025-08-26 17:46:43 +08:00
DHR60
cddf88730f Fix dns (#7834) 2025-08-26 17:34:12 +08:00
DHR60
3eb49aa24c Add mieru support (#7828) 2025-08-25 17:43:53 +08:00
2dust
45c987fd86 up 7.14.3 2025-08-23 16:36:12 +08:00
2dust
7bec05ec23 Fix
https://github.com/2dust/v2rayN/issues/7819
2025-08-23 16:28:52 +08:00
2dust
606b216cd0 Press the Esc button to close the window
https://github.com/2dust/v2rayN/issues/7819
2025-08-23 16:23:30 +08:00
2dust
bb4f33559f Code clean 2025-08-21 19:55:17 +08:00
2dust
c7f3e53f28 Customize MenuFlyoutMaxHeight for desktop version 2025-08-21 19:32:39 +08:00
JieXu
0035e836d7 Update build-linux.yml, Add RPM package for RHEL. (#7813)
* Update build-linux.yml

* Update build-linux.yml

* Update build-linux.yml

* Update build-linux.yml

* Update package-rhel.sh

* Update package-rhel.sh. Change describe information

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh
2025-08-21 17:18:19 +08:00
154 changed files with 3719 additions and 2063 deletions

View File

@@ -22,7 +22,7 @@ jobs:
matrix: matrix:
configuration: [Release] configuration: [Release]
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout - name: Checkout
@@ -32,7 +32,7 @@ jobs:
fetch-depth: '0' fetch-depth: '0'
- name: Setup - name: Setup
uses: actions/setup-dotnet@v4.3.1 uses: actions/setup-dotnet@v5.0.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
@@ -99,3 +99,37 @@ jobs:
tag: ${{ github.event.inputs.release_tag }} tag: ${{ github.event.inputs.release_tag }}
file_glob: true file_glob: true
prerelease: true prerelease: true
# release RHEL package
- name: Package RPM (RHEL-family)
if: github.event.inputs.release_tag != ''
run: |
chmod 755 package-rhel.sh
# Build for both x86_64 and aarch64 in one go (explicit version passed; no --buildfrom)
./package-rhel.sh "${{ github.event.inputs.release_tag }}" --arch all
- name: Collect RPMs into workspace
if: github.event.inputs.release_tag != ''
run: |
mkdir -p "${{ github.workspace }}/dist/rpm"
rsync -av "$HOME/rpmbuild/RPMS/" "${{ github.workspace }}/dist/rpm/"
# Rename to requested filenames
find "${{ github.workspace }}/dist/rpm" -name "v2rayN-*-1.x86_64.rpm" -exec mv {} "${{ github.workspace }}/dist/rpm/v2rayN-linux-rhel-x64.rpm" \; || true
find "${{ github.workspace }}/dist/rpm" -name "v2rayN-*-1.aarch64.rpm" -exec mv {} "${{ github.workspace }}/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
- name: Upload RPM artifacts
if: github.event.inputs.release_tag != ''
uses: actions/upload-artifact@v4.6.2
with:
name: v2rayN-rpm
path: |
${{ github.workspace }}/dist/rpm/**/*.rpm
- name: Upload RPMs to release
uses: svenstaro/upload-release-action@v2
if: github.event.inputs.release_tag != ''
with:
file: ${{ github.workspace }}/dist/rpm/**/*.rpm
tag: ${{ github.event.inputs.release_tag }}
file_glob: true
prerelease: true

View File

@@ -32,7 +32,7 @@ jobs:
fetch-depth: '0' fetch-depth: '0'
- name: Setup - name: Setup
uses: actions/setup-dotnet@v4.3.1 uses: actions/setup-dotnet@v5.0.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'

View File

@@ -32,7 +32,7 @@ jobs:
fetch-depth: '0' fetch-depth: '0'
- name: Setup - name: Setup
uses: actions/setup-dotnet@v4.3.1 uses: actions/setup-dotnet@v5.0.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'

View File

@@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v5.0.0 uses: actions/checkout@v5.0.0
- name: Setup - name: Setup
uses: actions/setup-dotnet@v4.3.1 uses: actions/setup-dotnet@v5.0.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'

View File

@@ -1,14 +1,67 @@
#!/bin/bash #!/bin/bash
set -euo pipefail
# Install deps
sudo apt update -y sudo apt update -y
sudo apt install -y libfuse2 sudo apt install -y libfuse2 wget file
wget -O pkg2appimage https://github.com/AppImageCommunity/pkg2appimage/releases/download/continuous/pkg2appimage-1eceb30-x86_64.AppImage
chmod a+x pkg2appimage # Get tools
export AppImageOutputArch=$OutputArch wget -qO appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
export OutputPath=$OutputPath64 chmod +x appimagetool
./pkg2appimage ./pkg2appimage.yml
mv out/*.AppImage v2rayN-${AppImageOutputArch}.AppImage # x86_64 AppDir
export AppImageOutputArch=$OutputArchArm APPDIR_X64="AppDir-x86_64"
export OutputPath=$OutputPathArm64 rm -rf "$APPDIR_X64"
./pkg2appimage ./pkg2appimage.yml mkdir -p "$APPDIR_X64/usr/lib/v2rayN" "$APPDIR_X64/usr/bin" "$APPDIR_X64/usr/share/applications" "$APPDIR_X64/usr/share/pixmaps"
mv out/*.AppImage v2rayN-${AppImageOutputArch}.AppImage cp -rf "$OutputPath64"/* "$APPDIR_X64/usr/lib/v2rayN" || true
[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/usr/share/pixmaps/v2rayN.png" || true
[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/v2rayN.png" || true
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_X64/AppRun"
chmod +x "$APPDIR_X64/AppRun"
ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_X64/usr/bin/v2rayN"
cat > "$APPDIR_X64/v2rayN.desktop" <<EOF
[Desktop Entry]
Name=v2rayN
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
Exec=v2rayN
Icon=v2rayN
Terminal=false
Type=Application
Categories=Network;
EOF
install -Dm644 "$APPDIR_X64/v2rayN.desktop" "$APPDIR_X64/usr/share/applications/v2rayN.desktop"
ARCH=x86_64 ./appimagetool "$APPDIR_X64" "v2rayN-${OutputArch}.AppImage"
file "v2rayN-${OutputArch}.AppImage" | grep -q 'x86-64'
# aarch64 AppDir
APPDIR_ARM64="AppDir-aarch64"
rm -rf "$APPDIR_ARM64"
mkdir -p "$APPDIR_ARM64/usr/lib/v2rayN" "$APPDIR_ARM64/usr/bin" "$APPDIR_ARM64/usr/share/applications" "$APPDIR_ARM64/usr/share/pixmaps"
cp -rf "$OutputPathArm64"/* "$APPDIR_ARM64/usr/lib/v2rayN" || true
[ -f "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_ARM64/usr/share/pixmaps/v2rayN.png" || true
[ -f "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_ARM64/v2rayN.png" || true
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_ARM64/AppRun"
chmod +x "$APPDIR_ARM64/AppRun"
ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_ARM64/usr/bin/v2rayN"
cat > "$APPDIR_ARM64/v2rayN.desktop" <<EOF
[Desktop Entry]
Name=v2rayN
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
Exec=v2rayN
Icon=v2rayN
Terminal=false
Type=Application
Categories=Network;
EOF
install -Dm644 "$APPDIR_ARM64/v2rayN.desktop" "$APPDIR_ARM64/usr/share/applications/v2rayN.desktop"
# aarch64 runtime
wget -qO runtime-aarch64 https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-aarch64
chmod +x runtime-aarch64
# build aarch64 AppImage
ARCH=aarch64 ./appimagetool --runtime-file ./runtime-aarch64 "$APPDIR_ARM64" "v2rayN-${OutputArchArm}.AppImage"
file "v2rayN-${OutputArchArm}.AppImage" | grep -q 'ARM aarch64'

View File

@@ -28,6 +28,7 @@ Package: v2rayN
Version: $Version Version: $Version
Architecture: $Arch2 Architecture: $Arch2
Maintainer: https://github.com/2dust/v2rayN Maintainer: https://github.com/2dust/v2rayN
Depends: desktop-file-utils, xdg-utils
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
EOF EOF
@@ -52,7 +53,17 @@ sudo chmod 0755 "${PackagePath}/DEBIAN/postinst"
sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN" sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN"
sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool" sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool"
# desktop && PATH # Patch
# set owner to root:root
sudo chown -R root:root "${PackagePath}"
# set all directories to 755 (readable & traversable by all users)
sudo find "${PackagePath}/opt/v2rayN" -type d -exec chmod 755 {} +
# set all regular files to 644 (readable by all users)
sudo find "${PackagePath}/opt/v2rayN" -type f -exec chmod 644 {} +
# ensure main binaries are 755 (executable by all users)
sudo chmod 755 "${PackagePath}/opt/v2rayN/v2rayN" 2>/dev/null || true
sudo chmod 755 "${PackagePath}/opt/v2rayN/AmazTool" 2>/dev/null || true
# build deb package
sudo dpkg-deb -Zxz --build $PackagePath sudo dpkg-deb -Zxz --build $PackagePath
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb" sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ===== Require Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ==== # == Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ==
if [[ -r /etc/os-release ]]; then if [[ -r /etc/os-release ]]; then
. /etc/os-release . /etc/os-release
case "$ID" in case "$ID" in
rhel|rocky|almalinux|centos|ubuntu|debian) rhel|rocky|almalinux|fedora|centos|ubuntu|debian)
echo "[OK] Detected supported system: $NAME $VERSION_ID" echo "[OK] Detected supported system: $NAME $VERSION_ID"
;; ;;
*) *)
@@ -332,6 +332,7 @@ download_xray() {
# Download Xray core and install to outdir/xray # Download Xray core and install to outdir/xray
local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip" local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
mkdir -p "$outdir" mkdir -p "$outdir"
if [[ -n "${XRAY_VER:-}" ]]; then ver="${XRAY_VER}"; fi
if [[ -z "$ver" ]]; then if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \ 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 | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
@@ -353,6 +354,7 @@ download_singbox() {
# Download sing-box core and install to outdir/sing-box # Download sing-box core and install to outdir/sing-box
local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
mkdir -p "$outdir" mkdir -p "$outdir"
if [[ -n "${SING_VER:-}" ]]; then ver="${SING_VER}"; fi
if [[ -z "$ver" ]]; then if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \ 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 | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
@@ -372,25 +374,46 @@ download_singbox() {
install -Dm755 "$bin" "$outdir/sing-box" install -Dm755 "$bin" "$outdir/sing-box"
} }
# Move geo files to a unified path: outroot/bin/xray/ # ---- NEW: download_mihomo (REQUIRED in --netcore mode) ----
download_mihomo() {
# Download mihomo into outroot/bin/mihomo/mihomo
local outroot="$1"
local url=""
if [[ "$RID_DIR" == "linux-arm64" ]]; then
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64/bin/mihomo/mihomo"
else
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64/bin/mihomo/mihomo"
fi
echo "[+] Download mihomo: $url"
mkdir -p "$outroot/bin/mihomo"
curl -fL "$url" -o "$outroot/bin/mihomo/mihomo"
chmod +x "$outroot/bin/mihomo/mihomo" || true
}
# Move geo files to a unified path: outroot/bin
unify_geo_layout() { unify_geo_layout() {
local outroot="$1" local outroot="$1"
mkdir -p "$outroot/bin/xray" mkdir -p "$outroot/bin"
local srcs=( \ local names=( \
"$outroot/bin/geosite.dat" \ "geosite.dat" \
"$outroot/bin/geoip.dat" \ "geoip.dat" \
"$outroot/bin/geoip-only-cn-private.dat" \ "geoip-only-cn-private.dat" \
"$outroot/bin/Country.mmdb" \ "Country.mmdb" \
"$outroot/bin/geoip.metadb" \ "geoip.metadb" \
) )
for s in "${srcs[@]}"; do for n in "${names[@]}"; do
if [[ -f "$s" ]]; then # If file exists under bin/xray/, move it up to bin/
mv -f "$s" "$outroot/bin/xray/$(basename "$s")" if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
# If file already in bin/, leave it as-is
if [[ -f "$outroot/bin/$n" ]]; then
:
fi fi
done done
} }
# Download geo/rule assets; then unify to bin/xray/ # Download geo/rule assets; then unify to bin/
download_geo_assets() { download_geo_assets() {
local outroot="$1" local outroot="$1"
local bin_dir="$outroot/bin" local bin_dir="$outroot/bin"
@@ -424,7 +447,7 @@ download_geo_assets() {
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true "https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done done
# Unify to bin/xray/ # Unify to bin/
unify_geo_layout "$outroot" unify_geo_layout "$outroot"
} }
@@ -451,7 +474,8 @@ download_v2rayn_bundle() {
fi fi
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true # keep mihomo
# find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
local nested_dir local nested_dir
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)" nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
@@ -461,7 +485,7 @@ download_v2rayn_bundle() {
rm -rf "$nested_dir" rm -rf "$nested_dir"
fi fi
# Unify to bin/xray/ # Unify to bin/
unify_geo_layout "$outroot" unify_geo_layout "$outroot"
echo "[+] Bundle extracted to $outroot" echo "[+] Bundle extracted to $outroot"
@@ -561,6 +585,8 @@ build_for_arch() {
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)" download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)" download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
# ---- REQUIRED: always fetch mihomo in netcore mode, per-arch ----
download_mihomo "$WORKDIR/$PKGROOT" || echo "[!] mihomo download failed (skipped)"
fi fi
# Tarball # Tarball
@@ -583,18 +609,20 @@ Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64) Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64)
License: GPL-3.0-only License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues
ExclusiveArch: aarch64 x86_64 ExclusiveArch: aarch64 x86_64
Source0: __PKGROOT__.tar.gz Source0: __PKGROOT__.tar.gz
# Runtime dependencies (Avalonia / X11 / Fonts / GL) # Runtime dependencies (Avalonia / X11 / Fonts / GL)
Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon
Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL, xdg-utils
%description %description
v2rayN GUI client built with Avalonia. v2rayN Linux for Red Hat Enterprise Linux
Installs self-contained publish under /opt/v2rayN and a launcher 'v2rayn'. Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
Cores (if bundled): /opt/v2rayN/bin/xray, /opt/v2rayN/bin/sing_box. Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS
Geo files for Xray are placed at /opt/v2rayN/bin/xray; launcher will symlink them into user's XDG data dir on first run. For more information, Please visit our website
https://github.com/2dust/v2rayN
%prep %prep
%setup -q -n __PKGROOT__ %setup -q -n __PKGROOT__
@@ -606,25 +634,13 @@ Geo files for Xray are placed at /opt/v2rayN/bin/xray; launcher will symlink the
install -dm0755 %{buildroot}/opt/v2rayN install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/ cp -a * %{buildroot}/opt/v2rayN/
# Launcher (prefer native ELF first, then DLL fallback; also create Geo symlinks for the user) # Launcher (prefer native ELF first, then DLL fallback)
install -dm0755 %{buildroot}%{_bindir} install -dm0755 %{buildroot}%{_bindir}
cat > %{buildroot}%{_bindir}/v2rayn << 'EOF' cat > %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash #!/usr/bin/bash
set -euo pipefail set -euo pipefail
DIR="/opt/v2rayN" DIR="/opt/v2rayN"
# --- Symlink GEO files into user's XDG dir (first-run convenience) ---
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
USR_GEO_DIR="$XDG_DATA_HOME/v2rayN/bin"
SYS_XRAY_DIR="$DIR/bin/xray"
mkdir -p "$USR_GEO_DIR"
for f in geosite.dat geoip.dat geoip-only-cn-private.dat Country.mmdb; do
if [[ -f "$SYS_XRAY_DIR/$f" && ! -e "$USR_GEO_DIR/$f" ]]; then
ln -s "$SYS_XRAY_DIR/$f" "$USR_GEO_DIR/$f" || true
fi
done
# --- end GEO ---
# Prefer native apphost # Prefer native apphost
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi
@@ -645,7 +661,7 @@ cat > %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=v2rayN Name=v2rayN
Comment=GUI client for Xray / sing-box Comment=v2rayN for Red Hat Enterprise Linux
Exec=v2rayn Exec=v2rayn
Icon=v2rayn Icon=v2rayn
Terminal=false Terminal=false

View File

@@ -1,37 +0,0 @@
app: v2rayN
binpatch: true
ingredients:
script:
- export FileName="v2rayN-${AppImageOutputArch}.zip"
- wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/${FileName}"
- 7z x $FileName -aoa
- cp -rf v2rayN-${AppImageOutputArch}/* $OutputPath
script:
- mkdir -p usr/bin usr/lib
- cp -rf $OutputPath usr/lib/v2rayN
- echo "When this file exists, app will not store configs under this folder" > usr/lib/v2rayN/NotStoreConfigHere.txt
- ln -sf usr/lib/v2rayN/v2rayN usr/bin/v2rayN
- chmod a+x usr/lib/v2rayN/v2rayN
- find usr -type f -exec sh -c 'file "{}" | grep -qi "executable" && chmod +x "{}"' \;
- install -Dm644 usr/lib/v2rayN/v2rayN.png v2rayN.png
- install -Dm644 usr/lib/v2rayN/v2rayN.png usr/share/pixmaps/v2rayN.png
- cat > v2rayN.desktop <<EOF
- [Desktop Entry]
- Name=v2rayN
- Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
- Exec=v2rayN
- Icon=v2rayN
- Terminal=false
- Type=Application
- Categories=Network;
- EOF
- install -Dm644 v2rayN.desktop usr/share/applications/v2rayN.desktop
- cat > AppRun <<\EOF
- #!/bin/sh
- HERE="$(dirname "$(readlink -f "${0}")")"
- cd ${HERE}/usr/lib/v2rayN
- exec ${HERE}/usr/lib/v2rayN/v2rayN $@
- EOF
- chmod a+x AppRun

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.14.2</Version> <Version>7.15.0</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -5,10 +5,11 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.4" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.4" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.4" /> <PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.4" /> <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.6" />
<PackageVersion Include="CliWrap" Version="3.9.0" /> <PackageVersion Include="CliWrap" Version="3.9.0" />
<PackageVersion Include="Downloader" Version="4.0.3" /> <PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
@@ -18,9 +19,10 @@
<PackageVersion Include="ReactiveUI" Version="20.4.1" /> <PackageVersion Include="ReactiveUI" Version="20.4.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" /> <PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.9" /> <PackageVersion Include="Semi.Avalonia" Version="11.2.1.10" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.9" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Splat.NLog" Version="15.5.3" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.10" />
<PackageVersion Include="NLog" Version="6.0.4" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" /> <PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" /> <PackageVersion Include="WebDav.Client" Version="2.9.0" />

View File

@@ -9,6 +9,31 @@ public class JsonUtils
{ {
private static readonly string _tag = "JsonUtils"; private static readonly string _tag = "JsonUtils";
private static readonly JsonSerializerOptions _defaultDeserializeOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
private static readonly JsonSerializerOptions _defaultSerializeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonSerializerOptions _nullValueSerializeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonDocumentOptions _defaultDocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip
};
/// <summary> /// <summary>
/// DeepCopy /// DeepCopy
/// </summary> /// </summary>
@@ -34,11 +59,7 @@ public class JsonUtils
{ {
return default; return default;
} }
var options = new JsonSerializerOptions return JsonSerializer.Deserialize<T>(strJson, _defaultDeserializeOptions);
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<T>(strJson, options);
} }
catch catch
{ {
@@ -59,7 +80,7 @@ public class JsonUtils
{ {
return null; return null;
} }
return JsonNode.Parse(strJson); return JsonNode.Parse(strJson, nodeOptions: null, _defaultDocumentOptions);
} }
catch catch
{ {
@@ -84,12 +105,7 @@ public class JsonUtils
{ {
return result; return result;
} }
var options = new JsonSerializerOptions var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions;
{
WriteIndented = indented,
DefaultIgnoreCondition = nullValue ? JsonIgnoreCondition.Never : JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
result = JsonSerializer.Serialize(obj, options); result = JsonSerializer.Serialize(obj, options);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -1,4 +1,5 @@
using QRCoder; using QRCoder;
using QRCoder.Exceptions;
using SkiaSharp; using SkiaSharp;
using ZXing.SkiaSharp; using ZXing.SkiaSharp;
@@ -8,11 +9,46 @@ public class QRCodeUtils
{ {
public static byte[]? GenQRCode(string? url) public static byte[]? GenQRCode(string? url)
{ {
if (url.IsNullOrEmpty())
{
return null;
}
using QRCodeGenerator qrGenerator = new(); using QRCodeGenerator qrGenerator = new();
using var qrCodeData = qrGenerator.CreateQrCode(url ?? string.Empty, QRCodeGenerator.ECCLevel.Q); DataTooLongException? lastDtle = null;
var levels = new[]
{
QRCodeGenerator.ECCLevel.H,
QRCodeGenerator.ECCLevel.Q,
QRCodeGenerator.ECCLevel.M,
QRCodeGenerator.ECCLevel.L
};
foreach (var level in levels)
{
try
{
using var qrCodeData = qrGenerator.CreateQrCode(url, level);
using PngByteQRCode qrCode = new(qrCodeData); using PngByteQRCode qrCode = new(qrCodeData);
return qrCode.GetGraphic(20); return qrCode.GetGraphic(20);
} }
catch (DataTooLongException ex)
{
lastDtle = ex;
continue;
}
catch
{
throw;
}
}
if (lastDtle != null)
{
throw lastDtle;
}
return null;
}
public static string? ParseBarcode(string? fileName) public static string? ParseBarcode(string? fileName)
{ {

View File

@@ -331,6 +331,32 @@ public class Utils
.ToList(); .ToList();
} }
public static Dictionary<string, List<string>> ParseHostsToDictionary(string hostsContent)
{
var userHostsMap = hostsContent
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
// skip full-line comments
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
// strip inline comments (truncate at '#')
.Select(line =>
{
var index = line.IndexOf('#');
return index >= 0 ? line.Substring(0, index).Trim() : line;
})
// ensure line still contains valid parts
.Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' '))
.Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
.Where(parts => parts.Length >= 2)
.GroupBy(parts => parts[0])
.ToDictionary(
group => group.Key,
group => group.SelectMany(parts => parts.Skip(1)).ToList()
);
return userHostsMap;
}
#endregion #endregion
#region #region
@@ -582,9 +608,9 @@ public class Utils
if (host.StartsWith("#")) if (host.StartsWith("#"))
continue; continue;
var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (hostItem.Length != 2) if (hostItem.Length < 2)
continue; continue;
systemHosts.Add(hostItem.Last(), hostItem.First()); systemHosts.Add(hostItem[1], hostItem[0]);
} }
} }
} }
@@ -857,6 +883,55 @@ public class Utils
return false; return false;
} }
public static bool IsPackagedInstall()
{
try
{
if (IsWindows() || IsOSX())
{
return false;
}
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPIMAGE")))
{
return true;
}
var exePath = GetExePath();
var baseDir = string.IsNullOrEmpty(exePath) ? StartupPath() : Path.GetDirectoryName(exePath) ?? "";
var p = baseDir.Replace('\\', '/');
if (string.IsNullOrEmpty(p))
{
return false;
}
if (p.Contains("/.mount_", StringComparison.Ordinal))
{
return true;
}
if (p.StartsWith("/opt/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (p.StartsWith("/usr/lib/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (p.StartsWith("/usr/share/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
catch
{
}
return false;
}
private static async Task<string?> GetLinuxUserId() private static async Task<string?> GetLinuxUserId()
{ {
var arg = new List<string>() { "-c", "id -u" }; var arg = new List<string>() { "-c", "id -u" };

View File

@@ -15,5 +15,6 @@ public enum ECoreType
brook = 27, brook = 27,
overtls = 28, overtls = 28,
shadowquic = 29, shadowquic = 29,
mieru = 30,
v2rayN = 99 v2rayN = 99
} }

View File

@@ -1,10 +0,0 @@
namespace ServiceLib.Enums;
public enum EMsgCommand
{
ClearMsg,
SendMsgView,
SendSnackMsg,
RefreshProfiles,
AppExit
}

View File

@@ -6,17 +6,14 @@ public enum EViewAction
ShowYesNo, ShowYesNo,
SaveFileDialog, SaveFileDialog,
AddBatchRoutingRulesYesNo, AddBatchRoutingRulesYesNo,
AdjustMainLvColWidth,
SetClipboardData, SetClipboardData,
AddServerViaClipboard, AddServerViaClipboard,
ImportRulesFromClipboard, ImportRulesFromClipboard,
ProfilesFocus, ProfilesFocus,
ShareSub, ShareSub,
ShareServer, ShareServer,
ShowHideWindow,
ScanScreenTask, ScanScreenTask,
ScanImageTask, ScanImageTask,
Shutdown,
BrowseServer, BrowseServer,
ImportRulesFromFile, ImportRulesFromFile,
InitSettingFont, InitSettingFont,
@@ -32,16 +29,7 @@ public enum EViewAction
FullConfigTemplateWindow, FullConfigTemplateWindow,
GlobalHotkeySettingWindow, GlobalHotkeySettingWindow,
SubSettingWindow, SubSettingWindow,
DispatcherSpeedTest,
DispatcherRefreshConnections,
DispatcherRefreshProxyGroups,
DispatcherProxiesDelayTest,
DispatcherStatistics,
DispatcherServerAvailability,
DispatcherReload,
DispatcherRefreshServersBiz, DispatcherRefreshServersBiz,
DispatcherRefreshIcon, DispatcherRefreshIcon,
DispatcherCheckUpdate,
DispatcherCheckUpdateFinished,
DispatcherShowMsg, DispatcherShowMsg,
} }

View File

@@ -0,0 +1,32 @@
using System.Reactive;
namespace ServiceLib.Events;
public static class AppEvents
{
public static readonly EventChannel<Unit> ReloadRequested = new();
public static readonly EventChannel<bool?> ShowHideWindowRequested = new();
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<Unit> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
public static readonly EventChannel<Unit> ProxiesReloadRequested = new();
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
public static readonly EventChannel<string> SendSnackMsgRequested = new();
public static readonly EventChannel<string> SendMsgViewRequested = new();
public static readonly EventChannel<Unit> AppExitRequested = new();
public static readonly EventChannel<bool> ShutdownRequested = new();
public static readonly EventChannel<Unit> AdjustMainLvColWidthRequested = new();
public static readonly EventChannel<string> SetDefaultServerRequested = new();
public static readonly EventChannel<Unit> RoutingsMenuRefreshRequested = new();
public static readonly EventChannel<Unit> TestServerRequested = new();
public static readonly EventChannel<Unit> InboundDisplayRequested = new();
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
}

View File

@@ -0,0 +1,29 @@
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace ServiceLib.Events;
public sealed class EventChannel<T>
{
private readonly ISubject<T> _subject = Subject.Synchronize(new Subject<T>());
public IObservable<T> AsObservable()
{
return _subject.AsObservable();
}
public void Publish(T value)
{
_subject.OnNext(value);
}
public void Publish()
{
if (typeof(T) != typeof(Unit))
{
throw new InvalidOperationException("Publish() without value is only valid for EventChannel<Unit>.");
}
_subject.OnNext((T)(object)Unit.Default);
}
}

View File

@@ -40,6 +40,7 @@ public class Global
public const string ProxySetLinuxShellFileName = NamespaceSample + "proxy_set_linux_sh"; public const string ProxySetLinuxShellFileName = NamespaceSample + "proxy_set_linux_sh";
public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh"; public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh";
public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh"; public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
public const string DefaultSecurity = "auto"; public const string DefaultSecurity = "auto";
public const string DefaultNetwork = "tcp"; public const string DefaultNetwork = "tcp";
@@ -448,6 +449,14 @@ public class Global
"none" "none"
]; ];
public static readonly Dictionary<string, string> LogLevelColors = new()
{
{ "debug", "#6C757D" },
{ "info", "#2ECC71" },
{ "warning", "#FFA500" },
{ "error", "#E74C3C" },
};
public static readonly List<string> InboundTags = public static readonly List<string> InboundTags =
[ [
"socks", "socks",
@@ -560,6 +569,7 @@ public class Global
{ ECoreType.brook, "txthinking/brook" }, { ECoreType.brook, "txthinking/brook" },
{ ECoreType.overtls, "ShadowsocksR-Live/overtls" }, { ECoreType.overtls, "ShadowsocksR-Live/overtls" },
{ ECoreType.shadowquic, "spongebob888/shadowquic" }, { ECoreType.shadowquic, "spongebob888/shadowquic" },
{ ECoreType.mieru, "enfein/mieru" },
{ ECoreType.v2rayN, "2dust/v2rayN" }, { ECoreType.v2rayN, "2dust/v2rayN" },
}; };
@@ -596,6 +606,7 @@ public class Global
{ "cloudflare-dns.com", new List<string> { "104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9" } }, { "cloudflare-dns.com", new List<string> { "104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9" } },
{ "dns.cloudflare.com", new List<string> { "104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5" } }, { "dns.cloudflare.com", new List<string> { "104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5" } },
{ "dot.pub", new List<string> { "1.12.12.12", "120.53.53.53" } }, { "dot.pub", new List<string> { "1.12.12.12", "120.53.53.53" } },
{ "doh.pub", new List<string> { "1.12.12.12", "120.53.53.53" } },
{ "dns.quad9.net", new List<string> { "9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9" } }, { "dns.quad9.net", new List<string> { "9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9" } },
{ "dns.yandex.net", new List<string> { "77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff" } }, { "dns.yandex.net", new List<string> { "77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff" } },
{ "dns.sb", new List<string> { "185.222.222.222", "2a09::" } }, { "dns.sb", new List<string> { "185.222.222.222", "2a09::" } },

View File

@@ -1,6 +1,7 @@
global using ServiceLib.Base; global using ServiceLib.Base;
global using ServiceLib.Common; global using ServiceLib.Common;
global using ServiceLib.Enums; global using ServiceLib.Enums;
global using ServiceLib.Events;
global using ServiceLib.Handler; global using ServiceLib.Handler;
global using ServiceLib.Helper; global using ServiceLib.Helper;
global using ServiceLib.Manager; global using ServiceLib.Manager;

View File

@@ -113,6 +113,10 @@ public static class ConfigHandler
config.ConstItem ??= new ConstItem(); config.ConstItem ??= new ConstItem();
config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
if (config.SimpleDNSItem.GlobalFakeIp is null)
{
config.SimpleDNSItem.GlobalFakeIp = true;
}
config.SpeedTestItem ??= new(); config.SpeedTestItem ??= new();
if (config.SpeedTestItem.SpeedTestTimeout < 10) if (config.SpeedTestItem.SpeedTestTimeout < 10)
@@ -1210,11 +1214,11 @@ public static class ConfigHandler
CoreType = ECoreType.sing_box, CoreType = ECoreType.sing_box,
ConfigType = EConfigType.SOCKS, ConfigType = EConfigType.SOCKS,
Address = Global.Loopback, Address = Global.Loopback,
Sni = node.Address, //Tun2SocksAddress SpiderX = node.Address, // Tun2SocksAddress
Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks) Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks)
}; };
} }
else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)) else if (node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)
{ {
var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
itemSocks = new ProfileItem() itemSocks = new ProfileItem()
@@ -2221,6 +2225,7 @@ public static class ConfigHandler
UseSystemHosts = false, UseSystemHosts = false,
AddCommonHosts = true, AddCommonHosts = true,
FakeIP = false, FakeIP = false,
GlobalFakeIp = true,
BlockBindingQuery = true, BlockBindingQuery = true,
DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(),
RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(), RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(),
@@ -2321,10 +2326,22 @@ public static class ConfigHandler
config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[1]; config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[1];
config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[1]; config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[1];
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[1] + "v2ray.json")); var xrayDnsRussia = await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[1] + "v2ray.json");
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[1] + "sing_box.json")); var singboxDnsRussia = await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[1] + "sing_box.json");
var simpleDnsRussia = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[1] + "simple_dns.json");
config.SimpleDNSItem = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[1] + "simple_dns.json") ?? InitBuiltinSimpleDNS(); if (simpleDnsRussia == null)
{
xrayDnsRussia.Enabled = true;
singboxDnsRussia.Enabled = true;
config.SimpleDNSItem = InitBuiltinSimpleDNS();
}
else
{
config.SimpleDNSItem = simpleDnsRussia;
}
await SaveDNSItems(config, xrayDnsRussia);
await SaveDNSItems(config, singboxDnsRussia);
break; break;
case EPresetType.Iran: case EPresetType.Iran:
@@ -2332,10 +2349,22 @@ public static class ConfigHandler
config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[2]; config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[2];
config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[2]; config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[2];
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[2] + "v2ray.json")); var xrayDnsIran = await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[2] + "v2ray.json");
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[2] + "sing_box.json")); var singboxDnsIran = await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[2] + "sing_box.json");
var simpleDnsIran = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[2] + "simple_dns.json");
config.SimpleDNSItem = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[2] + "simple_dns.json") ?? InitBuiltinSimpleDNS(); if (simpleDnsIran == null)
{
xrayDnsIran.Enabled = true;
singboxDnsIran.Enabled = true;
config.SimpleDNSItem = InitBuiltinSimpleDNS();
}
else
{
config.SimpleDNSItem = simpleDnsIran;
}
await SaveDNSItems(config, xrayDnsIran);
await SaveDNSItems(config, singboxDnsIran);
break; break;
} }

View File

@@ -4,7 +4,7 @@ public class ClashFmt : BaseFmt
{ {
public static ProfileItem? ResolveFull(string strData, string? subRemarks) public static ProfileItem? ResolveFull(string strData, string? subRemarks)
{ {
if (Contains(strData, "port", "socks-port", "proxies")) if (Contains(strData, "external-controller", "-port", "proxies"))
{ {
var fileName = WriteAllText(strData, "yaml"); var fileName = WriteAllText(strData, "yaml");

View File

@@ -1,5 +1,3 @@
using SkiaSharp;
namespace ServiceLib.Handler.Fmt; namespace ServiceLib.Handler.Fmt;
public class HtmlPageFmt : BaseFmt public class HtmlPageFmt : BaseFmt

View File

@@ -2,17 +2,18 @@ namespace ServiceLib.Handler;
public static class SubscriptionHandler public static class SubscriptionHandler
{ {
public static async Task UpdateProcess(Config config, string subId, bool blProxy, Action<bool, string> updateFunc) public static async Task UpdateProcess(Config config, string subId, bool blProxy, Func<bool, string, Task> updateFunc)
{ {
updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart); await updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart);
var subItem = await AppManager.Instance.SubItems(); var subItem = await AppManager.Instance.SubItems();
if (subItem is not { Count: > 0 }) if (subItem is not { Count: > 0 })
{ {
updateFunc?.Invoke(false, ResUI.MsgNoValidSubscription); await updateFunc?.Invoke(false, ResUI.MsgNoValidSubscription);
return; return;
} }
var successCount = 0;
foreach (var item in subItem) foreach (var item in subItem)
{ {
try try
@@ -25,32 +26,35 @@ public static class SubscriptionHandler
var hashCode = $"{item.Remarks}->"; var hashCode = $"{item.Remarks}->";
if (item.Enabled == false) if (item.Enabled == false)
{ {
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}"); await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}");
continue; continue;
} }
// Create download handler // Create download handler
var downloadHandle = CreateDownloadHandler(hashCode, updateFunc); var downloadHandle = CreateDownloadHandler(hashCode, updateFunc);
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}"); await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
// Get all subscription content (main subscription + additional subscriptions) // Get all subscription content (main subscription + additional subscriptions)
var result = await DownloadAllSubscriptions(config, item, blProxy, downloadHandle); var result = await DownloadAllSubscriptions(config, item, blProxy, downloadHandle);
// Process download result // Process download result
await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc); if (await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc))
{
successCount++;
}
updateFunc?.Invoke(false, "-------------------------------------------------------"); await updateFunc?.Invoke(false, "-------------------------------------------------------");
} }
catch (Exception ex) catch (Exception ex)
{ {
var hashCode = $"{item.Remarks}->"; var hashCode = $"{item.Remarks}->";
Logging.SaveLog("UpdateSubscription", ex); Logging.SaveLog("UpdateSubscription", ex);
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgFailedImportSubscription}: {ex.Message}"); await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgFailedImportSubscription}: {ex.Message}");
updateFunc?.Invoke(false, "-------------------------------------------------------"); await updateFunc?.Invoke(false, "-------------------------------------------------------");
} }
} }
updateFunc?.Invoke(true, $"{ResUI.MsgUpdateSubscriptionEnd}"); await updateFunc?.Invoke(successCount > 0, $"{ResUI.MsgUpdateSubscriptionEnd}");
} }
private static bool IsValidSubscription(SubItem item, string subId) private static bool IsValidSubscription(SubItem item, string subId)
@@ -76,7 +80,7 @@ public static class SubscriptionHandler
return true; return true;
} }
private static DownloadService CreateDownloadHandler(string hashCode, Action<bool, string> updateFunc) private static DownloadService CreateDownloadHandler(string hashCode, Func<bool, string, Task> updateFunc)
{ {
var downloadHandle = new DownloadService(); var downloadHandle = new DownloadService();
downloadHandle.Error += (sender2, args) => downloadHandle.Error += (sender2, args) =>
@@ -181,22 +185,24 @@ public static class SubscriptionHandler
return result; return result;
} }
private static async Task ProcessDownloadResult(Config config, string id, string result, string hashCode, Action<bool, string> updateFunc) private static async Task<bool> ProcessDownloadResult(Config config, string id, string result, string hashCode, Func<bool, string, Task> updateFunc)
{ {
if (result.IsNullOrEmpty()) if (result.IsNullOrEmpty())
{ {
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecodingFailed}"); await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecodingFailed}");
return; return false;
} }
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgGetSubscriptionSuccessfully}"); await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgGetSubscriptionSuccessfully}");
// If result is too short, display content directly // If result is too short, display content directly
if (result.Length < 99) if (result.Length < 99)
{ {
updateFunc?.Invoke(false, $"{hashCode}{result}"); await updateFunc?.Invoke(false, $"{hashCode}{result}");
} }
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartParsingSubscription}");
// Add servers to configuration // Add servers to configuration
var ret = await ConfigHandler.AddBatchServers(config, result, id, true); var ret = await ConfigHandler.AddBatchServers(config, result, id, true);
if (ret <= 0) if (ret <= 0)
@@ -206,9 +212,10 @@ public static class SubscriptionHandler
} }
// Update completion message // Update completion message
updateFunc?.Invoke(false, await updateFunc?.Invoke(false, ret > 0
ret > 0
? $"{hashCode}{ResUI.MsgUpdateSubscriptionEnd}" ? $"{hashCode}{ResUI.MsgUpdateSubscriptionEnd}"
: $"{hashCode}{ResUI.MsgFailedImportSubscription}"); : $"{hashCode}{ResUI.MsgFailedImportSubscription}");
return ret > 0;
} }
} }

View File

@@ -26,7 +26,7 @@ public sealed class SQLiteHelper
public async Task<int> InsertAllAsync(IEnumerable models) public async Task<int> InsertAllAsync(IEnumerable models)
{ {
return await _dbAsync.InsertAllAsync(models); return await _dbAsync.InsertAllAsync(models, runInTransaction: true).ConfigureAwait(false);
} }
public async Task<int> InsertAsync(object model) public async Task<int> InsertAsync(object model)
@@ -46,7 +46,7 @@ public sealed class SQLiteHelper
public async Task<int> UpdateAllAsync(IEnumerable models) public async Task<int> UpdateAllAsync(IEnumerable models)
{ {
return await _dbAsync.UpdateAllAsync(models); return await _dbAsync.UpdateAllAsync(models, runInTransaction: true).ConfigureAwait(false);
} }
public async Task<int> DeleteAsync(object model) public async Task<int> DeleteAsync(object model)

View File

@@ -34,7 +34,7 @@ public sealed class AppManager
#endregion Property #endregion Property
#region Init #region App
public bool InitApp() public bool InitApp()
{ {
@@ -87,7 +87,46 @@ public sealed class AppManager
return true; return true;
} }
#endregion Init public async Task AppExitAsync(bool needShutdown)
{
try
{
Logging.SaveLog("AppExitAsync Begin");
await SysProxyHandler.UpdateSysProxy(_config, true);
AppEvents.AppExitRequested.Publish();
await Task.Delay(50); //Wait for AppExitRequested to be processed
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
await StatisticsManager.Instance.SaveTo();
await CoreManager.Instance.CoreStop();
StatisticsManager.Instance.Close();
Logging.SaveLog("AppExitAsync End");
}
catch { }
finally
{
if (needShutdown)
{
Shutdown(false);
}
}
}
public void Shutdown(bool byUser)
{
AppEvents.ShutdownRequested.Publish(byUser);
}
public async Task RebootAsAdmin()
{
ProcUtils.RebootAsAdmin();
await AppManager.Instance.AppExitAsync(true);
}
#endregion App
#region Config #region Config

View File

@@ -35,7 +35,7 @@ public sealed class ClashApiManager
return null; return null;
} }
public void ClashProxiesDelayTest(bool blAll, List<ClashProxyModel> lstProxy, Action<ClashProxyModel?, string> updateFunc) public void ClashProxiesDelayTest(bool blAll, List<ClashProxyModel> lstProxy, Func<ClashProxyModel?, string, Task> updateFunc)
{ {
Task.Run(async () => Task.Run(async () =>
{ {
@@ -79,12 +79,12 @@ public sealed class ClashApiManager
tasks.Add(Task.Run(async () => tasks.Add(Task.Run(async () =>
{ {
var result = await HttpClientHelper.Instance.TryGetAsync(url); var result = await HttpClientHelper.Instance.TryGetAsync(url);
updateFunc?.Invoke(it, result); await updateFunc?.Invoke(it, result);
})); }));
} }
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
await Task.Delay(1000); await Task.Delay(1000);
updateFunc?.Invoke(null, ""); await updateFunc?.Invoke(null, "");
}); });
} }

View File

@@ -10,11 +10,11 @@ public class CoreAdminManager
private static readonly Lazy<CoreAdminManager> _instance = new(() => new()); private static readonly Lazy<CoreAdminManager> _instance = new(() => new());
public static CoreAdminManager Instance => _instance.Value; public static CoreAdminManager Instance => _instance.Value;
private Config _config; private Config _config;
private Action<bool, string>? _updateFunc; private Func<bool, string, Task>? _updateFunc;
private int _linuxSudoPid = -1; private int _linuxSudoPid = -1;
private const string _tag = "CoreAdminHandler"; private const string _tag = "CoreAdminHandler";
public async Task Init(Config config, Action<bool, string> updateFunc) public async Task Init(Config config, Func<bool, string, Task> updateFunc)
{ {
if (_config != null) if (_config != null)
{ {
@@ -26,9 +26,9 @@ public class CoreAdminManager
await Task.CompletedTask; await Task.CompletedTask;
} }
private void UpdateFunc(bool notify, string msg) private async Task UpdateFunc(bool notify, string msg)
{ {
_updateFunc?.Invoke(notify, msg); await _updateFunc?.Invoke(notify, msg);
} }
public async Task<Process?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath) public async Task<Process?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath)
@@ -60,7 +60,7 @@ public class CoreAdminManager
{ {
if (e.Data.IsNotEmpty()) if (e.Data.IsNotEmpty())
{ {
UpdateFunc(false, e.Data + Environment.NewLine); _ = UpdateFunc(false, e.Data + Environment.NewLine);
} }
} }
@@ -106,7 +106,7 @@ public class CoreAdminManager
.WithStandardInputPipe(PipeSource.FromString(AppManager.Instance.LinuxSudoPwd)) .WithStandardInputPipe(PipeSource.FromString(AppManager.Instance.LinuxSudoPwd))
.ExecuteBufferedAsync(); .ExecuteBufferedAsync();
UpdateFunc(false, result.StandardOutput.ToString()); await UpdateFunc(false, result.StandardOutput.ToString());
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -80,6 +80,10 @@ public sealed class CoreInfoManager
Url = GetCoreUrl(ECoreType.v2fly), Url = GetCoreUrl(ECoreType.v2fly),
Match = "V2Ray", Match = "V2Ray",
VersionArg = "-version", VersionArg = "-version",
Environment = new Dictionary<string, string?>()
{
{ Global.V2RayLocalAsset, Utils.GetBinPath("") },
},
}, },
new CoreInfo new CoreInfo
@@ -90,6 +94,10 @@ public sealed class CoreInfoManager
Url = GetCoreUrl(ECoreType.v2fly_v5), Url = GetCoreUrl(ECoreType.v2fly_v5),
Match = "V2Ray", Match = "V2Ray",
VersionArg = "version", VersionArg = "version",
Environment = new Dictionary<string, string?>()
{
{ Global.V2RayLocalAsset, Utils.GetBinPath("") },
},
}, },
new CoreInfo new CoreInfo
@@ -107,20 +115,25 @@ public sealed class CoreInfoManager
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip", DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
Match = "Xray", Match = "Xray",
VersionArg = "-version", VersionArg = "-version",
Environment = new Dictionary<string, string?>()
{
{ Global.XrayLocalAsset, Utils.GetBinPath("") },
{ Global.XrayLocalCert, Utils.GetBinPath("") },
},
}, },
new CoreInfo new CoreInfo
{ {
CoreType = ECoreType.mihomo, CoreType = ECoreType.mihomo,
CoreExes = ["mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "clash", "mihomo"], CoreExes = ["mihomo-windows-amd64-v1", "mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "clash", "mihomo"],
Arguments = "-f {0}" + PortableMode(), Arguments = "-f {0}" + PortableMode(),
Url = GetCoreUrl(ECoreType.mihomo), Url = GetCoreUrl(ECoreType.mihomo),
ReleaseApiUrl = urlMihomo.Replace(Global.GithubUrl, Global.GithubApiUrl), ReleaseApiUrl = urlMihomo.Replace(Global.GithubUrl, Global.GithubApiUrl),
DownloadUrlWin64 = urlMihomo + "/download/{0}/mihomo-windows-amd64-compatible-{0}.zip", DownloadUrlWin64 = urlMihomo + "/download/{0}/mihomo-windows-amd64-v1-{0}.zip",
DownloadUrlWinArm64 = urlMihomo + "/download/{0}/mihomo-windows-arm64-{0}.zip", DownloadUrlWinArm64 = urlMihomo + "/download/{0}/mihomo-windows-arm64-{0}.zip",
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-compatible-{0}.gz", DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz", DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-compatible-{0}.gz", DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz", DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
Match = "Mihomo", Match = "Mihomo",
VersionArg = "-v", VersionArg = "-v",
@@ -205,12 +218,24 @@ public sealed class CoreInfoManager
new CoreInfo new CoreInfo
{ {
CoreType = ECoreType.shadowquic, CoreType = ECoreType.shadowquic,
CoreExes = [ "shadowquic", "shadowquic"], CoreExes = [ "shadowquic" ],
Arguments = "-c {0}", Arguments = "-c {0}",
Url = GetCoreUrl(ECoreType.shadowquic), Url = GetCoreUrl(ECoreType.shadowquic),
AbsolutePath = false, AbsolutePath = false,
} },
new CoreInfo
{
CoreType = ECoreType.mieru,
CoreExes = [ "mieru" ],
Arguments = "run",
Url = GetCoreUrl(ECoreType.mieru),
AbsolutePath = false,
Environment = new Dictionary<string, string?>()
{
{ "MIERU_CONFIG_JSON_FILE", "{0}" },
},
},
]; ];
} }

View File

@@ -14,18 +14,14 @@ public class CoreManager
private Process? _process; private Process? _process;
private Process? _processPre; private Process? _processPre;
private bool _linuxSudo = false; private bool _linuxSudo = false;
private Action<bool, string>? _updateFunc; private Func<bool, string, Task>? _updateFunc;
private const string _tag = "CoreHandler"; private const string _tag = "CoreHandler";
public async Task Init(Config config, Action<bool, string> updateFunc) public async Task Init(Config config, Func<bool, string, Task> updateFunc)
{ {
_config = config; _config = config;
_updateFunc = updateFunc; _updateFunc = updateFunc;
Environment.SetEnvironmentVariable(Global.V2RayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable(Global.XrayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable(Global.XrayLocalCert, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
//Copy the bin folder to the storage location (for init) //Copy the bin folder to the storage location (for init)
if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1") if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1")
{ {
@@ -67,7 +63,7 @@ public class CoreManager
{ {
if (node == null) if (node == null)
{ {
UpdateFunc(false, ResUI.CheckServerSettings); await UpdateFunc(false, ResUI.CheckServerSettings);
return; return;
} }
@@ -75,13 +71,13 @@ public class CoreManager
var result = await CoreConfigHandler.GenerateClientConfig(node, fileName); var result = await CoreConfigHandler.GenerateClientConfig(node, fileName);
if (result.Success != true) if (result.Success != true)
{ {
UpdateFunc(true, result.Msg); await UpdateFunc(true, result.Msg);
return; return;
} }
UpdateFunc(false, $"{node.GetSummary()}"); await UpdateFunc(false, $"{node.GetSummary()}");
UpdateFunc(false, $"{Utils.GetRuntimeInfo()}"); await UpdateFunc(false, $"{Utils.GetRuntimeInfo()}");
UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
await CoreStop(); await CoreStop();
await Task.Delay(100); await Task.Delay(100);
@@ -95,7 +91,7 @@ public class CoreManager
await CoreStartPreService(node); await CoreStartPreService(node);
if (_process != null) if (_process != null)
{ {
UpdateFunc(true, $"{node.GetSummary()}"); await UpdateFunc(true, $"{node.GetSummary()}");
} }
} }
@@ -105,14 +101,14 @@ public class CoreManager
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
var configPath = Utils.GetBinConfigPath(fileName); var configPath = Utils.GetBinConfigPath(fileName);
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType);
UpdateFunc(false, result.Msg); await UpdateFunc(false, result.Msg);
if (result.Success != true) if (result.Success != true)
{ {
return -1; return -1;
} }
UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
UpdateFunc(false, configPath); await UpdateFunc(false, configPath);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, fileName, true, false); var proc = await RunProcess(coreInfo, fileName, true, false);
@@ -220,9 +216,9 @@ public class CoreManager
} }
} }
private void UpdateFunc(bool notify, string msg) private async Task UpdateFunc(bool notify, string msg)
{ {
_updateFunc?.Invoke(notify, msg); await _updateFunc?.Invoke(notify, msg);
} }
#endregion Private #endregion Private
@@ -234,7 +230,7 @@ public class CoreManager
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg); var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
{ {
UpdateFunc(false, msg); await UpdateFunc(false, msg);
return null; return null;
} }
@@ -255,7 +251,7 @@ public class CoreManager
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
UpdateFunc(mayNeedSudo, ex.Message); await UpdateFunc(mayNeedSudo, ex.Message);
return null; return null;
} }
} }
@@ -277,6 +273,10 @@ public class CoreManager
StandardErrorEncoding = displayLog ? Encoding.UTF8 : null, StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
} }
}; };
foreach (var kv in coreInfo.Environment)
{
proc.StartInfo.Environment[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath);
}
if (displayLog) if (displayLog)
{ {
@@ -284,7 +284,7 @@ public class CoreManager
{ {
if (e.Data.IsNotEmpty()) if (e.Data.IsNotEmpty())
{ {
UpdateFunc(false, e.Data + Environment.NewLine); _ = UpdateFunc(false, e.Data + Environment.NewLine);
} }
} }
proc.OutputDataReceived += dataHandler; proc.OutputDataReceived += dataHandler;

View File

@@ -1,5 +1,3 @@
using ReactiveUI;
namespace ServiceLib.Manager; namespace ServiceLib.Manager;
public class NoticeManager public class NoticeManager
@@ -13,7 +11,7 @@ public class NoticeManager
{ {
return; return;
} }
MessageBus.Current.SendMessage(content, EMsgCommand.SendSnackMsg.ToString()); AppEvents.SendSnackMsgRequested.Publish(content);
} }
public void SendMessage(string? content) public void SendMessage(string? content)
@@ -22,7 +20,7 @@ public class NoticeManager
{ {
return; return;
} }
MessageBus.Current.SendMessage(content, EMsgCommand.SendMsgView.ToString()); AppEvents.SendMsgViewRequested.Publish(content);
} }
public void SendMessageEx(string? content) public void SendMessageEx(string? content)

View File

@@ -8,14 +8,14 @@ public class StatisticsManager
private Config _config; private Config _config;
private ServerStatItem? _serverStatItem; private ServerStatItem? _serverStatItem;
private List<ServerStatItem> _lstServerStat; private List<ServerStatItem> _lstServerStat;
private Action<ServerSpeedItem>? _updateFunc; private Func<ServerSpeedItem, Task>? _updateFunc;
private StatisticsXrayService? _statisticsXray; private StatisticsXrayService? _statisticsXray;
private StatisticsSingboxService? _statisticsSingbox; private StatisticsSingboxService? _statisticsSingbox;
private static readonly string _tag = "StatisticsHandler"; private static readonly string _tag = "StatisticsHandler";
public List<ServerStatItem> ServerStat => _lstServerStat; public List<ServerStatItem> ServerStat => _lstServerStat;
public async Task Init(Config config, Action<ServerSpeedItem> updateFunc) public async Task Init(Config config, Func<ServerSpeedItem, Task> updateFunc)
{ {
_config = config; _config = config;
_updateFunc = updateFunc; _updateFunc = updateFunc;
@@ -97,9 +97,9 @@ public class StatisticsManager
_lstServerStat = await SQLiteHelper.Instance.TableAsync<ServerStatItem>().ToListAsync(); _lstServerStat = await SQLiteHelper.Instance.TableAsync<ServerStatItem>().ToListAsync();
} }
private void UpdateServerStatHandler(ServerSpeedItem server) private async Task UpdateServerStatHandler(ServerSpeedItem server)
{ {
_ = UpdateServerStat(server); await UpdateServerStat(server);
} }
private async Task UpdateServerStat(ServerSpeedItem server) private async Task UpdateServerStat(ServerSpeedItem server)
@@ -123,7 +123,7 @@ public class StatisticsManager
server.TodayDown = _serverStatItem.TodayDown; server.TodayDown = _serverStatItem.TodayDown;
server.TotalUp = _serverStatItem.TotalUp; server.TotalUp = _serverStatItem.TotalUp;
server.TotalDown = _serverStatItem.TotalDown; server.TotalDown = _serverStatItem.TotalDown;
_updateFunc?.Invoke(server); await _updateFunc?.Invoke(server);
} }
private async Task GetServerStatItem(string indexId) private async Task GetServerStatItem(string indexId)

View File

@@ -4,13 +4,18 @@ public class TaskManager
{ {
private static readonly Lazy<TaskManager> _instance = new(() => new()); private static readonly Lazy<TaskManager> _instance = new(() => new());
public static TaskManager Instance => _instance.Value; public static TaskManager Instance => _instance.Value;
private Config _config;
private Func<bool, string, Task>? _updateFunc;
public void RegUpdateTask(Config config, Action<bool, string> updateFunc) public void RegUpdateTask(Config config, Func<bool, string, Task> updateFunc)
{ {
Task.Run(() => ScheduledTasks(config, updateFunc)); _config = config;
_updateFunc = updateFunc;
Task.Run(ScheduledTasks);
} }
private async Task ScheduledTasks(Config config, Action<bool, string> updateFunc) private async Task ScheduledTasks()
{ {
Logging.SaveLog("Setup Scheduled Tasks"); Logging.SaveLog("Setup Scheduled Tasks");
@@ -21,14 +26,14 @@ public class TaskManager
await Task.Delay(1000 * 60); await Task.Delay(1000 * 60);
//Execute once 1 minute //Execute once 1 minute
await UpdateTaskRunSubscription(config, updateFunc); await UpdateTaskRunSubscription();
//Execute once 20 minute //Execute once 20 minute
if (numOfExecuted % 20 == 0) if (numOfExecuted % 20 == 0)
{ {
//Logging.SaveLog("Execute save config"); //Logging.SaveLog("Execute save config");
await ConfigHandler.SaveConfig(config); await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo(); await ProfileExManager.Instance.SaveTo();
} }
@@ -42,14 +47,14 @@ public class TaskManager
FileManager.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1)); FileManager.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
//Check once 1 hour //Check once 1 hour
await UpdateTaskRunGeo(config, numOfExecuted / 60, updateFunc); await UpdateTaskRunGeo(numOfExecuted / 60);
} }
numOfExecuted++; numOfExecuted++;
} }
} }
private async Task UpdateTaskRunSubscription(Config config, Action<bool, string> updateFunc) private async Task UpdateTaskRunSubscription()
{ {
var updateTime = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds(); var updateTime = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds();
var lstSubs = (await AppManager.Instance.SubItems())? var lstSubs = (await AppManager.Instance.SubItems())?
@@ -66,30 +71,30 @@ public class TaskManager
foreach (var item in lstSubs) foreach (var item in lstSubs)
{ {
await SubscriptionHandler.UpdateProcess(config, item.Id, true, (success, msg) => await SubscriptionHandler.UpdateProcess(_config, item.Id, true, async (success, msg) =>
{ {
updateFunc?.Invoke(success, msg); await _updateFunc?.Invoke(success, msg);
if (success) if (success)
{ {
Logging.SaveLog($"Update subscription end. {msg}"); Logging.SaveLog($"Update subscription end. {msg}");
} }
}); });
item.UpdateTime = updateTime; item.UpdateTime = updateTime;
await ConfigHandler.AddSubItem(config, item); await ConfigHandler.AddSubItem(_config, item);
await Task.Delay(1000); await Task.Delay(1000);
} }
} }
private async Task UpdateTaskRunGeo(Config config, int hours, Action<bool, string> updateFunc) private async Task UpdateTaskRunGeo(int hours)
{ {
if (config.GuiItem.AutoUpdateInterval > 0 && hours > 0 && hours % config.GuiItem.AutoUpdateInterval == 0) if (_config.GuiItem.AutoUpdateInterval > 0 && hours > 0 && hours % _config.GuiItem.AutoUpdateInterval == 0)
{ {
Logging.SaveLog("Execute update geo files"); Logging.SaveLog("Execute update geo files");
var updateHandle = new UpdateService(); var updateHandle = new UpdateService();
await updateHandle.UpdateGeoFileAll(config, (success, msg) => await updateHandle.UpdateGeoFileAll(_config, async (success, msg) =>
{ {
updateFunc?.Invoke(false, msg); await _updateFunc?.Invoke(false, msg);
}); });
} }
} }

View File

@@ -1,10 +1,13 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.Models; namespace ServiceLib.Models;
public class CheckUpdateModel public class CheckUpdateModel : ReactiveObject
{ {
public bool? IsSelected { get; set; } public bool? IsSelected { get; set; }
public string? CoreType { get; set; } public string? CoreType { get; set; }
public string? Remarks { get; set; } [Reactive] public string? Remarks { get; set; }
public string? FileName { get; set; } public string? FileName { get; set; }
public bool? IsFinished { get; set; } public bool? IsFinished { get; set; }
} }

View File

@@ -1,7 +1,10 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.Models; namespace ServiceLib.Models;
[Serializable] [Serializable]
public class ClashProxyModel public class ClashProxyModel : ReactiveObject
{ {
public string? Name { get; set; } public string? Name { get; set; }
@@ -9,9 +12,9 @@ public class ClashProxyModel
public string? Now { get; set; } public string? Now { get; set; }
public int Delay { get; set; } [Reactive] public int Delay { get; set; }
public string? DelayName { get; set; } [Reactive] public string? DelayName { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }
} }

View File

@@ -260,6 +260,7 @@ public class SimpleDNSItem
public bool? UseSystemHosts { get; set; } public bool? UseSystemHosts { get; set; }
public bool? AddCommonHosts { get; set; } public bool? AddCommonHosts { get; set; }
public bool? FakeIP { get; set; } public bool? FakeIP { get; set; }
public bool? GlobalFakeIp { get; set; }
public bool? BlockBindingQuery { get; set; } public bool? BlockBindingQuery { get; set; }
public string? DirectDNS { get; set; } public string? DirectDNS { get; set; }
public string? RemoteDNS { get; set; } public string? RemoteDNS { get; set; }

View File

@@ -17,4 +17,5 @@ public class CoreInfo
public string? Match { get; set; } public string? Match { get; set; }
public string? VersionArg { get; set; } public string? VersionArg { get; set; }
public bool AbsolutePath { get; set; } public bool AbsolutePath { get; set; }
public IDictionary<string, string?> Environment { get; set; } = new Dictionary<string, string?>();
} }

View File

@@ -1860,6 +1860,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Start parsing and processing subscription content 的本地化字符串。
/// </summary>
public static string MsgStartParsingSubscription {
get {
return ResourceManager.GetString("MsgStartParsingSubscription", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Started updating {0}... 的本地化字符串。 /// 查找类似 Started updating {0}... 的本地化字符串。
/// </summary> /// </summary>
@@ -2292,15 +2301,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Apply to Proxy Domains Only 的本地化字符串。
/// </summary>
public static string TbApplyProxyDomainsOnly {
get {
return ResourceManager.GetString("TbApplyProxyDomainsOnly", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Auto refresh 的本地化字符串。 /// 查找类似 Auto refresh 的本地化字符串。
/// </summary> /// </summary>
@@ -2517,6 +2517,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Applies globally by default, with built-in FakeIP filtering (sing-box only). 的本地化字符串。
/// </summary>
public static string TbFakeIPTips {
get {
return ResourceManager.GetString("TbFakeIPTips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Fingerprint 的本地化字符串。 /// 查找类似 Fingerprint 的本地化字符串。
/// </summary> /// </summary>
@@ -3021,6 +3030,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Select Profile 的本地化字符串。
/// </summary>
public static string TbSelectProfile {
get {
return ResourceManager.GetString("TbSelectProfile", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Set system proxy 的本地化字符串。 /// 查找类似 Set system proxy 的本地化字符串。
/// </summary> /// </summary>
@@ -3736,28 +3754,9 @@ namespace ServiceLib.Resx {
/// 查找类似 Auto Route 的本地化字符串。 /// 查找类似 Auto Route 的本地化字符串。
/// </summary> /// </summary>
public static string TbSettingsTunAutoRoute { public static string TbSettingsTunAutoRoute {
get { return ResourceManager.GetString("TbSettingsTunAutoRoute", resourceCulture); } get {
return ResourceManager.GetString("TbSettingsTunAutoRoute", resourceCulture);
} }
/// <summary>
/// 查找类似 Strict Route 的本地化字符串。
/// </summary>
public static string TbSettingsTunStrictRoute {
get { return ResourceManager.GetString("TbSettingsTunStrictRoute", resourceCulture); }
}
/// <summary>
/// 查找类似 Stack 的本地化字符串。
/// </summary>
public static string TbSettingsTunStack {
get { return ResourceManager.GetString("TbSettingsTunStack", resourceCulture); }
}
/// <summary>
/// 查找类似 MTU 的本地化字符串。
/// </summary>
public static string TbSettingsTunMtu {
get { return ResourceManager.GetString("TbSettingsTunMtu", resourceCulture); }
} }
/// <summary> /// <summary>
@@ -3769,6 +3768,33 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 MTU 的本地化字符串。
/// </summary>
public static string TbSettingsTunMtu {
get {
return ResourceManager.GetString("TbSettingsTunMtu", resourceCulture);
}
}
/// <summary>
/// 查找类似 Stack 的本地化字符串。
/// </summary>
public static string TbSettingsTunStack {
get {
return ResourceManager.GetString("TbSettingsTunStack", resourceCulture);
}
}
/// <summary>
/// 查找类似 Strict Route 的本地化字符串。
/// </summary>
public static string TbSettingsTunStrictRoute {
get {
return ResourceManager.GetString("TbSettingsTunStrictRoute", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Enable UDP 的本地化字符串。 /// 查找类似 Enable UDP 的本地化字符串。
/// </summary> /// </summary>

View File

@@ -1455,9 +1455,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value> <value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value> <value>Basic DNS Settings</value>
</data> </data>
@@ -1509,4 +1506,13 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value> <value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
</root> </root>

View File

@@ -1455,9 +1455,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value> <value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value> <value>Basic DNS Settings</value>
</data> </data>
@@ -1509,4 +1506,13 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value> <value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
</root> </root>

View File

@@ -1455,9 +1455,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value> <value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value> <value>Basic DNS Settings</value>
</data> </data>
@@ -1509,4 +1506,13 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value> <value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
</root> </root>

View File

@@ -1455,9 +1455,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS hosts: (каждая строка в формате "domain1 ip1 ip2")</value> <value>DNS hosts: (каждая строка в формате "domain1 ip1 ip2")</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Применять только к доменам через прокси</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Базовые настройки DNS</value> <value>Базовые настройки DNS</value>
</data> </data>
@@ -1509,4 +1506,13 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value> <value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
</root> </root>

View File

@@ -1452,9 +1452,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts“域名1 ip1 ip2” 一行一个)</value> <value>DNS Hosts“域名1 ip1 ip2” 一行一个)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>仅对代理域名生效</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>DNS 基础设置</value> <value>DNS 基础设置</value>
</data> </data>
@@ -1506,4 +1503,13 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>此功能供高级用户和有特殊需求的用户使用。 启用此功能后,将忽略 Core 的基础设置DNS 设置 ,路由设置。你需要保证系统代理的端口和流量统计等功能的配置正确,一切都由你来设置。</value> <value>此功能供高级用户和有特殊需求的用户使用。 启用此功能后,将忽略 Core 的基础设置DNS 设置 ,路由设置。你需要保证系统代理的端口和流量统计等功能的配置正确,一切都由你来设置。</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>开始解析和处理订阅内容</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>选择配置文件</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value>
</data>
</root> </root>

View File

@@ -1452,9 +1452,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value> <value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value> <value>Basic DNS Settings</value>
</data> </data>
@@ -1506,4 +1503,13 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve"> <data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value> <value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
</data> </data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>開始解析和處理訂閱內容</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
</root> </root>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
{
"domain": [
"amobile.music.tc.qq.com",
"api-jooxtt.sanook.com",
"api.joox.com",
"aqqmusic.tc.qq.com",
"dl.stream.qqmusic.qq.com",
"ff.dorado.sdo.com",
"heartbeat.belkin.com",
"isure.stream.qqmusic.qq.com",
"joox.com",
"lens.l.google.com",
"localhost.ptlogin2.qq.com",
"localhost.sec.qq.com",
"mesu.apple.com",
"mobileoc.music.tc.qq.com",
"music.taihe.com",
"musicapi.taihe.com",
"na.b.g-tun.com",
"proxy.golang.org",
"ps.res.netease.com",
"shark007.net",
"songsearch.kugou.com",
"static.adtidy.org",
"streamoc.music.tc.qq.com",
"swcdn.apple.com",
"swdist.apple.com",
"swdownload.apple.com",
"swquery.apple.com",
"swscan.apple.com",
"trackercdn.kugou.com",
"xnotify.xboxlive.com"
],
"domain_keyword": [
"ntp",
"stun",
"time"
],
"domain_regex": [
"^[^.]+$",
"^[^.]+\\.[^.]+\\.xboxlive\\.com$",
"^localhost\\.[^.]+\\.weixin\\.qq\\.com$",
"^mijia\\scloud$",
"^xbox\\.[^.]+\\.microsoft\\.com$",
"^xbox\\.[^.]+\\.[^.]+\\.microsoft\\.com$"
],
"domain_suffix": [
"126.net",
"3gppnetwork.org",
"battle.net",
"battlenet.com.cn",
"cdn.nintendo.net",
"cmbchina.com",
"cmbimg.com",
"ff14.sdo.com",
"ffxiv.com",
"finalfantasyxiv.com",
"gcloudcs.com",
"home.arpa",
"invalid",
"kuwo.cn",
"lan",
"linksys.com",
"linksyssmartwifi.com",
"local",
"localdomain",
"localhost",
"market.xiaomi.com",
"mcdn.bilivideo.cn",
"media.dssott.com",
"msftconnecttest.com",
"msftncsi.com",
"music.163.com",
"music.migu.cn",
"n0808.com",
"nflxvideo.net",
"oray.com",
"orayimg.com",
"router.asus.com",
"sandai.net",
"square-enix.com",
"srv.nintendo.net",
"steamcontent.com",
"uu.163.com",
"wargaming.net",
"wggames.cn",
"wotgame.cn",
"wowsgame.cn",
"xiami.com",
"y.qq.com"
]
}

View File

@@ -11,7 +11,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI.Fody" /> <PackageReference Include="ReactiveUI.Fody" />
<PackageReference Include="sqlite-net-pcl" /> <PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="Splat.NLog" /> <PackageReference Include="NLog" />
<PackageReference Include="WebDav.Client" /> <PackageReference Include="WebDav.Client" />
<PackageReference Include="YamlDotNet" /> <PackageReference Include="YamlDotNet" />
<PackageReference Include="QRCoder" /> <PackageReference Include="QRCoder" />
@@ -44,6 +44,7 @@
<EmbeddedResource Include="Sample\tun_singbox_inbound" /> <EmbeddedResource Include="Sample\tun_singbox_inbound" />
<EmbeddedResource Include="Sample\tun_singbox_rules" /> <EmbeddedResource Include="Sample\tun_singbox_rules" />
<EmbeddedResource Include="Sample\linux_autostart_config" /> <EmbeddedResource Include="Sample\linux_autostart_config" />
<EmbeddedResource Include="Sample\singbox_fakeip_filter" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -79,6 +79,7 @@ public class CoreConfigClashService
//external-controller //external-controller
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}"; fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
fileContent.Remove("secret");
//allow-lan //allow-lan
if (_config.Inbound.First().AllowLANConn) if (_config.Inbound.First().AllowLANConn)
{ {

View File

@@ -33,17 +33,17 @@ public partial class CoreConfigSingboxService
lastRule.Ip?.Contains("0.0.0.0/0") == true); lastRule.Ip?.Contains("0.0.0.0/0") == true);
} }
singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag; singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag;
if ((!useDirectDns) && simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false)
// Tun2SocksAddress
if (node != null && Utils.IsDomain(node.Address))
{ {
singboxConfig.dns.rules ??= new List<Rule4Sbox>(); singboxConfig.dns.rules.Add(new()
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{ {
server = Global.SingboxOutboundResolverTag, server = Global.SingboxFakeDNSTag,
domain = [node.Address], query_type = new List<int> { 1, 28 }, // A and AAAA
rewrite_ttl = 1,
}); });
} }
await GenOutboundDnsRule(node, singboxConfig, Global.SingboxOutboundResolverTag);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -80,25 +80,21 @@ public partial class CoreConfigSingboxService
hostsDns.predefined = Global.PredefinedHosts; hostsDns.predefined = Global.PredefinedHosts;
} }
if (simpleDNSItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts != null && systemHosts.Count > 0)
{
foreach (var host in systemHosts)
{
hostsDns.predefined.TryAdd(host.Key, new List<string> { host.Value });
}
}
}
if (!simpleDNSItem.Hosts.IsNullOrEmpty()) if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{ {
var userHostsMap = simpleDNSItem.Hosts? var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Where(line => !string.IsNullOrWhiteSpace(line))
.Where(line => line.Contains(' '))
.ToDictionary(
line =>
{
var parts = line.Trim().Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
return parts[0];
},
line =>
{
var parts = line.Trim().Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
var values = parts.Skip(1).ToList();
return values;
}
) ?? new Dictionary<string, List<string>>();
foreach (var kvp in userHostsMap) foreach (var kvp in userHostsMap)
{ {
@@ -106,22 +102,6 @@ public partial class CoreConfigSingboxService
} }
} }
if (simpleDNSItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts.Count > 0)
{
foreach (var host in systemHosts)
{
if (hostsDns.predefined[host.Key] != null)
{
continue;
}
hostsDns.predefined[host.Key] = new List<string> { host.Value };
}
}
}
foreach (var host in hostsDns.predefined) foreach (var host in hostsDns.predefined)
{ {
if (finalDns.server == host.Key) if (finalDns.server == host.Key)
@@ -207,6 +187,28 @@ public partial class CoreConfigSingboxService
}); });
} }
if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == true)
{
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
fakeipFilterRule.invert = true;
var rule4Fake = new Rule4Sbox
{
server = Global.SingboxFakeDNSTag,
type = "logical",
mode = "and",
rewrite_ttl = 1,
rules = new List<Rule4Sbox>
{
new() {
query_type = new List<int> { 1, 28 }, // A and AAAA
},
fakeipFilterRule,
}
};
singboxConfig.dns.rules.Add(rule4Fake);
}
var routing = await ConfigHandler.GetDefaultRouting(_config); var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing == null) if (routing == null)
return 0; return 0;
@@ -286,10 +288,12 @@ public partial class CoreConfigSingboxService
} }
else else
{ {
if (simpleDNSItem.FakeIP == true) if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false)
{ {
var rule4Fake = JsonUtils.DeepCopy(rule); var rule4Fake = JsonUtils.DeepCopy(rule);
rule4Fake.server = Global.SingboxFakeDNSTag; rule4Fake.server = Global.SingboxFakeDNSTag;
rule4Fake.query_type = new List<int> { 1, 28 }; // A and AAAA
rule4Fake.rewrite_ttl = 1;
singboxConfig.dns.rules.Add(rule4Fake); singboxConfig.dns.rules.Add(rule4Fake);
} }
rule.server = Global.SingboxRemoteDNSTag; rule.server = Global.SingboxRemoteDNSTag;
@@ -333,16 +337,7 @@ public partial class CoreConfigSingboxService
await GenDnsDomainsLegacyCompatible(singboxConfig, item); await GenDnsDomainsLegacyCompatible(singboxConfig, item);
} }
// Tun2SocksAddress await GenOutboundDnsRule(node, singboxConfig, Global.SingboxFinalResolverTag);
if (node != null && Utils.IsDomain(node.Address))
{
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = Global.SingboxFinalResolverTag,
domain = [node.Address],
});
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -412,6 +407,37 @@ public partial class CoreConfigSingboxService
return await Task.FromResult(0); return await Task.FromResult(0);
} }
private async Task<int> GenOutboundDnsRule(ProfileItem? node, SingboxConfig singboxConfig, string? server)
{
if (node == null)
{
return 0;
}
var domain = string.Empty;
if (Utils.IsDomain(node.Address)) // normal outbound
{
domain = node.Address;
}
else if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty() && Utils.IsDomain(node.SpiderX)) // Tun2SocksAddress
{
domain = node.SpiderX;
}
if (domain.IsNullOrEmpty())
{
return 0;
}
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = server,
domain = [domain],
});
return await Task.FromResult(0);
}
private static Server4Sbox? ParseDnsAddress(string address) private static Server4Sbox? ParseDnsAddress(string address)
{ {
var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim(); var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim();

View File

@@ -10,7 +10,7 @@ public partial class CoreConfigSingboxService
singboxConfig.inbounds = []; singboxConfig.inbounds = [];
if (!_config.TunModeItem.EnableTun if (!_config.TunModeItem.EnableTun
|| _config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && _config.RunningCoreType == ECoreType.sing_box) || (_config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && _config.RunningCoreType == ECoreType.sing_box))
{ {
var inbound = new Inbound4Sbox() var inbound = new Inbound4Sbox()
{ {
@@ -78,7 +78,7 @@ public partial class CoreConfigSingboxService
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return 0; return await Task.FromResult(0);
} }
private Inbound4Sbox GetInbound(Inbound4Sbox inItem, EInboundProtocol protocol, bool bSocks) private Inbound4Sbox GetInbound(Inbound4Sbox inItem, EInboundProtocol protocol, bool bSocks)

View File

@@ -71,6 +71,37 @@ public partial class CoreConfigSingboxService
}); });
} }
var hostsDomains = new List<string>();
var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (dnsItem == null || dnsItem.Enabled == false)
{
var simpleDNSItem = _config.SimpleDNSItem;
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
foreach (var kvp in userHostsMap)
{
hostsDomains.Add(kvp.Key);
}
}
if (simpleDNSItem.UseSystemHosts == true)
{
var systemHostsMap = Utils.GetSystemHosts();
foreach (var kvp in systemHostsMap)
{
hostsDomains.Add(kvp.Key);
}
}
}
if (hostsDomains.Count > 0)
{
singboxConfig.route.rules.Add(new()
{
action = "resolve",
domain = hostsDomains,
});
}
singboxConfig.route.rules.Add(new() singboxConfig.route.rules.Add(new()
{ {
outbound = Global.DirectTag, outbound = Global.DirectTag,
@@ -343,6 +374,13 @@ public partial class CoreConfigSingboxService
return Global.ProxyTag; return Global.ProxyTag;
} }
var tag = Global.ProxyTag + node.IndexId.ToString();
if (singboxConfig.outbounds.Any(o => o.tag == tag)
|| (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag)))
{
return tag;
}
var server = await GenServer(node); var server = await GenServer(node);
if (server is null) if (server is null)
{ {

View File

@@ -248,43 +248,21 @@ public partial class CoreConfigV2rayService
if (simpleDNSItem.UseSystemHosts == true) if (simpleDNSItem.UseSystemHosts == true)
{ {
var systemHosts = Utils.GetSystemHosts(); var systemHosts = Utils.GetSystemHosts();
if (systemHosts.Count > 0) var normalHost = v2rayConfig?.dns?.hosts;
{
var normalHost = v2rayConfig.dns.hosts; if (normalHost != null && systemHosts?.Count > 0)
if (normalHost != null)
{ {
foreach (var host in systemHosts) foreach (var host in systemHosts)
{ {
if (normalHost[host.Key] != null) normalHost.TryAdd(host.Key, new List<string> { host.Value });
{
continue;
}
normalHost[host.Key] = new List<string> { host.Value };
}
} }
} }
} }
var userHostsMap = simpleDNSItem.Hosts? if (!simpleDNSItem.Hosts.IsNullOrEmpty())
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Where(line => !string.IsNullOrWhiteSpace(line))
.Where(line => line.Contains(' '))
.ToDictionary(
line =>
{ {
var parts = line.Trim().Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
return parts[0];
},
line =>
{
var parts = line.Trim().Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
var values = parts.Skip(1).ToList();
return values;
}
);
if (userHostsMap != null)
{
foreach (var kvp in userHostsMap) foreach (var kvp in userHostsMap)
{ {
v2rayConfig.dns.hosts[kvp.Key] = kvp.Value; v2rayConfig.dns.hosts[kvp.Key] = kvp.Value;

View File

@@ -131,10 +131,16 @@ public partial class CoreConfigV2rayService
return Global.ProxyTag; return Global.ProxyTag;
} }
var tag = Global.ProxyTag + node.IndexId.ToString();
if (v2rayConfig.outbounds.Any(p => p.tag == tag))
{
return tag;
}
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(node, outbound); await GenOutbound(node, outbound);
outbound.tag = Global.ProxyTag + node.IndexId.ToString(); outbound.tag = tag;
v2rayConfig.outbounds.Add(outbound); v2rayConfig.outbounds.Add(outbound);
return outbound.tag; return outbound.tag;

View File

@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Sockets; using System.Net.Sockets;
@@ -16,7 +15,7 @@ public class DownloadService
private static readonly string _tag = "DownloadService"; private static readonly string _tag = "DownloadService";
public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Action<bool, string> updateFunc) public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
{ {
try try
{ {
@@ -32,10 +31,10 @@ public class DownloadService
} }
catch (Exception ex) catch (Exception ex)
{ {
updateFunc?.Invoke(false, ex.Message); await updateFunc?.Invoke(false, ex.Message);
if (ex.InnerException != null) if (ex.InnerException != null)
{ {
updateFunc?.Invoke(false, ex.InnerException.Message); await updateFunc?.Invoke(false, ex.InnerException.Message);
} }
} }
return 0; return 0;

View File

@@ -5,26 +5,20 @@ using System.Net.Sockets;
namespace ServiceLib.Services; namespace ServiceLib.Services;
public class SpeedtestService public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateFunc)
{ {
private static readonly string _tag = "SpeedtestService"; private static readonly string _tag = "SpeedtestService";
private Config? _config; private readonly Config? _config = config;
private Action<SpeedTestResult>? _updateFunc; private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private static readonly ConcurrentBag<string> _lstExitLoop = new(); private static readonly ConcurrentBag<string> _lstExitLoop = new();
public SpeedtestService(Config config, Action<SpeedTestResult> updateFunc)
{
_config = config;
_updateFunc = updateFunc;
}
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds) public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
{ {
Task.Run(async () => Task.Run(async () =>
{ {
await RunAsync(actionType, selecteds); await RunAsync(actionType, selecteds);
await ProfileExManager.Instance.SaveTo(); await ProfileExManager.Instance.SaveTo();
UpdateFunc("", ResUI.SpeedtestingCompleted); await UpdateFunc("", ResUI.SpeedtestingCompleted);
}); });
} }
@@ -43,7 +37,7 @@ public class SpeedtestService
var exitLoopKey = Utils.GetGuid(false); var exitLoopKey = Utils.GetGuid(false);
_lstExitLoop.Add(exitLoopKey); _lstExitLoop.Add(exitLoopKey);
var lstSelected = GetClearItem(actionType, selecteds); var lstSelected = await GetClearItem(actionType, selecteds);
switch (actionType) switch (actionType)
{ {
@@ -65,7 +59,7 @@ public class SpeedtestService
} }
} }
private List<ServerTestItem> GetClearItem(ESpeedActionType actionType, List<ProfileItem> selecteds) private async Task<List<ServerTestItem>> GetClearItem(ESpeedActionType actionType, List<ProfileItem> selecteds)
{ {
var lstSelected = new List<ServerTestItem>(); var lstSelected = new List<ServerTestItem>();
foreach (var it in selecteds) foreach (var it in selecteds)
@@ -97,17 +91,17 @@ public class SpeedtestService
{ {
case ESpeedActionType.Tcping: case ESpeedActionType.Tcping:
case ESpeedActionType.Realping: case ESpeedActionType.Realping:
UpdateFunc(it.IndexId, ResUI.Speedtesting, ""); await UpdateFunc(it.IndexId, ResUI.Speedtesting, "");
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
break; break;
case ESpeedActionType.Speedtest: case ESpeedActionType.Speedtest:
UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait); await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0); ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break; break;
case ESpeedActionType.Mixedtest: case ESpeedActionType.Mixedtest:
UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait); await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0); ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break; break;
@@ -133,7 +127,7 @@ public class SpeedtestService
var responseTime = await GetTcpingTime(it.Address, it.Port); var responseTime = await GetTcpingTime(it.Address, it.Port);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -169,11 +163,11 @@ public class SpeedtestService
{ {
if (_lstExitLoop.Any(p => p == exitLoopKey) == false) if (_lstExitLoop.Any(p => p == exitLoopKey) == false)
{ {
UpdateFunc("", ResUI.SpeedtestingSkip); await UpdateFunc("", ResUI.SpeedtestingSkip);
return; return;
} }
UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount) if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount)
{ {
@@ -239,7 +233,7 @@ public class SpeedtestService
{ {
if (_lstExitLoop.Any(p => p == exitLoopKey) == false) if (_lstExitLoop.Any(p => p == exitLoopKey) == false)
{ {
UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
continue; continue;
} }
if (it.ConfigType == EConfigType.Custom) if (it.ConfigType == EConfigType.Custom)
@@ -256,7 +250,7 @@ public class SpeedtestService
pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (pid < 0) if (pid < 0)
{ {
UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
} }
else else
{ {
@@ -270,7 +264,7 @@ public class SpeedtestService
} }
else else
{ {
UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
} }
} }
} }
@@ -298,25 +292,25 @@ public class SpeedtestService
var responseTime = await HttpClientHelper.Instance.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); var responseTime = await HttpClientHelper.Instance.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
return responseTime; return responseTime;
} }
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it) private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it)
{ {
UpdateFunc(it.IndexId, "", ResUI.Speedtesting); await UpdateFunc(it.IndexId, "", ResUI.Speedtesting);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var url = _config.SpeedTestItem.SpeedTestUrl; var url = _config.SpeedTestItem.SpeedTestUrl;
var timeout = _config.SpeedTestItem.SpeedTestTimeout; var timeout = _config.SpeedTestItem.SpeedTestTimeout;
await downloadHandle.DownloadDataAsync(url, webProxy, timeout, (success, msg) => await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) =>
{ {
decimal.TryParse(msg, out var dec); decimal.TryParse(msg, out var dec);
if (dec > 0) if (dec > 0)
{ {
ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec); ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
} }
UpdateFunc(it.IndexId, "", msg); await UpdateFunc(it.IndexId, "", msg);
}); });
} }
@@ -371,9 +365,9 @@ public class SpeedtestService
return lstTest; return lstTest;
} }
private void UpdateFunc(string indexId, string delay, string speed = "") private async Task UpdateFunc(string indexId, string delay, string speed = "")
{ {
_updateFunc?.Invoke(new() { IndexId = indexId, Delay = delay, Speed = speed }); await _updateFunc?.Invoke(new() { IndexId = indexId, Delay = delay, Speed = speed });
if (indexId.IsNotEmpty() && speed.IsNotEmpty()) if (indexId.IsNotEmpty() && speed.IsNotEmpty())
{ {
ProfileExManager.Instance.SetTestMessage(indexId, speed); ProfileExManager.Instance.SetTestMessage(indexId, speed);

View File

@@ -8,11 +8,11 @@ public class StatisticsSingboxService
private readonly Config _config; private readonly Config _config;
private bool _exitFlag; private bool _exitFlag;
private ClientWebSocket? webSocket; private ClientWebSocket? webSocket;
private Action<ServerSpeedItem>? _updateFunc; private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic"; private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic";
private static readonly string _tag = "StatisticsSingboxService"; private static readonly string _tag = "StatisticsSingboxService";
public StatisticsSingboxService(Config config, Action<ServerSpeedItem> updateFunc) public StatisticsSingboxService(Config config, Func<ServerSpeedItem, Task> updateFunc)
{ {
_config = config; _config = config;
_updateFunc = updateFunc; _updateFunc = updateFunc;
@@ -90,7 +90,7 @@ public class StatisticsSingboxService
{ {
ParseOutput(result, out var up, out var down); ParseOutput(result, out var up, out var down);
_updateFunc?.Invoke(new ServerSpeedItem() await _updateFunc?.Invoke(new ServerSpeedItem()
{ {
ProxyUp = (long)(up / 1000), ProxyUp = (long)(up / 1000),
ProxyDown = (long)(down / 1000) ProxyDown = (long)(down / 1000)

View File

@@ -6,10 +6,10 @@ public class StatisticsXrayService
private ServerSpeedItem _serverSpeedItem = new(); private ServerSpeedItem _serverSpeedItem = new();
private readonly Config _config; private readonly Config _config;
private bool _exitFlag; private bool _exitFlag;
private Action<ServerSpeedItem>? _updateFunc; private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars"; private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars";
public StatisticsXrayService(Config config, Action<ServerSpeedItem> updateFunc) public StatisticsXrayService(Config config, Func<ServerSpeedItem, Task> updateFunc)
{ {
_config = config; _config = config;
_updateFunc = updateFunc; _updateFunc = updateFunc;
@@ -39,7 +39,7 @@ public class StatisticsXrayService
if (result != null) if (result != null)
{ {
var server = ParseOutput(result) ?? new ServerSpeedItem(); var server = ParseOutput(result) ?? new ServerSpeedItem();
_updateFunc?.Invoke(server); await _updateFunc?.Invoke(server);
} }
} }
catch catch

View File

@@ -5,11 +5,11 @@ namespace ServiceLib.Services;
public class UpdateService public class UpdateService
{ {
private Action<bool, string>? _updateFunc; private Func<bool, string, Task>? _updateFunc;
private readonly int _timeout = 30; private readonly int _timeout = 30;
private static readonly string _tag = "UpdateService"; private static readonly string _tag = "UpdateService";
public async Task CheckUpdateGuiN(Config config, Action<bool, string> updateFunc, bool preRelease) public async Task CheckUpdateGuiN(Config config, Func<bool, string, Task> updateFunc, bool preRelease)
{ {
_updateFunc = updateFunc; _updateFunc = updateFunc;
var url = string.Empty; var url = string.Empty;
@@ -20,25 +20,25 @@ public class UpdateService
{ {
if (args.Success) if (args.Success)
{ {
_updateFunc?.Invoke(false, ResUI.MsgDownloadV2rayCoreSuccessfully); UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully);
_updateFunc?.Invoke(true, Utils.UrlEncode(fileName)); UpdateFunc(true, Utils.UrlEncode(fileName));
} }
else else
{ {
_updateFunc?.Invoke(false, args.Msg); UpdateFunc(false, args.Msg);
} }
}; };
downloadHandle.Error += (sender2, args) => downloadHandle.Error += (sender2, args) =>
{ {
_updateFunc?.Invoke(false, args.GetException().Message); UpdateFunc(false, args.GetException().Message);
}; };
_updateFunc?.Invoke(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN)); await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN));
var result = await CheckUpdateAsync(downloadHandle, ECoreType.v2rayN, preRelease); var result = await CheckUpdateAsync(downloadHandle, ECoreType.v2rayN, preRelease);
if (result.Success) if (result.Success)
{ {
_updateFunc?.Invoke(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN)); await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN));
_updateFunc?.Invoke(false, result.Msg); await UpdateFunc(false, result.Msg);
url = result.Data?.ToString(); url = result.Data?.ToString();
fileName = Utils.GetTempPath(Utils.GetGuid()); fileName = Utils.GetTempPath(Utils.GetGuid());
@@ -46,11 +46,11 @@ public class UpdateService
} }
else else
{ {
_updateFunc?.Invoke(false, result.Msg); await UpdateFunc(false, result.Msg);
} }
} }
public async Task CheckUpdateCore(ECoreType type, Config config, Action<bool, string> updateFunc, bool preRelease) public async Task CheckUpdateCore(ECoreType type, Config config, Func<bool, string, Task> updateFunc, bool preRelease)
{ {
_updateFunc = updateFunc; _updateFunc = updateFunc;
var url = string.Empty; var url = string.Empty;
@@ -61,34 +61,34 @@ public class UpdateService
{ {
if (args.Success) if (args.Success)
{ {
_updateFunc?.Invoke(false, ResUI.MsgDownloadV2rayCoreSuccessfully); UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully);
_updateFunc?.Invoke(false, ResUI.MsgUnpacking); UpdateFunc(false, ResUI.MsgUnpacking);
try try
{ {
_updateFunc?.Invoke(true, fileName); UpdateFunc(true, fileName);
} }
catch (Exception ex) catch (Exception ex)
{ {
_updateFunc?.Invoke(false, ex.Message); UpdateFunc(false, ex.Message);
} }
} }
else else
{ {
_updateFunc?.Invoke(false, args.Msg); UpdateFunc(false, args.Msg);
} }
}; };
downloadHandle.Error += (sender2, args) => downloadHandle.Error += (sender2, args) =>
{ {
_updateFunc?.Invoke(false, args.GetException().Message); UpdateFunc(false, args.GetException().Message);
}; };
_updateFunc?.Invoke(false, string.Format(ResUI.MsgStartUpdating, type)); await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, type));
var result = await CheckUpdateAsync(downloadHandle, type, preRelease); var result = await CheckUpdateAsync(downloadHandle, type, preRelease);
if (result.Success) if (result.Success)
{ {
_updateFunc?.Invoke(false, string.Format(ResUI.MsgParsingSuccessfully, type)); await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type));
_updateFunc?.Invoke(false, result.Msg); await UpdateFunc(false, result.Msg);
url = result.Data?.ToString(); url = result.Data?.ToString();
var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url); var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url);
@@ -99,17 +99,17 @@ public class UpdateService
{ {
if (!result.Msg.IsNullOrEmpty()) if (!result.Msg.IsNullOrEmpty())
{ {
_updateFunc?.Invoke(false, result.Msg); await UpdateFunc(false, result.Msg);
} }
} }
} }
public async Task UpdateGeoFileAll(Config config, Action<bool, string> updateFunc) public async Task UpdateGeoFileAll(Config config, Func<bool, string, Task> updateFunc)
{ {
await UpdateGeoFiles(config, updateFunc); await UpdateGeoFiles(config, updateFunc);
await UpdateOtherFiles(config, updateFunc); await UpdateOtherFiles(config, updateFunc);
await UpdateSrsFileAll(config, updateFunc); await UpdateSrsFileAll(config, updateFunc);
_updateFunc?.Invoke(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
} }
#region CheckUpdate private #region CheckUpdate private
@@ -128,7 +128,7 @@ public class UpdateService
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
_updateFunc?.Invoke(false, ex.Message); await UpdateFunc(false, ex.Message);
return new RetResult(false, ex.Message); return new RetResult(false, ex.Message);
} }
} }
@@ -212,7 +212,7 @@ public class UpdateService
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
_updateFunc?.Invoke(false, ex.Message); await UpdateFunc(false, ex.Message);
return new SemanticVersion(""); return new SemanticVersion("");
} }
} }
@@ -272,7 +272,7 @@ public class UpdateService
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
_updateFunc?.Invoke(false, ex.Message); await UpdateFunc(false, ex.Message);
return new RetResult(false, ex.Message); return new RetResult(false, ex.Message);
} }
} }
@@ -333,7 +333,7 @@ public class UpdateService
#region Geo private #region Geo private
private async Task UpdateGeoFiles(Config config, Action<bool, string> updateFunc) private async Task UpdateGeoFiles(Config config, Func<bool, string, Task> updateFunc)
{ {
_updateFunc = updateFunc; _updateFunc = updateFunc;
@@ -352,7 +352,7 @@ public class UpdateService
} }
} }
private async Task UpdateOtherFiles(Config config, Action<bool, string> updateFunc) private async Task UpdateOtherFiles(Config config, Func<bool, string, Task> updateFunc)
{ {
//If it is not in China area, no update is required //If it is not in China area, no update is required
if (config.ConstItem.GeoSourceUrl.IsNotEmpty()) if (config.ConstItem.GeoSourceUrl.IsNotEmpty())
@@ -371,7 +371,7 @@ public class UpdateService
} }
} }
private async Task UpdateSrsFileAll(Config config, Action<bool, string> updateFunc) private async Task UpdateSrsFileAll(Config config, Func<bool, string, Task> updateFunc)
{ {
_updateFunc = updateFunc; _updateFunc = updateFunc;
@@ -426,7 +426,7 @@ public class UpdateService
} }
} }
private async Task UpdateSrsFile(string type, string srsName, Config config, Action<bool, string> updateFunc) private async Task UpdateSrsFile(string type, string srsName, Config config, Func<bool, string, Task> updateFunc)
{ {
var srsUrl = string.IsNullOrEmpty(config.ConstItem.SrsSourceUrl) var srsUrl = string.IsNullOrEmpty(config.ConstItem.SrsSourceUrl)
? Global.SingboxRulesetUrl ? Global.SingboxRulesetUrl
@@ -439,7 +439,7 @@ public class UpdateService
await DownloadGeoFile(url, fileName, targetPath, updateFunc); await DownloadGeoFile(url, fileName, targetPath, updateFunc);
} }
private async Task DownloadGeoFile(string url, string fileName, string targetPath, Action<bool, string> updateFunc) private async Task DownloadGeoFile(string url, string fileName, string targetPath, Func<bool, string, Task> updateFunc)
{ {
var tmpFileName = Utils.GetTempPath(Utils.GetGuid()); var tmpFileName = Utils.GetTempPath(Utils.GetGuid());
@@ -448,7 +448,7 @@ public class UpdateService
{ {
if (args.Success) if (args.Success)
{ {
_updateFunc?.Invoke(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName)); UpdateFunc(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName));
try try
{ {
@@ -457,26 +457,31 @@ public class UpdateService
File.Copy(tmpFileName, targetPath, true); File.Copy(tmpFileName, targetPath, true);
File.Delete(tmpFileName); File.Delete(tmpFileName);
//_updateFunc?.Invoke(true, ""); //await UpdateFunc(true, "");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_updateFunc?.Invoke(false, ex.Message); UpdateFunc(false, ex.Message);
} }
} }
else else
{ {
_updateFunc?.Invoke(false, args.Msg); UpdateFunc(false, args.Msg);
} }
}; };
downloadHandle.Error += (sender2, args) => downloadHandle.Error += (sender2, args) =>
{ {
_updateFunc?.Invoke(false, args.GetException().Message); UpdateFunc(false, args.GetException().Message);
}; };
await downloadHandle.DownloadFileAsync(url, tmpFileName, true, _timeout); await downloadHandle.DownloadFileAsync(url, tmpFileName, true, _timeout);
} }
#endregion Geo private #endregion Geo private
private async Task UpdateFunc(bool notify, string msg)
{
await _updateFunc?.Invoke(notify, msg);
}
} }

View File

@@ -1,7 +1,6 @@
using System.Reactive; using System.Reactive;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
@@ -136,8 +135,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
var result = await CreateZipFileFromDirectory(fileBackup); var result = await CreateZipFileFromDirectory(fileBackup);
if (result) if (result)
{ {
var service = Locator.Current.GetService<MainWindowViewModel>(); await AppManager.Instance.AppExitAsync(false);
await service?.MyAppExitAsync(true);
await SQLiteHelper.Instance.DisposeDbConnectionAsync(); await SQLiteHelper.Instance.DisposeDbConnectionAsync();
var toPath = Utils.GetConfigPath(); var toPath = Utils.GetConfigPath();
@@ -154,7 +152,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
_ = ProcUtils.ProcessStart(upgradeFileName, Global.RebootAs, Utils.StartupPath()); _ = ProcUtils.ProcessStart(upgradeFileName, Global.RebootAs, Utils.StartupPath());
} }
} }
service?.Shutdown(true); AppManager.Instance.Shutdown(true);
} }
else else
{ {

View File

@@ -1,21 +1,21 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class CheckUpdateViewModel : MyReactiveObject public class CheckUpdateViewModel : MyReactiveObject
{ {
private const string _geo = "GeoFiles"; private const string _geo = "GeoFiles";
private string _v2rayN = ECoreType.v2rayN.ToString(); private readonly string _v2rayN = ECoreType.v2rayN.ToString();
private List<CheckUpdateModel> _lstUpdated = []; private List<CheckUpdateModel> _lstUpdated = [];
private static readonly string _tag = "CheckUpdateViewModel";
private IObservableCollection<CheckUpdateModel> _checkUpdateModel = new ObservableCollectionExtended<CheckUpdateModel>(); public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>();
public IObservableCollection<CheckUpdateModel> CheckUpdateModels => _checkUpdateModel;
public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; } public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; }
[Reactive] public bool EnableCheckPreReleaseUpdate { get; set; } [Reactive] public bool EnableCheckPreReleaseUpdate { get; set; }
@@ -24,9 +24,11 @@ public class CheckUpdateViewModel : MyReactiveObject
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
CheckUpdateCmd = ReactiveCommand.CreateFromTask(async () => CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate);
CheckUpdateCmd.ThrownExceptions.Subscribe(ex =>
{ {
await CheckUpdate(); Logging.SaveLog(_tag, ex);
_ = UpdateView(_v2rayN, ex.Message);
}); });
EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate; EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate;
@@ -34,31 +36,41 @@ public class CheckUpdateViewModel : MyReactiveObject
this.WhenAnyValue( this.WhenAnyValue(
x => x.EnableCheckPreReleaseUpdate, x => x.EnableCheckPreReleaseUpdate,
y => y == true) y => y == true)
.Subscribe(c => { _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate; }); .Subscribe(c => _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate);
RefreshCheckUpdateItems(); RefreshCheckUpdateItems();
} }
private void RefreshCheckUpdateItems() private void RefreshCheckUpdateItems()
{ {
_checkUpdateModel.Clear(); CheckUpdateModels.Clear();
if (RuntimeInformation.ProcessArchitecture != Architecture.X86) if (RuntimeInformation.ProcessArchitecture != Architecture.X86)
{ {
_checkUpdateModel.Add(GetCheckUpdateModel(_v2rayN)); CheckUpdateModels.Add(GetCheckUpdateModel(_v2rayN));
//Not Windows and under Win10 //Not Windows and under Win10
if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10)) if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10))
{ {
_checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.Xray.ToString())); CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.Xray.ToString()));
_checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString())); CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString()));
_checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString())); CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString()));
} }
} }
_checkUpdateModel.Add(GetCheckUpdateModel(_geo)); CheckUpdateModels.Add(GetCheckUpdateModel(_geo));
} }
private CheckUpdateModel GetCheckUpdateModel(string coreType) private CheckUpdateModel GetCheckUpdateModel(string coreType)
{ {
if (coreType == _v2rayN && Utils.IsPackagedInstall())
{
return new()
{
IsSelected = false,
CoreType = coreType,
Remarks = ResUI.menuCheckUpdate + " (Not Support)",
};
}
return new() return new()
{ {
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType) ?? true, IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType) ?? true,
@@ -69,7 +81,7 @@ public class CheckUpdateViewModel : MyReactiveObject
private async Task SaveSelectedCoreTypes() private async Task SaveSelectedCoreTypes()
{ {
_config.CheckUpdateItem.SelectedCoreTypes = _checkUpdateModel.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList(); _config.CheckUpdateItem.SelectedCoreTypes = CheckUpdateModels.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList();
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
} }
@@ -81,23 +93,30 @@ public class CheckUpdateViewModel : MyReactiveObject
private async Task CheckUpdateTask() private async Task CheckUpdateTask()
{ {
_lstUpdated.Clear(); _lstUpdated.Clear();
_lstUpdated = _checkUpdateModel.Where(x => x.IsSelected == true) _lstUpdated = CheckUpdateModels.Where(x => x.IsSelected == true)
.Select(x => new CheckUpdateModel() { CoreType = x.CoreType }).ToList(); .Select(x => new CheckUpdateModel() { CoreType = x.CoreType }).ToList();
await SaveSelectedCoreTypes(); await SaveSelectedCoreTypes();
for (var k = _checkUpdateModel.Count - 1; k >= 0; k--) for (var k = CheckUpdateModels.Count - 1; k >= 0; k--)
{ {
var item = _checkUpdateModel[k]; var item = CheckUpdateModels[k];
if (item.IsSelected != true) if (item.IsSelected != true)
{
continue; continue;
}
UpdateView(item.CoreType, "..."); await UpdateView(item.CoreType, "...");
if (item.CoreType == _geo) if (item.CoreType == _geo)
{ {
await CheckUpdateGeo(); await CheckUpdateGeo();
} }
else if (item.CoreType == _v2rayN) else if (item.CoreType == _v2rayN)
{ {
if (Utils.IsPackagedInstall())
{
await UpdateView(_v2rayN, "Not Support");
continue;
}
await CheckUpdateN(EnableCheckPreReleaseUpdate); await CheckUpdateN(EnableCheckPreReleaseUpdate);
} }
else if (item.CoreType == ECoreType.Xray.ToString()) else if (item.CoreType == ECoreType.Xray.ToString())
@@ -129,90 +148,90 @@ public class CheckUpdateViewModel : MyReactiveObject
private async Task CheckUpdateGeo() private async Task CheckUpdateGeo()
{ {
void _updateUI(bool success, string msg) async Task _updateUI(bool success, string msg)
{ {
UpdateView(_geo, msg); await UpdateView(_geo, msg);
if (success) if (success)
{ {
UpdatedPlusPlus(_geo, ""); UpdatedPlusPlus(_geo, "");
} }
} }
await (new UpdateService()).UpdateGeoFileAll(_config, _updateUI) await new UpdateService().UpdateGeoFileAll(_config, _updateUI)
.ContinueWith(t => .ContinueWith(t => UpdatedPlusPlus(_geo, ""));
{
UpdatedPlusPlus(_geo, "");
});
} }
private async Task CheckUpdateN(bool preRelease) private async Task CheckUpdateN(bool preRelease)
{ {
void _updateUI(bool success, string msg) async Task _updateUI(bool success, string msg)
{ {
UpdateView(_v2rayN, msg); await UpdateView(_v2rayN, msg);
if (success) if (success)
{ {
UpdateView(_v2rayN, ResUI.OperationSuccess); await UpdateView(_v2rayN, ResUI.OperationSuccess);
UpdatedPlusPlus(_v2rayN, msg); UpdatedPlusPlus(_v2rayN, msg);
} }
} }
await (new UpdateService()).CheckUpdateGuiN(_config, _updateUI, preRelease) await new UpdateService().CheckUpdateGuiN(_config, _updateUI, preRelease)
.ContinueWith(t => .ContinueWith(t => UpdatedPlusPlus(_v2rayN, ""));
{
UpdatedPlusPlus(_v2rayN, "");
});
} }
private async Task CheckUpdateCore(CheckUpdateModel model, bool preRelease) private async Task CheckUpdateCore(CheckUpdateModel model, bool preRelease)
{ {
void _updateUI(bool success, string msg) async Task _updateUI(bool success, string msg)
{ {
UpdateView(model.CoreType, msg); await UpdateView(model.CoreType, msg);
if (success) if (success)
{ {
UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore); await UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore);
UpdatedPlusPlus(model.CoreType, msg); UpdatedPlusPlus(model.CoreType, msg);
} }
} }
var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType); var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType);
await (new UpdateService()).CheckUpdateCore(type, _config, _updateUI, preRelease) await new UpdateService().CheckUpdateCore(type, _config, _updateUI, preRelease)
.ContinueWith(t => .ContinueWith(t => UpdatedPlusPlus(model.CoreType, ""));
{
UpdatedPlusPlus(model.CoreType, "");
});
} }
private async Task UpdateFinished() private async Task UpdateFinished()
{ {
if (_lstUpdated.Count > 0 && _lstUpdated.Count(x => x.IsFinished == true) == _lstUpdated.Count) if (_lstUpdated.Count > 0 && _lstUpdated.Count(x => x.IsFinished == true) == _lstUpdated.Count)
{ {
_updateView?.Invoke(EViewAction.DispatcherCheckUpdateFinished, false); await UpdateFinishedSub(false);
await Task.Delay(2000); await Task.Delay(2000);
await UpgradeCore(); await UpgradeCore();
if (_lstUpdated.Any(x => x.CoreType == _v2rayN && x.IsFinished == true)) if (_lstUpdated.Any(x => x.CoreType == _v2rayN && x.IsFinished == true))
{ {
await Task.Delay(1000); await Task.Delay(1000);
UpgradeN(); await UpgradeN();
} }
await Task.Delay(1000); await Task.Delay(1000);
_updateView?.Invoke(EViewAction.DispatcherCheckUpdateFinished, true); await UpdateFinishedSub(true);
} }
} }
public void UpdateFinishedResult(bool blReload) private async Task UpdateFinishedSub(bool blReload)
{
RxApp.MainThreadScheduler.Schedule(blReload, (scheduler, blReload) =>
{
_ = UpdateFinishedResult(blReload);
return Disposable.Empty;
});
}
public async Task UpdateFinishedResult(bool blReload)
{ {
if (blReload) if (blReload)
{ {
Locator.Current.GetService<MainWindowViewModel>()?.Reload(); AppEvents.ReloadRequested.Publish();
} }
else else
{ {
Locator.Current.GetService<MainWindowViewModel>()?.CloseCore(); await CoreManager.Instance.CoreStop();
} }
} }
private void UpgradeN() private async Task UpgradeN()
{ {
try try
{ {
@@ -221,16 +240,23 @@ public class CheckUpdateViewModel : MyReactiveObject
{ {
return; return;
} }
if (!Utils.UpgradeAppExists(out _)) if (!Utils.UpgradeAppExists(out var upgradeFileName))
{ {
UpdateView(_v2rayN, ResUI.UpgradeAppNotExistTip); await UpdateView(_v2rayN, ResUI.UpgradeAppNotExistTip);
NoticeManager.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip);
Logging.SaveLog("UpgradeApp does not exist");
return; return;
} }
Locator.Current.GetService<MainWindowViewModel>()?.UpgradeApp(fileName);
var id = ProcUtils.ProcessStart(upgradeFileName, fileName, Utils.StartupPath());
if (id > 0)
{
await AppManager.Instance.AppExitAsync(true);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
UpdateView(_v2rayN, ex.Message); await UpdateView(_v2rayN, ex.Message);
} }
} }
@@ -274,14 +300,14 @@ public class CheckUpdateViewModel : MyReactiveObject
if (Utils.IsNonWindows()) if (Utils.IsNonWindows())
{ {
var filesList = (new DirectoryInfo(toPath)).GetFiles().Select(u => u.FullName).ToList(); var filesList = new DirectoryInfo(toPath).GetFiles().Select(u => u.FullName).ToList();
foreach (var file in filesList) foreach (var file in filesList)
{ {
await Utils.SetLinuxChmod(Path.Combine(toPath, item.CoreType.ToLower())); await Utils.SetLinuxChmod(Path.Combine(toPath, item.CoreType.ToLower()));
} }
} }
UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully); await UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully);
if (File.Exists(fileName)) if (File.Exists(fileName))
{ {
@@ -290,23 +316,28 @@ public class CheckUpdateViewModel : MyReactiveObject
} }
} }
private void UpdateView(string coreType, string msg) private async Task UpdateView(string coreType, string msg)
{ {
var item = new CheckUpdateModel() var item = new CheckUpdateModel()
{ {
CoreType = coreType, CoreType = coreType,
Remarks = msg, Remarks = msg,
}; };
_updateView?.Invoke(EViewAction.DispatcherCheckUpdate, item);
RxApp.MainThreadScheduler.Schedule(item, (scheduler, model) =>
{
_ = UpdateViewResult(model);
return Disposable.Empty;
});
} }
public void UpdateViewResult(CheckUpdateModel model) public async Task UpdateViewResult(CheckUpdateModel model)
{ {
var found = _checkUpdateModel.FirstOrDefault(t => t.CoreType == model.CoreType); var found = CheckUpdateModels.FirstOrDefault(t => t.CoreType == model.CoreType);
if (found == null) if (found == null)
{
return; return;
var itemCopy = JsonUtils.DeepCopy(found); }
itemCopy.Remarks = model.Remarks; found.Remarks = model.Remarks;
_checkUpdateModel.Replace(found, itemCopy);
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
@@ -9,8 +10,7 @@ namespace ServiceLib.ViewModels;
public class ClashConnectionsViewModel : MyReactiveObject public class ClashConnectionsViewModel : MyReactiveObject
{ {
private IObservableCollection<ClashConnectionModel> _connectionItems = new ObservableCollectionExtended<ClashConnectionModel>(); public IObservableCollection<ClashConnectionModel> ConnectionItems { get; } = new ObservableCollectionExtended<ClashConnectionModel>();
public IObservableCollection<ClashConnectionModel> ConnectionItems => _connectionItems;
[Reactive] [Reactive]
public ClashConnectionModel SelectedSource { get; set; } public ClashConnectionModel SelectedSource { get; set; }
@@ -64,12 +64,16 @@ public class ClashConnectionsViewModel : MyReactiveObject
return; return;
} }
_ = _updateView?.Invoke(EViewAction.DispatcherRefreshConnections, ret?.connections); RxApp.MainThreadScheduler.Schedule(ret?.connections, (scheduler, model) =>
{
_ = RefreshConnections(model);
return Disposable.Empty;
});
} }
public void RefreshConnections(List<ConnectionItem>? connections) public async Task RefreshConnections(List<ConnectionItem>? connections)
{ {
_connectionItems.Clear(); ConnectionItems.Clear();
var dtNow = DateTime.Now; var dtNow = DateTime.Now;
var lstModel = new List<ClashConnectionModel>(); var lstModel = new List<ClashConnectionModel>();
@@ -99,7 +103,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
return; return;
} }
_connectionItems.AddRange(lstModel); ConnectionItems.AddRange(lstModel);
} }
public async Task ClashConnectionClose(bool all) public async Task ClashConnectionClose(bool all)
@@ -116,7 +120,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
} }
else else
{ {
_connectionItems.Clear(); ConnectionItems.Clear();
} }
await ClashApiManager.Instance.ClashConnectionClose(id); await ClashApiManager.Instance.ClashConnectionClose(id);
await GetClashConnections(); await GetClashConnections();

View File

@@ -1,4 +1,6 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
@@ -15,11 +17,8 @@ public class ClashProxiesViewModel : MyReactiveObject
private Dictionary<string, ProvidersItem>? _providers; private Dictionary<string, ProvidersItem>? _providers;
private readonly int _delayTimeout = 99999999; private readonly int _delayTimeout = 99999999;
private IObservableCollection<ClashProxyModel> _proxyGroups = new ObservableCollectionExtended<ClashProxyModel>(); public IObservableCollection<ClashProxyModel> ProxyGroups { get; } = new ObservableCollectionExtended<ClashProxyModel>();
private IObservableCollection<ClashProxyModel> _proxyDetails = new ObservableCollectionExtended<ClashProxyModel>(); public IObservableCollection<ClashProxyModel> ProxyDetails { get; } = new ObservableCollectionExtended<ClashProxyModel>();
public IObservableCollection<ClashProxyModel> ProxyGroups => _proxyGroups;
public IObservableCollection<ClashProxyModel> ProxyDetails => _proxyDetails;
[Reactive] [Reactive]
public ClashProxyModel SelectedGroup { get; set; } public ClashProxyModel SelectedGroup { get; set; }
@@ -70,6 +69,8 @@ public class ClashProxiesViewModel : MyReactiveObject
SortingSelected = _config.ClashUIItem.ProxiesSorting; SortingSelected = _config.ClashUIItem.ProxiesSorting;
RuleModeSelected = (int)_config.ClashUIItem.RuleMode; RuleModeSelected = (int)_config.ClashUIItem.RuleMode;
#region WhenAnyValue && ReactiveCommand
this.WhenAnyValue( this.WhenAnyValue(
x => x.SelectedGroup, x => x.SelectedGroup,
y => y != null && y.Name.IsNotEmpty()) y => y != null && y.Name.IsNotEmpty())
@@ -90,6 +91,17 @@ public class ClashProxiesViewModel : MyReactiveObject
y => y == true) y => y == true)
.Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; }); .Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; });
#endregion WhenAnyValue && ReactiveCommand
#region AppEvents
AppEvents.ProxiesReloadRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await ProxiesReload());
#endregion AppEvents
_ = Init(); _ = Init();
} }
@@ -168,11 +180,11 @@ public class ClashProxiesViewModel : MyReactiveObject
if (refreshUI) if (refreshUI)
{ {
_updateView?.Invoke(EViewAction.DispatcherRefreshProxyGroups, null); RxApp.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups());
} }
} }
public void RefreshProxyGroups() public async Task RefreshProxyGroups()
{ {
if (_proxies == null) if (_proxies == null)
{ {
@@ -180,7 +192,7 @@ public class ClashProxiesViewModel : MyReactiveObject
} }
var selectedName = SelectedGroup?.Name; var selectedName = SelectedGroup?.Name;
_proxyGroups.Clear(); ProxyGroups.Clear();
var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups(); var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups();
if (proxyGroups != null && proxyGroups.Count > 0) if (proxyGroups != null && proxyGroups.Count > 0)
@@ -196,7 +208,7 @@ public class ClashProxiesViewModel : MyReactiveObject
{ {
continue; continue;
} }
_proxyGroups.Add(new ClashProxyModel() ProxyGroups.Add(new ClashProxyModel()
{ {
Now = item.now, Now = item.now,
Name = item.name, Name = item.name,
@@ -212,12 +224,12 @@ public class ClashProxiesViewModel : MyReactiveObject
{ {
continue; continue;
} }
var item = _proxyGroups.FirstOrDefault(t => t.Name == kv.Key); var item = ProxyGroups.FirstOrDefault(t => t.Name == kv.Key);
if (item != null && item.Name.IsNotEmpty()) if (item != null && item.Name.IsNotEmpty())
{ {
continue; continue;
} }
_proxyGroups.Add(new ClashProxyModel() ProxyGroups.Add(new ClashProxyModel()
{ {
Now = kv.Value.now, Now = kv.Value.now,
Name = kv.Key, Name = kv.Key,
@@ -225,15 +237,15 @@ public class ClashProxiesViewModel : MyReactiveObject
}); });
} }
if (_proxyGroups != null && _proxyGroups.Count > 0) if (ProxyGroups != null && ProxyGroups.Count > 0)
{ {
if (selectedName != null && _proxyGroups.Any(t => t.Name == selectedName)) if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName))
{ {
SelectedGroup = _proxyGroups.FirstOrDefault(t => t.Name == selectedName); SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName);
} }
else else
{ {
SelectedGroup = _proxyGroups.First(); SelectedGroup = ProxyGroups.First();
} }
} }
else else
@@ -244,7 +256,7 @@ public class ClashProxiesViewModel : MyReactiveObject
private void RefreshProxyDetails(bool c) private void RefreshProxyDetails(bool c)
{ {
_proxyDetails.Clear(); ProxyDetails.Clear();
if (!c) if (!c)
{ {
return; return;
@@ -297,7 +309,7 @@ public class ClashProxiesViewModel : MyReactiveObject
default: default:
break; break;
} }
_proxyDetails.AddRange(lstDetails); ProxyDetails.AddRange(lstDetails);
} }
private ProxiesItem? TryGetProxy(string name) private ProxiesItem? TryGetProxy(string name)
@@ -359,12 +371,12 @@ public class ClashProxiesViewModel : MyReactiveObject
await ClashApiManager.Instance.ClashSetActiveProxy(name, nameNode); await ClashApiManager.Instance.ClashSetActiveProxy(name, nameNode);
selectedProxy.now = nameNode; selectedProxy.now = nameNode;
var group = _proxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name); var group = ProxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name);
if (group != null) if (group != null)
{ {
group.Now = nameNode; group.Now = nameNode;
var group2 = JsonUtils.DeepCopy(group); var group2 = JsonUtils.DeepCopy(group);
_proxyGroups.Replace(group, group2); ProxyGroups.Replace(group, group2);
SelectedGroup = group2; SelectedGroup = group2;
} }
@@ -373,22 +385,26 @@ public class ClashProxiesViewModel : MyReactiveObject
private async Task ProxiesDelayTest(bool blAll = true) private async Task ProxiesDelayTest(bool blAll = true)
{ {
ClashApiManager.Instance.ClashProxiesDelayTest(blAll, _proxyDetails.ToList(), (item, result) => ClashApiManager.Instance.ClashProxiesDelayTest(blAll, ProxyDetails.ToList(), async (item, result) =>
{ {
if (item == null || result.IsNullOrEmpty()) if (item == null || result.IsNullOrEmpty())
{ {
return; return;
} }
_updateView?.Invoke(EViewAction.DispatcherProxiesDelayTest, new SpeedTestResult() { IndexId = item.Name, Delay = result }); var model = new SpeedTestResult() { IndexId = item.Name, Delay = result };
RxApp.MainThreadScheduler.Schedule(model, (scheduler, model) =>
{
_ = ProxiesDelayTestResult(model);
return Disposable.Empty;
});
}); });
await Task.CompletedTask; await Task.CompletedTask;
} }
public void ProxiesDelayTestResult(SpeedTestResult result) public async Task ProxiesDelayTestResult(SpeedTestResult result)
{ {
//UpdateHandler(false, $"{item.name}={result}"); var detail = ProxyDetails.FirstOrDefault(it => it.Name == result.IndexId);
var detail = _proxyDetails.FirstOrDefault(it => it.Name == result.IndexId);
if (detail == null) if (detail == null)
{ {
return; return;
@@ -410,7 +426,6 @@ public class ClashProxiesViewModel : MyReactiveObject
detail.Delay = _delayTimeout; detail.Delay = _delayTimeout;
detail.DelayName = string.Empty; detail.DelayName = string.Empty;
} }
_proxyDetails.Replace(detail, JsonUtils.DeepCopy(detail));
} }
#endregion proxy function #endregion proxy function

View File

@@ -1,7 +1,8 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
@@ -183,7 +184,7 @@ public class MainWindowViewModel : MyReactiveObject
}); });
RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () => RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await RebootAsAdmin(); await AppManager.Instance.RebootAsAdmin();
}); });
ClearServerStatisticsCmd = ReactiveCommand.CreateFromTask(async () => ClearServerStatisticsCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -216,6 +217,30 @@ public class MainWindowViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
#region AppEvents
AppEvents.ReloadRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await Reload());
AppEvents.AddServerViaScanRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaScanAsync());
AppEvents.AddServerViaClipboardRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaClipboardAsync(null));
AppEvents.SubscriptionsUpdateRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
#endregion AppEvents
_ = Init(); _ = Init();
} }
@@ -223,7 +248,7 @@ public class MainWindowViewModel : MyReactiveObject
{ {
_config.UiItem.ShowInTaskbar = true; _config.UiItem.ShowInTaskbar = true;
await ConfigHandler.InitBuiltinRouting(_config); //await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinDNS(_config); await ConfigHandler.InitBuiltinDNS(_config);
await ConfigHandler.InitBuiltinFullConfigTemplate(_config); await ConfigHandler.InitBuiltinFullConfigTemplate(_config);
await ProfileExManager.Instance.Init(); await ProfileExManager.Instance.Init();
@@ -234,18 +259,18 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler); await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler);
} }
await RefreshServers();
BlReloadEnabled = true; BlReloadEnabled = true;
await Reload(); await Reload();
await AutoHideStartup(); await AutoHideStartup();
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu();
} }
#endregion Init #endregion Init
#region Actions #region Actions
private void UpdateHandler(bool notify, string msg) private async Task UpdateHandler(bool notify, string msg)
{ {
NoticeManager.Instance.SendMessage(msg); NoticeManager.Instance.SendMessage(msg);
if (notify) if (notify)
@@ -254,110 +279,47 @@ public class MainWindowViewModel : MyReactiveObject
} }
} }
private void UpdateTaskHandler(bool success, string msg) private async Task UpdateTaskHandler(bool success, string msg)
{ {
NoticeManager.Instance.SendMessageEx(msg); NoticeManager.Instance.SendMessageEx(msg);
if (success) if (success)
{ {
var indexIdOld = _config.IndexId; var indexIdOld = _config.IndexId;
RefreshServers(); await RefreshServers();
if (indexIdOld != _config.IndexId) if (indexIdOld != _config.IndexId)
{ {
_ = Reload(); await Reload();
} }
if (_config.UiItem.EnableAutoAdjustMainLvColWidth) if (_config.UiItem.EnableAutoAdjustMainLvColWidth)
{ {
_updateView?.Invoke(EViewAction.AdjustMainLvColWidth, null); AppEvents.AdjustMainLvColWidthRequested.Publish();
} }
} }
} }
private void UpdateStatisticsHandler(ServerSpeedItem update) private async Task UpdateStatisticsHandler(ServerSpeedItem update)
{ {
if (!_config.UiItem.ShowInTaskbar) if (!_config.UiItem.ShowInTaskbar)
{ {
return; return;
} }
_updateView?.Invoke(EViewAction.DispatcherStatistics, update); AppEvents.DispatcherStatisticsRequested.Publish(update);
}
public void SetStatisticsResult(ServerSpeedItem update)
{
if (_config.GuiItem.DisplayRealTimeSpeed)
{
Locator.Current.GetService<StatusBarViewModel>()?.UpdateStatistics(update);
}
if (_config.GuiItem.EnableStatistics && (update.ProxyUp + update.ProxyDown) > 0 && DateTime.Now.Second % 9 == 0)
{
Locator.Current.GetService<ProfilesViewModel>()?.UpdateStatistics(update);
}
}
public async Task MyAppExitAsync(bool blWindowsShutDown)
{
try
{
Logging.SaveLog("MyAppExitAsync Begin");
await SysProxyHandler.UpdateSysProxy(_config, true);
MessageBus.Current.SendMessage("", EMsgCommand.AppExit.ToString());
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
await StatisticsManager.Instance.SaveTo();
await CoreManager.Instance.CoreStop();
StatisticsManager.Instance.Close();
Logging.SaveLog("MyAppExitAsync End");
}
catch { }
finally
{
if (!blWindowsShutDown)
{
_updateView?.Invoke(EViewAction.Shutdown, false);
}
}
}
public async Task UpgradeApp(string arg)
{
if (!Utils.UpgradeAppExists(out var upgradeFileName))
{
NoticeManager.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip);
Logging.SaveLog("UpgradeApp does not exist");
return;
}
var id = ProcUtils.ProcessStart(upgradeFileName, arg, Utils.StartupPath());
if (id > 0)
{
await MyAppExitAsync(false);
}
}
public void ShowHideWindow(bool? blShow)
{
_updateView?.Invoke(EViewAction.ShowHideWindow, blShow);
}
public void Shutdown(bool byUser)
{
_updateView?.Invoke(EViewAction.Shutdown, byUser);
} }
#endregion Actions #endregion Actions
#region Servers && Groups #region Servers && Groups
private void RefreshServers() private async Task RefreshServers()
{ {
MessageBus.Current.SendMessage("", EMsgCommand.RefreshProfiles.ToString()); AppEvents.ProfilesRefreshRequested.Publish();
await Task.Delay(200);
} }
private void RefreshSubscriptions() private void RefreshSubscriptions()
{ {
Locator.Current.GetService<ProfilesViewModel>()?.RefreshSubscriptions(); AppEvents.SubscriptionsRefreshRequested.Publish();
} }
#endregion Servers && Groups #endregion Servers && Groups
@@ -384,7 +346,7 @@ public class MainWindowViewModel : MyReactiveObject
} }
if (ret == true) if (ret == true)
{ {
RefreshServers(); await RefreshServers();
if (item.IndexId == _config.IndexId) if (item.IndexId == _config.IndexId)
{ {
await Reload(); await Reload();
@@ -399,11 +361,11 @@ public class MainWindowViewModel : MyReactiveObject
await _updateView?.Invoke(EViewAction.AddServerViaClipboard, null); await _updateView?.Invoke(EViewAction.AddServerViaClipboard, null);
return; return;
} }
int ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false); var ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false);
if (ret > 0) if (ret > 0)
{ {
RefreshSubscriptions(); RefreshSubscriptions();
RefreshServers(); await RefreshServers();
NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret)); NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret));
} }
else else
@@ -449,11 +411,11 @@ public class MainWindowViewModel : MyReactiveObject
} }
else else
{ {
int ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false); var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false);
if (ret > 0) if (ret > 0)
{ {
RefreshSubscriptions(); RefreshSubscriptions();
RefreshServers(); await RefreshServers();
NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan); NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan);
} }
else else
@@ -477,7 +439,7 @@ public class MainWindowViewModel : MyReactiveObject
public async Task UpdateSubscriptionProcess(string subId, bool blProxy) public async Task UpdateSubscriptionProcess(string subId, bool blProxy)
{ {
await SubscriptionHandler.UpdateProcess(_config, subId, blProxy, UpdateTaskHandler); await Task.Run(async () => await SubscriptionHandler.UpdateProcess(_config, subId, blProxy, UpdateTaskHandler));
} }
#endregion Subscription #endregion Subscription
@@ -489,7 +451,7 @@ public class MainWindowViewModel : MyReactiveObject
var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null); var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null);
if (ret == true) if (ret == true)
{ {
Locator.Current.GetService<StatusBarViewModel>()?.InboundDisplayStatus(); AppEvents.InboundDisplayRequested.Publish();
await Reload(); await Reload();
} }
} }
@@ -500,7 +462,7 @@ public class MainWindowViewModel : MyReactiveObject
if (ret == true) if (ret == true)
{ {
await ConfigHandler.InitBuiltinRouting(_config); await ConfigHandler.InitBuiltinRouting(_config);
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu(); AppEvents.RoutingsMenuRefreshRequested.Publish();
await Reload(); await Reload();
} }
} }
@@ -523,16 +485,10 @@ public class MainWindowViewModel : MyReactiveObject
} }
} }
public async Task RebootAsAdmin()
{
ProcUtils.RebootAsAdmin();
await MyAppExitAsync(false);
}
private async Task ClearServerStatistics() private async Task ClearServerStatistics()
{ {
await StatisticsManager.Instance.ClearAllServerStatistics(); await StatisticsManager.Instance.ClearAllServerStatistics();
RefreshServers(); await RefreshServers();
} }
private async Task OpenTheFileLocation() private async Task OpenTheFileLocation()
@@ -544,7 +500,7 @@ public class MainWindowViewModel : MyReactiveObject
} }
else if (Utils.IsLinux()) else if (Utils.IsLinux())
{ {
ProcUtils.ProcessStart("nautilus", path); ProcUtils.ProcessStart("xdg-open", path);
} }
else if (Utils.IsOSX()) else if (Utils.IsOSX())
{ {
@@ -574,9 +530,15 @@ public class MainWindowViewModel : MyReactiveObject
await SysProxyHandler.UpdateSysProxy(_config, false); await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000); await Task.Delay(1000);
}); });
Locator.Current.GetService<StatusBarViewModel>()?.TestServerAvailability(); AppEvents.TestServerRequested.Publish();
_updateView?.Invoke(EViewAction.DispatcherReload, null); var showClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (showClashUI)
{
AppEvents.ProxiesReloadRequested.Publish();
}
RxApp.MainThreadScheduler.Schedule(() => ReloadResult(showClashUI));
BlReloadEnabled = true; BlReloadEnabled = true;
if (_hasNextReloadJob) if (_hasNextReloadJob)
@@ -586,17 +548,11 @@ public class MainWindowViewModel : MyReactiveObject
} }
} }
public void ReloadResult() private void ReloadResult(bool showClashUI)
{ {
// BlReloadEnabled = true; // BlReloadEnabled = true;
//Locator.Current.GetService<StatusBarViewModel>()?.ChangeSystemProxyAsync(_config.systemProxyItem.sysProxyType, false); ShowClashUI = showClashUI;
ShowClashUI = _config.IsRunningCore(ECoreType.sing_box); TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
if (ShowClashUI)
{
Locator.Current.GetService<ClashProxiesViewModel>()?.ProxiesReload();
}
else
{ TabMainSelectedIndex = 0; }
} }
private async Task LoadCore() private async Task LoadCore()
@@ -605,17 +561,11 @@ public class MainWindowViewModel : MyReactiveObject
await CoreManager.Instance.LoadCore(node); await CoreManager.Instance.LoadCore(node);
} }
public async Task CloseCore()
{
await ConfigHandler.SaveConfig(_config);
await CoreManager.Instance.CoreStop();
}
private async Task AutoHideStartup() private async Task AutoHideStartup()
{ {
if (_config.UiItem.AutoHideStartup) if (_config.UiItem.AutoHideStartup)
{ {
ShowHideWindow(false); AppEvents.ShowHideWindowRequested.Publish(false);
} }
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -628,10 +578,10 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await ConfigHandler.ApplyRegionalPreset(_config, type); await ConfigHandler.ApplyRegionalPreset(_config, type);
await ConfigHandler.InitRouting(_config); await ConfigHandler.InitRouting(_config);
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu(); AppEvents.RoutingsMenuRefreshRequested.Publish();
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
await new UpdateService().UpdateGeoFileAll(_config, UpdateHandler); await new UpdateService().UpdateGeoFileAll(_config, UpdateTaskHandler);
await Reload(); await Reload();
} }

View File

@@ -1,4 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Reactive.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
@@ -7,10 +9,10 @@ namespace ServiceLib.ViewModels;
public class MsgViewModel : MyReactiveObject public class MsgViewModel : MyReactiveObject
{ {
private ConcurrentQueue<string> _queueMsg = new(); private readonly ConcurrentQueue<string> _queueMsg = new();
private int _numMaxMsg = 500; private volatile bool _lastMsgFilterNotAvailable;
private bool _lastMsgFilterNotAvailable; private int _showLock = 0; // 0 = unlocked, 1 = locked
private bool _blLockShow = false; public int NumMaxMsg { get; } = 500;
[Reactive] [Reactive]
public string MsgFilter { get; set; } public string MsgFilter { get; set; }
@@ -32,48 +34,52 @@ public class MsgViewModel : MyReactiveObject
this.WhenAnyValue( this.WhenAnyValue(
x => x.AutoRefresh, x => x.AutoRefresh,
y => y == true) y => y == true)
.Subscribe(c => { _config.MsgUIItem.AutoRefresh = AutoRefresh; }); .Subscribe(c => _config.MsgUIItem.AutoRefresh = AutoRefresh);
MessageBus.Current.Listen<string>(EMsgCommand.SendMsgView.ToString()).Subscribe(OnNext); AppEvents.SendMsgViewRequested
} .AsObservable()
//.ObserveOn(RxApp.MainThreadScheduler)
private async void OnNext(string x) .Subscribe(content => _ = AppendQueueMsg(content));
{
await AppendQueueMsg(x);
} }
private async Task AppendQueueMsg(string msg) private async Task AppendQueueMsg(string msg)
{ {
//if (msg == Global.CommandClearMsg)
//{
// ClearMsg();
// return;
//}
if (AutoRefresh == false) if (AutoRefresh == false)
{ {
return; return;
} }
_ = EnqueueQueueMsg(msg);
if (_blLockShow) EnqueueQueueMsg(msg);
{
return;
}
if (!_config.UiItem.ShowInTaskbar) if (!_config.UiItem.ShowInTaskbar)
{ {
return; return;
} }
_blLockShow = true; if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
{
await Task.Delay(500); return;
var txt = string.Join("", _queueMsg.ToArray());
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, txt);
_blLockShow = false;
} }
private async Task EnqueueQueueMsg(string msg) try
{
await Task.Delay(500).ConfigureAwait(false);
var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var line))
{
sb.Append(line);
}
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString());
}
finally
{
Interlocked.Exchange(ref _showLock, 0);
}
}
private void EnqueueQueueMsg(string msg)
{ {
//filter msg //filter msg
if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable) if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable)
@@ -92,26 +98,17 @@ public class MsgViewModel : MyReactiveObject
} }
} }
//Enqueue
if (_queueMsg.Count > _numMaxMsg)
{
for (int k = 0; k < _queueMsg.Count - _numMaxMsg; k++)
{
_queueMsg.TryDequeue(out _);
}
}
_queueMsg.Enqueue(msg); _queueMsg.Enqueue(msg);
if (!msg.EndsWith(Environment.NewLine)) if (!msg.EndsWith(Environment.NewLine))
{ {
_queueMsg.Enqueue(Environment.NewLine); _queueMsg.Enqueue(Environment.NewLine);
} }
await Task.CompletedTask;
} }
public void ClearMsg() //public void ClearMsg()
{ //{
_queueMsg.Clear(); // _queueMsg.Clear();
} //}
private void DoMsgFilter() private void DoMsgFilter()
{ {

View File

@@ -0,0 +1,352 @@
using System.Reactive.Linq;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.ViewModels;
public class ProfilesSelectViewModel : MyReactiveObject
{
#region private prop
private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new();
private string _subIndexId = string.Empty;
// ConfigType filter state: default include-mode with all types selected
private List<EConfigType> _filterConfigTypes = new();
private bool _filterExclude = false;
#endregion private prop
#region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive]
public ProfileItemModel SelectedProfile { get; set; }
public IList<ProfileItemModel> SelectedProfiles { get; set; }
[Reactive]
public SubItem SelectedSub { get; set; }
[Reactive]
public string ServerFilter { get; set; }
// Include/Exclude filter for ConfigType
public List<EConfigType> FilterConfigTypes
{
get => _filterConfigTypes;
set => this.RaiseAndSetIfChanged(ref _filterConfigTypes, value);
}
[Reactive]
public bool FilterExclude
{
get => _filterExclude;
set => this.RaiseAndSetIfChanged(ref _filterExclude, value);
}
#endregion ObservableCollection
#region Init
public ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{
_config = AppManager.Instance.Config;
_updateView = updateView;
_subIndexId = _config.SubIndexId ?? string.Empty;
#region WhenAnyValue && ReactiveCommand
this.WhenAnyValue(
x => x.SelectedSub,
y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id)
.Subscribe(async c => await SubSelectedChangedAsync(c));
this.WhenAnyValue(
x => x.ServerFilter,
y => y != null && _serverFilter != y)
.Subscribe(async c => await ServerFilterChanged(c));
// React to ConfigType filter changes
this.WhenAnyValue(x => x.FilterExclude)
.Skip(1)
.Subscribe(async _ => await RefreshServersBiz());
this.WhenAnyValue(x => x.FilterConfigTypes)
.Skip(1)
.Subscribe(async _ => await RefreshServersBiz());
#endregion WhenAnyValue && ReactiveCommand
_ = Init();
}
private async Task Init()
{
SelectedProfile = new();
SelectedSub = new();
// Default: include mode with all ConfigTypes selected
try
{
FilterExclude = false;
FilterConfigTypes = Enum.GetValues(typeof(EConfigType)).Cast<EConfigType>().ToList();
}
catch
{
FilterConfigTypes = new();
}
await RefreshSubscriptions();
await RefreshServers();
}
#endregion Init
#region Actions
public bool CanOk()
{
return SelectedProfile != null && !SelectedProfile.IndexId.IsNullOrEmpty();
}
public bool SelectFinish()
{
if (!CanOk())
{
return false;
}
_updateView?.Invoke(EViewAction.CloseWindow, null);
return true;
}
#endregion Actions
#region Servers && Groups
private async Task SubSelectedChangedAsync(bool c)
{
if (!c)
{
return;
}
_subIndexId = SelectedSub?.Id;
await RefreshServers();
await _updateView?.Invoke(EViewAction.ProfilesFocus, null);
}
private async Task ServerFilterChanged(bool c)
{
if (!c)
{
return;
}
_serverFilter = ServerFilter;
if (_serverFilter.IsNullOrEmpty())
{
await RefreshServers();
}
}
public async Task RefreshServers()
{
await RefreshServersBiz();
}
private async Task RefreshServersBiz()
{
var lstModel = await GetProfileItemsEx(_subIndexId, _serverFilter);
ProfileItems.Clear();
ProfileItems.AddRange(lstModel);
if (lstModel.Count > 0)
{
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
if (selected != null)
{
SelectedProfile = selected;
}
else
{
SelectedProfile = lstModel.First();
}
}
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
}
public async Task RefreshSubscriptions()
{
SubItems.Clear();
SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
foreach (var item in await AppManager.Instance.SubItems())
{
SubItems.Add(item);
}
if (_subIndexId != null && SubItems.FirstOrDefault(t => t.Id == _subIndexId) != null)
{
SelectedSub = SubItems.FirstOrDefault(t => t.Id == _subIndexId);
}
else
{
SelectedSub = SubItems.First();
}
}
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{
var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter);
lstModel = (from t in lstModel
select new ProfileItemModel
{
IndexId = t.IndexId,
ConfigType = t.ConfigType,
Remarks = t.Remarks,
Address = t.Address,
Port = t.Port,
Security = t.Security,
Network = t.Network,
StreamSecurity = t.StreamSecurity,
Subid = t.Subid,
SubRemarks = t.SubRemarks,
IsActive = t.IndexId == _config.IndexId,
}).OrderBy(t => t.Sort).ToList();
// Apply ConfigType filter (include or exclude)
if (FilterConfigTypes != null && FilterConfigTypes.Count > 0)
{
if (FilterExclude)
{
lstModel = lstModel.Where(t => !FilterConfigTypes.Contains(t.ConfigType)).ToList();
}
else
{
lstModel = lstModel.Where(t => FilterConfigTypes.Contains(t.ConfigType)).ToList();
}
}
return lstModel;
}
public async Task<ProfileItem?> GetProfileItem()
{
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
{
return null;
}
var indexId = SelectedProfile.IndexId;
var item = await AppManager.Instance.GetProfileItem(indexId);
if (item is null)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return null;
}
return item;
}
public async Task<List<ProfileItem>?> GetProfileItems()
{
if (SelectedProfiles == null || SelectedProfiles.Count == 0)
{
return null;
}
var lst = new List<ProfileItem>();
foreach (var sp in SelectedProfiles)
{
if (string.IsNullOrEmpty(sp?.IndexId))
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(sp.IndexId);
if (item != null)
{
lst.Add(item);
}
}
if (lst.Count == 0)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return null;
}
return lst;
}
public void SortServer(string colName)
{
if (colName.IsNullOrEmpty())
{
return;
}
var prop = typeof(ProfileItemModel).GetProperty(colName);
if (prop == null)
{
return;
}
_dicHeaderSort.TryAdd(colName, true);
var asc = _dicHeaderSort[colName];
var comparer = Comparer<object?>.Create((a, b) =>
{
if (ReferenceEquals(a, b))
{
return 0;
}
if (a is null)
{
return -1;
}
if (b is null)
{
return 1;
}
if (a.GetType() == b.GetType() && a is IComparable ca)
{
return ca.CompareTo(b);
}
return string.Compare(a.ToString(), b.ToString(), StringComparison.OrdinalIgnoreCase);
});
object? KeySelector(ProfileItemModel x)
{
return prop.GetValue(x);
}
IEnumerable<ProfileItemModel> sorted = asc
? ProfileItems.OrderBy(KeySelector, comparer)
: ProfileItems.OrderByDescending(KeySelector, comparer);
var list = sorted.ToList();
ProfileItems.Clear();
ProfileItems.AddRange(list);
_dicHeaderSort[colName] = !asc;
return;
}
#endregion Servers && Groups
#region Public API
// External setter for ConfigType filter
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
{
FilterConfigTypes = types?.Distinct().ToList() ?? new List<EConfigType>();
FilterExclude = exclude;
}
#endregion Public API
}

View File

@@ -1,11 +1,11 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Text; using System.Text;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
@@ -22,13 +22,9 @@ public class ProfilesViewModel : MyReactiveObject
#region ObservableCollection #region ObservableCollection
private IObservableCollection<ProfileItemModel> _profileItems = new ObservableCollectionExtended<ProfileItemModel>(); public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
public IObservableCollection<ProfileItemModel> ProfileItems => _profileItems;
private IObservableCollection<SubItem> _subItems = new ObservableCollectionExtended<SubItem>(); public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
public IObservableCollection<SubItem> SubItems => _subItems;
private IObservableCollection<ComboItem> _servers = new ObservableCollectionExtended<ComboItem>();
[Reactive] [Reactive]
public ProfileItemModel SelectedProfile { get; set; } public ProfileItemModel SelectedProfile { get; set; }
@@ -41,15 +37,9 @@ public class ProfilesViewModel : MyReactiveObject
[Reactive] [Reactive]
public SubItem SelectedMoveToGroup { get; set; } public SubItem SelectedMoveToGroup { get; set; }
[Reactive]
public ComboItem SelectedServer { get; set; }
[Reactive] [Reactive]
public string ServerFilter { get; set; } public string ServerFilter { get; set; }
[Reactive]
public bool BlServers { get; set; }
#endregion ObservableCollection #endregion ObservableCollection
#region Menu #region Menu
@@ -118,15 +108,10 @@ public class ProfilesViewModel : MyReactiveObject
y => y != null && !y.Remarks.IsNullOrEmpty()) y => y != null && !y.Remarks.IsNullOrEmpty())
.Subscribe(async c => await MoveToGroup(c)); .Subscribe(async c => await MoveToGroup(c));
this.WhenAnyValue(
x => x.SelectedServer,
y => y != null && !y.Text.IsNullOrEmpty())
.Subscribe(async c => await ServerSelectedChanged(c));
this.WhenAnyValue( this.WhenAnyValue(
x => x.ServerFilter, x => x.ServerFilter,
y => y != null && _serverFilter != y) y => y != null && _serverFilter != y)
.Subscribe(c => ServerFilterChanged(c)); .Subscribe(async c => await ServerFilterChanged(c));
//servers delete //servers delete
EditServerCmd = ReactiveCommand.CreateFromTask(async () => EditServerCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -247,10 +232,29 @@ public class ProfilesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
if (_updateView != null) #region AppEvents
{
MessageBus.Current.Listen<string>(EMsgCommand.RefreshProfiles.ToString()).Subscribe(OnNext); AppEvents.ProfilesRefreshRequested
} .AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz());
AppEvents.SubscriptionsRefreshRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshSubscriptions());
AppEvents.DispatcherStatisticsRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result));
AppEvents.SetDefaultServerRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async indexId => await SetDefaultServer(indexId));
#endregion AppEvents
_ = Init(); _ = Init();
} }
@@ -260,27 +264,21 @@ public class ProfilesViewModel : MyReactiveObject
SelectedProfile = new(); SelectedProfile = new();
SelectedSub = new(); SelectedSub = new();
SelectedMoveToGroup = new(); SelectedMoveToGroup = new();
SelectedServer = new();
await RefreshSubscriptions(); await RefreshSubscriptions();
RefreshServers(); //await RefreshServers();
} }
#endregion Init #endregion Init
#region Actions #region Actions
private async void OnNext(string x)
{
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
}
private void Reload() private void Reload()
{ {
Locator.Current.GetService<MainWindowViewModel>()?.Reload(); AppEvents.ReloadRequested.Publish();
} }
public void SetSpeedTestResult(SpeedTestResult result) public async Task SetSpeedTestResult(SpeedTestResult result)
{ {
if (result.IndexId.IsNullOrEmpty()) if (result.IndexId.IsNullOrEmpty())
{ {
@@ -288,7 +286,7 @@ public class ProfilesViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(result.Delay); NoticeManager.Instance.Enqueue(result.Delay);
return; return;
} }
var item = _profileItems.FirstOrDefault(it => it.IndexId == result.IndexId); var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId);
if (item == null) if (item == null)
{ {
return; return;
@@ -304,31 +302,26 @@ public class ProfilesViewModel : MyReactiveObject
{ {
item.SpeedVal = result.Speed ?? string.Empty; item.SpeedVal = result.Speed ?? string.Empty;
} }
//_profileItems.Replace(item, JsonUtils.DeepCopy(item));
} }
public void UpdateStatistics(ServerSpeedItem update) public async Task UpdateStatistics(ServerSpeedItem update)
{ {
if (!_config.GuiItem.EnableStatistics
|| (update.ProxyUp + update.ProxyDown) <= 0
|| DateTime.Now.Second % 3 != 0)
{
return;
}
try try
{ {
var item = _profileItems.FirstOrDefault(it => it.IndexId == update.IndexId); var item = ProfileItems.FirstOrDefault(it => it.IndexId == update.IndexId);
if (item != null) if (item != null)
{ {
item.TodayDown = Utils.HumanFy(update.TodayDown); item.TodayDown = Utils.HumanFy(update.TodayDown);
item.TodayUp = Utils.HumanFy(update.TodayUp); item.TodayUp = Utils.HumanFy(update.TodayUp);
item.TotalDown = Utils.HumanFy(update.TotalDown); item.TotalDown = Utils.HumanFy(update.TotalDown);
item.TotalUp = Utils.HumanFy(update.TotalUp); item.TotalUp = Utils.HumanFy(update.TotalUp);
//if (SelectedProfile?.IndexId == item.IndexId)
//{
// var temp = JsonUtils.DeepCopy(item);
// _profileItems.Replace(item, temp);
// SelectedProfile = temp;
//}
//else
//{
// _profileItems.Replace(item, JsonUtils.DeepCopy(item));
//}
} }
} }
catch catch
@@ -336,11 +329,6 @@ public class ProfilesViewModel : MyReactiveObject
} }
} }
public async Task AutofitColumnWidthAsync()
{
await _updateView?.Invoke(EViewAction.AdjustMainLvColWidth, null);
}
#endregion Actions #endregion Actions
#region Servers && Groups #region Servers && Groups
@@ -353,12 +341,12 @@ public class ProfilesViewModel : MyReactiveObject
} }
_config.SubIndexId = SelectedSub?.Id; _config.SubIndexId = SelectedSub?.Id;
RefreshServers(); await RefreshServers();
await _updateView?.Invoke(EViewAction.ProfilesFocus, null); await _updateView?.Invoke(EViewAction.ProfilesFocus, null);
} }
private void ServerFilterChanged(bool c) private async Task ServerFilterChanged(bool c)
{ {
if (!c) if (!c)
{ {
@@ -367,22 +355,24 @@ public class ProfilesViewModel : MyReactiveObject
_serverFilter = ServerFilter; _serverFilter = ServerFilter;
if (_serverFilter.IsNullOrEmpty()) if (_serverFilter.IsNullOrEmpty())
{ {
RefreshServers(); await RefreshServers();
} }
} }
public void RefreshServers() public async Task RefreshServers()
{ {
MessageBus.Current.SendMessage("", EMsgCommand.RefreshProfiles.ToString()); AppEvents.ProfilesRefreshRequested.Publish();
await Task.Delay(200);
} }
public async Task RefreshServersBiz() private async Task RefreshServersBiz()
{ {
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter); var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? []; _lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
_profileItems.Clear(); ProfileItems.Clear();
_profileItems.AddRange(lstModel); ProfileItems.AddRange(lstModel);
if (lstModel.Count > 0) if (lstModel.Count > 0)
{ {
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
@@ -395,25 +385,27 @@ public class ProfilesViewModel : MyReactiveObject
SelectedProfile = lstModel.First(); SelectedProfile = lstModel.First();
} }
} }
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
} }
public async Task RefreshSubscriptions() private async Task RefreshSubscriptions()
{ {
_subItems.Clear(); SubItems.Clear();
_subItems.Add(new SubItem { Remarks = ResUI.AllGroupServers }); SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
foreach (var item in await AppManager.Instance.SubItems()) foreach (var item in await AppManager.Instance.SubItems())
{ {
_subItems.Add(item); SubItems.Add(item);
} }
if (_config.SubIndexId != null && _subItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null) if (_config.SubIndexId != null && SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null)
{ {
SelectedSub = _subItems.FirstOrDefault(t => t.Id == _config.SubIndexId); SelectedSub = SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId);
} }
else else
{ {
SelectedSub = _subItems.First(); SelectedSub = SubItems.First();
} }
} }
@@ -514,7 +506,7 @@ public class ProfilesViewModel : MyReactiveObject
} }
if (ret == true) if (ret == true)
{ {
RefreshServers(); await RefreshServers();
if (item.IndexId == _config.IndexId) if (item.IndexId == _config.IndexId)
{ {
Reload(); Reload();
@@ -537,11 +529,11 @@ public class ProfilesViewModel : MyReactiveObject
await ConfigHandler.RemoveServers(_config, lstSelected); await ConfigHandler.RemoveServers(_config, lstSelected);
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
if (lstSelected.Count == _profileItems.Count) if (lstSelected.Count == ProfileItems.Count)
{ {
_profileItems.Clear(); ProfileItems.Clear();
} }
RefreshServers(); await RefreshServers();
if (exists) if (exists)
{ {
Reload(); Reload();
@@ -553,7 +545,7 @@ public class ProfilesViewModel : MyReactiveObject
var tuple = await ConfigHandler.DedupServerList(_config, _config.SubIndexId); var tuple = await ConfigHandler.DedupServerList(_config, _config.SubIndexId);
if (tuple.Item1 > 0 || tuple.Item2 > 0) if (tuple.Item1 > 0 || tuple.Item2 > 0)
{ {
RefreshServers(); await RefreshServers();
Reload(); Reload();
} }
NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2)); NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2));
@@ -568,7 +560,7 @@ public class ProfilesViewModel : MyReactiveObject
} }
if (await ConfigHandler.CopyServer(_config, lstSelected) == 0) if (await ConfigHandler.CopyServer(_config, lstSelected) == 0)
{ {
RefreshServers(); await RefreshServers();
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
} }
@@ -582,7 +574,7 @@ public class ProfilesViewModel : MyReactiveObject
await SetDefaultServer(SelectedProfile.IndexId); await SetDefaultServer(SelectedProfile.IndexId);
} }
public async Task SetDefaultServer(string? indexId) private async Task SetDefaultServer(string? indexId)
{ {
if (indexId.IsNullOrEmpty()) if (indexId.IsNullOrEmpty())
{ {
@@ -601,24 +593,11 @@ public class ProfilesViewModel : MyReactiveObject
if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0)
{ {
RefreshServers(); await RefreshServers();
Reload(); Reload();
} }
} }
private async Task ServerSelectedChanged(bool c)
{
if (!c)
{
return;
}
if (SelectedServer == null || SelectedServer.ID.IsNullOrEmpty())
{
return;
}
await SetDefaultServer(SelectedServer.ID);
}
public async Task ShareServerAsync() public async Task ShareServerAsync()
{ {
var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId);
@@ -652,7 +631,7 @@ public class ProfilesViewModel : MyReactiveObject
} }
if (ret?.Data?.ToString() == _config.IndexId) if (ret?.Data?.ToString() == _config.IndexId)
{ {
RefreshServers(); await RefreshServers();
Reload(); Reload();
} }
else else
@@ -675,13 +654,13 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
_dicHeaderSort[colName] = !asc; _dicHeaderSort[colName] = !asc;
RefreshServers(); await RefreshServers();
} }
public async Task RemoveInvalidServerResult() public async Task RemoveInvalidServerResult()
{ {
var count = await ConfigHandler.RemoveInvalidServerResult(_config, _config.SubIndexId); var count = await ConfigHandler.RemoveInvalidServerResult(_config, _config.SubIndexId);
RefreshServers(); await RefreshServers();
NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count)); NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count));
} }
@@ -702,7 +681,7 @@ public class ProfilesViewModel : MyReactiveObject
await ConfigHandler.MoveToGroup(_config, lstSelected, SelectedMoveToGroup.Id); await ConfigHandler.MoveToGroup(_config, lstSelected, SelectedMoveToGroup.Id);
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
RefreshServers(); await RefreshServers();
SelectedMoveToGroup = null; SelectedMoveToGroup = null;
SelectedMoveToGroup = new(); SelectedMoveToGroup = new();
} }
@@ -723,18 +702,18 @@ public class ProfilesViewModel : MyReactiveObject
} }
if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0) if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0)
{ {
RefreshServers(); await RefreshServers();
} }
} }
public async Task MoveServerTo(int startIndex, ProfileItemModel targetItem) public async Task MoveServerTo(int startIndex, ProfileItemModel targetItem)
{ {
var targetIndex = _profileItems.IndexOf(targetItem); var targetIndex = ProfileItems.IndexOf(targetItem);
if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex) if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex)
{ {
if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0) if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0)
{ {
RefreshServers(); await RefreshServers();
} }
} }
} }
@@ -743,7 +722,7 @@ public class ProfilesViewModel : MyReactiveObject
{ {
if (actionType == ESpeedActionType.Mixedtest) if (actionType == ESpeedActionType.Mixedtest)
{ {
SelectedProfiles = _profileItems; SelectedProfiles = ProfileItems;
} }
var lstSelected = await GetProfileItems(false); var lstSelected = await GetProfileItems(false);
if (lstSelected == null) if (lstSelected == null)
@@ -751,7 +730,14 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
_speedtestService ??= new SpeedtestService(_config, (SpeedTestResult result) => _updateView?.Invoke(EViewAction.DispatcherSpeedTest, result)); _speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) =>
{
RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) =>
{
_ = SetSpeedTestResult(result);
return Disposable.Empty;
});
});
_speedtestService?.RunLoop(actionType, lstSelected); _speedtestService?.RunLoop(actionType, lstSelected);
} }

View File

@@ -14,8 +14,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
[Reactive] [Reactive]
public RoutingItem SelectedRouting { get; set; } public RoutingItem SelectedRouting { get; set; }
private IObservableCollection<RulesItemModel> _rulesItems = new ObservableCollectionExtended<RulesItemModel>(); public IObservableCollection<RulesItemModel> RulesItems { get; } = new ObservableCollectionExtended<RulesItemModel>();
public IObservableCollection<RulesItemModel> RulesItems => _rulesItems;
[Reactive] [Reactive]
public RulesItemModel SelectedSource { get; set; } public RulesItemModel SelectedSource { get; set; }
@@ -101,7 +100,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
public void RefreshRulesItems() public void RefreshRulesItems()
{ {
_rulesItems.Clear(); RulesItems.Clear();
foreach (var item in _rules) foreach (var item in _rules)
{ {
@@ -118,7 +117,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
Enabled = item.Enabled, Enabled = item.Enabled,
Remarks = item.Remarks, Remarks = item.Remarks,
}; };
_rulesItems.Add(it); RulesItems.Add(it);
} }
} }

View File

@@ -9,8 +9,7 @@ public class RoutingSettingViewModel : MyReactiveObject
{ {
#region Reactive #region Reactive
private IObservableCollection<RoutingItemModel> _routingItems = new ObservableCollectionExtended<RoutingItemModel>(); public IObservableCollection<RoutingItemModel> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItemModel>();
public IObservableCollection<RoutingItemModel> RoutingItems => _routingItems;
[Reactive] [Reactive]
public RoutingItemModel SelectedSource { get; set; } public RoutingItemModel SelectedSource { get; set; }
@@ -82,7 +81,7 @@ public class RoutingSettingViewModel : MyReactiveObject
public async Task RefreshRoutingItems() public async Task RefreshRoutingItems()
{ {
_routingItems.Clear(); RoutingItems.Clear();
var routings = await AppManager.Instance.RoutingItems(); var routings = await AppManager.Instance.RoutingItems();
foreach (var item in routings) foreach (var item in routings)
@@ -98,7 +97,7 @@ public class RoutingSettingViewModel : MyReactiveObject
CustomRulesetPath4Singbox = item.CustomRulesetPath4Singbox, CustomRulesetPath4Singbox = item.CustomRulesetPath4Singbox,
Sort = item.Sort, Sort = item.Sort,
}; };
_routingItems.Add(it); RoutingItems.Add(it);
} }
} }

View File

@@ -1,21 +1,23 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text; using System.Text;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class StatusBarViewModel : MyReactiveObject public class StatusBarViewModel : MyReactiveObject
{ {
private static readonly Lazy<StatusBarViewModel> _instance = new(() => new(null));
public static StatusBarViewModel Instance => _instance.Value;
#region ObservableCollection #region ObservableCollection
private IObservableCollection<RoutingItem> _routingItems = new ObservableCollectionExtended<RoutingItem>(); public IObservableCollection<RoutingItem> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItem>();
public IObservableCollection<RoutingItem> RoutingItems => _routingItems;
private IObservableCollection<ComboItem> _servers = new ObservableCollectionExtended<ComboItem>(); public IObservableCollection<ComboItem> Servers { get; } = new ObservableCollectionExtended<ComboItem>();
public IObservableCollection<ComboItem> Servers => _servers;
[Reactive] [Reactive]
public RoutingItem SelectedRouting { get; set; } public RoutingItem SelectedRouting { get; set; }
@@ -146,17 +148,17 @@ public class StatusBarViewModel : MyReactiveObject
NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () => NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
Locator.Current.GetService<MainWindowViewModel>()?.ShowHideWindow(null); AppEvents.ShowHideWindowRequested.Publish(null);
await Task.CompletedTask; await Task.CompletedTask;
}); });
ShowWindowCmd = ReactiveCommand.CreateFromTask(async () => ShowWindowCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
Locator.Current.GetService<MainWindowViewModel>()?.ShowHideWindow(true); AppEvents.ShowHideWindowRequested.Publish(true);
await Task.CompletedTask; await Task.CompletedTask;
}); });
HideWindowCmd = ReactiveCommand.CreateFromTask(async () => HideWindowCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
Locator.Current.GetService<MainWindowViewModel>()?.ShowHideWindow(false); AppEvents.ShowHideWindowRequested.Publish(false);
await Task.CompletedTask; await Task.CompletedTask;
}); });
@@ -197,15 +199,46 @@ public class StatusBarViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
#region AppEvents
if (updateView != null) if (updateView != null)
{ {
InitUpdateView(updateView); InitUpdateView(updateView);
} }
AppEvents.DispatcherStatisticsRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result));
AppEvents.RoutingsMenuRefreshRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshRoutingsMenu());
AppEvents.TestServerRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await TestServerAvailability());
AppEvents.InboundDisplayRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await InboundDisplayStatus());
AppEvents.SysProxyChangeRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async result => await SetListenerType(result));
#endregion AppEvents
_ = Init(); _ = Init();
} }
private async Task Init() private async Task Init()
{ {
await ConfigHandler.InitBuiltinRouting(_config);
await RefreshRoutingsMenu(); await RefreshRoutingsMenu();
await InboundDisplayStatus(); await InboundDisplayStatus();
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true); await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true);
@@ -216,15 +249,13 @@ public class StatusBarViewModel : MyReactiveObject
_updateView = updateView; _updateView = updateView;
if (_updateView != null) if (_updateView != null)
{ {
MessageBus.Current.Listen<string>(EMsgCommand.RefreshProfiles.ToString()).Subscribe(OnNext); AppEvents.ProfilesRefreshRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz()); //.DisposeWith(_disposables);
} }
} }
private async void OnNext(string x)
{
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
}
private async Task CopyProxyCmdToClipboard() private async Task CopyProxyCmdToClipboard()
{ {
var cmd = Utils.IsWindows() ? "set" : "export"; var cmd = Utils.IsWindows() ? "set" : "export";
@@ -244,26 +275,23 @@ public class StatusBarViewModel : MyReactiveObject
private async Task AddServerViaClipboard() private async Task AddServerViaClipboard()
{ {
var service = Locator.Current.GetService<MainWindowViewModel>(); AppEvents.AddServerViaClipboardRequested.Publish();
if (service != null) await Task.Delay(1000);
await service.AddServerViaClipboardAsync(null);
} }
private async Task AddServerViaScan() private async Task AddServerViaScan()
{ {
var service = Locator.Current.GetService<MainWindowViewModel>(); AppEvents.AddServerViaScanRequested.Publish();
if (service != null) await Task.Delay(1000);
await service.AddServerViaScanAsync();
} }
private async Task UpdateSubscriptionProcess(bool blProxy) private async Task UpdateSubscriptionProcess(bool blProxy)
{ {
var service = Locator.Current.GetService<MainWindowViewModel>(); AppEvents.SubscriptionsUpdateRequested.Publish(blProxy);
if (service != null) await Task.Delay(1000);
await service.UpdateSubscriptionProcess("", blProxy);
} }
public async Task RefreshServersBiz() private async Task RefreshServersBiz()
{ {
await RefreshServersMenu(); await RefreshServersMenu();
@@ -285,7 +313,7 @@ public class StatusBarViewModel : MyReactiveObject
{ {
var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, ""); var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, "");
_servers.Clear(); Servers.Clear();
if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit) if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit)
{ {
BlServers = false; BlServers = false;
@@ -299,7 +327,7 @@ public class StatusBarViewModel : MyReactiveObject
string name = it.GetSummary(); string name = it.GetSummary();
var item = new ComboItem() { ID = it.IndexId, Text = name }; var item = new ComboItem() { ID = it.IndexId, Text = name };
_servers.Add(item); Servers.Add(item);
if (_config.IndexId == it.IndexId) if (_config.IndexId == it.IndexId)
{ {
SelectedServer = item; SelectedServer = item;
@@ -321,7 +349,7 @@ public class StatusBarViewModel : MyReactiveObject
{ {
return; return;
} }
Locator.Current.GetService<ProfilesViewModel>()?.SetDefaultServer(SelectedServer.ID); AppEvents.SetDefaultServerRequested.Publish(SelectedServer.ID);
} }
public async Task TestServerAvailability() public async Task TestServerAvailability()
@@ -332,22 +360,31 @@ public class StatusBarViewModel : MyReactiveObject
return; return;
} }
_updateView?.Invoke(EViewAction.DispatcherServerAvailability, ResUI.Speedtesting); await TestServerAvailabilitySub(ResUI.Speedtesting);
var msg = await Task.Run(ConnectionHandler.RunAvailabilityCheck); var msg = await Task.Run(ConnectionHandler.RunAvailabilityCheck);
NoticeManager.Instance.SendMessageEx(msg); NoticeManager.Instance.SendMessageEx(msg);
_updateView?.Invoke(EViewAction.DispatcherServerAvailability, msg); await TestServerAvailabilitySub(msg);
} }
public void TestServerAvailabilityResult(string msg) private async Task TestServerAvailabilitySub(string msg)
{
RxApp.MainThreadScheduler.Schedule(msg, (scheduler, msg) =>
{
_ = TestServerAvailabilityResult(msg);
return Disposable.Empty;
});
}
public async Task TestServerAvailabilityResult(string msg)
{ {
RunningInfoDisplay = msg; RunningInfoDisplay = msg;
} }
#region System proxy and Routings #region System proxy and Routings
public async Task SetListenerType(ESysProxyType type) private async Task SetListenerType(ESysProxyType type)
{ {
if (_config.SystemProxyItem.SysProxyType == type) if (_config.SystemProxyItem.SysProxyType == type)
{ {
@@ -376,15 +413,15 @@ public class StatusBarViewModel : MyReactiveObject
} }
} }
public async Task RefreshRoutingsMenu() private async Task RefreshRoutingsMenu()
{ {
_routingItems.Clear(); RoutingItems.Clear();
BlRouting = true; BlRouting = true;
var routings = await AppManager.Instance.RoutingItems(); var routings = await AppManager.Instance.RoutingItems();
foreach (var item in routings) foreach (var item in routings)
{ {
_routingItems.Add(item); RoutingItems.Add(item);
if (item.IsActive) if (item.IsActive)
{ {
SelectedRouting = item; SelectedRouting = item;
@@ -413,7 +450,7 @@ public class StatusBarViewModel : MyReactiveObject
if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) if (await ConfigHandler.SetDefaultRouting(_config, item) == 0)
{ {
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting); NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
Locator.Current.GetService<MainWindowViewModel>()?.Reload(); AppEvents.ReloadRequested.Publish();
_updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); _updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null);
} }
} }
@@ -446,7 +483,7 @@ public class StatusBarViewModel : MyReactiveObject
if (Utils.IsWindows()) if (Utils.IsWindows())
{ {
_config.TunModeItem.EnableTun = false; _config.TunModeItem.EnableTun = false;
Locator.Current.GetService<MainWindowViewModel>()?.RebootAsAdmin(); await AppManager.Instance.RebootAsAdmin();
return; return;
} }
else else
@@ -460,7 +497,7 @@ public class StatusBarViewModel : MyReactiveObject
} }
} }
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
Locator.Current.GetService<MainWindowViewModel>()?.Reload(); AppEvents.ReloadRequested.Publish();
} }
private bool AllowEnableTun() private bool AllowEnableTun()
@@ -484,7 +521,7 @@ public class StatusBarViewModel : MyReactiveObject
#region UI #region UI
public async Task InboundDisplayStatus() private async Task InboundDisplayStatus()
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"); sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}");
@@ -509,8 +546,13 @@ public class StatusBarViewModel : MyReactiveObject
await Task.CompletedTask; await Task.CompletedTask;
} }
public void UpdateStatistics(ServerSpeedItem update) public async Task UpdateStatistics(ServerSpeedItem update)
{ {
if (!_config.GuiItem.DisplayRealTimeSpeed)
{
return;
}
try try
{ {
if (_config.IsRunningCore(ECoreType.sing_box)) if (_config.IsRunningCore(ECoreType.sing_box))

View File

@@ -8,8 +8,7 @@ namespace ServiceLib.ViewModels;
public class SubSettingViewModel : MyReactiveObject public class SubSettingViewModel : MyReactiveObject
{ {
private IObservableCollection<SubItem> _subItems = new ObservableCollectionExtended<SubItem>(); public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
public IObservableCollection<SubItem> SubItems => _subItems;
[Reactive] [Reactive]
public SubItem SelectedSource { get; set; } public SubItem SelectedSource { get; set; }
@@ -60,8 +59,8 @@ public class SubSettingViewModel : MyReactiveObject
public async Task RefreshSubItems() public async Task RefreshSubItems()
{ {
_subItems.Clear(); SubItems.Clear();
_subItems.AddRange(await AppManager.Instance.SubItems()); SubItems.AddRange(await AppManager.Instance.SubItems());
} }
public async Task EditSubAsync(bool blNew) public async Task EditSubAsync(bool blNew)

View File

@@ -11,6 +11,7 @@
RequestedThemeVariant="Default"> RequestedThemeVariant="Default">
<Application.Styles> <Application.Styles>
<semi:SemiTheme /> <semi:SemiTheme />
<semi:AvaloniaEditSemiTheme />
<StyleInclude Source="Assets/GlobalStyles.axaml" /> <StyleInclude Source="Assets/GlobalStyles.axaml" />
<StyleInclude Source="avares://Semi.Avalonia.DataGrid/Index.axaml" /> <StyleInclude Source="avares://Semi.Avalonia.DataGrid/Index.axaml" />
<dialogHost:DialogHostStyles /> <dialogHost:DialogHostStyles />

View File

@@ -1,9 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using ServiceLib.Manager;
using Splat;
using v2rayN.Desktop.Common;
using v2rayN.Desktop.Views; using v2rayN.Desktop.Views;
namespace v2rayN.Desktop; namespace v2rayN.Desktop;
@@ -17,9 +14,7 @@ public partial class App : Application
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
var ViewModel = new StatusBarViewModel(null); DataContext = StatusBarViewModel.Instance;
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(StatusBarViewModel));
DataContext = ViewModel;
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
@@ -58,27 +53,15 @@ public partial class App : Application
{ {
if (desktop.MainWindow != null) if (desktop.MainWindow != null)
{ {
var clipboardData = await AvaUtils.GetClipboardData(desktop.MainWindow); AppEvents.AddServerViaClipboardRequested.Publish();
if (clipboardData.IsNullOrEmpty()) await Task.Delay(1000);
{
return;
}
var service = Locator.Current.GetService<MainWindowViewModel>();
if (service != null)
{
_ = service.AddServerViaClipboardAsync(clipboardData);
}
} }
} }
} }
private async void MenuExit_Click(object? sender, EventArgs e) private async void MenuExit_Click(object? sender, EventArgs e)
{ {
var service = Locator.Current.GetService<MainWindowViewModel>(); await AppManager.Instance.AppExitAsync(false);
if (service != null) AppManager.Instance.Shutdown(true);
{
await service.MyAppExitAsync(true);
}
service?.Shutdown(true);
} }
} }

View File

@@ -10,6 +10,7 @@
<x:Double x:Key="IconButtonWidth">32</x:Double> <x:Double x:Key="IconButtonWidth">32</x:Double>
<x:Double x:Key="IconButtonHeight">32</x:Double> <x:Double x:Key="IconButtonHeight">32</x:Double>
<x:Double x:Key="MenuFlyoutMaxHeight">1000</x:Double>
<Thickness x:Key="Margin2">2</Thickness> <Thickness x:Key="Margin2">2</Thickness>
<Thickness x:Key="MarginLr4">4,0</Thickness> <Thickness x:Key="MarginLr4">4,0</Thickness>

View File

@@ -22,4 +22,8 @@
<Style Selector="ScrollViewer"> <Style Selector="ScrollViewer">
<Setter Property="AllowAutoHide" Value="False" /> <Setter Property="AllowAutoHide" Value="False" />
</Style> </Style>
<Style Selector="TabControl">
<Setter Property="Theme" Value="{StaticResource LineTabControl}" />
</Style>
</Styles> </Styles>

View File

@@ -1,7 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using ServiceLib.Manager;
namespace v2rayN.Desktop.Base; namespace v2rayN.Desktop.Base;

View File

@@ -0,0 +1,129 @@
using Avalonia.Media;
using AvaloniaEdit;
using AvaloniaEdit.Document;
using AvaloniaEdit.Rendering;
namespace v2rayN.Desktop.Common;
public class KeywordColorizer : DocumentColorizingTransformer
{
private readonly string[] _keywords;
private readonly Dictionary<string, IBrush> _brushMap;
public KeywordColorizer(IDictionary<string, IBrush> keywordBrushMap)
{
if (keywordBrushMap == null || keywordBrushMap.Count == 0)
{
throw new ArgumentException("keywordBrushMap must not be null or empty", nameof(keywordBrushMap));
}
_brushMap = new Dictionary<string, IBrush>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in keywordBrushMap)
{
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value == null)
{
continue;
}
if (!_brushMap.ContainsKey(kvp.Key))
{
_brushMap[kvp.Key] = kvp.Value;
}
}
if (_brushMap.Count == 0)
{
throw new ArgumentException("keywordBrushMap must contain at least one non-empty key with a non-null brush", nameof(keywordBrushMap));
}
_keywords = _brushMap.Keys.ToArray();
}
protected override void ColorizeLine(DocumentLine line)
{
var text = CurrentContext.Document.GetText(line);
if (string.IsNullOrEmpty(text))
{
return;
}
foreach (var kw in _keywords)
{
if (string.IsNullOrEmpty(kw))
{
continue;
}
var searchStart = 0;
while (true)
{
var idx = text.IndexOf(kw, searchStart, StringComparison.OrdinalIgnoreCase);
if (idx < 0)
{
break;
}
var kwEndIndex = idx + kw.Length;
if (IsWordCharBefore(text, idx) || IsWordCharAfter(text, kwEndIndex))
{
searchStart = idx + Math.Max(1, kw.Length);
continue;
}
var start = line.Offset + idx;
var end = start + kw.Length;
if (_brushMap.TryGetValue(kw, out var brush) && brush != null)
{
ChangeLinePart(start, end, element => element.TextRunProperties.SetForegroundBrush(brush));
}
searchStart = idx + Math.Max(1, kw.Length);
}
}
}
private static bool IsWordCharBefore(string text, int idx)
{
if (idx <= 0)
{
return false;
}
var c = text[idx - 1];
return char.IsLetterOrDigit(c) || c == '_';
}
private static bool IsWordCharAfter(string text, int idx)
{
if (idx >= text.Length)
{
return false;
}
var c = text[idx];
return char.IsLetterOrDigit(c) || c == '_';
}
}
public static class TextEditorKeywordHighlighter
{
public static void Attach(TextEditor editor, IDictionary<string, IBrush> keywordBrushMap)
{
ArgumentNullException.ThrowIfNull(editor);
if (keywordBrushMap == null || keywordBrushMap.Count == 0)
{
return;
}
if (editor.TextArea?.TextView?.LineTransformers?.OfType<KeywordColorizer>().Any() == true)
{
return;
}
var colorizer = new KeywordColorizer(keywordBrushMap);
editor.TextArea.TextView.LineTransformers.Add(colorizer);
editor.TextArea.TextView.InvalidateVisual();
}
}

View File

@@ -1,8 +1,10 @@
global using ServiceLib; global using ServiceLib;
global using ServiceLib.Base; global using ServiceLib.Base;
global using ServiceLib.Common; global using ServiceLib.Common;
global using ServiceLib.Enums; global using ServiceLib.Enums;
global using ServiceLib.Events;
global using ServiceLib.Handler; global using ServiceLib.Handler;
global using ServiceLib.Manager;
global using ServiceLib.Models; global using ServiceLib.Models;
global using ServiceLib.Resx; global using ServiceLib.Resx;
global using ServiceLib.ViewModels; global using ServiceLib.ViewModels;

View File

@@ -1,6 +1,5 @@
using Avalonia; using Avalonia;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using ServiceLib.Manager;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
namespace v2rayN.Desktop; namespace v2rayN.Desktop;

View File

@@ -5,10 +5,10 @@ using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using AvaloniaEdit;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Semi.Avalonia; using Semi.Avalonia;
using ServiceLib.Manager;
namespace v2rayN.Desktop.ViewModels; namespace v2rayN.Desktop.ViewModels;
@@ -113,7 +113,8 @@ public class ThemeSettingViewModel : MyReactiveObject
x.OfType<ContextMenu>(), x.OfType<ContextMenu>(),
x.OfType<DataGridRow>(), x.OfType<DataGridRow>(),
x.OfType<ListBoxItem>(), x.OfType<ListBoxItem>(),
x.OfType<HeaderedContentControl>() x.OfType<HeaderedContentControl>(),
x.OfType<TextEditor>()
)); ));
style.Add(new Setter() style.Add(new Setter()
{ {
@@ -154,7 +155,8 @@ public class ThemeSettingViewModel : MyReactiveObject
x.OfType<DataGridRow>(), x.OfType<DataGridRow>(),
x.OfType<ListBoxItem>(), x.OfType<ListBoxItem>(),
x.OfType<HeaderedContentControl>(), x.OfType<HeaderedContentControl>(),
x.OfType<WindowNotificationManager>() x.OfType<WindowNotificationManager>(),
x.OfType<TextEditor>()
)); ));
style.Add(new Setter() style.Add(new Setter()
{ {

View File

@@ -23,14 +23,12 @@
x:Name="btnSave" x:Name="btnSave"
Width="100" Width="100"
Content="{x:Static resx:ResUI.TbConfirm}" Content="{x:Static resx:ResUI.TbConfirm}"
Cursor="Hand"
IsDefault="True" /> IsDefault="True" />
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
Width="100" Width="100"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}" Content="{x:Static resx:ResUI.TbCancel}"
Cursor="Hand"
IsCancel="True" /> IsCancel="True" />
</StackPanel> </StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">

View File

@@ -23,14 +23,12 @@
x:Name="btnSave" x:Name="btnSave"
Width="100" Width="100"
Content="{x:Static resx:ResUI.TbConfirm}" Content="{x:Static resx:ResUI.TbConfirm}"
Cursor="Hand"
IsDefault="True" /> IsDefault="True" />
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
Width="100" Width="100"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}" Content="{x:Static resx:ResUI.TbCancel}"
Cursor="Hand"
IsCancel="True" /> IsCancel="True" />
</StackPanel> </StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
@@ -402,7 +400,7 @@
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="1000:2000,3000:4000" /> Watermark="1000-2000,3000,4000" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="2" Grid.Column="2"

View File

@@ -2,7 +2,6 @@ using System.Reactive.Disposables;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;

View File

@@ -1,6 +1,5 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -24,25 +23,6 @@ public partial class CheckUpdateView : ReactiveUserControl<CheckUpdateViewModel>
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{ {
switch (action)
{
case EViewAction.DispatcherCheckUpdate:
if (obj is null)
return false;
Dispatcher.UIThread.Post(() =>
ViewModel?.UpdateViewResult((CheckUpdateModel)obj),
DispatcherPriority.Default);
break;
case EViewAction.DispatcherCheckUpdateFinished:
if (obj is null)
return false;
Dispatcher.UIThread.Post(() =>
ViewModel?.UpdateFinishedResult((bool)obj),
DispatcherPriority.Default);
break;
}
return await Task.FromResult(true); return await Task.FromResult(true);
} }
} }

View File

@@ -2,7 +2,6 @@ using System.Reactive.Disposables;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -31,17 +30,6 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{ {
switch (action)
{
case EViewAction.DispatcherRefreshConnections:
if (obj is null)
return false;
Dispatcher.UIThread.Post(() =>
ViewModel?.RefreshConnections((List<ConnectionItem>?)obj),
DispatcherPriority.Default);
break;
}
return await Task.FromResult(true); return await Task.FromResult(true);
} }

View File

@@ -1,10 +1,8 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using DynamicData; using DynamicData;
using ReactiveUI; using ReactiveUI;
using Splat;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -14,7 +12,6 @@ public partial class ClashProxiesView : ReactiveUserControl<ClashProxiesViewMode
{ {
InitializeComponent(); InitializeComponent();
ViewModel = new ClashProxiesViewModel(UpdateViewHandler); ViewModel = new ClashProxiesViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ClashProxiesViewModel));
lstProxyDetails.DoubleTapped += LstProxyDetails_DoubleTapped; lstProxyDetails.DoubleTapped += LstProxyDetails_DoubleTapped;
this.KeyDown += ClashProxiesView_KeyDown; this.KeyDown += ClashProxiesView_KeyDown;
@@ -40,23 +37,6 @@ public partial class ClashProxiesView : ReactiveUserControl<ClashProxiesViewMode
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{ {
switch (action)
{
case EViewAction.DispatcherRefreshProxyGroups:
Dispatcher.UIThread.Post(() =>
ViewModel?.RefreshProxyGroups(),
DispatcherPriority.Default);
break;
case EViewAction.DispatcherProxiesDelayTest:
if (obj is null)
return false;
Dispatcher.UIThread.Post(() =>
ViewModel?.ProxiesDelayTestResult((SpeedTestResult)obj),
DispatcherPriority.Default);
break;
}
return await Task.FromResult(true); return await Task.FromResult(true);
} }

View File

@@ -24,14 +24,12 @@
x:Name="btnSave" x:Name="btnSave"
Width="100" Width="100"
Content="{x:Static resx:ResUI.TbConfirm}" Content="{x:Static resx:ResUI.TbConfirm}"
Cursor="Hand"
IsDefault="True" /> IsDefault="True" />
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
Width="100" Width="100"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}" Content="{x:Static resx:ResUI.TbCancel}"
Cursor="Hand"
IsCancel="True" /> IsCancel="True" />
</StackPanel> </StackPanel>
@@ -61,7 +59,7 @@
x:Name="cmbDirectDNS" x:Name="cmbDirectDNS"
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Width="200" Width="300"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding DirectDNS, Mode=TwoWay}" /> Text="{Binding DirectDNS, Mode=TwoWay}" />
@@ -75,7 +73,7 @@
x:Name="cmbRemoteDNS" x:Name="cmbRemoteDNS"
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
Width="200" Width="300"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding RemoteDNS, Mode=TwoWay}" /> Text="{Binding RemoteDNS, Mode=TwoWay}" />
@@ -89,7 +87,7 @@
x:Name="cmbSBResolverDNS" x:Name="cmbSBResolverDNS"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
Width="200" Width="300"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding SingboxOutboundsResolveDNS, Mode=TwoWay}" /> Text="{Binding SingboxOutboundsResolveDNS, Mode=TwoWay}" />
<TextBlock <TextBlock
@@ -110,7 +108,7 @@
x:Name="cmbSBFinalResolverDNS" x:Name="cmbSBFinalResolverDNS"
Grid.Row="4" Grid.Row="4"
Grid.Column="1" Grid.Column="1"
Width="200" Width="300"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{Binding SingboxFinalResolveDNS, Mode=TwoWay}" /> Text="{Binding SingboxFinalResolveDNS, Mode=TwoWay}" />
<TextBlock <TextBlock
@@ -231,7 +229,7 @@
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbApplyProxyDomainsOnly}" Text="{x:Static resx:ResUI.TbFakeIPTips}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
@@ -331,8 +329,7 @@
<Button <Button
x:Name="btnImportDefConfig4V2rayCompatible" x:Name="btnImportDefConfig4V2rayCompatible"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSettingDnsImportDefConfig}" Content="{x:Static resx:ResUI.TbSettingDnsImportDefConfig}" />
Cursor="Hand" />
</StackPanel> </StackPanel>
</Grid> </Grid>
@@ -415,8 +412,7 @@
<Button <Button
x:Name="btnImportDefConfig4SingboxCompatible" x:Name="btnImportDefConfig4SingboxCompatible"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSettingDnsImportDefConfig}" Content="{x:Static resx:ResUI.TbSettingDnsImportDefConfig}" />
Cursor="Hand" />
</StackPanel> </StackPanel>
</Grid> </Grid>

View File

@@ -2,7 +2,6 @@ using System.Reactive.Disposables;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -16,6 +15,7 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
InitializeComponent(); InitializeComponent();
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close(); btnCancel.Click += (s, e) => this.Close();
ViewModel = new DNSSettingViewModel(UpdateViewHandler); ViewModel = new DNSSettingViewModel(UpdateViewHandler);
@@ -100,4 +100,9 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
{ {
ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/"); ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/");
} }
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
btnCancel.Focus();
}
} }

View File

@@ -23,14 +23,12 @@
x:Name="btnSave" x:Name="btnSave"
Width="100" Width="100"
Content="{x:Static resx:ResUI.TbConfirm}" Content="{x:Static resx:ResUI.TbConfirm}"
Cursor="Hand"
IsDefault="True" /> IsDefault="True" />
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
Width="100" Width="100"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}" Content="{x:Static resx:ResUI.TbCancel}"
Cursor="Hand"
IsCancel="True" /> IsCancel="True" />
</StackPanel> </StackPanel>

View File

@@ -1,7 +1,6 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -15,6 +14,7 @@ public partial class FullConfigTemplateWindow : WindowBase<FullConfigTemplateVie
InitializeComponent(); InitializeComponent();
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close(); btnCancel.Click += (s, e) => this.Close();
ViewModel = new FullConfigTemplateViewModel(UpdateViewHandler); ViewModel = new FullConfigTemplateViewModel(UpdateViewHandler);
@@ -49,4 +49,9 @@ public partial class FullConfigTemplateWindow : WindowBase<FullConfigTemplateVie
{ {
ProcUtils.ProcessStart("https://github.com/2dust/v2rayN/wiki/Description-of-some-ui#%E5%AE%8C%E6%95%B4%E9%85%8D%E7%BD%AE%E6%A8%A1%E6%9D%BF%E8%AE%BE%E7%BD%AE"); ProcUtils.ProcessStart("https://github.com/2dust/v2rayN/wiki/Description-of-some-ui#%E5%AE%8C%E6%95%B4%E9%85%8D%E7%BD%AE%E6%A8%A1%E6%9D%BF%E8%AE%BE%E7%BD%AE");
} }
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
btnCancel.Focus();
}
} }

View File

@@ -28,14 +28,12 @@
x:Name="btnSave" x:Name="btnSave"
Width="100" Width="100"
Content="{x:Static resx:ResUI.TbConfirm}" Content="{x:Static resx:ResUI.TbConfirm}"
Cursor="Hand"
IsDefault="True" /> IsDefault="True" />
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
Width="100" Width="100"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}" Content="{x:Static resx:ResUI.TbCancel}"
Cursor="Hand"
IsCancel="True" /> IsCancel="True" />
</StackPanel> </StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">

View File

@@ -22,6 +22,7 @@ public partial class GlobalHotkeySettingWindow : WindowBase<GlobalHotkeySettingV
btnReset.Click += btnReset_Click; btnReset.Click += btnReset_Click;
HotkeyManager.Instance.IsPause = true; HotkeyManager.Instance.IsPause = true;
Loaded += Window_Loaded;
this.Closing += (s, e) => HotkeyManager.Instance.IsPause = false; this.Closing += (s, e) => HotkeyManager.Instance.IsPause = false;
btnCancel.Click += (s, e) => this.Close(); btnCancel.Click += (s, e) => this.Close();
@@ -134,4 +135,9 @@ public partial class GlobalHotkeySettingWindow : WindowBase<GlobalHotkeySettingV
return res.ToString(); return res.ToString();
} }
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
btnCancel.Focus();
}
} }

View File

@@ -1,4 +1,5 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
@@ -9,8 +10,6 @@ using Avalonia.Threading;
using DialogHostAvalonia; using DialogHostAvalonia;
using MsBox.Avalonia.Enums; using MsBox.Avalonia.Enums;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager;
using Splat;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
using v2rayN.Desktop.Manager; using v2rayN.Desktop.Manager;
@@ -20,7 +19,7 @@ namespace v2rayN.Desktop.Views;
public partial class MainWindow : WindowBase<MainWindowViewModel> public partial class MainWindow : WindowBase<MainWindowViewModel>
{ {
private static Config _config; private static Config _config;
private WindowNotificationManager? _manager; private readonly WindowNotificationManager? _manager;
private CheckUpdateView? _checkUpdateView; private CheckUpdateView? _checkUpdateView;
private BackupAndRestoreView? _backupAndRestoreView; private BackupAndRestoreView? _backupAndRestoreView;
private bool _blCloseByUser = false; private bool _blCloseByUser = false;
@@ -39,9 +38,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
menuBackupAndRestore.Click += MenuBackupAndRestore_Click; menuBackupAndRestore.Click += MenuBackupAndRestore_Click;
menuClose.Click += MenuClose_Click; menuClose.Click += MenuClose_Click;
MessageBus.Current.Listen<string>(EMsgCommand.SendSnackMsg.ToString()).Subscribe(DelegateSnackMsg);
ViewModel = new MainWindowViewModel(UpdateViewHandler); ViewModel = new MainWindowViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(MainWindowViewModel));
switch (_config.UiItem.MainGirdOrientation) switch (_config.UiItem.MainGirdOrientation)
{ {
@@ -136,6 +133,30 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain2.SelectedIndex).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain2.SelectedIndex).DisposeWith(disposables);
break; break;
} }
AppEvents.SendSnackMsgRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async content => await DelegateSnackMsg(content))
.DisposeWith(disposables);
AppEvents.AppExitRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => StorageUI())
.DisposeWith(disposables);
AppEvents.ShutdownRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(content => Shutdown(content))
.DisposeWith(disposables);
AppEvents.ShowHideWindowRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(blShow => ShowHideWindow(blShow))
.DisposeWith(disposables);
}); });
if (Utils.IsWindows()) if (Utils.IsWindows())
@@ -156,7 +177,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
menuAddServerViaScan.IsVisible = false; menuAddServerViaScan.IsVisible = false;
AddHelpMenuItem(); AddHelpMenuItem();
MessageBus.Current.Listen<string>(EMsgCommand.AppExit.ToString()).Subscribe(StorageUI);
} }
#region Event #region Event
@@ -168,11 +188,9 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
DispatcherPriority.Default); DispatcherPriority.Default);
} }
private void DelegateSnackMsg(string content) private async Task DelegateSnackMsg(string content)
{ {
Dispatcher.UIThread.Post(() => _manager?.Show(new Notification(null, content, NotificationType.Information));
_manager?.Show(new Notification(null, content, NotificationType.Information)),
DispatcherPriority.Normal);
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@@ -207,39 +225,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
case EViewAction.SubSettingWindow: case EViewAction.SubSettingWindow:
return await new SubSettingWindow().ShowDialog<bool>(this); return await new SubSettingWindow().ShowDialog<bool>(this);
case EViewAction.ShowHideWindow:
Dispatcher.UIThread.Post(() =>
ShowHideWindow((bool?)obj),
DispatcherPriority.Default);
break;
case EViewAction.DispatcherStatistics:
if (obj is null)
return false;
Dispatcher.UIThread.Post(() =>
ViewModel?.SetStatisticsResult((ServerSpeedItem)obj),
DispatcherPriority.Default);
break;
case EViewAction.DispatcherReload:
Dispatcher.UIThread.Post(() =>
ViewModel?.ReloadResult(),
DispatcherPriority.Default);
break;
case EViewAction.Shutdown:
if (obj != null && _blCloseByUser == false)
{
_blCloseByUser = (bool)obj;
}
StorageUI();
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
HotkeyManager.Instance.Dispose();
desktop.Shutdown();
}
break;
case EViewAction.ScanScreenTask: case EViewAction.ScanScreenTask:
await ScanScreenTaskAsync(); await ScanScreenTaskAsync();
break; break;
@@ -249,17 +234,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
break; break;
case EViewAction.AddServerViaClipboard: case EViewAction.AddServerViaClipboard:
var clipboardData = await AvaUtils.GetClipboardData(this); await AddServerViaClipboardAsync();
if (clipboardData.IsNotEmpty() && ViewModel != null)
{
await ViewModel.AddServerViaClipboardAsync(clipboardData);
}
break;
case EViewAction.AdjustMainLvColWidth:
Dispatcher.UIThread.Post(() =>
Locator.Current.GetService<ProfilesViewModel>()?.AutofitColumnWidthAsync(),
DispatcherPriority.Default);
break; break;
} }
@@ -278,7 +253,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
case EGlobalHotkey.SystemProxySet: case EGlobalHotkey.SystemProxySet:
case EGlobalHotkey.SystemProxyUnchanged: case EGlobalHotkey.SystemProxyUnchanged:
case EGlobalHotkey.SystemProxyPac: case EGlobalHotkey.SystemProxyPac:
Locator.Current.GetService<StatusBarViewModel>()?.SetListenerType((ESysProxyType)((int)e - 1)); AppEvents.SysProxyChangeRequested.Publish((ESysProxyType)((int)e - 1));
break; break;
} }
} }
@@ -300,10 +275,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
break; break;
case WindowCloseReason.ApplicationShutdown or WindowCloseReason.OSShutdown: case WindowCloseReason.ApplicationShutdown or WindowCloseReason.OSShutdown:
if (ViewModel != null) await AppManager.Instance.AppExitAsync(false);
{
await ViewModel.MyAppExitAsync(true);
}
break; break;
} }
@@ -317,11 +289,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
switch (e.Key) switch (e.Key)
{ {
case Key.V: case Key.V:
var clipboardData = await AvaUtils.GetClipboardData(this); await AddServerViaClipboardAsync();
if (clipboardData.IsNotEmpty() && ViewModel != null)
{
await ViewModel.AddServerViaClipboardAsync(clipboardData);
}
break; break;
case Key.S: case Key.S:
@@ -348,6 +316,15 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe"));
} }
public async Task AddServerViaClipboardAsync()
{
var clipboardData = await AvaUtils.GetClipboardData(this);
if (clipboardData.IsNotEmpty() && ViewModel != null)
{
await ViewModel.AddServerViaClipboardAsync(clipboardData);
}
}
public async Task ScanScreenTaskAsync() public async Task ScanScreenTaskAsync()
{ {
//ShowHideWindow(false); //ShowHideWindow(false);
@@ -398,9 +375,21 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
_blCloseByUser = true; _blCloseByUser = true;
StorageUI(); StorageUI();
if (ViewModel != null)
await AppManager.Instance.AppExitAsync(true);
}
private void Shutdown(bool obj)
{ {
await ViewModel.MyAppExitAsync(false); if (obj is bool b && _blCloseByUser == false)
{
_blCloseByUser = b;
}
StorageUI();
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
HotkeyManager.Instance.Dispose();
desktop.Shutdown();
} }
} }
@@ -462,7 +451,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
} }
} }
private void StorageUI(string? n = null) private void StorageUI()
{ {
ConfigHandler.SaveWindowSizeItem(_config, GetType().Name, Width, Height); ConfigHandler.SaveWindowSizeItem(_config, GetType().Name, Width, Height);

View File

@@ -2,6 +2,7 @@
x:Class="v2rayN.Desktop.Views.MsgView" x:Class="v2rayN.Desktop.Views.MsgView"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@@ -70,16 +71,17 @@
Theme="{DynamicResource SimpleToggleSwitch}" /> Theme="{DynamicResource SimpleToggleSwitch}" />
</WrapPanel> </WrapPanel>
<ScrollViewer x:Name="msgScrollViewer" VerticalScrollBarVisibility="Auto"> <avaloniaEdit:TextEditor
<SelectableTextBlock
Name="txtMsg" Name="txtMsg"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
VerticalAlignment="Stretch" IsReadOnly="True"
Classes="TextArea" VerticalScrollBarVisibility="Auto"
TextAlignment="Left" WordWrap="True">
TextWrapping="Wrap"> <avaloniaEdit:TextEditor.Options>
<SelectableTextBlock.ContextMenu> <avaloniaEdit:TextEditorOptions AllowScrollBelowDocument="False"/>
<ContextMenu> </avaloniaEdit:TextEditor.Options>
<avaloniaEdit:TextEditor.ContextFlyout>
<MenuFlyout>
<MenuItem <MenuItem
x:Name="menuMsgViewSelectAll" x:Name="menuMsgViewSelectAll"
Click="menuMsgViewSelectAll_Click" Click="menuMsgViewSelectAll_Click"
@@ -96,9 +98,9 @@
x:Name="menuMsgViewClear" x:Name="menuMsgViewClear"
Click="menuMsgViewClear_Click" Click="menuMsgViewClear_Click"
Header="{x:Static resx:ResUI.menuMsgViewClear}" /> Header="{x:Static resx:ResUI.menuMsgViewClear}" />
</ContextMenu> </MenuFlyout>
</SelectableTextBlock.ContextMenu> </avaloniaEdit:TextEditor.ContextFlyout>
</SelectableTextBlock> </avaloniaEdit:TextEditor>
</ScrollViewer>
</DockPanel> </DockPanel>
</UserControl> </UserControl>

View File

@@ -1,6 +1,6 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading; using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
@@ -10,13 +10,12 @@ namespace v2rayN.Desktop.Views;
public partial class MsgView : ReactiveUserControl<MsgViewModel> public partial class MsgView : ReactiveUserControl<MsgViewModel>
{ {
private readonly ScrollViewer _scrollViewer; //private const int KeepLines = 30;
public MsgView() public MsgView()
{ {
InitializeComponent(); InitializeComponent();
_scrollViewer = this.FindControl<ScrollViewer>("msgScrollViewer"); txtMsg.TextArea.TextView.Options.EnableHyperlinks = false;
ViewModel = new MsgViewModel(UpdateViewHandler); ViewModel = new MsgViewModel(UpdateViewHandler);
this.WhenActivated(disposables => this.WhenActivated(disposables =>
@@ -24,6 +23,11 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
}); });
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
kv => kv.Key,
kv => (IBrush)new SolidColorBrush(Color.Parse(kv.Value))
));
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@@ -34,8 +38,7 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
if (obj is null) if (obj is null)
return false; return false;
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() => ShowMsg(obj),
ShowMsg(obj),
DispatcherPriority.ApplicationIdle); DispatcherPriority.ApplicationIdle);
break; break;
} }
@@ -44,23 +47,37 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
private void ShowMsg(object msg) private void ShowMsg(object msg)
{ {
txtMsg.Text = msg.ToString(); //var lineCount = txtMsg.LineCount;
//if (lineCount > ViewModel?.NumMaxMsg)
//{
// var cutLine = txtMsg.Document.GetLineByNumber(lineCount - KeepLines);
// txtMsg.Document.Remove(0, cutLine.Offset);
//}
if (txtMsg.LineCount > ViewModel?.NumMaxMsg)
{
ClearMsg();
}
txtMsg.AppendText(msg.ToString());
if (togScrollToEnd.IsChecked ?? true) if (togScrollToEnd.IsChecked ?? true)
{ {
_scrollViewer?.ScrollToEnd(); txtMsg.ScrollToEnd();
} }
} }
public void ClearMsg() public void ClearMsg()
{ {
ViewModel?.ClearMsg(); txtMsg.Clear();
txtMsg.Text = ""; txtMsg.AppendText("----- Message cleared -----\n");
} }
private void menuMsgViewSelectAll_Click(object? sender, RoutedEventArgs e) private void menuMsgViewSelectAll_Click(object? sender, RoutedEventArgs e)
{ {
txtMsg.Focus(); Dispatcher.UIThread.Post(() =>
{
txtMsg.TextArea.Focus();
txtMsg.SelectAll(); txtMsg.SelectAll();
}, DispatcherPriority.Render);
} }
private async void menuMsgViewCopy_Click(object? sender, RoutedEventArgs e) private async void menuMsgViewCopy_Click(object? sender, RoutedEventArgs e)

View File

@@ -24,14 +24,12 @@
x:Name="btnSave" x:Name="btnSave"
Width="100" Width="100"
Content="{x:Static resx:ResUI.TbConfirm}" Content="{x:Static resx:ResUI.TbConfirm}"
Cursor="Hand"
IsDefault="True" /> IsDefault="True" />
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
Width="100" Width="100"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}" Content="{x:Static resx:ResUI.TbCancel}"
Cursor="Hand"
IsCancel="True" /> IsCancel="True" />
</StackPanel> </StackPanel>

View File

@@ -1,7 +1,7 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -14,6 +14,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
{ {
InitializeComponent(); InitializeComponent();
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close(); btnCancel.Click += (s, e) => this.Close();
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
@@ -209,4 +210,9 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
ViewModel.destOverride = clbdestOverride.SelectedItems.Cast<string>().ToList(); ViewModel.destOverride = clbdestOverride.SelectedItems.Cast<string>().ToList();
} }
} }
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
btnCancel.Focus();
}
} }

View File

@@ -0,0 +1,128 @@
<Window
x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="{x:Static resx:ResUI.TbSelectProfile}"
Width="800"
Height="450"
x:DataType="vms:ProfilesSelectViewModel"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<DockPanel Margin="8">
<!-- Bottom buttons -->
<StackPanel
Margin="4"
HorizontalAlignment="Center"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
Click="BtnSave_Click"
Content="{x:Static resx:ResUI.TbConfirm}" />
<Button
x:Name="btnCancel"
Width="100"
Margin="8,0"
Content="{x:Static resx:ResUI.TbCancel}" />
</StackPanel>
<Grid>
<DockPanel>
<!-- Top tools -->
<WrapPanel Margin="4" DockPanel.Dock="Top">
<ListBox
x:Name="lstGroup"
Margin="{StaticResource MarginLr4}"
DisplayMemberBinding="{Binding Remarks}"
ItemsSource="{Binding SubItems}"
Theme="{DynamicResource ButtonRadioGroupListBox}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<Button
x:Name="btnAutofitColumnWidth"
Width="32"
Height="32"
Margin="8,0"
ToolTip.Tip="{x:Static resx:ResUI.menuProfileAutofitColumnWidth}">
<Button.Content>
<PathIcon Data="{StaticResource building_fit}" />
</Button.Content>
</Button>
<TextBox
x:Name="txtServerFilter"
Width="200"
Margin="8,0"
VerticalContentAlignment="Center"
Text="{Binding ServerFilter, Mode=TwoWay}"
Watermark="{x:Static resx:ResUI.MsgServerTitle}" />
</WrapPanel>
<!-- Profiles grid -->
<DataGrid
x:Name="lstProfiles"
AutoGenerateColumns="False"
BorderThickness="1"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
GridLinesVisibility="All"
HeadersVisibility="All"
IsReadOnly="True"
ItemsSource="{Binding ProfileItems}"
SelectionMode="Single">
<DataGrid.KeyBindings>
<KeyBinding Command="{Binding SelectFinish}" Gesture="Enter" />
</DataGrid.KeyBindings>
<DataGrid.Columns>
<DataGridTextColumn
Width="80"
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}"
Tag="ConfigType" />
<DataGridTextColumn
Width="120"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}"
Tag="Remarks" />
<DataGridTextColumn
Width="120"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}"
Tag="Address" />
<DataGridTextColumn
Width="60"
Binding="{Binding Port}"
Header="{x:Static resx:ResUI.LvPort}"
Tag="Port" />
<DataGridTextColumn
Width="100"
Binding="{Binding Network}"
Header="{x:Static resx:ResUI.LvTransportProtocol}"
Tag="Network" />
<DataGridTextColumn
Width="100"
Binding="{Binding StreamSecurity}"
Header="{x:Static resx:ResUI.LvTLS}"
Tag="StreamSecurity" />
<DataGridTextColumn
Width="100"
Binding="{Binding SubRemarks}"
Header="{x:Static resx:ResUI.LvSubscription}"
Tag="SubRemarks" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
</DockPanel>
</Window>

View File

@@ -0,0 +1,194 @@
using System.Reactive.Disposables;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using ReactiveUI;
using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views;
public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
{
private static Config _config;
public Task<ProfileItem?> ProfileItem => GetProfileItem();
public Task<List<ProfileItem>?> ProfileItems => GetProfileItems();
private bool _allowMultiSelect = false;
public ProfilesSelectWindow()
{
InitializeComponent();
_config = AppManager.Instance.Config;
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
txtServerFilter.KeyDown += TxtServerFilter_KeyDown;
lstProfiles.KeyDown += LstProfiles_KeyDown;
lstProfiles.SelectionChanged += LstProfiles_SelectionChanged;
lstProfiles.LoadingRow += LstProfiles_LoadingRow;
lstProfiles.Sorting += LstProfiles_Sorting;
lstProfiles.DoubleTapped += LstProfiles_DoubleTapped;
ViewModel = new ProfilesSelectViewModel(UpdateViewHandler);
DataContext = ViewModel;
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.ProfileItems, v => v.lstProfiles.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedProfile, v => v.lstProfiles.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables);
});
btnCancel.Click += (s, e) => Close(false);
}
public void AllowMultiSelect(bool allow)
{
_allowMultiSelect = allow;
if (allow)
{
lstProfiles.SelectionMode = DataGridSelectionMode.Extended;
lstProfiles.SelectedItems.Clear();
}
else
{
lstProfiles.SelectionMode = DataGridSelectionMode.Single;
if (lstProfiles.SelectedItems.Count > 0)
{
var first = lstProfiles.SelectedItems[0];
lstProfiles.SelectedItems.Clear();
lstProfiles.SelectedItem = first;
}
}
}
// Expose ConfigType filter controls to callers
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
=> ViewModel?.SetConfigTypeFilter(types, exclude);
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
}
return await Task.FromResult(true);
}
private void LstProfiles_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (ViewModel != null)
{
ViewModel.SelectedProfiles = lstProfiles.SelectedItems.Cast<ProfileItemModel>().ToList();
}
}
private void LstProfiles_LoadingRow(object? sender, DataGridRowEventArgs e)
{
e.Row.Header = $" {e.Row.Index + 1}";
}
private void LstProfiles_DoubleTapped(object? sender, TappedEventArgs e)
{
// 忽略表头区域的双击
if (e.Source is Control src)
{
if (src.FindAncestorOfType<DataGridColumnHeader>() != null)
{
e.Handled = true;
return;
}
// 仅当在数据行或其子元素上双击时才触发选择
if (src.FindAncestorOfType<DataGridRow>() != null)
{
ViewModel?.SelectFinish();
e.Handled = true;
}
}
}
private void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e)
{
// 自定义排序,防止默认行为导致误触发
e.Handled = true;
if (ViewModel != null && e.Column?.Tag?.ToString() != null)
{
ViewModel.SortServer(e.Column.Tag.ToString());
}
}
private void LstProfiles_KeyDown(object? sender, KeyEventArgs e)
{
if (e.KeyModifiers is KeyModifiers.Control or KeyModifiers.Meta)
{
if (e.Key == Key.A)
{
if (_allowMultiSelect)
{
lstProfiles.SelectAll();
}
e.Handled = true;
}
}
else
{
if (e.Key is Key.Enter or Key.Return)
{
ViewModel?.SelectFinish();
e.Handled = true;
}
}
}
private void BtnAutofitColumnWidth_Click(object? sender, RoutedEventArgs e)
{
AutofitColumnWidth();
}
private void AutofitColumnWidth()
{
try
{
foreach (var col in lstProfiles.Columns)
{
col.Width = new DataGridLength(1, DataGridLengthUnitType.Auto);
}
}
catch
{
}
}
private void TxtServerFilter_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key is Key.Enter or Key.Return)
{
ViewModel?.RefreshServers();
}
}
public async Task<ProfileItem?> GetProfileItem()
{
var item = await ViewModel?.GetProfileItem();
return item;
}
public async Task<List<ProfileItem>?> GetProfileItems()
{
var item = await ViewModel?.GetProfileItems();
return item;
}
private void BtnSave_Click(object sender, RoutedEventArgs e)
{
// Trigger selection finalize when Confirm is clicked
ViewModel?.SelectFinish();
}
}

View File

@@ -1,4 +1,5 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@@ -7,8 +8,6 @@ using Avalonia.Threading;
using DialogHostAvalonia; using DialogHostAvalonia;
using MsBox.Avalonia.Enums; using MsBox.Avalonia.Enums;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager;
using Splat;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -48,7 +47,6 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
//} //}
ViewModel = new ProfilesViewModel(UpdateViewHandler); ViewModel = new ProfilesViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ProfilesViewModel));
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
@@ -96,11 +94,21 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
AppEvents.AppExitRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => StorageUI())
.DisposeWith(disposables);
AppEvents.AdjustMainLvColWidthRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => AutofitColumnWidth())
.DisposeWith(disposables);
}); });
RestoreUI(); RestoreUI();
ViewModel?.RefreshServers();
MessageBus.Current.Listen<string>(EMsgCommand.AppExit.ToString()).Subscribe(StorageUI);
} }
private async void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e) private async void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e)
@@ -127,13 +135,6 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
await AvaUtils.SetClipboardData(this, (string)obj); await AvaUtils.SetClipboardData(this, (string)obj);
break; break;
case EViewAction.AdjustMainLvColWidth:
Dispatcher.UIThread.Post(() =>
AutofitColumnWidth(),
DispatcherPriority.Default);
break;
case EViewAction.ProfilesFocus: case EViewAction.ProfilesFocus:
lstProfiles.Focus(); lstProfiles.Focus();
break; break;
@@ -177,21 +178,8 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
return false; return false;
return await new SubEditWindow((SubItem)obj).ShowDialog<bool>(_window); return await new SubEditWindow((SubItem)obj).ShowDialog<bool>(_window);
case EViewAction.DispatcherSpeedTest:
if (obj is null)
return false;
Dispatcher.UIThread.Post(() =>
ViewModel?.SetSpeedTestResult((SpeedTestResult)obj),
DispatcherPriority.Default);
break;
case EViewAction.DispatcherRefreshServersBiz: case EViewAction.DispatcherRefreshServersBiz:
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(RefreshServersBiz, DispatcherPriority.Default);
{
_ = RefreshServersBiz();
},
DispatcherPriority.Default);
break; break;
} }
@@ -209,13 +197,8 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
await DialogHost.Show(dialog); await DialogHost.Show(dialog);
} }
public async Task RefreshServersBiz() public void RefreshServersBiz()
{ {
if (ViewModel != null)
{
await ViewModel.RefreshServersBiz();
}
if (lstProfiles.SelectedIndex >= 0) if (lstProfiles.SelectedIndex >= 0)
{ {
lstProfiles.ScrollIntoView(lstProfiles.SelectedItem, null); lstProfiles.ScrollIntoView(lstProfiles.SelectedItem, null);
@@ -421,7 +404,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
} }
} }
private void StorageUI(string? n = null) private void StorageUI()
{ {
List<ColumnItem> lvColumnItem = new(); List<ColumnItem> lvColumnItem = new();
foreach (var item2 in lstProfiles.Columns) foreach (var item2 in lstProfiles.Columns)

Some files were not shown because too many files have changed in this diff Show More