mirror of
https://github.com/MaxiFan/TunnelX.git
synced 2026-05-17 21:14:37 +03:00
8283b9d6d1
Co-authored-by: Cursor <cursoragent@cursor.com>
436 lines
16 KiB
C#
436 lines
16 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using AppTunnel.Models;
|
|
using AppTunnel.Services;
|
|
using AppTunnel.Views;
|
|
|
|
namespace AppTunnel.ViewModels;
|
|
|
|
public partial class MainViewModel
|
|
{
|
|
#region Profile Management
|
|
|
|
public ObservableCollection<ConnectionProfile> Profiles { get; } = new();
|
|
|
|
private ConnectionProfile? _selectedProfile;
|
|
public ConnectionProfile? SelectedProfile
|
|
{
|
|
get => _selectedProfile;
|
|
set
|
|
{
|
|
if (_selectedProfile == value) return;
|
|
_saveDebounceTimer.Stop();
|
|
SaveCurrentProfileState();
|
|
_selectedProfile = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(SelectedProfileName));
|
|
RaiseProfileCardChanged();
|
|
if (value != null)
|
|
LoadProfileIntoUi(value);
|
|
SaveProfiles();
|
|
}
|
|
}
|
|
|
|
public string SelectedProfileName => _selectedProfile?.Name ?? "";
|
|
public string ProfileCountText => Profiles.Count == 1
|
|
? "۱ پروفایل ذخیرهشده"
|
|
: $"{Profiles.Count} پروفایل ذخیرهشده";
|
|
|
|
public string ActiveProfileTypeText => CurrentTunnelType switch
|
|
{
|
|
TunnelType.L2tpIpsec => "L2TP/IPsec",
|
|
TunnelType.V2Ray => TunnelProviderFactory.RequiresXray(SelectedV2RayConfig) ? "V2Ray / Xray" : "V2Ray / sing-box",
|
|
TunnelType.OpenVpn => "OpenVPN",
|
|
TunnelType.SocksProxy => ProxyProtocol == ProxyProtocol.Http ? "HTTP Proxy" : "SOCKS5 Proxy",
|
|
_ => "نوع اتصال نامشخص"
|
|
};
|
|
|
|
public string ActiveProfileEndpointText => CurrentTunnelType switch
|
|
{
|
|
TunnelType.L2tpIpsec => string.IsNullOrWhiteSpace(ServerAddress) ? "آدرس سرور هنوز وارد نشده" : ServerAddress.Trim(),
|
|
TunnelType.V2Ray => TryExtractProxyEndpoint(SelectedV2RayConfig.Trim(), out var server, out var port, out _)
|
|
? $"{server}:{port}"
|
|
: "کانفیگ V2Ray/Xray آماده نمایش نیست",
|
|
TunnelType.OpenVpn => string.IsNullOrWhiteSpace(SelectedOpenVpnConfigPath)
|
|
? "فایل OpenVPN انتخاب نشده"
|
|
: Path.GetFileName(SelectedOpenVpnConfigPath),
|
|
TunnelType.SocksProxy => string.IsNullOrWhiteSpace(ProxyServerAddress)
|
|
? "آدرس پراکسی هنوز وارد نشده"
|
|
: $"{ProxyServerAddress.Trim()}:{ProxyPort}",
|
|
_ => ""
|
|
};
|
|
|
|
public string ProfileSaveHintText => string.IsNullOrWhiteSpace(SaveStatusText)
|
|
? "تغییرات این پروفایل بهصورت خودکار ذخیره میشود"
|
|
: SaveStatusText;
|
|
|
|
/// <summary>
|
|
/// Event to notify code-behind to update PasswordBox controls.
|
|
/// </summary>
|
|
public event Action<string, string>? PasswordChanged;
|
|
public event Action<string>? OpenVpnPasswordChanged;
|
|
public event Action<string>? ProxyPasswordChanged;
|
|
|
|
private void LoadProfiles()
|
|
{
|
|
var profiles = _profileService.LoadProfiles();
|
|
Profiles.Clear();
|
|
|
|
if (profiles.Count == 0)
|
|
profiles.Add(new ConnectionProfile { Name = "پیشفرض" });
|
|
|
|
foreach (var p in profiles.OrderByDescending(p => p.LastUsedAt))
|
|
Profiles.Add(p);
|
|
OnPropertyChanged(nameof(ProfileCountText));
|
|
|
|
_selectedProfile = Profiles[0];
|
|
OnPropertyChanged(nameof(SelectedProfile));
|
|
OnPropertyChanged(nameof(SelectedProfileName));
|
|
LoadProfileIntoUi(Profiles[0]);
|
|
}
|
|
|
|
private void SaveProfiles() => _profileService.SaveProfiles(Profiles);
|
|
|
|
private void LoadExcludes()
|
|
{
|
|
var excludes = _profileService.LoadExcludes();
|
|
ExcludedDestinations.Clear();
|
|
foreach (var e in excludes)
|
|
ExcludedDestinations.Add(e);
|
|
}
|
|
|
|
private void SaveExcludes() => _profileService.SaveExcludes(ExcludedDestinations);
|
|
|
|
private void LoadIncludes()
|
|
{
|
|
var includes = _profileService.LoadIncludes();
|
|
IncludedDestinations.Clear();
|
|
foreach (var i in includes)
|
|
IncludedDestinations.Add(i);
|
|
}
|
|
|
|
private void SaveIncludes() => _profileService.SaveIncludes(IncludedDestinations);
|
|
|
|
private void LoadTunnelApps()
|
|
{
|
|
var apps = _profileService.LoadTunnelApps();
|
|
TunnelApps.Clear();
|
|
foreach (var app in apps)
|
|
{
|
|
var icon = AppDiscoveryService.ExtractIcon(app.ExecutablePath);
|
|
TunnelApps.Add(new AppItemViewModel(new TunnelApp
|
|
{
|
|
DisplayName = app.DisplayName,
|
|
ExecutablePath = app.ExecutablePath,
|
|
ExecutableName = app.ExecutableName,
|
|
Icon = icon,
|
|
IsEnabled = app.IsEnabled
|
|
}) { IsEnabled = app.IsEnabled });
|
|
}
|
|
RefreshAllFilters();
|
|
OnPropertyChanged(nameof(EnabledAppsCount));
|
|
}
|
|
|
|
private void SaveTunnelApps() => _profileService.SaveTunnelApps(
|
|
TunnelApps.Select(a => new ProfileApp
|
|
{
|
|
DisplayName = a.DisplayName,
|
|
ExecutablePath = a.ExecutablePath,
|
|
ExecutableName = a.ExecutableName,
|
|
IsEnabled = a.IsEnabled
|
|
}));
|
|
|
|
private void SaveCurrentProfileState()
|
|
{
|
|
if (_selectedProfile == null) return;
|
|
_selectedProfile.ServerAddress = ServerAddress;
|
|
_selectedProfile.Username = Username;
|
|
_selectedProfile.Password = Password;
|
|
_selectedProfile.PreSharedKey = PreSharedKey;
|
|
_selectedProfile.TunnelType = _currentTunnelType;
|
|
_selectedProfile.V2RayConfig = SelectedV2RayConfig;
|
|
_selectedProfile.OpenVpnConfig = SelectedOpenVpnConfig;
|
|
_selectedProfile.OpenVpnConfigPath = SelectedOpenVpnConfigPath;
|
|
_selectedProfile.OpenVpnUsername = OpenVpnUsername;
|
|
_selectedProfile.OpenVpnPassword = OpenVpnPassword;
|
|
_selectedProfile.ProxyProtocol = ProxyProtocol;
|
|
_selectedProfile.ProxyServerAddress = ProxyServerAddress;
|
|
_selectedProfile.ProxyPort = ProxyPort;
|
|
_selectedProfile.ProxyUsername = ProxyUsername;
|
|
_selectedProfile.ProxyPassword = ProxyPassword;
|
|
_selectedProfile.MixedProxyPort = MixedProxyPort;
|
|
_selectedProfile.AutoTuneMtu = AutoTuneMtu;
|
|
_selectedProfile.EnableDnsOptimization = IsDnsOptimizationEnabled;
|
|
_selectedProfile.EnableGameMode = IsGameModeEnabled;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called from code-behind after PasswordBox changes to persist state.
|
|
/// Uses debounce to avoid saving on every keystroke.
|
|
/// </summary>
|
|
public void SaveCurrentState()
|
|
{
|
|
if (_isLoadingProfile) return;
|
|
SaveStatusText = "در حال ذخیره...";
|
|
_saveDebounceTimer.Stop();
|
|
_saveDebounceTimer.Start(); // Restart timer - will save after 1 second of no changes
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force immediate save without debounce (for app shutdown).
|
|
/// </summary>
|
|
public void ForceSave()
|
|
{
|
|
_saveDebounceTimer.Stop();
|
|
SaveCurrentProfileState();
|
|
SaveProfiles();
|
|
SaveStatusText = "ذخیره شد";
|
|
}
|
|
|
|
private void LoadProfileIntoUi(ConnectionProfile profile)
|
|
{
|
|
_isLoadingProfile = true;
|
|
try
|
|
{
|
|
ServerAddress = profile.ServerAddress;
|
|
Username = profile.Username;
|
|
Password = profile.Password;
|
|
PreSharedKey = profile.PreSharedKey;
|
|
_mixedProxyPort = profile.MixedProxyPort > 0 ? profile.MixedProxyPort : 1080;
|
|
_trafficRouter.Socks5Port = _mixedProxyPort;
|
|
OnPropertyChanged(nameof(MixedProxyPort));
|
|
OnPropertyChanged(nameof(MixedProxyPortText));
|
|
OnPropertyChanged(nameof(MixedProxyInfo));
|
|
UpdateMixedProxyPortStatus();
|
|
_autoTuneMtu = profile.AutoTuneMtu;
|
|
_isDnsOptimizationEnabled = profile.EnableDnsOptimization;
|
|
_isGameModeEnabled = profile.EnableGameMode;
|
|
_trafficRouter.EnableDnsOptimization = _isDnsOptimizationEnabled;
|
|
_trafficRouter.EnableGameMode = _isGameModeEnabled;
|
|
OnPropertyChanged(nameof(AutoTuneMtu));
|
|
OnPropertyChanged(nameof(IsDnsOptimizationEnabled));
|
|
OnPropertyChanged(nameof(IsGameModeEnabled));
|
|
OnPropertyChanged(nameof(GameModeStatusText));
|
|
// Use the field directly to avoid writing back to the old profile
|
|
// while the new profile is being loaded.
|
|
_currentTunnelType = profile.TunnelType;
|
|
_selectedV2RayConfig = profile.V2RayConfig;
|
|
_selectedOpenVpnConfig = profile.OpenVpnConfig;
|
|
_selectedOpenVpnConfigPath = profile.OpenVpnConfigPath;
|
|
_openVpnUsername = profile.OpenVpnUsername;
|
|
_openVpnPassword = profile.OpenVpnPassword;
|
|
_proxyProtocol = profile.ProxyProtocol;
|
|
_proxyServerAddress = profile.ProxyServerAddress;
|
|
_proxyPort = profile.ProxyPort > 0 ? profile.ProxyPort : 1080;
|
|
_proxyUsername = profile.ProxyUsername;
|
|
_proxyPassword = profile.ProxyPassword;
|
|
OnPropertyChanged(nameof(CurrentTunnelType));
|
|
OnPropertyChanged(nameof(ConnectedBadgeText));
|
|
OnPropertyChanged(nameof(SelectedV2RayConfig));
|
|
OnPropertyChanged(nameof(SelectedOpenVpnConfig));
|
|
OnPropertyChanged(nameof(SelectedOpenVpnConfigPath));
|
|
OnPropertyChanged(nameof(OpenVpnUsername));
|
|
OnPropertyChanged(nameof(ProxyProtocol));
|
|
OnPropertyChanged(nameof(ProxyServerAddress));
|
|
OnPropertyChanged(nameof(ProxyPort));
|
|
OnPropertyChanged(nameof(ProxyPortText));
|
|
OnPropertyChanged(nameof(ProxyUsername));
|
|
UpdateConfigDiagnostics();
|
|
RaiseProfileCardChanged();
|
|
|
|
PasswordChanged?.Invoke(profile.Password, profile.PreSharedKey);
|
|
OpenVpnPasswordChanged?.Invoke(profile.OpenVpnPassword);
|
|
ProxyPasswordChanged?.Invoke(profile.ProxyPassword);
|
|
}
|
|
finally
|
|
{
|
|
_isLoadingProfile = false;
|
|
}
|
|
}
|
|
|
|
private void CreateNewProfile()
|
|
{
|
|
SaveCurrentProfileState();
|
|
var profile = new ConnectionProfile
|
|
{
|
|
Name = $"پروفایل {Profiles.Count + 1}",
|
|
MixedProxyPort = MixedProxyPort,
|
|
AutoTuneMtu = AutoTuneMtu,
|
|
EnableDnsOptimization = IsDnsOptimizationEnabled,
|
|
EnableGameMode = IsGameModeEnabled
|
|
};
|
|
|
|
if (ProfileEditorDialog.Show(profile, "افزودن کانفیگ جدید", System.Windows.Application.Current.MainWindow) != true)
|
|
return;
|
|
|
|
Profiles.Add(profile);
|
|
OnPropertyChanged(nameof(ProfileCountText));
|
|
SelectedProfile = profile;
|
|
SaveProfiles();
|
|
}
|
|
|
|
private void DuplicateCurrentProfile(object? parameter = null)
|
|
{
|
|
var source = parameter as ConnectionProfile ?? _selectedProfile;
|
|
if (source == null) return;
|
|
SaveCurrentProfileState();
|
|
|
|
var clone = CloneProfile(source);
|
|
clone.Name = $"{source.Name} (کپی)";
|
|
|
|
if (ProfileEditorDialog.Show(clone, "کپی پروفایل", System.Windows.Application.Current.MainWindow) != true)
|
|
return;
|
|
|
|
Profiles.Add(clone);
|
|
OnPropertyChanged(nameof(ProfileCountText));
|
|
SelectedProfile = clone;
|
|
SaveProfiles();
|
|
}
|
|
|
|
private void EditProfile(object? parameter)
|
|
{
|
|
var profile = parameter as ConnectionProfile ?? _selectedProfile;
|
|
if (profile == null) return;
|
|
|
|
SaveCurrentProfileState();
|
|
var editable = CloneProfile(profile);
|
|
editable.Id = profile.Id;
|
|
editable.CreatedAt = profile.CreatedAt;
|
|
editable.LastUsedAt = profile.LastUsedAt;
|
|
|
|
if (ProfileEditorDialog.Show(editable, "ویرایش پروفایل", System.Windows.Application.Current.MainWindow) != true)
|
|
return;
|
|
|
|
ApplyProfileValues(profile, editable);
|
|
if (_selectedProfile == profile)
|
|
LoadProfileIntoUi(profile);
|
|
SaveProfiles();
|
|
RaiseProfileCardChanged();
|
|
}
|
|
|
|
private void SelectProfile(object? parameter)
|
|
{
|
|
if (parameter is ConnectionProfile profile)
|
|
SelectedProfile = profile;
|
|
}
|
|
|
|
private void DeleteCurrentProfile(object? parameter = null)
|
|
{
|
|
var toRemove = parameter as ConnectionProfile ?? _selectedProfile;
|
|
if (toRemove == null || Profiles.Count <= 1) return;
|
|
if (!Helpers.DialogService.Confirm($"پروفایل «{toRemove.Name}» حذف شود؟", "حذف پروفایل"))
|
|
return;
|
|
|
|
var idx = Profiles.IndexOf(toRemove);
|
|
Profiles.Remove(toRemove);
|
|
OnPropertyChanged(nameof(ProfileCountText));
|
|
SelectedProfile = Profiles[Math.Min(idx, Profiles.Count - 1)];
|
|
SaveProfiles();
|
|
}
|
|
|
|
private static ConnectionProfile CloneProfile(ConnectionProfile source) => new()
|
|
{
|
|
Name = source.Name,
|
|
ServerAddress = source.ServerAddress,
|
|
Username = source.Username,
|
|
Password = source.Password,
|
|
PreSharedKey = source.PreSharedKey,
|
|
TunnelType = source.TunnelType,
|
|
V2RayConfig = source.V2RayConfig,
|
|
OpenVpnConfig = source.OpenVpnConfig,
|
|
OpenVpnConfigPath = source.OpenVpnConfigPath,
|
|
OpenVpnUsername = source.OpenVpnUsername,
|
|
OpenVpnPassword = source.OpenVpnPassword,
|
|
ProxyProtocol = source.ProxyProtocol,
|
|
ProxyServerAddress = source.ProxyServerAddress,
|
|
ProxyPort = source.ProxyPort,
|
|
ProxyUsername = source.ProxyUsername,
|
|
ProxyPassword = source.ProxyPassword,
|
|
MixedProxyPort = source.MixedProxyPort,
|
|
AutoTuneMtu = source.AutoTuneMtu,
|
|
EnableDnsOptimization = source.EnableDnsOptimization,
|
|
EnableGameMode = source.EnableGameMode
|
|
};
|
|
|
|
private static void ApplyProfileValues(ConnectionProfile target, ConnectionProfile source)
|
|
{
|
|
target.Name = source.Name;
|
|
target.ServerAddress = source.ServerAddress;
|
|
target.Username = source.Username;
|
|
target.Password = source.Password;
|
|
target.PreSharedKey = source.PreSharedKey;
|
|
target.TunnelType = source.TunnelType;
|
|
target.V2RayConfig = source.V2RayConfig;
|
|
target.OpenVpnConfig = source.OpenVpnConfig;
|
|
target.OpenVpnConfigPath = source.OpenVpnConfigPath;
|
|
target.OpenVpnUsername = source.OpenVpnUsername;
|
|
target.OpenVpnPassword = source.OpenVpnPassword;
|
|
target.ProxyProtocol = source.ProxyProtocol;
|
|
target.ProxyServerAddress = source.ProxyServerAddress;
|
|
target.ProxyPort = source.ProxyPort;
|
|
target.ProxyUsername = source.ProxyUsername;
|
|
target.ProxyPassword = source.ProxyPassword;
|
|
target.MixedProxyPort = source.MixedProxyPort;
|
|
target.AutoTuneMtu = source.AutoTuneMtu;
|
|
target.EnableDnsOptimization = source.EnableDnsOptimization;
|
|
target.EnableGameMode = source.EnableGameMode;
|
|
}
|
|
|
|
private void RaiseProfileCardChanged()
|
|
{
|
|
OnPropertyChanged(nameof(ProfileCountText));
|
|
OnPropertyChanged(nameof(ActiveProfileTypeText));
|
|
OnPropertyChanged(nameof(ActiveProfileEndpointText));
|
|
OnPropertyChanged(nameof(ProfileSaveHintText));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region History
|
|
|
|
private void LoadHistory()
|
|
{
|
|
var entries = _historyService.LoadHistory();
|
|
ConnectionHistory.Clear();
|
|
foreach (var entry in entries)
|
|
ConnectionHistory.Add(entry);
|
|
OnPropertyChanged(nameof(TotalHistoryData));
|
|
}
|
|
|
|
private void ClearHistory()
|
|
{
|
|
_historyService.ClearHistory();
|
|
ConnectionHistory.Clear();
|
|
OnPropertyChanged(nameof(TotalHistoryData));
|
|
}
|
|
|
|
private void SaveConnectionToHistory()
|
|
{
|
|
if (_connectionStartTime == default) return;
|
|
|
|
// Use authoritative VPN-interface counters for the total,
|
|
// not the sum of per-app counters (which may miss tail packets
|
|
// and non-attributed traffic like VPN keepalives).
|
|
var (totalSent, totalReceived) = _trafficRouter.GetTotalVpnTraffic();
|
|
|
|
var entry = new ConnectionHistoryEntry
|
|
{
|
|
ProfileName = _selectedProfile?.Name ?? "پیشفرض",
|
|
ServerAddress = CurrentTunnelType == TunnelType.SocksProxy
|
|
? $"{ProxyServerAddress}:{ProxyPort}"
|
|
: ServerAddress,
|
|
ConnectedAt = _connectionStartTime,
|
|
DisconnectedAt = DateTime.Now,
|
|
BytesSent = totalSent,
|
|
BytesReceived = totalReceived
|
|
};
|
|
|
|
_historyService.AddEntry(entry);
|
|
ConnectionHistory.Insert(0, entry);
|
|
_connectionStartTime = default;
|
|
OnPropertyChanged(nameof(TotalHistoryData));
|
|
}
|
|
|
|
#endregion
|
|
}
|