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.Media.Animation;
|
||||
using System.Windows.Media;
|
||||
using AppTunnel.Models;
|
||||
using AppTunnel.ViewModels;
|
||||
using AppTunnel.Helpers;
|
||||
using AppTunnel.Services;
|
||||
@@ -16,6 +17,8 @@ public partial class MainWindow : Window
|
||||
private CancellationTokenSource _loadCts = new();
|
||||
private System.Windows.Forms.NotifyIcon? _trayIcon;
|
||||
private bool _isRealExit;
|
||||
private ConnectionState _lastNotifiedConnectionState = ConnectionState.Disconnected;
|
||||
private bool _updateNotificationShown;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -69,8 +72,25 @@ public partial class MainWindow : Window
|
||||
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());
|
||||
|
||||
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)
|
||||
{
|
||||
// Force window to foreground — borderless/transparent windows sometimes
|
||||
|
||||
@@ -5,6 +5,7 @@ public static class AppInfo
|
||||
public const string AppName = "TunnelX";
|
||||
public const string CreatorName = "MaxFan";
|
||||
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 PayPalEmail = "gallafan@gmail.com";
|
||||
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));
|
||||
OpenDonateCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.PayPalDonateUrl));
|
||||
CopyDonationInfoCommand = new RelayCommand(_ => CopyDonationInfoToClipboard());
|
||||
CheckForUpdatesCommand = new RelayCommand(_ => _ = CheckForUpdatesAsync(false), _ => !IsCheckingForUpdates);
|
||||
OpenLatestReleaseCommand = new RelayCommand(_ => OpenExternalLink(LatestReleaseUrl), _ => !string.IsNullOrWhiteSpace(LatestReleaseUrl));
|
||||
|
||||
_trafficRouter.TrafficUpdated += OnTrafficUpdated;
|
||||
|
||||
@@ -88,6 +90,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
LoadExcludes();
|
||||
LoadIncludes();
|
||||
LoadHistory();
|
||||
_ = CheckForUpdatesAsync(true);
|
||||
}
|
||||
|
||||
#region Properties
|
||||
@@ -251,6 +254,59 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
public string DonatePayPalText => $"پیپل: {AppInfo.PayPalEmail}";
|
||||
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
|
||||
{
|
||||
ConnectionState.Disconnected => "🔌 اتصال",
|
||||
@@ -619,6 +675,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
public ICommand OpenGitHubCommand { get; }
|
||||
public ICommand OpenDonateCommand { get; }
|
||||
public ICommand CopyDonationInfoCommand { get; }
|
||||
public ICommand CheckForUpdatesCommand { get; }
|
||||
public ICommand OpenLatestReleaseCommand { get; }
|
||||
|
||||
#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()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -102,6 +102,41 @@
|
||||
</Grid>
|
||||
</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 -->
|
||||
<Border Style="{StaticResource CardPanel}">
|
||||
<StackPanel>
|
||||
|
||||
+35
-1
@@ -16,7 +16,7 @@ Candidate checks:
|
||||
- WinDivert and Wintun native component availability
|
||||
- extraction folder write permissions
|
||||
- route and packet interception readiness
|
||||
- GitHub release/update status
|
||||
- deeper release asset verification for future automatic update flows
|
||||
|
||||
Potential actions:
|
||||
|
||||
@@ -28,3 +28,37 @@ Potential actions:
|
||||
- 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.
|
||||
|
||||
### 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