Add startup and auto-connect app settings

Persist global app settings and expose startup/auto-connect controls. Adds AppSettings (StartWithWindows, AutoConnectOnStartup, LastActiveProfileId) plus LoadAppSettings/SaveAppSettings to ProfileService (appsettings.json). MainViewModel now loads/saves these settings, exposes StartWithWindows/AutoConnectOnStartup/LastActiveProfileId properties, restores last profile and triggers auto-connect on startup when enabled, and updates the HKCU Run registry entry to enable/disable startup (with a user warning message on enable). Adds corresponding UI controls to SettingsTabView and includes defensive error handling and sensible defaults.
This commit is contained in:
mohamad parvizi
2026-05-14 13:38:46 +03:30
parent 5f44e10e7b
commit aa345b1680
4 changed files with 189 additions and 0 deletions
+40
View File
@@ -27,6 +27,7 @@ public class ProfileService
private static readonly string ExcludesFile = Path.Combine(ProfileDir, "excludes.json");
private static readonly string IncludesFile = Path.Combine(ProfileDir, "includes.json");
private static readonly string TunnelAppsFile = Path.Combine(ProfileDir, "tunnelapps.json");
private static readonly string AppSettingsFile = Path.Combine(ProfileDir, "appsettings.json");
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -51,6 +52,45 @@ public class ProfileService
}
}
/// <summary>
/// Load global application settings from disk.
/// </summary>
public AppSettings LoadAppSettings()
{
if (!File.Exists(AppSettingsFile))
return new AppSettings();
try
{
var json = File.ReadAllText(AppSettingsFile, Encoding.UTF8);
return JsonSerializer.Deserialize<AppSettings>(json, JsonOptions) ?? new AppSettings();
}
catch
{
return new AppSettings();
}
}
/// <summary>
/// Save global application settings to disk.
/// </summary>
public void SaveAppSettings(AppSettings settings)
{
Directory.CreateDirectory(ProfileDir);
var json = JsonSerializer.Serialize(settings, JsonOptions);
File.WriteAllText(AppSettingsFile, json, Encoding.UTF8);
}
/// <summary>
/// Global application settings (startup + auto-connect preferences).
/// </summary>
public class AppSettings
{
public bool StartWithWindows { get; set; } = false;
public bool AutoConnectOnStartup { get; set; } = false;
public string? LastActiveProfileId { get; set; } = null;
}
/// <summary>
/// Load all saved profiles from disk.
/// </summary>
@@ -113,6 +113,7 @@ public partial class MainViewModel
VpnIp = _vpnService.Status.VpnLocalIp;
VpnAdapterName = ResolveInterfaceName(_vpnService.Status.VpnInterfaceIndex);
_connectionStartTime = DateTime.Now;
LastActiveProfileId = _selectedProfile?.Id;
RaiseHealthStatusChanged();
// Start traffic routing for enabled apps
+102
View File
@@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
using System.Text.Json.Nodes;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using Application = System.Windows.Application;
using System.Windows.Threading;
using AppTunnel.Models;
@@ -23,6 +24,7 @@ public partial class MainViewModel : INotifyPropertyChanged
private readonly DispatcherTimer _saveDebounceTimer;
private CancellationTokenSource? _connectionCts;
private DateTime _connectionStartTime;
private ProfileService.AppSettings _appSettings = new();
public MainViewModel()
{
@@ -90,7 +92,24 @@ public partial class MainViewModel : INotifyPropertyChanged
LoadExcludes();
LoadIncludes();
LoadHistory();
LoadAppSettings();
_ = CheckForUpdatesAsync(true);
// Auto-connect to last active profile if enabled
if (_appSettings.AutoConnectOnStartup && !string.IsNullOrEmpty(_appSettings.LastActiveProfileId))
{
var lastProfile = Profiles.FirstOrDefault(p => p.Id == _appSettings.LastActiveProfileId);
if (lastProfile != null)
{
SelectedProfile = lastProfile;
_ = ToggleConnectionAsync().ContinueWith(t =>
{
if (t.IsFaulted)
Application.Current?.Dispatcher.Invoke(() =>
StatusText = $"خطای اتصال خودکار: {t.Exception?.InnerException?.Message}");
}, TaskScheduler.Default);
}
}
}
#region Properties
@@ -220,6 +239,46 @@ public partial class MainViewModel : INotifyPropertyChanged
? "Game Mode فعال است: Route نگهداری طولانی‌تر، DNS سریع‌تر و DSCP برای بسته‌های بازی اعمال می‌شود."
: "Game Mode غیرفعال است: حالت متعادل برای مصرف عمومی.";
private bool _startWithWindows;
public bool StartWithWindows
{
get => _startWithWindows;
set
{
if (_startWithWindows == value) return;
_startWithWindows = value;
OnPropertyChanged();
UpdateStartupRegistry(value);
_appSettings.StartWithWindows = value;
_profileService.SaveAppSettings(_appSettings);
}
}
private bool _autoConnectOnStartup;
public bool AutoConnectOnStartup
{
get => _autoConnectOnStartup;
set
{
if (_autoConnectOnStartup == value) return;
_autoConnectOnStartup = value;
OnPropertyChanged();
_appSettings.AutoConnectOnStartup = value;
_profileService.SaveAppSettings(_appSettings);
}
}
public string? LastActiveProfileId
{
get => _appSettings.LastActiveProfileId;
set
{
if (_appSettings.LastActiveProfileId == value) return;
_appSettings.LastActiveProfileId = value;
_profileService.SaveAppSettings(_appSettings);
}
}
private bool _isBusy;
public bool IsBusy
{
@@ -918,6 +977,49 @@ public partial class MainViewModel : INotifyPropertyChanged
OnPropertyChanged(nameof(HealthRoutesText));
}
private void LoadAppSettings()
{
_appSettings = _profileService.LoadAppSettings();
_startWithWindows = _appSettings.StartWithWindows;
_autoConnectOnStartup = _appSettings.AutoConnectOnStartup;
OnPropertyChanged(nameof(StartWithWindows));
OnPropertyChanged(nameof(AutoConnectOnStartup));
}
private static void UpdateStartupRegistry(bool enable)
{
const string runKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
const string appName = "TunnelX";
try
{
using var key = Registry.CurrentUser.OpenSubKey(runKey, writable: true);
if (key == null) return;
if (enable)
{
var exePath = Environment.ProcessPath ??
Process.GetCurrentProcess().MainModule?.FileName ??
System.IO.Path.Combine(AppContext.BaseDirectory, "TunnelX.exe");
key.SetValue(appName, $"\"{exePath}\"");
System.Windows.MessageBox.Show(
"استارت‌آپ فعال شد.\n\n⚠️ برای کارکرد صحیح، پس از این نباید محل فایل اجرایی TunnelX را تغییر دهید.",
"TunnelX — استارت‌آپ",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
else
{
if (key.GetValue(appName) != null)
key.DeleteValue(appName);
}
}
catch (Exception ex)
{
Logger.Warning($"[STARTUP] Registry update failed: {ex.Message}");
}
}
#endregion
#region INotifyPropertyChanged
+46
View File
@@ -112,6 +112,52 @@
</StackPanel>
</Border>
<!-- Startup & Auto-Connect -->
<Border Style="{StaticResource CardPanel}">
<StackPanel>
<TextBlock Style="{StaticResource SectionHeader}" Text="🖥️ استارت‌آپ و اتصال خودکار"
Margin="0,0,0,4"/>
<Grid Margin="0,2,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="اجرای خودکار هنگام روشن شدن ویندوز" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock Text="⚠️ پس از فعال کردن، نباید محل فایل اجرایی TunnelX را تغییر دهید."
FontSize="10" Foreground="{StaticResource WarningBrush}"
TextWrapping="Wrap" Margin="0,2,0,0"/>
</StackPanel>
<CheckBox Grid.Column="1"
Style="{StaticResource ToggleSwitch}"
IsChecked="{Binding StartWithWindows, Mode=TwoWay}"
VerticalAlignment="Center"/>
</Grid>
<Border Background="#18FFFFFF" Height="1" Margin="0,8,0,8"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="اتصال خودکار به آخرین کانکشن فعال" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock Text="اگر آخرین بار یک پروفایل متصل بوده، هنگام اجرای برنامه به آن وصل می‌شود."
FontSize="10" Foreground="{StaticResource TextSecondaryBrush}"
TextWrapping="Wrap" Margin="0,2,0,0"/>
</StackPanel>
<CheckBox Grid.Column="1"
Style="{StaticResource ToggleSwitch}"
IsChecked="{Binding AutoConnectOnStartup, Mode=TwoWay}"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>