mirror of
https://github.com/MaxiFan/TunnelX.git
synced 2026-05-17 21:14:37 +03:00
Add update checks and tray notifications
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Windows.Controls;
|
|||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using AppTunnel.Models;
|
||||||
using AppTunnel.ViewModels;
|
using AppTunnel.ViewModels;
|
||||||
using AppTunnel.Helpers;
|
using AppTunnel.Helpers;
|
||||||
using AppTunnel.Services;
|
using AppTunnel.Services;
|
||||||
@@ -16,6 +17,8 @@ public partial class MainWindow : Window
|
|||||||
private CancellationTokenSource _loadCts = new();
|
private CancellationTokenSource _loadCts = new();
|
||||||
private System.Windows.Forms.NotifyIcon? _trayIcon;
|
private System.Windows.Forms.NotifyIcon? _trayIcon;
|
||||||
private bool _isRealExit;
|
private bool _isRealExit;
|
||||||
|
private ConnectionState _lastNotifiedConnectionState = ConnectionState.Disconnected;
|
||||||
|
private bool _updateNotificationShown;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -69,8 +72,25 @@ public partial class MainWindow : Window
|
|||||||
statusItem.Text = $"وضعیت: {_viewModel.StatusText}";
|
statusItem.Text = $"وضعیت: {_viewModel.StatusText}";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (args.PropertyName == nameof(MainViewModel.ConnectionState))
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(NotifyConnectionStateChanged);
|
||||||
|
}
|
||||||
|
else if (args.PropertyName == nameof(MainViewModel.IsUpdateAvailable))
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(NotifyUpdateAvailable);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var updateItem = new System.Windows.Forms.ToolStripMenuItem("بررسی بروزرسانی");
|
||||||
|
updateItem.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
BringToForeground();
|
||||||
|
if (_viewModel.CheckForUpdatesCommand.CanExecute(null))
|
||||||
|
_viewModel.CheckForUpdatesCommand.Execute(null);
|
||||||
|
};
|
||||||
|
menu.Items.Add(updateItem);
|
||||||
|
|
||||||
menu.Items.Add(new System.Windows.Forms.ToolStripSeparator());
|
menu.Items.Add(new System.Windows.Forms.ToolStripSeparator());
|
||||||
|
|
||||||
var exitItem = new System.Windows.Forms.ToolStripMenuItem("خروج از برنامه");
|
var exitItem = new System.Windows.Forms.ToolStripMenuItem("خروج از برنامه");
|
||||||
@@ -136,6 +156,49 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void NotifyConnectionStateChanged()
|
||||||
|
{
|
||||||
|
if (_trayIcon == null) return;
|
||||||
|
|
||||||
|
var state = _viewModel.ConnectionState;
|
||||||
|
if (state == _lastNotifiedConnectionState) return;
|
||||||
|
_lastNotifiedConnectionState = state;
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ConnectionState.Connected:
|
||||||
|
ShowTrayNotification("TunnelX متصل شد", _viewModel.StatusText,
|
||||||
|
System.Windows.Forms.ToolTipIcon.Info);
|
||||||
|
break;
|
||||||
|
case ConnectionState.Disconnected:
|
||||||
|
ShowTrayNotification("TunnelX قطع شد", _viewModel.StatusText,
|
||||||
|
System.Windows.Forms.ToolTipIcon.Info);
|
||||||
|
break;
|
||||||
|
case ConnectionState.Error:
|
||||||
|
ShowTrayNotification("خطا در اتصال TunnelX", _viewModel.StatusText,
|
||||||
|
System.Windows.Forms.ToolTipIcon.Warning);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyUpdateAvailable()
|
||||||
|
{
|
||||||
|
if (_trayIcon == null || _updateNotificationShown || !_viewModel.IsUpdateAvailable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_updateNotificationShown = true;
|
||||||
|
ShowTrayNotification("بروزرسانی TunnelX آماده است",
|
||||||
|
_viewModel.UpdateStatusText,
|
||||||
|
System.Windows.Forms.ToolTipIcon.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowTrayNotification(string title, string message, System.Windows.Forms.ToolTipIcon icon)
|
||||||
|
{
|
||||||
|
if (_trayIcon == null) return;
|
||||||
|
_trayIcon.Visible = true;
|
||||||
|
_trayIcon.ShowBalloonTip(3500, title, message, icon);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Force window to foreground — borderless/transparent windows sometimes
|
// Force window to foreground — borderless/transparent windows sometimes
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public static class AppInfo
|
|||||||
public const string AppName = "TunnelX";
|
public const string AppName = "TunnelX";
|
||||||
public const string CreatorName = "MaxFan";
|
public const string CreatorName = "MaxFan";
|
||||||
public const string GitHubUrl = "https://github.com/MaxiFan/TunnelX";
|
public const string GitHubUrl = "https://github.com/MaxiFan/TunnelX";
|
||||||
|
public const string LatestReleaseUrl = GitHubUrl + "/releases/latest";
|
||||||
public const string LicenseName = "GPL-3.0-or-later";
|
public const string LicenseName = "GPL-3.0-or-later";
|
||||||
public const string PayPalEmail = "gallafan@gmail.com";
|
public const string PayPalEmail = "gallafan@gmail.com";
|
||||||
public const string PayPalDonateUrl = "https://www.paypal.com/donate/?business=gallafan%40gmail.com¤cy_code=USD";
|
public const string PayPalDonateUrl = "https://www.paypal.com/donate/?business=gallafan%40gmail.com¤cy_code=USD";
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AppTunnel.Services;
|
||||||
|
|
||||||
|
public sealed record GitHubReleaseInfo(
|
||||||
|
Version Version,
|
||||||
|
string TagName,
|
||||||
|
string Name,
|
||||||
|
string Url,
|
||||||
|
bool IsPrerelease);
|
||||||
|
|
||||||
|
public static class GitHubReleaseChecker
|
||||||
|
{
|
||||||
|
private const string LatestReleaseApi =
|
||||||
|
"https://api.github.com/repos/MaxiFan/TunnelX/releases/latest";
|
||||||
|
|
||||||
|
public static async Task<GitHubReleaseInfo?> GetLatestReleaseAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var http = new HttpClient();
|
||||||
|
http.DefaultRequestHeaders.UserAgent.ParseAdd("TunnelX");
|
||||||
|
http.Timeout = TimeSpan.FromSeconds(8);
|
||||||
|
|
||||||
|
using var response = await http.GetAsync(LatestReleaseApi, ct);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync(ct);
|
||||||
|
using var json = await JsonDocument.ParseAsync(stream, cancellationToken: ct);
|
||||||
|
var root = json.RootElement;
|
||||||
|
|
||||||
|
var tag = root.TryGetProperty("tag_name", out var tagElement)
|
||||||
|
? tagElement.GetString() ?? ""
|
||||||
|
: "";
|
||||||
|
if (!TryParseVersion(tag, out var version))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var name = root.TryGetProperty("name", out var nameElement)
|
||||||
|
? nameElement.GetString() ?? tag
|
||||||
|
: tag;
|
||||||
|
var url = root.TryGetProperty("html_url", out var urlElement)
|
||||||
|
? urlElement.GetString() ?? AppInfo.LatestReleaseUrl
|
||||||
|
: AppInfo.LatestReleaseUrl;
|
||||||
|
var prerelease = root.TryGetProperty("prerelease", out var preElement) &&
|
||||||
|
preElement.ValueKind == JsonValueKind.True;
|
||||||
|
|
||||||
|
return new GitHubReleaseInfo(version, tag, name, url, prerelease);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseVersion(string value, out Version version)
|
||||||
|
{
|
||||||
|
value = (value ?? "").Trim().TrimStart('v', 'V');
|
||||||
|
return Version.TryParse(value, out version!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
|||||||
OpenGitHubCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.GitHubUrl));
|
OpenGitHubCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.GitHubUrl));
|
||||||
OpenDonateCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.PayPalDonateUrl));
|
OpenDonateCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.PayPalDonateUrl));
|
||||||
CopyDonationInfoCommand = new RelayCommand(_ => CopyDonationInfoToClipboard());
|
CopyDonationInfoCommand = new RelayCommand(_ => CopyDonationInfoToClipboard());
|
||||||
|
CheckForUpdatesCommand = new RelayCommand(_ => _ = CheckForUpdatesAsync(false), _ => !IsCheckingForUpdates);
|
||||||
|
OpenLatestReleaseCommand = new RelayCommand(_ => OpenExternalLink(LatestReleaseUrl), _ => !string.IsNullOrWhiteSpace(LatestReleaseUrl));
|
||||||
|
|
||||||
_trafficRouter.TrafficUpdated += OnTrafficUpdated;
|
_trafficRouter.TrafficUpdated += OnTrafficUpdated;
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
|||||||
LoadExcludes();
|
LoadExcludes();
|
||||||
LoadIncludes();
|
LoadIncludes();
|
||||||
LoadHistory();
|
LoadHistory();
|
||||||
|
_ = CheckForUpdatesAsync(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
@@ -251,6 +254,59 @@ public partial class MainViewModel : INotifyPropertyChanged
|
|||||||
public string DonatePayPalText => $"پیپل: {AppInfo.PayPalEmail}";
|
public string DonatePayPalText => $"پیپل: {AppInfo.PayPalEmail}";
|
||||||
public string CryptoDonationText => AppInfo.CryptoDonationText;
|
public string CryptoDonationText => AppInfo.CryptoDonationText;
|
||||||
|
|
||||||
|
private bool _isCheckingForUpdates;
|
||||||
|
public bool IsCheckingForUpdates
|
||||||
|
{
|
||||||
|
get => _isCheckingForUpdates;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isCheckingForUpdates == value) return;
|
||||||
|
_isCheckingForUpdates = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(UpdateButtonText));
|
||||||
|
CommandManager.InvalidateRequerySuggested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isUpdateAvailable;
|
||||||
|
public bool IsUpdateAvailable
|
||||||
|
{
|
||||||
|
get => _isUpdateAvailable;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isUpdateAvailable == value) return;
|
||||||
|
_isUpdateAvailable = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _updateStatusText = "برای بررسی نسخه جدید، دکمه بررسی بروزرسانی را بزنید.";
|
||||||
|
public string UpdateStatusText
|
||||||
|
{
|
||||||
|
get => _updateStatusText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_updateStatusText == value) return;
|
||||||
|
_updateStatusText = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _latestReleaseUrl = AppInfo.LatestReleaseUrl;
|
||||||
|
public string LatestReleaseUrl
|
||||||
|
{
|
||||||
|
get => _latestReleaseUrl;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_latestReleaseUrl == value) return;
|
||||||
|
_latestReleaseUrl = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
CommandManager.InvalidateRequerySuggested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UpdateButtonText => IsCheckingForUpdates ? "در حال بررسی..." : "بررسی بروزرسانی";
|
||||||
|
|
||||||
public string ConnectButtonText => _connectionState switch
|
public string ConnectButtonText => _connectionState switch
|
||||||
{
|
{
|
||||||
ConnectionState.Disconnected => "🔌 اتصال",
|
ConnectionState.Disconnected => "🔌 اتصال",
|
||||||
@@ -619,6 +675,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
|||||||
public ICommand OpenGitHubCommand { get; }
|
public ICommand OpenGitHubCommand { get; }
|
||||||
public ICommand OpenDonateCommand { get; }
|
public ICommand OpenDonateCommand { get; }
|
||||||
public ICommand CopyDonationInfoCommand { get; }
|
public ICommand CopyDonationInfoCommand { get; }
|
||||||
|
public ICommand CheckForUpdatesCommand { get; }
|
||||||
|
public ICommand OpenLatestReleaseCommand { get; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -658,6 +716,61 @@ public partial class MainViewModel : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CheckForUpdatesAsync(bool silent)
|
||||||
|
{
|
||||||
|
if (IsCheckingForUpdates) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsCheckingForUpdates = true;
|
||||||
|
if (!silent)
|
||||||
|
UpdateStatusText = "در حال بررسی آخرین نسخه در GitHub...";
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||||
|
var latest = await GitHubReleaseChecker.GetLatestReleaseAsync(cts.Token);
|
||||||
|
if (latest == null)
|
||||||
|
{
|
||||||
|
if (!silent)
|
||||||
|
UpdateStatusText = "بررسی نسخه جدید ناموفق بود. اتصال اینترنت یا GitHub را بررسی کنید.";
|
||||||
|
Logger.Warning("[UPDATE] Latest release check failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LatestReleaseUrl = latest.Url;
|
||||||
|
var currentVersion = System.Reflection.Assembly.GetExecutingAssembly()
|
||||||
|
.GetName().Version ?? new Version(0, 0, 0);
|
||||||
|
var current = new Version(currentVersion.Major, currentVersion.Minor, currentVersion.Build);
|
||||||
|
|
||||||
|
if (latest.Version > current)
|
||||||
|
{
|
||||||
|
IsUpdateAvailable = true;
|
||||||
|
UpdateStatusText = $"نسخه جدید آماده است: {latest.TagName} - برای دانلود از GitHub باز کنید.";
|
||||||
|
Logger.Info($"[UPDATE] New version available: current={current} latest={latest.TagName}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsUpdateAvailable = false;
|
||||||
|
UpdateStatusText = $"TunnelX بهروز است. نسخه فعلی: {AppInfo.VersionText}";
|
||||||
|
Logger.Info($"[UPDATE] App is up to date: current={current} latest={latest.TagName}");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (!silent)
|
||||||
|
UpdateStatusText = "بررسی بروزرسانی به زمان مجاز نرسید.";
|
||||||
|
Logger.Warning("[UPDATE] Latest release check timed out");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!silent)
|
||||||
|
UpdateStatusText = $"بررسی بروزرسانی ناموفق بود: {ex.Message}";
|
||||||
|
Logger.Warning($"[UPDATE] Latest release check failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsCheckingForUpdates = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void PasteConfigFromClipboard()
|
private void PasteConfigFromClipboard()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -102,6 +102,41 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Updates -->
|
||||||
|
<Border Style="{StaticResource CardPanel}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Style="{StaticResource SectionHeader}" Text="بروزرسانی"/>
|
||||||
|
<TextBlock Text="{Binding UpdateStatusText}"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{StaticResource TextSecondaryBrush}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
LineHeight="18"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2"
|
||||||
|
Orientation="Vertical"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MinWidth="150">
|
||||||
|
<Button Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="{Binding UpdateButtonText}"
|
||||||
|
Command="{Binding CheckForUpdatesCommand}"
|
||||||
|
Padding="14,8"/>
|
||||||
|
<Button Style="{StaticResource PrimaryButton}"
|
||||||
|
Content="باز کردن صفحه انتشار"
|
||||||
|
Command="{Binding OpenLatestReleaseCommand}"
|
||||||
|
Padding="14,8"
|
||||||
|
Margin="0,8,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Quick Path -->
|
<!-- Quick Path -->
|
||||||
<Border Style="{StaticResource CardPanel}">
|
<Border Style="{StaticResource CardPanel}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
|||||||
+35
-1
@@ -16,7 +16,7 @@ Candidate checks:
|
|||||||
- WinDivert and Wintun native component availability
|
- WinDivert and Wintun native component availability
|
||||||
- extraction folder write permissions
|
- extraction folder write permissions
|
||||||
- route and packet interception readiness
|
- route and packet interception readiness
|
||||||
- GitHub release/update status
|
- deeper release asset verification for future automatic update flows
|
||||||
|
|
||||||
Potential actions:
|
Potential actions:
|
||||||
|
|
||||||
@@ -28,3 +28,37 @@ Potential actions:
|
|||||||
- avoid silent driver or system-level changes
|
- avoid silent driver or system-level changes
|
||||||
|
|
||||||
For public releases, TunnelX should continue to prefer self-contained standalone EXE builds so end users do not need to install the .NET Runtime separately.
|
For public releases, TunnelX should continue to prefer self-contained standalone EXE builds so end users do not need to install the .NET Runtime separately.
|
||||||
|
|
||||||
|
### Profile management
|
||||||
|
|
||||||
|
- import and export connection profiles
|
||||||
|
- clone profiles with clearer naming
|
||||||
|
- per-profile health and last-test status
|
||||||
|
- presets for common workflows such as browser-only, messaging, development, and full VPN
|
||||||
|
|
||||||
|
### Split tunnel rule clarity
|
||||||
|
|
||||||
|
- explicit rule priority between apps, include destinations, and exclude destinations
|
||||||
|
- wildcard and domain-suffix previews
|
||||||
|
- conflict detection when the same destination is both included and excluded
|
||||||
|
- show which rule caused a destination to be routed or bypassed
|
||||||
|
|
||||||
|
### Health dashboard
|
||||||
|
|
||||||
|
- user-facing states: safe, protected, needs attention, and broken
|
||||||
|
- short explanation for the latest important network event
|
||||||
|
- suggested action next to DNS, IPv6, route, and leak status
|
||||||
|
|
||||||
|
### Kill switch
|
||||||
|
|
||||||
|
- optional per-app kill switch when the tunnel core or TUN bridge drops
|
||||||
|
- keep selected apps blocked until the tunnel is restored or the user disconnects
|
||||||
|
|
||||||
|
### Traffic accounting clarity
|
||||||
|
|
||||||
|
- keep tunnel, direct, per-app, DNS, and history counters aligned to one accounting model
|
||||||
|
- expose whether counters are interface-based, rewritten-flow-based, or app-attributed
|
||||||
|
|
||||||
|
### Installer and prerequisite checks
|
||||||
|
|
||||||
|
- optional installer/bootstrapper for shortcuts, uninstall support, prerequisite checks, and future update flow
|
||||||
|
|||||||
Reference in New Issue
Block a user