Compare commits

..

26 commits

Author SHA1 Message Date
DHR60
5d8ff38be6 PreCheck 2025-09-12 12:33:39 +08:00
DHR60
2a7095aa1e Avoid duplicate tags 2025-09-12 11:35:47 +08:00
DHR60
531bdc522d Add group in traffic splitting support 2025-09-11 17:18:53 +08:00
DHR60
6ff6f58129 Add PolicyGroup include other Group support 2025-09-11 16:24:20 +08:00
DHR60
f8fb492bc5 Add fallback support 2025-09-11 15:51:45 +08:00
DHR60
73c2a203de Fix 2025-09-11 14:50:58 +08:00
DHR60
00ed1edf92 Add Proxy Chain support 2025-09-11 14:29:14 +08:00
DHR60
76f39ccac4 Adjust UI 2025-09-11 13:49:10 +08:00
DHR60
5b073927d2 Add generate policy group 2025-09-11 13:23:38 +08:00
DHR60
5452341b60 Add Policy Group support 2025-09-11 13:00:12 +08:00
DHR60
d8a658037d Rename 2025-09-11 12:59:13 +08:00
DHR60
91eb1846fb Exclude specific profile types from selection 2025-09-11 12:24:05 +08:00
DHR60
e1bb6abfdf Fix right click not working 2025-09-11 12:14:18 +08:00
DHR60
1b9e5eca3e avalonia 2025-09-11 12:10:59 +08:00
DHR60
d0c177643c VM and wpf 2025-09-11 11:58:03 +08:00
DHR60
352490c165 Multi Profile 2025-09-10 23:44:32 +08:00
DHR60
2d29a4596a Add global fakeip and fakeip filter 2025-09-10 20:54:12 +08:00
2dust
29a5abf4d6 Optimization and improvement
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-10 19:43:11 +08:00
2dust
b54c67d6f1 up 7.14.9
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-09 20:18:55 +08:00
2dust
b49486cc23 Update ProfilesSelectWindow.axaml 2025-09-09 20:00:00 +08:00
JieXu
b95830b3d5
Update package-rhel.sh package-debian.sh MainWindowViewModel.cs (#7910)
* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update MainWindowViewModel.cs

* Update package-rhel.sh

* Update package-debian.sh
2025-09-09 19:51:10 +08:00
2dust
8e0c5cb9aa Bug fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/7914
2025-09-09 17:55:15 +08:00
2dust
6ffb3bd30c up 7.14.8
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-08 18:48:56 +08:00
2dust
2826444ffc Code clean 2025-09-08 18:45:21 +08:00
JieXu
56c3e9c46d
Fix package-appimage.sh bugs. (#7904)
* Update package-appimage.sh

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

示例hosts
```
127.0.0.1 test1.com
127.0.0.1 test2.com # test
```
在之前仅仅会添加`127.0.0.1 test1.com`这条记录,而忽略另一条
2025-09-08 18:02:44 +08:00
68 changed files with 2881 additions and 594 deletions

View file

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

View file

@ -28,7 +28,7 @@ Package: v2rayN
Version: $Version Version: $Version
Architecture: $Arch2 Architecture: $Arch2
Maintainer: https://github.com/2dust/v2rayN Maintainer: https://github.com/2dust/v2rayN
Depends: desktop-file-utils Depends: desktop-file-utils, xdg-utils
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
EOF EOF

View file

@ -1,11 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ===== Require Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ==== # == Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ==
if [[ -r /etc/os-release ]]; then if [[ -r /etc/os-release ]]; then
. /etc/os-release . /etc/os-release
case "$ID" in case "$ID" in
rhel|rocky|almalinux|centos|ubuntu|debian) rhel|rocky|almalinux|fedora|centos|ubuntu|debian)
echo "[OK] Detected supported system: $NAME $VERSION_ID" echo "[OK] Detected supported system: $NAME $VERSION_ID"
;; ;;
*) *)
@ -390,25 +390,30 @@ download_mihomo() {
chmod +x "$outroot/bin/mihomo/mihomo" || true chmod +x "$outroot/bin/mihomo/mihomo" || true
} }
# Move geo files to a unified path: outroot/bin/xray/ # Move geo files to a unified path: outroot/bin
unify_geo_layout() { unify_geo_layout() {
local outroot="$1" local outroot="$1"
mkdir -p "$outroot/bin/xray" mkdir -p "$outroot/bin"
local srcs=( \ local names=( \
"$outroot/bin/geosite.dat" \ "geosite.dat" \
"$outroot/bin/geoip.dat" \ "geoip.dat" \
"$outroot/bin/geoip-only-cn-private.dat" \ "geoip-only-cn-private.dat" \
"$outroot/bin/Country.mmdb" \ "Country.mmdb" \
"$outroot/bin/geoip.metadb" \ "geoip.metadb" \
) )
for s in "${srcs[@]}"; do for n in "${names[@]}"; do
if [[ -f "$s" ]]; then # If file exists under bin/xray/, move it up to bin/
mv -f "$s" "$outroot/bin/xray/$(basename "$s")" if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
# If file already in bin/, leave it as-is
if [[ -f "$outroot/bin/$n" ]]; then
:
fi fi
done done
} }
# Download geo/rule assets; then unify to bin/xray/ # Download geo/rule assets; then unify to bin/
download_geo_assets() { download_geo_assets() {
local outroot="$1" local outroot="$1"
local bin_dir="$outroot/bin" local bin_dir="$outroot/bin"
@ -442,7 +447,7 @@ download_geo_assets() {
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true "https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done done
# Unify to bin/xray/ # Unify to bin/
unify_geo_layout "$outroot" unify_geo_layout "$outroot"
} }
@ -480,7 +485,7 @@ download_v2rayn_bundle() {
rm -rf "$nested_dir" rm -rf "$nested_dir"
fi fi
# Unify to bin/xray/ # Unify to bin/
unify_geo_layout "$outroot" unify_geo_layout "$outroot"
echo "[+] Bundle extracted to $outroot" echo "[+] Bundle extracted to $outroot"
@ -610,7 +615,7 @@ Source0: __PKGROOT__.tar.gz
# Runtime dependencies (Avalonia / X11 / Fonts / GL) # Runtime dependencies (Avalonia / X11 / Fonts / GL)
Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon
Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL, xdg-utils
%description %description
v2rayN Linux for Red Hat Enterprise Linux v2rayN Linux for Red Hat Enterprise Linux
@ -629,25 +634,13 @@ https://github.com/2dust/v2rayN
install -dm0755 %{buildroot}/opt/v2rayN install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/ cp -a * %{buildroot}/opt/v2rayN/
# Launcher (prefer native ELF first, then DLL fallback; also create Geo symlinks for the user) # Launcher (prefer native ELF first, then DLL fallback)
install -dm0755 %{buildroot}%{_bindir} install -dm0755 %{buildroot}%{_bindir}
cat > %{buildroot}%{_bindir}/v2rayn << 'EOF' cat > %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash #!/usr/bin/bash
set -euo pipefail set -euo pipefail
DIR="/opt/v2rayN" DIR="/opt/v2rayN"
# --- Symlink GEO files into user's XDG dir (first-run convenience) ---
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
USR_GEO_DIR="$XDG_DATA_HOME/v2rayN/bin"
SYS_XRAY_DIR="$DIR/bin/xray"
mkdir -p "$USR_GEO_DIR"
for f in geosite.dat geoip.dat geoip-only-cn-private.dat Country.mmdb; do
if [[ -f "$SYS_XRAY_DIR/$f" && ! -e "$USR_GEO_DIR/$f" ]]; then
ln -s "$SYS_XRAY_DIR/$f" "$USR_GEO_DIR/$f" || true
fi
done
# --- end GEO ---
# Prefer native apphost # Prefer native apphost
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi

View file

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

View file

@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.14.7</Version> <Version>7.14.9</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View file

@ -582,9 +582,9 @@ public class Utils
if (host.StartsWith("#")) if (host.StartsWith("#"))
continue; continue;
var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (hostItem.Length != 2) if (hostItem.Length < 2)
continue; continue;
systemHosts.Add(hostItem.Last(), hostItem.First()); systemHosts.Add(hostItem[1], hostItem[0]);
} }
} }
} }

View file

@ -12,5 +12,9 @@ public enum EConfigType
TUIC = 8, TUIC = 8,
WireGuard = 9, WireGuard = 9,
HTTP = 10, HTTP = 10,
Anytls = 11 Anytls = 11,
Group = 1000,
PolicyGroup = 1001,
ProxyChain = 1002,
} }

View file

@ -2,8 +2,9 @@ namespace ServiceLib.Enums;
public enum EMultipleLoad public enum EMultipleLoad
{ {
LeastPing,
Fallback,
Random, Random,
RoundRobin, RoundRobin,
LeastPing,
LeastLoad LeastLoad
} }

View file

@ -24,6 +24,7 @@ public enum EViewAction
RoutingRuleDetailsWindow, RoutingRuleDetailsWindow,
AddServerWindow, AddServerWindow,
AddServer2Window, AddServer2Window,
AddGroupServerWindow,
DNSSettingWindow, DNSSettingWindow,
RoutingSettingWindow, RoutingSettingWindow,
OptionSettingWindow, OptionSettingWindow,

View file

@ -40,6 +40,7 @@ public class Global
public const string ProxySetLinuxShellFileName = NamespaceSample + "proxy_set_linux_sh"; public const string ProxySetLinuxShellFileName = NamespaceSample + "proxy_set_linux_sh";
public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh"; public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh";
public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh"; public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
public const string DefaultSecurity = "auto"; public const string DefaultSecurity = "auto";
public const string DefaultNetwork = "tcp"; public const string DefaultNetwork = "tcp";
@ -49,6 +50,7 @@ public class Global
public const string DirectTag = "direct"; public const string DirectTag = "direct";
public const string BlockTag = "block"; public const string BlockTag = "block";
public const string DnsTag = "dns-module"; public const string DnsTag = "dns-module";
public const string BalancerTagSuffix = "-round";
public const string StreamSecurity = "tls"; public const string StreamSecurity = "tls";
public const string StreamSecurityReality = "reality"; public const string StreamSecurityReality = "reality";
public const string Loopback = "127.0.0.1"; public const string Loopback = "127.0.0.1";

View file

@ -113,6 +113,10 @@ public static class ConfigHandler
config.ConstItem ??= new ConstItem(); config.ConstItem ??= new ConstItem();
config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
if (config.SimpleDNSItem.GlobalFakeIp is null)
{
config.SimpleDNSItem.GlobalFakeIp = true;
}
config.SpeedTestItem ??= new(); config.SpeedTestItem ??= new();
if (config.SpeedTestItem.SpeedTestTimeout < 10) if (config.SpeedTestItem.SpeedTestTimeout < 10)
@ -353,6 +357,11 @@ public static class ConfigHandler
{ {
} }
} }
else if (profileItem.ConfigType > EConfigType.Group)
{
var profileGroupItem = await AppManager.Instance.GetProfileGroupItem(it.IndexId);
await AddGroupServerCommon(config, profileItem, profileGroupItem, true);
}
else else
{ {
await AddServerCommon(config, profileItem, true); await AddServerCommon(config, profileItem, true);
@ -1070,6 +1079,35 @@ public static class ConfigHandler
return 0; return 0;
} }
public static async Task<int> AddGroupServerCommon(Config config, ProfileItem profileItem, ProfileGroupItem profileGroupItem, bool toFile = true)
{
var maxSort = -1;
if (profileItem.IndexId.IsNullOrEmpty())
{
profileItem.IndexId = Utils.GetGuid(false);
maxSort = ProfileExManager.Instance.GetMaxSort();
}
if (maxSort > 0)
{
ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1);
}
if (toFile)
{
await SQLiteHelper.Instance.ReplaceAsync(profileItem);
if (profileGroupItem != null)
{
profileGroupItem.ParentIndexId = profileItem.IndexId;
await ProfileGroupItemManager.Instance.SaveItemAsync(profileGroupItem);
}
else
{
ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(profileItem.IndexId);
await ProfileGroupItemManager.Instance.SaveTo();
}
}
return 0;
}
/// <summary> /// <summary>
/// Compare two profile items to determine if they represent the same server /// Compare two profile items to determine if they represent the same server
/// Used for deduplication and server matching /// Used for deduplication and server matching
@ -1141,7 +1179,7 @@ public static class ConfigHandler
} }
/// <summary> /// <summary>
/// Create a custom server that combines multiple servers for load balancing /// Create a group server that combines multiple servers for load balancing
/// Generates a configuration file that references multiple servers /// Generates a configuration file that references multiple servers
/// </summary> /// </summary>
/// <param name="config">Current configuration</param> /// <param name="config">Current configuration</param>
@ -1149,45 +1187,55 @@ public static class ConfigHandler
/// <param name="coreType">Core type to use (Xray or sing_box)</param> /// <param name="coreType">Core type to use (Xray or sing_box)</param>
/// <param name="multipleLoad">Load balancing algorithm</param> /// <param name="multipleLoad">Load balancing algorithm</param>
/// <returns>Result object with success state and data</returns> /// <returns>Result object with success state and data</returns>
public static async Task<RetResult> AddCustomServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad) public static async Task<RetResult> AddGroupServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad, string? subId)
{ {
var indexId = Utils.GetMd5(Global.CoreMultipleLoadConfigFileName); var result = new RetResult();
var configPath = Utils.GetConfigPath(Global.CoreMultipleLoadConfigFileName);
var result = await CoreConfigHandler.GenerateClientMultipleLoadConfig(config, configPath, selecteds, coreType, multipleLoad); var indexId = Utils.GetGuid(false);
if (result.Success != true) var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList());
{
return result;
}
if (!File.Exists(configPath)) var remark = string.Empty;
{
return result;
}
var profileItem = await AppManager.Instance.GetProfileItem(indexId) ?? new();
profileItem.IndexId = indexId;
if (coreType == ECoreType.Xray) if (coreType == ECoreType.Xray)
{ {
profileItem.Remarks = multipleLoad switch remark = multipleLoad switch
{ {
EMultipleLoad.Random => ResUI.menuSetDefaultMultipleServerXrayRandom, EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing,
EMultipleLoad.RoundRobin => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback,
EMultipleLoad.LeastPing => ResUI.menuSetDefaultMultipleServerXrayLeastPing, EMultipleLoad.Random => ResUI.menuGenGroupMultipleServerXrayRandom,
EMultipleLoad.LeastLoad => ResUI.menuSetDefaultMultipleServerXrayLeastLoad, EMultipleLoad.RoundRobin => ResUI.menuGenGroupMultipleServerXrayRoundRobin,
_ => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, EMultipleLoad.LeastLoad => ResUI.menuGenGroupMultipleServerXrayLeastLoad,
_ => ResUI.menuGenGroupMultipleServerXrayRoundRobin,
}; };
} }
else if (coreType == ECoreType.sing_box) else if (coreType == ECoreType.sing_box)
{ {
profileItem.Remarks = ResUI.menuSetDefaultMultipleServerSingBoxLeastPing; remark = multipleLoad switch
{
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback,
_ => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
};
} }
profileItem.Address = Global.CoreMultipleLoadConfigFileName; var profile = new ProfileItem
profileItem.ConfigType = EConfigType.Custom; {
profileItem.CoreType = coreType; IndexId = indexId,
CoreType = coreType,
await AddServerCommon(config, profileItem, true); ConfigType = EConfigType.PolicyGroup,
Remarks = remark,
Address = childProfileIndexId,
};
if (!subId.IsNullOrEmpty())
{
profile.Subid = subId;
}
var profileGroup = new ProfileGroupItem
{
ChildItems = childProfileIndexId,
MultipleLoad = multipleLoad,
ParentIndexId = indexId,
};
var ret = await AddGroupServerCommon(config, profile, profileGroup, true);
result.Success = ret == 0;
result.Data = indexId; result.Data = indexId;
return result; return result;
} }
@ -1203,7 +1251,7 @@ public static class ConfigHandler
public static async Task<ProfileItem?> GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType) public static async Task<ProfileItem?> GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
{ {
ProfileItem? itemSocks = null; ProfileItem? itemSocks = null;
if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun) if (node.ConfigType != EConfigType.Custom && node.ConfigType < EConfigType.Group && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun)
{ {
itemSocks = new ProfileItem() itemSocks = new ProfileItem()
{ {
@ -1214,7 +1262,7 @@ public static class ConfigHandler
Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks) Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks)
}; };
} }
else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)) else if ((node.ConfigType == EConfigType.Custom && node.ConfigType < EConfigType.Group && node.PreSocksPort > 0))
{ {
var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
itemSocks = new ProfileItem() itemSocks = new ProfileItem()
@ -2221,6 +2269,7 @@ public static class ConfigHandler
UseSystemHosts = false, UseSystemHosts = false,
AddCommonHosts = true, AddCommonHosts = true,
FakeIP = false, FakeIP = false,
GlobalFakeIp = true,
BlockBindingQuery = true, BlockBindingQuery = true,
DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(),
RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(), RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(),

View file

@ -132,24 +132,4 @@ public static class CoreConfigHandler
await File.WriteAllTextAsync(fileName, result.Data.ToString()); await File.WriteAllTextAsync(fileName, result.Data.ToString());
return result; return result;
} }
public static async Task<RetResult> GenerateClientMultipleLoadConfig(Config config, string fileName, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad)
{
var result = new RetResult();
if (coreType == ECoreType.sing_box)
{
result = await new CoreConfigSingboxService(config).GenerateClientMultipleLoadConfig(selecteds);
}
else
{
result = await new CoreConfigV2rayService(config).GenerateClientMultipleLoadConfig(selecteds, multipleLoad);
}
if (result.Success != true)
{
return result;
}
await File.WriteAllTextAsync(fileName, result.Data.ToString());
return result;
}
} }

View file

@ -67,6 +67,7 @@ public sealed class AppManager
SQLiteHelper.Instance.CreateTable<ProfileExItem>(); SQLiteHelper.Instance.CreateTable<ProfileExItem>();
SQLiteHelper.Instance.CreateTable<DNSItem>(); SQLiteHelper.Instance.CreateTable<DNSItem>();
SQLiteHelper.Instance.CreateTable<FullConfigTemplateItem>(); SQLiteHelper.Instance.CreateTable<FullConfigTemplateItem>();
SQLiteHelper.Instance.CreateTable<ProfileGroupItem>();
return true; return true;
} }
@ -101,6 +102,7 @@ public sealed class AppManager
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo(); await ProfileExManager.Instance.SaveTo();
await ProfileGroupItemManager.Instance.SaveTo();
await StatisticsManager.Instance.SaveTo(); await StatisticsManager.Instance.SaveTo();
await CoreManager.Instance.CoreStop(); await CoreManager.Instance.CoreStop();
StatisticsManager.Instance.Close(); StatisticsManager.Instance.Close();
@ -219,6 +221,15 @@ public sealed class AppManager
return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.Remarks == remarks); return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.Remarks == remarks);
} }
public async Task<ProfileGroupItem?> GetProfileGroupItem(string parentIndexId)
{
if (parentIndexId.IsNullOrEmpty())
{
return null;
}
return await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().FirstOrDefaultAsync(it => it.ParentIndexId == parentIndexId);
}
public async Task<List<RoutingItem>?> RoutingItems() public async Task<List<RoutingItem>?> RoutingItems()
{ {
return await SQLiteHelper.Instance.TableAsync<RoutingItem>().OrderBy(t => t.Sort).ToListAsync(); return await SQLiteHelper.Instance.TableAsync<RoutingItem>().OrderBy(t => t.Sort).ToListAsync();

View file

@ -0,0 +1,167 @@
using System.Collections.Concurrent;
namespace ServiceLib.Manager;
public class ProfileGroupItemManager
{
private static readonly Lazy<ProfileGroupItemManager> _instance = new(() => new());
private ConcurrentDictionary<string, ProfileGroupItem> _items = new();
public static ProfileGroupItemManager Instance => _instance.Value;
private static readonly string _tag = "ProfileGroupItemManager";
private ProfileGroupItemManager()
{
}
public async Task Init()
{
await InitData();
}
// Read-only getters: do not create or mark dirty
public bool TryGet(string indexId, out ProfileGroupItem? item)
{
item = null;
if (string.IsNullOrWhiteSpace(indexId))
{
return false;
}
return _items.TryGetValue(indexId, out item);
}
public ProfileGroupItem? GetOrDefault(string indexId)
{
return string.IsNullOrWhiteSpace(indexId) ? null : (_items.TryGetValue(indexId, out var v) ? v : null);
}
private async Task InitData()
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem where parentIndexId not in ( select indexId from ProfileItem )");
var list = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
_items = new ConcurrentDictionary<string, ProfileGroupItem>(list.Where(t => !string.IsNullOrEmpty(t.ParentIndexId)).ToDictionary(t => t.ParentIndexId!));
}
private ProfileGroupItem AddProfileGroupItem(string indexId)
{
var profileGroupItem = new ProfileGroupItem()
{
ParentIndexId = indexId,
ChildItems = string.Empty,
MultipleLoad = EMultipleLoad.LeastPing
};
_items[indexId] = profileGroupItem;
return profileGroupItem;
}
private ProfileGroupItem GetProfileGroupItem(string indexId)
{
if (string.IsNullOrEmpty(indexId))
{
indexId = Utils.GetGuid(false);
}
return _items.GetOrAdd(indexId, AddProfileGroupItem);
}
public async Task ClearAll()
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem ");
_items.Clear();
}
public async Task SaveTo()
{
try
{
var lstExists = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
var existsMap = lstExists.Where(t => !string.IsNullOrEmpty(t.ParentIndexId)).ToDictionary(t => t.ParentIndexId!);
var lstInserts = new List<ProfileGroupItem>();
var lstUpdates = new List<ProfileGroupItem>();
foreach (var item in _items.Values)
{
if (string.IsNullOrEmpty(item.ParentIndexId))
{
continue;
}
if (existsMap.ContainsKey(item.ParentIndexId))
{
lstUpdates.Add(item);
}
else
{
lstInserts.Add(item);
}
}
try
{
if (lstInserts.Count > 0)
{
await SQLiteHelper.Instance.InsertAllAsync(lstInserts);
}
if (lstUpdates.Count > 0)
{
await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
public ProfileGroupItem GetOrCreateAndMarkDirty(string indexId)
{
return GetProfileGroupItem(indexId);
}
public async ValueTask DisposeAsync()
{
await SaveTo();
}
public async Task SaveItemAsync(ProfileGroupItem item)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
if (string.IsNullOrWhiteSpace(item.ParentIndexId))
{
throw new ArgumentException("ParentIndexId required", nameof(item));
}
_items[item.ParentIndexId] = item;
try
{
var lst = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().Where(t => t.ParentIndexId == item.ParentIndexId).ToListAsync();
if (lst != null && lst.Count > 0)
{
await SQLiteHelper.Instance.UpdateAllAsync(new List<ProfileGroupItem> { item });
}
else
{
await SQLiteHelper.Instance.InsertAllAsync(new List<ProfileGroupItem> { item });
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
}

View file

@ -35,6 +35,7 @@ public class TaskManager
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo(); await ProfileExManager.Instance.SaveTo();
await ProfileGroupItemManager.Instance.SaveTo();
} }
//Execute once 1 hour //Execute once 1 hour

View file

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

View file

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

View file

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

View file

@ -0,0 +1,13 @@
using SQLite;
namespace ServiceLib.Models;
[Serializable]
public class ProfileGroupItem
{
[PrimaryKey]
public string ParentIndexId { get; set; }
public string ChildItems { get; set; }
public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing;
}

View file

@ -39,11 +39,14 @@ public class ProfileItem : ReactiveObject
> 1 => $"***{arrAddr.Last()}", > 1 => $"***{arrAddr.Last()}",
_ => Address _ => Address
}; };
summary += ConfigType switch if (ConfigType is EConfigType.Custom or > EConfigType.Group)
{ {
EConfigType.Custom => $"[{CoreType.ToString()}]{Remarks}", summary += $"[{CoreType.ToString()}]{Remarks}";
_ => $"{Remarks}({addr}:{Port})" }
}; else
{
summary += $"{Remarks}({addr}:{Port})";
}
return summary; return summary;
} }

View file

@ -145,6 +145,7 @@ public class Outbound4Sbox : BaseServer4Sbox
public string? plugin_opts { get; set; } public string? plugin_opts { get; set; }
public List<string>? outbounds { get; set; } public List<string>? outbounds { get; set; }
public bool? interrupt_exist_connections { get; set; } public bool? interrupt_exist_connections { get; set; }
public int? tolerance { get; set; }
} }
public class Endpoints4Sbox : BaseServer4Sbox public class Endpoints4Sbox : BaseServer4Sbox

View file

@ -294,6 +294,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Group &apos;{0}&apos; is empty. Please add at least one node. 的本地化字符串。
/// </summary>
public static string GroupEmpty {
get {
return ResourceManager.GetString("GroupEmpty", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 This is not the correct configuration, please check 的本地化字符串。 /// 查找类似 This is not the correct configuration, please check 的本地化字符串。
/// </summary> /// </summary>
@ -699,6 +708,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Add Child Configuration 的本地化字符串。
/// </summary>
public static string menuAddChildServer {
get {
return ResourceManager.GetString("menuAddChildServer", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Add a custom configuration Configuration 的本地化字符串。 /// 查找类似 Add a custom configuration Configuration 的本地化字符串。
/// </summary> /// </summary>
@ -726,6 +744,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Add Policy Group Configuration 的本地化字符串。
/// </summary>
public static string menuAddPolicyGroupServer {
get {
return ResourceManager.GetString("menuAddPolicyGroupServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Add Proxy Chain Configuration 的本地化字符串。
/// </summary>
public static string menuAddProxyChainServer {
get {
return ResourceManager.GetString("menuAddProxyChainServer", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。 /// 查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。
/// </summary> /// </summary>
@ -978,6 +1014,78 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Generate Policy Group from Multiple Profiles 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServer {
get {
return ResourceManager.GetString("menuGenGroupMultipleServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration Fallback by sing-box 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerSingBoxFallback {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxFallback", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerSingBoxLeastPing {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxLeastPing", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration Fallback by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayFallback {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayFallback", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayLeastLoad {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastLoad", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayLeastPing {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastPing", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayRandom {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayRandom", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayRoundRobin {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayRoundRobin", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Global Hotkey Setting 的本地化字符串。 /// 查找类似 Global Hotkey Setting 的本地化字符串。
/// </summary> /// </summary>
@ -1347,6 +1455,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Remove Child Configuration 的本地化字符串。
/// </summary>
public static string menuRemoveChildServer {
get {
return ResourceManager.GetString("menuRemoveChildServer", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Remove duplicate Configurations 的本地化字符串。 /// 查找类似 Remove duplicate Configurations 的本地化字符串。
/// </summary> /// </summary>
@ -1500,6 +1617,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Server List 的本地化字符串。
/// </summary>
public static string menuServerList {
get {
return ResourceManager.GetString("menuServerList", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Configurations 的本地化字符串。 /// 查找类似 Configurations 的本地化字符串。
/// </summary> /// </summary>
@ -1509,60 +1635,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Multi-Configuration to custom configuration 的本地化字符串。
/// </summary>
public static string menuSetDefaultMultipleServer {
get {
return ResourceManager.GetString("menuSetDefaultMultipleServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。
/// </summary>
public static string menuSetDefaultMultipleServerSingBoxLeastPing {
get {
return ResourceManager.GetString("menuSetDefaultMultipleServerSingBoxLeastPing", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。
/// </summary>
public static string menuSetDefaultMultipleServerXrayLeastLoad {
get {
return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastLoad", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。
/// </summary>
public static string menuSetDefaultMultipleServerXrayLeastPing {
get {
return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastPing", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。
/// </summary>
public static string menuSetDefaultMultipleServerXrayRandom {
get {
return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRandom", resourceCulture);
}
}
/// <summary>
/// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。
/// </summary>
public static string menuSetDefaultMultipleServerXrayRoundRobin {
get {
return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRoundRobin", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Set as active Configuration (Enter) 的本地化字符串。 /// 查找类似 Set as active Configuration (Enter) 的本地化字符串。
/// </summary> /// </summary>
@ -1968,6 +2040,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Node alias &apos;{0}&apos; does not exist. 的本地化字符串。
/// </summary>
public static string NodeTagNotExist {
get {
return ResourceManager.GetString("NodeTagNotExist", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Non-VMess or SS protocol 的本地化字符串。 /// 查找类似 Non-VMess or SS protocol 的本地化字符串。
/// </summary> /// </summary>
@ -2022,6 +2103,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Please Add At Least One Configuration 的本地化字符串。
/// </summary>
public static string PleaseAddAtLeastOneServer {
get {
return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Please fill Remarks 的本地化字符串。 /// 查找类似 Please fill Remarks 的本地化字符串。
/// </summary> /// </summary>
@ -2068,20 +2158,20 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Proxy chained node alias &apos;{0}&apos; does not exist. 的本地化字符串。 /// 查找类似 Policy group: 的本地化字符串。
/// </summary> /// </summary>
public static string ProxyChainedNodeTagNotExist { public static string PolicyGroupPrefix {
get { get {
return ResourceManager.GetString("ProxyChainedNodeTagNotExist", resourceCulture); return ResourceManager.GetString("PolicyGroupPrefix", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// 查找类似 Proxy chained node remark &apos;{0}&apos; refers to an outbound that does not support config type &apos;{1}&apos;. 的本地化字符串。 /// 查找类似 Proxy chained: 的本地化字符串。
/// </summary> /// </summary>
public static string ProxyChainedNodeTagNotSupportConfigType { public static string ProxyChainedPrefix {
get { get {
return ResourceManager.GetString("ProxyChainedNodeTagNotSupportConfigType", resourceCulture); return ResourceManager.GetString("ProxyChainedPrefix", resourceCulture);
} }
} }
@ -2149,20 +2239,11 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Routing rule references outbound remark &apos;{0}&apos;, but no outbound with this remark exists. 的本地化字符串。 /// 查找类似 Routing rule outbound: 的本地化字符串。
/// </summary> /// </summary>
public static string RoutingRuleOutboundTagNotExist { public static string RoutingRuleOutboundPrefix {
get { get {
return ResourceManager.GetString("RoutingRuleOutboundTagNotExist", resourceCulture); return ResourceManager.GetString("RoutingRuleOutboundPrefix", resourceCulture);
}
}
/// <summary>
/// 查找类似 Routing rule outbound remark &apos;{0}&apos; refers to an outbound that does not support config type &apos;{1}&apos;. 的本地化字符串。
/// </summary>
public static string RoutingRuleOutboundTagNotSupportConfigType {
get {
return ResourceManager.GetString("RoutingRuleOutboundTagNotSupportConfigType", resourceCulture);
} }
} }
@ -2364,15 +2445,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Apply to Proxy Domains Only 的本地化字符串。
/// </summary>
public static string TbApplyProxyDomainsOnly {
get {
return ResourceManager.GetString("TbApplyProxyDomainsOnly", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Auto refresh 的本地化字符串。 /// 查找类似 Auto refresh 的本地化字符串。
/// </summary> /// </summary>
@ -2445,6 +2517,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Policy Group 的本地化字符串。
/// </summary>
public static string TbConfigTypePolicyGroup {
get {
return ResourceManager.GetString("TbConfigTypePolicyGroup", resourceCulture);
}
}
/// <summary>
/// 查找类似 Proxy Chain 的本地化字符串。
/// </summary>
public static string TbConfigTypeProxyChain {
get {
return ResourceManager.GetString("TbConfigTypeProxyChain", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Confirm 的本地化字符串。 /// 查找类似 Confirm 的本地化字符串。
/// </summary> /// </summary>
@ -2589,6 +2679,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Applies globally by default, with built-in FakeIP filtering (sing-box only). 的本地化字符串。
/// </summary>
public static string TbFakeIPTips {
get {
return ResourceManager.GetString("TbFakeIPTips", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback 的本地化字符串。
/// </summary>
public static string TbFallback {
get {
return ResourceManager.GetString("TbFallback", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Fingerprint 的本地化字符串。 /// 查找类似 Fingerprint 的本地化字符串。
/// </summary> /// </summary>
@ -2706,6 +2814,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Most Stable 的本地化字符串。
/// </summary>
public static string TbLeastLoad {
get {
return ResourceManager.GetString("TbLeastLoad", resourceCulture);
}
}
/// <summary>
/// 查找类似 Lowest Latency 的本地化字符串。
/// </summary>
public static string TbLeastPing {
get {
return ResourceManager.GetString("TbLeastPing", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Address (IPv4, IPv6) 的本地化字符串。 /// 查找类似 Address (IPv4, IPv6) 的本地化字符串。
/// </summary> /// </summary>
@ -2760,6 +2886,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Policy Group Type 的本地化字符串。
/// </summary>
public static string TbPolicyGroupType {
get {
return ResourceManager.GetString("TbPolicyGroupType", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Port 的本地化字符串。 /// 查找类似 Port 的本地化字符串。
/// </summary> /// </summary>
@ -2832,6 +2967,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Random 的本地化字符串。
/// </summary>
public static string TbRandom {
get {
return ResourceManager.GetString("TbRandom", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 v2ray Full Config Template 的本地化字符串。 /// 查找类似 v2ray Full Config Template 的本地化字符串。
/// </summary> /// </summary>
@ -2895,6 +3039,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Round Robin 的本地化字符串。
/// </summary>
public static string TbRoundRobin {
get {
return ResourceManager.GetString("TbRoundRobin", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// </summary> /// </summary>

View file

@ -1377,22 +1377,22 @@
<data name="TbPorts7Tips" xml:space="preserve"> <data name="TbPorts7Tips" xml:space="preserve">
<value>مخفی و پورت می شود، با کاما (،) جدا می شود</value> <value>مخفی و پورت می شود، با کاما (،) جدا می شود</value>
</data> </data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve"> <data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>چند سرور به پیکربندی سفارشی</value> <value>Generate Policy Group from Multiple Profiles</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>چند سرور تصادفی توسط Xray</value> <value>چند سرور تصادفی توسط Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>چند سرور RoundRobin توسط Xray</value> <value>چند سرور RoundRobin توسط Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>چند سرور LeastPing توسط Xray</value> <value>چند سرور LeastPing توسط Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>چند سرور LeastLoad توسط Xray</value> <value>چند سرور LeastLoad توسط Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>LeastPing چند سرور توسط sing-box</value> <value>LeastPing چند سرور توسط sing-box</value>
</data> </data>
<data name="menuExportConfig" xml:space="preserve"> <data name="menuExportConfig" xml:space="preserve">
@ -1455,9 +1455,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value> <value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value> <value>Basic DNS Settings</value>
</data> </data>
@ -1515,6 +1512,57 @@
<data name="TbSelectProfile" xml:space="preserve"> <data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'.</value>
</data> </data>
@ -1524,16 +1572,19 @@
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'.</value>
</data> </data>
<data name="RoutingRuleOutboundTagNotExist" xml:space="preserve"> <data name="ProxyChainedPrefix" xml:space="preserve">
<value>Routing rule references outbound remark '{0}', but no outbound with this remark exists.</value> <value>Proxy chained: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotSupportConfigType" xml:space="preserve"> <data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Routing rule outbound: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotExist" xml:space="preserve"> <data name="PolicyGroupPrefix" xml:space="preserve">
<value>Proxy chained node alias '{0}' does not exist.</value> <value>Policy group: </value>
</data> </data>
<data name="RoutingRuleOutboundTagNotSupportConfigType" xml:space="preserve"> <data name="NodeTagNotExist" xml:space="preserve">
<value>Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data> </data>
</root> </root>

View file

@ -1377,22 +1377,22 @@
<data name="TbPorts7Tips" xml:space="preserve"> <data name="TbPorts7Tips" xml:space="preserve">
<value>A portot lefedi, vesszővel (,) elválasztva</value> <value>A portot lefedi, vesszővel (,) elválasztva</value>
</data> </data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve"> <data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Több konfiguráció egyéni konfigurációra</value> <value>Generate Policy Group from Multiple Profiles</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Több konfiguráció véletlenszerűen Xray szerint</value> <value>Több konfiguráció véletlenszerűen Xray szerint</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Több konfiguráció RoundRobin Xray szerint</value> <value>Több konfiguráció RoundRobin Xray szerint</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Több konfiguráció legkisebb pinggel Xray szerint</value> <value>Több konfiguráció legkisebb pinggel Xray szerint</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Több konfiguráció legkisebb terheléssel Xray szerint</value> <value>Több konfiguráció legkisebb terheléssel Xray szerint</value>
</data> </data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>Több konfiguráció legkisebb pinggel sing-box szerint</value> <value>Több konfiguráció legkisebb pinggel sing-box szerint</value>
</data> </data>
<data name="menuExportConfig" xml:space="preserve"> <data name="menuExportConfig" xml:space="preserve">
@ -1455,9 +1455,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value> <value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value> <value>Basic DNS Settings</value>
</data> </data>
@ -1515,6 +1512,57 @@
<data name="TbSelectProfile" xml:space="preserve"> <data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'.</value>
</data> </data>
@ -1524,16 +1572,19 @@
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'.</value>
</data> </data>
<data name="RoutingRuleOutboundTagNotExist" xml:space="preserve"> <data name="ProxyChainedPrefix" xml:space="preserve">
<value>Routing rule references outbound remark '{0}', but no outbound with this remark exists.</value> <value>Proxy chained: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotSupportConfigType" xml:space="preserve"> <data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Routing rule outbound: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotExist" xml:space="preserve"> <data name="PolicyGroupPrefix" xml:space="preserve">
<value>Proxy chained node alias '{0}' does not exist.</value> <value>Policy group: </value>
</data> </data>
<data name="RoutingRuleOutboundTagNotSupportConfigType" xml:space="preserve"> <data name="NodeTagNotExist" xml:space="preserve">
<value>Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data> </data>
</root> </root>

View file

@ -1377,22 +1377,22 @@
<data name="TbPorts7Tips" xml:space="preserve"> <data name="TbPorts7Tips" xml:space="preserve">
<value>Will cover the port, separate with commas (,)</value> <value>Will cover the port, separate with commas (,)</value>
</data> </data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve"> <data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Multi-Configuration to custom configuration</value> <value>Generate Policy Group from Multiple Profiles</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Multi-Configuration Random by Xray</value> <value>Multi-Configuration Random by Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Multi-Configuration RoundRobin by Xray</value> <value>Multi-Configuration RoundRobin by Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Multi-Configuration LeastPing by Xray</value> <value>Multi-Configuration LeastPing by Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Multi-Configuration LeastLoad by Xray</value> <value>Multi-Configuration LeastLoad by Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>Multi-Configuration LeastPing by sing-box</value> <value>Multi-Configuration LeastPing by sing-box</value>
</data> </data>
<data name="menuExportConfig" xml:space="preserve"> <data name="menuExportConfig" xml:space="preserve">
@ -1455,9 +1455,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value> <value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value> <value>Basic DNS Settings</value>
</data> </data>
@ -1515,6 +1512,57 @@
<data name="TbSelectProfile" xml:space="preserve"> <data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'.</value>
</data> </data>
@ -1524,16 +1572,19 @@
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'.</value>
</data> </data>
<data name="RoutingRuleOutboundTagNotExist" xml:space="preserve"> <data name="ProxyChainedPrefix" xml:space="preserve">
<value>Routing rule references outbound remark '{0}', but no outbound with this remark exists.</value> <value>Proxy chained: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotSupportConfigType" xml:space="preserve"> <data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Routing rule outbound: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotExist" xml:space="preserve"> <data name="PolicyGroupPrefix" xml:space="preserve">
<value>Proxy chained node alias '{0}' does not exist.</value> <value>Policy group: </value>
</data> </data>
<data name="RoutingRuleOutboundTagNotSupportConfigType" xml:space="preserve"> <data name="NodeTagNotExist" xml:space="preserve">
<value>Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data> </data>
</root> </root>

View file

@ -1377,22 +1377,22 @@
<data name="TbPorts7Tips" xml:space="preserve"> <data name="TbPorts7Tips" xml:space="preserve">
<value>Заменит указанный порт, перечисляйте через запятую (,)</value> <value>Заменит указанный порт, перечисляйте через запятую (,)</value>
</data> </data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve"> <data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>От мультиконфигурации к пользовательской конфигурации</value> <value>Generate Policy Group from Multiple Profiles</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Случайный (Xray)</value> <value>Случайный (Xray)</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Круговой (Xray)</value> <value>Круговой (Xray)</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Минимальное RTT (минимальное время туда-обратно) (Xray)</value> <value>Минимальное RTT (минимальное время туда-обратно) (Xray)</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Минимальная нагрузка (Xray)</value> <value>Минимальная нагрузка (Xray)</value>
</data> </data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>Минимальное RTT (минимальное время туда-обратно) (sing-box)</value> <value>Минимальное RTT (минимальное время туда-обратно) (sing-box)</value>
</data> </data>
<data name="menuExportConfig" xml:space="preserve"> <data name="menuExportConfig" xml:space="preserve">
@ -1455,9 +1455,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS hosts: (каждая строка в формате "domain1 ip1 ip2")</value> <value>DNS hosts: (каждая строка в формате "domain1 ip1 ip2")</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Применять только к доменам через прокси</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Базовые настройки DNS</value> <value>Базовые настройки DNS</value>
</data> </data>
@ -1515,6 +1512,57 @@
<data name="TbSelectProfile" xml:space="preserve"> <data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'.</value>
</data> </data>
@ -1524,16 +1572,19 @@
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'.</value>
</data> </data>
<data name="RoutingRuleOutboundTagNotExist" xml:space="preserve"> <data name="ProxyChainedPrefix" xml:space="preserve">
<value>Routing rule references outbound remark '{0}', but no outbound with this remark exists.</value> <value>Proxy chained: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotSupportConfigType" xml:space="preserve"> <data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Routing rule outbound: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotExist" xml:space="preserve"> <data name="PolicyGroupPrefix" xml:space="preserve">
<value>Proxy chained node alias '{0}' does not exist.</value> <value>Policy group: </value>
</data> </data>
<data name="RoutingRuleOutboundTagNotSupportConfigType" xml:space="preserve"> <data name="NodeTagNotExist" xml:space="preserve">
<value>Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data> </data>
</root> </root>

View file

@ -1374,22 +1374,22 @@
<data name="TbPorts7Tips" xml:space="preserve"> <data name="TbPorts7Tips" xml:space="preserve">
<value>会覆盖端口,多组时用逗号 (,) 隔开</value> <value>会覆盖端口,多组时用逗号 (,) 隔开</value>
</data> </data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve"> <data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>多配置文件产生自定义配置 (多选)</value> <value>多配置文件生成策略组</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>多配置文件随机 Xray</value> <value>多配置文件随机 Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>多配置文件负载均衡 Xray</value> <value>多配置文件负载均衡 Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>多配置文件最低延迟 Xray</value> <value>多配置文件最低延迟 Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>多配置文件最稳定 Xray</value> <value>多配置文件最稳定 Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>多配置文件最低延迟 sing-box</value> <value>多配置文件最低延迟 sing-box</value>
</data> </data>
<data name="menuExportConfig" xml:space="preserve"> <data name="menuExportConfig" xml:space="preserve">
@ -1452,9 +1452,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts“域名1 ip1 ip2” 一行一个)</value> <value>DNS Hosts“域名1 ip1 ip2” 一行一个)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>仅对代理域名生效</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>DNS 基础设置</value> <value>DNS 基础设置</value>
</data> </data>
@ -1512,6 +1509,57 @@
<data name="TbSelectProfile" xml:space="preserve"> <data name="TbSelectProfile" xml:space="preserve">
<value>选择配置文件</value> <value>选择配置文件</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>请至少添加一个配置文件</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>策略组</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>链式代理</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>最低延迟</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>随机</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>负载均衡</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>最稳定</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>策略组类型</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>添加策略组配置文件</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>添加链式代理配置文件</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>添加子配置文件</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>删除子配置文件</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>服务器列表</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>故障转移</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>多配置文件故障转移 sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>多配置文件故障转移 Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="CoreNotSupportNetwork" xml:space="preserve">
<value>核心 '{0}' 不支持网络类型 '{1}'。</value> <value>核心 '{0}' 不支持网络类型 '{1}'。</value>
</data> </data>
@ -1521,16 +1569,19 @@
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="CoreNotSupportProtocol" xml:space="preserve">
<value>核心 '{0}' 不支持协议 '{1}'。</value> <value>核心 '{0}' 不支持协议 '{1}'。</value>
</data> </data>
<data name="RoutingRuleOutboundTagNotExist" xml:space="preserve"> <data name="ProxyChainedPrefix" xml:space="preserve">
<value>路由规则引用了出站别名 '{0}',但不存在具有该别名的出站。</value> <value>代理链: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotSupportConfigType" xml:space="preserve"> <data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>代理链节点别名 '{0}' 引用的出站不支持配置类型 '{1}'。</value> <value>路由规则出站: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotExist" xml:space="preserve"> <data name="PolicyGroupPrefix" xml:space="preserve">
<value>代理链节点别名 '{0}' 不存在。</value> <value>策略组: </value>
</data> </data>
<data name="RoutingRuleOutboundTagNotSupportConfigType" xml:space="preserve"> <data name="NodeTagNotExist" xml:space="preserve">
<value>路由规则出站别名 '{0}' 引用的出站不支持配置类型 '{1}'。</value> <value>节点别名 '{0}' 不存在。</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>组“{0}”为空。请至少添加一个节点。</value>
</data> </data>
</root> </root>

View file

@ -1374,22 +1374,22 @@
<data name="TbPorts7Tips" xml:space="preserve"> <data name="TbPorts7Tips" xml:space="preserve">
<value>會覆蓋埠,多組時用逗號 (,) 隔開</value> <value>會覆蓋埠,多組時用逗號 (,) 隔開</value>
</data> </data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve"> <data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>多設定檔產生自訂配置 (多選)</value> <value>Generate Policy Group from Multiple Profiles</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>多設定檔隨機 Xray</value> <value>多設定檔隨機 Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>多設定檔負載平衡 Xray</value> <value>多設定檔負載平衡 Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>多設定檔最低延遲 Xray</value> <value>多設定檔最低延遲 Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>多設定檔最穩定 Xray</value> <value>多設定檔最穩定 Xray</value>
</data> </data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve"> <data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>多設定檔最低延遲 sing-box</value> <value>多設定檔最低延遲 sing-box</value>
</data> </data>
<data name="menuExportConfig" xml:space="preserve"> <data name="menuExportConfig" xml:space="preserve">
@ -1452,9 +1452,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve"> <data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value> <value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data> </data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve"> <data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value> <value>Basic DNS Settings</value>
</data> </data>
@ -1512,6 +1509,57 @@
<data name="TbSelectProfile" xml:space="preserve"> <data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'.</value>
</data> </data>
@ -1521,16 +1569,19 @@
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'.</value>
</data> </data>
<data name="RoutingRuleOutboundTagNotExist" xml:space="preserve"> <data name="ProxyChainedPrefix" xml:space="preserve">
<value>Routing rule references outbound remark '{0}', but no outbound with this remark exists.</value> <value>Proxy chained: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotSupportConfigType" xml:space="preserve"> <data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Routing rule outbound: </value>
</data> </data>
<data name="ProxyChainedNodeTagNotExist" xml:space="preserve"> <data name="PolicyGroupPrefix" xml:space="preserve">
<value>Proxy chained node alias '{0}' does not exist.</value> <value>Policy group: </value>
</data> </data>
<data name="RoutingRuleOutboundTagNotSupportConfigType" xml:space="preserve"> <data name="NodeTagNotExist" xml:space="preserve">
<value>Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'.</value> <value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data> </data>
</root> </root>

View file

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

View file

@ -44,6 +44,7 @@
<EmbeddedResource Include="Sample\tun_singbox_inbound" /> <EmbeddedResource Include="Sample\tun_singbox_inbound" />
<EmbeddedResource Include="Sample\tun_singbox_rules" /> <EmbeddedResource Include="Sample\tun_singbox_rules" />
<EmbeddedResource Include="Sample\linux_autostart_config" /> <EmbeddedResource Include="Sample\linux_autostart_config" />
<EmbeddedResource Include="Sample\singbox_fakeip_filter" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -8,92 +8,114 @@ namespace ServiceLib.Services;
/// <summary> /// <summary>
/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.). /// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.).
/// Return (ok, msg) for VMs to decide.
/// </summary> /// </summary>
public class ActionPrecheckService public class ActionPrecheckService(Config config)
{ {
private static readonly Lazy<ActionPrecheckService> _instance = new(() => new ActionPrecheckService(AppManager.Instance.Config)); private static readonly Lazy<ActionPrecheckService> _instance = new(() => new ActionPrecheckService(AppManager.Instance.Config));
public static ActionPrecheckService Instance => _instance.Value; public static ActionPrecheckService Instance => _instance.Value;
private readonly Config _config; private readonly Config _config = config;
public ActionPrecheckService(Config config) public async Task<List<string>> CheckBeforeSetActive(string? indexId)
{
_config = config;
}
private static List<string> OneMsg(string msg) => new() { msg };
public async Task<(bool ok, List<string> msgs)> CheckBeforeSetActive(string? indexId)
{ {
if (indexId.IsNullOrEmpty()) if (indexId.IsNullOrEmpty())
{ {
return (false, OneMsg(ResUI.PleaseSelectServer)); return [ResUI.PleaseSelectServer];
} }
var item = await AppManager.Instance.GetProfileItem(indexId); var item = await AppManager.Instance.GetProfileItem(indexId);
if (item is null) if (item is null)
{ {
return (false, OneMsg(ResUI.PleaseSelectServer)); return [ResUI.PleaseSelectServer];
} }
return await CheckBeforeGenerateConfig(item); return await CheckBeforeGenerateConfig(item);
} }
public async Task<(bool ok, List<string> msgs)> CheckBeforeGenerateConfig(ProfileItem? item) public async Task<List<string>> CheckBeforeGenerateConfig(ProfileItem? item)
{ {
if (item is null) if (item is null)
{ {
return (false, OneMsg(ResUI.PleaseSelectServer)); return [ResUI.PleaseSelectServer];
} }
var msgs = new List<string>(); var errors = new List<string>();
var currentNodeMsgs = ValidateCurrentNodeAndCoreSupport(item).ToList(); errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item));
if (currentNodeMsgs.Count > 0) errors.AddRange(await ValidateRelatedNodesExistAndValid(item));
return errors;
}
private async Task<List<string>> ValidateCurrentNodeAndCoreSupport(ProfileItem item)
{
if (item.ConfigType == EConfigType.Custom)
{ {
msgs.AddRange(currentNodeMsgs); return [];
return (false, msgs);
} }
msgs.AddRange(await ValidateRelatedNodesExistAndValid(item));
return (true, msgs);
}
private IEnumerable<string> ValidateCurrentNodeAndCoreSupport(ProfileItem item)
{
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
return ValidateNodeAndCoreSupport(item, coreType); return await ValidateNodeAndCoreSupport(item, coreType);
} }
/// <summary> private async Task<List<string>> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null)
/// <summary>
/// Validate whether the node and chosen core combination is supported. Returns a collection of messages to show the user.
/// An empty collection means there are no blocking errors.
/// </summary>
///
private IEnumerable<string> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null)
{ {
var errors = new List<string>();
// sing-box does not support xhttp / kcp // sing-box does not support xhttp / kcp
// sing-box does not support transports like ws/http/httpupgrade/etc. when the node is not vmess/trojan/vless // sing-box does not support transports like ws/http/httpupgrade/etc. when the node is not vmess/trojan/vless
coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType); coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType);
if (item.ConfigType is EConfigType.Custom)
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol, coreType.ToString(), item.ConfigType.ToString()));
return errors;
}
if (item.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group);
if (group is null || group.ChildItems.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks));
return errors;
}
foreach (var child in Utils.String2List(group.ChildItems))
{
if (child.IsNullOrEmpty())
{
continue;
}
var childItem = await AppManager.Instance.GetProfileItem(child);
if (childItem is null)
{
errors.Add(string.Format(ResUI.NodeTagNotExist, child));
continue;
}
var childErrors = await ValidateNodeAndCoreSupport(childItem, coreType);
errors.AddRange(childErrors);
}
return errors;
}
var net = item.GetNetwork() ?? item.Network; var net = item.GetNetwork() ?? item.Network;
if (coreType == ECoreType.sing_box) if (coreType == ECoreType.sing_box)
{ {
if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
{ {
yield return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net); errors.Add(string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net));
yield break; return errors;
} }
if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan)) if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan))
{ {
if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade)) if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade))
{ {
yield return string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net); errors.Add(string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net));
yield break; return errors;
} }
} }
} }
@ -102,80 +124,74 @@ public class ActionPrecheckService
// Xray core does not support these protocols // Xray core does not support these protocols
if (item.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) if (item.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls)
{ {
yield return string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString()); errors.Add(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString()));
yield break; return errors;
} }
} }
yield break; // explicit for clarity; no blocking errors return errors;
} }
/// <summary> private async Task<List<string>> ValidateRelatedNodesExistAndValid(ProfileItem? item)
/// Validate that nodes related to the current node (chained/routing) exist and are valid.
/// </summary>
private async Task<IEnumerable<string>> ValidateRelatedNodesExistAndValid(ProfileItem? item)
{ {
var msgs = new List<string>(); var errors = new List<string>();
msgs.AddRange(await ValidateProxyChainedNodeExistAndValid(item)); errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item));
msgs.AddRange(await ValidateRoutingNodeExistAndValid(item)); errors.AddRange(await ValidateRoutingNodeExistAndValid(item));
return msgs; return errors;
} }
private async Task<IEnumerable<string>> ValidateProxyChainedNodeExistAndValid(ProfileItem? item) private async Task<List<string>> ValidateProxyChainedNodeExistAndValid(ProfileItem? item)
{ {
var msgs = new List<string>(); var errors = new List<string>();
if (item is null) if (item is null)
{ {
return msgs; return errors;
} }
// prev node and next node // prev node and next node
var subItem = await AppManager.Instance.GetSubItem(item.Subid); var subItem = await AppManager.Instance.GetSubItem(item.Subid);
if (subItem is null) if (subItem is null)
{ {
return msgs; return errors;
} }
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, msgs); await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors);
CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, msgs); await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors);
return msgs; return errors;
} }
private void CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List<string> msgs) private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List<string> errors)
{ {
if (node is not null) if (node is not null)
{ {
msgs.AddRange(ValidateNodeAndCoreSupport(node, coreType)); var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType);
if (node.ConfigType is EConfigType.Custom) errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + s));
{
msgs.Add(string.Format(ResUI.ProxyChainedNodeTagNotSupportConfigType, node.Remarks, node.ConfigType.ToString()));
}
} }
else if (tag.IsNotEmpty()) else if (tag.IsNotEmpty())
{ {
msgs.Add(string.Format(ResUI.ProxyChainedNodeTagNotExist, tag)); errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag));
} }
} }
private async Task<IEnumerable<string>> ValidateRoutingNodeExistAndValid(ProfileItem? item) private async Task<List<string>> ValidateRoutingNodeExistAndValid(ProfileItem? item)
{ {
var msgs = new List<string>(); var errors = new List<string>();
if (item is null) if (item is null)
{ {
return msgs; return errors;
} }
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
var routing = await ConfigHandler.GetDefaultRouting(_config); var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing == null) if (routing == null)
{ {
return msgs; return errors;
} }
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet); var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
@ -195,17 +211,14 @@ public class ActionPrecheckService
var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (tagItem is null) if (tagItem is null)
{ {
msgs.Add(string.Format(ResUI.RoutingRuleOutboundTagNotExist, outboundTag)); errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag));
continue; continue;
} }
msgs.AddRange(ValidateNodeAndCoreSupport(tagItem, coreType)); var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType);
if (tagItem.ConfigType is EConfigType.Custom) errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + s));
{
msgs.Add(string.Format(ResUI.RoutingRuleOutboundTagNotSupportConfigType, outboundTag, tagItem.ConfigType.ToString()));
}
} }
return msgs; return errors;
} }
} }

View file

@ -15,6 +15,33 @@ public partial class CoreConfigSingboxService(Config config)
var ret = new RetResult(); var ret = new RetResult();
try try
{ {
if (node?.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
return await GenerateClientMultipleLoadConfig(childProfiles, profileGroupItem.MultipleLoad);
case EConfigType.ProxyChain:
return await GenerateClientChainConfig(childProfiles);
}
}
if (node == null if (node == null
|| node.Port <= 0) || node.Port <= 0)
{ {
@ -344,7 +371,105 @@ public partial class CoreConfigSingboxService(Config config)
} }
} }
public async Task<RetResult> GenerateClientMultipleLoadConfig(List<ProfileItem> selecteds) 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;
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;
}
singboxConfig.outbounds.RemoveAt(0);
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
var proxyProfiles = new List<ProfileItem>();
foreach (var it in selecteds)
{
if (it.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var itemGroup = await AppManager.Instance.GetProfileItem(it.IndexId);
proxyProfiles.Add(itemGroup);
}
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, multipleLoad);
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> GenerateClientChainConfig(List<ProfileItem> selecteds)
{ {
var ret = new RetResult(); var ret = new RetResult();
try try
@ -419,7 +544,7 @@ public partial class CoreConfigSingboxService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
await GenOutboundsList(proxyProfiles, singboxConfig); await GenChainOutboundsList(proxyProfiles, singboxConfig);
await GenDns(null, singboxConfig); await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig); await ConvertGeo2Ruleset(singboxConfig);

View file

@ -33,6 +33,15 @@ public partial class CoreConfigSingboxService
lastRule.Ip?.Contains("0.0.0.0/0") == true); lastRule.Ip?.Contains("0.0.0.0/0") == true);
} }
singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag; singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag;
if ((!useDirectDns) && simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false)
{
singboxConfig.dns.rules.Add(new()
{
server = Global.SingboxFakeDNSTag,
query_type = new List<int> { 1, 28 }, // A and AAAA
rewrite_ttl = 1,
});
}
// Tun2SocksAddress // Tun2SocksAddress
if (node != null && Utils.IsDomain(node.Address)) if (node != null && Utils.IsDomain(node.Address))
@ -197,6 +206,28 @@ public partial class CoreConfigSingboxService
}); });
} }
if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == true)
{
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
fakeipFilterRule.invert = true;
var rule4Fake = new Rule4Sbox
{
server = Global.SingboxFakeDNSTag,
type = "logical",
mode = "and",
rewrite_ttl = 1,
rules = new List<Rule4Sbox>
{
new() {
query_type = new List<int> { 1, 28 }, // A and AAAA
},
fakeipFilterRule,
}
};
singboxConfig.dns.rules.Add(rule4Fake);
}
var routing = await ConfigHandler.GetDefaultRouting(_config); var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing == null) if (routing == null)
return 0; return 0;
@ -276,10 +307,12 @@ public partial class CoreConfigSingboxService
} }
else else
{ {
if (simpleDNSItem.FakeIP == true) if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false)
{ {
var rule4Fake = JsonUtils.DeepCopy(rule); var rule4Fake = JsonUtils.DeepCopy(rule);
rule4Fake.server = Global.SingboxFakeDNSTag; rule4Fake.server = Global.SingboxFakeDNSTag;
rule4Fake.query_type = new List<int> { 1, 28 }; // A and AAAA
rule4Fake.rewrite_ttl = 1;
singboxConfig.dns.rules.Add(rule4Fake); singboxConfig.dns.rules.Add(rule4Fake);
} }
rule.server = Global.SingboxRemoteDNSTag; rule.server = Global.SingboxRemoteDNSTag;

View file

@ -410,7 +410,7 @@ public partial class CoreConfigSingboxService
return 0; return 0;
} }
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig) private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
{ {
try try
{ {
@ -438,6 +438,38 @@ public partial class CoreConfigSingboxService
{ {
index++; index++;
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
continue;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{index}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
if (ret == 0)
{
proxyTags.Add(childBaseTagName);
}
continue;
}
// Handle proxy chain // Handle proxy chain
string? prevTag = null; string? prevTag = null;
var currentServer = await GenServer(node); var currentServer = await GenServer(node);
@ -450,7 +482,7 @@ public partial class CoreConfigSingboxService
var subItem = await AppManager.Instance.GetSubItem(node.Subid); var subItem = await AppManager.Instance.GetSubItem(node.Subid);
// current proxy // current proxy
currentServer.tag = $"{Global.ProxyTag}-{index}"; currentServer.tag = $"{baseTagName}-{index}";
proxyTags.Add(currentServer.tag); proxyTags.Add(currentServer.tag);
if (!node.Subid.IsNullOrEmpty()) if (!node.Subid.IsNullOrEmpty())
@ -467,7 +499,7 @@ public partial class CoreConfigSingboxService
{ {
var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound); var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(prevNode, prevOutbound); await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; prevTag = $"prev-{baseTagName}-{++prevIndex}";
prevOutbound.tag = prevTag; prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound); prevOutbounds.Add(prevOutbound);
} }
@ -508,16 +540,21 @@ public partial class CoreConfigSingboxService
var outUrltest = new Outbound4Sbox var outUrltest = new Outbound4Sbox
{ {
type = "urltest", type = "urltest",
tag = $"{Global.ProxyTag}-auto", tag = $"{baseTagName}-auto",
outbounds = proxyTags, outbounds = proxyTags,
interrupt_exist_connections = false, interrupt_exist_connections = false,
}; };
if (multipleLoad == EMultipleLoad.Fallback)
{
outUrltest.tolerance = 5000;
}
// Add selector outbound (manual selection) // Add selector outbound (manual selection)
var outSelector = new Outbound4Sbox var outSelector = new Outbound4Sbox
{ {
type = "selector", type = "selector",
tag = Global.ProxyTag, tag = baseTagName,
outbounds = JsonUtils.DeepCopy(proxyTags), outbounds = JsonUtils.DeepCopy(proxyTags),
interrupt_exist_connections = false, interrupt_exist_connections = false,
}; };
@ -574,4 +611,52 @@ public partial class CoreConfigSingboxService
} }
return null; return null;
} }
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag)
{
var resultOutbounds = new List<Outbound4Sbox>();
var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
var server = await GenServer(node);
if (server is null)
{
break;
}
if (i == 0)
{
server.tag = baseTagName;
}
else
{
server.tag = baseTagName + i.ToString();
}
if (i != nodes.Count - 1)
{
server.detour = baseTagName + (i + 1).ToString();
}
if (server is Endpoints4Sbox endpoint)
{
resultEndpoints.Add(endpoint);
}
else if (server is Outbound4Sbox outbound)
{
resultOutbounds.Add(outbound);
}
}
singboxConfig.outbounds ??= new();
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
singboxConfig.endpoints ??= new();
resultEndpoints.AddRange(singboxConfig.endpoints);
singboxConfig.endpoints = resultEndpoints;
return await Task.FromResult(0);
}
} }

View file

@ -337,19 +337,60 @@ public partial class CoreConfigSingboxService
} }
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (node == null if (node == null
|| !Global.SingboxSupportConfigType.Contains(node.ConfigType)) || (!Global.SingboxSupportConfigType.Contains(node.ConfigType)
&& node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain)))
{ {
return Global.ProxyTag; return Global.ProxyTag;
} }
var tag = Global.ProxyTag + node.IndexId.ToString();
if (singboxConfig.outbounds.Any(o => o.tag == tag)
|| (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag)))
{
return tag;
}
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return Global.ProxyTag;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0)
{
return Global.ProxyTag;
}
var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
if (ret == 0)
{
return childBaseTagName;
}
return Global.ProxyTag;
}
var server = await GenServer(node); var server = await GenServer(node);
if (server is null) if (server is null)
{ {
return Global.ProxyTag; return Global.ProxyTag;
} }
server.tag = Global.ProxyTag + node.IndexId.ToString(); server.tag = tag;
if (server is Endpoints4Sbox endpoint) if (server is Endpoints4Sbox endpoint)
{ {
singboxConfig.endpoints ??= new(); singboxConfig.endpoints ??= new();

View file

@ -15,6 +15,33 @@ public partial class CoreConfigV2rayService(Config config)
var ret = new RetResult(); var ret = new RetResult();
try try
{ {
if (node?.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
return await GenerateClientMultipleLoadConfig(childProfiles, profileGroupItem.MultipleLoad);
case EConfigType.ProxyChain:
return await GenerateClientChainConfig(childProfiles);
}
}
if (node == null if (node == null
|| node.Port <= 0) || node.Port <= 0)
{ {
@ -75,6 +102,156 @@ public partial class CoreConfigV2rayService(Config config)
{ {
var ret = new RetResult(); 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;
}
v2rayConfig.outbounds.RemoveAt(0);
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
var proxyProfiles = new List<ProfileItem>();
foreach (var it in selecteds)
{
if (it.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var itemGroup = await AppManager.Instance.GetProfileItem(it.IndexId);
proxyProfiles.Add(itemGroup);
}
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 GenObservatory(v2rayConfig, multipleLoad);
await GenBalancer(v2rayConfig, multipleLoad);
var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}";
//add rule
var rules = v2rayConfig.routing.rules;
if (rules?.Count > 0)
{
var balancerTagSet = v2rayConfig.routing.balancers
.Select(b => b.tag)
.ToHashSet();
foreach (var rule in rules)
{
if (rule.outboundTag == null)
continue;
if (balancerTagSet.Contains(rule.outboundTag))
{
rule.balancerTag = rule.outboundTag;
rule.outboundTag = null;
continue;
}
var outboundWithSuffix = rule.outboundTag + Global.BalancerTagSuffix;
if (balancerTagSet.Contains(outboundWithSuffix))
{
rule.balancerTag = outboundWithSuffix;
rule.outboundTag = null;
}
}
}
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
{
v2rayConfig.routing.rules.Add(new()
{
ip = ["0.0.0.0/0", "::/0"],
balancerTag = defaultBalancerTag,
type = "field"
});
}
else
{
v2rayConfig.routing.rules.Add(new()
{
network = "tcp,udp",
balancerTag = defaultBalancerTag,
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> GenerateClientChainConfig(List<ProfileItem> selecteds)
{
var ret = new RetResult();
try try
{ {
if (_config == null) if (_config == null)
@ -148,41 +325,7 @@ public partial class CoreConfigV2rayService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
await GenOutboundsList(proxyProfiles, v2rayConfig); await GenChainOutboundsList(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.Success = true;

View file

@ -2,7 +2,7 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<int> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) private async Task<int> GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad)
{ {
if (multipleLoad == EMultipleLoad.LeastPing) if (multipleLoad == EMultipleLoad.LeastPing)
{ {
@ -15,7 +15,7 @@ public partial class CoreConfigV2rayService
}; };
v2rayConfig.observatory = observatory; v2rayConfig.observatory = observatory;
} }
else if (multipleLoad == EMultipleLoad.LeastLoad) else if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback)
{ {
var burstObservatory = new BurstObservatory4Ray var burstObservatory = new BurstObservatory4Ray
{ {
@ -30,6 +30,11 @@ public partial class CoreConfigV2rayService
}; };
v2rayConfig.burstObservatory = burstObservatory; v2rayConfig.burstObservatory = burstObservatory;
} }
return await Task.FromResult(0);
}
private async Task<string> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string selector = Global.ProxyTag)
{
var strategyType = multipleLoad switch var strategyType = multipleLoad switch
{ {
EMultipleLoad.Random => "random", EMultipleLoad.Random => "random",
@ -38,13 +43,22 @@ public partial class CoreConfigV2rayService
EMultipleLoad.LeastLoad => "leastLoad", EMultipleLoad.LeastLoad => "leastLoad",
_ => "roundRobin", _ => "roundRobin",
}; };
var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
var balancer = new BalancersItem4Ray var balancer = new BalancersItem4Ray
{ {
selector = [Global.ProxyTag], selector = [selector],
strategy = new() { type = strategyType }, strategy = new()
tag = $"{Global.ProxyTag}-round", {
type = strategyType,
settings = new()
{
expected = 1,
},
},
tag = balancerTag,
}; };
v2rayConfig.routing.balancers = [balancer]; v2rayConfig.routing.balancers ??= new();
return await Task.FromResult(0); v2rayConfig.routing.balancers.Add(balancer);
return await Task.FromResult(balancerTag);
} }
} }

View file

@ -552,7 +552,7 @@ public partial class CoreConfigV2rayService
return 0; return 0;
} }
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig) private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag)
{ {
try try
{ {
@ -577,6 +577,34 @@ public partial class CoreConfigV2rayService
{ {
index++; index++;
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
continue;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{index}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
continue;
}
// Handle proxy chain // Handle proxy chain
string? prevTag = null; string? prevTag = null;
var currentOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); var currentOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
@ -590,7 +618,7 @@ public partial class CoreConfigV2rayService
// current proxy // current proxy
await GenOutbound(node, currentOutbound); await GenOutbound(node, currentOutbound);
currentOutbound.tag = $"{Global.ProxyTag}-{index}"; currentOutbound.tag = $"{baseTagName}-{index}";
if (!node.Subid.IsNullOrEmpty()) if (!node.Subid.IsNullOrEmpty())
{ {
@ -606,7 +634,7 @@ public partial class CoreConfigV2rayService
{ {
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound); await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; prevTag = $"prev-{baseTagName}-{++prevIndex}";
prevOutbound.tag = prevTag; prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound); prevOutbounds.Add(prevOutbound);
} }
@ -692,4 +720,50 @@ public partial class CoreConfigV2rayService
} }
return null; return null;
} }
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, V2rayConfig v2RayConfig, string baseTagName = Global.ProxyTag)
{
var resultOutbounds = new List<Outbounds4Ray>();
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
break;
}
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
var result = await GenOutbound(node, outbound);
if (result != 0)
{
break;
}
if (i == 0)
{
outbound.tag = baseTagName;
}
else
{
// avoid v2ray observe
outbound.tag = "chain-" + baseTagName + i.ToString();
}
if (i != nodes.Count - 1)
{
outbound.streamSettings.sockopt = new()
{
dialerProxy = "chain-" + baseTagName + (i + 1).ToString()
};
}
resultOutbounds.Add(outbound);
}
v2RayConfig.outbounds ??= new();
resultOutbounds.AddRange(v2RayConfig.outbounds);
v2RayConfig.outbounds = resultOutbounds;
return await Task.FromResult(0);
}
} }

View file

@ -125,16 +125,60 @@ public partial class CoreConfigV2rayService
} }
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (node == null if (node == null
|| !Global.XraySupportConfigType.Contains(node.ConfigType)) || (!Global.XraySupportConfigType.Contains(node.ConfigType)
&& node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain)))
{ {
return Global.ProxyTag; return Global.ProxyTag;
} }
var tag = Global.ProxyTag + node.IndexId.ToString();
if (v2rayConfig.outbounds.Any(p => p.tag == tag))
{
return tag;
}
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return Global.ProxyTag;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0)
{
return Global.ProxyTag;
}
var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
if (node.ConfigType == EConfigType.PolicyGroup)
{
await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, childBaseTagName);
}
if (ret == 0)
{
return childBaseTagName;
}
return Global.ProxyTag;
}
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(node, outbound); await GenOutbound(node, outbound);
outbound.tag = Global.ProxyTag + node.IndexId.ToString(); outbound.tag = tag;
v2rayConfig.outbounds.Add(outbound); v2rayConfig.outbounds.Add(outbound);
return outbound.tag; return outbound.tag;

View file

@ -0,0 +1,222 @@
using System.Data;
using System.Reactive;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.ViewModels;
public class AddGroupServerViewModel : MyReactiveObject
{
[Reactive]
public ProfileItem SelectedSource { get; set; }
[Reactive]
public ProfileItem SelectedChild { get; set; }
[Reactive]
public IList<ProfileItem> SelectedChildren { get; set; }
[Reactive]
public string? CoreType { get; set; }
[Reactive]
public string? PolicyGroupType { get; set; }
public IObservableCollection<ProfileItem> ChildItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>();
//public ReactiveCommand<Unit, Unit> AddCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveCmd { get; }
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
public ReactiveCommand<Unit, Unit> MoveUpCmd { get; }
public ReactiveCommand<Unit, Unit> MoveDownCmd { get; }
public ReactiveCommand<Unit, Unit> MoveBottomCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddGroupServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView)
{
_config = AppManager.Instance.Config;
_updateView = updateView;
RemoveCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ChildRemoveAsync();
});
MoveTopCmd = ReactiveCommand.CreateFromTask(async () =>
{
await MoveServer(EMove.Top);
});
MoveUpCmd = ReactiveCommand.CreateFromTask(async () =>
{
await MoveServer(EMove.Up);
});
MoveDownCmd = ReactiveCommand.CreateFromTask(async () =>
{
await MoveServer(EMove.Down);
});
MoveBottomCmd = ReactiveCommand.CreateFromTask(async () =>
{
await MoveServer(EMove.Bottom);
});
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SaveServerAsync();
});
SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem);
CoreType = (SelectedSource?.CoreType ?? ECoreType.Xray).ToString();
ProfileGroupItemManager.Instance.TryGet(profileItem.IndexId, out var profileGroup);
PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch
{
EMultipleLoad.LeastPing => ResUI.TbLeastPing,
EMultipleLoad.Fallback => ResUI.TbFallback,
EMultipleLoad.Random => ResUI.TbRandom,
EMultipleLoad.RoundRobin => ResUI.TbRoundRobin,
EMultipleLoad.LeastLoad => ResUI.TbLeastLoad,
_ => ResUI.TbLeastPing,
};
_ = Init();
}
public async Task Init()
{
var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId);
if (childItemMulti != null)
{
var childIndexIds = childItemMulti.ChildItems.IsNullOrEmpty() ? new List<string>() : Utils.String2List(childItemMulti.ChildItems);
foreach (var item in childIndexIds)
{
var child = await AppManager.Instance.GetProfileItem(item);
if (child == null)
{
continue;
}
ChildItemsObs.Add(child);
}
}
}
public async Task ChildRemoveAsync()
{
if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return;
}
foreach (var it in SelectedChildren ?? [SelectedChild])
{
if (it != null)
{
ChildItemsObs.Remove(it);
}
}
await Task.CompletedTask;
}
public async Task MoveServer(EMove eMove)
{
if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return;
}
var index = ChildItemsObs.IndexOf(SelectedChild);
if (index < 0)
{
return;
}
var selectedChild = JsonUtils.DeepCopy(SelectedChild);
switch (eMove)
{
case EMove.Top:
if (index == 0)
{
return;
}
ChildItemsObs.RemoveAt(index);
ChildItemsObs.Insert(0, selectedChild);
break;
case EMove.Up:
if (index == 0)
{
return;
}
ChildItemsObs.RemoveAt(index);
ChildItemsObs.Insert(index - 1, selectedChild);
break;
case EMove.Down:
if (index == ChildItemsObs.Count - 1)
{
return;
}
ChildItemsObs.RemoveAt(index);
ChildItemsObs.Insert(index + 1, selectedChild);
break;
case EMove.Bottom:
if (index == ChildItemsObs.Count - 1)
{
return;
}
ChildItemsObs.RemoveAt(index);
ChildItemsObs.Add(selectedChild);
break;
default:
break;
}
await Task.CompletedTask;
}
private async Task SaveServerAsync()
{
var remarks = SelectedSource.Remarks;
if (remarks.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks);
return;
}
if (ChildItemsObs.Count == 0)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer);
return;
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) ||
SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup))
{
return;
}
var childIndexIds = new List<string>();
foreach (var item in ChildItemsObs)
{
if (!item.IndexId.IsNullOrEmpty())
{
childIndexIds.Add(item.IndexId);
}
}
SelectedSource.Address = Utils.List2String(childIndexIds);
var profileGroup = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource.IndexId);
profileGroup.ChildItems = Utils.List2String(childIndexIds);
profileGroup.MultipleLoad = PolicyGroupType switch
{
var s when s == ResUI.TbLeastPing => EMultipleLoad.LeastPing,
var s when s == ResUI.TbFallback => EMultipleLoad.Fallback,
var s when s == ResUI.TbRandom => EMultipleLoad.Random,
var s when s == ResUI.TbRoundRobin => EMultipleLoad.RoundRobin,
var s when s == ResUI.TbLeastLoad => EMultipleLoad.LeastLoad,
_ => EMultipleLoad.LeastPing,
};
if (await ConfigHandler.AddGroupServerCommon(_config, SelectedSource, profileGroup, true) == 0)
{
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null);
}
else
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
}
}
}

View file

@ -1,7 +1,6 @@
using System.Reactive; using System.Reactive;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;

View file

@ -334,9 +334,6 @@ public class CheckUpdateViewModel : MyReactiveObject
{ {
return; return;
} }
found.Remarks = model.Remarks;
var itemCopy = JsonUtils.DeepCopy(found);
itemCopy.Remarks = model.Remarks;
CheckUpdateModels.Replace(found, itemCopy);
} }
} }

View file

@ -391,7 +391,6 @@ public class ClashProxiesViewModel : MyReactiveObject
public async Task ProxiesDelayTestResult(SpeedTestResult result) public async Task ProxiesDelayTestResult(SpeedTestResult result)
{ {
//UpdateHandler(false, $"{item.name}={result}");
var detail = ProxyDetails.FirstOrDefault(it => it.Name == result.IndexId); var detail = ProxyDetails.FirstOrDefault(it => it.Name == result.IndexId);
if (detail == null) if (detail == null)
{ {
@ -414,7 +413,6 @@ public class ClashProxiesViewModel : MyReactiveObject
detail.Delay = _delayTimeout; detail.Delay = _delayTimeout;
detail.DelayName = string.Empty; detail.DelayName = string.Empty;
} }
ProxyDetails.Replace(detail, JsonUtils.DeepCopy(detail));
} }
#endregion proxy function #endregion proxy function

View file

@ -23,6 +23,8 @@ public class MainWindowViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> AddWireguardServerCmd { get; } public ReactiveCommand<Unit, Unit> AddWireguardServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddAnytlsServerCmd { get; } public ReactiveCommand<Unit, Unit> AddAnytlsServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; } public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddPolicyGroupServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddProxyChainServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaClipboardCmd { get; } public ReactiveCommand<Unit, Unit> AddServerViaClipboardCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaScanCmd { get; } public ReactiveCommand<Unit, Unit> AddServerViaScanCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaImageCmd { get; } public ReactiveCommand<Unit, Unit> AddServerViaImageCmd { get; }
@ -122,6 +124,14 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await AddServerAsync(true, EConfigType.Custom); await AddServerAsync(true, EConfigType.Custom);
}); });
AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.PolicyGroup);
});
AddProxyChainServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.ProxyChain);
});
AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () => AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await AddServerViaClipboardAsync(null); await AddServerViaClipboardAsync(null);
@ -228,6 +238,7 @@ public class MainWindowViewModel : MyReactiveObject
await ConfigHandler.InitBuiltinDNS(_config); await ConfigHandler.InitBuiltinDNS(_config);
await ConfigHandler.InitBuiltinFullConfigTemplate(_config); await ConfigHandler.InitBuiltinFullConfigTemplate(_config);
await ProfileExManager.Instance.Init(); await ProfileExManager.Instance.Init();
await ProfileGroupItemManager.Instance.Init();
await CoreManager.Instance.Init(_config, UpdateHandler); await CoreManager.Instance.Init(_config, UpdateHandler);
TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler); TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler);
@ -235,6 +246,7 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler); await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler);
} }
await RefreshServers();
BlReloadEnabled = true; BlReloadEnabled = true;
await Reload(); await Reload();
@ -321,6 +333,10 @@ public class MainWindowViewModel : MyReactiveObject
{ {
ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item);
} }
else if (eConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item);
}
else else
{ {
ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item);
@ -487,7 +503,7 @@ public class MainWindowViewModel : MyReactiveObject
} }
else if (Utils.IsLinux()) else if (Utils.IsLinux())
{ {
ProcUtils.ProcessStart("nautilus", path); ProcUtils.ProcessStart("xdg-open", path);
} }
else if (Utils.IsOSX()) else if (Utils.IsOSX())
{ {

View file

@ -1,27 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ProfilesSelectViewModel : MyReactiveObject public class ProfilesSelectViewModel : MyReactiveObject
{ {
#region private prop #region private prop
private List<ProfileItem> _lstProfile;
private string _serverFilter = string.Empty; private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new(); private Dictionary<string, bool> _dicHeaderSort = new();
private string _subIndexId = string.Empty; private string _subIndexId = string.Empty;
// ConfigType filter state: default include-mode with all types selected // ConfigType filter state: default include-mode with all types selected
private List<EConfigType> _filterConfigTypes = new(); private List<EConfigType> _filterConfigTypes = new();
private bool _filterExclude = false; private bool _filterExclude = false;
#endregion private prop #endregion private prop
@ -66,6 +61,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
_subIndexId = _config.SubIndexId ?? string.Empty; _subIndexId = _config.SubIndexId ?? string.Empty;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
this.WhenAnyValue( this.WhenAnyValue(
@ -130,6 +126,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
_updateView?.Invoke(EViewAction.CloseWindow, null); _updateView?.Invoke(EViewAction.CloseWindow, null);
return true; return true;
} }
#endregion Actions #endregion Actions
#region Servers && Groups #region Servers && Groups
@ -168,7 +165,6 @@ public class ProfilesSelectViewModel : MyReactiveObject
private async Task RefreshServersBiz() private async Task RefreshServersBiz()
{ {
var lstModel = await GetProfileItemsEx(_subIndexId, _serverFilter); var lstModel = await GetProfileItemsEx(_subIndexId, _serverFilter);
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
ProfileItems.Clear(); ProfileItems.Clear();
ProfileItems.AddRange(lstModel); ProfileItems.AddRange(lstModel);
@ -211,9 +207,6 @@ public class ProfilesSelectViewModel : MyReactiveObject
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{ {
var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter); var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter);
//await ConfigHandler.SetDefaultServer(_config, lstModel);
lstModel = (from t in lstModel lstModel = (from t in lstModel
select new ProfileItemModel select new ProfileItemModel
{ {

View file

@ -53,11 +53,13 @@ public class ProfilesViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> CopyServerCmd { get; } public ReactiveCommand<Unit, Unit> CopyServerCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultServerCmd { get; } public ReactiveCommand<Unit, Unit> SetDefaultServerCmd { get; }
public ReactiveCommand<Unit, Unit> ShareServerCmd { get; } public ReactiveCommand<Unit, Unit> ShareServerCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayRandomCmd { get; } public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayRandomCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayRoundRobinCmd { get; } public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayRoundRobinCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayLeastPingCmd { get; } public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayLeastPingCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayLeastLoadCmd { get; } public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayLeastLoadCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerSingBoxLeastPingCmd { get; } public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayFallbackCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerSingBoxLeastPingCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerSingBoxFallbackCmd { get; }
//servers move //servers move
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
@ -139,25 +141,33 @@ public class ProfilesViewModel : MyReactiveObject
{ {
await ShareServerAsync(); await ShareServerAsync();
}, canEditRemove); }, canEditRemove);
SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => GenGroupMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random); await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Random);
}, canEditRemove); }, canEditRemove);
SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => GenGroupMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin);
}, canEditRemove); }, canEditRemove);
SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => GenGroupMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing);
}, canEditRemove); }, canEditRemove);
SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => GenGroupMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad);
}, canEditRemove); }, canEditRemove);
SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => GenGroupMultipleServerXrayFallbackCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Fallback);
}, canEditRemove);
GenGroupMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing);
}, canEditRemove);
GenGroupMultipleServerSingBoxFallbackCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.Fallback);
}, canEditRemove); }, canEditRemove);
//servers move //servers move
@ -257,7 +267,7 @@ public class ProfilesViewModel : MyReactiveObject
SelectedMoveToGroup = new(); SelectedMoveToGroup = new();
await RefreshSubscriptions(); await RefreshSubscriptions();
await RefreshServers(); //await RefreshServers();
} }
#endregion Init #endregion Init
@ -293,7 +303,6 @@ public class ProfilesViewModel : MyReactiveObject
{ {
item.SpeedVal = result.Speed ?? string.Empty; item.SpeedVal = result.Speed ?? string.Empty;
} }
//_profileItems.Replace(item, JsonUtils.DeepCopy(item));
} }
public async Task UpdateStatistics(ServerSpeedItem update) public async Task UpdateStatistics(ServerSpeedItem update)
@ -314,17 +323,6 @@ public class ProfilesViewModel : MyReactiveObject
item.TodayUp = Utils.HumanFy(update.TodayUp); item.TodayUp = Utils.HumanFy(update.TodayUp);
item.TotalDown = Utils.HumanFy(update.TotalDown); item.TotalDown = Utils.HumanFy(update.TotalDown);
item.TotalUp = Utils.HumanFy(update.TotalUp); item.TotalUp = Utils.HumanFy(update.TotalUp);
//if (SelectedProfile?.IndexId == item.IndexId)
//{
// var temp = JsonUtils.DeepCopy(item);
// _profileItems.Replace(item, temp);
// SelectedProfile = temp;
//}
//else
//{
// _profileItems.Replace(item, JsonUtils.DeepCopy(item));
//}
} }
} }
catch catch
@ -503,6 +501,10 @@ public class ProfilesViewModel : MyReactiveObject
{ {
ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item);
} }
else if (eConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item);
}
else else
{ {
ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item);
@ -594,14 +596,10 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
var (ok, msgs) = await ActionPrecheckService.Instance.CheckBeforeSetActive(indexId); var msgs = await ActionPrecheckService.Instance.CheckBeforeSetActive(indexId);
if (msgs.Count > 0) foreach (var msg in msgs)
{ {
NoticeManager.Instance.Enqueue(msgs.First()); NoticeManager.Instance.SendMessage(msg);
}
if (!ok)
{
return;
} }
if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0)
@ -628,7 +626,7 @@ public class ProfilesViewModel : MyReactiveObject
await _updateView?.Invoke(EViewAction.ShareServer, url); await _updateView?.Invoke(EViewAction.ShareServer, url);
} }
private async Task SetDefaultMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) private async Task GenGroupMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad)
{ {
var lstSelected = await GetProfileItems(true); var lstSelected = await GetProfileItems(true);
if (lstSelected == null) if (lstSelected == null)
@ -636,7 +634,7 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad); var ret = await ConfigHandler.AddGroupServer4Multiple(_config, lstSelected, coreType, multipleLoad, SelectedSub?.Id);
if (ret.Success != true) if (ret.Success != true)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
@ -768,14 +766,10 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
var (ok, msgs) = await ActionPrecheckService.Instance.CheckBeforeGenerateConfig(item); var msgs = await ActionPrecheckService.Instance.CheckBeforeGenerateConfig(item);
if (msgs.Count > 0) foreach (var msg in msgs)
{ {
NoticeManager.Instance.Enqueue(msgs.First()); NoticeManager.Instance.SendMessage(msg);
}
if (!ok)
{
return;
} }
if (blClipboard) if (blClipboard)
{ {

View file

@ -13,6 +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>(); public IObservableCollection<RulesItemModel> RulesItems { get; } = new ObservableCollectionExtended<RulesItemModel>();
[Reactive] [Reactive]

View file

@ -0,0 +1,151 @@
<Window
x:Class="v2rayN.Desktop.Views.AddGroupServerWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="{x:Static resx:ResUI.menuServers}"
Width="900"
Height="700"
x:DataType="vms:AddGroupServerViewModel"
ShowInTaskbar="False"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin4}"
HorizontalAlignment="Center"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
</StackPanel>
<Grid DockPanel.Dock="Top" RowDefinitions="Auto,*,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid
Grid.Row="0"
ColumnDefinitions="180,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.menuServers}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRemarks}" />
<TextBox
x:Name="txtRemarks"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCoreType}" />
<ComboBox
x:Name="cmbCoreType"
Grid.Row="2"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<Grid
x:Name="gridPolicyGroup"
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="3"
ColumnDefinitions="180,Auto,Auto">
<TextBlock
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPolicyGroupType}" />
<ComboBox
x:Name="cmbPolicyGroupType"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
</Grid>
</Grid>
</Grid>
<TabControl>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}">
<DataGrid
x:Name="lstChild"
Grid.Row="1"
AutoGenerateColumns="False"
Background="Transparent"
BorderThickness="1"
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="False"
GridLinesVisibility="All"
HeadersVisibility="Column"
IsReadOnly="True"
ItemsSource="{Binding ChildItemsObs}"
SelectionMode="Extended">
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuAddChildServer" Header="{x:Static resx:ResUI.menuAddChildServer}" />
<MenuItem x:Name="menuRemoveChildServer" Header="{x:Static resx:ResUI.menuRemoveChildServer}" />
<MenuItem x:Name="menuSelectAllChild" Header="{x:Static resx:ResUI.menuSelectAll}" />
<Separator />
<MenuItem x:Name="menuMoveTop" Header="{x:Static resx:ResUI.menuMoveTop}" />
<MenuItem x:Name="menuMoveUp" Header="{x:Static resx:ResUI.menuMoveUp}" />
<MenuItem x:Name="menuMoveDown" Header="{x:Static resx:ResUI.menuMoveDown}" />
<MenuItem x:Name="menuMoveBottom" Header="{x:Static resx:ResUI.menuMoveBottom}" />
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn
Width="150"
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Port}"
Header="{x:Static resx:ResUI.LvPort}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Network}"
Header="{x:Static resx:ResUI.LvTransportProtocol}" />
<DataGridTextColumn
Width="100"
Binding="{Binding StreamSecurity}"
Header="{x:Static resx:ResUI.LvTLS}" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
</TabControl>
</DockPanel>
</Window>

View file

@ -0,0 +1,166 @@
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using DynamicData;
using ReactiveUI;
using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views;
public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
{
public AddGroupServerWindow()
{
InitializeComponent();
}
public AddGroupServerWindow(ProfileItem profileItem)
{
InitializeComponent();
this.Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close();
lstChild.SelectionChanged += LstChild_SelectionChanged;
ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Global.CoreTypes;
cmbPolicyGroupType.ItemsSource = new List<string>
{
ResUI.TbLeastPing,
ResUI.TbFallback,
ResUI.TbRandom,
ResUI.TbRoundRobin,
ResUI.TbLeastLoad,
};
switch (profileItem.ConfigType)
{
case EConfigType.PolicyGroup:
this.Title = ResUI.TbConfigTypePolicyGroup;
break;
case EConfigType.ProxyChain:
this.Title = ResUI.TbConfigTypeProxyChain;
gridPolicyGroup.IsVisible = false;
break;
}
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
// Context menu actions that require custom logic (Add, SelectAll)
menuAddChildServer.Click += MenuAddChild_Click;
menuSelectAllChild.Click += (s, e) => lstChild.SelectAll();
// Keyboard shortcuts when focus is within grid
this.AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
lstChild.LoadingRow += LstChild_LoadingRow;
}
private void LstChild_LoadingRow(object? sender, DataGridRowEventArgs e)
{
e.Row.Header = $" {e.Row.Index + 1}";
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
break;
}
return await Task.FromResult(true);
}
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
txtRemarks.Focus();
}
private void AddGroupServerWindow_KeyDown(object? sender, KeyEventArgs e)
{
if (!lstChild.IsKeyboardFocusWithin)
return;
if ((e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Meta)) != 0)
{
if (e.Key == Key.A)
{
lstChild.SelectAll();
e.Handled = true;
}
}
else
{
switch (e.Key)
{
case Key.T:
ViewModel?.MoveServer(EMove.Top);
e.Handled = true;
break;
case Key.U:
ViewModel?.MoveServer(EMove.Up);
e.Handled = true;
break;
case Key.D:
ViewModel?.MoveServer(EMove.Down);
e.Handled = true;
break;
case Key.B:
ViewModel?.MoveServer(EMove.Bottom);
e.Handled = true;
break;
case Key.Delete:
ViewModel?.ChildRemoveAsync();
e.Handled = true;
break;
}
}
}
private async void MenuAddChild_Click(object? sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
if (ViewModel?.SelectedSource?.ConfigType == EConfigType.PolicyGroup)
{
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
}
else
{
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
}
selectWindow.AllowMultiSelect(true);
var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true)
{
var profiles = await selectWindow.ProfileItems;
ViewModel?.ChildItemsObs.AddRange(profiles);
}
}
private void LstChild_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (ViewModel != null)
{
ViewModel.SelectedChildren = lstChild.SelectedItems.Cast<ProfileItem>().ToList();
}
}
}

View file

@ -229,7 +229,7 @@
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbApplyProxyDomainsOnly}" Text="{x:Static resx:ResUI.TbFakeIPTips}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock

View file

@ -35,6 +35,8 @@
<MenuItem x:Name="menuAddServerViaScan" Header="{x:Static resx:ResUI.menuAddServerViaScan}" /> <MenuItem x:Name="menuAddServerViaScan" Header="{x:Static resx:ResUI.menuAddServerViaScan}" />
<MenuItem x:Name="menuAddServerViaImage" Header="{x:Static resx:ResUI.menuAddServerViaImage}" /> <MenuItem x:Name="menuAddServerViaImage" Header="{x:Static resx:ResUI.menuAddServerViaImage}" />
<MenuItem x:Name="menuAddCustomServer" Header="{x:Static resx:ResUI.menuAddCustomServer}" /> <MenuItem x:Name="menuAddCustomServer" Header="{x:Static resx:ResUI.menuAddCustomServer}" />
<MenuItem x:Name="menuAddPolicyGroupServer" Header="{x:Static resx:ResUI.menuAddPolicyGroupServer}" />
<MenuItem x:Name="menuAddProxyChainServer" Header="{x:Static resx:ResUI.menuAddProxyChainServer}" />
<Separator /> <Separator />
<MenuItem x:Name="menuAddVmessServer" Header="{x:Static resx:ResUI.menuAddVmessServer}" /> <MenuItem x:Name="menuAddVmessServer" Header="{x:Static resx:ResUI.menuAddVmessServer}" />
<MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" /> <MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" />

View file

@ -86,6 +86,8 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables);
@ -204,6 +206,11 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
return false; return false;
return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(this); return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(this);
case EViewAction.AddGroupServerWindow:
if (obj is null)
return false;
return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog<bool>(this);
case EViewAction.DNSSettingWindow: case EViewAction.DNSSettingWindow:
return await new DNSSettingWindow().ShowDialog<bool>(this); return await new DNSSettingWindow().ShowDialog<bool>(this);

View file

@ -38,9 +38,10 @@
<WrapPanel Margin="4" DockPanel.Dock="Top"> <WrapPanel Margin="4" DockPanel.Dock="Top">
<ListBox <ListBox
x:Name="lstGroup" x:Name="lstGroup"
Margin="4,0" Margin="{StaticResource MarginLr4}"
DisplayMemberBinding="{Binding Remarks}" DisplayMemberBinding="{Binding Remarks}"
ItemsSource="{Binding SubItems}"> ItemsSource="{Binding SubItems}"
Theme="{DynamicResource ButtonRadioGroupListBox}">
<ListBox.ItemsPanel> <ListBox.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel /> <WrapPanel />
@ -89,20 +90,11 @@
Binding="{Binding ConfigType}" Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" Header="{x:Static resx:ResUI.LvServiceType}"
Tag="ConfigType" /> Tag="ConfigType" />
<DataGridTextColumn
<DataGridTemplateColumn SortMemberPath="Remarks" Tag="Remarks"> Width="120"
<DataGridTemplateColumn.Header> Binding="{Binding Remarks}"
<TextBlock Text="{x:Static resx:ResUI.LvRemarks}" /> Header="{x:Static resx:ResUI.LvRemarks}"
</DataGridTemplateColumn.Header> Tag="Remarks" />
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Margin="8,0" Orientation="Horizontal">
<TextBlock Text="{Binding Remarks}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn <DataGridTextColumn
Width="120" Width="120"
Binding="{Binding Address}" Binding="{Binding Address}"

View file

@ -1,17 +1,12 @@
using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager; using ServiceLib.Manager;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;

View file

@ -99,13 +99,14 @@
<MenuItem x:Name="menuCopyServer" Header="{x:Static resx:ResUI.menuCopyServer}" /> <MenuItem x:Name="menuCopyServer" Header="{x:Static resx:ResUI.menuCopyServer}" />
<MenuItem x:Name="menuShareServer" Header="{x:Static resx:ResUI.menuShareServer}" /> <MenuItem x:Name="menuShareServer" Header="{x:Static resx:ResUI.menuShareServer}" />
<Separator /> <Separator />
<MenuItem Header="{x:Static resx:ResUI.menuSetDefaultMultipleServer}"> <MenuItem Header="{x:Static resx:ResUI.menuGenGroupMultipleServer}">
<MenuItem x:Name="menuSetDefaultMultipleServerXrayRandom" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayRandom}" /> <MenuItem x:Name="menuGenGroupMultipleServerXrayRandom" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRandom}" />
<MenuItem x:Name="menuSetDefaultMultipleServerXrayRoundRobin" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayRoundRobin}" /> <MenuItem x:Name="menuGenGroupMultipleServerXrayRoundRobin" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRoundRobin}" />
<MenuItem x:Name="menuSetDefaultMultipleServerXrayLeastPing" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayLeastPing}" /> <MenuItem x:Name="menuGenGroupMultipleServerXrayLeastPing" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastPing}" />
<MenuItem x:Name="menuSetDefaultMultipleServerXrayLeastLoad" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayLeastLoad}" /> <MenuItem x:Name="menuGenGroupMultipleServerXrayLeastLoad" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastLoad}" />
<Separator /> <Separator />
<MenuItem x:Name="menuSetDefaultMultipleServerSingBoxLeastPing" Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerSingBoxLeastPing}" /> <MenuItem x:Name="menuGenGroupMultipleServerSingBoxLeastPing" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxLeastPing}" />
<MenuItem x:Name="menuGenGroupMultipleServerSingBoxFallback" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxFallback}" />
</MenuItem> </MenuItem>
<Separator /> <Separator />
<MenuItem x:Name="menuMixedTestServer" Header="{x:Static resx:ResUI.menuMixedTestServer}" /> <MenuItem x:Name="menuMixedTestServer" Header="{x:Static resx:ResUI.menuMixedTestServer}" />

View file

@ -69,11 +69,12 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRandomCmd, v => v.menuSetDefaultMultipleServerXrayRandom).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRoundRobinCmd, v => v.menuSetDefaultMultipleServerXrayRoundRobin).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastPingCmd, v => v.menuSetDefaultMultipleServerXrayLeastPing).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastLoadCmd, v => v.menuSetDefaultMultipleServerXrayLeastLoad).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerSingBoxLeastPingCmd, v => v.menuSetDefaultMultipleServerSingBoxLeastPing).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables);
//servers move //servers move
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables);
@ -112,7 +113,6 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
}); });
RestoreUI(); RestoreUI();
ViewModel?.RefreshServers();
} }
private async void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e) private async void LstProfiles_Sorting(object? sender, DataGridColumnEventArgs e)
@ -171,6 +171,11 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
return false; return false;
return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(_window); return await new AddServer2Window((ProfileItem)obj).ShowDialog<bool>(_window);
case EViewAction.AddGroupServerWindow:
if (obj is null)
return false;
return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog<bool>(_window);
case EViewAction.ShareServer: case EViewAction.ShareServer:
if (obj is null) if (obj is null)
return false; return false;

View file

@ -3,7 +3,6 @@ using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ReactiveUI; using ReactiveUI;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using System.Threading.Tasks;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;

View file

@ -3,7 +3,6 @@ using Avalonia;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ReactiveUI; using ReactiveUI;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using System.Threading.Tasks;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@ -64,7 +63,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e) private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e)
{ {
var selectWindow = new ProfilesSelectWindow(); var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true); selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
var result = await selectWindow.ShowDialog<bool?>(this); var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true) if (result == true)
{ {
@ -79,7 +78,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e) private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e)
{ {
var selectWindow = new ProfilesSelectWindow(); var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true); selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
var result = await selectWindow.ShowDialog<bool?>(this); var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true) if (result == true)
{ {

View file

@ -0,0 +1,213 @@
<base:WindowBase
x:Class="v2rayN.Views.AddGroupServerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:v2rayN.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveui="http://reactiveui.net"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="{x:Static resx:ResUI.menuServers}"
Width="900"
Height="700"
x:TypeArguments="vms:AddGroupServerViewModel"
ResizeMode="CanResize"
ShowInTaskbar="False"
Style="{StaticResource WindowGlobal}"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin4}"
HorizontalAlignment="Center"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
Style="{StaticResource DefButton}" />
</StackPanel>
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
Style="{StaticResource ModuleTitle}"
Text="{x:Static resx:ResUI.menuServers}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbRemarks}" />
<TextBox
x:Name="txtRemarks"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbCoreType}" />
<ComboBox
x:Name="cmbCoreType"
Grid.Row="2"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.TbCoreType}"
Style="{StaticResource DefComboBox}" />
<Grid
x:Name="gridPolicyGroup"
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbPolicyGroupType}" />
<ComboBox
x:Name="cmbPolicyGroupType"
Grid.Row="3"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.TbPolicyGroupType}"
Style="{StaticResource DefComboBox}" />
</Grid>
</Grid>
</Grid>
<TabControl>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}">
<DataGrid
x:Name="lstChild"
AutoGenerateColumns="False"
BorderThickness="1"
CanUserAddRows="False"
CanUserResizeRows="False"
CanUserSortColumns="False"
EnableRowVirtualization="True"
GridLinesVisibility="All"
HeadersVisibility="Column"
IsReadOnly="True"
Style="{StaticResource DefDataGrid}">
<DataGrid.ContextMenu>
<ContextMenu Style="{StaticResource DefContextMenu}">
<MenuItem
x:Name="menuAddChildServer"
Height="{StaticResource MenuItemHeight}"
Click="MenuAddChild_Click"
Header="{x:Static resx:ResUI.menuAddChildServer}" />
<MenuItem
x:Name="menuRemoveChildServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuRemoveChildServer}" />
<MenuItem
x:Name="menuSelectAllChild"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSelectAll}" />
<Separator />
<MenuItem
x:Name="menuMoveTop"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuMoveTop}" />
<MenuItem
x:Name="menuMoveUp"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuMoveUp}" />
<MenuItem
x:Name="menuMoveDown"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuMoveDown}" />
<MenuItem
x:Name="menuMoveBottom"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuMoveBottom}" />
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn
Width="150"
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Port}"
Header="{x:Static resx:ResUI.LvPort}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Network}"
Header="{x:Static resx:ResUI.LvTransportProtocol}" />
<DataGridTextColumn
Width="100"
Binding="{Binding StreamSecurity}"
Header="{x:Static resx:ResUI.LvTLS}" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
</TabControl>
</DockPanel>
</base:WindowBase>

View file

@ -0,0 +1,141 @@
using System.Windows;
using System.Windows.Input;
using ReactiveUI;
using System.Reactive.Disposables;
using DynamicData;
namespace v2rayN.Views;
public partial class AddGroupServerWindow
{
public AddGroupServerWindow(ProfileItem profileItem)
{
InitializeComponent();
this.Owner = Application.Current.MainWindow;
this.Loaded += Window_Loaded;
this.PreviewKeyDown += AddGroupServerWindow_PreviewKeyDown;
lstChild.SelectionChanged += LstChild_SelectionChanged;
ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Global.CoreTypes;
cmbPolicyGroupType.ItemsSource = new List<string>
{
ResUI.TbLeastPing,
ResUI.TbFallback,
ResUI.TbRandom,
ResUI.TbRoundRobin,
ResUI.TbLeastLoad,
};
switch (profileItem.ConfigType)
{
case EConfigType.PolicyGroup:
this.Title = ResUI.TbConfigTypePolicyGroup;
break;
case EConfigType.ProxyChain:
this.Title = ResUI.TbConfigTypeProxyChain;
gridPolicyGroup.Visibility = Visibility.Collapsed;
break;
}
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.Text).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
this.DialogResult = true;
break;
}
return await Task.FromResult(true);
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
txtRemarks.Focus();
}
private void AddGroupServerWindow_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (!lstChild.IsKeyboardFocusWithin)
return;
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
if (e.Key == Key.A)
{
lstChild.SelectAll();
}
}
else
{
if (e.Key == Key.T)
{
ViewModel?.MoveServer(EMove.Top);
}
else if (e.Key == Key.U)
{
ViewModel?.MoveServer(EMove.Up);
}
else if (e.Key == Key.D)
{
ViewModel?.MoveServer(EMove.Down);
}
else if (e.Key == Key.B)
{
ViewModel?.MoveServer(EMove.Bottom);
}
else if (e.Key == Key.Delete)
{
ViewModel?.ChildRemoveAsync();
}
}
}
private async void MenuAddChild_Click(object sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
if (ViewModel?.SelectedSource?.ConfigType == EConfigType.PolicyGroup)
{
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
}
else
{
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
}
selectWindow.AllowMultiSelect(true);
if (selectWindow.ShowDialog() == true)
{
var profiles = await selectWindow.ProfileItems;
ViewModel?.ChildItemsObs.AddRange(profiles);
}
}
private void LstChild_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (ViewModel != null)
{
ViewModel.SelectedChildren = lstChild.SelectedItems.Cast<ProfileItem>().ToList();
}
}
}

View file

@ -281,7 +281,7 @@
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbApplyProxyDomainsOnly}" Text="{x:Static resx:ResUI.TbFakeIPTips}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock

View file

@ -71,6 +71,14 @@
x:Name="menuAddCustomServer" x:Name="menuAddCustomServer"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddCustomServer}" /> Header="{x:Static resx:ResUI.menuAddCustomServer}" />
<MenuItem
x:Name="menuAddPolicyGroupServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddPolicyGroupServer}" />
<MenuItem
x:Name="menuAddProxyChainServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddProxyChainServer}" />
<Separator Margin="-40,5" /> <Separator Margin="-40,5" />
<MenuItem <MenuItem
x:Name="menuAddVmessServer" x:Name="menuAddVmessServer"

View file

@ -83,6 +83,8 @@ public partial class MainWindow
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables);
@ -193,6 +195,11 @@ public partial class MainWindow
return false; return false;
return (new AddServer2Window((ProfileItem)obj)).ShowDialog() ?? false; return (new AddServer2Window((ProfileItem)obj)).ShowDialog() ?? false;
case EViewAction.AddGroupServerWindow:
if (obj is null)
return false;
return (new AddGroupServerWindow((ProfileItem)obj)).ShowDialog() ?? false;
case EViewAction.DNSSettingWindow: case EViewAction.DNSSettingWindow:
return (new DNSSettingWindow().ShowDialog() ?? false); return (new DNSSettingWindow().ShowDialog() ?? false);

View file

@ -3,10 +3,8 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager; using ServiceLib.Manager;
using Splat;
using v2rayN.Base; using v2rayN.Base;
namespace v2rayN.Views; namespace v2rayN.Views;
@ -34,7 +32,6 @@ public partial class ProfilesSelectWindow
ViewModel = new ProfilesSelectViewModel(UpdateViewHandler); ViewModel = new ProfilesSelectViewModel(UpdateViewHandler);
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.OneWayBind(ViewModel, vm => vm.ProfileItems, v => v.lstProfiles.ItemsSource).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.ProfileItems, v => v.lstProfiles.ItemsSource).DisposeWith(disposables);
@ -44,6 +41,8 @@ public partial class ProfilesSelectWindow
this.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables);
}); });
WindowsUtils.SetDarkBorder(this, AppManager.Instance.Config.UiItem.CurrentTheme);
} }
public void AllowMultiSelect(bool allow) public void AllowMultiSelect(bool allow)
@ -190,5 +189,6 @@ public partial class ProfilesSelectWindow
// Trigger selection finalize when Confirm is clicked // Trigger selection finalize when Confirm is clicked
ViewModel?.SelectFinish(); ViewModel?.SelectFinish();
} }
#endregion Event #endregion Event
} }

View file

@ -125,28 +125,32 @@
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuShareServer}" /> Header="{x:Static resx:ResUI.menuShareServer}" />
<Separator /> <Separator />
<MenuItem Header="{x:Static resx:ResUI.menuSetDefaultMultipleServer}"> <MenuItem Header="{x:Static resx:ResUI.menuGenGroupMultipleServer}">
<MenuItem <MenuItem
x:Name="menuSetDefaultMultipleServerXrayRandom" x:Name="menuGenGroupMultipleServerXrayRandom"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayRandom}" /> Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRandom}" />
<MenuItem <MenuItem
x:Name="menuSetDefaultMultipleServerXrayRoundRobin" x:Name="menuGenGroupMultipleServerXrayRoundRobin"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayRoundRobin}" /> Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRoundRobin}" />
<MenuItem <MenuItem
x:Name="menuSetDefaultMultipleServerXrayLeastPing" x:Name="menuGenGroupMultipleServerXrayLeastPing"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayLeastPing}" /> Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastPing}" />
<MenuItem <MenuItem
x:Name="menuSetDefaultMultipleServerXrayLeastLoad" x:Name="menuGenGroupMultipleServerXrayLeastLoad"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerXrayLeastLoad}" /> Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastLoad}" />
<Separator /> <Separator />
<MenuItem <MenuItem
x:Name="menuSetDefaultMultipleServerSingBoxLeastPing" x:Name="menuGenGroupMultipleServerSingBoxLeastPing"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuSetDefaultMultipleServerSingBoxLeastPing}" /> Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxLeastPing}" />
<MenuItem
x:Name="menuGenGroupMultipleServerSingBoxFallback"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxFallback}" />
</MenuItem> </MenuItem>
<Separator /> <Separator />
<MenuItem <MenuItem

View file

@ -63,11 +63,12 @@ public partial class ProfilesView
this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRandomCmd, v => v.menuSetDefaultMultipleServerXrayRandom).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRoundRobinCmd, v => v.menuSetDefaultMultipleServerXrayRoundRobin).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastPingCmd, v => v.menuSetDefaultMultipleServerXrayLeastPing).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastLoadCmd, v => v.menuSetDefaultMultipleServerXrayLeastLoad).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerSingBoxLeastPingCmd, v => v.menuSetDefaultMultipleServerSingBoxLeastPing).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables);
//servers move //servers move
this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables);
@ -106,7 +107,6 @@ public partial class ProfilesView
}); });
RestoreUI(); RestoreUI();
ViewModel?.RefreshServers();
} }
#region Event #region Event
@ -152,6 +152,11 @@ public partial class ProfilesView
return false; return false;
return (new AddServer2Window((ProfileItem)obj)).ShowDialog() ?? false; return (new AddServer2Window((ProfileItem)obj)).ShowDialog() ?? false;
case EViewAction.AddGroupServerWindow:
if (obj is null)
return false;
return (new AddGroupServerWindow((ProfileItem)obj)).ShowDialog() ?? false;
case EViewAction.ShareServer: case EViewAction.ShareServer:
if (obj is null) if (obj is null)
return false; return false;

View file

@ -1,6 +1,5 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Windows; using System.Windows;
using System.Windows.Threading;
using ReactiveUI; using ReactiveUI;
using ServiceLib.Manager; using ServiceLib.Manager;

View file

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