mirror of
https://github.com/MaxiFan/TunnelX.git
synced 2026-05-18 23:54:50 +03:00
Update release packaging and connection UX
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -95,10 +95,12 @@ jobs:
|
||||
}
|
||||
|
||||
$artifactName = "TunnelX-$tag-standalone-compressed.exe"
|
||||
$frameworkArtifactName = "TunnelX-$tag-framework-dependent-win-x64.zip"
|
||||
|
||||
"version=$version" >> $env:GITHUB_OUTPUT
|
||||
"tag=$tag" >> $env:GITHUB_OUTPUT
|
||||
"artifact_name=$artifactName" >> $env:GITHUB_OUTPUT
|
||||
"framework_artifact_name=$frameworkArtifactName" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Update version and changelog
|
||||
id: release_notes
|
||||
@@ -213,6 +215,63 @@ jobs:
|
||||
"checksum=$checksum" >> $env:GITHUB_OUTPUT
|
||||
"sha256=$hash" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Publish framework-dependent package
|
||||
run: >
|
||||
dotnet publish AppTunnel\AppTunnel.csproj
|
||||
-c Release
|
||||
-r win-x64
|
||||
--self-contained false
|
||||
-p:DebugType=None
|
||||
-p:DebugSymbols=false
|
||||
-o publish\TunnelX-framework-dependent
|
||||
|
||||
- name: Package framework-dependent asset
|
||||
id: framework_package
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDir = "publish/TunnelX-framework-dependent"
|
||||
$assetName = "${{ steps.meta.outputs.framework_artifact_name }}"
|
||||
$asset = "publish/$assetName"
|
||||
$checksum = "$asset.sha256"
|
||||
$readme = Join-Path $publishDir "README.txt"
|
||||
|
||||
if (-not (Test-Path (Join-Path $publishDir "TunnelX.exe"))) {
|
||||
throw "Framework-dependent executable was not found in $publishDir"
|
||||
}
|
||||
|
||||
$readmeLines = @(
|
||||
"TunnelX framework-dependent package",
|
||||
"===================================",
|
||||
"",
|
||||
"This package is smaller than the standalone download because it does not include the .NET runtime.",
|
||||
"",
|
||||
"Use this ZIP only if Microsoft .NET 8 Desktop Runtime (x64) is already installed on this Windows PC.",
|
||||
"If .NET 8 Desktop Runtime is not installed, download the standalone TunnelX EXE instead.",
|
||||
"",
|
||||
"Download .NET 8 Desktop Runtime:",
|
||||
"https://dotnet.microsoft.com/en-us/download/dotnet/8.0",
|
||||
"",
|
||||
"Run:",
|
||||
"1. Extract the ZIP to a folder.",
|
||||
"2. Run TunnelX.exe as Administrator.",
|
||||
"",
|
||||
"Recommended for most users:",
|
||||
"TunnelX standalone compressed EXE."
|
||||
)
|
||||
($readmeLines -join "`r`n") | Set-Content -Encoding UTF8 -LiteralPath $readme
|
||||
|
||||
if (Test-Path $asset) {
|
||||
Remove-Item -LiteralPath $asset -Force
|
||||
}
|
||||
Compress-Archive -Path (Join-Path $publishDir "*") -DestinationPath $asset -Force
|
||||
|
||||
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $asset).Hash.ToLowerInvariant()
|
||||
"$hash $assetName" | Set-Content -Encoding ASCII -LiteralPath $checksum
|
||||
|
||||
"asset=$asset" >> $env:GITHUB_OUTPUT
|
||||
"checksum=$checksum" >> $env:GITHUB_OUTPUT
|
||||
"sha256=$hash" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
@@ -220,6 +279,8 @@ jobs:
|
||||
path: |
|
||||
${{ steps.package.outputs.asset }}
|
||||
${{ steps.package.outputs.checksum }}
|
||||
${{ steps.framework_package.outputs.asset }}
|
||||
${{ steps.framework_package.outputs.checksum }}
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create GitHub release
|
||||
@@ -231,9 +292,17 @@ jobs:
|
||||
$title = "TunnelX $tag"
|
||||
$runUrl = "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
$sha256 = "${{ steps.package.outputs.sha256 }}".ToUpperInvariant()
|
||||
$frameworkSha256 = "${{ steps.framework_package.outputs.sha256 }}".ToUpperInvariant()
|
||||
$artifactName = "${{ steps.meta.outputs.artifact_name }}"
|
||||
$frameworkArtifactName = "${{ steps.meta.outputs.framework_artifact_name }}"
|
||||
|
||||
$notes = Get-Content -Raw -LiteralPath "${{ steps.release_notes.outputs.notes_file }}"
|
||||
$downloadLines = @(
|
||||
"## Downloads",
|
||||
"",
|
||||
"- Recommended for most users: ``$artifactName``. This is the standalone self-contained EXE and does not require a separate .NET installation.",
|
||||
"- Smaller package for users who already have Microsoft .NET 8 Desktop Runtime (x64): ``$frameworkArtifactName``. Extract the ZIP and run ``TunnelX.exe`` as Administrator. The ZIP includes ``README.txt`` with the same requirement."
|
||||
)
|
||||
$provenanceLines = @(
|
||||
"<!-- release-provenance:start -->",
|
||||
"## Build provenance",
|
||||
@@ -243,9 +312,10 @@ jobs:
|
||||
"- Run: $runUrl",
|
||||
"- Commit: ``${{ steps.release_commit.outputs.sha }}``",
|
||||
"- SHA256: ``$sha256 $artifactName``",
|
||||
"- SHA256: ``$frameworkSha256 $frameworkArtifactName``",
|
||||
"<!-- release-provenance:end -->"
|
||||
)
|
||||
$notes = "$($notes.Trim())`n`n$($provenanceLines -join "`n")"
|
||||
$notes = "$($notes.Trim())`n`n$($downloadLines -join "`n")`n`n$($provenanceLines -join "`n")"
|
||||
|
||||
$notesFile = Join-Path $env:RUNNER_TEMP "final-release-notes.md"
|
||||
$notes | Set-Content -Encoding UTF8 -LiteralPath $notesFile
|
||||
@@ -254,6 +324,8 @@ jobs:
|
||||
"release", "create", $tag,
|
||||
"${{ steps.package.outputs.asset }}",
|
||||
"${{ steps.package.outputs.checksum }}",
|
||||
"${{ steps.framework_package.outputs.asset }}",
|
||||
"${{ steps.framework_package.outputs.checksum }}",
|
||||
"--title", $title,
|
||||
"--notes-file", $notesFile,
|
||||
"--latest"
|
||||
|
||||
+33
-26
@@ -275,8 +275,8 @@
|
||||
<TabItem Style="{StaticResource ModernTabItem}" ToolTip="وضعیت اتصال VPN و کنترل اتصال">
|
||||
<TabItem.Header>
|
||||
<StackPanel>
|
||||
<TextBlock Text="⚡" FontSize="13" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="اتصال VPN" FontSize="11" FontWeight="Medium" TextAlignment="Center"/>
|
||||
<TextBlock Text="⚡" FontSize="12" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="اتصال VPN" FontSize="10" FontWeight="Medium" TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<views:ConnectionTabView/>
|
||||
@@ -284,8 +284,8 @@
|
||||
<TabItem Style="{StaticResource ModernTabItem}" ToolTip="انتخاب برنامههایی که باید از تونل عبور کنند">
|
||||
<TabItem.Header>
|
||||
<StackPanel>
|
||||
<TextBlock Text="📱" FontSize="13" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="برنامهها" FontSize="11" FontWeight="Medium" TextAlignment="Center"/>
|
||||
<TextBlock Text="📱" FontSize="12" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="برنامهها" FontSize="10" FontWeight="Medium" TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<views:AppsTabView/>
|
||||
@@ -294,8 +294,8 @@
|
||||
<TabItem Style="{StaticResource ModernTabItem}" ToolTip="تنظیمات عمومی تونل و DNS">
|
||||
<TabItem.Header>
|
||||
<StackPanel>
|
||||
<TextBlock Text="⚙" FontSize="13" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="تنظیمات" FontSize="11" FontWeight="Medium" TextAlignment="Center"/>
|
||||
<TextBlock Text="⚙" FontSize="12" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="تنظیمات" FontSize="10" FontWeight="Medium" TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<views:SettingsTabView/>
|
||||
@@ -305,8 +305,8 @@
|
||||
<TabItem Style="{StaticResource ModernTabItem}" ToolTip="قوانین Include و Exclude مسیرها">
|
||||
<TabItem.Header>
|
||||
<StackPanel>
|
||||
<TextBlock Text="🧭" FontSize="13" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="قوانین مسیر" FontSize="11" FontWeight="Medium" TextAlignment="Center"/>
|
||||
<TextBlock Text="🧭" FontSize="12" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="قوانین مسیر" FontSize="10" FontWeight="Medium" TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
|
||||
@@ -548,8 +548,8 @@
|
||||
<TabItem Style="{StaticResource ModernTabItem}" ToolTip="نمایش ترافیک، تاریخچه و آمار اتصال">
|
||||
<TabItem.Header>
|
||||
<StackPanel>
|
||||
<TextBlock Text="📊" FontSize="13" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="ترافیک/تاریخچه" FontSize="10.5" FontWeight="Medium" TextAlignment="Center"/>
|
||||
<TextBlock Text="📊" FontSize="12" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="ترافیک/تاریخچه" FontSize="9.5" FontWeight="Medium" TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
|
||||
@@ -753,6 +753,17 @@
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<!-- ███ TAB 6: HELP ███ -->
|
||||
<TabItem Style="{StaticResource ModernTabItem}" ToolTip="راهنما و عیبیابی">
|
||||
<TabItem.Header>
|
||||
<StackPanel>
|
||||
<TextBlock Text="❔" FontSize="12" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="راهنما" FontSize="10" FontWeight="Medium" TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<views:HelpTabView/>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
|
||||
</Grid>
|
||||
@@ -801,6 +812,18 @@
|
||||
Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding GitHubInstallCountText}"
|
||||
Visibility="{Binding HasGitHubInstallCount, Converter={StaticResource BoolToVis}}"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FlowDirection="{Binding AppFlowDirection}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Margin="12,0"/>
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="{Binding AppEndHorizontalAlignment}"
|
||||
@@ -822,22 +845,6 @@
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="حمایت از پروژه"/>
|
||||
<Button Content="راهنما"
|
||||
Click="OnShowHelpClick"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Padding="9,4"
|
||||
FontSize="10"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="راهنما و عیبیابی"/>
|
||||
<Button Content="GitHub"
|
||||
Command="{Binding OpenGitHubCommand}"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Padding="9,4"
|
||||
FontSize="10"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="باز کردن صفحه GitHub پروژه TunnelX"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -299,7 +299,7 @@
|
||||
<!-- Tab header bar with bottom border -->
|
||||
<Border Grid.Row="0" Background="{StaticResource SurfaceBrush}"
|
||||
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,0,1"
|
||||
Padding="6,6,6,4">
|
||||
Padding="4,4,4,3">
|
||||
<TabPanel IsItemsHost="True" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<!-- Tab content -->
|
||||
@@ -319,9 +319,9 @@
|
||||
<Setter Property="TextOptions.TextFormattingMode" Value="Ideal"/>
|
||||
<Setter Property="TextOptions.TextRenderingMode" Value="Auto"/>
|
||||
<Setter Property="TextOptions.TextHintingMode" Value="Fixed"/>
|
||||
<Setter Property="Padding" Value="6,6"/>
|
||||
<Setter Property="Width" Value="104"/>
|
||||
<Setter Property="MinHeight" Value="50"/>
|
||||
<Setter Property="Padding" Value="4,4"/>
|
||||
<Setter Property="Width" Value="88"/>
|
||||
<Setter Property="MinHeight" Value="44"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
@@ -341,13 +341,13 @@
|
||||
<ContentPresenter Grid.Row="0" ContentSource="Header"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,0,5"/>
|
||||
Margin="0,0,0,3"/>
|
||||
<!-- Bottom indicator line -->
|
||||
<Border x:Name="indicator" Grid.Row="1"
|
||||
Height="3" CornerRadius="1.5"
|
||||
Height="2" CornerRadius="1"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="10,0"/>
|
||||
Margin="8,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
|
||||
@@ -6,6 +6,7 @@ public static class AppInfo
|
||||
public const string CreatorName = "MaxFan";
|
||||
public const string GitHubUrl = "https://github.com/MaxiFan/TunnelX";
|
||||
public const string LatestReleaseUrl = GitHubUrl + "/releases/latest";
|
||||
public const string TelegramContactUrl = "https://t.me/maxifaan";
|
||||
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";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Net.Http;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AppTunnel.Services;
|
||||
@@ -14,6 +15,8 @@ public static class GitHubReleaseChecker
|
||||
{
|
||||
private const string LatestReleaseApi =
|
||||
"https://api.github.com/repos/MaxiFan/TunnelX/releases/latest";
|
||||
private const string ReleasesApi =
|
||||
"https://api.github.com/repos/MaxiFan/TunnelX/releases";
|
||||
|
||||
public static async Task<GitHubReleaseInfo?> GetLatestReleaseAsync(CancellationToken ct)
|
||||
{
|
||||
@@ -47,9 +50,78 @@ public static class GitHubReleaseChecker
|
||||
return new GitHubReleaseInfo(version, tag, name, url, prerelease);
|
||||
}
|
||||
|
||||
public static async Task<long?> GetAppDownloadCountAsync(CancellationToken ct, int proxyPort = 0)
|
||||
{
|
||||
if (proxyPort > 0)
|
||||
{
|
||||
var proxied = await TryGetAppDownloadCountAsync(ct, proxyPort);
|
||||
if (proxied.HasValue)
|
||||
return proxied;
|
||||
}
|
||||
|
||||
return await TryGetAppDownloadCountAsync(ct, proxyPort: 0);
|
||||
}
|
||||
|
||||
private static async Task<long?> TryGetAppDownloadCountAsync(CancellationToken ct, int proxyPort)
|
||||
{
|
||||
using var handler = new HttpClientHandler();
|
||||
if (proxyPort > 0)
|
||||
{
|
||||
handler.Proxy = new WebProxy($"http://127.0.0.1:{proxyPort}");
|
||||
handler.UseProxy = true;
|
||||
}
|
||||
|
||||
using var http = new HttpClient(handler)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd("TunnelX");
|
||||
|
||||
using var response = await http.GetAsync(ReleasesApi, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(ct);
|
||||
using var json = await JsonDocument.ParseAsync(stream, cancellationToken: ct);
|
||||
if (json.RootElement.ValueKind != JsonValueKind.Array)
|
||||
return null;
|
||||
|
||||
long total = 0;
|
||||
foreach (var release in json.RootElement.EnumerateArray())
|
||||
{
|
||||
if (!release.TryGetProperty("assets", out var assets) || assets.ValueKind != JsonValueKind.Array)
|
||||
continue;
|
||||
|
||||
foreach (var asset in assets.EnumerateArray())
|
||||
{
|
||||
var name = asset.TryGetProperty("name", out var nameElement)
|
||||
? nameElement.GetString() ?? ""
|
||||
: "";
|
||||
if (!IsAppDownloadAsset(name))
|
||||
continue;
|
||||
|
||||
if (asset.TryGetProperty("download_count", out var countElement) &&
|
||||
countElement.TryGetInt64(out var count))
|
||||
total += count;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
public static bool TryParseVersion(string value, out Version version)
|
||||
{
|
||||
value = (value ?? "").Trim().TrimStart('v', 'V');
|
||||
return Version.TryParse(value, out version!);
|
||||
}
|
||||
|
||||
private static bool IsAppDownloadAsset(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return false;
|
||||
|
||||
return (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) &&
|
||||
!name.EndsWith(".sha256", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +435,9 @@ public sealed class LocalizationService : INotifyPropertyChanged
|
||||
["حمایت و تماس"] = "Support and Contact",
|
||||
["حمایت با پیپل"] = "Donate with PayPal",
|
||||
["حمایت"] = "Donate",
|
||||
["محل تبلیغات شما"] = "Your ad space",
|
||||
["درخواست تبلیغ"] = "Request advertising",
|
||||
["تبلیغ شما میتواند در معرض دید کاربران TunnelX با بیش از {0} نصب از GitHub باشد."] = "Your ad can reach TunnelX users backed by more than {0} GitHub installs.",
|
||||
["کپی اطلاعات حمایت"] = "Copy donation info",
|
||||
["اگر TunnelX برایتان مفید بوده، میتوانید با PayPal یا کریپتو از توسعه آن حمایت کنید."] = "If TunnelX has been useful, you can support its development with PayPal or crypto.",
|
||||
["پرداخت با PayPal"] = "Pay with PayPal",
|
||||
@@ -448,6 +451,7 @@ public sealed class LocalizationService : INotifyPropertyChanged
|
||||
["تنظیمات این کانفیگ بعد از ذخیره در لیست پروفایلها نمایش داده میشود."] = "This config appears in the profile list after saving.",
|
||||
["اطلاعات پروفایل"] = "Profile Info",
|
||||
["نام پروفایل"] = "Profile Name",
|
||||
["نام پروفایل *"] = "Profile Name *",
|
||||
["مثلاً کار، تلگرام، گیمینگ..."] = "e.g. Work, Telegram, Gaming...",
|
||||
["تنظیمات اتصال"] = "Connection Settings",
|
||||
["کانفیگ V2Ray / Xray"] = "V2Ray / Xray Config",
|
||||
@@ -532,6 +536,8 @@ public sealed class LocalizationService : INotifyPropertyChanged
|
||||
["حالت انتخابی"] = "Selected Mode",
|
||||
["ترافیک کل سیستم از تونل عبور خواهد کرد؛ برای وقتی مناسب است که همه برنامهها باید پشت تونل باشند."] = "All system traffic will use the tunnel; useful when every app must be behind the tunnel.",
|
||||
["فقط برنامهها و مقصدهای انتخابی از تونل عبور میکنند؛ بقیه ترافیک مستقیم میماند."] = "Only selected apps and destinations use the tunnel; the rest stays direct.",
|
||||
["پروفایل فعال"] = "Active profile",
|
||||
["پروفایل فعال: {0}"] = "Active profile: {0}",
|
||||
["متصل به پراکسی"] = "Connected to proxy",
|
||||
["متصل به VPN"] = "Connected to VPN",
|
||||
["توقف تست"] = "Stop test",
|
||||
@@ -544,6 +550,7 @@ public sealed class LocalizationService : INotifyPropertyChanged
|
||||
["بررسی نسخه جدید ناموفق بود. اتصال اینترنت یا GitHub را بررسی کنید."] = "Version check failed. Check your internet connection or GitHub access.",
|
||||
["نسخه جدید آماده است: {0} - برای دانلود از GitHub باز کنید."] = "New version available: {0} - open GitHub to download.",
|
||||
["TunnelX بهروز است. نسخه فعلی: {0}"] = "TunnelX is up to date. Current version: {0}",
|
||||
["تعداد نصب این برنامه از گیت هاب: {0}"] = "Installations from GitHub: {0}",
|
||||
["بررسی بروزرسانی به زمان مجاز نرسید."] = "Update check timed out.",
|
||||
["بررسی بروزرسانی ناموفق بود: {0}"] = "Update check failed: {0}",
|
||||
["آماده تست و اتصال"] = "Ready to test and connect",
|
||||
|
||||
@@ -91,6 +91,7 @@ public class ProfileService
|
||||
public bool AutoConnectOnStartup { get; set; } = false;
|
||||
public string? LastActiveProfileId { get; set; } = null;
|
||||
public string Language { get; set; } = LocalizationService.AutoLanguage;
|
||||
public long? GitHubAppDownloadCount { get; set; } = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -161,6 +161,8 @@ public partial class MainViewModel
|
||||
_currentVpnGatewayIp = _vpnService.Status.VpnGatewayIp;
|
||||
_connectionStartTime = DateTime.Now;
|
||||
LastActiveProfileId = _selectedProfile?.Id;
|
||||
OnPropertyChanged(nameof(ConnectedProfileName));
|
||||
OnPropertyChanged(nameof(SelectedProfileSummaryText));
|
||||
RaiseHealthStatusChanged();
|
||||
|
||||
StartTrafficRouterForCurrentStatus(resetAppCounters: true);
|
||||
@@ -172,6 +174,7 @@ public partial class MainViewModel
|
||||
? _vpnService.Status.SingBoxMixedPort
|
||||
: _trafficRouter.Socks5Port;
|
||||
_ = RefreshExitIpAsync(exitIpProxyPort);
|
||||
_ = RefreshGitHubInstallCountAsync(exitIpProxyPort);
|
||||
|
||||
}
|
||||
else
|
||||
@@ -463,6 +466,7 @@ public partial class MainViewModel
|
||||
? _vpnService.Status.SingBoxMixedPort
|
||||
: _trafficRouter.Socks5Port;
|
||||
_ = RefreshExitIpAsync(exitIpProxyPort);
|
||||
_ = RefreshGitHubInstallCountAsync(exitIpProxyPort);
|
||||
if (wasFullRoute)
|
||||
{
|
||||
_isFullRouteEnabled = _trafficRouter.SetFullRouteEnabled(true);
|
||||
|
||||
@@ -73,6 +73,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
OpenOpenVpnCommunityDownloadCommand = new RelayCommand(_ => OpenExternalLink(OpenVpnCommunityDownloadUrl));
|
||||
OpenGitHubCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.GitHubUrl));
|
||||
OpenDonateCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.PayPalDonateUrl));
|
||||
OpenAdRequestCommand = new RelayCommand(_ => OpenExternalLink(AppInfo.TelegramContactUrl));
|
||||
CopyDonationInfoCommand = new RelayCommand(_ => CopyDonationInfoToClipboard());
|
||||
CheckForUpdatesCommand = new RelayCommand(_ => _ = CheckForUpdatesAsync(false), _ => !IsCheckingForUpdates);
|
||||
OpenLatestReleaseCommand = new RelayCommand(_ => OpenExternalLink(LatestReleaseUrl), _ => !string.IsNullOrWhiteSpace(LatestReleaseUrl));
|
||||
@@ -380,6 +381,11 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
public string AppGitHubUrl => AppInfo.GitHubUrl;
|
||||
public string AppLicenseText => AppInfo.LicenseName;
|
||||
public string AppLicenseDisplayText => LocalizationService.Instance.Format("لایسنس: {0}", AppInfo.LicenseName);
|
||||
public string AdPlaceholderTitleText => LocalizationService.Instance.T("محل تبلیغات شما");
|
||||
public string AdRequestButtonText => LocalizationService.Instance.T("درخواست تبلیغ");
|
||||
public string AdAudienceText => _githubInstallCount.HasValue
|
||||
? LocalizationService.Instance.Format("تبلیغ شما میتواند در معرض دید کاربران TunnelX با بیش از {0} نصب از GitHub باشد.", GitHubInstallCountDisplay)
|
||||
: "";
|
||||
public string DonatePayPalText => LocalizationService.Instance.IsRightToLeft
|
||||
? $"پیپل: {AppInfo.PayPalEmail}"
|
||||
: $"PayPal: {AppInfo.PayPalEmail}";
|
||||
@@ -438,6 +444,20 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
private long? _githubInstallCount;
|
||||
private int _githubInstallCountRequestId;
|
||||
|
||||
public bool HasGitHubInstallCount => _githubInstallCount.HasValue;
|
||||
|
||||
public string GitHubInstallCountText => _githubInstallCount.HasValue
|
||||
? LocalizationService.Instance.Format(
|
||||
"تعداد نصب این برنامه از گیت هاب: {0}",
|
||||
GitHubInstallCountDisplay)
|
||||
: "";
|
||||
|
||||
private string GitHubInstallCountDisplay =>
|
||||
(_githubInstallCount ?? 0).ToString("N0", System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
public string UpdateButtonText => IsCheckingForUpdates
|
||||
? LocalizationService.Instance.T("در حال بررسی...")
|
||||
: LocalizationService.Instance.T("بررسی بروزرسانی");
|
||||
@@ -819,6 +839,14 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
? LocalizationService.Instance.T("متصل به پراکسی")
|
||||
: LocalizationService.Instance.T("متصل به VPN");
|
||||
|
||||
public string ConnectedProfileName => string.IsNullOrWhiteSpace(SelectedProfileName)
|
||||
? LocalizationService.Instance.T("پروفایل فعال")
|
||||
: SelectedProfileName;
|
||||
|
||||
public string SelectedProfileSummaryText => LocalizationService.Instance.Format(
|
||||
"پروفایل فعال: {0}",
|
||||
ConnectedProfileName);
|
||||
|
||||
private string ActiveCoreName => CurrentTunnelType switch
|
||||
{
|
||||
TunnelType.L2tpIpsec => "L2TP",
|
||||
@@ -1036,6 +1064,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
public ICommand OpenOpenVpnCommunityDownloadCommand { get; }
|
||||
public ICommand OpenGitHubCommand { get; }
|
||||
public ICommand OpenDonateCommand { get; }
|
||||
public ICommand OpenAdRequestCommand { get; }
|
||||
public ICommand CopyDonationInfoCommand { get; }
|
||||
public ICommand CheckForUpdatesCommand { get; }
|
||||
public ICommand OpenLatestReleaseCommand { get; }
|
||||
@@ -1134,6 +1163,61 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshGitHubInstallCountAsync(int proxyPort)
|
||||
{
|
||||
var requestId = ++_githubInstallCountRequestId;
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(12));
|
||||
var count = await GitHubReleaseChecker.GetAppDownloadCountAsync(cts.Token, proxyPort);
|
||||
if (requestId != _githubInstallCountRequestId || !IsConnected)
|
||||
return;
|
||||
|
||||
if (count.HasValue)
|
||||
{
|
||||
SetGitHubInstallCount(count.Value, persist: true);
|
||||
Logger.Info($"[GITHUB-STATS] App downloads={count.Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning("[GITHUB-STATS] App download count unavailable");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.Warning("[GITHUB-STATS] App download count timed out");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"[GITHUB-STATS] App download count failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetGitHubInstallCount(long? count, bool persist = false)
|
||||
{
|
||||
if (_githubInstallCount == count)
|
||||
{
|
||||
if (persist && _appSettings.GitHubAppDownloadCount != count)
|
||||
{
|
||||
_appSettings.GitHubAppDownloadCount = count;
|
||||
_profileService.SaveAppSettings(_appSettings);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_githubInstallCount = count;
|
||||
if (persist)
|
||||
{
|
||||
_appSettings.GitHubAppDownloadCount = count;
|
||||
_profileService.SaveAppSettings(_appSettings);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasGitHubInstallCount));
|
||||
OnPropertyChanged(nameof(GitHubInstallCountText));
|
||||
OnPropertyChanged(nameof(AdAudienceText));
|
||||
}
|
||||
|
||||
private void PasteConfigFromClipboard()
|
||||
{
|
||||
try
|
||||
@@ -1347,6 +1431,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
|
||||
_selectedProfile.Name = remark;
|
||||
OnPropertyChanged(nameof(SelectedProfileName));
|
||||
OnPropertyChanged(nameof(ConnectedProfileName));
|
||||
OnPropertyChanged(nameof(SelectedProfileSummaryText));
|
||||
}
|
||||
|
||||
private static string ExtractConfigRemark(string config)
|
||||
@@ -1395,8 +1481,12 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
LocalizationService.Instance.Initialize(_appSettings.Language);
|
||||
_startWithWindows = _appSettings.StartWithWindows;
|
||||
_autoConnectOnStartup = _appSettings.AutoConnectOnStartup;
|
||||
_githubInstallCount = _appSettings.GitHubAppDownloadCount;
|
||||
OnPropertyChanged(nameof(StartWithWindows));
|
||||
OnPropertyChanged(nameof(AutoConnectOnStartup));
|
||||
OnPropertyChanged(nameof(HasGitHubInstallCount));
|
||||
OnPropertyChanged(nameof(GitHubInstallCountText));
|
||||
OnPropertyChanged(nameof(AdAudienceText));
|
||||
OnPropertyChanged(nameof(LanguageToggleText));
|
||||
OnPropertyChanged(nameof(AppIsRightToLeft));
|
||||
OnPropertyChanged(nameof(AppFlowDirection));
|
||||
@@ -1429,8 +1519,12 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
OnPropertyChanged(nameof(DonatePayPalText));
|
||||
OnPropertyChanged(nameof(CryptoDonationText));
|
||||
OnPropertyChanged(nameof(AppCreatorText));
|
||||
OnPropertyChanged(nameof(AdPlaceholderTitleText));
|
||||
OnPropertyChanged(nameof(AdRequestButtonText));
|
||||
OnPropertyChanged(nameof(AdAudienceText));
|
||||
OnPropertyChanged(nameof(UpdateButtonText));
|
||||
OnPropertyChanged(nameof(UpdateStatusText));
|
||||
OnPropertyChanged(nameof(GitHubInstallCountText));
|
||||
OnPropertyChanged(nameof(ConnectButtonText));
|
||||
OnPropertyChanged(nameof(ConnectButtonToolTip));
|
||||
OnPropertyChanged(nameof(StatusText));
|
||||
@@ -1452,6 +1546,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
OnPropertyChanged(nameof(HealthIpv6Text));
|
||||
OnPropertyChanged(nameof(HealthRoutesText));
|
||||
OnPropertyChanged(nameof(ConnectedBadgeText));
|
||||
OnPropertyChanged(nameof(ConnectedProfileName));
|
||||
OnPropertyChanged(nameof(SelectedProfileSummaryText));
|
||||
OnPropertyChanged(nameof(PingButtonText));
|
||||
OnPropertyChanged(nameof(ConnectedServerPingButtonText));
|
||||
OnPropertyChanged(nameof(ServerPingButtonText));
|
||||
|
||||
@@ -24,6 +24,8 @@ public partial class MainViewModel
|
||||
_selectedProfile = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(SelectedProfileName));
|
||||
OnPropertyChanged(nameof(ConnectedProfileName));
|
||||
OnPropertyChanged(nameof(SelectedProfileSummaryText));
|
||||
RaiseProfileCardChanged();
|
||||
if (value != null)
|
||||
LoadProfileIntoUi(value);
|
||||
@@ -86,6 +88,8 @@ public partial class MainViewModel
|
||||
_selectedProfile = Profiles[0];
|
||||
OnPropertyChanged(nameof(SelectedProfile));
|
||||
OnPropertyChanged(nameof(SelectedProfileName));
|
||||
OnPropertyChanged(nameof(ConnectedProfileName));
|
||||
OnPropertyChanged(nameof(SelectedProfileSummaryText));
|
||||
LoadProfileIntoUi(Profiles[0]);
|
||||
}
|
||||
|
||||
@@ -253,7 +257,7 @@ public partial class MainViewModel
|
||||
SaveCurrentProfileState();
|
||||
var profile = new ConnectionProfile
|
||||
{
|
||||
Name = LocalizationService.Instance.Format("پروفایل {0}", Profiles.Count + 1),
|
||||
Name = "",
|
||||
MixedProxyPort = MixedProxyPort,
|
||||
AutoTuneMtu = AutoTuneMtu,
|
||||
EnableDnsOptimization = IsDnsOptimizationEnabled,
|
||||
@@ -379,6 +383,8 @@ public partial class MainViewModel
|
||||
private void RaiseProfileCardChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(ProfileCountText));
|
||||
OnPropertyChanged(nameof(ConnectedProfileName));
|
||||
OnPropertyChanged(nameof(SelectedProfileSummaryText));
|
||||
OnPropertyChanged(nameof(ActiveProfileTypeText));
|
||||
OnPropertyChanged(nameof(ActiveProfileEndpointText));
|
||||
OnPropertyChanged(nameof(ProfileSaveHintText));
|
||||
|
||||
@@ -252,12 +252,11 @@
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
<TextBlock FontSize="10"
|
||||
Text="{Binding SelectedProfileSummaryText}"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,4,0,0"
|
||||
TextTrimming="CharacterEllipsis">
|
||||
<Run Text="پروفایل فعال: "/>
|
||||
<Run Text="{Binding SelectedProfile.Name}" FontWeight="SemiBold"/>
|
||||
</TextBlock>
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
@@ -671,10 +670,13 @@
|
||||
Foreground="{StaticResource SuccessBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,2,0,0"/>
|
||||
<TextBlock Text="{Binding SelectedProfile.Name}" FontSize="9"
|
||||
<TextBlock Text="{Binding ConnectedProfileName}" FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
TextAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxWidth="120"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -909,6 +911,48 @@
|
||||
</Button.Style>
|
||||
</Button>
|
||||
|
||||
<Border Background="#24E8803A"
|
||||
BorderBrush="#66E8803A"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Padding="24,18"
|
||||
Margin="0,16,0,0"
|
||||
MinWidth="560"
|
||||
MaxWidth="760"
|
||||
HorizontalAlignment="Stretch">
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FlowDirection="{Binding AppFlowDirection}">
|
||||
<TextBlock Text="{Binding AdPlaceholderTitleText}"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"/>
|
||||
<TextBlock Text="{Binding AdAudienceText}"
|
||||
Visibility="{Binding HasGitHubInstallCount, Converter={StaticResource BoolToVis}}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="560"
|
||||
Margin="0,6,0,0"/>
|
||||
<Button
|
||||
Content="{Binding AdRequestButtonText}"
|
||||
Command="{Binding OpenAdRequestCommand}"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
Padding="18,10"
|
||||
FontSize="13"
|
||||
MinWidth="130"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,12,0,0"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="{Binding AdRequestButtonText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
<!-- End Connected State -->
|
||||
|
||||
|
||||
@@ -73,10 +73,20 @@
|
||||
<Border Style="{StaticResource CardPanel}" Padding="12" Margin="0,0,0,8">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="اطلاعات پروفایل"/>
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="نام پروفایل"/>
|
||||
<TextBox Style="{StaticResource ModernTextBox}"
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="نام پروفایل *"/>
|
||||
<TextBox x:Name="ProfileNameField"
|
||||
Style="{StaticResource ModernTextBox}"
|
||||
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
|
||||
Tag="مثلاً کار، تلگرام، گیمینگ..."/>
|
||||
Tag="مثلاً کار، تلگرام، گیمینگ..."
|
||||
TextChanged="OnProfileNameTextChanged"/>
|
||||
<TextBlock x:Name="ProfileNameValidationText"
|
||||
Text="نام پروفایل را وارد کنید"
|
||||
Visibility="Collapsed"
|
||||
FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource WarningBrush}"
|
||||
Margin="0,4,0,0"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="نوع اتصال" Margin="0,8,0,3"/>
|
||||
<ComboBox Style="{StaticResource DarkComboBox}"
|
||||
|
||||
@@ -79,6 +79,13 @@ public partial class ProfileEditorDialog : Window
|
||||
|
||||
private void OnSaveClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_profile.Name = (_profile.Name ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(_profile.Name))
|
||||
{
|
||||
ShowProfileNameError();
|
||||
return;
|
||||
}
|
||||
|
||||
_profile.Password = L2tpPasswordField.Password;
|
||||
_profile.PreSharedKey = PskField.Password;
|
||||
_profile.OpenVpnPassword = OpenVpnPasswordField.Password;
|
||||
@@ -125,6 +132,30 @@ public partial class ProfileEditorDialog : Window
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnProfileNameTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ProfileNameField.Text))
|
||||
ClearProfileNameError();
|
||||
}
|
||||
|
||||
private void ShowProfileNameError()
|
||||
{
|
||||
var warningBrush = TryFindResource("WarningBrush") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Orange;
|
||||
ProfileNameField.BorderBrush = warningBrush;
|
||||
ProfileNameField.BorderThickness = new Thickness(2);
|
||||
ProfileNameValidationText.Text = LocalizationService.Instance.T("نام پروفایل را وارد کنید");
|
||||
ProfileNameValidationText.Visibility = Visibility.Visible;
|
||||
ValidationText.Text = "";
|
||||
ProfileNameField.Focus();
|
||||
}
|
||||
|
||||
private void ClearProfileNameError()
|
||||
{
|
||||
ProfileNameField.ClearValue(BorderBrushProperty);
|
||||
ProfileNameField.ClearValue(BorderThicknessProperty);
|
||||
ProfileNameValidationText.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void OnCancelClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
|
||||
@@ -136,6 +136,8 @@ dotnet publish AppTunnel\AppTunnel.csproj -c Release -r win-x64 --self-contained
|
||||
|
||||
خدمات پولی میتواند به صورت جداگانه برای پشتیبانی خصوصی، راهاندازی، بیلد اختصاصی، سفارشیسازی برای شرکتها، یا توسعه برنامهای مشابه ارائه شود. این خدمات پولی حقوقی را که مجوز <span dir="ltr">GPL</span> به کاربران میدهد محدود نمیکند.
|
||||
|
||||
پذیرش تبلیغات ثابت داخل <span dir="ltr">TunnelX</span> امکانپذیر است. تبلیغات بهصورت مستقیم با نگهدارنده هماهنگ میشود، از طریق شبکههای تبلیغاتی یا سایتهای واسط نمایش داده نمیشود و با هدف ساده، ثابت و امن ماندن تجربه کاربر انجام میشود.
|
||||
|
||||
گزینههای حمایت مالی از طریق <span dir="ltr">GitHub Sponsors/Funding</span> یا فایل <span dir="ltr">`docs/DONATE.md`</span> در دسترس هستند.
|
||||
|
||||
## نکته ایمنی و سلب مسئولیت
|
||||
|
||||
@@ -146,6 +146,8 @@ For direct contact, support requests, private customization, or paid development
|
||||
|
||||
Paid services may be available separately for private support, deployment help, custom builds, company-specific customization, or development of a similar application. These paid services do not limit the rights granted by the GPL license.
|
||||
|
||||
Fixed advertising placements may be available inside TunnelX. Advertising is handled directly with the maintainer, is not served through third-party ad networks or intermediary websites, and is intended to stay simple, static, and safe for users.
|
||||
|
||||
Use the GitHub funding button or see `docs/DONATE.md` for donation options.
|
||||
|
||||
## Safety Notice
|
||||
|
||||
Reference in New Issue
Block a user