mirror of
https://github.com/MaxiFan/TunnelX.git
synced 2026-05-17 21:14:37 +03:00
819 lines
27 KiB
C#
819 lines
27 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Net.NetworkInformation;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text.Json.Nodes;
|
|
using System.Windows;
|
|
using System.Windows.Input;
|
|
using Application = System.Windows.Application;
|
|
using System.Windows.Threading;
|
|
using AppTunnel.Models;
|
|
using AppTunnel.Services;
|
|
|
|
namespace AppTunnel.ViewModels;
|
|
|
|
public partial class MainViewModel : INotifyPropertyChanged
|
|
{
|
|
private readonly VpnService _vpnService = new();
|
|
private readonly TrafficRouterService _trafficRouter = new();
|
|
private readonly ProfileService _profileService = new();
|
|
private readonly HistoryService _historyService = new();
|
|
private readonly DispatcherTimer _timer;
|
|
private readonly DispatcherTimer _saveDebounceTimer;
|
|
private CancellationTokenSource? _connectionCts;
|
|
private DateTime _connectionStartTime;
|
|
|
|
public MainViewModel()
|
|
{
|
|
ConnectCommand = new RelayCommand(_ =>
|
|
{
|
|
// Fire-and-forget safety: catch exceptions to avoid unobserved task exceptions
|
|
_ = ToggleConnectionAsync().ContinueWith(t =>
|
|
{
|
|
if (t.IsFaulted)
|
|
Application.Current?.Dispatcher.Invoke(() =>
|
|
StatusText = $"خطا: {t.Exception?.InnerException?.Message}");
|
|
}, TaskScheduler.Default);
|
|
}, _ => !IsBusy);
|
|
AddAppCommand = new RelayCommand(_ => AddCustomApp());
|
|
RemoveAppCommand = new RelayCommand(RemoveApp);
|
|
ToggleAppCommand = new RelayCommand(ToggleApp);
|
|
RefreshAppsCommand = new RelayCommand(_ => LoadInstalledApps(), _ => !IsBusy);
|
|
|
|
// Profile commands
|
|
NewProfileCommand = new RelayCommand(_ => CreateNewProfile(), _ => !IsConnected);
|
|
DeleteProfileCommand = new RelayCommand(_ => DeleteCurrentProfile(), _ => !IsConnected && Profiles.Count > 1);
|
|
DuplicateProfileCommand = new RelayCommand(_ => DuplicateCurrentProfile(), _ => !IsConnected);
|
|
|
|
// History command
|
|
ClearHistoryCommand = new RelayCommand(_ => ClearHistory());
|
|
|
|
// Exclude list commands
|
|
AddExcludeCommand = new RelayCommand(_ => AddExcludedDestination());
|
|
RemoveExcludeCommand = new RelayCommand(RemoveExcludedDestination);
|
|
|
|
// Include list commands
|
|
AddIncludeCommand = new RelayCommand(_ => AddIncludedDestination());
|
|
RemoveIncludeCommand = new RelayCommand(RemoveIncludedDestination);
|
|
|
|
// Ping command
|
|
TogglePingCommand = new RelayCommand(_ => TogglePing(), _ => IsConnected);
|
|
TestServerPingCommand = new RelayCommand(_ => _ = TestServerPingAsync(), _ => !IsConnected && !IsTestingServerPing);
|
|
PasteConfigCommand = new RelayCommand(_ => PasteConfigFromClipboard(), _ => !IsConnected && CurrentTunnelType == TunnelType.V2Ray);
|
|
ClearConfigCommand = new RelayCommand(_ => SelectedV2RayConfig = "", _ => !IsConnected && CurrentTunnelType == TunnelType.V2Ray);
|
|
OpenGitHubCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.GitHubUrl));
|
|
OpenDonateCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.PayPalDonateUrl));
|
|
CopyDonationInfoCommand = new RelayCommand(_ => CopyDonationInfoToClipboard());
|
|
|
|
_trafficRouter.TrafficUpdated += OnTrafficUpdated;
|
|
|
|
// Timer for updating UI (duration, traffic stats)
|
|
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
|
_timer.Tick += (_, _) => UpdateTimerTick();
|
|
|
|
// Auto-save timer with debounce (save 1 second after last change)
|
|
_saveDebounceTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
|
_saveDebounceTimer.Tick += (_, _) =>
|
|
{
|
|
_saveDebounceTimer.Stop();
|
|
SaveCurrentProfileState();
|
|
SaveProfiles();
|
|
SaveStatusText = "ذخیره شد";
|
|
};
|
|
|
|
// Load saved profiles, global tunnel apps, global excludes, global includes, and history on startup
|
|
LoadProfiles();
|
|
LoadTunnelApps();
|
|
LoadExcludes();
|
|
LoadIncludes();
|
|
LoadHistory();
|
|
}
|
|
|
|
#region Properties
|
|
|
|
private string _serverAddress = "";
|
|
public string ServerAddress
|
|
{
|
|
get => _serverAddress;
|
|
set { _serverAddress = value; OnPropertyChanged(); UpdateConfigDiagnostics(); SaveCurrentState(); }
|
|
}
|
|
|
|
private string _username = "";
|
|
public string Username
|
|
{
|
|
get => _username;
|
|
set { _username = value; OnPropertyChanged(); SaveCurrentState(); }
|
|
}
|
|
|
|
private string _password = "";
|
|
public string Password
|
|
{
|
|
get => _password;
|
|
set { _password = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _preSharedKey = "";
|
|
public string PreSharedKey
|
|
{
|
|
get => _preSharedKey;
|
|
set { _preSharedKey = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private int _socks5Port = 1080;
|
|
public int Socks5Port
|
|
{
|
|
get => _socks5Port;
|
|
set
|
|
{
|
|
var normalized = value;
|
|
if (_socks5Port == normalized) return;
|
|
_socks5Port = normalized;
|
|
_trafficRouter.Socks5Port = normalized;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(Socks5PortText));
|
|
OnPropertyChanged(nameof(Socks5Info));
|
|
UpdateSocks5PortStatus();
|
|
SaveCurrentState();
|
|
}
|
|
}
|
|
|
|
public string Socks5PortText
|
|
{
|
|
get => _socks5Port.ToString();
|
|
set
|
|
{
|
|
if (int.TryParse((value ?? "").Trim(), out var port))
|
|
{
|
|
if (_socks5Port != port)
|
|
{
|
|
_socks5Port = port;
|
|
_trafficRouter.Socks5Port = port;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(Socks5Port));
|
|
OnPropertyChanged(nameof(Socks5Info));
|
|
UpdateSocks5PortStatus();
|
|
SaveCurrentState();
|
|
}
|
|
return;
|
|
}
|
|
|
|
Socks5PortStatusText = string.IsNullOrWhiteSpace(value)
|
|
? "پورت SOCKS5 را وارد کنید"
|
|
: "فقط عدد مجاز است";
|
|
}
|
|
}
|
|
|
|
private string _socks5PortStatusText = "";
|
|
public string Socks5PortStatusText
|
|
{
|
|
get => _socks5PortStatusText;
|
|
set { _socks5PortStatusText = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private bool _autoTuneMtu = true;
|
|
public bool AutoTuneMtu
|
|
{
|
|
get => _autoTuneMtu;
|
|
set
|
|
{
|
|
if (_autoTuneMtu == value) return;
|
|
_autoTuneMtu = value;
|
|
OnPropertyChanged();
|
|
SaveCurrentState();
|
|
}
|
|
}
|
|
|
|
private bool _isDnsOptimizationEnabled = true;
|
|
public bool IsDnsOptimizationEnabled
|
|
{
|
|
get => _isDnsOptimizationEnabled;
|
|
set
|
|
{
|
|
if (_isDnsOptimizationEnabled == value) return;
|
|
_isDnsOptimizationEnabled = value;
|
|
_trafficRouter.EnableDnsOptimization = value;
|
|
OnPropertyChanged();
|
|
SaveCurrentState();
|
|
}
|
|
}
|
|
|
|
private bool _isGameModeEnabled;
|
|
public bool IsGameModeEnabled
|
|
{
|
|
get => _isGameModeEnabled;
|
|
set
|
|
{
|
|
if (_isGameModeEnabled == value) return;
|
|
_isGameModeEnabled = value;
|
|
_trafficRouter.EnableGameMode = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(GameModeStatusText));
|
|
SaveCurrentState();
|
|
}
|
|
}
|
|
|
|
public string GameModeStatusText => IsGameModeEnabled
|
|
? "Game Mode فعال است: Route نگهداری طولانیتر، DNS سریعتر و DSCP برای بستههای بازی اعمال میشود."
|
|
: "Game Mode غیرفعال است: حالت متعادل برای مصرف عمومی.";
|
|
|
|
private bool _isBusy;
|
|
public bool IsBusy
|
|
{
|
|
get => _isBusy;
|
|
set { _isBusy = value; OnPropertyChanged(); OnPropertyChanged(nameof(ConnectButtonText)); }
|
|
}
|
|
|
|
private ConnectionState _connectionState = ConnectionState.Disconnected;
|
|
public ConnectionState ConnectionState
|
|
{
|
|
get => _connectionState;
|
|
set
|
|
{
|
|
_connectionState = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(IsConnected));
|
|
OnPropertyChanged(nameof(ConnectButtonText));
|
|
OnPropertyChanged(nameof(StatusColor));
|
|
OnPropertyChanged(nameof(StatusText));
|
|
RaiseHealthStatusChanged();
|
|
}
|
|
}
|
|
|
|
public bool IsConnected => _connectionState == ConnectionState.Connected;
|
|
|
|
/// <summary>App version read from a single app-wide source.</summary>
|
|
public string AppVersion => AppInfo.VersionText;
|
|
public string AppReleaseText => AppInfo.ReleaseText;
|
|
public string AppCreatorText => AppInfo.CreatorText;
|
|
public string AppGitHubUrl => AppInfo.GitHubUrl;
|
|
public string AppLicenseText => AppInfo.LicenseName;
|
|
public string DonatePayPalText => $"پیپل: {AppInfo.PayPalEmail}";
|
|
public string CryptoDonationText => AppInfo.CryptoDonationText;
|
|
|
|
public string ConnectButtonText => _connectionState switch
|
|
{
|
|
ConnectionState.Disconnected => "🔌 اتصال",
|
|
ConnectionState.Connecting => "❌ لغو اتصال",
|
|
ConnectionState.Connected => "🔴 قطع اتصال",
|
|
ConnectionState.Disconnecting => "⏳ در حال قطع...",
|
|
ConnectionState.Error => "🔌 اتصال مجدد",
|
|
_ => "اتصال"
|
|
};
|
|
|
|
public string StatusColor => _connectionState switch
|
|
{
|
|
ConnectionState.Connected => "#4CAF50",
|
|
ConnectionState.Connecting or ConnectionState.Disconnecting => "#E07820",
|
|
ConnectionState.Error => "#E05252",
|
|
_ => "#666666"
|
|
};
|
|
|
|
private string _statusText = "آماده اتصال";
|
|
public string StatusText
|
|
{
|
|
get => _statusText;
|
|
set { _statusText = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors SelectedProfile.TunnelType as a direct ViewModel property so XAML
|
|
/// visibility bindings update reliably when the selected profile changes.
|
|
/// Setter also writes back to the active profile.
|
|
/// </summary>
|
|
private TunnelType _currentTunnelType = TunnelType.L2tpIpsec;
|
|
public TunnelType CurrentTunnelType
|
|
{
|
|
get => _currentTunnelType;
|
|
set
|
|
{
|
|
if (_currentTunnelType == value) return;
|
|
_currentTunnelType = value;
|
|
OnPropertyChanged();
|
|
if (_selectedProfile != null)
|
|
_selectedProfile.TunnelType = value;
|
|
UpdateConfigDiagnostics();
|
|
RaiseHealthStatusChanged();
|
|
SaveCurrentState();
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
}
|
|
|
|
private string _selectedV2RayConfig = "";
|
|
public string SelectedV2RayConfig
|
|
{
|
|
get => _selectedV2RayConfig;
|
|
set
|
|
{
|
|
if (_selectedV2RayConfig == value) return;
|
|
_selectedV2RayConfig = value;
|
|
if (_selectedProfile != null)
|
|
_selectedProfile.V2RayConfig = value;
|
|
OnPropertyChanged();
|
|
TryAutoNameProfileFromConfig(value);
|
|
UpdateConfigDiagnostics();
|
|
RaiseHealthStatusChanged();
|
|
SaveCurrentState();
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
}
|
|
|
|
private string _configCoreHint = "";
|
|
public string ConfigCoreHint
|
|
{
|
|
get => _configCoreHint;
|
|
set { _configCoreHint = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _configValidationText = "";
|
|
public string ConfigValidationText
|
|
{
|
|
get => _configValidationText;
|
|
set { _configValidationText = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _saveStatusText = "";
|
|
public string SaveStatusText
|
|
{
|
|
get => _saveStatusText;
|
|
set { _saveStatusText = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _connectionDuration = "--:--:--";
|
|
public string ConnectionDuration
|
|
{
|
|
get => _connectionDuration;
|
|
set { _connectionDuration = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _vpnIp = "";
|
|
public string VpnIp
|
|
{
|
|
get => _vpnIp;
|
|
set { _vpnIp = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _vpnAdapterName = "";
|
|
public string VpnAdapterName
|
|
{
|
|
get => _vpnAdapterName;
|
|
set { _vpnAdapterName = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private bool _isFullRouteEnabled;
|
|
public bool IsFullRouteEnabled
|
|
{
|
|
get => _isFullRouteEnabled;
|
|
set
|
|
{
|
|
if (_isFullRouteEnabled == value) return;
|
|
|
|
if (IsConnected)
|
|
{
|
|
var ok = _trafficRouter.SetFullRouteEnabled(value);
|
|
if (!ok)
|
|
{
|
|
OnPropertyChanged();
|
|
StatusText = "تغییر حالت Full Route ناموفق بود";
|
|
return;
|
|
}
|
|
}
|
|
|
|
_isFullRouteEnabled = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(FullRouteStatusText));
|
|
OnPropertyChanged(nameof(RouteModeTitle));
|
|
OnPropertyChanged(nameof(RouteModeDescription));
|
|
RaiseHealthStatusChanged();
|
|
}
|
|
}
|
|
|
|
public string FullRouteStatusText => _isFullRouteEnabled
|
|
? "Full Route فعال است؛ کل سیستم از تونل عبور میکند"
|
|
: "Split فعال است؛ فقط برنامهها و مقصدهای انتخابی از تونل عبور میکنند";
|
|
|
|
public string RouteModeTitle => IsFullRouteEnabled ? "حالت کل سیستم" : "حالت انتخابی";
|
|
public string RouteModeDescription => IsFullRouteEnabled
|
|
? "همه برنامهها از تونل عبور میکنند. برای تست یا وقتی میخواهید کل ویندوز پشت VPN باشد مناسب است."
|
|
: "فقط برنامههای فعال در تب برنامهها و مقصدهای لزومی از تونل عبور میکنند. حالت پیشنهادی برای مصرف کمتر و کنترل بهتر.";
|
|
|
|
public string HeaderCoreText => $"Core: {ActiveCoreName}";
|
|
public string HeaderRouteText => IsFullRouteEnabled ? "Mode: Full" : "Mode: Split";
|
|
public string HeaderLeakText => IsConnected
|
|
? (_trafficRouter.LeakCount == 0
|
|
? (_trafficRouter.LeakBlockedCount == 0
|
|
? "Leak: OK"
|
|
: $"Leak: OK (blocked {_trafficRouter.LeakBlockedCount})")
|
|
: $"Leak: {_trafficRouter.LeakCount}")
|
|
: "Leak: -";
|
|
public string HeaderLeakColor => !IsConnected
|
|
? "#6CCB5F"
|
|
: _trafficRouter.LeakCount > 0
|
|
? "#E05252"
|
|
: (_trafficRouter.LeakBlockedCount > 0 ? "#E07820" : "#6CCB5F");
|
|
|
|
public string HealthLeakText => IsConnected
|
|
? (_trafficRouter.LeakCount == 0
|
|
? (_trafficRouter.LeakBlockedCount == 0
|
|
? "0 leak"
|
|
: $"0 leak / {_trafficRouter.LeakBlockedCount} blocked")
|
|
: $"{_trafficRouter.LeakCount} leak")
|
|
: "-";
|
|
public string HealthDnsText => IsConnected
|
|
? (_trafficRouter.DnsRedirectCount > 0 ? $"DNS tunnel {_trafficRouter.DnsRedirectCount}" : "DNS ready")
|
|
: "-";
|
|
public string HealthIpv6Text => IsConnected
|
|
? $"IPv6 blocked {_trafficRouter.Ipv6BlockedCount}"
|
|
: "-";
|
|
public string HealthRoutesText => IsConnected
|
|
? $"routes {_trafficRouter.ActiveRouteCount}/{_trafficRouter.RouteFailureCount} fail"
|
|
: "-";
|
|
|
|
private string ActiveCoreName => CurrentTunnelType switch
|
|
{
|
|
TunnelType.L2tpIpsec => "L2TP",
|
|
TunnelType.V2Ray when TunnelProviderFactory.RequiresXray(SelectedV2RayConfig) => "Xray",
|
|
TunnelType.V2Ray => "sing-box",
|
|
_ => "-"
|
|
};
|
|
|
|
private string _totalTraffic = "0 B";
|
|
public string TotalTraffic
|
|
{
|
|
get => _totalTraffic;
|
|
set { _totalTraffic = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _appTrafficTotal = "0 B";
|
|
public string AppTrafficTotal
|
|
{
|
|
get => _appTrafficTotal;
|
|
set { _appTrafficTotal = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _otherTunnelTraffic = "0 B";
|
|
public string OtherTunnelTraffic
|
|
{
|
|
get => _otherTunnelTraffic;
|
|
set { _otherTunnelTraffic = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _directTraffic = "0 B";
|
|
public string DirectTraffic
|
|
{
|
|
get => _directTraffic;
|
|
set { _directTraffic = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _pingTarget = "8.8.8.8";
|
|
public string PingTarget
|
|
{
|
|
get => _pingTarget;
|
|
set { _pingTarget = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private bool _isPinging;
|
|
public bool IsPinging
|
|
{
|
|
get => _isPinging;
|
|
set { _isPinging = value; OnPropertyChanged(); OnPropertyChanged(nameof(PingButtonText)); }
|
|
}
|
|
|
|
public string PingButtonText => _isPinging ? "⏹ توقف" : "▶ شروع";
|
|
|
|
private string _pingResult = "";
|
|
public string PingResult
|
|
{
|
|
get => _pingResult;
|
|
set { _pingResult = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private bool _isTestingServerPing;
|
|
public bool IsTestingServerPing
|
|
{
|
|
get => _isTestingServerPing;
|
|
set
|
|
{
|
|
_isTestingServerPing = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(ServerPingButtonText));
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
}
|
|
|
|
public string ServerPingButtonText => _isTestingServerPing ? "در حال تست..." : "تست سرور";
|
|
|
|
private string _serverPingResult = "";
|
|
public string ServerPingResult
|
|
{
|
|
get => _serverPingResult;
|
|
set { _serverPingResult = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
public int EnabledAppsCount => TunnelApps.Count(a => a.IsEnabled);
|
|
|
|
public string Socks5Info => $"127.0.0.1:{_trafficRouter.Socks5Port}";
|
|
|
|
// Exclude list
|
|
public ObservableCollection<string> ExcludedDestinations { get; } = new();
|
|
|
|
private string _excludeInput = "";
|
|
public string ExcludeInput
|
|
{
|
|
get => _excludeInput;
|
|
set { _excludeInput = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
// Include list
|
|
public ObservableCollection<string> IncludedDestinations { get; } = new();
|
|
|
|
private string _includeInput = "";
|
|
public string IncludeInput
|
|
{
|
|
get => _includeInput;
|
|
set { _includeInput = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
public ObservableCollection<AppItemViewModel> TunnelApps { get; } = new();
|
|
|
|
public ObservableCollection<AppItemViewModel> AvailableApps { get; } = new();
|
|
|
|
private AppItemViewModel? _selectedApp;
|
|
public AppItemViewModel? SelectedApp
|
|
{
|
|
get => _selectedApp;
|
|
set { _selectedApp = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private string _searchText = "";
|
|
public string SearchText
|
|
{
|
|
get => _searchText;
|
|
set
|
|
{
|
|
_searchText = value;
|
|
OnPropertyChanged();
|
|
FilterAvailableApps();
|
|
}
|
|
}
|
|
|
|
private string _tunnelSearchText = "";
|
|
public string TunnelSearchText
|
|
{
|
|
get => _tunnelSearchText;
|
|
set
|
|
{
|
|
_tunnelSearchText = value;
|
|
OnPropertyChanged();
|
|
FilterTunnelApps();
|
|
}
|
|
}
|
|
|
|
private ObservableCollection<AppItemViewModel> _filteredAvailableApps = new();
|
|
public ObservableCollection<AppItemViewModel> FilteredAvailableApps
|
|
{
|
|
get => _filteredAvailableApps;
|
|
set { _filteredAvailableApps = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
private ObservableCollection<AppItemViewModel> _filteredTunnelApps = new();
|
|
public ObservableCollection<AppItemViewModel> FilteredTunnelApps
|
|
{
|
|
get => _filteredTunnelApps;
|
|
set { _filteredTunnelApps = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
// Connection History
|
|
public ObservableCollection<ConnectionHistoryEntry> ConnectionHistory { get; } = new();
|
|
|
|
public string TotalHistoryData
|
|
{
|
|
get
|
|
{
|
|
long total = ConnectionHistory.Sum(e => e.BytesSent + e.BytesReceived);
|
|
return FormatBytes(total);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Commands
|
|
|
|
public ICommand ConnectCommand { get; }
|
|
public ICommand AddAppCommand { get; }
|
|
public ICommand RemoveAppCommand { get; }
|
|
public ICommand ToggleAppCommand { get; }
|
|
public ICommand RefreshAppsCommand { get; }
|
|
public ICommand NewProfileCommand { get; }
|
|
public ICommand DeleteProfileCommand { get; }
|
|
public ICommand DuplicateProfileCommand { get; }
|
|
public ICommand ClearHistoryCommand { get; }
|
|
public ICommand AddExcludeCommand { get; }
|
|
public ICommand RemoveExcludeCommand { get; }
|
|
public ICommand AddIncludeCommand { get; }
|
|
public ICommand RemoveIncludeCommand { get; }
|
|
public ICommand TogglePingCommand { get; }
|
|
public ICommand TestServerPingCommand { get; }
|
|
public ICommand PasteConfigCommand { get; }
|
|
public ICommand ClearConfigCommand { get; }
|
|
public ICommand OpenGitHubCommand { get; }
|
|
public ICommand OpenDonateCommand { get; }
|
|
public ICommand CopyDonationInfoCommand { get; }
|
|
|
|
#endregion
|
|
|
|
#region Config UX
|
|
|
|
private static void OpenExternalLink(string url)
|
|
{
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = url,
|
|
UseShellExecute = true
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Warning($"[UI] Open link failed: {url} — {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void CopyDonationInfoToClipboard()
|
|
{
|
|
try
|
|
{
|
|
var text =
|
|
$"{AppInfo.AppName} - حمایت از پروژه\n" +
|
|
$"PayPal: {AppInfo.PayPalEmail}\n" +
|
|
$"PayPal link: {AppInfo.PayPalDonateUrl}\n\n" +
|
|
AppInfo.CryptoDonationText;
|
|
System.Windows.Clipboard.SetText(text);
|
|
Logger.Info("[UI] Donation info copied to clipboard");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Warning($"[UI] Copy donation info failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void PasteConfigFromClipboard()
|
|
{
|
|
try
|
|
{
|
|
if (System.Windows.Clipboard.ContainsText())
|
|
SelectedV2RayConfig = System.Windows.Clipboard.GetText().Trim();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ConfigValidationText = $"خواندن کلیپبورد ناموفق بود: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private void UpdateConfigDiagnostics()
|
|
{
|
|
if (CurrentTunnelType != TunnelType.V2Ray)
|
|
{
|
|
ConfigCoreHint = "L2TP/IPsec";
|
|
ConfigValidationText = string.IsNullOrWhiteSpace(ServerAddress)
|
|
? "آدرس سرور L2TP را وارد کنید"
|
|
: "آماده تست و اتصال";
|
|
return;
|
|
}
|
|
|
|
var config = SelectedV2RayConfig.Trim();
|
|
if (string.IsNullOrWhiteSpace(config))
|
|
{
|
|
ConfigCoreHint = "منتظر کانفیگ";
|
|
ConfigValidationText = "کانفیگ V2Ray/Xray را وارد یا پیست کنید";
|
|
return;
|
|
}
|
|
|
|
ConfigCoreHint = TunnelProviderFactory.RequiresXray(config)
|
|
? "هسته: Xray-core"
|
|
: "هسته: sing-box";
|
|
|
|
ConfigValidationText = TryExtractProxyEndpoint(config, out var server, out var port, out var error)
|
|
? $"سرور: {server}:{port}"
|
|
: error;
|
|
}
|
|
|
|
private bool ValidateSocks5Port(out string message)
|
|
{
|
|
var port = _socks5Port;
|
|
if (port < 1024 || port > 65535)
|
|
{
|
|
message = "پورت باید بین 1024 تا 65535 باشد";
|
|
return false;
|
|
}
|
|
|
|
var blocked = new HashSet<int>
|
|
{
|
|
1433, 1521, 1723, 1900, 2049, 2080, 2375, 2376,
|
|
3000, 3306, 3389, 5000, 5432, 5353, 5355, 5900,
|
|
6379, 8000, 8080, 8443, 8888, 9000, 9090, 27017
|
|
};
|
|
|
|
if (blocked.Contains(port))
|
|
{
|
|
message = "این پورت رایج/حساس است؛ یک پورت آزاد مثل 1080، 1081 یا 18080 انتخاب کنید";
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var used = IPGlobalProperties.GetIPGlobalProperties()
|
|
.GetActiveTcpListeners()
|
|
.Any(p => p.Port == port);
|
|
if (used && !IsConnected)
|
|
{
|
|
message = "این پورت همین حالا توسط برنامه دیگری استفاده میشود";
|
|
return false;
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
message = "پورت SOCKS5 داخلی آماده است";
|
|
return true;
|
|
}
|
|
|
|
private void UpdateSocks5PortStatus()
|
|
{
|
|
ValidateSocks5Port(out var message);
|
|
Socks5PortStatusText = message;
|
|
}
|
|
|
|
private void TryAutoNameProfileFromConfig(string config)
|
|
{
|
|
if (_selectedProfile == null || string.IsNullOrWhiteSpace(config))
|
|
return;
|
|
|
|
var currentName = _selectedProfile.Name?.Trim() ?? "";
|
|
var canRename = string.IsNullOrWhiteSpace(currentName) ||
|
|
currentName.StartsWith("پروفایل ", StringComparison.OrdinalIgnoreCase) ||
|
|
currentName == "پروفایل جدید" ||
|
|
currentName == "پیشفرض";
|
|
if (!canRename)
|
|
return;
|
|
|
|
var remark = ExtractConfigRemark(config);
|
|
if (string.IsNullOrWhiteSpace(remark))
|
|
return;
|
|
|
|
_selectedProfile.Name = remark;
|
|
OnPropertyChanged(nameof(SelectedProfileName));
|
|
}
|
|
|
|
private static string ExtractConfigRemark(string config)
|
|
{
|
|
try
|
|
{
|
|
if (config.Contains('#'))
|
|
return Uri.UnescapeDataString(config[(config.IndexOf('#') + 1)..]).Trim();
|
|
|
|
if (config.StartsWith("vmess://", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var json = TryBase64DecodeConfig(config["vmess://".Length..]);
|
|
return JsonNode.Parse(json)?["ps"]?.GetValue<string>()?.Trim() ?? "";
|
|
}
|
|
|
|
if (config.StartsWith("{"))
|
|
{
|
|
var root = JsonNode.Parse(config);
|
|
return root?["remarks"]?.GetValue<string>()?.Trim() ??
|
|
root?["name"]?.GetValue<string>()?.Trim() ??
|
|
root?["outbounds"]?[0]?["tag"]?.GetValue<string>()?.Trim() ?? "";
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
return "";
|
|
}
|
|
|
|
private void RaiseHealthStatusChanged()
|
|
{
|
|
OnPropertyChanged(nameof(HeaderCoreText));
|
|
OnPropertyChanged(nameof(HeaderRouteText));
|
|
OnPropertyChanged(nameof(HeaderLeakText));
|
|
OnPropertyChanged(nameof(HeaderLeakColor));
|
|
OnPropertyChanged(nameof(RouteModeTitle));
|
|
OnPropertyChanged(nameof(RouteModeDescription));
|
|
OnPropertyChanged(nameof(HealthLeakText));
|
|
OnPropertyChanged(nameof(HealthDnsText));
|
|
OnPropertyChanged(nameof(HealthIpv6Text));
|
|
OnPropertyChanged(nameof(HealthRoutesText));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region INotifyPropertyChanged
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
|
|
#endregion
|
|
}
|