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; /// App version read from a single app-wide source. 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(); } } /// /// 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. /// 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: Protected {_trafficRouter.LeakBlockedCount}") : $"Leak: {_trafficRouter.LeakCount}") : "Leak: -"; public string HeaderLeakColor => !IsConnected ? "#6CCB5F" : _trafficRouter.LeakCount > 0 ? "#E05252" : "#6CCB5F"; public string HealthLeakText => IsConnected ? (_trafficRouter.LeakCount == 0 ? (_trafficRouter.LeakBlockedCount == 0 ? "0 leak" : $"0 leak / {_trafficRouter.LeakBlockedCount} protected") : $"{_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 ExcludedDestinations { get; } = new(); private string _excludeInput = ""; public string ExcludeInput { get => _excludeInput; set { _excludeInput = value; OnPropertyChanged(); } } // Include list public ObservableCollection IncludedDestinations { get; } = new(); private string _includeInput = ""; public string IncludeInput { get => _includeInput; set { _includeInput = value; OnPropertyChanged(); } } public ObservableCollection TunnelApps { get; } = new(); public ObservableCollection 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 _filteredAvailableApps = new(); public ObservableCollection FilteredAvailableApps { get => _filteredAvailableApps; set { _filteredAvailableApps = value; OnPropertyChanged(); } } private ObservableCollection _filteredTunnelApps = new(); public ObservableCollection FilteredTunnelApps { get => _filteredTunnelApps; set { _filteredTunnelApps = value; OnPropertyChanged(); } } // Connection History public ObservableCollection 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 { 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()?.Trim() ?? ""; } if (config.StartsWith("{")) { var root = JsonNode.Parse(config); return root?["remarks"]?.GetValue()?.Trim() ?? root?["name"]?.GetValue()?.Trim() ?? root?["outbounds"]?[0]?["tag"]?.GetValue()?.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 }