Compare commits

..

45 Commits

Author SHA1 Message Date
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
2dust
e6da14f4a8 up 7.14.2 2025-08-19 19:05:55 +08:00
2dust
f748f1849c Update Directory.Packages.props 2025-08-19 19:05:20 +08:00
DHR60
f8995b78f6 Passes srsName as third format argument (#7805) 2025-08-19 17:00:27 +08:00
JieXu
a861020828 Update package-rhel.sh (#7806) 2025-08-19 16:59:52 +08:00
DHR60
dc94962900 Fix tun (#7802) 2025-08-19 09:10:54 +08:00
JieXu
4a40b87bba Update package-rhel.sh (#7799) 2025-08-19 09:09:52 +08:00
DHR60
4853e2348d Fix dns (#7797) 2025-08-19 09:09:35 +08:00
2dust
e104f9f9b2 Rename Manager 2025-08-18 20:09:58 +08:00
JieXu
876381a7fb Create package-rhel.sh (#7770)
* Create package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh
2025-08-18 17:29:10 +08:00
Miheichev Aleksandr Sergeevich
4f711b1bd3 i18n(ru/zh-Hans/zh-Hant/hu/fa): translate TUN settings, unify MTU, use resx (#7787)
* feat(i18n,ui): externalize TUN settings labels, add translations

- Replace hard-coded labels "Auto Route", "Strict Route", "Stack",
  and "Mtu/mtu" with resource keys in both Avalonia and WPF views:
  - v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
  - v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
  - v2rayN/v2rayN/Views/AddServerWindow.xaml
  - v2rayN/v2rayN/Views/OptionSettingWindow.xaml
- Add new resource keys in ResUI:
  TbSettingsTunAutoRoute, TbSettingsTunStrictRoute,
  TbSettingsTunStack, TbSettingsTunMtu (unified casing as "MTU").
  Files:
  - v2rayN/ServiceLib/Resx/ResUI.resx
- Provide translations in:
  - v2rayN/ServiceLib/Resx/ResUI.ru.resx
  - v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
  - v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx
  - v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx
  - v2rayN/ServiceLib/Resx/ResUI.hu.resx
- Normalize XML comments/whitespace in .resx files.
- Update submodule v2rayN/GlobalHotKeys to 5201dd5.

No breaking changes.

* i18n: TUN labels across locales; unify MTU

* chore: ignore local IDE/venv files

* chore(resx): regenerate ResUI.Designer with TUN string accessors

- Add strongly-typed accessors in ServiceLib.Resx.ResUI:
  - TbSettingsTunAutoRoute → "Auto Route"
  - TbSettingsTunStrictRoute → "Strict Route"
  - TbSettingsTunStack → "Stack"
  - TbSettingsTunMtu → "MTU"
- Keep auto-generated structure intact; normalize minor whitespace.

Refs: v2rayN/ServiceLib/Resx/ResUI.resx
No functional changes beyond exposing new i18n keys.

* chore(gitignore): ignore JetBrains Rider artifacts (.idea/, *.sln.iml)

---------

Co-authored-by: Aleksandr Miheichev <alexandr.gmail@tuta.com>
2025-08-18 17:28:59 +08:00
DHR60
89893c0945 Adds Xray and Singbox support config type (#7789)
* Adds Xray and Singbox config type support

* Unify multiline logical expression formatting
2025-08-18 17:28:49 +08:00
2dust
7b7fe0ef46 Refactoring GetRealPingTime 2025-08-17 20:51:49 +08:00
2dust
f66226c103 Simple refactoring of CoreConfig generated code 2025-08-17 20:09:41 +08:00
2dust
d5c50ef27c Rename Manager 2025-08-17 17:31:55 +08:00
2dust
2060ac18fd Add Manager folder 2025-08-17 16:52:51 +08:00
2dust
c9c1cd8cbb Add Helper folder 2025-08-17 16:26:13 +08:00
157 changed files with 6639 additions and 5679 deletions

View File

@@ -98,4 +98,38 @@ jobs:
file: ${{ github.workspace }}/v2rayN*.zip file: ${{ github.workspace }}/v2rayN*.zip
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

3
.gitignore vendored
View File

@@ -397,4 +397,5 @@ FodyWeavers.xsd
*.msp *.msp
# JetBrains Rider # JetBrains Rider
*.sln.iml .idea/
*.sln.iml

815
package-rhel.sh Normal file
View File

@@ -0,0 +1,815 @@
#!/usr/bin/env bash
set -euo pipefail
# ===== Require Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ====
if [[ -r /etc/os-release ]]; then
. /etc/os-release
case "$ID" in
rhel|rocky|almalinux|centos|ubuntu|debian)
echo "[OK] Detected supported system: $NAME $VERSION_ID"
;;
*)
echo "[ERROR] Unsupported system: $NAME ($ID)."
echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS or Ubuntu/Debian."
exit 1
;;
esac
else
echo "[ERROR] Cannot detect system (missing /etc/os-release)."
exit 1
fi
# ===== Config & Parse arguments =========================================================
VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty
WITH_CORE="both" # Default: bundle both xray+sing-box
AUTOSTART=0 # 1 = enable system-wide autostart (/etc/xdg/autostart)
FORCE_NETCORE=0 # --netcore => skip archive bundle, use separate downloads
ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target)
BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively
# If the first argument starts with --, do not treat it as a version number
if [[ "${VERSION_ARG:-}" == --* ]]; then
VERSION_ARG=""
fi
# Take the first non --* argument as version, discard it
if [[ -n "${VERSION_ARG:-}" ]]; then shift || true; fi
# Parse remaining optional arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--with-core) WITH_CORE="${2:-both}"; shift 2;;
--autostart) AUTOSTART=1; shift;;
--xray-ver) XRAY_VER="${2:-}"; shift 2;;
--singbox-ver) SING_VER="${2:-}"; shift 2;;
--netcore) FORCE_NETCORE=1; shift;;
--arch) ARCH_OVERRIDE="${2:-}"; shift 2;;
--buildfrom) BUILD_FROM="${2:-}"; shift 2;;
*)
if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi
shift;;
esac
done
# Conflict: version number AND --buildfrom cannot be used together
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
echo "[ERROR] You cannot specify both an explicit version and --buildfrom at the same time."
echo " Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
exit 1
fi
# ===== Environment check + Dependencies ========================================
host_arch="$(uname -m)"
[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; }
install_ok=0
case "$ID" in
# ------------------------------ RHEL family (UNCHANGED) ------------------------------
rhel|rocky|almalinux|centos)
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \
sudo dnf -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
install_ok=1
elif command -v yum >/dev/null 2>&1; then
sudo yum -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \
sudo yum -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
install_ok=1
fi
;;
# ------------------------------ Ubuntu ----------------------------------------------
ubuntu)
sudo apt-get update
# Ensure 'universe' (Ubuntu) to get 'rpm'
if ! apt-cache policy | grep -q '^500 .*ubuntu.com/ubuntu.* universe'; then
sudo apt-get -y install software-properties-common || true
sudo add-apt-repository -y universe || true
sudo apt-get update
fi
# Base tools + rpm (provides rpmbuild)
sudo apt-get -y install curl unzip tar rsync rpm || true
# Cross-arch binutils so strip matches target arch + objdump for brp scripts
sudo apt-get -y install binutils binutils-x86-64-linux-gnu binutils-aarch64-linux-gnu || true
# rpmbuild presence check
if ! command -v rpmbuild >/dev/null 2>&1; then
echo "[ERROR] 'rpmbuild' not found after installing 'rpm'."
echo " Please ensure the 'rpm' package is available from your repos (universe on Ubuntu)."
exit 1
fi
# .NET SDK 8 (best effort via apt)
if ! command -v dotnet >/dev/null 2>&1; then
sudo apt-get -y install dotnet-sdk-8.0 || true
sudo apt-get -y install dotnet-sdk-8 || true
sudo apt-get -y install dotnet-sdk || true
fi
install_ok=1
;;
# ------------------------------ Debian (KEEP, with local dotnet install) ------------
debian)
sudo apt-get update
# Base tools + rpm (provides rpmbuild on Debian) + objdump/strip
sudo apt-get -y install curl unzip tar rsync rpm binutils || true
# rpmbuild presence check
if ! command -v rpmbuild >/dev/null 2>&1; then
echo "[ERROR] 'rpmbuild' not found after installing 'rpm'."
echo " Please ensure 'rpm' is available from Debian repos."
exit 1
fi
# Try apt for dotnet; fallback to official installer into $HOME/.dotnet
if ! command -v dotnet >/dev/null 2>&1; then
echo "[INFO] 'dotnet' not found. Installing .NET 8 SDK locally to \$HOME/.dotnet ..."
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
curl -fsSL https://dot.net/v1/dotnet-install.sh -o "$tmp/dotnet-install.sh"
bash "$tmp/dotnet-install.sh" --channel 8.0 --install-dir "$HOME/.dotnet"
export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
if ! command -v dotnet >/dev/null 2>&1; then
echo "[ERROR] dotnet installation failed."
exit 1
fi
fi
install_ok=1
;;
esac
if [[ "$install_ok" -ne 1 ]]; then
echo "[WARN] Could not auto-install dependencies for '$ID'. Make sure these are available:"
echo " dotnet-sdk 8.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on RPM-based distros)"
fi
command -v curl >/dev/null
# Root directory = the script's location
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Git submodules (best effort)
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
# ===== Locate project ================================================================
PROJECT="v2rayN.Desktop/v2rayN.Desktop.csproj"
if [[ ! -f "$PROJECT" ]]; then
PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
fi
[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; }
# ===== Resolve GUI version & auto checkout ============================================
VERSION=""
choose_channel() {
# If --buildfrom provided, map it directly and skip interaction.
if [[ -n "${BUILD_FROM:-}" ]]; then
case "$BUILD_FROM" in
1) echo "latest"; return 0;;
2) echo "prerelease"; return 0;;
3) echo "keep"; return 0;;
*) echo "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." >&2; exit 1;;
esac
fi
# Print menu to stderr and read from /dev/tty so stdout only carries the token.
local ch="latest" sel=""
if [[ -t 0 ]]; then
echo "[?] Choose v2rayN release channel:" >&2
echo " 1) Latest (stable) [default]" >&2
echo " 2) Pre-release (preview)" >&2
echo " 3) Keep current (do nothing)" >&2
printf "Enter 1, 2 or 3 [default 1]: " >&2
if read -r sel </dev/tty; then
case "${sel:-}" in
2) ch="prerelease" ;;
3) ch="keep" ;;
*) ch="latest" ;;
esac
else
ch="latest"
fi
else
ch="latest"
fi
echo "$ch"
}
get_latest_tag_latest() {
# Resolve /releases/latest → tag_name
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
| grep -Eo '"tag_name":\s*"v?[^"]+"' \
| head -n1 \
| sed -E 's/.*"tag_name":\s*"v?([^"]+)".*/\1/'
}
get_latest_tag_prerelease() {
# Resolve newest prerelease=true tag; prefer jq, fallback to sed/grep (no awk)
local json tag
json="$(curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20")" || return 1
# 1) Use jq if present
if command -v jq >/dev/null 2>&1; then
tag="$(printf '%s' "$json" \
| jq -r '[.[] | select(.prerelease==true)][0].tag_name' 2>/dev/null \
| sed 's/^v//')" || true
fi
# 2) Fallback to sed/grep only
if [[ -z "${tag:-}" || "${tag:-}" == "null" ]]; then
tag="$(printf '%s' "$json" \
| tr '\n' ' ' \
| sed 's/},[[:space:]]*{/\n/g' \
| grep -m1 -E '"prerelease"[[:space:]]*:[[:space:]]*true' \
| grep -Eo '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' \
| head -n1 \
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/')" || true
fi
[[ -n "${tag:-}" && "${tag:-}" != "null" ]] || return 1
printf '%s\n' "$tag"
}
git_try_checkout() {
# Try a series of refs and checkout when found.
local want="$1" ref=""
if git rev-parse --git-dir >/dev/null 2>&1; then
git fetch --tags --force --prune --depth=1 || true
if git rev-parse "refs/tags/v${want}" >/dev/null 2>&1; then
ref="v${want}"
elif git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then
ref="${want}"
elif git rev-parse --verify "${want}" >/dev/null 2>&1; then
ref="${want}"
fi
if [[ -n "$ref" ]]; then
echo "[OK] Found ref '${ref}', checking out..."
git checkout -f "${ref}"
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
return 0
fi
fi
return 1
}
if git rev-parse --git-dir >/dev/null 2>&1; then
if [[ -n "${VERSION_ARG:-}" ]]; then
echo "[*] Trying to switch v2rayN repo to version: ${VERSION_ARG}"
if git_try_checkout "${VERSION_ARG#v}"; then
VERSION="${VERSION_ARG#v}"
else
echo "[WARN] Tag '${VERSION_ARG}' not found."
ch="$(choose_channel)"
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
VERSION="${VERSION#v}"
else
echo "[*] Resolving ${ch} tag from GitHub releases..."
tag=""
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
if [[ -z "$tag" ]]; then
echo "[WARN] Failed to resolve prerelease tag, falling back to latest."
tag="$(get_latest_tag_latest || true)"
fi
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
fi
fi
else
ch="$(choose_channel)"
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
VERSION="${VERSION#v}"
else
echo "[*] Resolving ${ch} tag from GitHub releases..."
tag=""
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
if [[ -z "$tag" ]]; then
echo "[WARN] Failed to resolve prerelease tag, falling back to latest."
tag="$(get_latest_tag_latest || true)"
fi
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
fi
fi
else
echo "[WARN] Current directory is not a git repo; cannot checkout version. Proceeding on current tree."
VERSION="${VERSION_ARG:-}"
if [[ -z "$VERSION" ]]; then
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
fi
VERSION="${VERSION#v}"
fi
echo "[*] GUI version resolved as: ${VERSION}"
# ===== Helpers for core/rules download (use RID_DIR for arch sync) =====================
download_xray() {
# Download Xray core and install to outdir/xray
local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
mkdir -p "$outdir"
if [[ -n "${XRAY_VER:-}" ]]; then ver="${XRAY_VER}"; fi
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
fi
[[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; }
if [[ "$RID_DIR" == "linux-arm64" ]]; then
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip"
else
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip"
fi
echo "[+] Download xray: $url"
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
curl -fL "$url" -o "$tmp/$zipname"
unzip -q "$tmp/$zipname" -d "$tmp"
install -Dm755 "$tmp/xray" "$outdir/xray"
}
download_singbox() {
# Download sing-box core and install to outdir/sing-box
local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
mkdir -p "$outdir"
if [[ -n "${SING_VER:-}" ]]; then ver="${SING_VER}"; fi
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
fi
[[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; }
if [[ "$RID_DIR" == "linux-arm64" ]]; then
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz"
else
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz"
fi
echo "[+] Download sing-box: $url"
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
curl -fL "$url" -o "$tmp/$tarname"
tar -C "$tmp" -xzf "$tmp/$tarname"
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)"
[[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; return 1; }
install -Dm755 "$bin" "$outdir/sing-box"
}
# ---- 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/xray/
unify_geo_layout() {
local outroot="$1"
mkdir -p "$outroot/bin/xray"
local srcs=( \
"$outroot/bin/geosite.dat" \
"$outroot/bin/geoip.dat" \
"$outroot/bin/geoip-only-cn-private.dat" \
"$outroot/bin/Country.mmdb" \
"$outroot/bin/geoip.metadb" \
)
for s in "${srcs[@]}"; do
if [[ -f "$s" ]]; then
mv -f "$s" "$outroot/bin/xray/$(basename "$s")"
fi
done
}
# Download geo/rule assets; then unify to bin/xray/
download_geo_assets() {
local outroot="$1"
local bin_dir="$outroot/bin"
local srss_dir="$bin_dir/srss"
mkdir -p "$bin_dir" "$srss_dir"
echo "[+] Download Xray Geo to ${bin_dir}"
curl -fsSL -o "$bin_dir/geosite.dat" \
"https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat"
curl -fsSL -o "$bin_dir/geoip.dat" \
"https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat"
curl -fsSL -o "$bin_dir/geoip-only-cn-private.dat" \
"https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat"
curl -fsSL -o "$bin_dir/Country.mmdb" \
"https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
echo "[+] Download sing-box rule DB & rule-sets"
curl -fsSL -o "$bin_dir/geoip.metadb" \
"https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" || true
for f in \
geoip-private.srs geoip-cn.srs geoip-facebook.srs geoip-fastly.srs \
geoip-google.srs geoip-netflix.srs geoip-telegram.srs geoip-twitter.srs; do
curl -fsSL -o "$srss_dir/$f" \
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geoip/$f" || true
done
for f in \
geosite-cn.srs geosite-gfw.srs geosite-greatfire.srs \
geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do
curl -fsSL -o "$srss_dir/$f" \
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done
# Unify to bin/xray/
unify_geo_layout "$outroot"
}
# Prefer the prebuilt v2rayN core bundle; then unify geo layout
download_v2rayn_bundle() {
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.zip"
else
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip"
fi
echo "[+] Try v2rayN bundle archive: $url"
local tmp zipname
tmp="$(mktemp -d)"; zipname="$tmp/v2rayn.zip"
curl -fL "$url" -o "$zipname" || { echo "[!] Bundle download failed"; return 1; }
unzip -q "$zipname" -d "$tmp" || { echo "[!] Bundle unzip failed"; return 1; }
if [[ -d "$tmp/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$tmp/bin/" "$outroot/bin/"
else
rsync -a "$tmp/" "$outroot/"
fi
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
# keep mihomo
# find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
local nested_dir
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
if [[ -n "${nested_dir:-}" && -d "$nested_dir/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$nested_dir/bin/" "$outroot/bin/"
rm -rf "$nested_dir"
fi
# Unify to bin/xray/
unify_geo_layout "$outroot"
echo "[+] Bundle extracted to $outroot"
}
# ===== Build results collection for --arch all ========================================
BUILT_RPMS=() # Will collect absolute paths of built RPMs
BUILT_ALL=0 # Flag to know if we should print the final summary
# ===== Build (single-arch) function ====================================================
build_for_arch() {
# $1: target short arch: x64 | arm64
local short="$1"
local rid rpm_target archdir
case "$short" in
x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;;
arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;;
*) echo "[ERROR] Unknown arch '$short' (use x64|arm64)"; return 1;;
esac
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
# .NET publish (self-contained) for this RID
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net8.0" || true
dotnet restore "$PROJECT"
dotnet publish "$PROJECT" \
-c Release -r "$rid" \
-p:PublishSingleFile=false \
-p:SelfContained=true \
-p:IncludeNativeLibrariesForSelfExtract=true
# Per-arch variables (scoped)
local RID_DIR="$rid"
local PUBDIR
PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish"
[[ -d "$PUBDIR" ]]
# Make RID_DIR visible to download helpers (they read this var)
export RID_DIR
# Per-arch working area
local PKGROOT="v2rayN-publish"
local WORKDIR
WORKDIR="$(mktemp -d)"
trap '[[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"' RETURN
# rpmbuild topdir selection
local TOPDIR SPECDIR SOURCEDIR USE_TOPDIR_DEFINE
if [[ "$ID" =~ ^(rhel|rocky|almalinux|centos)$ ]]; then
rpmdev-setuptree
TOPDIR="${HOME}/rpmbuild"
SPECDIR="${TOPDIR}/SPECS"
SOURCEDIR="${TOPDIR}/SOURCES"
USE_TOPDIR_DEFINE=0
else
TOPDIR="${WORKDIR}/rpmbuild"
SPECDIR="${TOPDIR}/SPECS}"
SOURCEDIR="${TOPDIR}/SOURCES"
mkdir -p "${SPECDIR}" "${SOURCEDIR}" "${TOPDIR}/BUILD" "${TOPDIR}/RPMS" "${TOPDIR}/SRPMS"
USE_TOPDIR_DEFINE=1
fi
# Stage publish content
mkdir -p "$WORKDIR/$PKGROOT"
cp -a "$PUBDIR/." "$WORKDIR/$PKGROOT/"
# Optional icon
local ICON_CANDIDATE
ICON_CANDIDATE="$(dirname "$PROJECT")/../v2rayN.Desktop/v2rayN.png"
[[ -f "$ICON_CANDIDATE" ]] && cp "$ICON_CANDIDATE" "$WORKDIR/$PKGROOT/v2rayn.png" || true
# Prepare bin structure
mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box"
# Bundle / cores per-arch
if [[ "$FORCE_NETCORE" -eq 0 ]]; then
if download_v2rayn_bundle "$WORKDIR/$PKGROOT"; then
echo "[*] Using v2rayN bundle archive."
else
echo "[*] Bundle failed, fallback to separate core + rules."
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
fi
else
echo "[*] --netcore specified: use separate core + rules."
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
# ---- REQUIRED: always fetch mihomo in netcore mode, per-arch ----
download_mihomo "$WORKDIR/$PKGROOT" || echo "[!] mihomo download failed (skipped)"
fi
# Tarball
mkdir -p "$SOURCEDIR"
tar -C "$WORKDIR" -czf "$SOURCEDIR/$PKGROOT.tar.gz" "$PKGROOT"
# SPEC
local SPECFILE="$SPECDIR/v2rayN.spec"
mkdir -p "$SPECDIR"
cat > "$SPECFILE" <<'SPEC'
%global debug_package %{nil}
%undefine _debuginfo_subpackages
%undefine _debugsource_packages
# Ignore outdated LTTng dependencies incorrectly reported by the .NET runtime (to avoid installation failures)
%global __requires_exclude ^liblttng-ust\.so\..*$
Name: v2rayN
Version: __VERSION__
Release: 1%{?dist}
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64)
License: GPL-3.0-only
URL: https://github.com/2dust/v2rayN
BugURL: https://github.com/2dust/v2rayN/issues
ExclusiveArch: aarch64 x86_64
Source0: __PKGROOT__.tar.gz
# Runtime dependencies (Avalonia / X11 / Fonts / GL)
Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon
Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL
%description
v2rayN Linux for Red Hat Enterprise Linux
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS
For more information, Please visit our website
https://github.com/2dust/v2rayN
%prep
%setup -q -n __PKGROOT__
%build
# no build
%install
install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/
# Launcher (prefer native ELF first, then DLL fallback; also create Geo symlinks for the user)
install -dm0755 %{buildroot}%{_bindir}
cat > %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash
set -euo pipefail
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
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi
# DLL fallback
for dll in v2rayN.Desktop.dll v2rayN.dll; do
if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi
done
echo "v2rayN launcher: no executable found in $DIR" >&2
ls -l "$DIR" >&2 || true
exit 1
EOF
chmod 0755 %{buildroot}%{_bindir}/v2rayn
# Desktop file
install -dm0755 %{buildroot}%{_datadir}/applications
cat > %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=v2rayN
Comment=v2rayN for Red Hat Enterprise Linux
Exec=v2rayn
Icon=v2rayn
Terminal=false
Categories=Network;
EOF
# Icon
if [ -f "%{_builddir}/__PKGROOT__/v2rayn.png" ]; then
install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
fi
%post
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
%postun
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
%files
%{_bindir}/v2rayn
/opt/v2rayN
%{_datadir}/applications/v2rayn.desktop
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
SPEC
# Autostart injection (inside %install) and %files entry
if [[ "$AUTOSTART" -eq 1 ]]; then
awk '
BEGIN{ins=0}
/^%post$/ && !ins {
print "# --- Autostart (.desktop) ---"
print "install -dm0755 %{buildroot}/etc/xdg/autostart"
print "cat > %{buildroot}/etc/xdg/autostart/v2rayn.desktop << '\''EOF'\''"
print "[Desktop Entry]"
print "Type=Application"
print "Name=v2rayN (Autostart)"
print "Exec=v2rayn"
print "X-GNOME-Autostart-enabled=true"
print "NoDisplay=false"
print "EOF"
ins=1
}
{print}
' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE"
awk '
BEGIN{infiles=0; done=0}
/^%files$/ {infiles=1}
infiles && done==0 && $0 ~ /%{_datadir}\/icons\/hicolor\/256x256\/apps\/v2rayn\.png/ {
print
print "%config(noreplace) /etc/xdg/autostart/v2rayn.desktop"
done=1
next
}
{print}
' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE"
fi
# Replace placeholders
sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE"
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE"
# ----- Select proper 'strip' per target arch on Ubuntu only (cross-binutils) -----
# NOTE: We define only __strip to point to the target-arch strip.
# DO NOT override __brp_strip (it must stay the brp script path).
local STRIP_ARGS=()
if [[ "$ID" == "ubuntu" ]]; then
local STRIP_BIN=""
if [[ "$short" == "x64" ]]; then
STRIP_BIN="/usr/bin/x86_64-linux-gnu-strip"
else
STRIP_BIN="/usr/bin/aarch64-linux-gnu-strip"
fi
if [[ -x "$STRIP_BIN" ]]; then
STRIP_ARGS=( --define "__strip $STRIP_BIN" )
fi
fi
# Build RPM for this arch (force rpm --target to match compile arch)
if [[ "$USE_TOPDIR_DEFINE" -eq 1 ]]; then
rpmbuild -ba "$SPECFILE" --define "_topdir $TOPDIR" --target "$rpm_target" "${STRIP_ARGS[@]}"
else
rpmbuild -ba "$SPECFILE" --target "$rpm_target" "${STRIP_ARGS[@]}"
fi
# Copy temporary rpmbuild to ~/rpmbuild on Debian/Ubuntu path
if [[ "$USE_TOPDIR_DEFINE" -eq 1 ]]; then
mkdir -p "$HOME/rpmbuild"
rsync -a "$TOPDIR"/ "$HOME/rpmbuild"/
TOPDIR="$HOME/rpmbuild"
fi
echo "Build done for $short. RPM at:"
local f
for f in "${TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do
[[ -e "$f" ]] || continue
echo " $f"
BUILT_RPMS+=("$f")
done
}
# ===== Arch selection and build orchestration =========================================
case "${ARCH_OVERRIDE:-}" in
"")
# No --arch: use host architecture
if [[ "$host_arch" == "aarch64" ]]; then
build_for_arch arm64
else
build_for_arch x64
fi
;;
x64|amd64)
build_for_arch x64
;;
arm64|aarch64)
build_for_arch arm64
;;
all)
BUILT_ALL=1
# Build x64 and arm64 separately; each package contains its own arch-only binaries.
build_for_arch x64
build_for_arch arm64
;;
*)
echo "[ERROR] Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all."
exit 1
;;
esac
# ===== Final summary if building both arches ==========================================
if [[ "$BUILT_ALL" -eq 1 ]]; then
echo ""
echo "================ Build Summary (both architectures) ================"
if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then
for rp in "${BUILT_RPMS[@]}"; do
echo "$rp"
done
else
echo "[WARN] No RPMs detected in summary (check build logs above)."
fi
echo "==================================================================="
fi

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.14.1</Version> <Version>7.14.6</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -5,10 +5,10 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.3" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.4" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.3" /> <PackageVersion Include="Avalonia.Desktop" Version="11.3.4" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.3" /> <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.4" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.3" /> <PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.4" />
<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" />
@@ -20,7 +20,7 @@
<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.9" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.9" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.9" />
<PackageVersion Include="Splat.NLog" Version="15.4.1" /> <PackageVersion Include="Splat.NLog" Version="15.5.3" />
<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

@@ -4,7 +4,7 @@ using ZXing.SkiaSharp;
namespace ServiceLib.Common; namespace ServiceLib.Common;
public class QRCodeHelper public class QRCodeUtils
{ {
public static byte[]? GenQRCode(string? url) public static byte[]? GenQRCode(string? url)
{ {

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,7 +6,6 @@ public enum EViewAction
ShowYesNo, ShowYesNo,
SaveFileDialog, SaveFileDialog,
AddBatchRoutingRulesYesNo, AddBatchRoutingRulesYesNo,
AdjustMainLvColWidth,
SetClipboardData, SetClipboardData,
AddServerViaClipboard, AddServerViaClipboard,
ImportRulesFromClipboard, ImportRulesFromClipboard,
@@ -16,7 +15,6 @@ public enum EViewAction
ShowHideWindow, ShowHideWindow,
ScanScreenTask, ScanScreenTask,
ScanImageTask, ScanImageTask,
Shutdown,
BrowseServer, BrowseServer,
ImportRulesFromFile, ImportRulesFromFile,
InitSettingFont, InitSettingFont,
@@ -32,16 +30,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

@@ -289,6 +289,31 @@ public class Global
"sing_box" "sing_box"
]; ];
public static readonly HashSet<EConfigType> XraySupportConfigType =
[
EConfigType.VMess,
EConfigType.VLESS,
EConfigType.Shadowsocks,
EConfigType.Trojan,
EConfigType.WireGuard,
EConfigType.SOCKS,
EConfigType.HTTP,
];
public static readonly HashSet<EConfigType> SingboxSupportConfigType =
[
EConfigType.VMess,
EConfigType.VLESS,
EConfigType.Shadowsocks,
EConfigType.Trojan,
EConfigType.Hysteria2,
EConfigType.TUIC,
EConfigType.Anytls,
EConfigType.WireGuard,
EConfigType.SOCKS,
EConfigType.HTTP,
];
public static readonly List<string> DomainStrategies = public static readonly List<string> DomainStrategies =
[ [
AsIs, AsIs,
@@ -535,6 +560,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" },
}; };

View File

@@ -1,11 +1,13 @@
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.Handler; global using ServiceLib.Handler;
global using ServiceLib.Helper;
global using ServiceLib.Manager;
global using ServiceLib.Handler.Fmt; global using ServiceLib.Handler.Fmt;
global using ServiceLib.Services; global using ServiceLib.Services;
global using ServiceLib.Services.Statistics; global using ServiceLib.Services.Statistics;
global using ServiceLib.Services.CoreConfig; global using ServiceLib.Services.CoreConfig;
global using ServiceLib.Models; global using ServiceLib.Models;
global using ServiceLib.Resx; global using ServiceLib.Resx;
global using ServiceLib.Handler.SysProxy; global using ServiceLib.Handler.SysProxy;

View File

@@ -0,0 +1,21 @@
using System.Reactive;
using System.Reactive.Subjects;
namespace ServiceLib.Handler;
public static class AppEvents
{
public static readonly Subject<Unit> ProfilesRefreshRequested = new();
public static readonly Subject<string> SendSnackMsgRequested = new();
public static readonly Subject<string> SendMsgViewRequested = new();
public static readonly Subject<Unit> AppExitRequested = new();
public static readonly Subject<bool> ShutdownRequested = new();
public static readonly Subject<Unit> AdjustMainLvColWidthRequested = new();
public static readonly Subject<ServerSpeedItem> DispatcherStatisticsRequested = new();
}

View File

@@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
namespace ServiceLib.Handler; namespace ServiceLib.Handler;
public class ConfigHandler public static class ConfigHandler
{ {
private static readonly string _configRes = Global.ConfigFileName; private static readonly string _configRes = Global.ConfigFileName;
private static readonly string _tag = "ConfigHandler"; private static readonly string _tag = "ConfigHandler";
@@ -216,7 +216,7 @@ public class ConfigHandler
/// <returns>Result of the operation (0 if successful, -1 if failed)</returns> /// <returns>Result of the operation (0 if successful, -1 if failed)</returns>
public static async Task<int> AddServer(Config config, ProfileItem profileItem) public static async Task<int> AddServer(Config config, ProfileItem profileItem)
{ {
var item = await AppHandler.Instance.GetProfileItem(profileItem.IndexId); var item = await AppManager.Instance.GetProfileItem(profileItem.IndexId);
if (item is null) if (item is null)
{ {
item = profileItem; item = profileItem;
@@ -336,7 +336,7 @@ public class ConfigHandler
{ {
foreach (var it in indexes) foreach (var it in indexes)
{ {
var item = await AppHandler.Instance.GetProfileItem(it.IndexId); var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (item is null) if (item is null)
{ {
continue; continue;
@@ -418,7 +418,7 @@ public class ConfigHandler
/// <returns>The default profile item or null if none exists</returns> /// <returns>The default profile item or null if none exists</returns>
public static async Task<ProfileItem?> GetDefaultServer(Config config) public static async Task<ProfileItem?> GetDefaultServer(Config config)
{ {
var item = await AppHandler.Instance.GetProfileItem(config.IndexId); var item = await AppManager.Instance.GetProfileItem(config.IndexId);
if (item is null) if (item is null)
{ {
var item2 = await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(); var item2 = await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync();
@@ -449,7 +449,7 @@ public class ConfigHandler
for (int i = 0; i < lstProfile.Count; i++) for (int i = 0; i < lstProfile.Count; i++)
{ {
ProfileExHandler.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10); ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
} }
var sort = 0; var sort = 0;
@@ -461,7 +461,7 @@ public class ConfigHandler
{ {
return 0; return 0;
} }
sort = ProfileExHandler.Instance.GetSort(lstProfile.First().IndexId) - 1; sort = ProfileExManager.Instance.GetSort(lstProfile.First().IndexId) - 1;
break; break;
} }
@@ -471,7 +471,7 @@ public class ConfigHandler
{ {
return 0; return 0;
} }
sort = ProfileExHandler.Instance.GetSort(lstProfile[index - 1].IndexId) - 1; sort = ProfileExManager.Instance.GetSort(lstProfile[index - 1].IndexId) - 1;
break; break;
} }
@@ -482,7 +482,7 @@ public class ConfigHandler
{ {
return 0; return 0;
} }
sort = ProfileExHandler.Instance.GetSort(lstProfile[index + 1].IndexId) + 1; sort = ProfileExManager.Instance.GetSort(lstProfile[index + 1].IndexId) + 1;
break; break;
} }
@@ -492,7 +492,7 @@ public class ConfigHandler
{ {
return 0; return 0;
} }
sort = ProfileExHandler.Instance.GetSort(lstProfile[^1].IndexId) + 1; sort = ProfileExManager.Instance.GetSort(lstProfile[^1].IndexId) + 1;
break; break;
} }
@@ -501,7 +501,7 @@ public class ConfigHandler
break; break;
} }
ProfileExHandler.Instance.SetSort(lstProfile[index].IndexId, sort); ProfileExManager.Instance.SetSort(lstProfile[index].IndexId, sort);
return await Task.FromResult(0); return await Task.FromResult(0);
} }
@@ -559,7 +559,7 @@ public class ConfigHandler
/// <returns>0 if successful, -1 if failed</returns> /// <returns>0 if successful, -1 if failed</returns>
public static async Task<int> EditCustomServer(Config config, ProfileItem profileItem) public static async Task<int> EditCustomServer(Config config, ProfileItem profileItem)
{ {
var item = await AppHandler.Instance.GetProfileItem(profileItem.IndexId); var item = await AppManager.Instance.GetProfileItem(profileItem.IndexId);
if (item is null) if (item is null)
{ {
item = profileItem; item = profileItem;
@@ -601,7 +601,7 @@ public class ConfigHandler
profileItem.Id = profileItem.Id.TrimEx(); profileItem.Id = profileItem.Id.TrimEx();
profileItem.Security = profileItem.Security.TrimEx(); profileItem.Security = profileItem.Security.TrimEx();
if (!AppHandler.Instance.GetShadowsocksSecurities(profileItem).Contains(profileItem.Security)) if (!AppManager.Instance.GetShadowsocksSecurities(profileItem).Contains(profileItem.Security))
{ {
return -1; return -1;
} }
@@ -829,13 +829,13 @@ public class ConfigHandler
/// <returns>0 if successful, -1 if failed</returns> /// <returns>0 if successful, -1 if failed</returns>
public static async Task<int> SortServers(Config config, string subId, string colName, bool asc) public static async Task<int> SortServers(Config config, string subId, string colName, bool asc)
{ {
var lstModel = await AppHandler.Instance.ProfileItems(subId, ""); var lstModel = await AppManager.Instance.ProfileItems(subId, "");
if (lstModel.Count <= 0) if (lstModel.Count <= 0)
{ {
return -1; return -1;
} }
var lstServerStat = (config.GuiItem.EnableStatistics ? StatisticsHandler.Instance.ServerStat : null) ?? []; var lstServerStat = (config.GuiItem.EnableStatistics ? StatisticsManager.Instance.ServerStat : null) ?? [];
var lstProfileExs = await ProfileExHandler.Instance.GetProfileExs(); var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
var lstProfile = (from t in lstModel var lstProfile = (from t in lstModel
join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b
from t22 in t2b.DefaultIfEmpty() from t22 in t2b.DefaultIfEmpty()
@@ -905,7 +905,7 @@ public class ConfigHandler
for (var i = 0; i < lstProfile.Count; i++) for (var i = 0; i < lstProfile.Count; i++)
{ {
ProfileExHandler.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10); ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
} }
switch (name) switch (name)
{ {
@@ -914,7 +914,7 @@ public class ConfigHandler
var maxSort = lstProfile.Max(t => t.Sort) + 10; var maxSort = lstProfile.Max(t => t.Sort) + 10;
foreach (var item in lstProfile.Where(item => item.Delay <= 0)) foreach (var item in lstProfile.Where(item => item.Delay <= 0))
{ {
ProfileExHandler.Instance.SetSort(item.IndexId, maxSort); ProfileExManager.Instance.SetSort(item.IndexId, maxSort);
} }
break; break;
@@ -924,7 +924,7 @@ public class ConfigHandler
var maxSort = lstProfile.Max(t => t.Sort) + 10; var maxSort = lstProfile.Max(t => t.Sort) + 10;
foreach (var item in lstProfile.Where(item => item.Speed <= 0)) foreach (var item in lstProfile.Where(item => item.Speed <= 0))
{ {
ProfileExHandler.Instance.SetSort(item.IndexId, maxSort); ProfileExManager.Instance.SetSort(item.IndexId, maxSort);
} }
break; break;
@@ -982,7 +982,7 @@ public class ConfigHandler
/// <returns>Tuple with total count and remaining count after deduplication</returns> /// <returns>Tuple with total count and remaining count after deduplication</returns>
public static async Task<Tuple<int, int>> DedupServerList(Config config, string subId) public static async Task<Tuple<int, int>> DedupServerList(Config config, string subId)
{ {
var lstProfile = await AppHandler.Instance.ProfileItems(subId); var lstProfile = await AppManager.Instance.ProfileItems(subId);
if (lstProfile == null) if (lstProfile == null)
{ {
return new Tuple<int, int>(0, 0); return new Tuple<int, int>(0, 0);
@@ -1052,15 +1052,15 @@ public class ConfigHandler
if (profileItem.IndexId.IsNullOrEmpty()) if (profileItem.IndexId.IsNullOrEmpty())
{ {
profileItem.IndexId = Utils.GetGuid(false); profileItem.IndexId = Utils.GetGuid(false);
maxSort = ProfileExHandler.Instance.GetMaxSort(); maxSort = ProfileExManager.Instance.GetMaxSort();
} }
if (!toFile && maxSort < 0) if (!toFile && maxSort < 0)
{ {
maxSort = ProfileExHandler.Instance.GetMaxSort(); maxSort = ProfileExManager.Instance.GetMaxSort();
} }
if (maxSort > 0) if (maxSort > 0)
{ {
ProfileExHandler.Instance.SetSort(profileItem.IndexId, maxSort + 1); ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1);
} }
if (toFile) if (toFile)
@@ -1120,7 +1120,7 @@ public class ConfigHandler
{ {
try try
{ {
var item = await AppHandler.Instance.GetProfileItem(indexId); var item = await AppManager.Instance.GetProfileItem(indexId);
if (item == null) if (item == null)
{ {
return 0; return 0;
@@ -1165,7 +1165,7 @@ public class ConfigHandler
return result; return result;
} }
var profileItem = await AppHandler.Instance.GetProfileItem(indexId) ?? new(); var profileItem = await AppManager.Instance.GetProfileItem(indexId) ?? new();
profileItem.IndexId = indexId; profileItem.IndexId = indexId;
if (coreType == ECoreType.Xray) if (coreType == ECoreType.Xray)
{ {
@@ -1211,7 +1211,7 @@ public class ConfigHandler
ConfigType = EConfigType.SOCKS, ConfigType = EConfigType.SOCKS,
Address = Global.Loopback, Address = Global.Loopback,
Sni = node.Address, //Tun2SocksAddress Sni = node.Address, //Tun2SocksAddress
Port = AppHandler.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))
@@ -1238,12 +1238,12 @@ public class ConfigHandler
/// <returns>Number of removed servers or -1 if failed</returns> /// <returns>Number of removed servers or -1 if failed</returns>
public static async Task<int> RemoveInvalidServerResult(Config config, string subid) public static async Task<int> RemoveInvalidServerResult(Config config, string subid)
{ {
var lstModel = await AppHandler.Instance.ProfileItems(subid, ""); var lstModel = await AppManager.Instance.ProfileItems(subid, "");
if (lstModel is { Count: <= 0 }) if (lstModel is { Count: <= 0 })
{ {
return -1; return -1;
} }
var lstProfileExs = await ProfileExHandler.Instance.GetProfileExs(); var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
var lstProfile = (from t in lstModel var lstProfile = (from t in lstModel
join t2 in lstProfileExs on t.IndexId equals t2.IndexId join t2 in lstProfileExs on t.IndexId equals t2.IndexId
where t2.Delay == -1 where t2.Delay == -1
@@ -1279,7 +1279,7 @@ public class ConfigHandler
if (isSub && subid.IsNotEmpty()) if (isSub && subid.IsNotEmpty())
{ {
await RemoveServersViaSubid(config, subid, isSub); await RemoveServersViaSubid(config, subid, isSub);
subFilter = (await AppHandler.Instance.GetSubItem(subid))?.Filter ?? ""; subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? "";
} }
var countServers = 0; var countServers = 0;
@@ -1363,7 +1363,7 @@ public class ConfigHandler
return -1; return -1;
} }
var subItem = await AppHandler.Instance.GetSubItem(subid); var subItem = await AppManager.Instance.GetSubItem(subid);
var subRemarks = subItem?.Remarks; var subRemarks = subItem?.Remarks;
var preSocksPort = subItem?.PreSocksPort; var preSocksPort = subItem?.PreSocksPort;
@@ -1519,7 +1519,7 @@ public class ConfigHandler
ProfileItem? activeProfile = null; ProfileItem? activeProfile = null;
if (isSub && subid.IsNotEmpty()) if (isSub && subid.IsNotEmpty())
{ {
lstOriSub = await AppHandler.Instance.ProfileItems(subid); lstOriSub = await AppManager.Instance.ProfileItems(subid);
activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId); activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId);
} }
@@ -1551,7 +1551,7 @@ public class ConfigHandler
//Select active node //Select active node
if (activeProfile != null) if (activeProfile != null)
{ {
var lstSub = await AppHandler.Instance.ProfileItems(subid); var lstSub = await AppManager.Instance.ProfileItems(subid);
var existItem = lstSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == activeProfile.Remarks : CompareProfileItem(t, activeProfile, true)); var existItem = lstSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == activeProfile.Remarks : CompareProfileItem(t, activeProfile, true));
if (existItem != null) if (existItem != null)
{ {
@@ -1562,13 +1562,13 @@ public class ConfigHandler
//Keep the last traffic statistics //Keep the last traffic statistics
if (lstOriSub != null) if (lstOriSub != null)
{ {
var lstSub = await AppHandler.Instance.ProfileItems(subid); var lstSub = await AppManager.Instance.ProfileItems(subid);
foreach (var item in lstSub) foreach (var item in lstSub)
{ {
var existItem = lstOriSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == item.Remarks : CompareProfileItem(t, item, true)); var existItem = lstOriSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == item.Remarks : CompareProfileItem(t, item, true));
if (existItem != null) if (existItem != null)
{ {
await StatisticsHandler.Instance.CloneServerStatItem(existItem.IndexId, item.IndexId); await StatisticsManager.Instance.CloneServerStatItem(existItem.IndexId, item.IndexId);
} }
} }
} }
@@ -1608,7 +1608,7 @@ public class ConfigHandler
if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost)) if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost))
{ {
//TODO Temporary reminder to be removed later //TODO Temporary reminder to be removed later
NoticeHandler.Instance.Enqueue(ResUI.InsecureUrlProtocol); NoticeManager.Instance.Enqueue(ResUI.InsecureUrlProtocol);
//return -1; //return -1;
} }
@@ -1626,7 +1626,7 @@ public class ConfigHandler
/// <returns>0 if successful, -1 if failed</returns> /// <returns>0 if successful, -1 if failed</returns>
public static async Task<int> AddSubItem(Config config, SubItem subItem) public static async Task<int> AddSubItem(Config config, SubItem subItem)
{ {
var item = await AppHandler.Instance.GetSubItem(subItem.Id); var item = await AppManager.Instance.GetSubItem(subItem.Id);
if (item is null) if (item is null)
{ {
item = subItem; item = subItem;
@@ -1658,7 +1658,7 @@ public class ConfigHandler
var maxSort = 0; var maxSort = 0;
if (await SQLiteHelper.Instance.TableAsync<SubItem>().CountAsync() > 0) if (await SQLiteHelper.Instance.TableAsync<SubItem>().CountAsync() > 0)
{ {
var lstSubs = (await AppHandler.Instance.SubItems()); var lstSubs = (await AppManager.Instance.SubItems());
maxSort = lstSubs.LastOrDefault()?.Sort ?? 0; maxSort = lstSubs.LastOrDefault()?.Sort ?? 0;
} }
item.Sort = maxSort + 1; item.Sort = maxSort + 1;
@@ -1712,7 +1712,7 @@ public class ConfigHandler
/// <returns>0 if successful</returns> /// <returns>0 if successful</returns>
public static async Task<int> DeleteSubItem(Config config, string id) public static async Task<int> DeleteSubItem(Config config, string id)
{ {
var item = await AppHandler.Instance.GetSubItem(id); var item = await AppManager.Instance.GetSubItem(id);
if (item is null) if (item is null)
{ {
return 0; return 0;
@@ -1896,7 +1896,7 @@ public class ConfigHandler
/// <returns>0 if successful</returns> /// <returns>0 if successful</returns>
public static async Task<int> SetDefaultRouting(Config config, RoutingItem routingItem) public static async Task<int> SetDefaultRouting(Config config, RoutingItem routingItem)
{ {
var items = await AppHandler.Instance.RoutingItems(); var items = await AppManager.Instance.RoutingItems();
if (items.Any(t => t.Id == routingItem.Id && t.IsActive == true)) if (items.Any(t => t.Id == routingItem.Id && t.IsActive == true))
{ {
return -1; return -1;
@@ -1976,7 +1976,7 @@ public class ConfigHandler
if (template == null) if (template == null)
return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback
var items = await AppHandler.Instance.RoutingItems(); var items = await AppManager.Instance.RoutingItems();
var maxSort = items.Count; var maxSort = items.Count;
if (!blImportAdvancedRules && items.Where(t => t.Remarks.StartsWith(template.Version)).ToList().Count > 0) if (!blImportAdvancedRules && items.Where(t => t.Remarks.StartsWith(template.Version)).ToList().Count > 0)
{ {
@@ -2023,14 +2023,14 @@ public class ConfigHandler
public static async Task<int> InitBuiltinRouting(Config config, bool blImportAdvancedRules = false) public static async Task<int> InitBuiltinRouting(Config config, bool blImportAdvancedRules = false)
{ {
var ver = "V3-"; var ver = "V3-";
var items = await AppHandler.Instance.RoutingItems(); var items = await AppManager.Instance.RoutingItems();
//TODO Temporary code to be removed later //TODO Temporary code to be removed later
var lockItem = items?.FirstOrDefault(t => t.Locked == true); var lockItem = items?.FirstOrDefault(t => t.Locked == true);
if (lockItem != null) if (lockItem != null)
{ {
await ConfigHandler.RemoveRoutingItem(lockItem); await ConfigHandler.RemoveRoutingItem(lockItem);
items = await AppHandler.Instance.RoutingItems(); items = await AppManager.Instance.RoutingItems();
} }
if (!blImportAdvancedRules && items.Count > 0) if (!blImportAdvancedRules && items.Count > 0)
@@ -2107,7 +2107,7 @@ public class ConfigHandler
/// <returns>0 if successful</returns> /// <returns>0 if successful</returns>
public static async Task<int> InitBuiltinDNS(Config config) public static async Task<int> InitBuiltinDNS(Config config)
{ {
var items = await AppHandler.Instance.DNSItems(); var items = await AppManager.Instance.DNSItems();
// Check existing DNS items and disable those with empty NormalDNS // Check existing DNS items and disable those with empty NormalDNS
var needsUpdate = false; var needsUpdate = false;
@@ -2185,7 +2185,7 @@ public class ConfigHandler
/// <returns>DNS item with configuration from the URL</returns> /// <returns>DNS item with configuration from the URL</returns>
public static async Task<DNSItem> GetExternalDNSItem(ECoreType type, string url) public static async Task<DNSItem> GetExternalDNSItem(ECoreType type, string url)
{ {
var currentItem = await AppHandler.Instance.GetDNSItem(type); var currentItem = await AppManager.Instance.GetDNSItem(type);
var downloadHandle = new DownloadService(); var downloadHandle = new DownloadService();
var templateContent = await downloadHandle.TryDownloadString(url, true, ""); var templateContent = await downloadHandle.TryDownloadString(url, true, "");
@@ -2247,7 +2247,7 @@ public class ConfigHandler
public static async Task<int> InitBuiltinFullConfigTemplate(Config config) public static async Task<int> InitBuiltinFullConfigTemplate(Config config)
{ {
var items = await AppHandler.Instance.FullConfigTemplateItem(); var items = await AppManager.Instance.FullConfigTemplateItem();
if (items.Count <= 0) if (items.Count <= 0)
{ {
var item = new FullConfigTemplateItem() var item = new FullConfigTemplateItem()

View File

@@ -1,27 +1,28 @@
using System.Net;
namespace ServiceLib.Handler; namespace ServiceLib.Handler;
public class ConnectionHandler public static class ConnectionHandler
{ {
private static readonly Lazy<ConnectionHandler> _instance = new(() => new()); private static readonly string _tag = "ConnectionHandler";
public static ConnectionHandler Instance => _instance.Value;
public async Task<string> RunAvailabilityCheck() public static async Task<string> RunAvailabilityCheck()
{ {
var downloadHandle = new DownloadService(); var time = await GetRealPingTime();
var time = await downloadHandle.RunAvailabilityCheck(null); var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None;
var ip = time > 0 ? await GetIPInfo(downloadHandle) ?? Global.None : Global.None;
return string.Format(ResUI.TestMeOutput, time, ip); return string.Format(ResUI.TestMeOutput, time, ip);
} }
private async Task<string?> GetIPInfo(DownloadService downloadHandle) private static async Task<string?> GetIPInfo()
{ {
var url = AppHandler.Instance.Config.SpeedTestItem.IPAPIUrl; var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
if (url.IsNullOrEmpty()) if (url.IsNullOrEmpty())
{ {
return null; return null;
} }
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, true, ""); var result = await downloadHandle.TryDownloadString(url, true, "");
if (result == null) if (result == null)
{ {
@@ -39,4 +40,31 @@ public class ConnectionHandler
return $"({country ?? "unknown"}) {ip}"; return $"({country ?? "unknown"}) {ip}";
} }
private static async Task<int> GetRealPingTime()
{
var responseTime = -1;
try
{
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}");
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
for (var i = 0; i < 2; i++)
{
responseTime = await HttpClientHelper.Instance.GetRealPingTime(url, webProxy, 10);
if (responseTime > 0)
{
break;
}
await Task.Delay(500);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return -1;
}
return responseTime;
}
} }

View File

@@ -3,13 +3,13 @@ namespace ServiceLib.Handler;
/// <summary> /// <summary>
/// Core configuration file processing class /// Core configuration file processing class
/// </summary> /// </summary>
public class CoreConfigHandler public static class CoreConfigHandler
{ {
private static readonly string _tag = "CoreConfigHandler"; private static readonly string _tag = "CoreConfigHandler";
public static async Task<RetResult> GenerateClientConfig(ProfileItem node, string? fileName) public static async Task<RetResult> GenerateClientConfig(ProfileItem node, string? fileName)
{ {
var config = AppHandler.Instance.Config; var config = AppManager.Instance.Config;
var result = new RetResult(); var result = new RetResult();
if (node.ConfigType == EConfigType.Custom) if (node.ConfigType == EConfigType.Custom)
@@ -21,7 +21,7 @@ public class CoreConfigHandler
_ => await GenerateClientCustomConfig(node, fileName) _ => await GenerateClientCustomConfig(node, fileName)
}; };
} }
else if (AppHandler.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box) else if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
{ {
result = await new CoreConfigSingboxService(config).GenerateClientConfigContent(node); result = await new CoreConfigSingboxService(config).GenerateClientConfigContent(node);
} }
@@ -112,11 +112,11 @@ public class CoreConfigHandler
public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, ProfileItem node, ServerTestItem testItem, string fileName) public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, ProfileItem node, ServerTestItem testItem, string fileName)
{ {
var result = new RetResult(); var result = new RetResult();
var initPort = AppHandler.Instance.GetLocalPort(EInboundProtocol.speedtest); var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
var port = Utils.GetFreePort(initPort + testItem.QueueNum); var port = Utils.GetFreePort(initPort + testItem.QueueNum);
testItem.Port = port; testItem.Port = port;
if (AppHandler.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box) if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
{ {
result = await new CoreConfigSingboxService(config).GenerateClientSpeedtestConfig(node, port); result = await new CoreConfigSingboxService(config).GenerateClientSpeedtestConfig(node, port);
} }

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

@@ -1,18 +1,19 @@
namespace ServiceLib.Handler; namespace ServiceLib.Handler;
public 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 AppHandler.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 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 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 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 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

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

View File

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

View File

@@ -3,7 +3,7 @@ using static ServiceLib.Handler.SysProxy.ProxySettingWindows.InternetConnectionO
namespace ServiceLib.Handler.SysProxy; namespace ServiceLib.Handler.SysProxy;
public class ProxySettingWindows public static class ProxySettingWindows
{ {
private const string _regPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings"; private const string _regPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";

View File

@@ -15,7 +15,7 @@ public static class SysProxyHandler
try try
{ {
var port = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks); var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
var exceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", ""); var exceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
if (port <= 0) if (port <= 0)
{ {
@@ -56,7 +56,7 @@ public static class SysProxyHandler
if (type != ESysProxyType.Pac && Utils.IsWindows()) if (type != ESysProxyType.Pac && Utils.IsWindows())
{ {
PacHandler.Instance.Stop(); PacManager.Instance.Stop();
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -90,8 +90,8 @@ public static class SysProxyHandler
private static async Task SetWindowsProxyPac(int port) private static async Task SetWindowsProxyPac(int port)
{ {
var portPac = AppHandler.Instance.GetLocalPort(EInboundProtocol.pac); var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);
await PacHandler.Instance.StartAsync(Utils.GetConfigPath(), port, portPac); await PacManager.Instance.StartAsync(Utils.GetConfigPath(), port, portPac);
var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}"; var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
ProxySettingWindows.SetProxy(strProxy, "", 4); ProxySettingWindows.SetProxy(strProxy, "", 4);
} }

View File

@@ -1,7 +1,7 @@
using System.Net; using System.Net;
using Downloader; using Downloader;
namespace ServiceLib.Common; namespace ServiceLib.Helper;
public class DownloaderHelper public class DownloaderHelper
{ {

View File

@@ -1,8 +1,10 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
namespace ServiceLib.Common; namespace ServiceLib.Helper;
/// <summary> /// <summary>
/// </summary> /// </summary>
@@ -202,4 +204,35 @@ public class HttpClientHelper
} }
} while (isMoreToRead); } while (isMoreToRead);
} }
public async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
{
var responseTime = -1;
try
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout));
using var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
UseProxy = webProxy != null
});
List<int> oneTime = new();
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}
catch //(Exception ex)
{
//Utile.SaveLog(ex.Message, ex);
}
return responseTime;
}
} }

View File

@@ -1,7 +1,7 @@
using System.Collections; using System.Collections;
using SQLite; using SQLite;
namespace ServiceLib.Common; namespace ServiceLib.Helper;
public sealed class SQLiteHelper public sealed class SQLiteHelper
{ {
@@ -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

@@ -1,15 +1,17 @@
namespace ServiceLib.Handler; using System.Reactive;
public sealed class AppHandler namespace ServiceLib.Manager;
public sealed class AppManager
{ {
#region Property #region Property
private static readonly Lazy<AppHandler> _instance = new(() => new()); private static readonly Lazy<AppManager> _instance = new(() => new());
private Config _config; private Config _config;
private int? _statePort; private int? _statePort;
private int? _statePort2; private int? _statePort2;
private Job? _processJob; private Job? _processJob;
public static AppHandler Instance => _instance.Value; public static AppManager Instance => _instance.Value;
public Config Config => _config; public Config Config => _config;
public int StatePort public int StatePort
@@ -34,7 +36,7 @@ public sealed class AppHandler
#endregion Property #endregion Property
#region Init #region App
public bool InitApp() public bool InitApp()
{ {
@@ -87,7 +89,40 @@ public sealed class AppHandler
return true; return true;
} }
#endregion Init public async Task AppExitAsync(bool needShutdown)
{
try
{
Logging.SaveLog("AppExitAsync Begin");
await SysProxyHandler.UpdateSysProxy(_config, true);
AppEvents.AppExitRequested.OnNext(Unit.Default);
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.OnNext(byUser);
}
#endregion App
#region Config #region Config
@@ -97,7 +132,7 @@ public sealed class AppHandler
return localPort + (int)protocol; return localPort + (int)protocol;
} }
public void AddProcess(IntPtr processHandle) public void AddProcess(nint processHandle)
{ {
if (Utils.IsWindows()) if (Utils.IsWindows())
{ {

View File

@@ -1,11 +1,11 @@
using static ServiceLib.Models.ClashProxies; using static ServiceLib.Models.ClashProxies;
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
public sealed class ClashApiHandler public sealed class ClashApiManager
{ {
private static readonly Lazy<ClashApiHandler> instance = new(() => new()); private static readonly Lazy<ClashApiManager> instance = new(() => new());
public static ClashApiHandler Instance => instance.Value; public static ClashApiManager Instance => instance.Value;
private static readonly string _tag = "ClashApiHandler"; private static readonly string _tag = "ClashApiHandler";
private Dictionary<string, ProxiesItem>? _proxies; private Dictionary<string, ProxiesItem>? _proxies;
@@ -35,7 +35,7 @@ public sealed class ClashApiHandler
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 () =>
{ {
@@ -65,7 +65,7 @@ public sealed class ClashApiHandler
return; return;
} }
var urlBase = $"{GetApiUrl()}/proxies"; var urlBase = $"{GetApiUrl()}/proxies";
urlBase += @"/{0}/delay?timeout=10000&url=" + AppHandler.Instance.Config.SpeedTestItem.SpeedPingTestUrl; urlBase += @"/{0}/delay?timeout=10000&url=" + AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var tasks = new List<Task>(); var tasks = new List<Task>();
foreach (var it in lstProxy) foreach (var it in lstProxy)
@@ -79,12 +79,12 @@ public sealed class ClashApiHandler
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, "");
}); });
} }
@@ -182,6 +182,6 @@ public sealed class ClashApiHandler
private string GetApiUrl() private string GetApiUrl()
{ {
return $"{Global.HttpProtocol}{Global.Loopback}:{AppHandler.Instance.StatePort2}"; return $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}";
} }
} }

View File

@@ -3,18 +3,18 @@ using System.Text;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
public class CoreAdminHandler public class CoreAdminManager
{ {
private static readonly Lazy<CoreAdminHandler> _instance = new(() => new()); private static readonly Lazy<CoreAdminManager> _instance = new(() => new());
public static CoreAdminHandler 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 CoreAdminHandler
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 CoreAdminHandler
{ {
if (e.Data.IsNotEmpty()) if (e.Data.IsNotEmpty())
{ {
UpdateFunc(false, e.Data + Environment.NewLine); _ = UpdateFunc(false, e.Data + Environment.NewLine);
} }
} }
@@ -72,7 +72,7 @@ public class CoreAdminHandler
proc.BeginErrorReadLine(); proc.BeginErrorReadLine();
await Task.Delay(10); await Task.Delay(10);
await proc.StandardInput.WriteLineAsync(AppHandler.Instance.LinuxSudoPwd); await proc.StandardInput.WriteLineAsync(AppManager.Instance.LinuxSudoPwd);
await Task.Delay(100); await Task.Delay(100);
if (proc is null or { HasExited: true }) if (proc is null or { HasExited: true })
@@ -103,10 +103,10 @@ public class CoreAdminHandler
var arg = new List<string>() { "-c", $"sudo -S {shFilePath} {_linuxSudoPid}" }; var arg = new List<string>() { "-c", $"sudo -S {shFilePath} {_linuxSudoPid}" };
var result = await Cli.Wrap(Global.LinuxBash) var result = await Cli.Wrap(Global.LinuxBash)
.WithArguments(arg) .WithArguments(arg)
.WithStandardInputPipe(PipeSource.FromString(AppHandler.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

@@ -1,12 +1,12 @@
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
public sealed class CoreInfoHandler public sealed class CoreInfoManager
{ {
private static readonly Lazy<CoreInfoHandler> _instance = new(() => new()); private static readonly Lazy<CoreInfoManager> _instance = new(() => new());
private List<CoreInfo>? _coreInfo; private List<CoreInfo>? _coreInfo;
public static CoreInfoHandler Instance => _instance.Value; public static CoreInfoManager Instance => _instance.Value;
public CoreInfoHandler() public CoreInfoManager()
{ {
InitCoreInfo(); InitCoreInfo();
} }
@@ -80,6 +80,10 @@ public sealed class CoreInfoHandler
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 CoreInfoHandler
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 CoreInfoHandler
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 CoreInfoHandler
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

@@ -1,31 +1,27 @@
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
/// <summary> /// <summary>
/// Core process processing class /// Core process processing class
/// </summary> /// </summary>
public class CoreHandler public class CoreManager
{ {
private static readonly Lazy<CoreHandler> _instance = new(() => new()); private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreHandler Instance => _instance.Value; public static CoreManager Instance => _instance.Value;
private Config _config; private Config _config;
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")
{ {
@@ -39,7 +35,7 @@ public class CoreHandler
if (Utils.IsNonWindows()) if (Utils.IsNonWindows())
{ {
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(); var coreInfo = CoreInfoManager.Instance.GetCoreInfo();
foreach (var it in coreInfo) foreach (var it in coreInfo)
{ {
if (it.CoreType == ECoreType.v2rayN) if (it.CoreType == ECoreType.v2rayN)
@@ -67,7 +63,7 @@ public class CoreHandler
{ {
if (node == null) if (node == null)
{ {
UpdateFunc(false, ResUI.CheckServerSettings); await UpdateFunc(false, ResUI.CheckServerSettings);
return; return;
} }
@@ -75,13 +71,13 @@ public class CoreHandler
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 CoreHandler
await CoreStartPreService(node); await CoreStartPreService(node);
if (_process != null) if (_process != null)
{ {
UpdateFunc(true, $"{node.GetSummary()}"); await UpdateFunc(true, $"{node.GetSummary()}");
} }
} }
@@ -105,16 +101,16 @@ public class CoreHandler
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 = CoreInfoHandler.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);
if (proc is null) if (proc is null)
{ {
@@ -126,7 +122,7 @@ public class CoreHandler
public async Task<int> LoadCoreConfigSpeedtest(ServerTestItem testItem) public async Task<int> LoadCoreConfigSpeedtest(ServerTestItem testItem)
{ {
var node = await AppHandler.Instance.GetProfileItem(testItem.IndexId); var node = await AppManager.Instance.GetProfileItem(testItem.IndexId);
if (node is null) if (node is null)
{ {
return -1; return -1;
@@ -140,8 +136,8 @@ public class CoreHandler
return -1; return -1;
} }
var coreType = AppHandler.Instance.GetCoreType(node, node.ConfigType); var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreInfo = CoreInfoHandler.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);
if (proc is null) if (proc is null)
{ {
@@ -157,7 +153,7 @@ public class CoreHandler
{ {
if (_linuxSudo) if (_linuxSudo)
{ {
await CoreAdminHandler.Instance.KillProcessAsLinuxSudo(); await CoreAdminManager.Instance.KillProcessAsLinuxSudo();
_linuxSudo = false; _linuxSudo = false;
} }
@@ -183,8 +179,8 @@ public class CoreHandler
private async Task CoreStart(ProfileItem node) private async Task CoreStart(ProfileItem node)
{ {
var coreType = _config.RunningCoreType = AppHandler.Instance.GetCoreType(node, node.ConfigType); var coreType = _config.RunningCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog; var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true); var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true);
@@ -199,7 +195,7 @@ public class CoreHandler
{ {
if (_process != null && !_process.HasExited) if (_process != null && !_process.HasExited)
{ {
var coreType = AppHandler.Instance.GetCoreType(node, node.ConfigType); var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType); var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType);
if (itemSocks != null) if (itemSocks != null)
{ {
@@ -208,7 +204,7 @@ public class CoreHandler
var result = await CoreConfigHandler.GenerateClientConfig(itemSocks, fileName); var result = await CoreConfigHandler.GenerateClientConfig(itemSocks, fileName);
if (result.Success) if (result.Success)
{ {
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(preCoreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true); var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true);
if (proc is null) if (proc is null)
{ {
@@ -220,9 +216,9 @@ public class CoreHandler
} }
} }
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
@@ -231,10 +227,10 @@ public class CoreHandler
private async Task<Process?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo) private async Task<Process?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
{ {
var fileName = CoreInfoHandler.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;
} }
@@ -246,8 +242,8 @@ public class CoreHandler
&& Utils.IsNonWindows()) && Utils.IsNonWindows())
{ {
_linuxSudo = true; _linuxSudo = true;
await CoreAdminHandler.Instance.Init(_config, _updateFunc); await CoreAdminManager.Instance.Init(_config, _updateFunc);
return await CoreAdminHandler.Instance.RunProcessAsLinuxSudo(fileName, coreInfo, configPath); return await CoreAdminManager.Instance.RunProcessAsLinuxSudo(fileName, coreInfo, configPath);
} }
return await RunProcessNormal(fileName, coreInfo, configPath, displayLog); return await RunProcessNormal(fileName, coreInfo, configPath, displayLog);
@@ -255,7 +251,7 @@ public class CoreHandler
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 CoreHandler
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 CoreHandler
{ {
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;
@@ -299,7 +299,7 @@ public class CoreHandler
} }
await Task.Delay(100); await Task.Delay(100);
AppHandler.Instance.AddProcess(proc.Handle); AppManager.Instance.AddProcess(proc.Handle);
if (proc is null or { HasExited: true }) if (proc is null or { HasExited: true })
{ {
throw new Exception(ResUI.FailedToRunCore); throw new Exception(ResUI.FailedToRunCore);

View File

@@ -1,11 +1,9 @@
using ReactiveUI; namespace ServiceLib.Manager;
namespace ServiceLib.Handler; public class NoticeManager
public class NoticeHandler
{ {
private static readonly Lazy<NoticeHandler> _instance = new(() => new()); private static readonly Lazy<NoticeManager> _instance = new(() => new());
public static NoticeHandler Instance => _instance.Value; public static NoticeManager Instance => _instance.Value;
public void Enqueue(string? content) public void Enqueue(string? content)
{ {
@@ -13,7 +11,7 @@ public class NoticeHandler
{ {
return; return;
} }
MessageBus.Current.SendMessage(content, EMsgCommand.SendSnackMsg.ToString()); AppEvents.SendSnackMsgRequested.OnNext(content);
} }
public void SendMessage(string? content) public void SendMessage(string? content)
@@ -22,7 +20,7 @@ public class NoticeHandler
{ {
return; return;
} }
MessageBus.Current.SendMessage(content, EMsgCommand.SendMsgView.ToString()); AppEvents.SendMsgViewRequested.OnNext(content);
} }
public void SendMessageEx(string? content) public void SendMessageEx(string? content)

View File

@@ -1,12 +1,12 @@
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
public class PacHandler public class PacManager
{ {
private static readonly Lazy<PacHandler> _instance = new(() => new PacHandler()); private static readonly Lazy<PacManager> _instance = new(() => new PacManager());
public static PacHandler Instance => _instance.Value; public static PacManager Instance => _instance.Value;
private string _configPath; private string _configPath;
private int _httpPort; private int _httpPort;

View File

@@ -2,17 +2,17 @@ using System.Collections.Concurrent;
//using System.Reactive.Linq; //using System.Reactive.Linq;
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
public class ProfileExHandler public class ProfileExManager
{ {
private static readonly Lazy<ProfileExHandler> _instance = new(() => new()); private static readonly Lazy<ProfileExManager> _instance = new(() => new());
private ConcurrentBag<ProfileExItem> _lstProfileEx = []; private ConcurrentBag<ProfileExItem> _lstProfileEx = [];
private readonly Queue<string> _queIndexIds = new(); private readonly Queue<string> _queIndexIds = new();
public static ProfileExHandler Instance => _instance.Value; public static ProfileExManager Instance => _instance.Value;
private static readonly string _tag = "ProfileExHandler"; private static readonly string _tag = "ProfileExHandler";
public ProfileExHandler() public ProfileExManager()
{ {
//Init(); //Init();
} }

View File

@@ -1,21 +1,21 @@
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
public class StatisticsHandler public class StatisticsManager
{ {
private static readonly Lazy<StatisticsHandler> instance = new(() => new()); private static readonly Lazy<StatisticsManager> instance = new(() => new());
public static StatisticsHandler Instance => instance.Value; public static StatisticsManager Instance => instance.Value;
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;
@@ -91,15 +91,15 @@ public class StatisticsHandler
{ {
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem where indexId not in ( select indexId from ProfileItem )"); await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem where indexId not in ( select indexId from ProfileItem )");
long ticks = DateTime.Now.Date.Ticks; var ticks = DateTime.Now.Date.Ticks;
await SQLiteHelper.Instance.ExecuteAsync($"update ServerStatItem set todayUp = 0,todayDown=0,dateNow={ticks} where dateNow<>{ticks}"); await SQLiteHelper.Instance.ExecuteAsync($"update ServerStatItem set todayUp = 0,todayDown=0,dateNow={ticks} where dateNow<>{ticks}");
_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,12 +123,12 @@ public class StatisticsHandler
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)
{ {
long ticks = DateTime.Now.Date.Ticks; var ticks = DateTime.Now.Date.Ticks;
if (_serverStatItem != null && _serverStatItem.IndexId != indexId) if (_serverStatItem != null && _serverStatItem.IndexId != indexId)
{ {
_serverStatItem = null; _serverStatItem = null;

View File

@@ -1,16 +1,21 @@
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
public class TaskHandler public class TaskManager
{ {
private static readonly Lazy<TaskHandler> _instance = new(() => new()); private static readonly Lazy<TaskManager> _instance = new(() => new());
public static TaskHandler 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,15 +26,15 @@ public class TaskHandler
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 ProfileExHandler.Instance.SaveTo(); await ProfileExManager.Instance.SaveTo();
} }
//Execute once 1 hour //Execute once 1 hour
@@ -42,17 +47,17 @@ public class TaskHandler
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 AppHandler.Instance.SubItems())? var lstSubs = (await AppManager.Instance.SubItems())?
.Where(t => t.AutoUpdateInterval > 0) .Where(t => t.AutoUpdateInterval > 0)
.Where(t => updateTime - t.UpdateTime >= t.AutoUpdateInterval * 60) .Where(t => updateTime - t.UpdateTime >= t.AutoUpdateInterval * 60)
.ToList(); .ToList();
@@ -66,30 +71,30 @@ public class TaskHandler
foreach (var item in lstSubs) foreach (var item in lstSubs)
{ {
await SubscriptionHandler.UpdateProcess(config, item.Id, true, (bool success, string 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, (bool success, string msg) => await updateHandle.UpdateGeoFileAll(_config, async (success, msg) =>
{ {
updateFunc?.Invoke(false, msg); await _updateFunc?.Invoke(false, msg);
}); });
} }
} }

View File

@@ -1,12 +1,12 @@
using System.Net; using System.Net;
using WebDav; using WebDav;
namespace ServiceLib.Handler; namespace ServiceLib.Manager;
public sealed class WebDavHandler public sealed class WebDavManager
{ {
private static readonly Lazy<WebDavHandler> _instance = new(() => new()); private static readonly Lazy<WebDavManager> _instance = new(() => new());
public static WebDavHandler Instance => _instance.Value; public static WebDavManager Instance => _instance.Value;
private readonly Config? _config; private readonly Config? _config;
private WebDavClient? _client; private WebDavClient? _client;
@@ -15,9 +15,9 @@ public sealed class WebDavHandler
private readonly string _webFileName = "backup.zip"; private readonly string _webFileName = "backup.zip";
private readonly string _tag = "WebDav--"; private readonly string _tag = "WebDav--";
public WebDavHandler() public WebDavManager()
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
} }
private async Task<bool> GetClient() private async Task<bool> GetClient()

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>
@@ -3732,6 +3741,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Auto Route 的本地化字符串。
/// </summary>
public static string TbSettingsTunAutoRoute {
get {
return ResourceManager.GetString("TbSettingsTunAutoRoute", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Tun Mode settings 的本地化字符串。 /// 查找类似 Tun Mode settings 的本地化字符串。
/// </summary> /// </summary>
@@ -3741,6 +3759,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

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@@ -1059,6 +1059,18 @@
<data name="LvPrevProfileTip" xml:space="preserve"> <data name="LvPrevProfileTip" xml:space="preserve">
<value>لطفاً مطمئن شوید که ملاحظات وجود دارند و منحصر به فرد هستند</value> <value>لطفاً مطمئن شوید که ملاحظات وجود دارند و منحصر به فرد هستند</value>
</data> </data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>مسیریابی خودکار</value>
</data>
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
<value>مسیریابی سخت‌گیرانه</value>
</data>
<data name="TbSettingsTunStack" xml:space="preserve">
<value>پشته شبکه</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableExInbound" xml:space="preserve"> <data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>فعال سازی additional Inbound</value> <value>فعال سازی additional Inbound</value>
</data> </data>
@@ -1497,4 +1509,7 @@
<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>
</root> </root>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@@ -1059,6 +1059,18 @@
<data name="LvPrevProfileTip" xml:space="preserve"> <data name="LvPrevProfileTip" xml:space="preserve">
<value>Kérjük, győződjön meg arról, hogy a konfigurációs megjegyzések léteznek és egyediek</value> <value>Kérjük, győződjön meg arról, hogy a konfigurációs megjegyzések léteznek és egyediek</value>
</data> </data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>Automatikus útválasztás</value>
</data>
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
<value>Szigorú útválasztás</value>
</data>
<data name="TbSettingsTunStack" xml:space="preserve">
<value>Hálózati verem</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableExInbound" xml:space="preserve"> <data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>További bejövő engedélyezése</value> <value>További bejövő engedélyezése</value>
</data> </data>
@@ -1497,4 +1509,7 @@
<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>
</root> </root>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@@ -1059,6 +1059,18 @@
<data name="LvPrevProfileTip" xml:space="preserve"> <data name="LvPrevProfileTip" xml:space="preserve">
<value>Please make sure the Configuration remarks exist and are unique</value> <value>Please make sure the Configuration remarks exist and are unique</value>
</data> </data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>Auto Route</value>
</data>
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
<value>Strict Route</value>
</data>
<data name="TbSettingsTunStack" xml:space="preserve">
<value>Stack</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableExInbound" xml:space="preserve"> <data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>Enable additional Inbound</value> <value>Enable additional Inbound</value>
</data> </data>
@@ -1497,4 +1509,7 @@
<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>
</root> </root>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@@ -1059,6 +1059,18 @@
<data name="LvPrevProfileTip" xml:space="preserve"> <data name="LvPrevProfileTip" xml:space="preserve">
<value>Убедитесь, что примечание существует и является уникальным</value> <value>Убедитесь, что примечание существует и является уникальным</value>
</data> </data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>Автоматическая маршрутизация</value>
</data>
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
<value>Строгая маршрутизация</value>
</data>
<data name="TbSettingsTunStack" xml:space="preserve">
<value>Сетевой стек</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableExInbound" xml:space="preserve"> <data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>Включить дополнительный входящий канал</value> <value>Включить дополнительный входящий канал</value>
</data> </data>
@@ -1497,4 +1509,7 @@
<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>
</root> </root>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@@ -1056,6 +1056,18 @@
<data name="LvPrevProfileTip" xml:space="preserve"> <data name="LvPrevProfileTip" xml:space="preserve">
<value>请确保配置文件别名存在并唯一</value> <value>请确保配置文件别名存在并唯一</value>
</data> </data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>自动路由</value>
</data>
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
<value>严格路由</value>
</data>
<data name="TbSettingsTunStack" xml:space="preserve">
<value>协议栈</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableExInbound" xml:space="preserve"> <data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>启用额外监听端口</value> <value>启用额外监听端口</value>
</data> </data>
@@ -1494,4 +1506,7 @@
<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>
</root> </root>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@@ -1056,6 +1056,18 @@
<data name="LvPrevProfileTip" xml:space="preserve"> <data name="LvPrevProfileTip" xml:space="preserve">
<value>請確保設定檔別名存在並且唯一</value> <value>請確保設定檔別名存在並且唯一</value>
</data> </data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>自動路由</value>
</data>
<data name="TbSettingsTunStrictRoute" xml:space="preserve">
<value>嚴格路由</value>
</data>
<data name="TbSettingsTunStack" xml:space="preserve">
<value>協定堆疊</value>
</data>
<data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value>
</data>
<data name="TbSettingsEnableExInbound" xml:space="preserve"> <data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>啟用額外偵聽連接埠</value> <value>啟用額外偵聽連接埠</value>
</data> </data>
@@ -1494,4 +1506,7 @@
<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>
</root> </root>

View File

@@ -73,12 +73,12 @@ public class CoreConfigClashService
} }
//mixed-port //mixed-port
fileContent["mixed-port"] = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks); fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
//log-level //log-level
fileContent["log-level"] = GetLogLevel(_config.CoreBasicItem.Loglevel); fileContent["log-level"] = GetLogLevel(_config.CoreBasicItem.Loglevel);
//external-controller //external-controller
fileContent["external-controller"] = $"{Global.Loopback}:{AppHandler.Instance.StatePort2}"; fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
//allow-lan //allow-lan
if (_config.Inbound.First().AllowLANConn) if (_config.Inbound.First().AllowLANConn)
{ {
@@ -139,7 +139,7 @@ public class CoreConfigClashService
return ret; return ret;
} }
ClashApiHandler.Instance.ProfileContent = fileContent; ClashApiManager.Instance.ProfileContent = fileContent;
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}"); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}");
ret.Success = true; ret.Success = true;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,522 @@
using System.Net;
using System.Net.NetworkInformation;
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService(Config config)
{
private readonly Config _config = config;
private static readonly string _tag = "CoreConfigSingboxService";
#region public gen function
public async Task<RetResult> GenerateClientConfigContent(ProfileItem node)
{
var ret = new RetResult();
try
{
if (node == null
|| node.Port <= 0)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
if (node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
{
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}";
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
if (result.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
if (node.ConfigType == EConfigType.WireGuard)
{
singboxConfig.outbounds.RemoveAt(0);
var endpoints = new Endpoints4Sbox();
await GenEndpoint(node, endpoints);
endpoints.tag = Global.ProxyTag;
singboxConfig.endpoints = new() { endpoints };
}
else
{
await GenOutbound(node, singboxConfig.outbounds.First());
}
await GenMoreOutbounds(node, singboxConfig);
await GenRouting(singboxConfig);
await GenDns(node, singboxConfig);
await GenExperimental(singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(singboxConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientSpeedtestConfig(List<ServerTestItem> selecteds)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
List<IPEndPoint> lstIpEndPoints = new();
List<TcpConnectionInformation> lstTcpConns = new();
try
{
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners());
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners());
lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
await GenLog(singboxConfig);
//GenDns(new(), singboxConfig);
singboxConfig.inbounds.Clear();
singboxConfig.outbounds.RemoveAt(0);
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
foreach (var it in selecteds)
{
if (!Global.SingboxSupportConfigType.Contains(it.ConfigType))
{
continue;
}
if (it.Port <= 0)
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS)
{
if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
continue;
}
}
//find unused port
var port = initPort;
for (var k = initPort; k < Global.MaxPort; k++)
{
if (lstIpEndPoints?.FindIndex(_it => _it.Port == k) >= 0)
{
continue;
}
if (lstTcpConns?.FindIndex(_it => _it.LocalEndPoint.Port == k) >= 0)
{
continue;
}
//found
port = k;
initPort = port + 1;
break;
}
//Port In Used
if (lstIpEndPoints?.FindIndex(_it => _it.Port == port) >= 0)
{
continue;
}
it.Port = port;
it.AllowTest = true;
//inbound
Inbound4Sbox inbound = new()
{
listen = Global.Loopback,
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
};
inbound.tag = inbound.type + inbound.listen_port.ToString();
singboxConfig.inbounds.Add(inbound);
//outbound
if (item is null)
{
continue;
}
if (item.ConfigType == EConfigType.Shadowsocks
&& !Global.SsSecuritiesInSingbox.Contains(item.Security))
{
continue;
}
if (item.ConfigType == EConfigType.VLESS
&& !Global.Flows.Contains(item.Flow))
{
continue;
}
if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan
&& item.StreamSecurity == Global.StreamSecurityReality
&& item.PublicKey.IsNullOrEmpty())
{
continue;
}
var server = await GenServer(item);
if (server is null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
var tag = Global.ProxyTag + inbound.listen_port.ToString();
server.tag = tag;
if (server is Endpoints4Sbox endpoint)
{
singboxConfig.endpoints ??= new();
singboxConfig.endpoints.Add(endpoint);
}
else if (server is Outbound4Sbox outbound)
{
singboxConfig.outbounds.Add(outbound);
}
//rule
Rule4Sbox rule = new()
{
inbound = new List<string> { inbound.tag },
outbound = tag
};
singboxConfig.route.rules.Add(rule);
}
var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (rawDNSItem != null && rawDNSItem.Enabled == true)
{
await GenDnsDomainsCompatible(singboxConfig, rawDNSItem);
}
else
{
await GenDnsDomains(singboxConfig, _config.SimpleDNSItem);
}
singboxConfig.route.default_domain_resolver = new()
{
server = Global.SingboxFinalResolverTag
};
ret.Success = true;
ret.Data = JsonUtils.Serialize(singboxConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientSpeedtestConfig(ProfileItem node, int port)
{
var ret = new RetResult();
try
{
if (node is not { Port: > 0 })
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
if (node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
{
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}";
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
if (result.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenLog(singboxConfig);
if (node.ConfigType == EConfigType.WireGuard)
{
singboxConfig.outbounds.RemoveAt(0);
var endpoints = new Endpoints4Sbox();
await GenEndpoint(node, endpoints);
endpoints.tag = Global.ProxyTag;
singboxConfig.endpoints = new() { endpoints };
}
else
{
await GenOutbound(node, singboxConfig.outbounds.First());
}
await GenMoreOutbounds(node, singboxConfig);
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (item != null && item.Enabled == true)
{
await GenDnsDomainsCompatible(singboxConfig, item);
}
else
{
await GenDnsDomains(singboxConfig, _config.SimpleDNSItem);
}
singboxConfig.route.default_domain_resolver = new()
{
server = Global.SingboxFinalResolverTag
};
singboxConfig.route.rules.Clear();
singboxConfig.inbounds.Clear();
singboxConfig.inbounds.Add(new()
{
tag = $"{EInboundProtocol.mixed}{port}",
listen = Global.Loopback,
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
});
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = JsonUtils.Serialize(singboxConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientMultipleLoadConfig(List<ProfileItem> selecteds)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
singboxConfig.outbounds.RemoveAt(0);
var proxyProfiles = new List<ProfileItem>();
foreach (var it in selecteds)
{
if (!Global.SingboxSupportConfigType.Contains(it.ConfigType))
{
continue;
}
if (it.Port <= 0)
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (item is null)
{
continue;
}
if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS)
{
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
continue;
}
}
if (item.ConfigType == EConfigType.Shadowsocks
&& !Global.SsSecuritiesInSingbox.Contains(item.Security))
{
continue;
}
if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow))
{
continue;
}
//outbound
proxyProfiles.Add(item);
}
if (proxyProfiles.Count <= 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenOutboundsList(proxyProfiles, singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(singboxConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName)
{
var ret = new RetResult();
if (node == null || fileName is null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
try
{
if (node == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
if (File.Exists(fileName))
{
File.Delete(fileName);
}
var addressFileName = node.Address;
if (addressFileName.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
if (!File.Exists(addressFileName))
{
addressFileName = Path.Combine(Utils.GetConfigPath(), addressFileName);
}
if (!File.Exists(addressFileName))
{
ret.Msg = ResUI.FailedReadConfiguration + "1";
return ret;
}
if (node.Address == Global.CoreMultipleLoadConfigFileName)
{
var txtFile = File.ReadAllText(addressFileName);
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(txtFile);
if (singboxConfig == null)
{
File.Copy(addressFileName, fileName);
}
else
{
await GenInbounds(singboxConfig);
await GenExperimental(singboxConfig);
var content = JsonUtils.Serialize(singboxConfig, true);
await File.WriteAllTextAsync(fileName, content);
}
}
else
{
File.Copy(addressFileName, fileName);
}
//check again
if (!File.Exists(fileName))
{
ret.Msg = ResUI.FailedReadConfiguration + "2";
return ret;
}
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
#endregion public gen function
}

View File

@@ -0,0 +1,63 @@
using System.Text.Json.Nodes;
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService
{
private async Task<string> ApplyFullConfigTemplate(SingboxConfig singboxConfig)
{
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box);
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled)
{
return JsonUtils.Serialize(singboxConfig);
}
var fullConfigTemplateItem = _config.TunModeItem.EnableTun ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
if (fullConfigTemplateItem.IsNullOrEmpty())
{
return JsonUtils.Serialize(singboxConfig);
}
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
if (fullConfigTemplateNode == null)
{
return JsonUtils.Serialize(singboxConfig);
}
// Process outbounds
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
foreach (var outbound in singboxConfig.outbounds)
{
if (outbound.type.ToLower() is "direct" or "block")
{
if (fullConfigTemplate.AddProxyOnly == true)
{
continue;
}
}
else if (outbound.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound.server ?? string.Empty))
{
outbound.detour = fullConfigTemplate.ProxyDetour;
}
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
}
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
// Process endpoints
if (singboxConfig.endpoints != null && singboxConfig.endpoints.Count > 0)
{
var customEndpointsNode = fullConfigTemplateNode["endpoints"] is JsonArray endpoints ? endpoints : new JsonArray();
foreach (var endpoint in singboxConfig.endpoints)
{
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
{
endpoint.detour = fullConfigTemplate.ProxyDetour;
}
customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint));
}
fullConfigTemplateNode["endpoints"] = customEndpointsNode;
}
return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode));
}
}

View File

@@ -0,0 +1,496 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService
{
private async Task<int> GenDns(ProfileItem? node, SingboxConfig singboxConfig)
{
try
{
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (item != null && item.Enabled == true)
{
return await GenDnsCompatible(node, singboxConfig);
}
var simpleDNSItem = _config.SimpleDNSItem;
await GenDnsServers(singboxConfig, simpleDNSItem);
await GenDnsRules(singboxConfig, simpleDNSItem);
singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.independent_cache = true;
// final dns
var routing = await ConfigHandler.GetDefaultRouting(_config);
var useDirectDns = false;
if (routing != null)
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
useDirectDns = rules?.LastOrDefault() is { } lastRule &&
lastRule.OutboundTag == Global.DirectTag &&
(lastRule.Port == "0-65535" ||
lastRule.Network == "tcp,udp" ||
lastRule.Ip?.Contains("0.0.0.0/0") == true);
}
singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag;
// Tun2SocksAddress
if (node != null && Utils.IsDomain(node.Address))
{
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = Global.SingboxOutboundResolverTag,
domain = [node.Address],
});
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenDnsServers(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem)
{
var finalDns = await GenDnsDomains(singboxConfig, simpleDNSItem);
var directDns = ParseDnsAddress(simpleDNSItem.DirectDNS);
directDns.tag = Global.SingboxDirectDNSTag;
directDns.domain_resolver = Global.SingboxFinalResolverTag;
var remoteDns = ParseDnsAddress(simpleDNSItem.RemoteDNS);
remoteDns.tag = Global.SingboxRemoteDNSTag;
remoteDns.detour = Global.ProxyTag;
remoteDns.domain_resolver = Global.SingboxFinalResolverTag;
var resolverDns = ParseDnsAddress(simpleDNSItem.SingboxOutboundsResolveDNS);
resolverDns.tag = Global.SingboxOutboundResolverTag;
resolverDns.domain_resolver = Global.SingboxFinalResolverTag;
var hostsDns = new Server4Sbox
{
tag = Global.SingboxHostsDNSTag,
type = "hosts",
predefined = new(),
};
if (simpleDNSItem.AddCommonHosts == true)
{
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())
{
var userHostsMap = simpleDNSItem.Hosts
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.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()
);
foreach (var kvp in userHostsMap)
{
hostsDns.predefined[kvp.Key] = kvp.Value;
}
}
foreach (var host in hostsDns.predefined)
{
if (finalDns.server == host.Key)
{
finalDns.domain_resolver = Global.SingboxHostsDNSTag;
}
if (remoteDns.server == host.Key)
{
remoteDns.domain_resolver = Global.SingboxHostsDNSTag;
}
if (resolverDns.server == host.Key)
{
resolverDns.domain_resolver = Global.SingboxHostsDNSTag;
}
if (directDns.server == host.Key)
{
directDns.domain_resolver = Global.SingboxHostsDNSTag;
}
}
singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.servers ??= new List<Server4Sbox>();
singboxConfig.dns.servers.Add(remoteDns);
singboxConfig.dns.servers.Add(directDns);
singboxConfig.dns.servers.Add(resolverDns);
singboxConfig.dns.servers.Add(hostsDns);
// fake ip
if (simpleDNSItem.FakeIP == true)
{
var fakeip = new Server4Sbox
{
tag = Global.SingboxFakeDNSTag,
type = "fakeip",
inet4_range = "198.18.0.0/15",
inet6_range = "fc00::/18",
};
singboxConfig.dns.servers.Add(fakeip);
}
return await Task.FromResult(0);
}
private async Task<Server4Sbox> GenDnsDomains(SingboxConfig singboxConfig, SimpleDNSItem? simpleDNSItem)
{
var finalDns = ParseDnsAddress(simpleDNSItem.SingboxFinalResolveDNS);
finalDns.tag = Global.SingboxFinalResolverTag;
singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.servers ??= new List<Server4Sbox>();
singboxConfig.dns.servers.Add(finalDns);
return await Task.FromResult(finalDns);
}
private async Task<int> GenDnsRules(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem)
{
singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.AddRange(new[]
{
new Rule4Sbox { ip_accept_any = true, server = Global.SingboxHostsDNSTag },
new Rule4Sbox
{
server = Global.SingboxRemoteDNSTag,
strategy = simpleDNSItem.SingboxStrategy4Proxy.IsNullOrEmpty() ? null : simpleDNSItem.SingboxStrategy4Proxy,
clash_mode = ERuleMode.Global.ToString()
},
new Rule4Sbox
{
server = Global.SingboxDirectDNSTag,
strategy = simpleDNSItem.SingboxStrategy4Direct.IsNullOrEmpty() ? null : simpleDNSItem.SingboxStrategy4Direct,
clash_mode = ERuleMode.Direct.ToString()
}
});
if (simpleDNSItem.BlockBindingQuery == true)
{
singboxConfig.dns.rules.Add(new()
{
query_type = new List<int> { 64, 65 },
action = "predefined",
rcode = "NOTIMP"
});
}
var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing == null)
return 0;
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
var expectedIPCidr = new List<string>();
var expectedIPsRegions = new List<string>();
var regionNames = new HashSet<string>();
if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs))
{
var ipItems = simpleDNSItem.DirectExpectedIPs
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToList();
foreach (var ip in ipItems)
{
if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase))
{
var region = ip["geoip:".Length..];
if (!string.IsNullOrEmpty(region))
{
expectedIPsRegions.Add(region);
regionNames.Add(region);
regionNames.Add($"geolocation-{region}");
regionNames.Add($"tld-{region}");
}
}
else
{
expectedIPCidr.Add(ip);
}
}
}
foreach (var item in rules)
{
if (!item.Enabled || item.Domain is null || item.Domain.Count == 0)
{
continue;
}
var rule = new Rule4Sbox();
var validDomains = item.Domain.Count(it => ParseV2Domain(it, rule));
if (validDomains <= 0)
{
continue;
}
if (item.OutboundTag == Global.DirectTag)
{
rule.server = Global.SingboxDirectDNSTag;
rule.strategy = string.IsNullOrEmpty(simpleDNSItem.SingboxStrategy4Direct) ? null : simpleDNSItem.SingboxStrategy4Direct;
if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0)
{
var geositeSet = new HashSet<string>(rule.geosite);
if (regionNames.Intersect(geositeSet).Any())
{
if (expectedIPsRegions.Count > 0)
{
rule.geoip = expectedIPsRegions;
}
if (expectedIPCidr.Count > 0)
{
rule.ip_cidr = expectedIPCidr;
}
}
}
}
else if (item.OutboundTag == Global.BlockTag)
{
rule.action = "predefined";
rule.rcode = "NXDOMAIN";
}
else
{
if (simpleDNSItem.FakeIP == true)
{
var rule4Fake = JsonUtils.DeepCopy(rule);
rule4Fake.server = Global.SingboxFakeDNSTag;
singboxConfig.dns.rules.Add(rule4Fake);
}
rule.server = Global.SingboxRemoteDNSTag;
rule.strategy = string.IsNullOrEmpty(simpleDNSItem.SingboxStrategy4Proxy) ? null : simpleDNSItem.SingboxStrategy4Proxy;
}
singboxConfig.dns.rules.Add(rule);
}
return 0;
}
private async Task<int> GenDnsCompatible(ProfileItem? node, SingboxConfig singboxConfig)
{
try
{
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
var strDNS = string.Empty;
if (_config.TunModeItem.EnableTun)
{
strDNS = string.IsNullOrEmpty(item?.TunDNS) ? EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName) : item?.TunDNS;
}
else
{
strDNS = string.IsNullOrEmpty(item?.NormalDNS) ? EmbedUtils.GetEmbedText(Global.DNSSingboxNormalFileName) : item?.NormalDNS;
}
var dns4Sbox = JsonUtils.Deserialize<Dns4Sbox>(strDNS);
if (dns4Sbox is null)
{
return 0;
}
singboxConfig.dns = dns4Sbox;
if (dns4Sbox.servers != null && dns4Sbox.servers.Count > 0 && dns4Sbox.servers.First().address.IsNullOrEmpty())
{
await GenDnsDomainsCompatible(singboxConfig, item);
}
else
{
await GenDnsDomainsLegacyCompatible(singboxConfig, item);
}
// Tun2SocksAddress
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)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenDnsDomainsCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem)
{
var dns4Sbox = singboxConfig.dns ?? new();
dns4Sbox.servers ??= [];
dns4Sbox.rules ??= [];
var tag = Global.SingboxFinalResolverTag;
var localDnsAddress = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress;
var localDnsServer = ParseDnsAddress(localDnsAddress);
localDnsServer.tag = tag;
dns4Sbox.servers.Add(localDnsServer);
singboxConfig.dns = dns4Sbox;
return await Task.FromResult(0);
}
private async Task<int> GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem)
{
var dns4Sbox = singboxConfig.dns ?? new();
dns4Sbox.servers ??= [];
dns4Sbox.rules ??= [];
var tag = Global.SingboxFinalResolverTag;
dns4Sbox.servers.Add(new()
{
tag = tag,
address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress,
detour = Global.DirectTag,
strategy = string.IsNullOrEmpty(dNSItem?.DomainStrategy4Freedom) ? null : dNSItem?.DomainStrategy4Freedom,
});
dns4Sbox.rules.Insert(0, new()
{
server = tag,
clash_mode = ERuleMode.Direct.ToString()
});
dns4Sbox.rules.Insert(0, new()
{
server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote",
clash_mode = ERuleMode.Global.ToString()
});
var lstDomain = singboxConfig.outbounds
.Where(t => t.server.IsNotEmpty() && Utils.IsDomain(t.server))
.Select(t => t.server)
.Distinct()
.ToList();
if (lstDomain != null && lstDomain.Count > 0)
{
dns4Sbox.rules.Insert(0, new()
{
server = tag,
domain = lstDomain
});
}
singboxConfig.dns = dns4Sbox;
return await Task.FromResult(0);
}
private static Server4Sbox? ParseDnsAddress(string address)
{
var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim();
if (string.IsNullOrEmpty(addressFirst))
{
return null;
}
var server = new Server4Sbox();
if (addressFirst is "local" or "localhost")
{
server.type = "local";
return server;
}
if (addressFirst.StartsWith("dhcp://", StringComparison.OrdinalIgnoreCase))
{
var interface_name = addressFirst.Substring(7);
server.type = "dhcp";
server.Interface = interface_name == "auto" ? null : interface_name;
return server;
}
if (!addressFirst.Contains("://"))
{
// udp dns
server.type = "udp";
server.server = addressFirst;
return server;
}
try
{
var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal);
server.type = addressFirst.Substring(0, protocolEndIndex).ToLower();
var uri = new Uri(addressFirst);
server.server = uri.Host;
if (!uri.IsDefaultPort)
{
server.server_port = uri.Port;
}
if ((server.type == "https" || server.type == "h3") && !string.IsNullOrEmpty(uri.AbsolutePath) && uri.AbsolutePath != "/")
{
server.path = uri.AbsolutePath;
}
}
catch (UriFormatException)
{
var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal);
if (protocolEndIndex > 0)
{
server.type = addressFirst.Substring(0, protocolEndIndex).ToLower();
var remaining = addressFirst.Substring(protocolEndIndex + 3);
var portIndex = remaining.IndexOf(':');
var pathIndex = remaining.IndexOf('/');
if (portIndex > 0)
{
server.server = remaining.Substring(0, portIndex);
var portPart = pathIndex > portIndex
? remaining.Substring(portIndex + 1, pathIndex - portIndex - 1)
: remaining.Substring(portIndex + 1);
if (int.TryParse(portPart, out var parsedPort))
{
server.server_port = parsedPort;
}
}
else if (pathIndex > 0)
{
server.server = remaining.Substring(0, pathIndex);
}
else
{
server.server = remaining;
}
if (pathIndex > 0 && (server.type == "https" || server.type == "h3"))
{
server.path = remaining.Substring(pathIndex);
}
}
}
return server;
}
}

View File

@@ -0,0 +1,92 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService
{
private async Task<int> GenInbounds(SingboxConfig singboxConfig)
{
try
{
var listen = "0.0.0.0";
singboxConfig.inbounds = [];
if (!_config.TunModeItem.EnableTun
|| (_config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && _config.RunningCoreType == ECoreType.sing_box))
{
var inbound = new Inbound4Sbox()
{
type = EInboundProtocol.mixed.ToString(),
tag = EInboundProtocol.socks.ToString(),
listen = Global.Loopback,
};
singboxConfig.inbounds.Add(inbound);
inbound.listen_port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
if (_config.Inbound.First().SecondLocalPortEnabled)
{
var inbound2 = GetInbound(inbound, EInboundProtocol.socks2, true);
singboxConfig.inbounds.Add(inbound2);
}
if (_config.Inbound.First().AllowLANConn)
{
if (_config.Inbound.First().NewPort4LAN)
{
var inbound3 = GetInbound(inbound, EInboundProtocol.socks3, true);
inbound3.listen = listen;
singboxConfig.inbounds.Add(inbound3);
//auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
{
inbound3.users = new() { new() { username = _config.Inbound.First().User, password = _config.Inbound.First().Pass } };
}
}
else
{
inbound.listen = listen;
}
}
}
if (_config.TunModeItem.EnableTun)
{
if (_config.TunModeItem.Mtu <= 0)
{
_config.TunModeItem.Mtu = Global.TunMtus.First();
}
if (_config.TunModeItem.Stack.IsNullOrEmpty())
{
_config.TunModeItem.Stack = Global.TunStacks.First();
}
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
tunInbound.interface_name = Utils.IsOSX() ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.mtu = _config.TunModeItem.Mtu;
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute;
tunInbound.stack = _config.TunModeItem.Stack;
if (_config.TunModeItem.EnableIPv6Address == false)
{
tunInbound.address = ["172.18.0.1/30"];
}
singboxConfig.inbounds.Add(tunInbound);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private Inbound4Sbox GetInbound(Inbound4Sbox inItem, EInboundProtocol protocol, bool bSocks)
{
var inbound = JsonUtils.DeepCopy(inItem);
inbound.tag = protocol.ToString();
inbound.listen_port = inItem.listen_port + (int)protocol;
inbound.type = EInboundProtocol.mixed.ToString();
return inbound;
}
}

View File

@@ -0,0 +1,40 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService
{
private async Task<int> GenLog(SingboxConfig singboxConfig)
{
try
{
switch (_config.CoreBasicItem.Loglevel)
{
case "debug":
case "info":
case "error":
singboxConfig.log.level = _config.CoreBasicItem.Loglevel;
break;
case "warning":
singboxConfig.log.level = "warn";
break;
default:
break;
}
if (_config.CoreBasicItem.Loglevel == Global.None)
{
singboxConfig.log.disabled = true;
}
if (_config.CoreBasicItem.LogEnabled)
{
var dtNow = DateTime.Now;
singboxConfig.log.output = Utils.GetLogPath($"sbox_{dtNow:yyyy-MM-dd}.txt");
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
}

View File

@@ -0,0 +1,577 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService
{
private async Task<int> GenOutbound(ProfileItem node, Outbound4Sbox outbound)
{
try
{
outbound.server = node.Address;
outbound.server_port = node.Port;
outbound.type = Global.ProtocolTypes[node.ConfigType];
switch (node.ConfigType)
{
case EConfigType.VMess:
{
outbound.uuid = node.Id;
outbound.alter_id = node.AlterId;
if (Global.VmessSecurities.Contains(node.Security))
{
outbound.security = node.Security;
}
else
{
outbound.security = Global.DefaultSecurity;
}
await GenOutboundMux(node, outbound);
break;
}
case EConfigType.Shadowsocks:
{
outbound.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : Global.None;
outbound.password = node.Id;
await GenOutboundMux(node, outbound);
break;
}
case EConfigType.SOCKS:
{
outbound.version = "5";
if (node.Security.IsNotEmpty()
&& node.Id.IsNotEmpty())
{
outbound.username = node.Security;
outbound.password = node.Id;
}
break;
}
case EConfigType.HTTP:
{
if (node.Security.IsNotEmpty()
&& node.Id.IsNotEmpty())
{
outbound.username = node.Security;
outbound.password = node.Id;
}
break;
}
case EConfigType.VLESS:
{
outbound.uuid = node.Id;
outbound.packet_encoding = "xudp";
if (node.Flow.IsNullOrEmpty())
{
await GenOutboundMux(node, outbound);
}
else
{
outbound.flow = node.Flow;
}
break;
}
case EConfigType.Trojan:
{
outbound.password = node.Id;
await GenOutboundMux(node, outbound);
break;
}
case EConfigType.Hysteria2:
{
outbound.password = node.Id;
if (node.Path.IsNotEmpty())
{
outbound.obfs = new()
{
type = "salamander",
password = node.Path.TrimEx(),
};
}
outbound.up_mbps = _config.HysteriaItem.UpMbps > 0 ? _config.HysteriaItem.UpMbps : null;
outbound.down_mbps = _config.HysteriaItem.DownMbps > 0 ? _config.HysteriaItem.DownMbps : null;
if (node.Ports.IsNotEmpty() && (node.Ports.Contains(':') || node.Ports.Contains('-') || node.Ports.Contains(',')))
{
outbound.server_port = null;
outbound.server_ports = node.Ports.Split(',')
.Select(p => p.Trim())
.Where(p => p.IsNotEmpty())
.Select(p =>
{
var port = p.Replace('-', ':');
return port.Contains(':') ? port : $"{port}:{port}";
})
.ToList();
outbound.hop_interval = _config.HysteriaItem.HopInterval > 0 ? $"{_config.HysteriaItem.HopInterval}s" : null;
}
break;
}
case EConfigType.TUIC:
{
outbound.uuid = node.Id;
outbound.password = node.Security;
outbound.congestion_control = node.HeaderType;
break;
}
case EConfigType.Anytls:
{
outbound.password = node.Id;
break;
}
}
await GenOutboundTls(node, outbound);
await GenOutboundTransport(node, outbound);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenEndpoint(ProfileItem node, Endpoints4Sbox endpoint)
{
try
{
endpoint.address = Utils.String2List(node.RequestHost);
endpoint.type = Global.ProtocolTypes[node.ConfigType];
switch (node.ConfigType)
{
case EConfigType.WireGuard:
{
var peer = new Peer4Sbox
{
public_key = node.PublicKey,
reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(),
address = node.Address,
port = node.Port,
// TODO default ["0.0.0.0/0", "::/0"]
allowed_ips = new() { "0.0.0.0/0", "::/0" },
};
endpoint.private_key = node.Id;
endpoint.mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt();
endpoint.peers = new() { peer };
break;
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<BaseServer4Sbox?> GenServer(ProfileItem node)
{
try
{
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (node.ConfigType == EConfigType.WireGuard)
{
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
await GenEndpoint(node, endpoint);
return endpoint;
}
else
{
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(node, outbound);
return outbound;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult<BaseServer4Sbox?>(null);
}
private async Task<int> GenOutboundMux(ProfileItem node, Outbound4Sbox outbound)
{
try
{
var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
{
var mux = new Multiplex4Sbox()
{
enabled = true,
protocol = _config.Mux4SboxItem.Protocol,
max_connections = _config.Mux4SboxItem.MaxConnections,
padding = _config.Mux4SboxItem.Padding,
};
outbound.multiplex = mux;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<int> GenOutboundTls(ProfileItem node, Outbound4Sbox outbound)
{
try
{
if (node.StreamSecurity == Global.StreamSecurityReality || node.StreamSecurity == Global.StreamSecurity)
{
var server_name = string.Empty;
if (node.Sni.IsNotEmpty())
{
server_name = node.Sni;
}
else if (node.RequestHost.IsNotEmpty())
{
server_name = Utils.String2List(node.RequestHost)?.First();
}
var tls = new Tls4Sbox()
{
enabled = true,
record_fragment = _config.CoreBasicItem.EnableFragment,
server_name = server_name,
insecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure),
alpn = node.GetAlpn(),
};
if (node.Fingerprint.IsNotEmpty())
{
tls.utls = new Utls4Sbox()
{
enabled = true,
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint
};
}
if (node.StreamSecurity == Global.StreamSecurityReality)
{
tls.reality = new Reality4Sbox()
{
enabled = true,
public_key = node.PublicKey,
short_id = node.ShortId
};
tls.insecure = false;
}
outbound.tls = tls;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<int> GenOutboundTransport(ProfileItem node, Outbound4Sbox outbound)
{
try
{
var transport = new Transport4Sbox();
switch (node.GetNetwork())
{
case nameof(ETransport.h2):
transport.type = nameof(ETransport.http);
transport.host = node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(node.RequestHost);
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
break;
case nameof(ETransport.tcp): //http
if (node.HeaderType == Global.TcpHeaderHttp)
{
if (node.ConfigType == EConfigType.Shadowsocks)
{
outbound.plugin = "obfs-local";
outbound.plugin_opts = $"obfs=http;obfs-host={node.RequestHost};";
}
else
{
transport.type = nameof(ETransport.http);
transport.host = node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(node.RequestHost);
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
}
}
break;
case nameof(ETransport.ws):
transport.type = nameof(ETransport.ws);
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
if (node.RequestHost.IsNotEmpty())
{
transport.headers = new()
{
Host = node.RequestHost
};
}
break;
case nameof(ETransport.httpupgrade):
transport.type = nameof(ETransport.httpupgrade);
transport.path = node.Path.IsNullOrEmpty() ? null : node.Path;
transport.host = node.RequestHost.IsNullOrEmpty() ? null : node.RequestHost;
break;
case nameof(ETransport.quic):
transport.type = nameof(ETransport.quic);
break;
case nameof(ETransport.grpc):
transport.type = nameof(ETransport.grpc);
transport.service_name = node.Path;
transport.idle_timeout = _config.GrpcItem.IdleTimeout?.ToString("##s");
transport.ping_timeout = _config.GrpcItem.HealthCheckTimeout?.ToString("##s");
transport.permit_without_stream = _config.GrpcItem.PermitWithoutStream;
break;
default:
break;
}
if (transport.type != null)
{
outbound.transport = transport;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<int> GenMoreOutbounds(ProfileItem node, SingboxConfig singboxConfig)
{
if (node.Subid.IsNullOrEmpty())
{
return 0;
}
try
{
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem is null)
{
return 0;
}
//current proxy
BaseServer4Sbox? outbound = singboxConfig.endpoints?.FirstOrDefault(t => t.tag == Global.ProxyTag, null);
outbound ??= singboxConfig.outbounds.First();
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
//Previous proxy
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
string? prevOutboundTag = null;
if (prevNode is not null
&& Global.SingboxSupportConfigType.Contains(prevNode.ConfigType))
{
prevOutboundTag = $"prev-{Global.ProxyTag}";
var prevServer = await GenServer(prevNode);
prevServer.tag = prevOutboundTag;
if (prevServer is Endpoints4Sbox endpoint)
{
singboxConfig.endpoints ??= new();
singboxConfig.endpoints.Add(endpoint);
}
else if (prevServer is Outbound4Sbox outboundPrev)
{
singboxConfig.outbounds.Add(outboundPrev);
}
}
var nextServer = await GenChainOutbounds(subItem, outbound, prevOutboundTag);
if (nextServer is not null)
{
if (nextServer is Endpoints4Sbox endpoint)
{
singboxConfig.endpoints ??= new();
singboxConfig.endpoints.Insert(0, endpoint);
}
else if (nextServer is Outbound4Sbox outboundNext)
{
singboxConfig.outbounds.Insert(0, outboundNext);
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig)
{
try
{
// Get outbound template and initialize lists
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
return 0;
}
var resultOutbounds = new List<Outbound4Sbox>();
var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints
var prevOutbounds = new List<Outbound4Sbox>(); // Separate list for prev outbounds
var prevEndpoints = new List<Endpoints4Sbox>(); // Separate list for prev endpoints
var proxyTags = new List<string>(); // For selector and urltest outbounds
// Cache for chain proxies to avoid duplicate generation
var nextProxyCache = new Dictionary<string, BaseServer4Sbox?>();
var prevProxyTags = new Dictionary<string, string?>(); // Map from profile name to tag
var prevIndex = 0; // Index for prev outbounds
// Process each node
var index = 0;
foreach (var node in nodes)
{
index++;
// Handle proxy chain
string? prevTag = null;
var currentServer = await GenServer(node);
var nextServer = nextProxyCache.GetValueOrDefault(node.Subid, null);
if (nextServer != null)
{
nextServer = JsonUtils.DeepCopy(nextServer);
}
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
// current proxy
currentServer.tag = $"{Global.ProxyTag}-{index}";
proxyTags.Add(currentServer.tag);
if (!node.Subid.IsNullOrEmpty())
{
if (prevProxyTags.TryGetValue(node.Subid, out var value))
{
prevTag = value; // maybe null
}
else
{
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode is not null
&& Global.SingboxSupportConfigType.Contains(prevNode.ConfigType))
{
var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}";
prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound);
}
prevProxyTags[node.Subid] = prevTag;
}
nextServer = await GenChainOutbounds(subItem, currentServer, prevTag, nextServer);
if (!nextProxyCache.ContainsKey(node.Subid))
{
nextProxyCache[node.Subid] = nextServer;
}
}
if (nextServer is not null)
{
if (nextServer is Endpoints4Sbox nextEndpoint)
{
resultEndpoints.Add(nextEndpoint);
}
else if (nextServer is Outbound4Sbox nextOutbound)
{
resultOutbounds.Add(nextOutbound);
}
}
if (currentServer is Endpoints4Sbox currentEndpoint)
{
resultEndpoints.Add(currentEndpoint);
}
else if (currentServer is Outbound4Sbox currentOutbound)
{
resultOutbounds.Add(currentOutbound);
}
}
// Add urltest outbound (auto selection based on latency)
if (proxyTags.Count > 0)
{
var outUrltest = new Outbound4Sbox
{
type = "urltest",
tag = $"{Global.ProxyTag}-auto",
outbounds = proxyTags,
interrupt_exist_connections = false,
};
// Add selector outbound (manual selection)
var outSelector = new Outbound4Sbox
{
type = "selector",
tag = Global.ProxyTag,
outbounds = JsonUtils.DeepCopy(proxyTags),
interrupt_exist_connections = false,
};
outSelector.outbounds.Insert(0, outUrltest.tag);
// Insert these at the beginning
resultOutbounds.Insert(0, outUrltest);
resultOutbounds.Insert(0, outSelector);
}
// Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
singboxConfig.endpoints ??= new List<Endpoints4Sbox>();
resultEndpoints.AddRange(singboxConfig.endpoints);
singboxConfig.endpoints = resultEndpoints;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<BaseServer4Sbox?> GenChainOutbounds(SubItem subItem, BaseServer4Sbox outbound, string? prevOutboundTag, BaseServer4Sbox? nextOutbound = null)
{
try
{
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (!prevOutboundTag.IsNullOrEmpty())
{
outbound.detour = prevOutboundTag;
}
// Next proxy
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& Global.SingboxSupportConfigType.Contains(nextNode.ConfigType))
{
nextOutbound ??= await GenServer(nextNode);
nextOutbound.tag = outbound.tag;
outbound.tag = $"mid-{outbound.tag}";
nextOutbound.detour = outbound.tag;
}
return nextOutbound;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return null;
}
}

View File

@@ -0,0 +1,365 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService
{
private async Task<int> GenRouting(SingboxConfig singboxConfig)
{
try
{
singboxConfig.route.final = Global.ProxyTag;
var item = _config.SimpleDNSItem;
var defaultDomainResolverTag = Global.SingboxOutboundResolverTag;
var directDNSStrategy = item.SingboxStrategy4Direct.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : item.SingboxStrategy4Direct;
var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (rawDNSItem != null && rawDNSItem.Enabled == true)
{
defaultDomainResolverTag = Global.SingboxFinalResolverTag;
directDNSStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : rawDNSItem.DomainStrategy4Freedom;
}
singboxConfig.route.default_domain_resolver = new()
{
server = defaultDomainResolverTag,
strategy = directDNSStrategy
};
if (_config.TunModeItem.EnableTun)
{
singboxConfig.route.auto_detect_interface = true;
var tunRules = JsonUtils.Deserialize<List<Rule4Sbox>>(EmbedUtils.GetEmbedText(Global.TunSingboxRulesFileName));
if (tunRules != null)
{
singboxConfig.route.rules.AddRange(tunRules);
}
GenRoutingDirectExe(out var lstDnsExe, out var lstDirectExe);
singboxConfig.route.rules.Add(new()
{
port = new() { 53 },
action = "hijack-dns",
process_name = lstDnsExe
});
singboxConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
process_name = lstDirectExe
});
}
if (_config.Inbound.First().SniffingEnabled)
{
singboxConfig.route.rules.Add(new()
{
action = "sniff"
});
singboxConfig.route.rules.Add(new()
{
protocol = new() { "dns" },
action = "hijack-dns"
});
}
else
{
singboxConfig.route.rules.Add(new()
{
port = new() { 53 },
network = new() { "udp" },
action = "hijack-dns"
});
}
singboxConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
clash_mode = ERuleMode.Direct.ToString()
});
singboxConfig.route.rules.Add(new()
{
outbound = Global.ProxyTag,
clash_mode = ERuleMode.Global.ToString()
});
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.IsNullOrEmpty() ? null : _config.RoutingBasicItem.DomainStrategy4Singbox;
var defaultRouting = await ConfigHandler.GetDefaultRouting(_config);
if (defaultRouting.DomainStrategy4Singbox.IsNotEmpty())
{
domainStrategy = defaultRouting.DomainStrategy4Singbox;
}
var resolveRule = new Rule4Sbox
{
action = "resolve",
strategy = domainStrategy
};
if (_config.RoutingBasicItem.DomainStrategy == Global.IPOnDemand)
{
singboxConfig.route.rules.Add(resolveRule);
}
var routing = await ConfigHandler.GetDefaultRouting(_config);
var ipRules = new List<RulesItem>();
if (routing != null)
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var item1 in rules ?? [])
{
if (item1.Enabled)
{
await GenRoutingUserRule(item1, singboxConfig);
if (item1.Ip != null && item1.Ip.Count > 0)
{
ipRules.Add(item1);
}
}
}
}
if (_config.RoutingBasicItem.DomainStrategy == Global.IPIfNonMatch)
{
singboxConfig.route.rules.Add(resolveRule);
foreach (var item2 in ipRules)
{
await GenRoutingUserRule(item2, singboxConfig);
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private void GenRoutingDirectExe(out List<string> lstDnsExe, out List<string> lstDirectExe)
{
var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo();
foreach (var coreConfig in coreInfoResult)
{
if (coreConfig.CoreType == ECoreType.v2rayN)
{
continue;
}
foreach (var baseExeName in coreConfig.CoreExes)
{
if (coreConfig.CoreType != ECoreType.sing_box)
{
dnsExeSet.Add(Utils.GetExeName(baseExeName));
}
directExeSet.Add(Utils.GetExeName(baseExeName));
}
}
lstDnsExe = new List<string>(dnsExeSet);
lstDirectExe = new List<string>(directExeSet);
}
private async Task<int> GenRoutingUserRule(RulesItem item, SingboxConfig singboxConfig)
{
try
{
if (item == null)
{
return 0;
}
item.OutboundTag = await GenRoutingUserRuleOutbound(item.OutboundTag, singboxConfig);
var rules = singboxConfig.route.rules;
var rule = new Rule4Sbox();
if (item.OutboundTag == "block")
{
rule.action = "reject";
}
else
{
rule.outbound = item.OutboundTag;
}
if (item.Port.IsNotEmpty())
{
var portRanges = item.Port.Split(',').Where(it => it.Contains('-')).Select(it => it.Replace("-", ":")).ToList();
var ports = item.Port.Split(',').Where(it => !it.Contains('-')).Select(it => it.ToInt()).ToList();
rule.port_range = portRanges.Count > 0 ? portRanges : null;
rule.port = ports.Count > 0 ? ports : null;
}
if (item.Network.IsNotEmpty())
{
rule.network = Utils.String2List(item.Network);
}
if (item.Protocol?.Count > 0)
{
rule.protocol = item.Protocol;
}
if (item.InboundTag?.Count >= 0)
{
rule.inbound = item.InboundTag;
}
var rule1 = JsonUtils.DeepCopy(rule);
var rule2 = JsonUtils.DeepCopy(rule);
var rule3 = JsonUtils.DeepCopy(rule);
var hasDomainIp = false;
if (item.Domain?.Count > 0)
{
var countDomain = 0;
foreach (var it in item.Domain)
{
if (ParseV2Domain(it, rule1))
countDomain++;
}
if (countDomain > 0)
{
rules.Add(rule1);
hasDomainIp = true;
}
}
if (item.Ip?.Count > 0)
{
var countIp = 0;
foreach (var it in item.Ip)
{
if (ParseV2Address(it, rule2))
countIp++;
}
if (countIp > 0)
{
rules.Add(rule2);
hasDomainIp = true;
}
}
if (_config.TunModeItem.EnableTun && item.Process?.Count > 0)
{
rule3.process_name = item.Process;
rules.Add(rule3);
hasDomainIp = true;
}
if (!hasDomainIp
&& (rule.port != null || rule.port_range != null || rule.protocol != null || rule.inbound != null || rule.network != null))
{
rules.Add(rule);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private bool ParseV2Domain(string domain, Rule4Sbox rule)
{
if (domain.StartsWith("#") || domain.StartsWith("ext:") || domain.StartsWith("ext-domain:"))
{
return false;
}
else if (domain.StartsWith("geosite:"))
{
rule.geosite ??= [];
rule.geosite?.Add(domain.Substring(8));
}
else if (domain.StartsWith("regexp:"))
{
rule.domain_regex ??= [];
rule.domain_regex?.Add(domain.Replace(Global.RoutingRuleComma, ",").Substring(7));
}
else if (domain.StartsWith("domain:"))
{
rule.domain ??= [];
rule.domain_suffix ??= [];
rule.domain?.Add(domain.Substring(7));
rule.domain_suffix?.Add("." + domain.Substring(7));
}
else if (domain.StartsWith("full:"))
{
rule.domain ??= [];
rule.domain?.Add(domain.Substring(5));
}
else if (domain.StartsWith("keyword:"))
{
rule.domain_keyword ??= [];
rule.domain_keyword?.Add(domain.Substring(8));
}
else
{
rule.domain_keyword ??= [];
rule.domain_keyword?.Add(domain);
}
return true;
}
private bool ParseV2Address(string address, Rule4Sbox rule)
{
if (address.StartsWith("ext:") || address.StartsWith("ext-ip:"))
{
return false;
}
else if (address.Equals("geoip:private"))
{
rule.ip_is_private = true;
}
else if (address.StartsWith("geoip:"))
{
rule.geoip ??= new();
rule.geoip?.Add(address.Substring(6));
}
else if (address.Equals("geoip:!private"))
{
rule.ip_is_private = false;
}
else if (address.StartsWith("geoip:!"))
{
rule.geoip ??= new();
rule.geoip?.Add(address.Substring(6));
rule.invert = true;
}
else
{
rule.ip_cidr ??= new();
rule.ip_cidr?.Add(address);
}
return true;
}
private async Task<string?> GenRoutingUserRuleOutbound(string outboundTag, SingboxConfig singboxConfig)
{
if (Global.OutboundTags.Contains(outboundTag))
{
return outboundTag;
}
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (node == null
|| !Global.SingboxSupportConfigType.Contains(node.ConfigType))
{
return Global.ProxyTag;
}
var server = await GenServer(node);
if (server is null)
{
return Global.ProxyTag;
}
server.tag = Global.ProxyTag + node.IndexId.ToString();
if (server is Endpoints4Sbox endpoint)
{
singboxConfig.endpoints ??= new();
singboxConfig.endpoints.Add(endpoint);
}
else if (server is Outbound4Sbox outbound)
{
singboxConfig.outbounds.Add(outbound);
}
return server.tag;
}
}

View File

@@ -0,0 +1,119 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService
{
private async Task<int> ConvertGeo2Ruleset(SingboxConfig singboxConfig)
{
static void AddRuleSets(List<string> ruleSets, List<string>? rule_set)
{
if (rule_set != null)
ruleSets.AddRange(rule_set);
}
var geosite = "geosite";
var geoip = "geoip";
var ruleSets = new List<string>();
//convert route geosite & geoip to ruleset
foreach (var rule in singboxConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
rule.geosite = null;
AddRuleSets(ruleSets, rule.rule_set);
}
foreach (var rule in singboxConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
rule.geoip = null;
AddRuleSets(ruleSets, rule.rule_set);
}
//convert dns geosite & geoip to ruleset
foreach (var rule in singboxConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
rule.geosite = null;
}
foreach (var rule in singboxConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
{
rule.rule_set ??= new List<string>();
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
rule.geoip = null;
}
foreach (var dnsRule in singboxConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? [])
{
AddRuleSets(ruleSets, dnsRule.rule_set);
}
//rules in rules
foreach (var item in singboxConfig.dns?.rules.Where(t => t.rules?.Count > 0).Select(t => t.rules).ToList() ?? [])
{
foreach (var item2 in item ?? [])
{
AddRuleSets(ruleSets, item2.rule_set);
}
}
//load custom ruleset file
List<Ruleset4Sbox> customRulesets = [];
var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing.CustomRulesetPath4Singbox.IsNotEmpty())
{
var result = EmbedUtils.LoadResource(routing.CustomRulesetPath4Singbox);
if (result.IsNotEmpty())
{
customRulesets = (JsonUtils.Deserialize<List<Ruleset4Sbox>>(result) ?? [])
.Where(t => t.tag != null)
.Where(t => t.type != null)
.Where(t => t.format != null)
.ToList();
}
}
//Local srs files address
var localSrss = Utils.GetBinPath("srss");
//Add ruleset srs
singboxConfig.route.rule_set = [];
foreach (var item in new HashSet<string>(ruleSets))
{
if (item.IsNullOrEmpty())
{ continue; }
var customRuleset = customRulesets.FirstOrDefault(t => t.tag != null && t.tag.Equals(item));
if (customRuleset is null)
{
var pathSrs = Path.Combine(localSrss, $"{item}.srs");
if (File.Exists(pathSrs))
{
customRuleset = new()
{
type = "local",
format = "binary",
tag = item,
path = pathSrs
};
}
else
{
var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl)
? Global.SingboxRulesetUrl
: _config.ConstItem.SrsSourceUrl;
customRuleset = new()
{
type = "remote",
format = "binary",
tag = item,
url = string.Format(srsUrl, item.StartsWith(geosite) ? geosite : geoip, item),
download_detour = Global.ProxyTag
};
}
}
singboxConfig.route.rule_set.Add(customRuleset);
}
return 0;
}
}

View File

@@ -0,0 +1,29 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService
{
private async Task<int> GenExperimental(SingboxConfig singboxConfig)
{
//if (_config.guiItem.enableStatistics)
{
singboxConfig.experimental ??= new Experimental4Sbox();
singboxConfig.experimental.clash_api = new Clash_Api4Sbox()
{
external_controller = $"{Global.Loopback}:{AppManager.Instance.StatePort2}",
};
}
if (_config.CoreBasicItem.EnableCacheFile4Sbox)
{
singboxConfig.experimental ??= new Experimental4Sbox();
singboxConfig.experimental.cache_file = new CacheFile4Sbox()
{
enabled = true,
path = Utils.GetBinPath("cache.db"),
store_fakeip = _config.SimpleDNSItem.FakeIP == true
};
}
return await Task.FromResult(0);
}
}

View File

@@ -0,0 +1,411 @@
using System.Net;
using System.Net.NetworkInformation;
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService(Config config)
{
private readonly Config _config = config;
private static readonly string _tag = "CoreConfigV2rayService";
#region public gen function
public async Task<RetResult> GenerateClientConfigContent(ProfileItem node)
{
var ret = new RetResult();
try
{
if (node == null
|| node.Port <= 0)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
if (node.GetNetwork() is nameof(ETransport.quic))
{
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}";
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
if (result.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
await GenOutbound(node, v2rayConfig.outbounds.First());
await GenMoreOutbounds(node, v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(node, v2rayConfig);
await GenStatistic(v2rayConfig);
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientMultipleLoadConfig(List<ProfileItem> selecteds, EMultipleLoad multipleLoad)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
v2rayConfig.outbounds.RemoveAt(0);
var proxyProfiles = new List<ProfileItem>();
foreach (var it in selecteds)
{
if (!Global.XraySupportConfigType.Contains(it.ConfigType))
{
continue;
}
if (it.Port <= 0)
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (item is null)
{
continue;
}
if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS)
{
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
continue;
}
}
if (item.ConfigType == EConfigType.Shadowsocks
&& !Global.SsSecuritiesInSingbox.Contains(item.Security))
{
continue;
}
if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow))
{
continue;
}
//outbound
proxyProfiles.Add(item);
}
if (proxyProfiles.Count <= 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenOutboundsList(proxyProfiles, v2rayConfig);
//add balancers
await GenBalancer(v2rayConfig, multipleLoad);
var balancer = v2rayConfig.routing.balancers.First();
//add rule
var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList();
if (rules?.Count > 0)
{
foreach (var rule in rules)
{
rule.outboundTag = null;
rule.balancerTag = balancer.tag;
}
}
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
{
v2rayConfig.routing.rules.Add(new()
{
ip = ["0.0.0.0/0", "::/0"],
balancerTag = balancer.tag,
type = "field"
});
}
else
{
v2rayConfig.routing.rules.Add(new()
{
network = "tcp,udp",
balancerTag = balancer.tag,
type = "field"
});
}
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientSpeedtestConfig(List<ServerTestItem> selecteds)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
List<IPEndPoint> lstIpEndPoints = new();
List<TcpConnectionInformation> lstTcpConns = new();
try
{
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners());
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners());
lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
await GenLog(v2rayConfig);
v2rayConfig.inbounds.Clear();
v2rayConfig.outbounds.Clear();
v2rayConfig.routing.rules.Clear();
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
foreach (var it in selecteds)
{
if (!Global.XraySupportConfigType.Contains(it.ConfigType))
{
continue;
}
if (it.Port <= 0)
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS)
{
if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
continue;
}
}
//find unused port
var port = initPort;
for (var k = initPort; k < Global.MaxPort; k++)
{
if (lstIpEndPoints?.FindIndex(_it => _it.Port == k) >= 0)
{
continue;
}
if (lstTcpConns?.FindIndex(_it => _it.LocalEndPoint.Port == k) >= 0)
{
continue;
}
//found
port = k;
initPort = port + 1;
break;
}
//Port In Used
if (lstIpEndPoints?.FindIndex(_it => _it.Port == port) >= 0)
{
continue;
}
it.Port = port;
it.AllowTest = true;
//outbound
if (item is null)
{
continue;
}
if (item.ConfigType == EConfigType.Shadowsocks
&& !Global.SsSecuritiesInXray.Contains(item.Security))
{
continue;
}
if (item.ConfigType == EConfigType.VLESS
&& !Global.Flows.Contains(item.Flow))
{
continue;
}
if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan
&& item.StreamSecurity == Global.StreamSecurityReality
&& item.PublicKey.IsNullOrEmpty())
{
continue;
}
//inbound
Inbounds4Ray inbound = new()
{
listen = Global.Loopback,
port = port,
protocol = EInboundProtocol.mixed.ToString(),
};
inbound.tag = inbound.protocol + inbound.port.ToString();
v2rayConfig.inbounds.Add(inbound);
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(item, outbound);
outbound.tag = Global.ProxyTag + inbound.port.ToString();
v2rayConfig.outbounds.Add(outbound);
//rule
RulesItem4Ray rule = new()
{
inboundTag = new List<string> { inbound.tag },
outboundTag = outbound.tag,
type = "field"
};
v2rayConfig.routing.rules.Add(rule);
}
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
ret.Success = true;
ret.Data = JsonUtils.Serialize(v2rayConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientSpeedtestConfig(ProfileItem node, int port)
{
var ret = new RetResult();
try
{
if (node is not { Port: > 0 })
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
if (node.GetNetwork() is nameof(ETransport.quic))
{
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}";
return ret;
}
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
if (result.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenLog(v2rayConfig);
await GenOutbound(node, v2rayConfig.outbounds.First());
await GenMoreOutbounds(node, v2rayConfig);
v2rayConfig.routing.rules.Clear();
v2rayConfig.inbounds.Clear();
v2rayConfig.inbounds.Add(new()
{
tag = $"{EInboundProtocol.socks}{port}",
listen = Global.Loopback,
port = port,
protocol = EInboundProtocol.mixed.ToString(),
});
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = JsonUtils.Serialize(v2rayConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
#endregion public gen function
}

View File

@@ -0,0 +1,50 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<int> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad)
{
if (multipleLoad == EMultipleLoad.LeastPing)
{
var observatory = new Observatory4Ray
{
subjectSelector = [Global.ProxyTag],
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
probeInterval = "3m",
enableConcurrency = true,
};
v2rayConfig.observatory = observatory;
}
else if (multipleLoad == EMultipleLoad.LeastLoad)
{
var burstObservatory = new BurstObservatory4Ray
{
subjectSelector = [Global.ProxyTag],
pingConfig = new()
{
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
interval = "5m",
timeout = "30s",
sampling = 2,
}
};
v2rayConfig.burstObservatory = burstObservatory;
}
var strategyType = multipleLoad switch
{
EMultipleLoad.Random => "random",
EMultipleLoad.RoundRobin => "roundRobin",
EMultipleLoad.LeastPing => "leastPing",
EMultipleLoad.LeastLoad => "leastLoad",
_ => "roundRobin",
};
var balancer = new BalancersItem4Ray
{
selector = [Global.ProxyTag],
strategy = new() { type = strategyType },
tag = $"{Global.ProxyTag}-round",
};
v2rayConfig.routing.balancers = [balancer];
return await Task.FromResult(0);
}
}

View File

@@ -0,0 +1,86 @@
using System.Text.Json.Nodes;
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig, bool handleBalancerAndRules = false)
{
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty())
{
return JsonUtils.Serialize(v2rayConfig);
}
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplate.Config);
if (fullConfigTemplateNode == null)
{
return JsonUtils.Serialize(v2rayConfig);
}
// Handle balancer and rules modifications (for multiple load scenarios)
if (handleBalancerAndRules && v2rayConfig.routing?.balancers?.Count > 0)
{
var balancer = v2rayConfig.routing.balancers.First();
// Modify existing rules in custom config
var rulesNode = fullConfigTemplateNode["routing"]?["rules"];
if (rulesNode != null)
{
foreach (var rule in rulesNode.AsArray())
{
if (rule["outboundTag"]?.GetValue<string>() == Global.ProxyTag)
{
rule.AsObject().Remove("outboundTag");
rule["balancerTag"] = balancer.tag;
}
}
}
// Ensure routing node exists
if (fullConfigTemplateNode["routing"] == null)
{
fullConfigTemplateNode["routing"] = new JsonObject();
}
// Handle balancers - append instead of override
if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode)
{
if (JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers)) is JsonArray newBalancers)
{
foreach (var balancerNode in newBalancers)
{
customBalancersNode.Add(balancerNode?.DeepClone());
}
}
}
else
{
fullConfigTemplateNode["routing"]["balancers"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers));
}
}
// Handle outbounds - append instead of override
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
foreach (var outbound in v2rayConfig.outbounds)
{
if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom")
{
if (fullConfigTemplate.AddProxyOnly == true)
{
continue;
}
}
else if ((outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() == true) && (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) && !(Utils.IsPrivateNetwork(outbound.settings?.servers?.FirstOrDefault()?.address ?? string.Empty) || Utils.IsPrivateNetwork(outbound.settings?.vnext?.FirstOrDefault()?.address ?? string.Empty)))
{
outbound.streamSettings ??= new StreamSettings4Ray();
outbound.streamSettings.sockopt ??= new Sockopt4Ray();
outbound.streamSettings.sockopt.dialerProxy = fullConfigTemplate.ProxyDetour;
}
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
}
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode));
}
}

View File

@@ -0,0 +1,410 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<int> GenDns(ProfileItem? node, V2rayConfig v2rayConfig)
{
try
{
var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray);
if (item != null && item.Enabled == true)
{
var result = await GenDnsCompatible(node, v2rayConfig);
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
{
// DNS routing
v2rayConfig.dns.tag = Global.DnsTag;
v2rayConfig.routing.rules.Add(new RulesItem4Ray
{
type = "field",
inboundTag = new List<string> { Global.DnsTag },
outboundTag = Global.ProxyTag,
});
}
return result;
}
var simpleDNSItem = _config.SimpleDNSItem;
var domainStrategy4Freedom = simpleDNSItem?.RayStrategy4Freedom;
//Outbound Freedom domainStrategy
if (domainStrategy4Freedom.IsNotEmpty())
{
var outbound = v2rayConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
if (outbound != null)
{
outbound.settings = new()
{
domainStrategy = domainStrategy4Freedom,
userLevel = 0
};
}
}
await GenDnsServers(node, v2rayConfig, simpleDNSItem);
await GenDnsHosts(v2rayConfig, simpleDNSItem);
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
{
// DNS routing
v2rayConfig.dns.tag = Global.DnsTag;
v2rayConfig.routing.rules.Add(new RulesItem4Ray
{
type = "field",
inboundTag = new List<string> { Global.DnsTag },
outboundTag = Global.ProxyTag,
});
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenDnsServers(ProfileItem? node, V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem)
{
static List<string> ParseDnsAddresses(string? dnsInput, string defaultAddress)
{
var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';')
.Select(addr => addr.Trim())
.Where(addr => !string.IsNullOrEmpty(addr))
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
.Distinct()
.ToList() ?? new List<string> { defaultAddress };
return addresses.Count > 0 ? addresses : new List<string> { defaultAddress };
}
static object CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
{
var dnsServer = new DnsServer4Ray
{
address = dnsAddress,
skipFallback = true,
domains = domains.Count > 0 ? domains : null,
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
};
return JsonUtils.SerializeToNode(dnsServer, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.FirstOrDefault());
var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.FirstOrDefault());
var directDomainList = new List<string>();
var directGeositeList = new List<string>();
var proxyDomainList = new List<string>();
var proxyGeositeList = new List<string>();
var expectedDomainList = new List<string>();
var expectedIPs = new List<string>();
var regionNames = new HashSet<string>();
if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs))
{
expectedIPs = simpleDNSItem.DirectExpectedIPs
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToList();
foreach (var ip in expectedIPs)
{
if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase))
{
var region = ip["geoip:".Length..];
if (!string.IsNullOrEmpty(region))
{
regionNames.Add($"geosite:{region}");
regionNames.Add($"geosite:geolocation-{region}");
regionNames.Add($"geosite:tld-{region}");
}
}
}
}
var routing = await ConfigHandler.GetDefaultRouting(_config);
List<RulesItem>? rules = null;
if (routing != null)
{
rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
foreach (var item in rules)
{
if (!item.Enabled || item.Domain is null || item.Domain.Count == 0)
{
continue;
}
foreach (var domain in item.Domain)
{
if (domain.StartsWith('#'))
continue;
var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ",");
if (item.OutboundTag == Global.DirectTag)
{
if (normalizedDomain.StartsWith("geosite:"))
{
(regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain);
}
else
{
directDomainList.Add(normalizedDomain);
}
}
else if (item.OutboundTag != Global.BlockTag)
{
if (normalizedDomain.StartsWith("geosite:"))
{
proxyGeositeList.Add(normalizedDomain);
}
else
{
proxyDomainList.Add(normalizedDomain);
}
}
}
}
}
if (Utils.IsDomain(node?.Address))
{
directDomainList.Add(node.Address);
}
if (node?.Subid is not null)
{
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem is not null)
{
foreach (var profile in new[] { subItem.PrevProfile, subItem.NextProfile })
{
var profileNode = await AppManager.Instance.GetProfileItemViaRemarks(profile);
if (profileNode is not null
&& Global.XraySupportConfigType.Contains(profileNode.ConfigType)
&& Utils.IsDomain(profileNode.Address))
{
directDomainList.Add(profileNode.Address);
}
}
}
}
v2rayConfig.dns ??= new Dns4Ray();
v2rayConfig.dns.servers ??= new List<object>();
void AddDnsServers(List<string> dnsAddresses, List<string> domains, List<string>? expectedIPs = null)
{
if (domains.Count > 0)
{
foreach (var dnsAddress in dnsAddresses)
{
v2rayConfig.dns.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs));
}
}
}
AddDnsServers(remoteDNSAddress, proxyDomainList);
AddDnsServers(directDNSAddress, directDomainList);
AddDnsServers(remoteDNSAddress, proxyGeositeList);
AddDnsServers(directDNSAddress, directGeositeList);
AddDnsServers(directDNSAddress, expectedDomainList, expectedIPs);
var useDirectDns = rules?.LastOrDefault() is { } lastRule
&& lastRule.OutboundTag == Global.DirectTag
&& (lastRule.Port == "0-65535"
|| lastRule.Network == "tcp,udp"
|| lastRule.Ip?.Contains("0.0.0.0/0") == true);
var defaultDnsServers = useDirectDns ? directDNSAddress : remoteDNSAddress;
v2rayConfig.dns.servers.AddRange(defaultDnsServers);
return 0;
}
private async Task<int> GenDnsHosts(V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem)
{
if (simpleDNSItem.AddCommonHosts == false && simpleDNSItem.UseSystemHosts == false && simpleDNSItem.Hosts.IsNullOrEmpty())
{
return await Task.FromResult(0);
}
v2rayConfig.dns ??= new Dns4Ray();
v2rayConfig.dns.hosts ??= new Dictionary<string, object>();
if (simpleDNSItem.AddCommonHosts == true)
{
v2rayConfig.dns.hosts = Global.PredefinedHosts.ToDictionary(
kvp => kvp.Key,
kvp => (object)kvp.Value
);
}
if (simpleDNSItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
var normalHost = v2rayConfig?.dns?.hosts;
if (normalHost != null && systemHosts?.Count > 0)
{
foreach (var host in systemHosts)
{
normalHost.TryAdd(host.Key, new List<string> { host.Value });
}
}
}
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{
var userHostsMap = simpleDNSItem.Hosts
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.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()
);
foreach (var kvp in userHostsMap)
{
v2rayConfig.dns.hosts[kvp.Key] = kvp.Value;
}
}
return await Task.FromResult(0);
}
private async Task<int> GenDnsCompatible(ProfileItem? node, V2rayConfig v2rayConfig)
{
try
{
var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray);
var normalDNS = item?.NormalDNS;
var domainStrategy4Freedom = item?.DomainStrategy4Freedom;
if (normalDNS.IsNullOrEmpty())
{
normalDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName);
}
//Outbound Freedom domainStrategy
if (domainStrategy4Freedom.IsNotEmpty())
{
var outbound = v2rayConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
if (outbound != null)
{
outbound.settings = new();
outbound.settings.domainStrategy = domainStrategy4Freedom;
outbound.settings.userLevel = 0;
}
}
var obj = JsonUtils.ParseJson(normalDNS);
if (obj is null)
{
List<string> servers = [];
string[] arrDNS = normalDNS.Split(',');
foreach (string str in arrDNS)
{
servers.Add(str);
}
obj = JsonUtils.ParseJson("{}");
obj["servers"] = JsonUtils.SerializeToNode(servers);
}
// Append to dns settings
if (item.UseSystemHosts)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts.Count > 0)
{
var normalHost1 = obj["hosts"];
if (normalHost1 != null)
{
foreach (var host in systemHosts)
{
if (normalHost1[host.Key] != null)
continue;
normalHost1[host.Key] = host.Value;
}
}
}
}
var normalHost = obj["hosts"];
if (normalHost != null)
{
foreach (var hostProp in normalHost.AsObject().ToList())
{
if (hostProp.Value is JsonValue value && value.TryGetValue<string>(out var ip))
{
normalHost[hostProp.Key] = new JsonArray(ip);
}
}
}
await GenDnsDomainsCompatible(node, obj, item);
v2rayConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(obj));
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dNSItem)
{
if (node == null)
{
return 0;
}
var servers = dns["servers"];
if (servers != null)
{
var domainList = new List<string>();
if (Utils.IsDomain(node.Address))
{
domainList.Add(node.Address);
}
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem is not null)
{
// Previous proxy
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode is not null
&& Global.SingboxSupportConfigType.Contains(prevNode.ConfigType)
&& Utils.IsDomain(prevNode.Address))
{
domainList.Add(prevNode.Address);
}
// Next proxy
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& Global.SingboxSupportConfigType.Contains(nextNode.ConfigType)
&& Utils.IsDomain(nextNode.Address))
{
domainList.Add(nextNode.Address);
}
}
if (domainList.Count > 0)
{
var dnsServer = new DnsServer4Ray()
{
address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress,
skipFallback = true,
domains = domainList
};
servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer));
}
}
return await Task.FromResult(0);
}
}

View File

@@ -0,0 +1,72 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<int> GenInbounds(V2rayConfig v2rayConfig)
{
try
{
var listen = "0.0.0.0";
v2rayConfig.inbounds = [];
var inbound = GetInbound(_config.Inbound.First(), EInboundProtocol.socks, true);
v2rayConfig.inbounds.Add(inbound);
if (_config.Inbound.First().SecondLocalPortEnabled)
{
var inbound2 = GetInbound(_config.Inbound.First(), EInboundProtocol.socks2, true);
v2rayConfig.inbounds.Add(inbound2);
}
if (_config.Inbound.First().AllowLANConn)
{
if (_config.Inbound.First().NewPort4LAN)
{
var inbound3 = GetInbound(_config.Inbound.First(), EInboundProtocol.socks3, true);
inbound3.listen = listen;
v2rayConfig.inbounds.Add(inbound3);
//auth
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
{
inbound3.settings.auth = "password";
inbound3.settings.accounts = new List<AccountsItem4Ray> { new AccountsItem4Ray() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } };
}
}
else
{
inbound.listen = listen;
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private Inbounds4Ray GetInbound(InItem inItem, EInboundProtocol protocol, bool bSocks)
{
string result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound);
if (result.IsNullOrEmpty())
{
return new();
}
var inbound = JsonUtils.Deserialize<Inbounds4Ray>(result);
if (inbound == null)
{
return new();
}
inbound.tag = protocol.ToString();
inbound.port = inItem.LocalPort + (int)protocol;
inbound.protocol = EInboundProtocol.mixed.ToString();
inbound.settings.udp = inItem.UdpEnabled;
inbound.sniffing.enabled = inItem.SniffingEnabled;
inbound.sniffing.destOverride = inItem.DestOverride;
inbound.sniffing.routeOnly = inItem.RouteOnly;
return inbound;
}
}

View File

@@ -0,0 +1,29 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<int> GenLog(V2rayConfig v2rayConfig)
{
try
{
if (_config.CoreBasicItem.LogEnabled)
{
var dtNow = DateTime.Now;
v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel;
v2rayConfig.log.access = Utils.GetLogPath($"Vaccess_{dtNow:yyyy-MM-dd}.txt");
v2rayConfig.log.error = Utils.GetLogPath($"Verror_{dtNow:yyyy-MM-dd}.txt");
}
else
{
v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel;
v2rayConfig.log.access = null;
v2rayConfig.log.error = null;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
}

View File

@@ -0,0 +1,695 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<int> GenOutbound(ProfileItem node, Outbounds4Ray outbound)
{
try
{
var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
switch (node.ConfigType)
{
case EConfigType.VMess:
{
VnextItem4Ray vnextItem;
if (outbound.settings.vnext.Count <= 0)
{
vnextItem = new VnextItem4Ray();
outbound.settings.vnext.Add(vnextItem);
}
else
{
vnextItem = outbound.settings.vnext.First();
}
vnextItem.address = node.Address;
vnextItem.port = node.Port;
UsersItem4Ray usersItem;
if (vnextItem.users.Count <= 0)
{
usersItem = new UsersItem4Ray();
vnextItem.users.Add(usersItem);
}
else
{
usersItem = vnextItem.users.First();
}
usersItem.id = node.Id;
usersItem.alterId = node.AlterId;
usersItem.email = Global.UserEMail;
if (Global.VmessSecurities.Contains(node.Security))
{
usersItem.security = node.Security;
}
else
{
usersItem.security = Global.DefaultSecurity;
}
await GenOutboundMux(node, outbound, muxEnabled, muxEnabled);
outbound.settings.servers = null;
break;
}
case EConfigType.Shadowsocks:
{
ServersItem4Ray serversItem;
if (outbound.settings.servers.Count <= 0)
{
serversItem = new ServersItem4Ray();
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = node.Address;
serversItem.port = node.Port;
serversItem.password = node.Id;
serversItem.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : "none";
serversItem.ota = false;
serversItem.level = 1;
await GenOutboundMux(node, outbound);
outbound.settings.vnext = null;
break;
}
case EConfigType.SOCKS:
case EConfigType.HTTP:
{
ServersItem4Ray serversItem;
if (outbound.settings.servers.Count <= 0)
{
serversItem = new ServersItem4Ray();
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = node.Address;
serversItem.port = node.Port;
serversItem.method = null;
serversItem.password = null;
if (node.Security.IsNotEmpty()
&& node.Id.IsNotEmpty())
{
SocksUsersItem4Ray socksUsersItem = new()
{
user = node.Security,
pass = node.Id,
level = 1
};
serversItem.users = new List<SocksUsersItem4Ray>() { socksUsersItem };
}
await GenOutboundMux(node, outbound);
outbound.settings.vnext = null;
break;
}
case EConfigType.VLESS:
{
VnextItem4Ray vnextItem;
if (outbound.settings.vnext?.Count <= 0)
{
vnextItem = new VnextItem4Ray();
outbound.settings.vnext.Add(vnextItem);
}
else
{
vnextItem = outbound.settings.vnext.First();
}
vnextItem.address = node.Address;
vnextItem.port = node.Port;
UsersItem4Ray usersItem;
if (vnextItem.users.Count <= 0)
{
usersItem = new UsersItem4Ray();
vnextItem.users.Add(usersItem);
}
else
{
usersItem = vnextItem.users.First();
}
usersItem.id = node.Id;
usersItem.email = Global.UserEMail;
usersItem.encryption = node.Security;
if (node.Flow.IsNullOrEmpty())
{
await GenOutboundMux(node, outbound, muxEnabled, muxEnabled);
}
else
{
usersItem.flow = node.Flow;
await GenOutboundMux(node, outbound, false, muxEnabled);
}
outbound.settings.servers = null;
break;
}
case EConfigType.Trojan:
{
ServersItem4Ray serversItem;
if (outbound.settings.servers.Count <= 0)
{
serversItem = new ServersItem4Ray();
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = node.Address;
serversItem.port = node.Port;
serversItem.password = node.Id;
serversItem.ota = false;
serversItem.level = 1;
await GenOutboundMux(node, outbound);
outbound.settings.vnext = null;
break;
}
case EConfigType.WireGuard:
{
var peer = new WireguardPeer4Ray
{
publicKey = node.PublicKey,
endpoint = node.Address + ":" + node.Port.ToString()
};
var setting = new Outboundsettings4Ray
{
address = Utils.String2List(node.RequestHost),
secretKey = node.Id,
reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(),
mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(),
peers = new List<WireguardPeer4Ray> { peer }
};
outbound.settings = setting;
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
}
outbound.protocol = Global.ProtocolTypes[node.ConfigType];
await GenBoundStreamSettings(node, outbound);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenOutboundMux(ProfileItem node, Outbounds4Ray outbound, bool enabledTCP = false, bool enabledUDP = false)
{
try
{
outbound.mux.enabled = false;
outbound.mux.concurrency = -1;
if (enabledTCP)
{
outbound.mux.enabled = true;
outbound.mux.concurrency = _config.Mux4RayItem.Concurrency;
}
else if (enabledUDP)
{
outbound.mux.enabled = true;
outbound.mux.xudpConcurrency = _config.Mux4RayItem.XudpConcurrency;
outbound.mux.xudpProxyUDP443 = _config.Mux4RayItem.XudpProxyUDP443;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<int> GenBoundStreamSettings(ProfileItem node, Outbounds4Ray outbound)
{
try
{
var streamSettings = outbound.streamSettings;
streamSettings.network = node.GetNetwork();
var host = node.RequestHost.TrimEx();
var path = node.Path.TrimEx();
var sni = node.Sni.TrimEx();
var useragent = "";
if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty())
{
try
{
useragent = Global.UserAgentTexts[_config.CoreBasicItem.DefUserAgent];
}
catch (KeyNotFoundException)
{
useragent = _config.CoreBasicItem.DefUserAgent;
}
}
//if tls
if (node.StreamSecurity == Global.StreamSecurity)
{
streamSettings.security = node.StreamSecurity;
TlsSettings4Ray tlsSettings = new()
{
allowInsecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure),
alpn = node.GetAlpn(),
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint
};
if (sni.IsNotEmpty())
{
tlsSettings.serverName = sni;
}
else if (host.IsNotEmpty())
{
tlsSettings.serverName = Utils.String2List(host)?.First();
}
streamSettings.tlsSettings = tlsSettings;
}
//if Reality
if (node.StreamSecurity == Global.StreamSecurityReality)
{
streamSettings.security = node.StreamSecurity;
TlsSettings4Ray realitySettings = new()
{
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint,
serverName = sni,
publicKey = node.PublicKey,
shortId = node.ShortId,
spiderX = node.SpiderX,
mldsa65Verify = node.Mldsa65Verify,
show = false,
};
streamSettings.realitySettings = realitySettings;
}
//streamSettings
switch (node.GetNetwork())
{
case nameof(ETransport.kcp):
KcpSettings4Ray kcpSettings = new()
{
mtu = _config.KcpItem.Mtu,
tti = _config.KcpItem.Tti
};
kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity;
kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity;
kcpSettings.congestion = _config.KcpItem.Congestion;
kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize;
kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize;
kcpSettings.header = new Header4Ray
{
type = node.HeaderType,
domain = host.IsNullOrEmpty() ? null : host
};
if (path.IsNotEmpty())
{
kcpSettings.seed = path;
}
streamSettings.kcpSettings = kcpSettings;
break;
//ws
case nameof(ETransport.ws):
WsSettings4Ray wsSettings = new();
wsSettings.headers = new Headers4Ray();
if (host.IsNotEmpty())
{
wsSettings.host = host;
wsSettings.headers.Host = host;
}
if (path.IsNotEmpty())
{
wsSettings.path = path;
}
if (useragent.IsNotEmpty())
{
wsSettings.headers.UserAgent = useragent;
}
streamSettings.wsSettings = wsSettings;
break;
//httpupgrade
case nameof(ETransport.httpupgrade):
HttpupgradeSettings4Ray httpupgradeSettings = new();
if (path.IsNotEmpty())
{
httpupgradeSettings.path = path;
}
if (host.IsNotEmpty())
{
httpupgradeSettings.host = host;
}
streamSettings.httpupgradeSettings = httpupgradeSettings;
break;
//xhttp
case nameof(ETransport.xhttp):
streamSettings.network = ETransport.xhttp.ToString();
XhttpSettings4Ray xhttpSettings = new();
if (path.IsNotEmpty())
{
xhttpSettings.path = path;
}
if (host.IsNotEmpty())
{
xhttpSettings.host = host;
}
if (node.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(node.HeaderType))
{
xhttpSettings.mode = node.HeaderType;
}
if (node.Extra.IsNotEmpty())
{
xhttpSettings.extra = JsonUtils.ParseJson(node.Extra);
}
streamSettings.xhttpSettings = xhttpSettings;
await GenOutboundMux(node, outbound);
break;
//h2
case nameof(ETransport.h2):
HttpSettings4Ray httpSettings = new();
if (host.IsNotEmpty())
{
httpSettings.host = Utils.String2List(host);
}
httpSettings.path = path;
streamSettings.httpSettings = httpSettings;
break;
//quic
case nameof(ETransport.quic):
QuicSettings4Ray quicsettings = new()
{
security = host,
key = path,
header = new Header4Ray
{
type = node.HeaderType
}
};
streamSettings.quicSettings = quicsettings;
if (node.StreamSecurity == Global.StreamSecurity)
{
if (sni.IsNotEmpty())
{
streamSettings.tlsSettings.serverName = sni;
}
else
{
streamSettings.tlsSettings.serverName = node.Address;
}
}
break;
case nameof(ETransport.grpc):
GrpcSettings4Ray grpcSettings = new()
{
authority = host.IsNullOrEmpty() ? null : host,
serviceName = path,
multiMode = node.HeaderType == Global.GrpcMultiMode,
idle_timeout = _config.GrpcItem.IdleTimeout,
health_check_timeout = _config.GrpcItem.HealthCheckTimeout,
permit_without_stream = _config.GrpcItem.PermitWithoutStream,
initial_windows_size = _config.GrpcItem.InitialWindowsSize,
};
streamSettings.grpcSettings = grpcSettings;
break;
default:
//tcp
if (node.HeaderType == Global.TcpHeaderHttp)
{
TcpSettings4Ray tcpSettings = new()
{
header = new Header4Ray
{
type = node.HeaderType
}
};
//request Host
string request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName);
string[] arrHost = host.Split(',');
string host2 = string.Join(",".AppendQuotes(), arrHost);
request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}");
request = request.Replace("$requestUserAgent$", $"{useragent.AppendQuotes()}");
//Path
string pathHttp = @"/";
if (path.IsNotEmpty())
{
string[] arrPath = path.Split(',');
pathHttp = string.Join(",".AppendQuotes(), arrPath);
}
request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}");
tcpSettings.header.request = JsonUtils.Deserialize<object>(request);
streamSettings.tcpSettings = tcpSettings;
}
break;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig)
{
//fragment proxy
if (_config.CoreBasicItem.EnableFragment
&& v2rayConfig.outbounds.First().streamSettings?.security.IsNullOrEmpty() == false)
{
var fragmentOutbound = new Outbounds4Ray
{
protocol = "freedom",
tag = $"{Global.ProxyTag}3",
settings = new()
{
fragment = new()
{
packets = _config.Fragment4RayItem?.Packets,
length = _config.Fragment4RayItem?.Length,
interval = _config.Fragment4RayItem?.Interval
}
}
};
v2rayConfig.outbounds.Add(fragmentOutbound);
v2rayConfig.outbounds.First().streamSettings.sockopt = new()
{
dialerProxy = fragmentOutbound.tag
};
return 0;
}
if (node.Subid.IsNullOrEmpty())
{
return 0;
}
try
{
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem is null)
{
return 0;
}
//current proxy
var outbound = v2rayConfig.outbounds.First();
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
//Previous proxy
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
string? prevOutboundTag = null;
if (prevNode is not null
&& Global.XraySupportConfigType.Contains(prevNode.ConfigType))
{
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevOutboundTag = $"prev-{Global.ProxyTag}";
prevOutbound.tag = prevOutboundTag;
v2rayConfig.outbounds.Add(prevOutbound);
}
var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag);
if (nextOutbound is not null)
{
v2rayConfig.outbounds.Insert(0, nextOutbound);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig)
{
try
{
// Get template and initialize list
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
return 0;
}
var resultOutbounds = new List<Outbounds4Ray>();
var prevOutbounds = new List<Outbounds4Ray>(); // Separate list for prev outbounds and fragment
// Cache for chain proxies to avoid duplicate generation
var nextProxyCache = new Dictionary<string, Outbounds4Ray?>();
var prevProxyTags = new Dictionary<string, string?>(); // Map from profile name to tag
int prevIndex = 0; // Index for prev outbounds
// Process nodes
int index = 0;
foreach (var node in nodes)
{
index++;
// Handle proxy chain
string? prevTag = null;
var currentOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null);
if (nextOutbound != null)
{
nextOutbound = JsonUtils.DeepCopy(nextOutbound);
}
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
// current proxy
await GenOutbound(node, currentOutbound);
currentOutbound.tag = $"{Global.ProxyTag}-{index}";
if (!node.Subid.IsNullOrEmpty())
{
if (prevProxyTags.TryGetValue(node.Subid, out var value))
{
prevTag = value; // maybe null
}
else
{
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode is not null
&& Global.XraySupportConfigType.Contains(prevNode.ConfigType))
{
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}";
prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound);
}
prevProxyTags[node.Subid] = prevTag;
}
nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound);
if (!nextProxyCache.ContainsKey(node.Subid))
{
nextProxyCache[node.Subid] = nextOutbound;
}
}
if (nextOutbound is not null)
{
resultOutbounds.Add(nextOutbound);
}
resultOutbounds.Add(currentOutbound);
}
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
/// <summary>
/// Generates a chained outbound configuration for the given subItem and outbound.
/// The outbound's tag must be set before calling this method.
/// Returns the next proxy's outbound configuration, which may be null if no next proxy exists.
/// </summary>
/// <param name="subItem">The subscription item containing proxy chain information.</param>
/// <param name="outbound">The current outbound configuration. Its tag must be set before calling this method.</param>
/// <param name="prevOutboundTag">The tag of the previous outbound in the chain, if any.</param>
/// <param name="nextOutbound">The outbound for the next proxy in the chain, if already created. If null, will be created inside.</param>
/// <returns>
/// The outbound configuration for the next proxy in the chain, or null if no next proxy exists.
/// </returns>
private async Task<Outbounds4Ray?> GenChainOutbounds(SubItem subItem, Outbounds4Ray outbound, string? prevOutboundTag, Outbounds4Ray? nextOutbound = null)
{
try
{
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (!prevOutboundTag.IsNullOrEmpty())
{
outbound.streamSettings.sockopt = new()
{
dialerProxy = prevOutboundTag
};
}
// Next proxy
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& Global.XraySupportConfigType.Contains(nextNode.ConfigType))
{
if (nextOutbound == null)
{
nextOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(nextNode, nextOutbound);
}
nextOutbound.tag = outbound.tag;
outbound.tag = $"mid-{outbound.tag}";
nextOutbound.streamSettings.sockopt = new()
{
dialerProxy = outbound.tag
};
}
return nextOutbound;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return null;
}
}

View File

@@ -0,0 +1,142 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<int> GenRouting(V2rayConfig v2rayConfig)
{
try
{
if (v2rayConfig.routing?.rules != null)
{
v2rayConfig.routing.domainStrategy = _config.RoutingBasicItem.DomainStrategy;
var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing != null)
{
if (routing.DomainStrategy.IsNotEmpty())
{
v2rayConfig.routing.domainStrategy = routing.DomainStrategy;
}
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var item in rules)
{
if (item.Enabled)
{
var item2 = JsonUtils.Deserialize<RulesItem4Ray>(JsonUtils.Serialize(item));
await GenRoutingUserRule(item2, v2rayConfig);
}
}
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenRoutingUserRule(RulesItem4Ray? rule, V2rayConfig v2rayConfig)
{
try
{
if (rule == null)
{
return 0;
}
rule.outboundTag = await GenRoutingUserRuleOutbound(rule.outboundTag, v2rayConfig);
if (rule.port.IsNullOrEmpty())
{
rule.port = null;
}
if (rule.network.IsNullOrEmpty())
{
rule.network = null;
}
if (rule.domain?.Count == 0)
{
rule.domain = null;
}
if (rule.ip?.Count == 0)
{
rule.ip = null;
}
if (rule.protocol?.Count == 0)
{
rule.protocol = null;
}
if (rule.inboundTag?.Count == 0)
{
rule.inboundTag = null;
}
var hasDomainIp = false;
if (rule.domain?.Count > 0)
{
var it = JsonUtils.DeepCopy(rule);
it.ip = null;
it.type = "field";
for (var k = it.domain.Count - 1; k >= 0; k--)
{
if (it.domain[k].StartsWith("#"))
{
it.domain.RemoveAt(k);
}
it.domain[k] = it.domain[k].Replace(Global.RoutingRuleComma, ",");
}
v2rayConfig.routing.rules.Add(it);
hasDomainIp = true;
}
if (rule.ip?.Count > 0)
{
var it = JsonUtils.DeepCopy(rule);
it.domain = null;
it.type = "field";
v2rayConfig.routing.rules.Add(it);
hasDomainIp = true;
}
if (!hasDomainIp)
{
if (rule.port.IsNotEmpty()
|| rule.protocol?.Count > 0
|| rule.inboundTag?.Count > 0
|| rule.network != null
)
{
var it = JsonUtils.DeepCopy(rule);
it.type = "field";
v2rayConfig.routing.rules.Add(it);
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<string?> GenRoutingUserRuleOutbound(string outboundTag, V2rayConfig v2rayConfig)
{
if (Global.OutboundTags.Contains(outboundTag))
{
return outboundTag;
}
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (node == null
|| !Global.XraySupportConfigType.Contains(node.ConfigType))
{
return Global.ProxyTag;
}
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(node, outbound);
outbound.tag = Global.ProxyTag + node.IndexId.ToString();
v2rayConfig.outbounds.Add(outbound);
return outbound.tag;
}
}

View File

@@ -0,0 +1,51 @@
namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<int> GenStatistic(V2rayConfig v2rayConfig)
{
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{
string tag = EInboundProtocol.api.ToString();
Metrics4Ray apiObj = new();
Policy4Ray policyObj = new();
SystemPolicy4Ray policySystemSetting = new();
v2rayConfig.stats = new Stats4Ray();
apiObj.tag = tag;
v2rayConfig.metrics = apiObj;
policySystemSetting.statsOutboundDownlink = true;
policySystemSetting.statsOutboundUplink = true;
policyObj.system = policySystemSetting;
v2rayConfig.policy = policyObj;
if (!v2rayConfig.inbounds.Exists(item => item.tag == tag))
{
Inbounds4Ray apiInbound = new();
Inboundsettings4Ray apiInboundSettings = new();
apiInbound.tag = tag;
apiInbound.listen = Global.Loopback;
apiInbound.port = AppManager.Instance.StatePort;
apiInbound.protocol = Global.InboundAPIProtocol;
apiInboundSettings.address = Global.Loopback;
apiInbound.settings = apiInboundSettings;
v2rayConfig.inbounds.Add(apiInbound);
}
if (!v2rayConfig.routing.rules.Exists(item => item.outboundTag == tag))
{
RulesItem4Ray apiRoutingRule = new()
{
inboundTag = new List<string> { tag },
outboundTag = tag,
type = "field"
};
v2rayConfig.routing.rules.Add(apiRoutingRule);
}
}
return await Task.FromResult(0);
}
}

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,11 +15,11 @@ 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
{ {
SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13);
var progress = new Progress<string>(); var progress = new Progress<string>();
progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}"); progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}");
@@ -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;
@@ -45,7 +44,7 @@ public class DownloadService
{ {
try try
{ {
SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13);
UpdateCompleted?.Invoke(this, new RetResult(false, $"{ResUI.Downloading} {url}")); UpdateCompleted?.Invoke(this, new RetResult(false, $"{ResUI.Downloading} {url}"));
var progress = new Progress<double>(); var progress = new Progress<double>();
@@ -72,7 +71,7 @@ public class DownloadService
public async Task<string?> UrlRedirectAsync(string url, bool blProxy) public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
{ {
SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13);
var webRequestHandler = new SocketsHttpHandler var webRequestHandler = new SocketsHttpHandler
{ {
AllowAutoRedirect = false, AllowAutoRedirect = false,
@@ -142,7 +141,7 @@ public class DownloadService
{ {
try try
{ {
SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13);
var webProxy = await GetWebProxy(blProxy); var webProxy = await GetWebProxy(blProxy);
var client = new HttpClient(new SocketsHttpHandler() var client = new HttpClient(new SocketsHttpHandler()
{ {
@@ -187,7 +186,7 @@ public class DownloadService
{ {
try try
{ {
SetSecurityProtocol(AppHandler.Instance.Config.GuiItem.EnableSecurityProtocolTls13); SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13);
var webProxy = await GetWebProxy(blProxy); var webProxy = await GetWebProxy(blProxy);
@@ -210,70 +209,13 @@ public class DownloadService
return null; return null;
} }
public async Task<int> RunAvailabilityCheck(IWebProxy? webProxy)
{
var responseTime = -1;
try
{
webProxy ??= await GetWebProxy(true);
var config = AppHandler.Instance.Config;
for (var i = 0; i < 2; i++)
{
responseTime = await GetRealPingTime(config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
if (responseTime > 0)
{
break;
}
await Task.Delay(500);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return -1;
}
return responseTime;
}
public async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
{
var responseTime = -1;
try
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout));
using var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
UseProxy = webProxy != null
});
List<int> oneTime = new();
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}
catch //(Exception ex)
{
//Utile.SaveLog(ex.Message, ex);
}
return responseTime;
}
private async Task<WebProxy?> GetWebProxy(bool blProxy) private async Task<WebProxy?> GetWebProxy(bool blProxy)
{ {
if (!blProxy) if (!blProxy)
{ {
return null; return null;
} }
var port = AppHandler.Instance.GetLocalPort(EInboundProtocol.socks); var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
if (await SocketCheck(Global.Loopback, port) == false) if (await SocketCheck(Global.Loopback, port) == false)
{ {
return null; return null;

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 ProfileExHandler.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,19 +91,19 @@ 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, "");
ProfileExHandler.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);
ProfileExHandler.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);
ProfileExHandler.Instance.SetTestDelay(it.IndexId, 0); ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
ProfileExHandler.Instance.SetTestSpeed(it.IndexId, 0); ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break; break;
} }
} }
@@ -132,8 +126,8 @@ public class SpeedtestService
{ {
var responseTime = await GetTcpingTime(it.Address, it.Port); var responseTime = await GetTcpingTime(it.Address, it.Port);
ProfileExHandler.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)
{ {
@@ -191,15 +185,13 @@ public class SpeedtestService
var pid = -1; var pid = -1;
try try
{ {
pid = await CoreHandler.Instance.LoadCoreConfigSpeedtest(selecteds); pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds);
if (pid < 0) if (pid < 0)
{ {
return false; return false;
} }
await Task.Delay(1000); await Task.Delay(1000);
var downloadHandle = new DownloadService();
List<Task> tasks = new(); List<Task> tasks = new();
foreach (var it in selecteds) foreach (var it in selecteds)
{ {
@@ -213,7 +205,7 @@ public class SpeedtestService
} }
tasks.Add(Task.Run(async () => tasks.Add(Task.Run(async () =>
{ {
await DoRealPing(downloadHandle, it); await DoRealPing(it);
})); }));
} }
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
@@ -241,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)
@@ -255,15 +247,15 @@ public class SpeedtestService
var pid = -1; var pid = -1;
try try
{ {
pid = await CoreHandler.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
{ {
await Task.Delay(1000); await Task.Delay(1000);
var delay = await DoRealPing(downloadHandle, it); var delay = await DoRealPing(it);
if (blSpeedTest) if (blSpeedTest)
{ {
if (delay > 0) if (delay > 0)
@@ -272,7 +264,7 @@ public class SpeedtestService
} }
else else
{ {
UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
} }
} }
} }
@@ -294,31 +286,31 @@ public class SpeedtestService
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
} }
private async Task<int> DoRealPing(DownloadService downloadHandle, ServerTestItem it) private async Task<int> DoRealPing(ServerTestItem it)
{ {
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await downloadHandle.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); var responseTime = await HttpClientHelper.Instance.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
ProfileExHandler.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)
{ {
ProfileExHandler.Instance.SetTestSpeed(it.IndexId, dec); ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
} }
UpdateFunc(it.IndexId, "", msg); await UpdateFunc(it.IndexId, "", msg);
}); });
} }
@@ -358,8 +350,8 @@ public class SpeedtestService
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize) private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
{ {
List<List<ServerTestItem>> lstTest = new(); List<List<ServerTestItem>> lstTest = new();
var lst1 = lstSelected.Where(t => t.ConfigType is not (EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls)).ToList(); var lst1 = lstSelected.Where(t => Global.XraySupportConfigType.Contains(t.ConfigType)).ToList();
var lst2 = lstSelected.Where(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls).ToList(); var lst2 = lstSelected.Where(t => Global.SingboxSupportConfigType.Contains(t.ConfigType) && !Global.XraySupportConfigType.Contains(t.ConfigType)).ToList();
for (var num = 0; num < (int)Math.Ceiling(lst1.Count * 1.0 / pageSize); num++) for (var num = 0; num < (int)Math.Ceiling(lst1.Count * 1.0 / pageSize); num++)
{ {
@@ -373,12 +365,12 @@ 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())
{ {
ProfileExHandler.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}:{AppHandler.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}:{AppHandler.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,14 +128,14 @@ 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);
} }
} }
private async Task<RetResult> GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease) private async Task<RetResult> GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease)
{ {
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(type); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
var tagName = string.Empty; var tagName = string.Empty;
if (preRelease) if (preRelease)
{ {
@@ -169,7 +169,7 @@ public class UpdateService
{ {
try try
{ {
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(type); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
string filePath = string.Empty; string filePath = string.Empty;
foreach (var name in coreInfo.CoreExes) foreach (var name in coreInfo.CoreExes)
{ {
@@ -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("");
} }
} }
@@ -221,7 +221,7 @@ public class UpdateService
{ {
try try
{ {
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(type); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
var coreUrl = await GetUrlFromCore(coreInfo) ?? string.Empty; var coreUrl = await GetUrlFromCore(coreInfo) ?? string.Empty;
SemanticVersion curVersion; SemanticVersion curVersion;
string message; string message;
@@ -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;
@@ -379,7 +379,7 @@ public class UpdateService
var geoSiteFiles = new List<string>(); var geoSiteFiles = new List<string>();
//Collect used files list //Collect used files list
var routingItems = await AppHandler.Instance.RoutingItems(); var routingItems = await AppManager.Instance.RoutingItems();
foreach (var routing in routingItems) foreach (var routing in routingItems)
{ {
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet); var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
@@ -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
@@ -434,12 +434,12 @@ public class UpdateService
var fileName = $"{type}-{srsName}.srs"; var fileName = $"{type}-{srsName}.srs";
var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName); var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName);
var url = string.Format(srsUrl, type, $"{type}-{srsName}"); var url = string.Format(srsUrl, type, $"{type}-{srsName}", srsName);
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

@@ -19,7 +19,7 @@ public class AddServer2ViewModel : MyReactiveObject
public AddServer2ViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) public AddServer2ViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
BrowseServerCmd = ReactiveCommand.CreateFromTask(async () => BrowseServerCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -45,25 +45,25 @@ public class AddServer2ViewModel : MyReactiveObject
var remarks = SelectedSource.Remarks; var remarks = SelectedSource.Remarks;
if (remarks.IsNullOrEmpty()) if (remarks.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseFillRemarks); NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks);
return; return;
} }
if (SelectedSource.Address.IsNullOrEmpty()) if (SelectedSource.Address.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillServerAddressCustom); NoticeManager.Instance.Enqueue(ResUI.FillServerAddressCustom);
return; return;
} }
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0) if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0)
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); _updateView?.Invoke(EViewAction.CloseWindow, null);
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
@@ -74,12 +74,12 @@ public class AddServer2ViewModel : MyReactiveObject
return; return;
} }
var item = await AppHandler.Instance.GetProfileItem(SelectedSource.IndexId); var item = await AppManager.Instance.GetProfileItem(SelectedSource.IndexId);
item ??= SelectedSource; item ??= SelectedSource;
item.Address = fileName; item.Address = fileName;
if (await ConfigHandler.AddCustomServer(_config, item, false) == 0) if (await ConfigHandler.AddCustomServer(_config, item, false) == 0)
{ {
NoticeHandler.Instance.Enqueue(ResUI.SuccessfullyImportedCustomServer); NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedCustomServer);
if (item.IndexId.IsNotEmpty()) if (item.IndexId.IsNotEmpty())
{ {
SelectedSource = JsonUtils.DeepCopy(item); SelectedSource = JsonUtils.DeepCopy(item);
@@ -88,7 +88,7 @@ public class AddServer2ViewModel : MyReactiveObject
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.FailedImportedCustomServer); NoticeManager.Instance.Enqueue(ResUI.FailedImportedCustomServer);
} }
} }
@@ -97,7 +97,7 @@ public class AddServer2ViewModel : MyReactiveObject
var address = SelectedSource.Address; var address = SelectedSource.Address;
if (address.IsNullOrEmpty()) if (address.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillServerAddressCustom); NoticeManager.Instance.Enqueue(ResUI.FillServerAddressCustom);
return; return;
} }
@@ -108,7 +108,7 @@ public class AddServer2ViewModel : MyReactiveObject
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.FailedReadConfiguration); NoticeManager.Instance.Enqueue(ResUI.FailedReadConfiguration);
} }
await Task.CompletedTask; await Task.CompletedTask;
} }

View File

@@ -16,7 +16,7 @@ public class AddServerViewModel : MyReactiveObject
public AddServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) public AddServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -43,32 +43,32 @@ public class AddServerViewModel : MyReactiveObject
{ {
if (SelectedSource.Remarks.IsNullOrEmpty()) if (SelectedSource.Remarks.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseFillRemarks); NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks);
return; return;
} }
if (SelectedSource.Address.IsNullOrEmpty()) if (SelectedSource.Address.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillServerAddress); NoticeManager.Instance.Enqueue(ResUI.FillServerAddress);
return; return;
} }
var port = SelectedSource.Port.ToString(); var port = SelectedSource.Port.ToString();
if (port.IsNullOrEmpty() || !Utils.IsNumeric(port) if (port.IsNullOrEmpty() || !Utils.IsNumeric(port)
|| SelectedSource.Port <= 0 || SelectedSource.Port >= Global.MaxPort) || SelectedSource.Port <= 0 || SelectedSource.Port >= Global.MaxPort)
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillCorrectServerPort); NoticeManager.Instance.Enqueue(ResUI.FillCorrectServerPort);
return; return;
} }
if (SelectedSource.ConfigType == EConfigType.Shadowsocks) if (SelectedSource.ConfigType == EConfigType.Shadowsocks)
{ {
if (SelectedSource.Id.IsNullOrEmpty()) if (SelectedSource.Id.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillPassword); NoticeManager.Instance.Enqueue(ResUI.FillPassword);
return; return;
} }
if (SelectedSource.Security.IsNullOrEmpty()) if (SelectedSource.Security.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectEncryption); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectEncryption);
return; return;
} }
} }
@@ -76,7 +76,7 @@ public class AddServerViewModel : MyReactiveObject
{ {
if (SelectedSource.Id.IsNullOrEmpty()) if (SelectedSource.Id.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillUUID); NoticeManager.Instance.Enqueue(ResUI.FillUUID);
return; return;
} }
} }
@@ -84,12 +84,12 @@ public class AddServerViewModel : MyReactiveObject
if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) if (await ConfigHandler.AddServer(_config, SelectedSource) == 0)
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); _updateView?.Invoke(EViewAction.CloseWindow, null);
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
} }

View File

@@ -22,7 +22,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
public BackupAndRestoreViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public BackupAndRestoreViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () => WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -52,14 +52,14 @@ public class BackupAndRestoreViewModel : MyReactiveObject
_config.WebDavItem = SelectedSource; _config.WebDavItem = SelectedSource;
_ = await ConfigHandler.SaveConfig(_config); _ = await ConfigHandler.SaveConfig(_config);
var result = await WebDavHandler.Instance.CheckConnection(); var result = await WebDavManager.Instance.CheckConnection();
if (result) if (result)
{ {
DisplayOperationMsg(ResUI.OperationSuccess); DisplayOperationMsg(ResUI.OperationSuccess);
} }
else else
{ {
DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); DisplayOperationMsg(WebDavManager.Instance.GetLastError());
} }
} }
@@ -70,7 +70,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
var result = await CreateZipFileFromDirectory(fileName); var result = await CreateZipFileFromDirectory(fileName);
if (result) if (result)
{ {
var result2 = await WebDavHandler.Instance.PutFile(fileName); var result2 = await WebDavManager.Instance.PutFile(fileName);
if (result2) if (result2)
{ {
DisplayOperationMsg(ResUI.OperationSuccess); DisplayOperationMsg(ResUI.OperationSuccess);
@@ -78,21 +78,21 @@ public class BackupAndRestoreViewModel : MyReactiveObject
} }
} }
DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); DisplayOperationMsg(WebDavManager.Instance.GetLastError());
} }
private async Task RemoteRestore() private async Task RemoteRestore()
{ {
DisplayOperationMsg(); DisplayOperationMsg();
var fileName = Utils.GetTempPath(Utils.GetGuid()); var fileName = Utils.GetTempPath(Utils.GetGuid());
var result = await WebDavHandler.Instance.GetRawFile(fileName); var result = await WebDavManager.Instance.GetRawFile(fileName);
if (result) if (result)
{ {
await LocalRestore(fileName); await LocalRestore(fileName);
return; return;
} }
DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); DisplayOperationMsg(WebDavManager.Instance.GetLastError());
} }
public async Task<bool> LocalBackup(string fileName) public async Task<bool> LocalBackup(string fileName)
@@ -105,7 +105,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
} }
else else
{ {
DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); DisplayOperationMsg(WebDavManager.Instance.GetLastError());
} }
return result; return result;
@@ -136,8 +136,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,11 +153,11 @@ 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
{ {
DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); DisplayOperationMsg(WebDavManager.Instance.GetLastError());
} }
} }

View File

@@ -1,4 +1,6 @@
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;
using DynamicData.Binding; using DynamicData.Binding;
@@ -11,22 +13,24 @@ 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; }
public CheckUpdateViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public CheckUpdateViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.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;
@@ -41,20 +45,20 @@ public class CheckUpdateViewModel : MyReactiveObject
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)
@@ -69,7 +73,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,17 +85,19 @@ 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();
@@ -129,9 +135,9 @@ 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, "");
@@ -146,12 +152,12 @@ public class CheckUpdateViewModel : MyReactiveObject
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);
} }
} }
@@ -164,12 +170,12 @@ public class CheckUpdateViewModel : MyReactiveObject
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);
} }
@@ -186,21 +192,30 @@ public class CheckUpdateViewModel : MyReactiveObject
{ {
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)
{ {
@@ -212,7 +227,7 @@ public class CheckUpdateViewModel : MyReactiveObject
} }
} }
private void UpgradeN() private async Task UpgradeN()
{ {
try try
{ {
@@ -221,16 +236,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);
} }
} }
@@ -281,7 +303,7 @@ public class CheckUpdateViewModel : MyReactiveObject
} }
} }
UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully); await UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully);
if (File.Exists(fileName)) if (File.Exists(fileName))
{ {
@@ -290,23 +312,31 @@ 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); var itemCopy = JsonUtils.DeepCopy(found);
itemCopy.Remarks = model.Remarks; itemCopy.Remarks = model.Remarks;
_checkUpdateModel.Replace(found, itemCopy); CheckUpdateModels.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; }
@@ -26,7 +26,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
public ClashConnectionsViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ClashConnectionsViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh; AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh;
@@ -58,18 +58,22 @@ public class ClashConnectionsViewModel : MyReactiveObject
private async Task GetClashConnections() private async Task GetClashConnections()
{ {
var ret = await ClashApiHandler.Instance.GetClashConnectionsAsync(); var ret = await ClashApiManager.Instance.GetClashConnectionsAsync();
if (ret == null) if (ret == null)
{ {
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,9 +120,9 @@ public class ClashConnectionsViewModel : MyReactiveObject
} }
else else
{ {
_connectionItems.Clear(); ConnectionItems.Clear();
} }
await ClashApiHandler.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; }
@@ -43,7 +42,7 @@ public class ClashProxiesViewModel : MyReactiveObject
public ClashProxiesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ClashProxiesViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () => ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -152,13 +151,13 @@ public class ClashProxiesViewModel : MyReactiveObject
{ {
{ "mode", mode.ToString().ToLower() } { "mode", mode.ToString().ToLower() }
}; };
await ClashApiHandler.Instance.ClashConfigUpdate(headers); await ClashApiManager.Instance.ClashConfigUpdate(headers);
} }
} }
private async Task GetClashProxies(bool refreshUI) private async Task GetClashProxies(bool refreshUI)
{ {
var ret = await ClashApiHandler.Instance.GetClashProxiesAsync(); var ret = await ClashApiManager.Instance.GetClashProxiesAsync();
if (ret?.Item1 == null || ret.Item2 == null) if (ret?.Item1 == null || ret.Item2 == null)
{ {
return; return;
@@ -168,11 +167,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,9 +179,9 @@ public class ClashProxiesViewModel : MyReactiveObject
} }
var selectedName = SelectedGroup?.Name; var selectedName = SelectedGroup?.Name;
_proxyGroups.Clear(); ProxyGroups.Clear();
var proxyGroups = ClashApiHandler.Instance.GetClashProxyGroups(); var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups();
if (proxyGroups != null && proxyGroups.Count > 0) if (proxyGroups != null && proxyGroups.Count > 0)
{ {
foreach (var it in proxyGroups) foreach (var it in proxyGroups)
@@ -196,7 +195,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 +211,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 +224,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 +243,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 +296,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)
@@ -352,43 +351,48 @@ public class ClashProxiesViewModel : MyReactiveObject
var selectedProxy = TryGetProxy(name); var selectedProxy = TryGetProxy(name);
if (selectedProxy == null || selectedProxy.type != "Selector") if (selectedProxy == null || selectedProxy.type != "Selector")
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return; return;
} }
await ClashApiHandler.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;
} }
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
private async Task ProxiesDelayTest(bool blAll = true) private async Task ProxiesDelayTest(bool blAll = true)
{ {
ClashApiHandler.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}"); //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 +414,7 @@ public class ClashProxiesViewModel : MyReactiveObject
detail.Delay = _delayTimeout; detail.Delay = _delayTimeout;
detail.DelayName = string.Empty; detail.DelayName = string.Empty;
} }
_proxyDetails.Replace(detail, JsonUtils.DeepCopy(detail)); ProxyDetails.Replace(detail, JsonUtils.DeepCopy(detail));
} }
#endregion proxy function #endregion proxy function

View File

@@ -38,7 +38,7 @@ public class DNSSettingViewModel : MyReactiveObject
public DNSSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public DNSSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync); SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync);
@@ -60,7 +60,7 @@ public class DNSSettingViewModel : MyReactiveObject
private async Task Init() private async Task Init()
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
var item = _config.SimpleDNSItem; var item = _config.SimpleDNSItem;
UseSystemHosts = item.UseSystemHosts; UseSystemHosts = item.UseSystemHosts;
AddCommonHosts = item.AddCommonHosts; AddCommonHosts = item.AddCommonHosts;
@@ -76,14 +76,14 @@ public class DNSSettingViewModel : MyReactiveObject
Hosts = item.Hosts; Hosts = item.Hosts;
DirectExpectedIPs = item.DirectExpectedIPs; DirectExpectedIPs = item.DirectExpectedIPs;
var item1 = await AppHandler.Instance.GetDNSItem(ECoreType.Xray); var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray);
RayCustomDNSEnableCompatible = item1.Enabled; RayCustomDNSEnableCompatible = item1.Enabled;
UseSystemHostsCompatible = item1.UseSystemHosts; UseSystemHostsCompatible = item1.UseSystemHosts;
DomainStrategy4FreedomCompatible = item1?.DomainStrategy4Freedom ?? string.Empty; DomainStrategy4FreedomCompatible = item1?.DomainStrategy4Freedom ?? string.Empty;
DomainDNSAddressCompatible = item1?.DomainDNSAddress ?? string.Empty; DomainDNSAddressCompatible = item1?.DomainDNSAddress ?? string.Empty;
NormalDNSCompatible = item1?.NormalDNS ?? string.Empty; NormalDNSCompatible = item1?.NormalDNS ?? string.Empty;
var item2 = await AppHandler.Instance.GetDNSItem(ECoreType.sing_box); var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
SBCustomDNSEnableCompatible = item2.Enabled; SBCustomDNSEnableCompatible = item2.Enabled;
DomainStrategy4Freedom2Compatible = item2?.DomainStrategy4Freedom ?? string.Empty; DomainStrategy4Freedom2Compatible = item2?.DomainStrategy4Freedom ?? string.Empty;
DomainDNSAddress2Compatible = item2?.DomainDNSAddress ?? string.Empty; DomainDNSAddress2Compatible = item2?.DomainDNSAddress ?? string.Empty;
@@ -117,7 +117,7 @@ public class DNSSettingViewModel : MyReactiveObject
{ {
if (NormalDNSCompatible.Contains('{') || NormalDNSCompatible.Contains('}')) if (NormalDNSCompatible.Contains('{') || NormalDNSCompatible.Contains('}'))
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillCorrectDNSText); NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText);
return; return;
} }
} }
@@ -127,7 +127,7 @@ public class DNSSettingViewModel : MyReactiveObject
var obj2 = JsonUtils.Deserialize<Dns4Sbox>(NormalDNS2Compatible); var obj2 = JsonUtils.Deserialize<Dns4Sbox>(NormalDNS2Compatible);
if (obj2 == null) if (obj2 == null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillCorrectDNSText); NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText);
return; return;
} }
} }
@@ -136,12 +136,12 @@ public class DNSSettingViewModel : MyReactiveObject
var obj2 = JsonUtils.Deserialize<Dns4Sbox>(TunDNS2Compatible); var obj2 = JsonUtils.Deserialize<Dns4Sbox>(TunDNS2Compatible);
if (obj2 == null) if (obj2 == null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillCorrectDNSText); NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText);
return; return;
} }
} }
var item1 = await AppHandler.Instance.GetDNSItem(ECoreType.Xray); var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray);
item1.Enabled = RayCustomDNSEnableCompatible; item1.Enabled = RayCustomDNSEnableCompatible;
item1.DomainStrategy4Freedom = DomainStrategy4FreedomCompatible; item1.DomainStrategy4Freedom = DomainStrategy4FreedomCompatible;
item1.DomainDNSAddress = DomainDNSAddressCompatible; item1.DomainDNSAddress = DomainDNSAddressCompatible;
@@ -149,7 +149,7 @@ public class DNSSettingViewModel : MyReactiveObject
item1.NormalDNS = NormalDNSCompatible; item1.NormalDNS = NormalDNSCompatible;
await ConfigHandler.SaveDNSItems(_config, item1); await ConfigHandler.SaveDNSItems(_config, item1);
var item2 = await AppHandler.Instance.GetDNSItem(ECoreType.sing_box); var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
item2.Enabled = SBCustomDNSEnableCompatible; item2.Enabled = SBCustomDNSEnableCompatible;
item2.DomainStrategy4Freedom = DomainStrategy4Freedom2Compatible; item2.DomainStrategy4Freedom = DomainStrategy4Freedom2Compatible;
item2.DomainDNSAddress = DomainDNSAddress2Compatible; item2.DomainDNSAddress = DomainDNSAddress2Compatible;

View File

@@ -41,7 +41,7 @@ public class FullConfigTemplateViewModel : MyReactiveObject
public FullConfigTemplateViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public FullConfigTemplateViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -53,13 +53,13 @@ public class FullConfigTemplateViewModel : MyReactiveObject
private async Task Init() private async Task Init()
{ {
var item = await AppHandler.Instance.GetFullConfigTemplateItem(ECoreType.Xray); var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
EnableFullConfigTemplate4Ray = item?.Enabled ?? false; EnableFullConfigTemplate4Ray = item?.Enabled ?? false;
FullConfigTemplate4Ray = item?.Config ?? string.Empty; FullConfigTemplate4Ray = item?.Config ?? string.Empty;
AddProxyOnly4Ray = item?.AddProxyOnly ?? false; AddProxyOnly4Ray = item?.AddProxyOnly ?? false;
ProxyDetour4Ray = item?.ProxyDetour ?? string.Empty; ProxyDetour4Ray = item?.ProxyDetour ?? string.Empty;
var item2 = await AppHandler.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); var item2 = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box);
EnableFullConfigTemplate4Singbox = item2?.Enabled ?? false; EnableFullConfigTemplate4Singbox = item2?.Enabled ?? false;
FullConfigTemplate4Singbox = item2?.Config ?? string.Empty; FullConfigTemplate4Singbox = item2?.Config ?? string.Empty;
FullTunConfigTemplate4Singbox = item2?.TunConfig ?? string.Empty; FullTunConfigTemplate4Singbox = item2?.TunConfig ?? string.Empty;
@@ -75,13 +75,13 @@ public class FullConfigTemplateViewModel : MyReactiveObject
if (!await SaveSingboxConfigAsync()) if (!await SaveSingboxConfigAsync())
return; return;
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_ = _updateView?.Invoke(EViewAction.CloseWindow, null); _ = _updateView?.Invoke(EViewAction.CloseWindow, null);
} }
private async Task<bool> SaveXrayConfigAsync() private async Task<bool> SaveXrayConfigAsync()
{ {
var item = await AppHandler.Instance.GetFullConfigTemplateItem(ECoreType.Xray); var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
item.Enabled = EnableFullConfigTemplate4Ray; item.Enabled = EnableFullConfigTemplate4Ray;
item.Config = null; item.Config = null;
@@ -96,7 +96,7 @@ public class FullConfigTemplateViewModel : MyReactiveObject
private async Task<bool> SaveSingboxConfigAsync() private async Task<bool> SaveSingboxConfigAsync()
{ {
var item = await AppHandler.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box);
item.Enabled = EnableFullConfigTemplate4Singbox; item.Enabled = EnableFullConfigTemplate4Singbox;
item.Config = null; item.Config = null;
item.TunConfig = null; item.TunConfig = null;

View File

@@ -11,7 +11,7 @@ public class GlobalHotkeySettingViewModel : MyReactiveObject
public GlobalHotkeySettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public GlobalHotkeySettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
_globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys); _globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys);
@@ -58,7 +58,7 @@ public class GlobalHotkeySettingViewModel : MyReactiveObject
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Concurrency;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat; using Splat;
@@ -71,7 +72,7 @@ public class MainWindowViewModel : MyReactiveObject
public MainWindowViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public MainWindowViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
@@ -178,7 +179,7 @@ public class MainWindowViewModel : MyReactiveObject
{ {
if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true) if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true)
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
}); });
RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () => RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -226,13 +227,13 @@ public class MainWindowViewModel : MyReactiveObject
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 ProfileExHandler.Instance.Init(); await ProfileExManager.Instance.Init();
await CoreHandler.Instance.Init(_config, UpdateHandler); await CoreManager.Instance.Init(_config, UpdateHandler);
TaskHandler.Instance.RegUpdateTask(_config, UpdateTaskHandler); TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler);
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{ {
await StatisticsHandler.Instance.Init(_config, UpdateStatisticsHandler); await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler);
} }
BlReloadEnabled = true; BlReloadEnabled = true;
@@ -245,95 +246,40 @@ public class MainWindowViewModel : MyReactiveObject
#region Actions #region Actions
private void UpdateHandler(bool notify, string msg) private async Task UpdateHandler(bool notify, string msg)
{ {
NoticeHandler.Instance.SendMessage(msg); NoticeManager.Instance.SendMessage(msg);
if (notify) if (notify)
{ {
NoticeHandler.Instance.Enqueue(msg); NoticeManager.Instance.Enqueue(msg);
} }
} }
private void UpdateTaskHandler(bool success, string msg) private async Task UpdateTaskHandler(bool success, string msg)
{ {
NoticeHandler.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.OnNext(Unit.Default);
} }
} }
} }
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.OnNext(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 ProfileExHandler.Instance.SaveTo();
await StatisticsHandler.Instance.SaveTo();
await CoreHandler.Instance.CoreStop();
StatisticsHandler.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))
{
NoticeHandler.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) public void ShowHideWindow(bool? blShow)
@@ -341,18 +287,15 @@ public class MainWindowViewModel : MyReactiveObject
_updateView?.Invoke(EViewAction.ShowHideWindow, 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.OnNext(Unit.Default);
await Task.Delay(200);
} }
private void RefreshSubscriptions() private void RefreshSubscriptions()
@@ -384,7 +327,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,16 +342,16 @@ 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();
NoticeHandler.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret)); NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret));
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
@@ -420,7 +363,7 @@ public class MainWindowViewModel : MyReactiveObject
public async Task ScanScreenResult(byte[]? bytes) public async Task ScanScreenResult(byte[]? bytes)
{ {
var result = QRCodeHelper.ParseBarcode(bytes); var result = QRCodeUtils.ParseBarcode(bytes);
await AddScanResultAsync(result); await AddScanResultAsync(result);
} }
@@ -437,7 +380,7 @@ public class MainWindowViewModel : MyReactiveObject
return; return;
} }
var result = QRCodeHelper.ParseBarcode(fileName); var result = QRCodeUtils.ParseBarcode(fileName);
await AddScanResultAsync(result); await AddScanResultAsync(result);
} }
@@ -445,20 +388,20 @@ public class MainWindowViewModel : MyReactiveObject
{ {
if (result.IsNullOrEmpty()) if (result.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.NoValidQRcodeFound); NoticeManager.Instance.Enqueue(ResUI.NoValidQRcodeFound);
} }
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();
NoticeHandler.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan); NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan);
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
} }
@@ -477,7 +420,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
@@ -526,13 +469,13 @@ public class MainWindowViewModel : MyReactiveObject
public async Task RebootAsAdmin() public async Task RebootAsAdmin()
{ {
ProcUtils.RebootAsAdmin(); ProcUtils.RebootAsAdmin();
await MyAppExitAsync(false); await AppManager.Instance.AppExitAsync(true);
} }
private async Task ClearServerStatistics() private async Task ClearServerStatistics()
{ {
await StatisticsHandler.Instance.ClearAllServerStatistics(); await StatisticsManager.Instance.ClearAllServerStatistics();
RefreshServers(); await RefreshServers();
} }
private async Task OpenTheFileLocation() private async Task OpenTheFileLocation()
@@ -576,7 +519,7 @@ public class MainWindowViewModel : MyReactiveObject
}); });
Locator.Current.GetService<StatusBarViewModel>()?.TestServerAvailability(); Locator.Current.GetService<StatusBarViewModel>()?.TestServerAvailability();
_updateView?.Invoke(EViewAction.DispatcherReload, null); RxApp.MainThreadScheduler.Schedule(() => _ = ReloadResult());
BlReloadEnabled = true; BlReloadEnabled = true;
if (_hasNextReloadJob) if (_hasNextReloadJob)
@@ -586,7 +529,7 @@ public class MainWindowViewModel : MyReactiveObject
} }
} }
public void ReloadResult() public async Task ReloadResult()
{ {
// BlReloadEnabled = true; // BlReloadEnabled = true;
//Locator.Current.GetService<StatusBarViewModel>()?.ChangeSystemProxyAsync(_config.systemProxyItem.sysProxyType, false); //Locator.Current.GetService<StatusBarViewModel>()?.ChangeSystemProxyAsync(_config.systemProxyItem.sysProxyType, false);
@@ -596,19 +539,21 @@ public class MainWindowViewModel : MyReactiveObject
Locator.Current.GetService<ClashProxiesViewModel>()?.ProxiesReload(); Locator.Current.GetService<ClashProxiesViewModel>()?.ProxiesReload();
} }
else else
{ TabMainSelectedIndex = 0; } {
TabMainSelectedIndex = 0;
}
} }
private async Task LoadCore() private async Task LoadCore()
{ {
var node = await ConfigHandler.GetDefaultServer(_config); var node = await ConfigHandler.GetDefaultServer(_config);
await CoreHandler.Instance.LoadCore(node); await CoreManager.Instance.LoadCore(node);
} }
public async Task CloseCore() public async Task CloseCore()
{ {
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
await CoreHandler.Instance.CoreStop(); await CoreManager.Instance.CoreStop();
} }
private async Task AutoHideStartup() private async Task AutoHideStartup()
@@ -631,7 +576,7 @@ public class MainWindowViewModel : MyReactiveObject
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu(); Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu();
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,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Reactive.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
@@ -7,8 +8,8 @@ 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 readonly int _numMaxMsg = 500;
private bool _lastMsgFilterNotAvailable; private bool _lastMsgFilterNotAvailable;
private bool _blLockShow = false; private bool _blLockShow = false;
@@ -20,7 +21,7 @@ public class MsgViewModel : MyReactiveObject
public MsgViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public MsgViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty; MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty;
AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true; AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true;
@@ -34,12 +35,10 @@ public class MsgViewModel : MyReactiveObject
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(async content => await AppendQueueMsg(content));
{
await AppendQueueMsg(x);
} }
private async Task AppendQueueMsg(string msg) private async Task AppendQueueMsg(string msg)

View File

@@ -109,7 +109,7 @@ public class OptionSettingViewModel : MyReactiveObject
public OptionSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public OptionSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -276,7 +276,7 @@ public class OptionSettingViewModel : MyReactiveObject
if (localPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(localPort.ToString()) if (localPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(localPort.ToString())
|| localPort <= 0 || localPort >= Global.MaxPort) || localPort <= 0 || localPort >= Global.MaxPort)
{ {
NoticeHandler.Instance.Enqueue(ResUI.FillLocalListeningPort); NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return; return;
} }
var needReboot = (EnableStatistics != _config.GuiItem.EnableStatistics var needReboot = (EnableStatistics != _config.GuiItem.EnableStatistics
@@ -369,14 +369,14 @@ public class OptionSettingViewModel : MyReactiveObject
if (await ConfigHandler.SaveConfig(_config) == 0) if (await ConfigHandler.SaveConfig(_config) == 0)
{ {
await AutoStartupHandler.UpdateTask(_config); await AutoStartupHandler.UpdateTask(_config);
AppHandler.Instance.Reset(); AppManager.Instance.Reset();
NoticeHandler.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); _updateView?.Invoke(EViewAction.CloseWindow, null);
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }

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 System.Text; using System.Text;
using DynamicData; using DynamicData;
@@ -22,13 +23,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; }
@@ -100,7 +97,7 @@ public class ProfilesViewModel : MyReactiveObject
public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
@@ -126,7 +123,7 @@ public class ProfilesViewModel : MyReactiveObject
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 +244,19 @@ 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.DispatcherStatisticsRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result));
#endregion AppEvents
_ = Init(); _ = Init();
} }
@@ -263,32 +269,27 @@ public class ProfilesViewModel : MyReactiveObject
SelectedServer = 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(); Locator.Current.GetService<MainWindowViewModel>()?.Reload();
} }
public void SetSpeedTestResult(SpeedTestResult result) public async Task SetSpeedTestResult(SpeedTestResult result)
{ {
if (result.IndexId.IsNullOrEmpty()) if (result.IndexId.IsNullOrEmpty())
{ {
NoticeHandler.Instance.SendMessageEx(result.Delay); NoticeManager.Instance.SendMessageEx(result.Delay);
NoticeHandler.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;
@@ -307,11 +308,18 @@ public class ProfilesViewModel : MyReactiveObject
//_profileItems.Replace(item, JsonUtils.DeepCopy(item)); //_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);
@@ -336,11 +344,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 +356,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 +370,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.OnNext(Unit.Default);
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,36 +400,38 @@ public class ProfilesViewModel : MyReactiveObject
SelectedProfile = lstModel.First(); SelectedProfile = lstModel.First();
} }
} }
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
} }
public async Task RefreshSubscriptions() public 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 AppHandler.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();
} }
} }
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{ {
var lstModel = await AppHandler.Instance.ProfileItems(_config.SubIndexId, filter); var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, filter);
await ConfigHandler.SetDefaultServer(_config, lstModel); await ConfigHandler.SetDefaultServer(_config, lstModel);
var lstServerStat = (_config.GuiItem.EnableStatistics ? StatisticsHandler.Instance.ServerStat : null) ?? []; var lstServerStat = (_config.GuiItem.EnableStatistics ? StatisticsManager.Instance.ServerStat : null) ?? [];
var lstProfileExs = await ProfileExHandler.Instance.GetProfileExs(); var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
lstModel = (from t in lstModel lstModel = (from t in lstModel
join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b
from t22 in t2b.DefaultIfEmpty() from t22 in t2b.DefaultIfEmpty()
@@ -474,7 +481,7 @@ public class ProfilesViewModel : MyReactiveObject
{ {
foreach (var profile in orderProfiles) foreach (var profile in orderProfiles)
{ {
var item = await AppHandler.Instance.GetProfileItem(profile.IndexId); var item = await AppManager.Instance.GetProfileItem(profile.IndexId);
if (item is not null) if (item is not null)
{ {
lstSelected.Add(item); lstSelected.Add(item);
@@ -495,10 +502,10 @@ public class ProfilesViewModel : MyReactiveObject
{ {
return; return;
} }
var item = await AppHandler.Instance.GetProfileItem(SelectedProfile.IndexId); var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId);
if (item is null) if (item is null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return; return;
} }
eConfigType = item.ConfigType; eConfigType = item.ConfigType;
@@ -514,7 +521,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();
@@ -536,12 +543,12 @@ public class ProfilesViewModel : MyReactiveObject
var exists = lstSelected.Exists(t => t.IndexId == _config.IndexId); var exists = lstSelected.Exists(t => t.IndexId == _config.IndexId);
await ConfigHandler.RemoveServers(_config, lstSelected); await ConfigHandler.RemoveServers(_config, lstSelected);
NoticeHandler.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,10 +560,10 @@ 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();
} }
NoticeHandler.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2)); NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2));
} }
private async Task CopyServer() private async Task CopyServer()
@@ -568,8 +575,8 @@ public class ProfilesViewModel : MyReactiveObject
} }
if (await ConfigHandler.CopyServer(_config, lstSelected) == 0) if (await ConfigHandler.CopyServer(_config, lstSelected) == 0)
{ {
RefreshServers(); await RefreshServers();
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
} }
@@ -592,16 +599,16 @@ public class ProfilesViewModel : MyReactiveObject
{ {
return; return;
} }
var item = await AppHandler.Instance.GetProfileItem(indexId); var item = await AppManager.Instance.GetProfileItem(indexId);
if (item is null) if (item is null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return; return;
} }
if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0)
{ {
RefreshServers(); await RefreshServers();
Reload(); Reload();
} }
} }
@@ -621,10 +628,10 @@ public class ProfilesViewModel : MyReactiveObject
public async Task ShareServerAsync() public async Task ShareServerAsync()
{ {
var item = await AppHandler.Instance.GetProfileItem(SelectedProfile.IndexId); var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId);
if (item is null) if (item is null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return; return;
} }
var url = FmtHandler.GetShareUri(item); var url = FmtHandler.GetShareUri(item);
@@ -647,12 +654,12 @@ public class ProfilesViewModel : MyReactiveObject
var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad); var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad);
if (ret.Success != true) if (ret.Success != true)
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return; return;
} }
if (ret?.Data?.ToString() == _config.IndexId) if (ret?.Data?.ToString() == _config.IndexId)
{ {
RefreshServers(); await RefreshServers();
Reload(); Reload();
} }
else else
@@ -675,14 +682,14 @@ 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();
NoticeHandler.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count)); NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count));
} }
//move server //move server
@@ -700,9 +707,9 @@ public class ProfilesViewModel : MyReactiveObject
} }
await ConfigHandler.MoveToGroup(_config, lstSelected, SelectedMoveToGroup.Id); await ConfigHandler.MoveToGroup(_config, lstSelected, SelectedMoveToGroup.Id);
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
RefreshServers(); await RefreshServers();
SelectedMoveToGroup = null; SelectedMoveToGroup = null;
SelectedMoveToGroup = new(); SelectedMoveToGroup = new();
} }
@@ -712,7 +719,7 @@ public class ProfilesViewModel : MyReactiveObject
var item = _lstProfile.FirstOrDefault(t => t.IndexId == SelectedProfile.IndexId); var item = _lstProfile.FirstOrDefault(t => t.IndexId == SelectedProfile.IndexId);
if (item is null) if (item is null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return; return;
} }
@@ -723,18 +730,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 +750,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 +758,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);
} }
@@ -762,10 +776,10 @@ public class ProfilesViewModel : MyReactiveObject
private async Task Export2ClientConfigAsync(bool blClipboard) private async Task Export2ClientConfigAsync(bool blClipboard)
{ {
var item = await AppHandler.Instance.GetProfileItem(SelectedProfile.IndexId); var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId);
if (item is null) if (item is null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectServer); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return; return;
} }
if (blClipboard) if (blClipboard)
@@ -773,12 +787,12 @@ public class ProfilesViewModel : MyReactiveObject
var result = await CoreConfigHandler.GenerateClientConfig(item, null); var result = await CoreConfigHandler.GenerateClientConfig(item, null);
if (result.Success != true) if (result.Success != true)
{ {
NoticeHandler.Instance.Enqueue(result.Msg); NoticeManager.Instance.Enqueue(result.Msg);
} }
else else
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, result.Data); await _updateView?.Invoke(EViewAction.SetClipboardData, result.Data);
NoticeHandler.Instance.SendMessage(ResUI.OperationSuccess); NoticeManager.Instance.SendMessage(ResUI.OperationSuccess);
} }
} }
else else
@@ -796,11 +810,11 @@ public class ProfilesViewModel : MyReactiveObject
var result = await CoreConfigHandler.GenerateClientConfig(item, fileName); var result = await CoreConfigHandler.GenerateClientConfig(item, fileName);
if (result.Success != true) if (result.Success != true)
{ {
NoticeHandler.Instance.Enqueue(result.Msg); NoticeManager.Instance.Enqueue(result.Msg);
} }
else else
{ {
NoticeHandler.Instance.SendMessageAndEnqueue(string.Format(ResUI.SaveClientConfigurationIn, fileName)); NoticeManager.Instance.SendMessageAndEnqueue(string.Format(ResUI.SaveClientConfigurationIn, fileName));
} }
} }
@@ -833,7 +847,7 @@ public class ProfilesViewModel : MyReactiveObject
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString());
} }
NoticeHandler.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
} }
} }
@@ -850,7 +864,7 @@ public class ProfilesViewModel : MyReactiveObject
} }
else else
{ {
item = await AppHandler.Instance.GetSubItem(_config.SubIndexId); item = await AppManager.Instance.GetSubItem(_config.SubIndexId);
if (item is null) if (item is null)
{ {
return; return;

View File

@@ -28,7 +28,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
public RoutingRuleDetailsViewModel(RulesItem rulesItem, Func<EViewAction, object?, Task<bool>>? updateView) public RoutingRuleDetailsViewModel(RulesItem rulesItem, Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -83,7 +83,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
if (!hasRule) if (!hasRule)
{ {
NoticeHandler.Instance.Enqueue(string.Format(ResUI.RoutingRuleDetailRequiredTips, "Network/Port/Protocol/Domain/IP/Process")); NoticeManager.Instance.Enqueue(string.Format(ResUI.RoutingRuleDetailRequiredTips, "Network/Port/Protocol/Domain/IP/Process"));
return; return;
} }
//NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); //NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess);

View File

@@ -13,9 +13,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
[Reactive] [Reactive]
public RoutingItem SelectedRouting { get; set; } public RoutingItem SelectedRouting { get; set; }
public IObservableCollection<RulesItemModel> RulesItems { get; } = new ObservableCollectionExtended<RulesItemModel>();
private IObservableCollection<RulesItemModel> _rulesItems = new ObservableCollectionExtended<RulesItemModel>();
public IObservableCollection<RulesItemModel> RulesItems => _rulesItems;
[Reactive] [Reactive]
public RulesItemModel SelectedSource { get; set; } public RulesItemModel SelectedSource { get; set; }
@@ -37,7 +35,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
public RoutingRuleSettingViewModel(RoutingItem routingItem, Func<EViewAction, object?, Task<bool>>? updateView) public RoutingRuleSettingViewModel(RoutingItem routingItem, Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
@@ -101,7 +99,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 +116,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
Enabled = item.Enabled, Enabled = item.Enabled,
Remarks = item.Remarks, Remarks = item.Remarks,
}; };
_rulesItems.Add(it); RulesItems.Add(it);
} }
} }
@@ -151,7 +149,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
{ {
if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false)
@@ -174,7 +172,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
{ {
if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }
@@ -205,7 +203,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
{ {
if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }
@@ -226,7 +224,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
string remarks = SelectedRouting.Remarks; string remarks = SelectedRouting.Remarks;
if (remarks.IsNullOrEmpty()) if (remarks.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseFillRemarks); NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks);
return; return;
} }
var item = SelectedRouting; var item = SelectedRouting;
@@ -239,12 +237,12 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
if (await ConfigHandler.SaveRoutingItem(_config, item) == 0) if (await ConfigHandler.SaveRoutingItem(_config, item) == 0)
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); _updateView?.Invoke(EViewAction.CloseWindow, null);
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
@@ -266,7 +264,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
if (ret == 0) if (ret == 0)
{ {
RefreshRulesItems(); RefreshRulesItems();
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
} }
@@ -281,7 +279,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
if (ret == 0) if (ret == 0)
{ {
RefreshRulesItems(); RefreshRulesItems();
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
} }
@@ -290,7 +288,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
var url = SelectedRouting.Url; var url = SelectedRouting.Url;
if (url.IsNullOrEmpty()) if (url.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.MsgNeedUrl); NoticeManager.Instance.Enqueue(ResUI.MsgNeedUrl);
return; return;
} }
@@ -300,7 +298,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
if (ret == 0) if (ret == 0)
{ {
RefreshRulesItems(); RefreshRulesItems();
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
} }

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; }
@@ -35,7 +34,7 @@ public class RoutingSettingViewModel : MyReactiveObject
public RoutingSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public RoutingSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
@@ -82,9 +81,9 @@ public class RoutingSettingViewModel : MyReactiveObject
public async Task RefreshRoutingItems() public async Task RefreshRoutingItems()
{ {
_routingItems.Clear(); RoutingItems.Clear();
var routings = await AppHandler.Instance.RoutingItems(); var routings = await AppManager.Instance.RoutingItems();
foreach (var item in routings) foreach (var item in routings)
{ {
var it = new RoutingItemModel() var it = new RoutingItemModel()
@@ -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);
} }
} }
@@ -109,12 +108,12 @@ public class RoutingSettingViewModel : MyReactiveObject
if (await ConfigHandler.SaveConfig(_config) == 0) if (await ConfigHandler.SaveConfig(_config) == 0)
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); _updateView?.Invoke(EViewAction.CloseWindow, null);
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
@@ -129,7 +128,7 @@ public class RoutingSettingViewModel : MyReactiveObject
} }
else else
{ {
item = await AppHandler.Instance.GetRoutingItem(SelectedSource?.Id); item = await AppManager.Instance.GetRoutingItem(SelectedSource?.Id);
if (item is null) if (item is null)
{ {
return; return;
@@ -146,7 +145,7 @@ public class RoutingSettingViewModel : MyReactiveObject
{ {
if (SelectedSource is null || SelectedSource.Remarks.IsNullOrEmpty()) if (SelectedSource is null || SelectedSource.Remarks.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false)
@@ -155,7 +154,7 @@ public class RoutingSettingViewModel : MyReactiveObject
} }
foreach (var it in SelectedSources ?? [SelectedSource]) foreach (var it in SelectedSources ?? [SelectedSource])
{ {
var item = await AppHandler.Instance.GetRoutingItem(it?.Id); var item = await AppManager.Instance.GetRoutingItem(it?.Id);
if (item != null) if (item != null)
{ {
await ConfigHandler.RemoveRoutingItem(item); await ConfigHandler.RemoveRoutingItem(item);
@@ -168,10 +167,10 @@ public class RoutingSettingViewModel : MyReactiveObject
public async Task RoutingAdvancedSetDefault() public async Task RoutingAdvancedSetDefault()
{ {
var item = await AppHandler.Instance.GetRoutingItem(SelectedSource?.Id); var item = await AppManager.Instance.GetRoutingItem(SelectedSource?.Id);
if (item is null) if (item is null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }

View File

@@ -1,4 +1,6 @@
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;
@@ -11,11 +13,9 @@ public class StatusBarViewModel : MyReactiveObject
{ {
#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; }
@@ -100,7 +100,7 @@ public class StatusBarViewModel : MyReactiveObject
public StatusBarViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public StatusBarViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
SelectedRouting = new(); SelectedRouting = new();
SelectedServer = new(); SelectedServer = new();
RunningServerToolTipText = "-"; RunningServerToolTipText = "-";
@@ -197,10 +197,20 @@ 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));
#endregion AppEvents
_ = Init(); _ = Init();
} }
@@ -216,19 +226,17 @@ 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";
var address = $"{Global.Loopback}:{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks)}"; var address = $"{Global.Loopback}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}";
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine($"{cmd} http_proxy={Global.HttpProtocol}{address}"); sb.AppendLine($"{cmd} http_proxy={Global.HttpProtocol}{address}");
@@ -263,7 +271,7 @@ public class StatusBarViewModel : MyReactiveObject
await service.UpdateSubscriptionProcess("", blProxy); await service.UpdateSubscriptionProcess("", blProxy);
} }
public async Task RefreshServersBiz() private async Task RefreshServersBiz()
{ {
await RefreshServersMenu(); await RefreshServersMenu();
@@ -283,9 +291,9 @@ public class StatusBarViewModel : MyReactiveObject
private async Task RefreshServersMenu() private async Task RefreshServersMenu()
{ {
var lstModel = await AppHandler.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 +307,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;
@@ -332,15 +340,24 @@ public class StatusBarViewModel : MyReactiveObject
return; return;
} }
_updateView?.Invoke(EViewAction.DispatcherServerAvailability, ResUI.Speedtesting); await TestServerAvailabilitySub(ResUI.Speedtesting);
var msg = await Task.Run(ConnectionHandler.Instance.RunAvailabilityCheck); var msg = await Task.Run(ConnectionHandler.RunAvailabilityCheck);
NoticeHandler.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;
} }
@@ -355,7 +372,7 @@ public class StatusBarViewModel : MyReactiveObject
} }
_config.SystemProxyItem.SysProxyType = type; _config.SystemProxyItem.SysProxyType = type;
await ChangeSystemProxyAsync(type, true); await ChangeSystemProxyAsync(type, true);
NoticeHandler.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType.ToString()}"); NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType.ToString()}");
SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType; SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType;
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
@@ -378,13 +395,13 @@ public class StatusBarViewModel : MyReactiveObject
public async Task RefreshRoutingsMenu() public async Task RefreshRoutingsMenu()
{ {
_routingItems.Clear(); RoutingItems.Clear();
BlRouting = true; BlRouting = true;
var routings = await AppHandler.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;
@@ -404,7 +421,7 @@ public class StatusBarViewModel : MyReactiveObject
return; return;
} }
var item = await AppHandler.Instance.GetRoutingItem(SelectedRouting?.Id); var item = await AppManager.Instance.GetRoutingItem(SelectedRouting?.Id);
if (item is null) if (item is null)
{ {
return; return;
@@ -412,7 +429,7 @@ public class StatusBarViewModel : MyReactiveObject
if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) if (await ConfigHandler.SetDefaultRouting(_config, item) == 0)
{ {
NoticeHandler.Instance.SendMessageEx(ResUI.TipChangeRouting); NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
Locator.Current.GetService<MainWindowViewModel>()?.Reload(); Locator.Current.GetService<MainWindowViewModel>()?.Reload();
_updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); _updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null);
} }
@@ -471,11 +488,11 @@ public class StatusBarViewModel : MyReactiveObject
} }
else if (Utils.IsLinux()) else if (Utils.IsLinux())
{ {
return AppHandler.Instance.LinuxSudoPwd.IsNotEmpty(); return AppManager.Instance.LinuxSudoPwd.IsNotEmpty();
} }
else if (Utils.IsOSX()) else if (Utils.IsOSX())
{ {
return AppHandler.Instance.LinuxSudoPwd.IsNotEmpty(); return AppManager.Instance.LinuxSudoPwd.IsNotEmpty();
} }
return false; return false;
} }
@@ -487,10 +504,10 @@ public class StatusBarViewModel : MyReactiveObject
public async Task InboundDisplayStatus() public async Task InboundDisplayStatus()
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.Append($"[{EInboundProtocol.mixed}:{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks)}"); sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}");
if (_config.Inbound.First().SecondLocalPortEnabled) if (_config.Inbound.First().SecondLocalPortEnabled)
{ {
sb.Append($",{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks2)}"); sb.Append($",{AppManager.Instance.GetLocalPort(EInboundProtocol.socks2)}");
} }
sb.Append(']'); sb.Append(']');
InboundDisplay = $"{ResUI.LabLocal}:{sb}"; InboundDisplay = $"{ResUI.LabLocal}:{sb}";
@@ -498,8 +515,8 @@ public class StatusBarViewModel : MyReactiveObject
if (_config.Inbound.First().AllowLANConn) if (_config.Inbound.First().AllowLANConn)
{ {
var lan = _config.Inbound.First().NewPort4LAN var lan = _config.Inbound.First().NewPort4LAN
? $"[{EInboundProtocol.mixed}:{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks3)}]" ? $"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks3)}]"
: $"[{EInboundProtocol.mixed}:{AppHandler.Instance.GetLocalPort(EInboundProtocol.socks)}]"; : $"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}]";
InboundLanDisplay = $"{ResUI.LabLAN}:{lan}"; InboundLanDisplay = $"{ResUI.LabLAN}:{lan}";
} }
else else
@@ -509,8 +526,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

@@ -13,7 +13,7 @@ public class SubEditViewModel : MyReactiveObject
public SubEditViewModel(SubItem subItem, Func<EViewAction, object?, Task<bool>>? updateView) public SubEditViewModel(SubItem subItem, Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -29,7 +29,7 @@ public class SubEditViewModel : MyReactiveObject
var remarks = SelectedSource.Remarks; var remarks = SelectedSource.Remarks;
if (remarks.IsNullOrEmpty()) if (remarks.IsNullOrEmpty())
{ {
NoticeHandler.Instance.Enqueue(ResUI.PleaseFillRemarks); NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks);
return; return;
} }
@@ -39,25 +39,25 @@ public class SubEditViewModel : MyReactiveObject
var uri = Utils.TryUri(url); var uri = Utils.TryUri(url);
if (uri == null) if (uri == null)
{ {
NoticeHandler.Instance.Enqueue(ResUI.InvalidUrlTip); NoticeManager.Instance.Enqueue(ResUI.InvalidUrlTip);
return; return;
} }
//Do not allow http protocol //Do not allow http protocol
if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost)) if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost))
{ {
NoticeHandler.Instance.Enqueue(ResUI.InsecureUrlProtocol); NoticeManager.Instance.Enqueue(ResUI.InsecureUrlProtocol);
//return; //return;
} }
} }
if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0) if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0)
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); _updateView?.Invoke(EViewAction.CloseWindow, null);
} }
else else
{ {
NoticeHandler.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
} }

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; }
@@ -24,7 +23,7 @@ public class SubSettingViewModel : MyReactiveObject
public SubSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public SubSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
@@ -60,8 +59,8 @@ public class SubSettingViewModel : MyReactiveObject
public async Task RefreshSubItems() public async Task RefreshSubItems()
{ {
_subItems.Clear(); SubItems.Clear();
_subItems.AddRange(await AppHandler.Instance.SubItems()); SubItems.AddRange(await AppManager.Instance.SubItems());
} }
public async Task EditSubAsync(bool blNew) public async Task EditSubAsync(bool blNew)
@@ -73,7 +72,7 @@ public class SubSettingViewModel : MyReactiveObject
} }
else else
{ {
item = await AppHandler.Instance.GetSubItem(SelectedSource?.Id); item = await AppManager.Instance.GetSubItem(SelectedSource?.Id);
if (item is null) if (item is null)
{ {
return; return;
@@ -98,7 +97,7 @@ public class SubSettingViewModel : MyReactiveObject
await ConfigHandler.DeleteSubItem(_config, it.Id); await ConfigHandler.DeleteSubItem(_config, it.Id);
} }
await RefreshSubItems(); await RefreshSubItems();
NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
IsModified = true; IsModified = true;
} }
} }

View File

@@ -1,6 +1,7 @@
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 Splat;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
using v2rayN.Desktop.Views; using v2rayN.Desktop.Views;
@@ -25,7 +26,7 @@ public partial class App : Application
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
AppHandler.Instance.InitComponents(); AppManager.Instance.InitComponents();
desktop.Exit += OnExit; desktop.Exit += OnExit;
desktop.MainWindow = new MainWindow(); desktop.MainWindow = new MainWindow();
@@ -73,11 +74,7 @@ public partial class App : Application
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,6 +1,7 @@
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;
@@ -20,7 +21,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
{ {
try try
{ {
var sizeItem = ConfigHandler.GetWindowSizeItem(AppHandler.Instance.Config, GetType().Name); var sizeItem = ConfigHandler.GetWindowSizeItem(AppManager.Instance.Config, GetType().Name);
if (sizeItem == null) if (sizeItem == null)
{ {
return; return;
@@ -45,7 +46,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
base.OnClosed(e); base.OnClosed(e);
try try
{ {
ConfigHandler.SaveWindowSizeItem(AppHandler.Instance.Config, GetType().Name, Width, Height); ConfigHandler.SaveWindowSizeItem(AppManager.Instance.Config, GetType().Name, Width, Height);
} }
catch { } catch { }
} }

View File

@@ -4,12 +4,12 @@ using Avalonia.ReactiveUI;
using Avalonia.Win32.Input; using Avalonia.Win32.Input;
using GlobalHotKeys; using GlobalHotKeys;
namespace v2rayN.Desktop.Handler; namespace v2rayN.Desktop.Manager;
public sealed class HotkeyHandler public sealed class HotkeyManager
{ {
private static readonly Lazy<HotkeyHandler> _instance = new(() => new()); private static readonly Lazy<HotkeyManager> _instance = new(() => new());
public static HotkeyHandler Instance = _instance.Value; public static HotkeyManager Instance = _instance.Value;
private readonly Dictionary<int, EGlobalHotkey> _hotkeyTriggerDic = new(); private readonly Dictionary<int, EGlobalHotkey> _hotkeyTriggerDic = new();
private HotKeyManager? _hotKeyManager; private HotKeyManager? _hotKeyManager;

View File

@@ -1,5 +1,6 @@
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;
@@ -46,7 +47,7 @@ internal class Program
} }
} }
if (!AppHandler.Instance.InitApp()) if (!AppManager.Instance.InitApp())
{ {
return false; return false;
} }
@@ -62,6 +63,6 @@ internal class Program
.WithFontByDefault() .WithFontByDefault()
.LogToTrace() .LogToTrace()
.UseReactiveUI() .UseReactiveUI()
.With(new MacOSPlatformOptions { ShowInDock = AppHandler.Instance.Config.UiItem.MacOSShowInDock }); .With(new MacOSPlatformOptions { ShowInDock = AppManager.Instance.Config.UiItem.MacOSShowInDock });
} }
} }

View File

@@ -8,6 +8,7 @@ using Avalonia.Styling;
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;
@@ -21,7 +22,7 @@ public class ThemeSettingViewModel : MyReactiveObject
public ThemeSettingViewModel() public ThemeSettingViewModel()
{ {
_config = AppHandler.Instance.Config; _config = AppManager.Instance.Config;
BindingUI(); BindingUI();
RestoreUI(); RestoreUI();
@@ -74,7 +75,7 @@ public class ThemeSettingViewModel : MyReactiveObject
_config.UiItem.CurrentLanguage = CurrentLanguage; _config.UiItem.CurrentLanguage = CurrentLanguage;
Thread.CurrentThread.CurrentUICulture = new(CurrentLanguage); Thread.CurrentThread.CurrentUICulture = new(CurrentLanguage);
ConfigHandler.SaveConfig(_config); ConfigHandler.SaveConfig(_config);
NoticeHandler.Instance.Enqueue(ResUI.NeedRebootTips); NoticeManager.Instance.Enqueue(ResUI.NeedRebootTips);
} }
}); });
} }

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">
@@ -523,7 +521,7 @@
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Mtu" /> Text="{x:Static resx:ResUI.TbSettingsTunMtu}" />
<TextBox <TextBox
x:Name="txtShortId9" x:Name="txtShortId9"
Grid.Row="5" Grid.Row="5"

View File

@@ -2,6 +2,7 @@ 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;
@@ -50,7 +51,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
case EConfigType.Shadowsocks: case EConfigType.Shadowsocks:
gridSs.IsVisible = true; gridSs.IsVisible = true;
cmbSecurity3.ItemsSource = AppHandler.Instance.GetShadowsocksSecurities(profileItem); cmbSecurity3.ItemsSource = AppManager.Instance.GetShadowsocksSecurities(profileItem);
break; break;
case EConfigType.SOCKS: case EConfigType.SOCKS:

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);
} }
} }

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