Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfaf2ae481 | |||
| ae61021c7c | |||
| 3145b9a01e | |||
| 04d8d52ba9 | |||
| bae98cf9e9 | |||
| 2ca962ddd6 | |||
| 0bc8992813 | |||
| b713074ee2 |
@@ -0,0 +1,13 @@
|
||||
---
|
||||
description: WPF localization and direction rules for TunnelX
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# TunnelX Localization Rules
|
||||
|
||||
- Treat Persian XAML literals as source keys. Every visible `Text`, `Content`, `Header`, `ToolTip`, `Tag`, and `Run.Text` literal must have an English entry in `LocalizationService.EnglishTranslations()`.
|
||||
- Prefer bindings for dynamic UI text. Expose localization-aware ViewModel properties for stateful text, and raise `PropertyChanged` for them from `OnLanguageChanged()`.
|
||||
- For DataTemplate, lazy-loaded, and virtualized controls, do not rely on one-time traversal only. Ensure controls can be re-localized on `Loaded` and after language changes.
|
||||
- Preserve the original source string. If a control is first seen while English is active, recover the Persian source key from the reverse translation table instead of storing the English value as the original.
|
||||
- Bind layout direction to `AppFlowDirection` or `LocalizationService.FlowDirection`. Keep technical fields (`IP`, ports, paths, config, logs, crypto addresses) explicitly `LeftToRight` and left-aligned.
|
||||
- After adding UI, test both language directions by toggling Persian -> English -> Persian once and checking newly loaded tabs/templates.
|
||||
@@ -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,22 @@ 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 = @(
|
||||
"## دانلودها",
|
||||
"",
|
||||
"- پیشنهادشده برای بیشتر کاربران: ``$artifactName``. این نسخه مستقل است و نیازی به نصب جداگانه .NET ندارد.",
|
||||
"- بسته کمحجمتر برای کاربرانی که Microsoft .NET 8 Desktop Runtime نسخه x64 را نصب دارند: ``$frameworkArtifactName``. فایل ZIP را استخراج کنید و ``TunnelX.exe`` را با دسترسی Administrator اجرا کنید. داخل ZIP یک ``README.txt`` هم برای توضیح همین پیشنیاز قرار دارد.",
|
||||
"",
|
||||
"## 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 +317,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 +329,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"
|
||||
|
||||
@@ -63,6 +63,11 @@ public partial class App : Application
|
||||
// registered and before any DllImport call is made.
|
||||
EnsureNativeLibsExtracted();
|
||||
|
||||
// Load the language before the main window is created so the first
|
||||
// rendered frame follows the saved setting or the system UI language.
|
||||
LocalizationService.Instance.Initialize(new ProfileService().LoadAppSettings().Language);
|
||||
TextInputBehavior.Register();
|
||||
|
||||
// Register a resolver so that DllImport("WinDivert.dll") and
|
||||
// DllImport("wintun.dll") load from AppDataDir rather than relying on
|
||||
// the default search order (which would only find them next to the exe).
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
|
||||
<NeutralLanguage>fa-IR</NeutralLanguage>
|
||||
<!-- Version Management -->
|
||||
<Version>1.2.28</Version>
|
||||
<AssemblyVersion>1.2.28.0</AssemblyVersion>
|
||||
<FileVersion>1.2.28.0</FileVersion>
|
||||
<Version>1.2.32</Version>
|
||||
<AssemblyVersion>1.2.32.0</AssemblyVersion>
|
||||
<FileVersion>1.2.32.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using AppTunnel.Services;
|
||||
using Color = System.Windows.Media.Color;
|
||||
using ColorConverter = System.Windows.Media.ColorConverter;
|
||||
using FlowDirection = System.Windows.FlowDirection;
|
||||
@@ -85,9 +86,7 @@ public class EnumToVisibilityConverter : IValueConverter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تشخیص خودکار جهت متن بر اساس محتوا
|
||||
/// اگر متن شامل حروف فارسی یا عربی باشد: RightToLeft
|
||||
/// اگر متن شامل حروف انگلیسی باشد: LeftToRight
|
||||
/// Detects text direction from content and falls back to the current app language.
|
||||
/// </summary>
|
||||
public class TextToFlowDirectionConverter : IValueConverter
|
||||
{
|
||||
@@ -95,24 +94,20 @@ public class TextToFlowDirectionConverter : IValueConverter
|
||||
{
|
||||
if (value is string text && !string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
// بررسی اولین کاراکتر معنادار
|
||||
foreach (char c in text)
|
||||
{
|
||||
if (char.IsWhiteSpace(c) || char.IsPunctuation(c) || char.IsDigit(c))
|
||||
continue;
|
||||
|
||||
// محدوده Unicode برای فارسی و عربی: 0x0600-0x06FF
|
||||
if (c >= 0x0600 && c <= 0x06FF)
|
||||
return FlowDirection.RightToLeft;
|
||||
|
||||
// محدوده Unicode برای حروف لاتین: A-Z, a-z
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
|
||||
return FlowDirection.LeftToRight;
|
||||
}
|
||||
}
|
||||
|
||||
// پیشفرض: راست به چپ (برای اپلیکیشن فارسی)
|
||||
return FlowDirection.RightToLeft;
|
||||
return LocalizationService.Instance.FlowDirection;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Windows;
|
||||
using AppTunnel.Views;
|
||||
using AppTunnel.Services;
|
||||
using Application = System.Windows.Application;
|
||||
|
||||
namespace AppTunnel.Helpers;
|
||||
@@ -19,7 +20,7 @@ public static class DialogService
|
||||
/// <returns>true اگر کاربر "بله" را انتخاب کند</returns>
|
||||
public static bool Confirm(string message, string title = "تاییدیه", Window? owner = null)
|
||||
{
|
||||
return ModernDialog.ShowConfirm(message, title, owner);
|
||||
return ModernDialog.ShowConfirm(LocalizationService.Instance.T(message), LocalizationService.Instance.T(title), owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -27,7 +28,7 @@ public static class DialogService
|
||||
/// </summary>
|
||||
public static void Info(string message, string title = "اطلاعات", Window? owner = null)
|
||||
{
|
||||
ModernDialog.ShowInfo(message, title, owner);
|
||||
ModernDialog.ShowInfo(LocalizationService.Instance.T(message), LocalizationService.Instance.T(title), owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -35,7 +36,7 @@ public static class DialogService
|
||||
/// </summary>
|
||||
public static void Success(string message, string title = "موفقیت", Window? owner = null)
|
||||
{
|
||||
ModernDialog.ShowSuccess(message, title, owner);
|
||||
ModernDialog.ShowSuccess(LocalizationService.Instance.T(message), LocalizationService.Instance.T(title), owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +44,7 @@ public static class DialogService
|
||||
/// </summary>
|
||||
public static void Error(string message, string title = "خطا", Window? owner = null)
|
||||
{
|
||||
ModernDialog.ShowError(message, title, owner);
|
||||
ModernDialog.ShowError(LocalizationService.Instance.T(message), LocalizationService.Instance.T(title), owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,7 +52,7 @@ public static class DialogService
|
||||
/// </summary>
|
||||
public static void Warning(string message, string title = "هشدار", Window? owner = null)
|
||||
{
|
||||
ModernDialog.ShowWarning(message, title, owner);
|
||||
ModernDialog.ShowWarning(LocalizationService.Instance.T(message), LocalizationService.Instance.T(title), owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -59,7 +60,7 @@ public static class DialogService
|
||||
/// </summary>
|
||||
public static void ShowCopied(string itemName = "متن")
|
||||
{
|
||||
ShowToast($"{itemName} کپی شد", "✅");
|
||||
ShowToast(LocalizationService.Instance.Format("{0} کپی شد", itemName), "✅");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,7 +70,7 @@ public static class DialogService
|
||||
{
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.ShowToast(message, icon);
|
||||
mainWindow.ShowToast(LocalizationService.Instance.T(message), icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Window x:Class="AppTunnel.LogWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:services="clr-namespace:AppTunnel.Services"
|
||||
Title="TunnelX — جزئیات عملکرد"
|
||||
Width="400" Height="600"
|
||||
MinWidth="350" MinHeight="400"
|
||||
@@ -8,7 +9,7 @@
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
FlowDirection="RightToLeft"
|
||||
FlowDirection="{Binding Source={x:Static services:LocalizationService.Instance}, Path=FlowDirection}"
|
||||
MouseLeftButtonDown="OnTitleBarMouseDown">
|
||||
|
||||
<Border Background="{StaticResource BackgroundBrush}"
|
||||
@@ -31,19 +32,26 @@
|
||||
CornerRadius="12,12,0,0"
|
||||
Padding="16,12"
|
||||
MouseLeftButtonDown="OnTitleBarMouseDown">
|
||||
<Grid>
|
||||
<TextBlock Text="🔍 جزئیات عملکرد — TunnelX"
|
||||
<Grid FlowDirection="{Binding Source={x:Static services:LocalizationService.Instance}, Path=FlowDirection}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="🔍 جزئیات عملکرد — TunnelX"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
VerticalAlignment="Center"/>
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
|
||||
<Button Content="✕"
|
||||
<Button Grid.Column="1"
|
||||
Content="✕"
|
||||
Click="OnCloseClick"
|
||||
Style="{StaticResource WindowControlButton}"
|
||||
Width="32" Height="24"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Right"
|
||||
HorizontalAlignment="{Binding Source={x:Static services:LocalizationService.Instance}, Path=EndHorizontalAlignment}"
|
||||
FlowDirection="LeftToRight"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -60,6 +68,7 @@
|
||||
IsReadOnly="True"
|
||||
TextWrapping="Wrap"
|
||||
FlowDirection="LeftToRight"
|
||||
TextAlignment="Left"
|
||||
VerticalScrollBarVisibility="Auto"/>
|
||||
</Border>
|
||||
|
||||
@@ -67,7 +76,9 @@
|
||||
<Border Grid.Row="2" Background="{StaticResource SurfaceBrush}"
|
||||
CornerRadius="0,0,12,12"
|
||||
Padding="12,10">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="{Binding Source={x:Static services:LocalizationService.Instance}, Path=EndHorizontalAlignment}"
|
||||
FlowDirection="{Binding Source={x:Static services:LocalizationService.Instance}, Path=FlowDirection}">
|
||||
<Button Content="🗑 پاک کردن"
|
||||
Click="OnClearClick"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
|
||||
@@ -11,9 +11,20 @@ public partial class LogWindow : Window
|
||||
public LogWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => LocalizationService.Instance.ApplyTo(this);
|
||||
LocalizationService.Instance.LanguageChanged += OnLanguageChanged;
|
||||
LoadLogs();
|
||||
Logger.LogAdded += OnLogAdded;
|
||||
Closed += (_, _) => Logger.LogAdded -= OnLogAdded;
|
||||
Closed += (_, _) =>
|
||||
{
|
||||
Logger.LogAdded -= OnLogAdded;
|
||||
LocalizationService.Instance.LanguageChanged -= OnLanguageChanged;
|
||||
};
|
||||
}
|
||||
|
||||
private void OnLanguageChanged(object? sender, EventArgs e)
|
||||
{
|
||||
LocalizationService.Instance.ApplyTo(this);
|
||||
}
|
||||
|
||||
private void LoadLogs()
|
||||
@@ -46,19 +57,26 @@ public partial class LogWindow : Window
|
||||
{
|
||||
Logger.Clear();
|
||||
LogTextBox.Clear();
|
||||
DialogService.Success(LocalizationService.Instance.T("لاگها پاک شدند"));
|
||||
}
|
||||
|
||||
private void OnCopyClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(LogTextBox.Text))
|
||||
{
|
||||
DialogService.Info(LocalizationService.Instance.T("لاگی برای کپی وجود ندارد"), "TunnelX");
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.SetText(LogTextBox.Text);
|
||||
DialogService.ShowCopied("لاگها");
|
||||
DialogService.ShowCopied(LocalizationService.Instance.T("لاگها"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Failed to copy logs: {ex}");
|
||||
DialogService.Error($"خطا در کپی کردن:\n{ex.Message}", "خطا");
|
||||
DialogService.Error(LocalizationService.Instance.Format("خطا در کپی کردن:\n{0}", ex.Message), "خطا");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
xmlns:vm="clr-namespace:AppTunnel.ViewModels"
|
||||
xmlns:model="clr-namespace:AppTunnel.Models"
|
||||
Title="TunnelX — Split Traffic Per App"
|
||||
Width="620" Height="760"
|
||||
MinWidth="580" MinHeight="680"
|
||||
Width="620" Height="740"
|
||||
MinWidth="560" MinHeight="620"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
FlowDirection="RightToLeft"
|
||||
FlowDirection="{Binding AppFlowDirection}"
|
||||
MouseLeftButtonDown="OnTitleBarMouseDown">
|
||||
|
||||
<Window.Resources>
|
||||
@@ -43,7 +44,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ═══ MAIN CONTENT ═══ -->
|
||||
<Grid Grid.Column="0" FlowDirection="RightToLeft">
|
||||
<Grid Grid.Column="0" FlowDirection="{Binding AppFlowDirection}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
@@ -51,7 +52,8 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ CUSTOM TITLE BAR ═══ -->
|
||||
<Border Grid.Row="0" Background="{StaticResource SurfaceBrush}"
|
||||
<Border x:Name="MainTitleBar"
|
||||
Grid.Row="0" Background="{StaticResource SurfaceBrush}"
|
||||
BorderBrush="#12FFFFFF"
|
||||
BorderThickness="0,0,0,1"
|
||||
CornerRadius="12,0,0,0" Padding="14,8">
|
||||
@@ -61,27 +63,53 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Logo & Title -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<!-- Tx Icon SVG -->
|
||||
<Viewbox Width="30" Height="30" Margin="0,0,10,0">
|
||||
<Canvas Width="48" Height="48">
|
||||
<Rectangle Width="48" Height="48" RadiusX="8" RadiusY="8" Fill="{StaticResource PrimaryBrush}"/>
|
||||
<TextBlock Text="Tx" FontSize="24" FontWeight="Bold"
|
||||
Foreground="White" Canvas.Left="6" Canvas.Top="8"
|
||||
FontFamily="Segoe UI"/>
|
||||
</Canvas>
|
||||
<Grid Width="48" Height="48">
|
||||
<Border CornerRadius="7"
|
||||
Background="{StaticResource PrimaryBrush}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="#C2410C"
|
||||
BlurRadius="8"
|
||||
Opacity="0.22"
|
||||
ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<TextBlock Text="Tx"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"
|
||||
FontFamily="Segoe UI"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,-1,0,0"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"/>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
<StackPanel VerticalAlignment="Bottom" Margin="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Tunnel" FontSize="17" FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="X" FontSize="17" FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentBrush}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="Per-app split tunneling" FontSize="9"
|
||||
<Grid HorizontalAlignment="Left">
|
||||
<TextBlock Text="{Binding AppTitleText}"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
FlowDirection="{Binding AppTitleFlowDirection}"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"/>
|
||||
<TextBlock Text="{Binding AppTitleAccentText}"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FlowDirection="{Binding AppTitleFlowDirection}"
|
||||
HorizontalAlignment="{Binding AppTitleAccentAlignment}"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"/>
|
||||
</Grid>
|
||||
<TextBlock Text="تفکیک ترافیک برنامه ها" FontSize="9"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,-3,0,0"/>
|
||||
</StackPanel>
|
||||
@@ -124,8 +152,69 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Update Button: visible only when a newer release exists -->
|
||||
<Button Grid.Column="2"
|
||||
Content="بروزرسانی"
|
||||
Command="{Binding OpenLatestReleaseCommand}"
|
||||
Visibility="{Binding IsUpdateAvailable, Converter={StaticResource BoolToVis}}"
|
||||
Background="{StaticResource PrimaryBrush}"
|
||||
Foreground="White"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="10,6"
|
||||
Margin="0,0,8,0"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Cursor="Hand"
|
||||
VerticalAlignment="Center"
|
||||
RenderTransformOrigin="0.5,0.5"
|
||||
ToolTip="دانلود نسخه جدید از صفحه Releases در GitHub">
|
||||
<Button.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1"/>
|
||||
</Button.RenderTransform>
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="9"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsUpdateAvailable}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard RepeatBehavior="Forever" AutoReverse="True">
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||
To="0.72"
|
||||
Duration="0:0:0.9"/>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
|
||||
To="1.04"
|
||||
Duration="0:0:0.9"/>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
|
||||
To="1.04"
|
||||
Duration="0:0:0.9"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="0.92"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
|
||||
<!-- Details Button -->
|
||||
<Button Grid.Column="2" Content="جزئیات"
|
||||
<Button Grid.Column="3" Content="جزئیات"
|
||||
Click="OnShowLogClick"
|
||||
Background="#121212"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
@@ -160,7 +249,7 @@
|
||||
</Button>
|
||||
|
||||
<!-- Window Controls: Minimize → Close (standard Windows order) -->
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal">
|
||||
<StackPanel Grid.Column="4" Orientation="Horizontal">
|
||||
<Button Content="—" Click="OnMinimizeClick"
|
||||
ToolTip="کوچک کردن به System Tray"
|
||||
Style="{StaticResource WindowControlButton}"
|
||||
@@ -186,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/>
|
||||
@@ -195,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/>
|
||||
@@ -205,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/>
|
||||
@@ -216,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>
|
||||
|
||||
@@ -459,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>
|
||||
|
||||
@@ -664,18 +753,30 @@
|
||||
</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>
|
||||
|
||||
<!-- ═══ FOOTER ═══ -->
|
||||
<Border Grid.Row="2"
|
||||
<Border x:Name="MainFooterBar"
|
||||
Grid.Row="2"
|
||||
Background="#121212"
|
||||
BorderBrush="#18FFFFFF"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="0,0,0,12"
|
||||
Padding="12,6">
|
||||
<Grid VerticalAlignment="Center" FlowDirection="RightToLeft">
|
||||
<Grid VerticalAlignment="Center" FlowDirection="{Binding AppFlowDirection}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -685,8 +786,8 @@
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
FlowDirection="RightToLeft"
|
||||
HorizontalAlignment="Right"
|
||||
FlowDirection="{Binding AppFlowDirection}"
|
||||
HorizontalAlignment="{Binding AppStartHorizontalAlignment}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="ساخته شده توسط Maxifan"
|
||||
FontSize="10"
|
||||
@@ -711,36 +812,39 @@
|
||||
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="Left"
|
||||
HorizontalAlignment="{Binding AppEndHorizontalAlignment}"
|
||||
VerticalAlignment="Center"
|
||||
FlowDirection="RightToLeft">
|
||||
<Button Content="بروزرسانی"
|
||||
Command="{Binding OpenLatestReleaseCommand}"
|
||||
Visibility="{Binding IsUpdateAvailable, Converter={StaticResource BoolToVis}}"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
Padding="10,4"
|
||||
FlowDirection="{Binding AppFlowDirection}">
|
||||
<Button Content="{Binding LanguageToggleText}"
|
||||
Command="{Binding ToggleLanguageCommand}"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Padding="9,4"
|
||||
FontSize="10"
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="دانلود نسخه جدید از صفحه Releases در GitHub"/>
|
||||
<Button Content="راهنما"
|
||||
Click="OnShowHelpClick"
|
||||
ToolTip="تغییر زبان"/>
|
||||
<Button Content="حمایت"
|
||||
Click="OnDonateClick"
|
||||
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"/>
|
||||
ToolTip="حمایت از پروژه"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -749,8 +853,12 @@
|
||||
|
||||
<!-- ═══ LOG PANEL (RIGHT SIDE) ═══ -->
|
||||
<Border x:Name="LogPanel" Grid.Column="1" Visibility="Collapsed"
|
||||
Width="0"
|
||||
Opacity="0"
|
||||
ClipToBounds="True"
|
||||
Background="{StaticResource SurfaceBrush}"
|
||||
BorderBrush="{StaticResource CardBrush}" BorderThickness="1,0,0,0">
|
||||
BorderBrush="{StaticResource CardBrush}" BorderThickness="1,0,0,0"
|
||||
FlowDirection="{Binding AppFlowDirection}">
|
||||
<Grid Width="350">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
@@ -758,21 +866,31 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Log Header -->
|
||||
<Border Grid.Row="0" Padding="12,10" Background="#11FFFFFF">
|
||||
<Grid>
|
||||
<Border Grid.Row="0" Padding="8,6" Background="#11FFFFFF">
|
||||
<Grid FlowDirection="{Binding AppFlowDirection}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="🔍 جزئیات عملکرد" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
|
||||
Text="🔍 جزئیات عملکرد" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,0,4"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<ComboBox x:Name="LogFilterCombo"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
SelectedIndex="0"
|
||||
SelectionChanged="OnLogFilterChanged"
|
||||
Margin="0,0,6,0"
|
||||
MinWidth="86"
|
||||
Margin="0,0,4,0"
|
||||
MinWidth="74"
|
||||
HorizontalAlignment="{Binding AppStartHorizontalAlignment}"
|
||||
FontSize="10"
|
||||
Style="{StaticResource DarkComboBox}">
|
||||
<ComboBoxItem Content="همه" Tag="All"/>
|
||||
<ComboBoxItem Content="خطا" Tag="Error"/>
|
||||
@@ -780,16 +898,19 @@
|
||||
<ComboBoxItem Content="DNS" Tag="Dns"/>
|
||||
<ComboBoxItem Content="Route" Tag="Route"/>
|
||||
</ComboBox>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||
<Button Content="🗑" ToolTip="پاک کردن" Click="OnLogClearClick"
|
||||
<StackPanel Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
FlowDirection="{Binding AppFlowDirection}">
|
||||
<Button Content="{Binding LogClearButtonText}" ToolTip="{Binding LogClearToolTipText}" Click="OnLogClearClick"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Padding="8,4" FontSize="12" Margin="0,0,4,0"/>
|
||||
<Button Content="⚠" ToolTip="کپی آخرین خطا یا هشدار" Click="OnLogCopyLastErrorClick"
|
||||
Padding="6,3" FontSize="9.5" Margin="0,0,3,0"/>
|
||||
<Button Content="{Binding LogCopyErrorButtonText}" ToolTip="{Binding LogCopyErrorToolTipText}" Click="OnLogCopyLastErrorClick"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Padding="8,4" FontSize="12" Margin="0,0,4,0"/>
|
||||
<Button Content="📋" ToolTip="کپی کردن" Click="OnLogCopyClick"
|
||||
Padding="6,3" FontSize="9.5" Margin="0,0,3,0"/>
|
||||
<Button Content="{Binding LogCopyAllButtonText}" ToolTip="{Binding LogCopyAllToolTipText}" Click="OnLogCopyClick"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Padding="8,4" FontSize="12"/>
|
||||
Padding="6,3" FontSize="9.5"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -805,6 +926,7 @@
|
||||
IsReadOnly="True"
|
||||
TextWrapping="Wrap"
|
||||
FlowDirection="LeftToRight"
|
||||
TextAlignment="Left"
|
||||
VerticalScrollBarVisibility="Auto"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -835,21 +957,39 @@
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<!-- Logo -->
|
||||
<Viewbox Width="60" Height="60" HorizontalAlignment="Center" Margin="0,0,0,20">
|
||||
<Canvas Width="48" Height="48">
|
||||
<Rectangle Width="48" Height="48" RadiusX="8" RadiusY="8"
|
||||
Fill="{StaticResource PrimaryBrush}"/>
|
||||
<TextBlock Text="Tx" FontSize="24" FontWeight="Bold"
|
||||
Foreground="White" Canvas.Left="6" Canvas.Top="8"
|
||||
FontFamily="Segoe UI"/>
|
||||
</Canvas>
|
||||
<Grid Width="48" Height="48">
|
||||
<Border CornerRadius="7"
|
||||
Background="{StaticResource PrimaryBrush}"/>
|
||||
<TextBlock Text="Tx"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"
|
||||
FontFamily="Segoe UI"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,-1,0,0"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"/>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
<!-- App name -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,6">
|
||||
<TextBlock Text="Tunnel" FontSize="24" FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="X" FontSize="24" FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentBrush}"/>
|
||||
</StackPanel>
|
||||
<Grid HorizontalAlignment="Center" Margin="0,0,0,6">
|
||||
<TextBlock Text="{Binding AppTitleText}"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
FlowDirection="{Binding AppTitleFlowDirection}"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"/>
|
||||
<TextBlock Text="{Binding AppTitleAccentText}"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FlowDirection="{Binding AppTitleFlowDirection}"
|
||||
HorizontalAlignment="{Binding AppTitleAccentAlignment}"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"/>
|
||||
</Grid>
|
||||
<TextBlock Text="Split Traffic Per App" FontSize="11"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media;
|
||||
using AppTunnel.Models;
|
||||
@@ -19,18 +20,77 @@ public partial class MainWindow : Window
|
||||
private bool _isRealExit;
|
||||
private ConnectionState _lastNotifiedConnectionState = ConnectionState.Disconnected;
|
||||
private bool _updateNotificationShown;
|
||||
private const int WmSysCommand = 0x0112;
|
||||
private const int WmGetMinMaxInfo = 0x0024;
|
||||
private const int ScSize = 0xF000;
|
||||
private const int ScMove = 0xF010;
|
||||
private const int ScMaximize = 0xF030;
|
||||
private const int ScKeyMenu = 0xF100;
|
||||
private const int ScRestore = 0xF120;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = new MainViewModel();
|
||||
DataContext = _viewModel;
|
||||
LocalizationService.Instance.ApplyTo(this);
|
||||
LocalizationService.Instance.LanguageChanged += (_, _) =>
|
||||
{
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
LocalizationService.Instance.ApplyTo(this);
|
||||
RefreshTrayText();
|
||||
});
|
||||
};
|
||||
|
||||
InitializeTrayIcon();
|
||||
Loaded += OnLoaded;
|
||||
Closing += OnClosing;
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
ApplyAdaptiveWindowSize();
|
||||
MaxWidth = Width;
|
||||
MaxHeight = Height;
|
||||
|
||||
if (PresentationSource.FromVisual(this) is HwndSource source)
|
||||
source.AddHook(WindowMessageHook);
|
||||
}
|
||||
|
||||
private void ApplyAdaptiveWindowSize()
|
||||
{
|
||||
var workArea = SystemParameters.WorkArea;
|
||||
const double screenMargin = 24;
|
||||
|
||||
Width = Math.Min(Width, Math.Max(MinWidth, workArea.Width - screenMargin));
|
||||
Height = Math.Min(Height, Math.Max(MinHeight, workArea.Height - screenMargin));
|
||||
|
||||
Left = workArea.Left + Math.Max(0, (workArea.Width - Width) / 2);
|
||||
Top = workArea.Top + Math.Max(0, (workArea.Height - Height) / 2);
|
||||
}
|
||||
|
||||
private IntPtr WindowMessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WmSysCommand)
|
||||
{
|
||||
var command = wParam.ToInt32() & 0xFFF0;
|
||||
if (command is ScMaximize or ScSize or ScKeyMenu)
|
||||
{
|
||||
handled = true;
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
else if (msg == WmGetMinMaxInfo)
|
||||
{
|
||||
MaxWidth = Width;
|
||||
MaxHeight = Height;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
private void InitializeTrayIcon()
|
||||
{
|
||||
var iconStream = Application.GetResourceStream(
|
||||
@@ -38,7 +98,7 @@ public partial class MainWindow : Window
|
||||
|
||||
_trayIcon = new System.Windows.Forms.NotifyIcon
|
||||
{
|
||||
Text = "TunnelX — Split Traffic Per App",
|
||||
Text = LocalizationService.Instance.T("TunnelX — Split Traffic Per App"),
|
||||
Visible = false
|
||||
};
|
||||
|
||||
@@ -58,7 +118,7 @@ public partial class MainWindow : Window
|
||||
|
||||
menu.Items.Add(new System.Windows.Forms.ToolStripSeparator());
|
||||
|
||||
var statusItem = new System.Windows.Forms.ToolStripMenuItem("وضعیت: آماده");
|
||||
var statusItem = new System.Windows.Forms.ToolStripMenuItem($"{LocalizationService.Instance.T("وضعیت")}: {_viewModel.StatusText}");
|
||||
statusItem.Enabled = false;
|
||||
menu.Items.Add(statusItem);
|
||||
|
||||
@@ -69,7 +129,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
statusItem.Text = $"وضعیت: {_viewModel.StatusText}";
|
||||
statusItem.Text = $"{LocalizationService.Instance.T("وضعیت")}: {_viewModel.StatusText}";
|
||||
});
|
||||
}
|
||||
else if (args.PropertyName == nameof(MainViewModel.ConnectionState))
|
||||
@@ -120,6 +180,23 @@ public partial class MainWindow : Window
|
||||
menu.Items.Add(exitItem);
|
||||
|
||||
_trayIcon.ContextMenuStrip = menu;
|
||||
RefreshTrayText();
|
||||
}
|
||||
|
||||
private void RefreshTrayText()
|
||||
{
|
||||
if (_trayIcon?.ContextMenuStrip == null) return;
|
||||
|
||||
_trayIcon.Text = LocalizationService.Instance.T("TunnelX — Split Traffic Per App");
|
||||
var items = _trayIcon.ContextMenuStrip.Items;
|
||||
if (items.Count > 0 && items[0] is System.Windows.Forms.ToolStripMenuItem showItem)
|
||||
showItem.Text = LocalizationService.Instance.T("نمایش TunnelX");
|
||||
if (items.Count > 2 && items[2] is System.Windows.Forms.ToolStripMenuItem statusItem)
|
||||
statusItem.Text = $"{LocalizationService.Instance.T("وضعیت")}: {_viewModel.StatusText}";
|
||||
if (items.Count > 3 && items[3] is System.Windows.Forms.ToolStripMenuItem updateItem)
|
||||
updateItem.Text = LocalizationService.Instance.T("بررسی بروزرسانی");
|
||||
if (items.Count > 5 && items[5] is System.Windows.Forms.ToolStripMenuItem exitItem)
|
||||
exitItem.Text = LocalizationService.Instance.T("خروج از برنامه");
|
||||
}
|
||||
|
||||
private void ShowFromTray()
|
||||
@@ -150,8 +227,8 @@ public partial class MainWindow : Window
|
||||
_trayIcon.Visible = true;
|
||||
_trayIcon.ShowBalloonTip(
|
||||
2000,
|
||||
"TunnelX در پسزمینه فعال است",
|
||||
"برای باز کردن پنجره، روی آیکن کنار ساعت دوبار کلیک کنید.",
|
||||
LocalizationService.Instance.T("TunnelX در پسزمینه فعال است"),
|
||||
LocalizationService.Instance.T("برای باز کردن پنجره، روی آیکن کنار ساعت دوبار کلیک کنید."),
|
||||
System.Windows.Forms.ToolTipIcon.Info);
|
||||
}
|
||||
}
|
||||
@@ -167,15 +244,15 @@ public partial class MainWindow : Window
|
||||
switch (state)
|
||||
{
|
||||
case ConnectionState.Connected:
|
||||
ShowTrayNotification("تونل فعال شد", GetConnectedTrayMessage(),
|
||||
ShowTrayNotification(LocalizationService.Instance.T("تونل فعال شد"), GetConnectedTrayMessage(),
|
||||
System.Windows.Forms.ToolTipIcon.Info);
|
||||
break;
|
||||
case ConnectionState.Disconnected:
|
||||
ShowTrayNotification("تونل خاموش شد", "ارتباط امن متوقف شده و ترافیک دیگر از TunnelX عبور نمیکند.",
|
||||
ShowTrayNotification(LocalizationService.Instance.T("تونل خاموش شد"), LocalizationService.Instance.T("ارتباط امن متوقف شده و ترافیک دیگر از TunnelX عبور نمیکند."),
|
||||
System.Windows.Forms.ToolTipIcon.Info);
|
||||
break;
|
||||
case ConnectionState.Error:
|
||||
ShowTrayNotification("اتصال برقرار نشد", GetErrorTrayMessage(),
|
||||
ShowTrayNotification(LocalizationService.Instance.T("اتصال برقرار نشد"), GetErrorTrayMessage(),
|
||||
System.Windows.Forms.ToolTipIcon.Warning);
|
||||
break;
|
||||
}
|
||||
@@ -187,8 +264,8 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
|
||||
_updateNotificationShown = true;
|
||||
ShowTrayNotification("نسخه جدید آماده است",
|
||||
"از منوی System Tray یا بخش بروزرسانی، صفحه دانلود TunnelX را باز کنید.",
|
||||
ShowTrayNotification(LocalizationService.Instance.T("نسخه جدید آماده است"),
|
||||
LocalizationService.Instance.T("از منوی System Tray یا بخش بروزرسانی، صفحه دانلود TunnelX را باز کنید."),
|
||||
System.Windows.Forms.ToolTipIcon.Info);
|
||||
}
|
||||
|
||||
@@ -196,20 +273,23 @@ public partial class MainWindow : Window
|
||||
{
|
||||
var profileName = _viewModel.SelectedProfileName;
|
||||
if (!string.IsNullOrWhiteSpace(profileName))
|
||||
return $"پروفایل «{profileName}» فعال است و ترافیک انتخابشده از تونل عبور میکند.";
|
||||
return LocalizationService.Instance.Format("پروفایل «{0}» فعال است و ترافیک انتخابشده از تونل عبور میکند.", profileName);
|
||||
|
||||
return "ترافیک انتخابشده از TunnelX عبور میکند.";
|
||||
return LocalizationService.Instance.T("ترافیک انتخابشده از TunnelX عبور میکند.");
|
||||
}
|
||||
|
||||
private string GetErrorTrayMessage()
|
||||
{
|
||||
var status = _viewModel.StatusText?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(status) || status == "خطا")
|
||||
return "جزئیات خطا را در پنجره برنامه یا لاگها بررسی کنید.";
|
||||
if (string.IsNullOrWhiteSpace(status) ||
|
||||
status == LocalizationService.Instance.T("خطا") ||
|
||||
status.Equals("Error", StringComparison.OrdinalIgnoreCase))
|
||||
return LocalizationService.Instance.T("جزئیات خطا را در پنجره برنامه یا لاگها بررسی کنید.");
|
||||
|
||||
return status.StartsWith("خطا", StringComparison.Ordinal)
|
||||
return status.StartsWith(LocalizationService.Instance.T("خطا"), StringComparison.OrdinalIgnoreCase) ||
|
||||
status.StartsWith("Error", StringComparison.OrdinalIgnoreCase)
|
||||
? status
|
||||
: $"جزئیات: {status}";
|
||||
: LocalizationService.Instance.Format("جزئیات: {0}", status);
|
||||
}
|
||||
|
||||
private void ShowTrayNotification(string title, string message, System.Windows.Forms.ToolTipIcon icon)
|
||||
@@ -221,6 +301,10 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
LocalizationService.Instance.ApplyTo(this);
|
||||
UpdateLogPanelCornerState(LogPanel.Visibility == Visibility.Visible);
|
||||
RefreshTrayText();
|
||||
|
||||
// Force window to foreground — borderless/transparent windows sometimes
|
||||
// start behind other windows or appear unfocused on slower machines.
|
||||
Activate();
|
||||
@@ -231,7 +315,7 @@ public partial class MainWindow : Window
|
||||
// Dismiss the startup overlay quickly; app discovery continues in the
|
||||
// background so the first screen becomes usable immediately.
|
||||
var ct = _loadCts.Token;
|
||||
LoadingStatusText.Text = "بارگذاری لیست برنامههای نصبشده...";
|
||||
LoadingStatusText.Text = LocalizationService.Instance.T("بارگذاری لیست برنامههای نصبشده...");
|
||||
_ = DismissLoadingOverlaySoonAsync(ct);
|
||||
Task.Run(() =>
|
||||
{
|
||||
@@ -359,17 +443,15 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
private bool _logPanelLoaded;
|
||||
private bool _isLogPanelAnimating;
|
||||
private const double LogPanelWidth = 350;
|
||||
private static readonly Duration LogPanelAnimationDuration = TimeSpan.FromMilliseconds(220);
|
||||
private string _logFilter = "All";
|
||||
|
||||
private void OnShowLogClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (LogPanel.Visibility == Visibility.Visible)
|
||||
{
|
||||
LogPanel.Visibility = Visibility.Collapsed;
|
||||
Width -= LogPanelWidth;
|
||||
if (_isLogPanelAnimating)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_logPanelLoaded)
|
||||
{
|
||||
@@ -379,9 +461,76 @@ public partial class MainWindow : Window
|
||||
Logger.LogAdded += OnLogAdded;
|
||||
}
|
||||
|
||||
Width += LogPanelWidth;
|
||||
LogPanel.Visibility = Visibility.Visible;
|
||||
LogTextBox.ScrollToEnd();
|
||||
var shouldOpen = LogPanel.Visibility != Visibility.Visible;
|
||||
AnimateLogPanel(shouldOpen);
|
||||
}
|
||||
|
||||
private void AnimateLogPanel(bool open)
|
||||
{
|
||||
_isLogPanelAnimating = true;
|
||||
UpdateLogPanelCornerState(open);
|
||||
|
||||
var easing = new CubicEase { EasingMode = EasingMode.EaseOut };
|
||||
var currentPanelWidth = LogPanel.Visibility == Visibility.Visible ? LogPanel.ActualWidth : 0;
|
||||
var maxWindowWidth = SystemParameters.WorkArea.Width - 24;
|
||||
var availablePanelWidth = Math.Max(0, maxWindowWidth - (Width - currentPanelWidth));
|
||||
var targetPanelWidth = open ? Math.Min(LogPanelWidth, availablePanelWidth) : 0;
|
||||
var targetWindowWidth = open
|
||||
? Math.Min(maxWindowWidth, Width + (targetPanelWidth - currentPanelWidth))
|
||||
: Math.Max(MinWidth, Width - currentPanelWidth);
|
||||
|
||||
if (open)
|
||||
{
|
||||
LogPanel.Visibility = Visibility.Visible;
|
||||
LogTextBox.ScrollToEnd();
|
||||
}
|
||||
|
||||
var windowAnimation = new DoubleAnimation(targetWindowWidth, LogPanelAnimationDuration)
|
||||
{
|
||||
EasingFunction = easing
|
||||
};
|
||||
var panelAnimation = new DoubleAnimation(targetPanelWidth, LogPanelAnimationDuration)
|
||||
{
|
||||
EasingFunction = easing
|
||||
};
|
||||
var opacityAnimation = new DoubleAnimation(open ? 1 : 0, LogPanelAnimationDuration)
|
||||
{
|
||||
EasingFunction = easing
|
||||
};
|
||||
|
||||
panelAnimation.Completed += (_, _) =>
|
||||
{
|
||||
BeginAnimation(WidthProperty, null);
|
||||
LogPanel.BeginAnimation(FrameworkElement.WidthProperty, null);
|
||||
LogPanel.BeginAnimation(OpacityProperty, null);
|
||||
|
||||
Width = targetWindowWidth;
|
||||
LogPanel.Width = targetPanelWidth;
|
||||
LogPanel.Opacity = open ? 1 : 0;
|
||||
|
||||
if (!open)
|
||||
LogPanel.Visibility = Visibility.Collapsed;
|
||||
|
||||
_isLogPanelAnimating = false;
|
||||
};
|
||||
|
||||
BeginAnimation(WidthProperty, windowAnimation, HandoffBehavior.SnapshotAndReplace);
|
||||
LogPanel.BeginAnimation(FrameworkElement.WidthProperty, panelAnimation, HandoffBehavior.SnapshotAndReplace);
|
||||
LogPanel.BeginAnimation(OpacityProperty, opacityAnimation, HandoffBehavior.SnapshotAndReplace);
|
||||
}
|
||||
|
||||
private void UpdateLogPanelCornerState(bool logOpen)
|
||||
{
|
||||
if (logOpen)
|
||||
{
|
||||
MainTitleBar.CornerRadius = new CornerRadius(12, 0, 0, 0);
|
||||
MainFooterBar.CornerRadius = new CornerRadius(0, 0, 0, 12);
|
||||
}
|
||||
else
|
||||
{
|
||||
MainTitleBar.CornerRadius = new CornerRadius(12, 12, 0, 0);
|
||||
MainFooterBar.CornerRadius = new CornerRadius(0, 0, 12, 12);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLogAdded(string logEntry)
|
||||
@@ -398,12 +547,27 @@ public partial class MainWindow : Window
|
||||
{
|
||||
Logger.Clear();
|
||||
LogTextBox.Clear();
|
||||
ShowToast(LocalizationService.Instance.T("لاگها پاک شدند"));
|
||||
}
|
||||
|
||||
private void OnLogCopyClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try { System.Windows.Clipboard.SetText(LogTextBox.Text); }
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(LogTextBox.Text))
|
||||
{
|
||||
ShowToast(LocalizationService.Instance.T("لاگی برای کپی وجود ندارد"), "ℹ");
|
||||
return;
|
||||
}
|
||||
|
||||
System.Windows.Clipboard.SetText(LogTextBox.Text);
|
||||
ShowToast(LocalizationService.Instance.T("لاگ کپی شد"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"[UI] Copy logs failed: {ex.Message}");
|
||||
ShowToast(LocalizationService.Instance.T("کپی لاگ ناموفق بود"), "⚠");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLogCopyLastErrorClick(object sender, RoutedEventArgs e)
|
||||
@@ -416,10 +580,20 @@ public partial class MainWindow : Window
|
||||
l.Contains("[ERROR]", StringComparison.OrdinalIgnoreCase)) ??
|
||||
lines.Reverse().FirstOrDefault(l =>
|
||||
l.Contains("[WARN]", StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
System.Windows.Clipboard.SetText(line);
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
ShowToast(LocalizationService.Instance.T("آخرین خطا یا هشدار پیدا نشد"), "ℹ");
|
||||
return;
|
||||
}
|
||||
|
||||
System.Windows.Clipboard.SetText(line);
|
||||
ShowToast(LocalizationService.Instance.T("آخرین خطا یا هشدار کپی شد"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"[UI] Copy last error/warning failed: {ex.Message}");
|
||||
ShowToast(LocalizationService.Instance.T("کپی لاگ ناموفق بود"), "⚠");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void OnLogFilterChanged(object sender, SelectionChangedEventArgs e)
|
||||
@@ -463,9 +637,20 @@ public partial class MainWindow : Window
|
||||
Owner = this,
|
||||
DataContext = _viewModel
|
||||
};
|
||||
LocalizationService.Instance.ApplyTo(helpWindow);
|
||||
helpWindow.ShowDialog();
|
||||
}
|
||||
|
||||
private void OnDonateClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var donationDialog = new AppTunnel.Views.DonationDialog
|
||||
{
|
||||
Owner = this
|
||||
};
|
||||
LocalizationService.Instance.ApplyTo(donationDialog);
|
||||
donationDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void OnNestedScrollPreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (sender is not DependencyObject source) return;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AppTunnel.Services;
|
||||
|
||||
namespace AppTunnel.Models;
|
||||
|
||||
@@ -51,6 +52,16 @@ public class ConnectionProfile : INotifyPropertyChanged
|
||||
private bool _enableDnsOptimization = true;
|
||||
private bool _enableGameMode = false;
|
||||
|
||||
public ConnectionProfile()
|
||||
{
|
||||
LocalizationService.Instance.LanguageChanged += (_, _) =>
|
||||
{
|
||||
OnPropertyChanged(nameof(TunnelTypeDisplay));
|
||||
OnPropertyChanged(nameof(EndpointDisplay));
|
||||
OnPropertyChanged(nameof(ReadinessText));
|
||||
};
|
||||
}
|
||||
|
||||
public string Id
|
||||
{
|
||||
get => _id;
|
||||
@@ -217,16 +228,16 @@ public class ConnectionProfile : INotifyPropertyChanged
|
||||
TunnelType.V2Ray => "V2Ray / Xray",
|
||||
TunnelType.OpenVpn => "OpenVPN",
|
||||
TunnelType.SocksProxy => ProxyProtocol == ProxyProtocol.Http ? "HTTP Proxy" : "SOCKS5 Proxy",
|
||||
_ => "نامشخص"
|
||||
_ => LocalizationService.Instance.T("نامشخص")
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
public string EndpointDisplay => TunnelType switch
|
||||
{
|
||||
TunnelType.L2tpIpsec => string.IsNullOrWhiteSpace(ServerAddress) ? "آدرس سرور وارد نشده" : ServerAddress,
|
||||
TunnelType.V2Ray => string.IsNullOrWhiteSpace(V2RayConfig) ? "کانفیگ وارد نشده" : "کانفیگ آماده",
|
||||
TunnelType.OpenVpn => string.IsNullOrWhiteSpace(OpenVpnConfigPath) ? "فایل .ovpn انتخاب نشده" : Path.GetFileName(OpenVpnConfigPath),
|
||||
TunnelType.SocksProxy => string.IsNullOrWhiteSpace(ProxyServerAddress) ? "آدرس پراکسی وارد نشده" : $"{ProxyServerAddress}:{ProxyPort}",
|
||||
TunnelType.L2tpIpsec => string.IsNullOrWhiteSpace(ServerAddress) ? LocalizationService.Instance.T("آدرس سرور وارد نشده") : ServerAddress,
|
||||
TunnelType.V2Ray => string.IsNullOrWhiteSpace(V2RayConfig) ? LocalizationService.Instance.T("کانفیگ وارد نشده") : LocalizationService.Instance.T("کانفیگ آماده"),
|
||||
TunnelType.OpenVpn => string.IsNullOrWhiteSpace(OpenVpnConfigPath) ? LocalizationService.Instance.T("فایل .ovpn انتخاب نشده") : Path.GetFileName(OpenVpnConfigPath),
|
||||
TunnelType.SocksProxy => string.IsNullOrWhiteSpace(ProxyServerAddress) ? LocalizationService.Instance.T("آدرس پراکسی وارد نشده") : $"{ProxyServerAddress}:{ProxyPort}",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
@@ -241,7 +252,7 @@ public class ConnectionProfile : INotifyPropertyChanged
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
public string ReadinessText => IsReady ? "آماده اتصال" : "نیاز به تکمیل";
|
||||
public string ReadinessText => IsReady ? LocalizationService.Instance.T("آماده اتصال") : LocalizationService.Instance.T("نیاز به تکمیل");
|
||||
|
||||
[JsonIgnore]
|
||||
public string ReadinessColor => IsReady ? "#6CCB5F" : "#E0A020";
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<FontFamily x:Key="AppFont">pack://application:,,,/AppTunnel;component/Fonts/#Vazirmatn, Vazirmatn, Vazir, Segoe UI, Tahoma</FontFamily>
|
||||
|
||||
<!-- Color Palette: Windows 11 Dark Mode + Orange Accent -->
|
||||
<Color x:Key="PrimaryColor">#E8803A</Color>
|
||||
<Color x:Key="PrimaryDarkColor">#C46A2A</Color>
|
||||
<Color x:Key="AccentColor">#F09848</Color>
|
||||
<Color x:Key="PrimaryColor">#F97316</Color>
|
||||
<Color x:Key="PrimaryDarkColor">#EA580C</Color>
|
||||
<Color x:Key="AccentColor">#F97316</Color>
|
||||
<Color x:Key="BackgroundColor">#202020</Color>
|
||||
<Color x:Key="SurfaceColor">#282828</Color>
|
||||
<Color x:Key="CardColor">#2D2D2D</Color>
|
||||
@@ -22,6 +22,10 @@
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource PrimaryColor}"/>
|
||||
<SolidColorBrush x:Key="PrimaryDarkBrush" Color="{StaticResource PrimaryDarkColor}"/>
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
|
||||
<LinearGradientBrush x:Key="PrimaryGradientBrush" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#F97316" Offset="0"/>
|
||||
<GradientStop Color="#F97316" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="BackgroundBrush" Color="{StaticResource BackgroundColor}"/>
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
|
||||
<SolidColorBrush x:Key="CardBrush" Color="{StaticResource CardColor}"/>
|
||||
@@ -61,6 +65,7 @@
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
Foreground="#555555"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
TextAlignment="{TemplateBinding TextAlignment}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
@@ -294,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 -->
|
||||
@@ -314,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>
|
||||
@@ -336,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";
|
||||
@@ -16,11 +17,20 @@ public static class AppInfo
|
||||
"TON: UQD65oL2Vu2OJDSrwQ0wLLSw3g668SREMJ3VPW9k8b6Sy-Yf\n" +
|
||||
"BNB Smart Chain: 0xE2a5b01cE2b3713D435Bc16d92eAdd88A82159f0\n" +
|
||||
"Dogecoin: DSZRNY65yF679uvjAh6sUAt6YiEEQHwKGb";
|
||||
public const string CryptoDonationTextEn =
|
||||
"Tron / USDT on TRC20: TNWV867fQDT6zpLunHgbeMjrN6ic63LQSu\n" +
|
||||
"Bitcoin: bc1qgx3g47c458fu6smnpqpu0l05hha82rq2xjet4y\n" +
|
||||
"Ethereum / USDT on ERC20: 0x72d94Bb250E8802441a0ED05686Ee925BC99Fef5\n" +
|
||||
"TON: UQD65oL2Vu2OJDSrwQ0wLLSw3g668SREMJ3VPW9k8b6Sy-Yf\n" +
|
||||
"BNB Smart Chain: 0xE2a5b01cE2b3713D435Bc16d92eAdd88A82159f0\n" +
|
||||
"Dogecoin: DSZRNY65yF679uvjAh6sUAt6YiEEQHwKGb";
|
||||
|
||||
public static string VersionText =>
|
||||
"v" + (System.Reflection.Assembly.GetExecutingAssembly()
|
||||
.GetName().Version?.ToString(3) ?? "1.0.0");
|
||||
|
||||
public static string ReleaseText => $"{AppName} {VersionText}";
|
||||
public static string CreatorText => $"ساخته شده توسط {CreatorName}";
|
||||
public static string CreatorText => LocalizationService.Instance.IsRightToLeft
|
||||
? $"ساخته شده توسط {CreatorName}"
|
||||
: $"Made by {CreatorName}";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class L2tpTunnelProvider : ITunnelProvider
|
||||
_config = config;
|
||||
_vpnInterfaceIndex = -1;
|
||||
Status.State = ConnectionState.Connecting;
|
||||
Status.Message = "در حال ایجاد اتصال VPN...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال ایجاد اتصال VPN...");
|
||||
Logger.Info($"Starting VPN connection to {config.ServerAddress}");
|
||||
|
||||
try
|
||||
@@ -107,13 +107,13 @@ public class L2tpTunnelProvider : ITunnelProvider
|
||||
if (!createResult.Success)
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"خطا در ایجاد VPN: {createResult.Error}";
|
||||
Status.Message = LocalizationService.Instance.Format("خطا در ایجاد VPN: {0}", createResult.Error);
|
||||
Logger.Error($"VPN creation failed: {createResult.Error}");
|
||||
return false;
|
||||
}
|
||||
Logger.Info("VPN connection profile created successfully");
|
||||
|
||||
Status.Message = "در حال اتصال به سرور...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال اتصال به سرور...");
|
||||
|
||||
// Step 3: Connect using rasdial (60-second timeout for L2TP negotiation)
|
||||
var connectResult = await RunProcessAsync(
|
||||
@@ -126,7 +126,7 @@ public class L2tpTunnelProvider : ITunnelProvider
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
var errorMsg = MapRasDialError(connectResult.ExitCode, connectResult.Output, connectResult.Error);
|
||||
Status.Message = $"خطا: {errorMsg}";
|
||||
Status.Message = LocalizationService.Instance.Format("خطا: {0}", errorMsg);
|
||||
Logger.Error($"VPN connection failed: ExitCode={connectResult.ExitCode}, Error={errorMsg}");
|
||||
return false;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ public class L2tpTunnelProvider : ITunnelProvider
|
||||
Status.VpnLocalIp = vpnInfo.localIp;
|
||||
Status.VpnServerIp = config.ServerAddress;
|
||||
Status.VpnInterfaceIndex = vpnInfo.interfaceIndex;
|
||||
Status.Message = $"متصل — IP: {vpnInfo.localIp}";
|
||||
Status.Message = LocalizationService.Instance.Format("متصل — IP: {0}", vpnInfo.localIp);
|
||||
Logger.Info($"VPN fully connected. Local IP: {vpnInfo.localIp}, Interface Index: {vpnInfo.interfaceIndex}");
|
||||
|
||||
return true;
|
||||
@@ -151,14 +151,14 @@ public class L2tpTunnelProvider : ITunnelProvider
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Status.State = ConnectionState.Disconnected;
|
||||
Status.Message = "اتصال لغو شد";
|
||||
Status.Message = LocalizationService.Instance.T("اتصال لغو شد");
|
||||
Logger.Warning("VPN connection cancelled by user");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"خطا: {ex.Message}";
|
||||
Status.Message = LocalizationService.Instance.Format("خطا: {0}", ex.Message);
|
||||
Logger.Error("VPN connection failed with exception", ex);
|
||||
return false;
|
||||
}
|
||||
@@ -173,7 +173,7 @@ public class L2tpTunnelProvider : ITunnelProvider
|
||||
if (_config == null) return;
|
||||
|
||||
Status.State = ConnectionState.Disconnecting;
|
||||
Status.Message = "در حال قطع اتصال...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال قطع اتصال...");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -195,7 +195,7 @@ public class L2tpTunnelProvider : ITunnelProvider
|
||||
Status.ConnectedSince = null;
|
||||
Status.VpnLocalIp = string.Empty;
|
||||
Status.VpnInterfaceIndex = -1;
|
||||
Status.Message = "قطع شد";
|
||||
Status.Message = LocalizationService.Instance.T("قطع شد");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,23 +281,23 @@ public class L2tpTunnelProvider : ITunnelProvider
|
||||
private static string MapRasDialError(int exitCode, string output, string error) =>
|
||||
exitCode switch
|
||||
{
|
||||
691 => "نام کاربری یا رمز عبور اشتباه است",
|
||||
692 => "پورت یا دستگاه اشغال است",
|
||||
718 => "انتظار برای پاسخ سرور به اتمام رسید (زمانبر)",
|
||||
720 => "پروتکل PPP بین کلاینت و سرور مطابقت ندارد",
|
||||
734 => "پروتکل Link Control قطع شد",
|
||||
735 => "آدرس درخواستی توسط سرور رد شد",
|
||||
742 => "رایانه به اینترنت متصل نیست",
|
||||
768 => "رمزنگاری L2TP/IPsec شکست خورد - PSK یا تنظیمات سرور را بررسی کنید",
|
||||
769 => "مقصد در دسترس نیست (سرور خاموش یا آدرس اشتباه)",
|
||||
781 => "Pre-Shared Key (PSK) اشتباه است",
|
||||
787 => "رمزنگاری L2TP شکست خورد",
|
||||
800 => "سرور VPN در دسترس نیست",
|
||||
809 => "نوع شبکه را نمیتوان مشخص کرد (فایروال مسدود کرده)",
|
||||
812 => "اتصال قبلی باعث تضاد شده",
|
||||
691 => LocalizationService.Instance.T("نام کاربری یا رمز عبور اشتباه است"),
|
||||
692 => LocalizationService.Instance.T("پورت یا دستگاه اشغال است"),
|
||||
718 => LocalizationService.Instance.T("انتظار برای پاسخ سرور به اتمام رسید (زمانبر)"),
|
||||
720 => LocalizationService.Instance.T("پروتکل PPP بین کلاینت و سرور مطابقت ندارد"),
|
||||
734 => LocalizationService.Instance.T("پروتکل Link Control قطع شد"),
|
||||
735 => LocalizationService.Instance.T("آدرس درخواستی توسط سرور رد شد"),
|
||||
742 => LocalizationService.Instance.T("رایانه به اینترنت متصل نیست"),
|
||||
768 => LocalizationService.Instance.T("رمزنگاری L2TP/IPsec شکست خورد - PSK یا تنظیمات سرور را بررسی کنید"),
|
||||
769 => LocalizationService.Instance.T("مقصد در دسترس نیست (سرور خاموش یا آدرس اشتباه)"),
|
||||
781 => LocalizationService.Instance.T("Pre-Shared Key (PSK) اشتباه است"),
|
||||
787 => LocalizationService.Instance.T("رمزنگاری L2TP شکست خورد"),
|
||||
800 => LocalizationService.Instance.T("سرور VPN در دسترس نیست"),
|
||||
809 => LocalizationService.Instance.T("نوع شبکه را نمیتوان مشخص کرد (فایروال مسدود کرده)"),
|
||||
812 => LocalizationService.Instance.T("اتصال قبلی باعث تضاد شده"),
|
||||
_ when !string.IsNullOrWhiteSpace(error) => error.Trim(),
|
||||
_ when !string.IsNullOrWhiteSpace(output) => output.Trim(),
|
||||
_ => $"خطای ناشناخته (exit code: {exitCode})"
|
||||
_ => LocalizationService.Instance.Format("خطای ناشناخته (exit code: {0})", exitCode)
|
||||
};
|
||||
|
||||
private static string SanitizeForPowerShell(string value) => value.Replace("'", "''");
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace AppTunnel.Services;
|
||||
|
||||
internal sealed class LocalPortReservation : IDisposable
|
||||
{
|
||||
private readonly TcpListener _listener;
|
||||
|
||||
private LocalPortReservation(TcpListener listener)
|
||||
{
|
||||
_listener = listener;
|
||||
Port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
}
|
||||
|
||||
public int Port { get; }
|
||||
|
||||
public static LocalPortReservation ReservePreferredOrRandom(int preferredPort, params int[] excludedPorts)
|
||||
{
|
||||
if (preferredPort > 0 && !excludedPorts.Contains(preferredPort) &&
|
||||
TryReserve(preferredPort, out var preferred))
|
||||
return preferred;
|
||||
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var reservation = Reserve(0);
|
||||
if (!excludedPorts.Contains(reservation.Port))
|
||||
return reservation;
|
||||
|
||||
reservation.Dispose();
|
||||
}
|
||||
|
||||
throw new IOException("Could not reserve a free local port.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_listener.Stop();
|
||||
}
|
||||
|
||||
private static bool TryReserve(int port, out LocalPortReservation reservation)
|
||||
{
|
||||
try
|
||||
{
|
||||
reservation = Reserve(port);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
reservation = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalPortReservation Reserve(int port)
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, port);
|
||||
listener.Start();
|
||||
return new LocalPortReservation(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace AppTunnel.Services;
|
||||
|
||||
public sealed class LocalizationService : INotifyPropertyChanged
|
||||
{
|
||||
public const string AutoLanguage = "auto";
|
||||
public const string PersianLanguage = "fa-IR";
|
||||
public const string EnglishLanguage = "en-US";
|
||||
|
||||
public static LocalizationService Instance { get; } = new();
|
||||
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _translations = new(StringComparer.Ordinal)
|
||||
{
|
||||
[EnglishLanguage] = EnglishTranslations()
|
||||
};
|
||||
|
||||
private string _languageSetting = AutoLanguage;
|
||||
private string _effectiveLanguage = PersianLanguage;
|
||||
|
||||
private LocalizationService()
|
||||
{
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(FrameworkElement),
|
||||
FrameworkElement.LoadedEvent,
|
||||
new RoutedEventHandler(OnFrameworkElementLoaded),
|
||||
handledEventsToo: true);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public event EventHandler? LanguageChanged;
|
||||
|
||||
public string LanguageSetting => _languageSetting;
|
||||
public string EffectiveLanguage => _effectiveLanguage;
|
||||
public bool IsRightToLeft => _effectiveLanguage.StartsWith("fa", StringComparison.OrdinalIgnoreCase);
|
||||
public System.Windows.FlowDirection FlowDirection => IsRightToLeft
|
||||
? System.Windows.FlowDirection.RightToLeft
|
||||
: System.Windows.FlowDirection.LeftToRight;
|
||||
public System.Windows.TextAlignment TextAlignment => IsRightToLeft
|
||||
? System.Windows.TextAlignment.Right
|
||||
: System.Windows.TextAlignment.Left;
|
||||
public System.Windows.HorizontalAlignment StartHorizontalAlignment => IsRightToLeft
|
||||
? System.Windows.HorizontalAlignment.Right
|
||||
: System.Windows.HorizontalAlignment.Left;
|
||||
public System.Windows.HorizontalAlignment EndHorizontalAlignment => IsRightToLeft
|
||||
? System.Windows.HorizontalAlignment.Left
|
||||
: System.Windows.HorizontalAlignment.Right;
|
||||
public string ToggleLanguageText => IsRightToLeft ? "English" : "فارسی";
|
||||
|
||||
public void Initialize(string? savedLanguage)
|
||||
{
|
||||
SetLanguageInternal(string.IsNullOrWhiteSpace(savedLanguage) ? AutoLanguage : savedLanguage!, raiseChanged: false);
|
||||
}
|
||||
|
||||
public void ToggleLanguage()
|
||||
{
|
||||
SetLanguage(IsRightToLeft ? EnglishLanguage : PersianLanguage);
|
||||
}
|
||||
|
||||
public void SetLanguage(string language)
|
||||
{
|
||||
SetLanguageInternal(language, raiseChanged: true);
|
||||
}
|
||||
|
||||
public string T(string source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source) || IsRightToLeft)
|
||||
return source;
|
||||
|
||||
return _translations.TryGetValue(_effectiveLanguage, out var table) &&
|
||||
table.TryGetValue(source, out var translated)
|
||||
? translated
|
||||
: source;
|
||||
}
|
||||
|
||||
public string Format(string sourceFormat, params object?[] args)
|
||||
=> string.Format(CultureInfo.CurrentCulture, T(sourceFormat), args);
|
||||
|
||||
public void ApplyToOpenWindows()
|
||||
{
|
||||
foreach (Window window in System.Windows.Application.Current.Windows)
|
||||
ApplyTo(window);
|
||||
}
|
||||
|
||||
public void ApplyTo(DependencyObject root)
|
||||
{
|
||||
ApplyTo(root, new HashSet<DependencyObject>());
|
||||
}
|
||||
|
||||
private void ApplyTo(DependencyObject root, HashSet<DependencyObject> visited)
|
||||
{
|
||||
if (!visited.Add(root))
|
||||
return;
|
||||
|
||||
ApplyFlowDirection(root);
|
||||
ApplyText(root);
|
||||
|
||||
try
|
||||
{
|
||||
var visualCount = VisualTreeHelper.GetChildrenCount(root);
|
||||
for (var i = 0; i < visualCount; i++)
|
||||
ApplyTo(VisualTreeHelper.GetChild(root, i), visited);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Some logical children, such as Run, are not visual children.
|
||||
}
|
||||
|
||||
foreach (var child in LogicalTreeHelper.GetChildren(root).OfType<DependencyObject>())
|
||||
ApplyTo(child, visited);
|
||||
}
|
||||
|
||||
private void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not DependencyObject element)
|
||||
return;
|
||||
|
||||
ApplyTo(element);
|
||||
}
|
||||
|
||||
private void SetLanguageInternal(string language, bool raiseChanged)
|
||||
{
|
||||
_languageSetting = NormalizeLanguageSetting(language);
|
||||
_effectiveLanguage = ResolveEffectiveLanguage(_languageSetting);
|
||||
|
||||
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(_effectiveLanguage);
|
||||
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(_effectiveLanguage);
|
||||
|
||||
OnPropertyChanged(nameof(LanguageSetting));
|
||||
OnPropertyChanged(nameof(EffectiveLanguage));
|
||||
OnPropertyChanged(nameof(IsRightToLeft));
|
||||
OnPropertyChanged(nameof(FlowDirection));
|
||||
OnPropertyChanged(nameof(TextAlignment));
|
||||
OnPropertyChanged(nameof(StartHorizontalAlignment));
|
||||
OnPropertyChanged(nameof(EndHorizontalAlignment));
|
||||
OnPropertyChanged(nameof(ToggleLanguageText));
|
||||
|
||||
if (!raiseChanged) return;
|
||||
|
||||
LanguageChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
var dispatcher = System.Windows.Application.Current?.Dispatcher;
|
||||
dispatcher?.BeginInvoke(ApplyToOpenWindows, DispatcherPriority.Loaded);
|
||||
dispatcher?.BeginInvoke(ApplyToOpenWindows, DispatcherPriority.ContextIdle);
|
||||
}
|
||||
|
||||
private static string NormalizeLanguageSetting(string language)
|
||||
{
|
||||
if (string.Equals(language, EnglishLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
return EnglishLanguage;
|
||||
if (string.Equals(language, PersianLanguage, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(language, "fa", StringComparison.OrdinalIgnoreCase))
|
||||
return PersianLanguage;
|
||||
return AutoLanguage;
|
||||
}
|
||||
|
||||
private static string ResolveEffectiveLanguage(string setting)
|
||||
{
|
||||
if (setting != AutoLanguage)
|
||||
return setting;
|
||||
|
||||
var ui = CultureInfo.CurrentUICulture;
|
||||
return ui.TwoLetterISOLanguageName.Equals("fa", StringComparison.OrdinalIgnoreCase)
|
||||
? PersianLanguage
|
||||
: EnglishLanguage;
|
||||
}
|
||||
|
||||
private static bool HasBinding(DependencyObject element, DependencyProperty property)
|
||||
=> BindingOperations.GetBindingExpressionBase(element, property) != null;
|
||||
|
||||
private static readonly DependencyProperty OriginalTextProperty =
|
||||
DependencyProperty.RegisterAttached("OriginalText", typeof(string), typeof(LocalizationService));
|
||||
|
||||
private static readonly DependencyProperty OriginalContentProperty =
|
||||
DependencyProperty.RegisterAttached("OriginalContent", typeof(string), typeof(LocalizationService));
|
||||
|
||||
private static readonly DependencyProperty OriginalHeaderProperty =
|
||||
DependencyProperty.RegisterAttached("OriginalHeader", typeof(string), typeof(LocalizationService));
|
||||
|
||||
private static readonly DependencyProperty OriginalToolTipProperty =
|
||||
DependencyProperty.RegisterAttached("OriginalToolTip", typeof(string), typeof(LocalizationService));
|
||||
|
||||
private static readonly DependencyProperty OriginalTagProperty =
|
||||
DependencyProperty.RegisterAttached("OriginalTag", typeof(string), typeof(LocalizationService));
|
||||
|
||||
private void ApplyFlowDirection(DependencyObject element)
|
||||
{
|
||||
if (element is not FrameworkElement fe)
|
||||
return;
|
||||
|
||||
if (HasBinding(fe, FrameworkElement.FlowDirectionProperty))
|
||||
return;
|
||||
|
||||
var local = fe.ReadLocalValue(FrameworkElement.FlowDirectionProperty);
|
||||
if (local == DependencyProperty.UnsetValue ||
|
||||
local is System.Windows.FlowDirection.LeftToRight)
|
||||
return;
|
||||
|
||||
// Explicit RTL follows the selected language. Explicit LTR is reserved
|
||||
// for technical fields: IP, path, port, config and logs.
|
||||
fe.FlowDirection = FlowDirection;
|
||||
}
|
||||
|
||||
private void ApplyText(DependencyObject element)
|
||||
{
|
||||
switch (element)
|
||||
{
|
||||
case Window window when !HasBinding(window, Window.TitleProperty):
|
||||
window.Title = TranslateProperty(window, Window.TitleProperty, OriginalTextProperty, window.Title);
|
||||
break;
|
||||
|
||||
case TextBlock textBlock when !HasBinding(textBlock, TextBlock.TextProperty) && !textBlock.Inlines.Any():
|
||||
textBlock.Text = TranslateProperty(textBlock, TextBlock.TextProperty, OriginalTextProperty, textBlock.Text);
|
||||
if (textBlock.ReadLocalValue(TextBlock.TextAlignmentProperty) == DependencyProperty.UnsetValue)
|
||||
textBlock.TextAlignment = textBlock.FlowDirection == System.Windows.FlowDirection.LeftToRight
|
||||
? System.Windows.TextAlignment.Left
|
||||
: TextAlignment;
|
||||
break;
|
||||
|
||||
case Run run:
|
||||
run.Text = TranslateProperty(run, Run.TextProperty, OriginalTextProperty, run.Text);
|
||||
break;
|
||||
|
||||
case HeaderedContentControl headered when headered.Header is string header && !HasBinding(headered, HeaderedContentControl.HeaderProperty):
|
||||
headered.Header = TranslateProperty(headered, HeaderedContentControl.HeaderProperty, OriginalHeaderProperty, header);
|
||||
break;
|
||||
|
||||
case ContentControl contentControl when contentControl.Content is string content && !HasBinding(contentControl, ContentControl.ContentProperty):
|
||||
contentControl.Content = TranslateProperty(contentControl, ContentControl.ContentProperty, OriginalContentProperty, content);
|
||||
break;
|
||||
|
||||
case System.Windows.Controls.TextBox textBox when textBox.Tag is string tag && !HasBinding(textBox, FrameworkElement.TagProperty):
|
||||
textBox.Tag = TranslateProperty(textBox, FrameworkElement.TagProperty, OriginalTagProperty, tag);
|
||||
ApplyTextBoxAlignment(textBox);
|
||||
break;
|
||||
|
||||
case System.Windows.Controls.TextBox textBox:
|
||||
ApplyTextBoxAlignment(textBox);
|
||||
break;
|
||||
}
|
||||
|
||||
if (element is FrameworkElement fe && fe.ToolTip is string toolTip && !HasBinding(fe, FrameworkElement.ToolTipProperty))
|
||||
fe.ToolTip = TranslateProperty(fe, FrameworkElement.ToolTipProperty, OriginalToolTipProperty, toolTip);
|
||||
}
|
||||
|
||||
private void ApplyTextBoxAlignment(System.Windows.Controls.TextBox textBox)
|
||||
{
|
||||
if (textBox.ReadLocalValue(System.Windows.Controls.TextBox.TextAlignmentProperty) != DependencyProperty.UnsetValue)
|
||||
return;
|
||||
|
||||
textBox.TextAlignment = textBox.FlowDirection == System.Windows.FlowDirection.LeftToRight
|
||||
? TextAlignment.Left
|
||||
: TextAlignment;
|
||||
}
|
||||
|
||||
private string TranslateProperty(DependencyObject owner, DependencyProperty property, DependencyProperty originalProperty, string current)
|
||||
{
|
||||
var original = owner.GetValue(originalProperty) as string;
|
||||
var resolvedCurrent = ResolveSourceText(current);
|
||||
if (original == null || !string.Equals(original, ResolveSourceText(original), StringComparison.Ordinal))
|
||||
{
|
||||
original = resolvedCurrent;
|
||||
owner.SetValue(originalProperty, original);
|
||||
}
|
||||
|
||||
return T(original);
|
||||
}
|
||||
|
||||
private string ResolveSourceText(string current)
|
||||
{
|
||||
if (string.IsNullOrEmpty(current))
|
||||
return current;
|
||||
|
||||
foreach (var table in _translations.Values)
|
||||
{
|
||||
if (table.ContainsKey(current))
|
||||
return current;
|
||||
|
||||
foreach (var pair in table)
|
||||
{
|
||||
if (string.Equals(pair.Value, current, StringComparison.Ordinal))
|
||||
return pair.Key;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
private static Dictionary<string, string> EnglishTranslations() => new(StringComparer.Ordinal)
|
||||
{
|
||||
["TunnelX — Split Traffic Per App"] = "TunnelX - Split Traffic Per App",
|
||||
["Per-app split tunneling"] = "Per-app split tunneling",
|
||||
["تانلکس"] = "TunnelX",
|
||||
["تفکیک ترافیک برنامه ها"] = "Per-app traffic splitting",
|
||||
["جزئیات"] = "Details",
|
||||
["کوچک کردن به System Tray"] = "Minimize to system tray",
|
||||
["خروج از برنامه"] = "Exit app",
|
||||
["وضعیت اتصال VPN و کنترل اتصال"] = "VPN connection status and controls",
|
||||
["اتصال VPN"] = "VPN Connection",
|
||||
["انتخاب برنامههایی که باید از تونل عبور کنند"] = "Select apps that should use the tunnel",
|
||||
["برنامهها"] = "Apps",
|
||||
["تنظیمات عمومی تونل و DNS"] = "General tunnel and DNS settings",
|
||||
["تنظیمات"] = "Settings",
|
||||
["قوانین Include و Exclude مسیرها"] = "Include and exclude routing rules",
|
||||
["قوانین مسیر"] = "Routing Rules",
|
||||
["مقصدهای مستقیم و مقصدهای اجباری تونل را اینجا مدیریت کنید."] = "Manage direct and forced tunnel destinations here.",
|
||||
["🚫 مستقیم بماند"] = "🚫 Stay Direct",
|
||||
["این مقصدها از تونل عبور نمیکنند."] = "These destinations do not use the tunnel.",
|
||||
["نمونه کاربرد"] = "Example",
|
||||
["سایتهای داخلی یا سرورهای بازی را مستقیم نگه دارید."] = "Keep internal sites or game servers direct.",
|
||||
["دامنه یا آیپی (مثلاً google.com یا 1.2.3.4)"] = "Domain or IP, e.g. google.com or 1.2.3.4",
|
||||
["افزودن"] = "Add",
|
||||
["آدرسهای مستقیم"] = "Direct Addresses",
|
||||
["حذف"] = "Delete",
|
||||
["✅ از تونل عبور کند"] = "✅ Use Tunnel",
|
||||
["این مقصدها همیشه از تونل عبور میکنند."] = "These destinations always use the tunnel.",
|
||||
["دامنه یا آیپی (مثلاً example.com یا 1.2.3.4)"] = "Domain or IP, e.g. example.com or 1.2.3.4",
|
||||
["آدرسهای اجباری"] = "Forced Addresses",
|
||||
["نمایش ترافیک، تاریخچه و آمار اتصال"] = "Traffic, history, and connection stats",
|
||||
["ترافیک/تاریخچه"] = "Traffic/History",
|
||||
["ترافیک و تاریخچه"] = "Traffic and History",
|
||||
["⏱ مدت"] = "⏱ Duration",
|
||||
["🌐 IP"] = "🌐 IP",
|
||||
["📊 تونل"] = "📊 Tunnel",
|
||||
["📡 خارج تونل"] = "📡 Direct",
|
||||
["📜 تاریخچه اتصالات"] = "📜 Connection History",
|
||||
["مصرف تونل به تفکیک برنامه"] = "Tunnel Usage by App",
|
||||
["اپهای تونل: "] = "Tunnel apps: ",
|
||||
["سایر تونل: "] = "Other tunnel: ",
|
||||
["هنوز برنامهای اضافه نشده. از تب «برنامهها» اضافه کنید."] = "No apps added yet. Add apps from the Apps tab.",
|
||||
["ساخته شده توسط Maxifan"] = "Made by Maxifan",
|
||||
["بروزرسانی"] = "Update",
|
||||
["دانلود نسخه جدید از صفحه Releases در GitHub"] = "Download the new version from GitHub Releases",
|
||||
["راهنما"] = "Help",
|
||||
["راهنما و عیبیابی"] = "Help and troubleshooting",
|
||||
["باز کردن صفحه GitHub پروژه TunnelX"] = "Open the TunnelX GitHub project",
|
||||
["🔍 جزئیات عملکرد"] = "🔍 Runtime Details",
|
||||
["همه"] = "All",
|
||||
["خطا"] = "Error",
|
||||
["هشدار"] = "Warning",
|
||||
["پاک کردن"] = "Clear",
|
||||
["کپی خطا"] = "Copy error",
|
||||
["کپی همه"] = "Copy all",
|
||||
["پاک کردن همه لاگها"] = "Clear all logs",
|
||||
["کپی آخرین خطا یا هشدار"] = "Copy latest error or warning",
|
||||
["کپی کردن همه لاگها"] = "Copy all logs",
|
||||
["کپی کردن"] = "Copy",
|
||||
["در حال بارگذاری..."] = "Loading...",
|
||||
|
||||
["در حال اتصال OpenVPN"] = "Connecting OpenVPN",
|
||||
["تا قبل از بالا آمدن آداپتر، مسیرهای سیستم تغییر داده نمیشود. اگر اتصال طولانی شد، فایل .ovpn، نام کاربری/رمز یا نصب OpenVPN Community را بررسی کنید."] = "System routes are not changed until the adapter is ready. If connection takes too long, check the .ovpn file, credentials, or OpenVPN Community installation.",
|
||||
["لغو اتصال"] = "Cancel",
|
||||
["کانفیگها و پروفایلها"] = "Configs and Profiles",
|
||||
["یک کانفیگ را انتخاب کنید، ویرایش کنید یا کانفیگ جدید بسازید."] = "Select, edit, or create a connection profile.",
|
||||
["افزودن کانفیگ جدید"] = "Add New Config",
|
||||
["فعال"] = "Use",
|
||||
["استفاده از این کانفیگ برای اتصال"] = "Use this config for connection",
|
||||
["ویرایش"] = "Edit",
|
||||
["کپی"] = "Copy",
|
||||
["پروفایل فعال: "] = "Active profile: ",
|
||||
["شروع اتصال با پروفایل انتخابشده"] = "Start connection with the selected profile",
|
||||
["🌐 تنظیمات سرور"] = "🌐 Server Settings",
|
||||
["نوع اتصال"] = "Connection Type",
|
||||
["آدرس سرور"] = "Server Address",
|
||||
["نام کاربری"] = "Username",
|
||||
["رمز عبور"] = "Password",
|
||||
["کانفیگ V2Ray"] = "V2Ray Config",
|
||||
["پیست"] = "Paste",
|
||||
["خواندن کانفیگ از کلیپبورد"] = "Read config from clipboard",
|
||||
["فایل OpenVPN (.ovpn)"] = "OpenVPN File (.ovpn)",
|
||||
["انتخاب فایل"] = "Choose File",
|
||||
["حذف فایل"] = "Remove File",
|
||||
["دانلود OpenVPN"] = "Download OpenVPN",
|
||||
["نوع پراکسی"] = "Proxy Type",
|
||||
["پورت"] = "Port",
|
||||
["آدرس IP یا دامنه سرور"] = "Server IP or Domain",
|
||||
|
||||
["✅ برنامههای داخل تونل"] = "✅ Tunneled Apps",
|
||||
["برنامههای فعالشده از مسیر تونل عبور میکنند"] = "Enabled apps use the tunnel route",
|
||||
["افزودن دستی"] = "Add Manually",
|
||||
["افزودن دستی فایل exe"] = "Manually add an exe file",
|
||||
["فیلتر برنامههای تونل..."] = "Filter tunneled apps...",
|
||||
["🔍 برنامههای نصبشده"] = "🔍 Installed Apps",
|
||||
["روی برنامه کلیک کنید تا به لیست تونل اضافه شود"] = "Click an app to add it to the tunnel list",
|
||||
["بارگذاری مجدد لیست برنامهها"] = "Reload the app list",
|
||||
["جستجوی برنامه..."] = "Search apps...",
|
||||
|
||||
["🧦 پروکسی محلی"] = "🧦 Local Proxy",
|
||||
["پورت پروکسی محلی (SOCKS5/HTTP)"] = "Local proxy port (SOCKS5/HTTP)",
|
||||
["پورت داخلی 127.0.0.1 برای پروکسی SOCKS5 و HTTP"] = "Internal 127.0.0.1 port for SOCKS5 and HTTP proxy",
|
||||
["پورتهای زیر 1024 و چند پورت رایج مثل 2080، 3000، 3389، 8080 و 9090 مجاز نیستند تا با سرویسهای سیستم یا ابزارهای توسعه تداخل نداشته باشند."] = "Ports below 1024 and common ports like 2080, 3000, 3389, 8080, and 9090 are blocked to avoid conflicts with system services or developer tools.",
|
||||
["🚀 بهینهسازی تونل"] = "🚀 Tunnel Optimization",
|
||||
["MTU خودکار"] = "Automatic MTU",
|
||||
["در زمان اتصال، MTU مناسب بر اساس شبکه فعلی انتخاب میشود."] = "At connection time, the best MTU is selected based on the current network.",
|
||||
["برای Resolveها از cache کوتاهمدت استفاده میشود و DNS redirect مسیر پایدارتر میگیرد."] = "Uses a short-lived cache for resolves and keeps DNS redirects more stable.",
|
||||
["Game Mode فعال است: Route نگهداری طولانیتر، DNS سریعتر و DSCP برای بستههای بازی اعمال میشود."] = "Game Mode is enabled: longer route retention, faster DNS, and DSCP for game packets.",
|
||||
["Game Mode غیرفعال است: حالت متعادل برای مصرف عمومی."] = "Game Mode is disabled: balanced mode for general use.",
|
||||
["🖥️ استارتآپ و اتصال خودکار"] = "🖥️ Startup and Auto-Connect",
|
||||
["اجرای خودکار هنگام روشن شدن ویندوز"] = "Start automatically with Windows",
|
||||
["⚠️ پس از فعال کردن، نباید محل فایل اجرایی TunnelX را تغییر دهید."] = "⚠️ After enabling this, do not move the TunnelX executable.",
|
||||
["اتصال خودکار به آخرین کانکشن فعال"] = "Auto-connect to the last active connection",
|
||||
["اگر آخرین بار یک پروفایل متصل بوده، هنگام اجرای برنامه به آن وصل میشود."] = "If a profile was connected last time, TunnelX connects to it when the app starts.",
|
||||
|
||||
["راهنمای TunnelX"] = "TunnelX Help",
|
||||
["شروع سریع، پروفایلها، بخشهای اپ و عیبیابی در یک صفحه ساده."] = "Quick start, profiles, app areas, and troubleshooting in one simple page.",
|
||||
["پروژه و بروزرسانی"] = "Project and Updates",
|
||||
["صفحه انتشار"] = "Release Page",
|
||||
["شروع سریع"] = "Quick Start",
|
||||
["۱. پروفایل"] = "1. Profile",
|
||||
["کانفیگ را بسازید و نوع اتصال را انتخاب کنید."] = "Create a config and select its connection type.",
|
||||
["۲. برنامهها"] = "2. Apps",
|
||||
["برنامههای داخل تونل را انتخاب یا دستی اضافه کنید."] = "Select tunneled apps or add them manually.",
|
||||
["۳. قوانین"] = "3. Rules",
|
||||
["مقصدهای مستقیم یا اجباری را مشخص کنید."] = "Set direct or forced destinations.",
|
||||
["۴. اتصال"] = "4. Connect",
|
||||
["وصل شوید و سلامت، IP و مصرف را بررسی کنید."] = "Connect and check health, IP, and usage.",
|
||||
["نوع پروفایل"] = "Profile Type",
|
||||
["فقط فیلدهای مربوط به نوع انتخابشده را پر کنید. هر پروفایل برنامهها و قوانین مسیر خودش را نگه میدارد."] = "Only fill the fields for the selected type. Each profile keeps its own apps and routing rules.",
|
||||
["بخشهای اپ"] = "App Areas",
|
||||
["پروفایل فعال، تست سرور، اتصال/قطع اتصال، IP خروجی، پینگ، مصرف و راهنمای پراکسی دستی اینجاست."] = "Active profile, server test, connect/disconnect, exit IP, ping, usage, and manual proxy guidance are here.",
|
||||
["نکات مهم"] = "Essentials",
|
||||
["حالت عادی: فقط برنامههای انتخابشده و مقصدهای لزومی وارد تونل میشوند."] = "Normal mode: only selected apps and included destinations use the tunnel.",
|
||||
["Full Route: کل سیستم وارد تونل میشود؛ استثناها میتوانند مستقیم بمانند."] = "Full Route: the whole system uses the tunnel; exclusions can stay direct.",
|
||||
["پراکسی داخلی"] = "Local Proxy",
|
||||
["برای ابزارهایی که آدرس محلی میخواهند:"] = "For tools that need a local address:",
|
||||
["سلامت: Leak باید صفر باشد. DNS، IPv6 و Route را بعد از اتصال بررسی کنید."] = "Health: Leak should be zero. Check DNS, IPv6, and Route after connecting.",
|
||||
["عیبیابی سریع"] = "Quick Troubleshooting",
|
||||
["حمایت و تماس"] = "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",
|
||||
["پرداخت با کریپتو"] = "Pay with crypto",
|
||||
["ترون / USDT روی TRC20"] = "Tron / USDT on TRC20",
|
||||
["بیتکوین"] = "Bitcoin",
|
||||
["اتریوم / USDT روی ERC20"] = "Ethereum / USDT on ERC20",
|
||||
["کپی"] = "Copy",
|
||||
|
||||
["ویرایش پروفایل"] = "Edit Profile",
|
||||
["تنظیمات این کانفیگ بعد از ذخیره در لیست پروفایلها نمایش داده میشود."] = "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",
|
||||
["پیست از کلیپبورد"] = "Paste from Clipboard",
|
||||
["فایل .ovpn و اطلاعات احراز هویت OpenVPN را وارد کنید. اگر سرور رمز نمیخواهد، فیلد رمز را خالی بگذارید."] = "Enter the .ovpn file and OpenVPN credentials. Leave password empty if the server does not require one.",
|
||||
["برای SOCKS5 یا HTTP Proxy، اطلاعات سرور را جداگانه وارد کنید."] = "For SOCKS5 or HTTP Proxy, enter the server details separately.",
|
||||
["لغو"] = "Cancel",
|
||||
["ذخیره"] = "Save",
|
||||
|
||||
["تاریخچه اتصالات"] = "Connection History",
|
||||
["سوابق اتصال و مصرف تونل"] = "Connection records and tunnel usage",
|
||||
["مجموع مصرف تونل: "] = "Total tunnel usage: ",
|
||||
["هنوز اتصالی ثبت نشده است."] = "No connection has been recorded yet.",
|
||||
|
||||
["آماده اتصال"] = "Ready to connect",
|
||||
["نیاز به تکمیل"] = "Needs setup",
|
||||
["نامشخص"] = "Unknown",
|
||||
["آدرس سرور وارد نشده"] = "Server address is missing",
|
||||
["کانفیگ وارد نشده"] = "Config is missing",
|
||||
["کانفیگ آماده"] = "Config ready",
|
||||
["فایل .ovpn انتخاب نشده"] = ".ovpn file not selected",
|
||||
["آدرس پراکسی وارد نشده"] = "Proxy address is missing"
|
||||
,
|
||||
["وضعیت"] = "Status",
|
||||
["نمایش TunnelX"] = "Show TunnelX",
|
||||
["بررسی بروزرسانی"] = "Check for Updates",
|
||||
["اتصال VPN فعال است. با خروج، اتصال قطع خواهد شد.\nآیا مطمئن هستید؟"] = "A VPN connection is active. Exiting will disconnect it.\nAre you sure?",
|
||||
["TunnelX — خروج"] = "TunnelX - Exit",
|
||||
["آیا میخواهید از TunnelX خارج شوید؟"] = "Do you want to exit TunnelX?",
|
||||
["TunnelX در پسزمینه فعال است"] = "TunnelX is running in the background",
|
||||
["برای باز کردن پنجره، روی آیکن کنار ساعت دوبار کلیک کنید."] = "Double-click the tray icon to open the window.",
|
||||
["تونل فعال شد"] = "Tunnel enabled",
|
||||
["تونل خاموش شد"] = "Tunnel disabled",
|
||||
["ارتباط امن متوقف شده و ترافیک دیگر از TunnelX عبور نمیکند."] = "The secure connection stopped and traffic no longer passes through TunnelX.",
|
||||
["اتصال برقرار نشد"] = "Connection failed",
|
||||
["نسخه جدید آماده است"] = "New version available",
|
||||
["از منوی System Tray یا بخش بروزرسانی، صفحه دانلود TunnelX را باز کنید."] = "Open the TunnelX download page from the system tray menu or the update section.",
|
||||
["پروفایل «{0}» فعال است و ترافیک انتخابشده از تونل عبور میکند."] = "Profile \"{0}\" is active and selected traffic uses the tunnel.",
|
||||
["ترافیک انتخابشده از TunnelX عبور میکند."] = "Selected traffic uses TunnelX.",
|
||||
["جزئیات خطا را در پنجره برنامه یا لاگها بررسی کنید."] = "Check the app window or logs for error details.",
|
||||
["جزئیات: {0}"] = "Details: {0}",
|
||||
["بارگذاری لیست برنامههای نصبشده..."] = "Loading installed apps...",
|
||||
["تاییدیه"] = "Confirmation",
|
||||
["اطلاعات"] = "Information",
|
||||
["موفقیت"] = "Success",
|
||||
["بله"] = "Yes",
|
||||
["خیر"] = "No",
|
||||
["متوجه شدم"] = "OK",
|
||||
["عالی"] = "Great",
|
||||
["{0} کپی شد"] = "{0} copied",
|
||||
["متن"] = "Text",
|
||||
["انتخاب فایل OpenVPN"] = "Choose OpenVPN File",
|
||||
["OpenVPN config (*.ovpn)|*.ovpn|All files (*.*)|*.*"] = "OpenVPN config (*.ovpn)|*.ovpn|All files (*.*)|*.*",
|
||||
["خواندن فایل OpenVPN ناموفق بود: {0}"] = "Failed to read OpenVPN file: {0}",
|
||||
["خواندن کلیپبورد ناموفق بود: {0}"] = "Failed to read clipboard: {0}",
|
||||
["نام پروفایل را وارد کنید"] = "Enter a profile name",
|
||||
["آدرس سرور L2TP را وارد کنید"] = "Enter the L2TP server address",
|
||||
["کانفیگ V2Ray/Xray را وارد کنید"] = "Enter the V2Ray/Xray config",
|
||||
["فایل OpenVPN (.ovpn) را انتخاب کنید"] = "Choose an OpenVPN (.ovpn) file",
|
||||
["آدرس سرور پراکسی را وارد کنید"] = "Enter the proxy server address",
|
||||
["پورت پراکسی باید بین 1 تا 65535 باشد"] = "Proxy port must be between 1 and 65535",
|
||||
["ذخیره شد"] = "Saved",
|
||||
["پورت SOCKS5 را وارد کنید"] = "Enter the SOCKS5 port",
|
||||
["فقط عدد مجاز است"] = "Only numbers are allowed",
|
||||
["پیشنیاز آماده است: نسخه Community اوپنویپیان پیدا شد: {0}"] = "Prerequisite ready: OpenVPN Community found: {0}",
|
||||
["اخطار: نسخه Community اوپنویپیان نصب نیست. برای استفاده از اسپلیتتانلینگ با این نوع اتصال، ابتدا آن را از لینک رسمی نصب کنید."] = "Warning: OpenVPN Community is not installed. Install it from the official link before using split tunneling with this connection type.",
|
||||
["برای بررسی نسخه جدید، دکمه بررسی بروزرسانی را بزنید."] = "Click Check for Updates to look for a new version.",
|
||||
["در حال بررسی..."] = "Checking...",
|
||||
["🔌 اتصال"] = "🔌 Connect",
|
||||
["❌ لغو اتصال"] = "❌ Cancel",
|
||||
["🔴 قطع اتصال"] = "🔴 Disconnect",
|
||||
["⏳ در حال قطع..."] = "⏳ Disconnecting...",
|
||||
["🔌 اتصال مجدد"] = "🔌 Reconnect",
|
||||
["اتصال"] = "Connect",
|
||||
["لغو تلاش اتصال"] = "Cancel connection attempt",
|
||||
["اتصال مجدد"] = "Reconnect",
|
||||
["IP خروجی"] = "Exit IP",
|
||||
["تغییر حالت Full Route ناموفق بود"] = "Failed to change Full Route mode",
|
||||
["Full Route فعال است؛ کل سیستم از تونل عبور میکند"] = "Full Route is enabled; the whole system uses the tunnel",
|
||||
["Split فعال است؛ فقط برنامهها و مقصدهای انتخابی از تونل عبور میکنند"] = "Split is enabled; only selected apps and destinations use the tunnel",
|
||||
["حالت کل سیستم"] = "Full-system Mode",
|
||||
["حالت انتخابی"] = "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",
|
||||
["تست مقصد"] = "Test target",
|
||||
["در حال پینگ..."] = "Pinging...",
|
||||
["پینگ سرور"] = "Ping server",
|
||||
["در حال تست..."] = "Testing...",
|
||||
["تست سرور"] = "Test server",
|
||||
["در حال بررسی آخرین نسخه در GitHub..."] = "Checking the latest version on GitHub...",
|
||||
["بررسی نسخه جدید ناموفق بود. اتصال اینترنت یا 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",
|
||||
["OpenVPN Community نصب نیست؛ ابتدا آن را از لینک رسمی نصب کنید"] = "OpenVPN Community is not installed; install it from the official link first",
|
||||
["فایل .ovpn را انتخاب کنید؛ TunnelX آن را در حالت split-compatible اجرا میکند"] = "Choose a .ovpn file; TunnelX runs it in split-compatible mode",
|
||||
["کانفیگ انتخاب شد؛ اگر سرور احراز هویت دارد نام کاربری را وارد کنید"] = "Config selected; enter username if the server requires authentication",
|
||||
["کانفیگ و نام کاربری OpenVPN آماده است"] = "OpenVPN config and username are ready",
|
||||
["منتظر کانفیگ"] = "Waiting for config",
|
||||
["کانفیگ V2Ray/Xray را وارد یا پیست کنید"] = "Enter or paste the V2Ray/Xray config",
|
||||
["هسته: Xray-core"] = "Core: Xray-core",
|
||||
["هسته: sing-box"] = "Core: sing-box",
|
||||
["سرور: {0}:{1}"] = "Server: {0}:{1}",
|
||||
["پراکسی آماده است: {0}"] = "Proxy is ready: {0}",
|
||||
["پراکسی آماده است: {0} — توجه: این پراکسی محلی است؛ برنامههایی که خودشان مستقیم از همین پراکسی استفاده کنند خارج از لیست برنامههای TunnelX هم پروکسی میشوند."] = "Proxy is ready: {0} - note: this is a local proxy; apps that directly use it will be proxied even outside TunnelX's app list.",
|
||||
["آدرس IP یا دامنه سرور پراکسی را وارد کنید"] = "Enter the proxy server IP or domain",
|
||||
["تنظیمات پراکسی آماده است"] = "Proxy settings are ready",
|
||||
["پورت باید بین 1024 تا 65535 باشد"] = "Port must be between 1024 and 65535",
|
||||
["این پورت رایج/حساس است؛ یک پورت آزاد مثل 1080، 1081 یا 18080 انتخاب کنید"] = "This port is common/sensitive; choose a free port like 1080, 1081, or 18080",
|
||||
["این پورت همین حالا توسط برنامه دیگری استفاده میشود"] = "This port is currently used by another app",
|
||||
["پورت SOCKS5 داخلی آماده است"] = "Internal SOCKS5 port is ready"
|
||||
,
|
||||
["خطای اتصال خودکار: {0}"] = "Auto-connect error: {0}",
|
||||
["حمایت از پروژه"] = "Support the project",
|
||||
["تغییر زبان"] = "Change language",
|
||||
["پورت پراکسی را وارد کنید"] = "Enter the proxy port",
|
||||
["پورت پراکسی باید عدد باشد"] = "Proxy port must be a number",
|
||||
["در حال آمادهسازی V2Ray..."] = "Preparing V2Ray...",
|
||||
["فایل sing-box.exe پیدا نشد: {0}"] = "sing-box.exe was not found: {0}",
|
||||
["خطا در پارس کانفیگ: {0}"] = "Config parse error: {0}",
|
||||
["در حال انتظار برای interface TunnelX-V2Ray..."] = "Waiting for TunnelX-V2Ray interface...",
|
||||
["sing-box زودتر خارج شد (exit code {0}) — کانفیگ را بررسی کنید"] = "sing-box exited early (exit code {0}) - check the config",
|
||||
["interface TunnelX-V2Ray ظاهر نشد (timeout 10s)"] = "TunnelX-V2Ray interface did not appear (timeout 10s)",
|
||||
["در حال قطع اتصال V2Ray..."] = "Disconnecting V2Ray...",
|
||||
["در حال آماده سازی Xray..."] = "Preparing Xray...",
|
||||
["فایل xray.exe پیدا نشد: {0}"] = "xray.exe was not found: {0}",
|
||||
["در حال قطع اتصال Xray..."] = "Disconnecting Xray...",
|
||||
["در حال ایجاد اتصال VPN..."] = "Creating VPN connection...",
|
||||
["خطا در ایجاد VPN: {0}"] = "VPN creation failed: {0}",
|
||||
["در حال اتصال به سرور..."] = "Connecting to server...",
|
||||
["متصل — IP: {0}"] = "Connected - IP: {0}",
|
||||
["نام کاربری یا رمز عبور اشتباه است"] = "The username or password is incorrect",
|
||||
["پورت یا دستگاه اشغال است"] = "The port or device is busy",
|
||||
["انتظار برای پاسخ سرور به اتمام رسید (زمانبر)"] = "Timed out waiting for the server response",
|
||||
["پروتکل PPP بین کلاینت و سرور مطابقت ندارد"] = "PPP protocol does not match between client and server",
|
||||
["پروتکل Link Control قطع شد"] = "Link Control Protocol terminated",
|
||||
["آدرس درخواستی توسط سرور رد شد"] = "The requested address was rejected by the server",
|
||||
["رایانه به اینترنت متصل نیست"] = "The computer is not connected to the internet",
|
||||
["رمزنگاری L2TP/IPsec شکست خورد - PSK یا تنظیمات سرور را بررسی کنید"] = "L2TP/IPsec encryption failed - check the PSK or server settings",
|
||||
["مقصد در دسترس نیست (سرور خاموش یا آدرس اشتباه)"] = "Destination is unreachable (server is down or address is wrong)",
|
||||
["Pre-Shared Key (PSK) اشتباه است"] = "Pre-Shared Key (PSK) is incorrect",
|
||||
["رمزنگاری L2TP شکست خورد"] = "L2TP encryption failed",
|
||||
["سرور VPN در دسترس نیست"] = "VPN server is not reachable",
|
||||
["نوع شبکه را نمیتوان مشخص کرد (فایروال مسدود کرده)"] = "Network type cannot be determined (firewall may be blocking it)",
|
||||
["اتصال قبلی باعث تضاد شده"] = "A previous connection caused a conflict",
|
||||
["خطای ناشناخته (exit code: {0})"] = "Unknown error (exit code: {0})",
|
||||
["xray زودتر خارج شد (exit code {0})"] = "xray exited early (exit code {0})",
|
||||
["sing-box bridge زودتر خارج شد (exit code {0})"] = "sing-box bridge exited early (exit code {0})",
|
||||
["نوع تانل ناشناخته: {0}"] = "Unknown tunnel type: {0}",
|
||||
["در حال لغو اتصال..."] = "Canceling connection...",
|
||||
["آدرس سرور را وارد کنید"] = "Enter the server address",
|
||||
["کانفیگ V2Ray را وارد کنید"] = "Enter the V2Ray config",
|
||||
["کانفیگ OpenVPN (.ovpn) را وارد کنید"] = "Enter the OpenVPN (.ovpn) config",
|
||||
["در حال آمادهسازی OpenVPN..."] = "Preparing OpenVPN...",
|
||||
["در حال اتصال..."] = "Connecting...",
|
||||
["اتصال لغو شد"] = "Connection canceled",
|
||||
["پراکسی متصل شد"] = "Proxy connected",
|
||||
["در حال دریافت..."] = "Fetching...",
|
||||
["در حال قطع اتصال..."] = "Disconnecting...",
|
||||
["قطع شد"] = "Disconnected",
|
||||
["اتصال VPN قطع شد..."] = "VPN connection dropped...",
|
||||
["اتصال VPN بهطور غیرمنتظره قطع شد"] = "VPN connection dropped unexpectedly",
|
||||
["اتصال VPN بهطور غیرمنتظره قطع شد.\nلطفاً دوباره متصل شوید."] = "VPN connection dropped unexpectedly.\nPlease reconnect.",
|
||||
["قطع اتصال"] = "Disconnect",
|
||||
["OpenVPN دوباره متصل شد؛ مسیرهای TunnelX در حال بروزرسانی است..."] = "OpenVPN reconnected; TunnelX routes are being updated...",
|
||||
["OpenVPN دوباره متصل شد و مسیرها بروزرسانی شدند"] = "OpenVPN reconnected and routes were updated",
|
||||
["آدرس سرور خالی است"] = "Server address is empty",
|
||||
["remote سرور در فایل .ovpn پیدا نشد"] = "Server remote was not found in the .ovpn file",
|
||||
["کانفیگ UDP است؛ تست دقیق قبل از اتصال ممکن نیست"] = "The config uses UDP; accurate pre-connect testing is not possible",
|
||||
["هیچ remote قابلدسترسی نبود ({0})"] = "No reachable remote was found ({0})",
|
||||
["خطا: {0}"] = "Error: {0}",
|
||||
["در حال پینگ سرور..."] = "Pinging server...",
|
||||
["پینگ سرور timeout شد"] = "Server ping timed out",
|
||||
["فایل .ovpn انتخاب نشده است"] = ".ovpn file is not selected",
|
||||
["کانفیگ خالی است"] = "Config is empty",
|
||||
["endpoint سرور از کانفیگ تشخیص داده نشد"] = "Server endpoint could not be detected from the config",
|
||||
["پارس کانفیگ ناموفق بود: {0}"] = "Failed to parse config: {0}",
|
||||
["آدرس ss:// نامعتبر است"] = "Invalid ss:// address",
|
||||
["سرور پیدا نشد"] = "Server not found",
|
||||
["پورت نامعتبر است"] = "Invalid port",
|
||||
["آدرس نامعتبر"] = "Invalid address",
|
||||
[" [پایان]"] = " [done]",
|
||||
["۱ پروفایل ذخیرهشده"] = "1 saved profile",
|
||||
["{0} پروفایل ذخیرهشده"] = "{0} saved profiles",
|
||||
["نوع اتصال نامشخص"] = "Unknown connection type",
|
||||
["آدرس سرور هنوز وارد نشده"] = "Server address not entered yet",
|
||||
["کانفیگ V2Ray/Xray آماده نمایش نیست"] = "V2Ray/Xray config is not ready to display",
|
||||
["فایل OpenVPN انتخاب نشده"] = "OpenVPN file not selected",
|
||||
["آدرس پراکسی هنوز وارد نشده"] = "Proxy address not entered yet",
|
||||
["تغییرات این پروفایل بهصورت خودکار ذخیره میشود"] = "Profile changes are saved automatically",
|
||||
["در حال ذخیره..."] = "Saving...",
|
||||
["پیشفرض"] = "Default",
|
||||
["پروفایل {0}"] = "Profile {0}",
|
||||
["{0} (کپی)"] = "{0} (copy)",
|
||||
["کپی پروفایل"] = "Copy Profile",
|
||||
["پروفایل «{0}» حذف شود؟"] = "Delete profile \"{0}\"?",
|
||||
["حذف پروفایل"] = "Delete Profile",
|
||||
["استارتآپ فعال شد.\n\n⚠️ برای کارکرد صحیح، پس از این نباید محل فایل اجرایی TunnelX را تغییر دهید."] = "Startup enabled.\n\n⚠️ For correct behavior, do not move the TunnelX executable after this.",
|
||||
["TunnelX — استارتآپ"] = "TunnelX - Startup"
|
||||
,
|
||||
["برای سرورهای SOCKS5 یا HTTP Proxy، اطلاعات را جداگانه وارد کنید. TunnelX از همین پراکسی یک TUN داخلی میسازد تا اسپلیتتانلینگ برنامههای انتخابی مثل سایر نوعهای اتصال کار کند."] = "For SOCKS5 or HTTP Proxy servers, enter the server details separately. TunnelX builds an internal TUN from this proxy so selected-app split tunneling works like other connection types.",
|
||||
["اگر پراکسی شما نام کاربری یا رمز ندارد، فیلدهای احراز هویت را خالی بگذارید."] = "If your proxy has no username or password, leave the authentication fields empty.",
|
||||
["🏓 سرور"] = "🏓 Server",
|
||||
["قبل از اتصال، دسترسی و latency سرور را تست میکند"] = "Tests server reachability and latency before connecting",
|
||||
["وضعیت و پروفایل فعال"] = "Status and active profile",
|
||||
["مدت زمان اتصال فعلی از لحظه برقراری اتصال"] = "Current connection duration since connection started",
|
||||
["مدت"] = "Duration",
|
||||
["IP عمومیای که مقصدهای اینترنتی شما را با آن میبینند"] = "The public IP seen by internet destinations",
|
||||
["مجموع ترافیک ارسال و دریافت عبوری از تونل VPN (کل تونل)"] = "Total sent and received traffic through the VPN tunnel",
|
||||
["تونل"] = "Tunnel",
|
||||
["نمایش تشخیصی ترافیک خارج از تونل. این عدد در مصرف تونل و تاریخچه ثبت نمیشود."] = "Diagnostic view of traffic outside the tunnel. This number is not recorded as tunnel usage or history.",
|
||||
["خارج تونل"] = "Direct",
|
||||
["🌐 عبور کل سیستم"] = "🌐 Full Route",
|
||||
["روشن: کل ویندوز از تونل. خاموش: فقط برنامهها و قوانین انتخابی."] = "On: all Windows traffic uses the tunnel. Off: only selected apps and rules.",
|
||||
["اگر برنامهای خودکار وارد تونل نشد، این آدرس را در تنظیمات Proxy همان برنامه وارد کنید."] = "If an app does not enter the tunnel automatically, enter this address in that app's proxy settings.",
|
||||
["🧦 پروکسی دستی"] = "🧦 Manual Proxy",
|
||||
["این آدرس داخلی را در برنامههایی وارد کنید که تنظیم Proxy جداگانه دارند یا خودکار وارد تونل نمیشوند."] = "Use this internal address in apps with separate proxy settings or apps that do not enter the tunnel automatically.",
|
||||
["🏓 تست مسیر"] = "🏓 Route Test",
|
||||
["یک دامنه یا IP را از داخل تونل تست کنید."] = "Test a domain or IP from inside the tunnel.",
|
||||
["IP یا دامنه مقصد برای تست از داخل تونل"] = "Destination IP or domain to test through the tunnel",
|
||||
["تست همین مقصد از داخل مسیر تونل"] = "Test this destination through the tunnel route",
|
||||
["دسترسی به سرور همین اتصال را تست میکند"] = "Tests reachability to this connection's server",
|
||||
["قطع اتصال فعلی"] = "Disconnect current connection",
|
||||
["💡 در حالت انتخابی، فقط برنامههای فعال در تب "] = "💡 In selected mode, only apps enabled in the ",
|
||||
["«برنامهها»"] = "\"Apps\"",
|
||||
[" از تونل عبور میکنند؛ بقیه مستقیم میمانند."] = " tab use the tunnel; the rest stays direct.",
|
||||
["📌 برای تلگرام، واتساپ و برنامههای Store، "] = "📌 For Telegram, WhatsApp, and Store apps, also add ",
|
||||
[" را هم به لیست تونل اضافه کنید."] = " to the tunnel list."
|
||||
,
|
||||
["TunnelX — جزئیات عملکرد"] = "TunnelX - Runtime Details",
|
||||
["🔍 جزئیات عملکرد — TunnelX"] = "🔍 Runtime Details - TunnelX",
|
||||
["🗑 پاک کردن"] = "🗑 Clear",
|
||||
["📋 کپی"] = "📋 Copy",
|
||||
["لاگها"] = "Logs",
|
||||
["خطا در کپی کردن:\n{0}"] = "Copy failed:\n{0}",
|
||||
["لاگها پاک شدند"] = "Logs cleared",
|
||||
["لاگی برای کپی وجود ندارد"] = "There are no logs to copy",
|
||||
["لاگ کپی شد"] = "Log copied",
|
||||
["آخرین خطا یا هشدار پیدا نشد"] = "No recent error or warning found",
|
||||
["آخرین خطا یا هشدار کپی شد"] = "Latest error or warning copied",
|
||||
["کپی لاگ ناموفق بود"] = "Failed to copy logs",
|
||||
["باز کردن لینک ناموفق بود: {0}"] = "Failed to open link: {0}",
|
||||
["کپی ناموفق بود: {0}"] = "Copy failed: {0}",
|
||||
["آیا مطمئن هستید؟"] = "Are you sure?",
|
||||
["در حال اجرای OpenVPN در حالت Split..."] = "Running OpenVPN in split mode...",
|
||||
["فقط OpenVPN Connect پیدا شد. برای Split Tunneling باید OpenVPN Community (openvpn.exe) هم نصب باشد."] = "Only OpenVPN Connect was found. Split tunneling requires OpenVPN Community (openvpn.exe).",
|
||||
["OpenVPN Community پیدا نشد. برای Split Tunneling باید openvpn.exe نصب باشد."] = "OpenVPN Community was not found. Split tunneling requires openvpn.exe.",
|
||||
["کانفیگ OpenVPN (.ovpn) وارد نشده است."] = "OpenVPN (.ovpn) config is missing.",
|
||||
["OpenVPN Community نصب نیست؛ ابتدا از لینک رسمی نصب کنید"] = "OpenVPN Community is not installed; install it from the official link first",
|
||||
["OpenVPN در حال اتصال است؛ مسیرهای پیشفرض آن برای Split Tunnel نادیده گرفته میشوند..."] = "OpenVPN is connecting; its default routes are ignored for split tunneling...",
|
||||
["OpenVPN زودتر از اتصال بسته شد (exit={0})"] = "OpenVPN exited before connecting (exit={0})",
|
||||
["منتظر بالا آمدن آداپتر OpenVPN... ({0}s)"] = "Waiting for the OpenVPN adapter... ({0}s)",
|
||||
["آداپتور OpenVPN بالا نیامد. لاگ OpenVPN را بررسی کنید؛ ممکن است ریموت اول پاسخ ندهد یا احراز هویت/شبکه مشکل داشته باشد."] = "The OpenVPN adapter did not come up. Check the OpenVPN log; the first remote may not respond or authentication/network may be failing.",
|
||||
["OpenVPN متصل شد (Split Tunnel)"] = "OpenVPN connected (Split Tunnel)",
|
||||
["در حال قطع اتصال OpenVPN..."] = "Disconnecting OpenVPN...",
|
||||
[" • 📊 تونل "] = " • 📊 Tunnel ",
|
||||
["برای سرورهای SOCKS5 یا HTTP Proxy، اطلاعات سرور را جداگانه وارد کنید."] = "For SOCKS5 or HTTP Proxy servers, enter the server details separately.",
|
||||
["TunnelX فایل .ovpn را با OpenVPN Community اجرا میکند و مسیر/DNS پیشفرض OpenVPN را کنترل میکند تا فقط برنامههای انتخابی از تونل عبور کنند."] = "TunnelX runs the .ovpn file with OpenVPN Community and controls OpenVPN's default route/DNS so only selected apps use the tunnel.",
|
||||
["OpenVPN Connect بهتنهایی کافی نیست؛ اگر Community نصب نباشد، از دکمه دانلود پایین استفاده کنید."] = "OpenVPN Connect alone is not enough; if Community is not installed, use the download button below.",
|
||||
["انتخاب برنامه"] = "Select App",
|
||||
["لایسنس: {0}"] = "License: {0}",
|
||||
["برای اتصال VPN ویندوز. آدرس سرور، نام کاربری، رمز عبور و Pre-Shared Key لازم است. اگر وصل نشد، PSK، فایروال و تنظیمات VPN ویندوز را بررسی کنید."] = "For Windows VPN connection. Server address, username, password, and Pre-Shared Key are required. If it does not connect, check the PSK, firewall, and Windows VPN settings.",
|
||||
["لینک یا JSON کانفیگ را وارد کنید یا از کلیپبورد پیست کنید. TunnelX معمولاً sing-box را اجرا میکند و برای قابلیتهایی مثل xhttp از Xray-core استفاده میکند."] = "Enter a config link or JSON, or paste it from the clipboard. TunnelX usually runs sing-box and uses Xray-core for features such as xhttp.",
|
||||
["برای پراکسی خارجی آماده. نوع پراکسی، آدرس، پورت و در صورت نیاز نام کاربری/رمز را وارد کنید. این با پراکسی داخلی 127.0.0.1 فرق دارد."] = "For an external ready proxy. Enter protocol, address, port, and credentials if needed. This is different from the internal 127.0.0.1 proxy.",
|
||||
["فایل ovpn را انتخاب کنید. OpenVPN Community باید جداگانه نصب باشد؛ OpenVPN Connect برای Split Tunneling مناسب نیست. اگر سرور رمز میخواهد، نام کاربری و رمز را در TunnelX وارد کنید."] = "Choose the ovpn file. OpenVPN Community must be installed separately; OpenVPN Connect is not suitable for split tunneling. If the server requires credentials, enter them in TunnelX.",
|
||||
["از لیست برنامههای پیدا شده انتخاب کنید یا فایل exe را دستی اضافه کنید. برای Store/MSIX/WebView2 برنامه را باز نگه دارید و بروزرسانی لیست را بزنید."] = "Select from discovered apps or manually add an exe file. For Store/MSIX/WebView2 apps, keep the app open and refresh the list.",
|
||||
["«مستقیم بماند» یعنی مقصد از تونل عبور نکند. «از تونل عبور کند» یعنی مقصد حتی بدون انتخاب برنامه وارد تونل شود. دامنهها زیردامنهها را هم پوشش میدهند."] = "\"Stay Direct\" means the destination bypasses the tunnel. \"Use Tunnel\" means the destination enters the tunnel even without selecting an app. Domains also cover subdomains.",
|
||||
["پورت پراکسی محلی، MTU خودکار، DNS Optimization، Game Mode، اجرای خودکار ویندوز و اتصال خودکار اینجاست."] = "Local proxy port, automatic MTU, DNS Optimization, Game Mode, Windows startup, and auto-connect are here.",
|
||||
["مدت اتصال، IP، مصرف تونل، مصرف خارج تونل، سلامت Split Tunnel، مصرف برنامهها و تاریخچه اتصالها را نشان میدهد."] = "Shows duration, IP, tunnel usage, direct usage, split tunnel health, app usage, and connection history.",
|
||||
["جزئیات و لاگها"] = "Details and Logs",
|
||||
["از دکمه جزئیات، لاگها را با فیلتر خطا، هشدار، DNS یا Route ببینید. قبل از ارسال عمومی لاگ، رمزها، کلیدها، UUID و endpoint خصوصی را حذف کنید."] = "Use Details to view logs filtered by error, warning, DNS, or route. Before sharing logs publicly, remove passwords, keys, UUIDs, and private endpoints.",
|
||||
["اتصال برقرار نمیشود"] = "Connection Does Not Start",
|
||||
["برنامه را با Administrator اجرا کنید. فایروال، آنتیویروس، آدرس سرور، پورت، رمزها، PSK، نصب OpenVPN Community و اعتبار کانفیگ را بررسی کنید."] = "Run the app as Administrator. Check firewall, antivirus, server address, port, credentials, PSK, OpenVPN Community installation, and config validity.",
|
||||
["ترافیک برنامه از تونل عبور نمیکند"] = "App Traffic Does Not Use Tunnel",
|
||||
["برنامه را در تب برنامهها فعال کنید. اگر چندپردازشی است، برنامه را باز نگه دارید و لیست برنامهها را دوباره بارگذاری کنید."] = "Enable the app in the Apps tab. If it is multi-process, keep it open and reload the app list.",
|
||||
["پراکسی کار نمیکند"] = "Proxy Does Not Work",
|
||||
["برای پروفایل پراکسی، آدرس، پورت، نوع و اطلاعات ورود را بررسی کنید. برای ابزارهای محلی، آدرس 127.0.0.1 و پورت تنظیمات را وارد کنید."] = "For proxy profiles, check address, port, protocol, and credentials. For local tools, enter 127.0.0.1 and the configured port.",
|
||||
["DNS، IPv6 یا Leak غیرعادی است"] = "DNS, IPv6, or Leak Looks Wrong",
|
||||
["یک بار قطع و وصل کنید تا مسیرها و قوانین DNS دوباره ساخته شوند. اگر مشکل ماند، لاگهای DNS و Route را بررسی کنید."] = "Disconnect and reconnect once so routes and DNS rules are rebuilt. If it persists, check DNS and Route logs."
|
||||
};
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
_assignedLocalIp = "";
|
||||
while (_recentOpenVpnOutput.TryDequeue(out _)) { }
|
||||
Status.State = ConnectionState.Connecting;
|
||||
Status.Message = "در حال اجرای OpenVPN در حالت Split...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال اجرای OpenVPN در حالت Split...");
|
||||
Logger.Info("[OpenVPN] ConnectAsync started");
|
||||
|
||||
try
|
||||
@@ -48,8 +48,8 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = IsOpenVpnConnectInstalled()
|
||||
? "فقط OpenVPN Connect پیدا شد. برای Split Tunneling باید OpenVPN Community (openvpn.exe) هم نصب باشد."
|
||||
: "OpenVPN Community پیدا نشد. برای Split Tunneling باید openvpn.exe نصب باشد.";
|
||||
? LocalizationService.Instance.T("فقط OpenVPN Connect پیدا شد. برای Split Tunneling باید OpenVPN Community (openvpn.exe) هم نصب باشد.")
|
||||
: LocalizationService.Instance.T("OpenVPN Community پیدا نشد. برای Split Tunneling باید openvpn.exe نصب باشد.");
|
||||
Logger.Error("[OpenVPN] Executable not found. Searched:");
|
||||
foreach (var p in GetCandidatePaths())
|
||||
Logger.Error($" '{p}' → {(File.Exists(p) ? "FOUND" : "not found")}");
|
||||
@@ -60,7 +60,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
if (string.IsNullOrWhiteSpace(config.OpenVpnConfig))
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = "کانفیگ OpenVPN (.ovpn) وارد نشده است.";
|
||||
Status.Message = LocalizationService.Instance.T("کانفیگ OpenVPN (.ovpn) وارد نشده است.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
_ = Task.Run(() => PumpOpenVpnOutputAsync(_process.StandardError, ct));
|
||||
Logger.Info($"[OpenVPN] Process started PID={_process.Id}");
|
||||
|
||||
Status.Message = "OpenVPN در حال اتصال است؛ مسیرهای پیشفرض آن برای Split Tunnel نادیده گرفته میشوند...";
|
||||
Status.Message = LocalizationService.Instance.T("OpenVPN در حال اتصال است؛ مسیرهای پیشفرض آن برای Split Tunnel نادیده گرفته میشوند...");
|
||||
Logger.Info("[OpenVPN] Waiting up to 180s for VPN adapter to come Up...");
|
||||
|
||||
var deadline = DateTime.UtcNow.AddSeconds(180);
|
||||
@@ -115,11 +115,11 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
if (_process.HasExited)
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"OpenVPN زودتر از اتصال بسته شد (exit={_process.ExitCode})";
|
||||
Status.Message = LocalizationService.Instance.Format("OpenVPN زودتر از اتصال بسته شد (exit={0})", _process.ExitCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
Status.Message = $"منتظر بالا آمدن آداپتر OpenVPN... ({remaining}s)";
|
||||
Status.Message = LocalizationService.Instance.Format("منتظر بالا آمدن آداپتر OpenVPN... ({0}s)", remaining);
|
||||
await Task.Delay(500, ct);
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
foreach (var nic in NetworkInterface.GetAllNetworkInterfaces())
|
||||
Logger.Error($" name='{nic.Name}' desc='{nic.Description}' status={nic.OperationalStatus}");
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = "آداپتور OpenVPN بالا نیامد. لاگ OpenVPN را بررسی کنید؛ ممکن است ریموت اول پاسخ ندهد یا احراز هویت/شبکه مشکل داشته باشد.";
|
||||
Status.Message = LocalizationService.Instance.T("آداپتور OpenVPN بالا نیامد. لاگ OpenVPN را بررسی کنید؛ ممکن است ریموت اول پاسخ ندهد یا احراز هویت/شبکه مشکل داشته باشد.");
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
}
|
||||
@@ -144,7 +144,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
: ResolveRemoteForRouting(remoteHost);
|
||||
Status.VpnServerPort = _connectedRemotePort;
|
||||
Status.VpnGatewayIp = _routeGatewayIp;
|
||||
Status.Message = "OpenVPN متصل شد (Split Tunnel)";
|
||||
Status.Message = LocalizationService.Instance.T("OpenVPN متصل شد (Split Tunnel)");
|
||||
Logger.Info($"[OpenVPN] Connected. LocalIP={Status.VpnLocalIp} Gateway={Status.VpnGatewayIp} Remote={Status.VpnServerIp}:{Status.VpnServerPort}");
|
||||
|
||||
return true;
|
||||
@@ -152,14 +152,14 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Status.State = ConnectionState.Disconnected;
|
||||
Status.Message = "اتصال لغو شد";
|
||||
Status.Message = LocalizationService.Instance.T("اتصال لغو شد");
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"خطا: {ex.Message}";
|
||||
Status.Message = LocalizationService.Instance.Format("خطا: {0}", ex.Message);
|
||||
Logger.Error("OpenVpnTunnelProvider.ConnectAsync failed", ex);
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
@@ -169,7 +169,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
Status.State = ConnectionState.Disconnecting;
|
||||
Status.Message = "در حال قطع اتصال OpenVPN...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال قطع اتصال OpenVPN...");
|
||||
await KillProcessAsync();
|
||||
_vpnInterfaceIndex = -1;
|
||||
Status.State = ConnectionState.Disconnected;
|
||||
@@ -179,7 +179,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
Status.VpnServerPort = 0;
|
||||
Status.VpnGatewayIp = string.Empty;
|
||||
Status.VpnInterfaceIndex = -1;
|
||||
Status.Message = "قطع شد";
|
||||
Status.Message = LocalizationService.Instance.T("قطع شد");
|
||||
}
|
||||
|
||||
public bool IsInterfaceUp()
|
||||
@@ -230,7 +230,7 @@ public class OpenVpnTunnelProvider : ITunnelProvider
|
||||
Status.VpnGatewayIp = _routeGatewayIp;
|
||||
Status.VpnServerIp = _connectedRemoteIp;
|
||||
Status.VpnServerPort = _connectedRemotePort;
|
||||
Status.Message = "OpenVPN متصل شد (Split Tunnel)";
|
||||
Status.Message = LocalizationService.Instance.T("OpenVPN متصل شد (Split Tunnel)");
|
||||
Logger.Warning($"[OpenVPN] Runtime endpoint changed. LocalIP={Status.VpnLocalIp} Gateway={Status.VpnGatewayIp} Remote={Status.VpnServerIp}:{Status.VpnServerPort} IF={Status.VpnInterfaceIndex}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ public class ProfileService
|
||||
public bool StartWithWindows { get; set; } = false;
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using WpfControl = System.Windows.Controls.Control;
|
||||
using WpfTextBox = System.Windows.Controls.TextBox;
|
||||
|
||||
namespace AppTunnel.Services;
|
||||
|
||||
public static class TextInputBehavior
|
||||
{
|
||||
private static bool _registered;
|
||||
|
||||
public static void Register()
|
||||
{
|
||||
if (_registered)
|
||||
return;
|
||||
|
||||
_registered = true;
|
||||
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(WpfTextBox),
|
||||
UIElement.PreviewMouseLeftButtonDownEvent,
|
||||
new MouseButtonEventHandler(OnTextBoxPreviewMouseLeftButtonDown),
|
||||
handledEventsToo: true);
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(WpfTextBox),
|
||||
UIElement.GotKeyboardFocusEvent,
|
||||
new KeyboardFocusChangedEventHandler(OnTextBoxGotKeyboardFocus));
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(WpfTextBox),
|
||||
WpfControl.MouseDoubleClickEvent,
|
||||
new MouseButtonEventHandler(OnTextBoxDoubleClick));
|
||||
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(PasswordBox),
|
||||
UIElement.PreviewMouseLeftButtonDownEvent,
|
||||
new MouseButtonEventHandler(OnPasswordBoxPreviewMouseLeftButtonDown),
|
||||
handledEventsToo: true);
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(PasswordBox),
|
||||
UIElement.GotKeyboardFocusEvent,
|
||||
new KeyboardFocusChangedEventHandler(OnPasswordBoxGotKeyboardFocus));
|
||||
EventManager.RegisterClassHandler(
|
||||
typeof(PasswordBox),
|
||||
WpfControl.MouseDoubleClickEvent,
|
||||
new MouseButtonEventHandler(OnPasswordBoxDoubleClick));
|
||||
}
|
||||
|
||||
private static void OnTextBoxPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not WpfTextBox textBox || textBox.IsReadOnly || e.ClickCount > 1)
|
||||
return;
|
||||
|
||||
textBox.Focus();
|
||||
textBox.CaretIndex = textBox.Text.Length;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnTextBoxGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
if (sender is not WpfTextBox textBox || textBox.IsReadOnly)
|
||||
return;
|
||||
|
||||
textBox.CaretIndex = textBox.Text.Length;
|
||||
}
|
||||
|
||||
private static void OnTextBoxDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not WpfTextBox textBox || textBox.IsReadOnly)
|
||||
return;
|
||||
|
||||
textBox.SelectAll();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnPasswordBoxGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
if (sender is PasswordBox passwordBox)
|
||||
passwordBox.GetType().GetMethod("Select", [typeof(int), typeof(int)])
|
||||
?.Invoke(passwordBox, [passwordBox.Password.Length, 0]);
|
||||
}
|
||||
|
||||
private static void OnPasswordBoxPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not PasswordBox passwordBox || e.ClickCount > 1)
|
||||
return;
|
||||
|
||||
passwordBox.Focus();
|
||||
passwordBox.GetType().GetMethod("Select", [typeof(int), typeof(int)])
|
||||
?.Invoke(passwordBox, [passwordBox.Password.Length, 0]);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnPasswordBoxDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not PasswordBox passwordBox)
|
||||
return;
|
||||
|
||||
passwordBox.SelectAll();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
private const string TunAddress = "172.18.0.1/30";
|
||||
private const string VpnLocalIp = "172.18.0.1"; // actual TUN interface address
|
||||
private const int DefaultTunMtu = 1500;
|
||||
private const int MixedProxyPort = 2080; // sing-box SOCKS5/HTTP inbound for accurate ping
|
||||
private const int DefaultMixedProxyPort = 2080; // preferred sing-box SOCKS5/HTTP inbound for accurate ping
|
||||
|
||||
private readonly string _singBoxExe;
|
||||
private readonly string _workDir;
|
||||
@@ -124,7 +124,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
_tunnelFailedFired = 0;
|
||||
|
||||
Status.State = ConnectionState.Connecting;
|
||||
Status.Message = "در حال آمادهسازی V2Ray...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال آمادهسازی V2Ray...");
|
||||
Logger.Info("V2RayTunnelProvider: starting");
|
||||
|
||||
try
|
||||
@@ -138,7 +138,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
if (!File.Exists(_singBoxExe))
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"فایل sing-box.exe پیدا نشد: {_singBoxExe}";
|
||||
Status.Message = LocalizationService.Instance.Format("فایل sing-box.exe پیدا نشد: {0}", _singBoxExe);
|
||||
Logger.Error(Status.Message);
|
||||
return false;
|
||||
}
|
||||
@@ -146,6 +146,9 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
// Build and write config
|
||||
int tunMtu = DefaultTunMtu;
|
||||
string singBoxJson;
|
||||
using var mixedProxyPortReservation = LocalPortReservation.ReservePreferredOrRandom(DefaultMixedProxyPort);
|
||||
var mixedProxyPort = mixedProxyPortReservation.Port;
|
||||
Logger.Info($"[PORT] sing-box mixed proxy=127.0.0.1:{mixedProxyPort}");
|
||||
try
|
||||
{
|
||||
if (config.AutoTuneMtu)
|
||||
@@ -163,18 +166,20 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
Logger.Info($"[MTU] Auto-tune disabled; using MTU={tunMtu}");
|
||||
}
|
||||
|
||||
singBoxJson = BuildSingBoxConfig(config.V2RayConfig, tunMtu, config.EnableDnsOptimization);
|
||||
singBoxJson = BuildSingBoxConfig(config.V2RayConfig, tunMtu, config.EnableDnsOptimization, mixedProxyPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
mixedProxyPortReservation.Dispose();
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"خطا در پارس کانفیگ: {ex.Message}";
|
||||
Status.Message = LocalizationService.Instance.Format("خطا در پارس کانفیگ: {0}", ex.Message);
|
||||
Logger.Error("V2Ray config parse error", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(_configPath, singBoxJson, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), ct);
|
||||
Logger.Info($"V2Ray config written to {_configPath}");
|
||||
mixedProxyPortReservation.Dispose();
|
||||
|
||||
// Start sing-box process
|
||||
_process = new Process
|
||||
@@ -257,7 +262,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
_process.BeginOutputReadLine();
|
||||
|
||||
Logger.Info($"sing-box started (PID {_process.Id})");
|
||||
Status.Message = "در حال انتظار برای interface TunnelX-V2Ray...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال انتظار برای interface TunnelX-V2Ray...");
|
||||
|
||||
// Wait up to 10 seconds for the TUN interface to appear
|
||||
int interfaceIndex = -1;
|
||||
@@ -268,7 +273,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
if (_process.HasExited)
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"sing-box زودتر خارج شد (exit code {_process.ExitCode}) — کانفیگ را بررسی کنید";
|
||||
Status.Message = LocalizationService.Instance.Format("sing-box زودتر خارج شد (exit code {0}) — کانفیگ را بررسی کنید", _process.ExitCode);
|
||||
Logger.Error(Status.Message);
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
@@ -282,7 +287,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
if (interfaceIndex <= 0)
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = "interface TunnelX-V2Ray ظاهر نشد (timeout 10s)";
|
||||
Status.Message = LocalizationService.Instance.T("interface TunnelX-V2Ray ظاهر نشد (timeout 10s)");
|
||||
Logger.Error(Status.Message);
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
@@ -299,7 +304,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
Status.VpnServerIp = statusServerHost;
|
||||
Status.VpnServerPort = statusServerPort;
|
||||
Status.VpnInterfaceIndex = interfaceIndex;
|
||||
Status.SingBoxMixedPort = MixedProxyPort;
|
||||
Status.SingBoxMixedPort = mixedProxyPort;
|
||||
Status.Message = config.TunnelType == TunnelType.SocksProxy ? "Proxy connected" : "V2Ray connected";
|
||||
Logger.Info($"{(config.TunnelType == TunnelType.SocksProxy ? "Proxy" : "V2Ray")} tunnel up — interface index {interfaceIndex}, server={Status.VpnServerIp}:{Status.VpnServerPort}");
|
||||
|
||||
@@ -308,14 +313,14 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Status.State = ConnectionState.Disconnected;
|
||||
Status.Message = "اتصال لغو شد";
|
||||
Status.Message = LocalizationService.Instance.T("اتصال لغو شد");
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"خطا: {ex.Message}";
|
||||
Status.Message = LocalizationService.Instance.Format("خطا: {0}", ex.Message);
|
||||
Logger.Error("V2RayTunnelProvider.ConnectAsync failed", ex);
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
@@ -329,7 +334,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
Status.State = ConnectionState.Disconnecting;
|
||||
Status.Message = "در حال قطع اتصال V2Ray...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال قطع اتصال V2Ray...");
|
||||
|
||||
// Stop watchdog from firing during/after deliberate disconnect.
|
||||
_tunnelFailedFired = 1;
|
||||
@@ -348,7 +353,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
Status.VpnServerPort = 0;
|
||||
Status.VpnInterfaceIndex = -1;
|
||||
Status.SingBoxMixedPort = 0;
|
||||
Status.Message = "قطع شد";
|
||||
Status.Message = LocalizationService.Instance.T("قطع شد");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -375,7 +380,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
// Config builder
|
||||
// =========================================================================
|
||||
|
||||
private string BuildSingBoxConfig(string userConfig, int tunMtu, bool enableDnsOptimization)
|
||||
private string BuildSingBoxConfig(string userConfig, int tunMtu, bool enableDnsOptimization, int mixedProxyPort)
|
||||
{
|
||||
userConfig = userConfig.Trim();
|
||||
|
||||
@@ -476,7 +481,7 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
["type"] = "mixed",
|
||||
["tag"] = "mixed-in",
|
||||
["listen"] = "127.0.0.1",
|
||||
["listen_port"] = MixedProxyPort
|
||||
["listen_port"] = mixedProxyPort
|
||||
}
|
||||
},
|
||||
// direct outbound is required so sing-box can reach the proxy
|
||||
@@ -612,6 +617,11 @@ public class V2RayTunnelProvider : ITunnelProvider
|
||||
};
|
||||
if (security == "reality")
|
||||
{
|
||||
tlsObj["utls"] = new JsonObject
|
||||
{
|
||||
["enabled"] = true,
|
||||
["fingerprint"] = query.GetValueOrDefault("fp", "chrome")
|
||||
};
|
||||
tlsObj["reality"] = new JsonObject
|
||||
{
|
||||
["enabled"] = true,
|
||||
|
||||
@@ -27,7 +27,7 @@ public class VpnService
|
||||
TunnelType.V2Ray => TunnelProviderFactory.Create(config.V2RayConfig),
|
||||
TunnelType.OpenVpn => new OpenVpnTunnelProvider(),
|
||||
TunnelType.SocksProxy => new V2RayTunnelProvider(),
|
||||
_ => throw new NotImplementedException($"نوع تانل ناشناخته: {config.TunnelType}")
|
||||
_ => throw new NotImplementedException(LocalizationService.Instance.Format("نوع تانل ناشناخته: {0}", config.TunnelType))
|
||||
};
|
||||
|
||||
// Wire up the tunnel-failure watchdog for V2Ray connections.
|
||||
|
||||
@@ -14,8 +14,8 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
private const string TunAddress = "172.18.0.1/30";
|
||||
private const string VpnLocalIp = "172.18.0.1";
|
||||
private const int DefaultTunMtu = 1500;
|
||||
private const int MixedProxyPort = 2080;
|
||||
private const int XraySocksPort = 2081;
|
||||
private const int DefaultMixedProxyPort = 2080;
|
||||
private const int DefaultXraySocksPort = 2081;
|
||||
|
||||
private readonly string _workDir;
|
||||
private readonly string _xrayConfigPath;
|
||||
@@ -53,7 +53,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
public async Task<bool> ConnectAsync(ServerConfig config, CancellationToken ct)
|
||||
{
|
||||
Status.State = ConnectionState.Connecting;
|
||||
Status.Message = "در حال آماده سازی Xray...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال آماده سازی Xray...");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -62,9 +62,9 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
await EnsureEmbeddedExeExtractedAsync("sing-box.exe", _singBoxExe, ct);
|
||||
|
||||
if (!File.Exists(_xrayExe))
|
||||
return Fail($"فایل xray.exe پیدا نشد: {_xrayExe}");
|
||||
return Fail(LocalizationService.Instance.Format("فایل xray.exe پیدا نشد: {0}", _xrayExe));
|
||||
if (!File.Exists(_singBoxExe))
|
||||
return Fail($"فایل sing-box.exe پیدا نشد: {_singBoxExe}");
|
||||
return Fail(LocalizationService.Instance.Format("فایل sing-box.exe پیدا نشد: {0}", _singBoxExe));
|
||||
|
||||
var outbound = BuildXrayOutbound(config.V2RayConfig);
|
||||
if (config.EnableDnsOptimization)
|
||||
@@ -85,12 +85,23 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
Logger.Info($"[MTU] Auto-tune disabled; using MTU={tunMtu}");
|
||||
}
|
||||
|
||||
var xrayJson = BuildXraySocksConfig(outbound);
|
||||
var singBoxJson = BuildTunBridgeConfig(tunMtu);
|
||||
using var xraySocksPortReservation = LocalPortReservation.ReservePreferredOrRandom(DefaultXraySocksPort);
|
||||
using var mixedProxyPortReservation = LocalPortReservation.ReservePreferredOrRandom(
|
||||
DefaultMixedProxyPort,
|
||||
xraySocksPortReservation.Port);
|
||||
var xraySocksPort = xraySocksPortReservation.Port;
|
||||
var mixedProxyPort = mixedProxyPortReservation.Port;
|
||||
Logger.Info($"[PORT] Xray SOCKS=127.0.0.1:{xraySocksPort}, mixed proxy=127.0.0.1:{mixedProxyPort}");
|
||||
|
||||
var xrayJson = BuildXraySocksConfig(outbound, xraySocksPort);
|
||||
var singBoxJson = BuildTunBridgeConfig(tunMtu, mixedProxyPort, xraySocksPort);
|
||||
|
||||
await File.WriteAllTextAsync(_xrayConfigPath, xrayJson, new UTF8Encoding(false), ct);
|
||||
await File.WriteAllTextAsync(_singBoxConfigPath, singBoxJson, new UTF8Encoding(false), ct);
|
||||
|
||||
xraySocksPortReservation.Dispose();
|
||||
mixedProxyPortReservation.Dispose();
|
||||
|
||||
_xrayProcess = StartProcess(
|
||||
_xrayExe,
|
||||
$"run -c \"{_xrayConfigPath}\"",
|
||||
@@ -106,13 +117,13 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
"[sing-box bridge stderr]");
|
||||
|
||||
Logger.Info($"sing-box TUN bridge started (PID {_singBoxProcess.Id})");
|
||||
Status.Message = "در حال انتظار برای interface TunnelX-V2Ray...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال انتظار برای interface TunnelX-V2Ray...");
|
||||
|
||||
var interfaceIndex = await WaitForTunInterfaceAsync(ct);
|
||||
if (interfaceIndex <= 0)
|
||||
{
|
||||
await KillProcessAsync();
|
||||
return Fail("interface TunnelX-V2Ray ظاهر نشد (timeout 10s)");
|
||||
return Fail(LocalizationService.Instance.T("interface TunnelX-V2Ray ظاهر نشد (timeout 10s)"));
|
||||
}
|
||||
|
||||
_vpnInterfaceIndex = interfaceIndex;
|
||||
@@ -123,7 +134,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
Status.VpnServerIp = Status.VpnServerHost;
|
||||
Status.VpnServerPort = ExtractServerPort(config.V2RayConfig);
|
||||
Status.VpnInterfaceIndex = interfaceIndex;
|
||||
Status.SingBoxMixedPort = MixedProxyPort;
|
||||
Status.SingBoxMixedPort = mixedProxyPort;
|
||||
Status.Message = "Xray connected";
|
||||
|
||||
Logger.Info($"Xray tunnel up via sing-box TUN bridge — interface index {interfaceIndex}, server={Status.VpnServerIp}");
|
||||
@@ -132,7 +143,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Status.State = ConnectionState.Disconnected;
|
||||
Status.Message = "اتصال لغو شد";
|
||||
Status.Message = LocalizationService.Instance.T("اتصال لغو شد");
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
}
|
||||
@@ -140,7 +151,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
{
|
||||
Logger.Error("XrayTunnelProvider.ConnectAsync failed", ex);
|
||||
Status.State = ConnectionState.Error;
|
||||
Status.Message = $"خطا: {ex.Message}";
|
||||
Status.Message = LocalizationService.Instance.Format("خطا: {0}", ex.Message);
|
||||
await KillProcessAsync();
|
||||
return false;
|
||||
}
|
||||
@@ -149,7 +160,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
Status.State = ConnectionState.Disconnecting;
|
||||
Status.Message = "در حال قطع اتصال Xray...";
|
||||
Status.Message = LocalizationService.Instance.T("در حال قطع اتصال Xray...");
|
||||
|
||||
await KillProcessAsync();
|
||||
|
||||
@@ -165,7 +176,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
Status.VpnServerPort = 0;
|
||||
Status.VpnInterfaceIndex = -1;
|
||||
Status.SingBoxMixedPort = 0;
|
||||
Status.Message = "قطع شد";
|
||||
Status.Message = LocalizationService.Instance.T("قطع شد");
|
||||
}
|
||||
|
||||
public bool IsInterfaceUp()
|
||||
@@ -219,7 +230,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
throw new InvalidOperationException("xhttp فعلا فقط برای vless:// یا JSON دارای outbound xhttp پشتیبانی میشود");
|
||||
}
|
||||
|
||||
private static string BuildXraySocksConfig(JsonObject outbound)
|
||||
private static string BuildXraySocksConfig(JsonObject outbound, int xraySocksPort)
|
||||
{
|
||||
var doc = new JsonObject
|
||||
{
|
||||
@@ -229,7 +240,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
new JsonObject
|
||||
{
|
||||
["listen"] = "127.0.0.1",
|
||||
["port"] = XraySocksPort,
|
||||
["port"] = xraySocksPort,
|
||||
["protocol"] = "socks",
|
||||
["settings"] = new JsonObject
|
||||
{
|
||||
@@ -265,7 +276,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildTunBridgeConfig(int tunMtu)
|
||||
private static string BuildTunBridgeConfig(int tunMtu, int mixedProxyPort, int xraySocksPort)
|
||||
{
|
||||
var doc = new JsonObject
|
||||
{
|
||||
@@ -288,7 +299,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
["type"] = "mixed",
|
||||
["tag"] = "mixed-in",
|
||||
["listen"] = "127.0.0.1",
|
||||
["listen_port"] = MixedProxyPort
|
||||
["listen_port"] = mixedProxyPort
|
||||
}
|
||||
},
|
||||
["outbounds"] = new JsonArray
|
||||
@@ -298,7 +309,7 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
["type"] = "socks",
|
||||
["tag"] = "xray-socks",
|
||||
["server"] = "127.0.0.1",
|
||||
["server_port"] = XraySocksPort,
|
||||
["server_port"] = xraySocksPort,
|
||||
["version"] = "5"
|
||||
},
|
||||
new JsonObject { ["type"] = "direct", ["tag"] = "direct" }
|
||||
@@ -491,9 +502,9 @@ public class XrayTunnelProvider : ITunnelProvider
|
||||
while (DateTime.UtcNow < deadline && !ct.IsCancellationRequested)
|
||||
{
|
||||
if (_xrayProcess?.HasExited == true)
|
||||
throw new InvalidOperationException($"xray زودتر خارج شد (exit code {_xrayProcess.ExitCode})");
|
||||
throw new InvalidOperationException(LocalizationService.Instance.Format("xray زودتر خارج شد (exit code {0})", _xrayProcess.ExitCode));
|
||||
if (_singBoxProcess?.HasExited == true)
|
||||
throw new InvalidOperationException($"sing-box bridge زودتر خارج شد (exit code {_singBoxProcess.ExitCode})");
|
||||
throw new InvalidOperationException(LocalizationService.Instance.Format("sing-box bridge زودتر خارج شد (exit code {0})", _singBoxProcess.ExitCode));
|
||||
|
||||
var idx = FindInterfaceIndex(TunInterfaceName);
|
||||
if (idx > 0) return idx;
|
||||
|
||||
@@ -69,7 +69,7 @@ public partial class MainViewModel
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "Applications (*.exe)|*.exe",
|
||||
Title = "انتخاب برنامه"
|
||||
Title = LocalizationService.Instance.T("انتخاب برنامه")
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == true)
|
||||
|
||||
@@ -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);
|
||||
@@ -536,26 +540,41 @@ public partial class MainViewModel
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
var hosts = new[] { "api.ipify.org", "ipv4.icanhazip.com" };
|
||||
Exception? lastError = null;
|
||||
|
||||
for (var attempt = 1; attempt <= 4 && _connectionState == ConnectionState.Connected; attempt++)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
var ip = await QueryPublicIpViaHttpConnectProxyAsync(proxyPort, cts.Token);
|
||||
ConnectionIpText = string.IsNullOrWhiteSpace(ip) ? "-" : ip;
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
var ip = await QueryPublicIpViaHttpConnectProxyAsync(proxyPort, host, cts.Token);
|
||||
if (IPAddress.TryParse(ip, out _))
|
||||
{
|
||||
ConnectionIpText = ip;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is OperationCanceledException or IOException or SocketException or AuthenticationException)
|
||||
{
|
||||
lastError = ex;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(1200 * attempt));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
ConnectionIpText = "timeout";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"[EXIT-IP] Public IP check failed: {ex.Message}");
|
||||
|
||||
if (lastError != null)
|
||||
Logger.Warning($"[EXIT-IP] Public IP check failed after retries: {lastError.Message}");
|
||||
|
||||
if (_connectionState == ConnectionState.Connected)
|
||||
ConnectionIpText = "-";
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> QueryPublicIpViaHttpConnectProxyAsync(int proxyPort, CancellationToken ct)
|
||||
private static async Task<string> QueryPublicIpViaHttpConnectProxyAsync(int proxyPort, string host, CancellationToken ct)
|
||||
{
|
||||
const string host = "api.ipify.org";
|
||||
using var tcp = new TcpClient();
|
||||
tcp.NoDelay = true;
|
||||
await tcp.ConnectAsync("127.0.0.1", proxyPort, ct);
|
||||
@@ -574,7 +593,7 @@ public partial class MainViewModel
|
||||
await ssl.AuthenticateAsClientAsync(host, null, SslProtocols.Tls12 | SslProtocols.Tls13, checkCertificateRevocation: false);
|
||||
|
||||
var request = Encoding.ASCII.GetBytes(
|
||||
$"GET /?format=text HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nUser-Agent: TunnelX\r\n\r\n");
|
||||
$"GET / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nUser-Agent: TunnelX\r\n\r\n");
|
||||
await ssl.WriteAsync(request, ct);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
@@ -676,7 +695,7 @@ public partial class MainViewModel
|
||||
}
|
||||
}
|
||||
|
||||
ServerPingResult = $"هیچ remote قابلدسترسی نبود ({lastError?.Message ?? "timeout"})";
|
||||
ServerPingResult = LocalizationService.Instance.Format("هیچ remote قابلدسترسی نبود ({0})", lastError?.Message ?? "timeout");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -712,7 +731,7 @@ public partial class MainViewModel
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ServerPingResult = $"خطا: {ex.Message}";
|
||||
ServerPingResult = LocalizationService.Instance.Format("خطا: {0}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -817,7 +836,7 @@ public partial class MainViewModel
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PingResult = $"خطا: {ex.Message}";
|
||||
PingResult = LocalizationService.Instance.Format("خطا: {0}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1019,7 +1038,7 @@ public partial class MainViewModel
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"پارس کانفیگ ناموفق بود: {ex.Message}";
|
||||
error = LocalizationService.Instance.Format("پارس کانفیگ ناموفق بود: {0}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1185,7 +1204,7 @@ public partial class MainViewModel
|
||||
{
|
||||
IsPinging = false;
|
||||
if (sent > 0)
|
||||
PingResult += " [پایان]";
|
||||
PingResult += LocalizationService.Instance.T(" [پایان]");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
StatusText = $"خطا: {t.Exception?.InnerException?.Message}");
|
||||
StatusText = LocalizationService.Instance.Format("خطا: {0}", t.Exception?.InnerException?.Message));
|
||||
}, TaskScheduler.Default);
|
||||
}, _ => !IsBusy || ConnectionState == ConnectionState.Connecting);
|
||||
AddAppCommand = new RelayCommand(_ => AddCustomApp());
|
||||
@@ -73,9 +73,13 @@ 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));
|
||||
ToggleLanguageCommand = new RelayCommand(_ => ToggleLanguage());
|
||||
|
||||
LocalizationService.Instance.LanguageChanged += (_, _) => OnLanguageChanged();
|
||||
|
||||
_trafficRouter.TrafficUpdated += OnTrafficUpdated;
|
||||
|
||||
@@ -114,7 +118,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
StatusText = $"خطای اتصال خودکار: {t.Exception?.InnerException?.Message}");
|
||||
StatusText = LocalizationService.Instance.Format("خطای اتصال خودکار: {0}", t.Exception?.InnerException?.Message));
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
}
|
||||
@@ -197,7 +201,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
private string _mixedProxyPortStatusText = "";
|
||||
public string MixedProxyPortStatusText
|
||||
{
|
||||
get => _mixedProxyPortStatusText;
|
||||
get => LocalizationService.Instance.T(_mixedProxyPortStatusText);
|
||||
set { _mixedProxyPortStatusText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
@@ -244,8 +248,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
|
||||
public string GameModeStatusText => IsGameModeEnabled
|
||||
? "Game Mode فعال است: Route نگهداری طولانیتر، DNS سریعتر و DSCP برای بستههای بازی اعمال میشود."
|
||||
: "Game Mode غیرفعال است: حالت متعادل برای مصرف عمومی.";
|
||||
? LocalizationService.Instance.T("Game Mode فعال است: Route نگهداری طولانیتر، DNS سریعتر و DSCP برای بستههای بازی اعمال میشود.")
|
||||
: LocalizationService.Instance.T("Game Mode غیرفعال است: حالت متعادل برای مصرف عمومی.");
|
||||
|
||||
private bool _startWithWindows;
|
||||
public bool StartWithWindows
|
||||
@@ -287,6 +291,19 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public string LanguageToggleText => LocalizationService.Instance.ToggleLanguageText;
|
||||
public bool AppIsRightToLeft => LocalizationService.Instance.IsRightToLeft;
|
||||
public string AppTitleText => LocalizationService.Instance.IsRightToLeft ? "تانلکس" : "TunnelX";
|
||||
public string AppTitleAccentText => LocalizationService.Instance.IsRightToLeft ? "س" : "X";
|
||||
public System.Windows.FlowDirection AppTitleFlowDirection => LocalizationService.Instance.FlowDirection;
|
||||
public System.Windows.HorizontalAlignment AppTitleAccentAlignment => LocalizationService.Instance.IsRightToLeft
|
||||
? System.Windows.HorizontalAlignment.Left
|
||||
: System.Windows.HorizontalAlignment.Right;
|
||||
public System.Windows.FlowDirection AppFlowDirection => LocalizationService.Instance.FlowDirection;
|
||||
public System.Windows.TextAlignment AppTextAlignment => LocalizationService.Instance.TextAlignment;
|
||||
public System.Windows.HorizontalAlignment AppStartHorizontalAlignment => LocalizationService.Instance.StartHorizontalAlignment;
|
||||
public System.Windows.HorizontalAlignment AppEndHorizontalAlignment => LocalizationService.Instance.EndHorizontalAlignment;
|
||||
|
||||
private bool _isBusy;
|
||||
public bool IsBusy
|
||||
{
|
||||
@@ -310,6 +327,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsConnected));
|
||||
OnPropertyChanged(nameof(ConnectButtonText));
|
||||
OnPropertyChanged(nameof(ConnectButtonToolTip));
|
||||
OnPropertyChanged(nameof(StatusColor));
|
||||
OnPropertyChanged(nameof(StatusText));
|
||||
OnPropertyChanged(nameof(IsOpenVpnConnectionPending));
|
||||
@@ -351,8 +369,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
|
||||
public string OpenVpnPrerequisiteText => IsOpenVpnCommunityInstalled
|
||||
? $"پیشنیاز آماده است: نسخه Community اوپنویپیان پیدا شد: {OpenVpnDetectedPath}"
|
||||
: "اخطار: نسخه Community اوپنویپیان نصب نیست. برای استفاده از اسپلیتتانلینگ با این نوع اتصال، ابتدا آن را از لینک رسمی نصب کنید.";
|
||||
? LocalizationService.Instance.Format("پیشنیاز آماده است: نسخه Community اوپنویپیان پیدا شد: {0}", OpenVpnDetectedPath)
|
||||
: LocalizationService.Instance.T("اخطار: نسخه Community اوپنویپیان نصب نیست. برای استفاده از اسپلیتتانلینگ با این نوع اتصال، ابتدا آن را از لینک رسمی نصب کنید.");
|
||||
|
||||
public string OpenVpnPrerequisiteColor => IsOpenVpnCommunityInstalled ? "#6CCB5F" : "#E0A020";
|
||||
|
||||
@@ -362,8 +380,24 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
public string AppCreatorText => AppInfo.CreatorText;
|
||||
public string AppGitHubUrl => AppInfo.GitHubUrl;
|
||||
public string AppLicenseText => AppInfo.LicenseName;
|
||||
public string DonatePayPalText => $"پیپل: {AppInfo.PayPalEmail}";
|
||||
public string CryptoDonationText => AppInfo.CryptoDonationText;
|
||||
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 LogClearButtonText => LocalizationService.Instance.T("پاک کردن");
|
||||
public string LogCopyErrorButtonText => LocalizationService.Instance.T("کپی خطا");
|
||||
public string LogCopyAllButtonText => LocalizationService.Instance.T("کپی همه");
|
||||
public string LogClearToolTipText => LocalizationService.Instance.T("پاک کردن همه لاگها");
|
||||
public string LogCopyErrorToolTipText => LocalizationService.Instance.T("کپی آخرین خطا یا هشدار");
|
||||
public string LogCopyAllToolTipText => LocalizationService.Instance.T("کپی کردن همه لاگها");
|
||||
public string DonatePayPalText => LocalizationService.Instance.IsRightToLeft
|
||||
? $"پیپل: {AppInfo.PayPalEmail}"
|
||||
: $"PayPal: {AppInfo.PayPalEmail}";
|
||||
public string CryptoDonationText => LocalizationService.Instance.IsRightToLeft
|
||||
? AppInfo.CryptoDonationText
|
||||
: AppInfo.CryptoDonationTextEn;
|
||||
|
||||
private bool _isCheckingForUpdates;
|
||||
public bool IsCheckingForUpdates
|
||||
@@ -394,7 +428,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
private string _updateStatusText = "برای بررسی نسخه جدید، دکمه بررسی بروزرسانی را بزنید.";
|
||||
public string UpdateStatusText
|
||||
{
|
||||
get => _updateStatusText;
|
||||
get => LocalizationService.Instance.T(_updateStatusText);
|
||||
set
|
||||
{
|
||||
if (_updateStatusText == value) return;
|
||||
@@ -416,16 +450,41 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public string UpdateButtonText => IsCheckingForUpdates ? "در حال بررسی..." : "بررسی بروزرسانی";
|
||||
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("بررسی بروزرسانی");
|
||||
|
||||
public string ConnectButtonText => _connectionState switch
|
||||
{
|
||||
ConnectionState.Disconnected => "🔌 اتصال",
|
||||
ConnectionState.Connecting => "❌ لغو اتصال",
|
||||
ConnectionState.Connected => "🔴 قطع اتصال",
|
||||
ConnectionState.Disconnecting => "⏳ در حال قطع...",
|
||||
ConnectionState.Error => "🔌 اتصال مجدد",
|
||||
_ => "اتصال"
|
||||
ConnectionState.Disconnected => LocalizationService.Instance.T("🔌 اتصال"),
|
||||
ConnectionState.Connecting => LocalizationService.Instance.T("❌ لغو اتصال"),
|
||||
ConnectionState.Connected => LocalizationService.Instance.T("🔴 قطع اتصال"),
|
||||
ConnectionState.Disconnecting => LocalizationService.Instance.T("⏳ در حال قطع..."),
|
||||
ConnectionState.Error => LocalizationService.Instance.T("🔌 اتصال مجدد"),
|
||||
_ => LocalizationService.Instance.T("اتصال")
|
||||
};
|
||||
|
||||
public string ConnectButtonToolTip => _connectionState switch
|
||||
{
|
||||
ConnectionState.Connecting => LocalizationService.Instance.T("لغو تلاش اتصال"),
|
||||
ConnectionState.Connected => LocalizationService.Instance.T("قطع اتصال فعلی"),
|
||||
ConnectionState.Disconnecting => LocalizationService.Instance.T("در حال قطع اتصال..."),
|
||||
ConnectionState.Error => LocalizationService.Instance.T("اتصال مجدد"),
|
||||
_ => LocalizationService.Instance.T("شروع اتصال با پروفایل انتخابشده")
|
||||
};
|
||||
|
||||
public string StatusColor => _connectionState switch
|
||||
@@ -439,7 +498,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
private string _statusText = "آماده اتصال";
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
get => LocalizationService.Instance.T(_statusText);
|
||||
set { _statusText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
@@ -663,21 +722,21 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
private string _configCoreHint = "";
|
||||
public string ConfigCoreHint
|
||||
{
|
||||
get => _configCoreHint;
|
||||
get => LocalizationService.Instance.T(_configCoreHint);
|
||||
set { _configCoreHint = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private string _configValidationText = "";
|
||||
public string ConfigValidationText
|
||||
{
|
||||
get => _configValidationText;
|
||||
get => LocalizationService.Instance.T(_configValidationText);
|
||||
set { _configValidationText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private string _saveStatusText = "";
|
||||
public string SaveStatusText
|
||||
{
|
||||
get => _saveStatusText;
|
||||
get => LocalizationService.Instance.T(_saveStatusText);
|
||||
set { _saveStatusText = value; OnPropertyChanged(); OnPropertyChanged(nameof(ProfileSaveHintText)); }
|
||||
}
|
||||
|
||||
@@ -698,11 +757,11 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
private string _connectionIpText = "-";
|
||||
public string ConnectionIpText
|
||||
{
|
||||
get => _connectionIpText;
|
||||
get => LocalizationService.Instance.T(_connectionIpText);
|
||||
set { _connectionIpText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string ConnectionIpLabel => "IP خروجی";
|
||||
public string ConnectionIpLabel => LocalizationService.Instance.T("IP خروجی");
|
||||
|
||||
private string _vpnAdapterName = "";
|
||||
public string VpnAdapterName
|
||||
@@ -740,13 +799,15 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
|
||||
public string FullRouteStatusText => _isFullRouteEnabled
|
||||
? "Full Route فعال است؛ کل سیستم از تونل عبور میکند"
|
||||
: "Split فعال است؛ فقط برنامهها و مقصدهای انتخابی از تونل عبور میکنند";
|
||||
? LocalizationService.Instance.T("Full Route فعال است؛ کل سیستم از تونل عبور میکند")
|
||||
: LocalizationService.Instance.T("Split فعال است؛ فقط برنامهها و مقصدهای انتخابی از تونل عبور میکنند");
|
||||
|
||||
public string RouteModeTitle => IsFullRouteEnabled ? "حالت کل سیستم" : "حالت انتخابی";
|
||||
public string RouteModeTitle => IsFullRouteEnabled
|
||||
? LocalizationService.Instance.T("حالت کل سیستم")
|
||||
: LocalizationService.Instance.T("حالت انتخابی");
|
||||
public string RouteModeDescription => IsFullRouteEnabled
|
||||
? "ترافیک کل سیستم از تونل عبور خواهد کرد؛ برای وقتی مناسب است که همه برنامهها باید پشت تونل باشند."
|
||||
: "فقط برنامهها و مقصدهای انتخابی از تونل عبور میکنند؛ بقیه ترافیک مستقیم میماند.";
|
||||
? LocalizationService.Instance.T("ترافیک کل سیستم از تونل عبور خواهد کرد؛ برای وقتی مناسب است که همه برنامهها باید پشت تونل باشند.")
|
||||
: LocalizationService.Instance.T("فقط برنامهها و مقصدهای انتخابی از تونل عبور میکنند؛ بقیه ترافیک مستقیم میماند.");
|
||||
|
||||
public string HeaderCoreText => $"Core: {ActiveCoreName}";
|
||||
public string HeaderRouteText => IsFullRouteEnabled ? "Mode: Full" : "Mode: Split";
|
||||
@@ -781,8 +842,16 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
: "-";
|
||||
|
||||
public string ConnectedBadgeText => CurrentTunnelType == TunnelType.SocksProxy
|
||||
? "متصل به پراکسی"
|
||||
: "متصل به VPN";
|
||||
? 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
|
||||
{
|
||||
@@ -836,7 +905,9 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
set { _isPinging = value; OnPropertyChanged(); OnPropertyChanged(nameof(PingButtonText)); }
|
||||
}
|
||||
|
||||
public string PingButtonText => _isPinging ? "توقف تست" : "تست مقصد";
|
||||
public string PingButtonText => _isPinging
|
||||
? LocalizationService.Instance.T("توقف تست")
|
||||
: LocalizationService.Instance.T("تست مقصد");
|
||||
|
||||
private bool _isTestingConnectedServerPing;
|
||||
public bool IsTestingConnectedServerPing
|
||||
@@ -851,12 +922,14 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public string ConnectedServerPingButtonText => IsTestingConnectedServerPing ? "در حال پینگ..." : "پینگ سرور";
|
||||
public string ConnectedServerPingButtonText => IsTestingConnectedServerPing
|
||||
? LocalizationService.Instance.T("در حال پینگ...")
|
||||
: LocalizationService.Instance.T("پینگ سرور");
|
||||
|
||||
private string _pingResult = "";
|
||||
public string PingResult
|
||||
{
|
||||
get => _pingResult;
|
||||
get => LocalizationService.Instance.T(_pingResult);
|
||||
set { _pingResult = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
@@ -873,12 +946,14 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public string ServerPingButtonText => _isTestingServerPing ? "در حال تست..." : "تست سرور";
|
||||
public string ServerPingButtonText => _isTestingServerPing
|
||||
? LocalizationService.Instance.T("در حال تست...")
|
||||
: LocalizationService.Instance.T("تست سرور");
|
||||
|
||||
private string _serverPingResult = "";
|
||||
public string ServerPingResult
|
||||
{
|
||||
get => _serverPingResult;
|
||||
get => LocalizationService.Instance.T(_serverPingResult);
|
||||
set { _serverPingResult = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
@@ -995,9 +1070,11 @@ 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; }
|
||||
public ICommand ToggleLanguageCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1024,10 +1101,10 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
try
|
||||
{
|
||||
var text =
|
||||
$"{AppInfo.AppName} - حمایت از پروژه\n" +
|
||||
$"{AppInfo.AppName} - {LocalizationService.Instance.T("حمایت از پروژه")}\n" +
|
||||
$"PayPal: {AppInfo.PayPalEmail}\n" +
|
||||
$"PayPal link: {AppInfo.PayPalDonateUrl}\n\n" +
|
||||
AppInfo.CryptoDonationText;
|
||||
CryptoDonationText;
|
||||
System.Windows.Clipboard.SetText(text);
|
||||
Logger.Info("[UI] Donation info copied to clipboard");
|
||||
}
|
||||
@@ -1065,13 +1142,13 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
if (latest.Version > current)
|
||||
{
|
||||
IsUpdateAvailable = true;
|
||||
UpdateStatusText = $"نسخه جدید آماده است: {latest.TagName} - برای دانلود از GitHub باز کنید.";
|
||||
UpdateStatusText = LocalizationService.Instance.Format("نسخه جدید آماده است: {0} - برای دانلود از GitHub باز کنید.", latest.TagName);
|
||||
Logger.Info($"[UPDATE] New version available: current={current} latest={latest.TagName}");
|
||||
return;
|
||||
}
|
||||
|
||||
IsUpdateAvailable = false;
|
||||
UpdateStatusText = $"TunnelX بهروز است. نسخه فعلی: {AppInfo.VersionText}";
|
||||
UpdateStatusText = LocalizationService.Instance.Format("TunnelX بهروز است. نسخه فعلی: {0}", AppInfo.VersionText);
|
||||
Logger.Info($"[UPDATE] App is up to date: current={current} latest={latest.TagName}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -1083,7 +1160,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!silent)
|
||||
UpdateStatusText = $"بررسی بروزرسانی ناموفق بود: {ex.Message}";
|
||||
UpdateStatusText = LocalizationService.Instance.Format("بررسی بروزرسانی ناموفق بود: {0}", ex.Message);
|
||||
Logger.Warning($"[UPDATE] Latest release check failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
@@ -1092,6 +1169,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
|
||||
@@ -1107,7 +1239,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConfigValidationText = $"خواندن کلیپبورد ناموفق بود: {ex.Message}";
|
||||
ConfigValidationText = LocalizationService.Instance.Format("خواندن کلیپبورد ناموفق بود: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1126,8 +1258,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
{
|
||||
var dialog = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "انتخاب فایل OpenVPN",
|
||||
Filter = "OpenVPN config (*.ovpn)|*.ovpn|All files (*.*)|*.*",
|
||||
Title = LocalizationService.Instance.T("انتخاب فایل OpenVPN"),
|
||||
Filter = LocalizationService.Instance.T("OpenVPN config (*.ovpn)|*.ovpn|All files (*.*)|*.*"),
|
||||
CheckFileExists = true,
|
||||
Multiselect = false
|
||||
};
|
||||
@@ -1141,7 +1273,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConfigValidationText = $"خواندن فایل OpenVPN ناموفق بود: {ex.Message}";
|
||||
ConfigValidationText = LocalizationService.Instance.Format("خواندن فایل OpenVPN ناموفق بود: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1199,7 +1331,7 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
: "هسته: sing-box";
|
||||
|
||||
ConfigValidationText = TryExtractProxyEndpoint(config, out var server, out var port, out var error)
|
||||
? $"سرور: {server}:{port}"
|
||||
? LocalizationService.Instance.Format("سرور: {0}:{1}", server, port)
|
||||
: error;
|
||||
}
|
||||
|
||||
@@ -1207,8 +1339,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
{
|
||||
var endpoint = $"{ProxyServerAddress.Trim()}:{ProxyPort}";
|
||||
return IsLoopbackProxyServer()
|
||||
? $"پراکسی آماده است: {endpoint} — توجه: این پراکسی محلی است؛ برنامههایی که خودشان مستقیم از همین پراکسی استفاده کنند خارج از لیست برنامههای TunnelX هم پروکسی میشوند."
|
||||
: $"پراکسی آماده است: {endpoint}";
|
||||
? LocalizationService.Instance.Format("پراکسی آماده است: {0} — توجه: این پراکسی محلی است؛ برنامههایی که خودشان مستقیم از همین پراکسی استفاده کنند خارج از لیست برنامههای TunnelX هم پروکسی میشوند.", endpoint)
|
||||
: LocalizationService.Instance.Format("پراکسی آماده است: {0}", endpoint);
|
||||
}
|
||||
|
||||
private bool IsLoopbackProxyServer()
|
||||
@@ -1291,8 +1423,11 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
var currentName = _selectedProfile.Name?.Trim() ?? "";
|
||||
var canRename = string.IsNullOrWhiteSpace(currentName) ||
|
||||
currentName.StartsWith("پروفایل ", StringComparison.OrdinalIgnoreCase) ||
|
||||
currentName.StartsWith("Profile ", StringComparison.OrdinalIgnoreCase) ||
|
||||
currentName == "پروفایل جدید" ||
|
||||
currentName == "پیشفرض";
|
||||
currentName == "پیشفرض" ||
|
||||
currentName == "New Profile" ||
|
||||
currentName == "Default";
|
||||
if (!canRename)
|
||||
return;
|
||||
|
||||
@@ -1302,6 +1437,8 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
|
||||
_selectedProfile.Name = remark;
|
||||
OnPropertyChanged(nameof(SelectedProfileName));
|
||||
OnPropertyChanged(nameof(ConnectedProfileName));
|
||||
OnPropertyChanged(nameof(SelectedProfileSummaryText));
|
||||
}
|
||||
|
||||
private static string ExtractConfigRemark(string config)
|
||||
@@ -1347,10 +1484,93 @@ public partial class MainViewModel : INotifyPropertyChanged
|
||||
private void LoadAppSettings()
|
||||
{
|
||||
_appSettings = _profileService.LoadAppSettings();
|
||||
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));
|
||||
OnPropertyChanged(nameof(AppTextAlignment));
|
||||
OnPropertyChanged(nameof(AppStartHorizontalAlignment));
|
||||
OnPropertyChanged(nameof(AppEndHorizontalAlignment));
|
||||
}
|
||||
|
||||
private void ToggleLanguage()
|
||||
{
|
||||
LocalizationService.Instance.ToggleLanguage();
|
||||
_appSettings.Language = LocalizationService.Instance.EffectiveLanguage;
|
||||
_profileService.SaveAppSettings(_appSettings);
|
||||
}
|
||||
|
||||
private void OnLanguageChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(LanguageToggleText));
|
||||
OnPropertyChanged(nameof(AppIsRightToLeft));
|
||||
OnPropertyChanged(nameof(AppTitleText));
|
||||
OnPropertyChanged(nameof(AppTitleAccentText));
|
||||
OnPropertyChanged(nameof(AppTitleFlowDirection));
|
||||
OnPropertyChanged(nameof(AppTitleAccentAlignment));
|
||||
OnPropertyChanged(nameof(AppFlowDirection));
|
||||
OnPropertyChanged(nameof(AppTextAlignment));
|
||||
OnPropertyChanged(nameof(AppStartHorizontalAlignment));
|
||||
OnPropertyChanged(nameof(AppEndHorizontalAlignment));
|
||||
OnPropertyChanged(nameof(GameModeStatusText));
|
||||
OnPropertyChanged(nameof(OpenVpnPrerequisiteText));
|
||||
OnPropertyChanged(nameof(DonatePayPalText));
|
||||
OnPropertyChanged(nameof(CryptoDonationText));
|
||||
OnPropertyChanged(nameof(AppCreatorText));
|
||||
OnPropertyChanged(nameof(AdPlaceholderTitleText));
|
||||
OnPropertyChanged(nameof(AdRequestButtonText));
|
||||
OnPropertyChanged(nameof(AdAudienceText));
|
||||
OnPropertyChanged(nameof(LogClearButtonText));
|
||||
OnPropertyChanged(nameof(LogCopyErrorButtonText));
|
||||
OnPropertyChanged(nameof(LogCopyAllButtonText));
|
||||
OnPropertyChanged(nameof(LogClearToolTipText));
|
||||
OnPropertyChanged(nameof(LogCopyErrorToolTipText));
|
||||
OnPropertyChanged(nameof(LogCopyAllToolTipText));
|
||||
OnPropertyChanged(nameof(UpdateButtonText));
|
||||
OnPropertyChanged(nameof(UpdateStatusText));
|
||||
OnPropertyChanged(nameof(GitHubInstallCountText));
|
||||
OnPropertyChanged(nameof(ConnectButtonText));
|
||||
OnPropertyChanged(nameof(ConnectButtonToolTip));
|
||||
OnPropertyChanged(nameof(StatusText));
|
||||
OnPropertyChanged(nameof(MixedProxyPortStatusText));
|
||||
OnPropertyChanged(nameof(AppLicenseDisplayText));
|
||||
OnPropertyChanged(nameof(ConfigCoreHint));
|
||||
OnPropertyChanged(nameof(ConfigValidationText));
|
||||
OnPropertyChanged(nameof(SaveStatusText));
|
||||
OnPropertyChanged(nameof(ConnectionIpText));
|
||||
OnPropertyChanged(nameof(ConnectionIpLabel));
|
||||
OnPropertyChanged(nameof(FullRouteStatusText));
|
||||
OnPropertyChanged(nameof(RouteModeTitle));
|
||||
OnPropertyChanged(nameof(RouteModeDescription));
|
||||
OnPropertyChanged(nameof(HeaderCoreText));
|
||||
OnPropertyChanged(nameof(HeaderRouteText));
|
||||
OnPropertyChanged(nameof(HeaderLeakText));
|
||||
OnPropertyChanged(nameof(HealthLeakText));
|
||||
OnPropertyChanged(nameof(HealthDnsText));
|
||||
OnPropertyChanged(nameof(HealthIpv6Text));
|
||||
OnPropertyChanged(nameof(HealthRoutesText));
|
||||
OnPropertyChanged(nameof(ConnectedBadgeText));
|
||||
OnPropertyChanged(nameof(ConnectedProfileName));
|
||||
OnPropertyChanged(nameof(SelectedProfileSummaryText));
|
||||
OnPropertyChanged(nameof(PingButtonText));
|
||||
OnPropertyChanged(nameof(ConnectedServerPingButtonText));
|
||||
OnPropertyChanged(nameof(ServerPingButtonText));
|
||||
OnPropertyChanged(nameof(PingResult));
|
||||
OnPropertyChanged(nameof(ServerPingResult));
|
||||
OnPropertyChanged(nameof(ProfileCountText));
|
||||
OnPropertyChanged(nameof(ActiveProfileTypeText));
|
||||
OnPropertyChanged(nameof(ActiveProfileEndpointText));
|
||||
OnPropertyChanged(nameof(ProfileSaveHintText));
|
||||
RaiseProfileCardChanged();
|
||||
OnPropertyChanged(nameof(Profiles));
|
||||
}
|
||||
|
||||
private static void UpdateStartupRegistry(bool enable)
|
||||
|
||||
@@ -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);
|
||||
@@ -33,8 +35,8 @@ public partial class MainViewModel
|
||||
|
||||
public string SelectedProfileName => _selectedProfile?.Name ?? "";
|
||||
public string ProfileCountText => Profiles.Count == 1
|
||||
? "۱ پروفایل ذخیرهشده"
|
||||
: $"{Profiles.Count} پروفایل ذخیرهشده";
|
||||
? LocalizationService.Instance.T("۱ پروفایل ذخیرهشده")
|
||||
: LocalizationService.Instance.Format("{0} پروفایل ذخیرهشده", Profiles.Count);
|
||||
|
||||
public string ActiveProfileTypeText => CurrentTunnelType switch
|
||||
{
|
||||
@@ -42,26 +44,26 @@ public partial class MainViewModel
|
||||
TunnelType.V2Ray => TunnelProviderFactory.RequiresXray(SelectedV2RayConfig) ? "V2Ray / Xray" : "V2Ray / sing-box",
|
||||
TunnelType.OpenVpn => "OpenVPN",
|
||||
TunnelType.SocksProxy => ProxyProtocol == ProxyProtocol.Http ? "HTTP Proxy" : "SOCKS5 Proxy",
|
||||
_ => "نوع اتصال نامشخص"
|
||||
_ => LocalizationService.Instance.T("نوع اتصال نامشخص")
|
||||
};
|
||||
|
||||
public string ActiveProfileEndpointText => CurrentTunnelType switch
|
||||
{
|
||||
TunnelType.L2tpIpsec => string.IsNullOrWhiteSpace(ServerAddress) ? "آدرس سرور هنوز وارد نشده" : ServerAddress.Trim(),
|
||||
TunnelType.L2tpIpsec => string.IsNullOrWhiteSpace(ServerAddress) ? LocalizationService.Instance.T("آدرس سرور هنوز وارد نشده") : ServerAddress.Trim(),
|
||||
TunnelType.V2Ray => TryExtractProxyEndpoint(SelectedV2RayConfig.Trim(), out var server, out var port, out _)
|
||||
? $"{server}:{port}"
|
||||
: "کانفیگ V2Ray/Xray آماده نمایش نیست",
|
||||
: LocalizationService.Instance.T("کانفیگ V2Ray/Xray آماده نمایش نیست"),
|
||||
TunnelType.OpenVpn => string.IsNullOrWhiteSpace(SelectedOpenVpnConfigPath)
|
||||
? "فایل OpenVPN انتخاب نشده"
|
||||
? LocalizationService.Instance.T("فایل OpenVPN انتخاب نشده")
|
||||
: Path.GetFileName(SelectedOpenVpnConfigPath),
|
||||
TunnelType.SocksProxy => string.IsNullOrWhiteSpace(ProxyServerAddress)
|
||||
? "آدرس پراکسی هنوز وارد نشده"
|
||||
? LocalizationService.Instance.T("آدرس پراکسی هنوز وارد نشده")
|
||||
: $"{ProxyServerAddress.Trim()}:{ProxyPort}",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
public string ProfileSaveHintText => string.IsNullOrWhiteSpace(SaveStatusText)
|
||||
? "تغییرات این پروفایل بهصورت خودکار ذخیره میشود"
|
||||
? LocalizationService.Instance.T("تغییرات این پروفایل بهصورت خودکار ذخیره میشود")
|
||||
: SaveStatusText;
|
||||
|
||||
/// <summary>
|
||||
@@ -77,7 +79,7 @@ public partial class MainViewModel
|
||||
Profiles.Clear();
|
||||
|
||||
if (profiles.Count == 0)
|
||||
profiles.Add(new ConnectionProfile { Name = "پیشفرض" });
|
||||
profiles.Add(new ConnectionProfile { Name = LocalizationService.Instance.T("پیشفرض") });
|
||||
|
||||
foreach (var p in profiles.OrderByDescending(p => p.LastUsedAt))
|
||||
Profiles.Add(p);
|
||||
@@ -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 = $"پروفایل {Profiles.Count + 1}",
|
||||
Name = "",
|
||||
MixedProxyPort = MixedProxyPort,
|
||||
AutoTuneMtu = AutoTuneMtu,
|
||||
EnableDnsOptimization = IsDnsOptimizationEnabled,
|
||||
@@ -276,7 +280,7 @@ public partial class MainViewModel
|
||||
SaveCurrentProfileState();
|
||||
|
||||
var clone = CloneProfile(source);
|
||||
clone.Name = $"{source.Name} (کپی)";
|
||||
clone.Name = LocalizationService.Instance.Format("{0} (کپی)", source.Name);
|
||||
|
||||
if (ProfileEditorDialog.Show(clone, "کپی پروفایل", System.Windows.Application.Current.MainWindow) != true)
|
||||
return;
|
||||
@@ -318,7 +322,7 @@ public partial class MainViewModel
|
||||
{
|
||||
var toRemove = parameter as ConnectionProfile ?? _selectedProfile;
|
||||
if (toRemove == null || Profiles.Count <= 1) return;
|
||||
if (!Helpers.DialogService.Confirm($"پروفایل «{toRemove.Name}» حذف شود؟", "حذف پروفایل"))
|
||||
if (!Helpers.DialogService.Confirm(LocalizationService.Instance.Format("پروفایل «{0}» حذف شود؟", toRemove.Name), "حذف پروفایل"))
|
||||
return;
|
||||
|
||||
var idx = Profiles.IndexOf(toRemove);
|
||||
@@ -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));
|
||||
@@ -415,7 +421,7 @@ public partial class MainViewModel
|
||||
|
||||
var entry = new ConnectionHistoryEntry
|
||||
{
|
||||
ProfileName = _selectedProfile?.Name ?? "پیشفرض",
|
||||
ProfileName = _selectedProfile?.Name ?? LocalizationService.Instance.T("پیشفرض"),
|
||||
ServerAddress = CurrentTunnelType == TunnelType.SocksProxy
|
||||
? $"{ProxyServerAddress}:{ProxyPort}"
|
||||
: ServerAddress,
|
||||
|
||||
@@ -2,6 +2,63 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="CompactToggleSwitch" TargetType="CheckBox">
|
||||
<Setter Property="Width" Value="32"/>
|
||||
<Setter Property="Height" Value="18"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<Grid Width="32" Height="18">
|
||||
<Border x:Name="track"
|
||||
Width="32"
|
||||
Height="18"
|
||||
CornerRadius="9"
|
||||
Background="#555"
|
||||
Cursor="Hand"/>
|
||||
<Border x:Name="thumb"
|
||||
Width="14"
|
||||
Height="14"
|
||||
CornerRadius="7"
|
||||
Background="White"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="2,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="track" Property="Background" Value="{StaticResource SuccessBrush}"/>
|
||||
<Setter TargetName="thumb" Property="HorizontalAlignment" Value="Right"/>
|
||||
<Setter TargetName="thumb" Property="Margin" Value="0,0,2,0"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CompactIconButton" TargetType="Button" BasedOn="{StaticResource SecondaryButton}">
|
||||
<Setter Property="MinWidth" Value="24"/>
|
||||
<Setter Property="Width" Value="24"/>
|
||||
<Setter Property="Height" Value="22"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="AppRowCard" TargetType="Border">
|
||||
<Setter Property="Background" Value="#0EFFFFFF"/>
|
||||
<Setter Property="BorderBrush" Value="#10FFFFFF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="10"/>
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Setter Property="Margin" Value="0,0,0,6"/>
|
||||
<Setter Property="MinHeight" Value="50"/>
|
||||
<Setter Property="FlowDirection" Value="LeftToRight"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Margin="12,8,12,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -50,56 +107,86 @@
|
||||
SelectedItem="{Binding SelectedApp}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
FlowDirection="LeftToRight"
|
||||
FlowDirection="{Binding AppFlowDirection}"
|
||||
Cursor="Arrow"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#12FFFFFF" BorderBrush="#12FFFFFF" BorderThickness="1" CornerRadius="9"
|
||||
Padding="9,7" Margin="0,2" Cursor="Arrow">
|
||||
<Border Style="{StaticResource AppRowCard}" Cursor="Arrow">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- App Icon -->
|
||||
<Image Grid.Column="0" Source="{Binding Icon}"
|
||||
Width="26" Height="26" Margin="0,0,9,0"/>
|
||||
Width="28" Height="28"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- App Name & Exe -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
FontSize="12" TextTrimming="CharacterEllipsis"/>
|
||||
FontSize="10.8"
|
||||
FontWeight="Normal"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"
|
||||
TextOptions.TextHintingMode="Fixed"
|
||||
TextWrapping="Wrap"
|
||||
FlowDirection="LeftToRight"
|
||||
TextAlignment="Left"/>
|
||||
<TextBlock Text="{Binding ExecutableName}"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FontSize="9" FlowDirection="LeftToRight"/>
|
||||
FontSize="8.5"
|
||||
Margin="0,2,0,0"
|
||||
FlowDirection="LeftToRight"
|
||||
TextAlignment="Left"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Traffic -->
|
||||
<TextBlock Grid.Column="2" Text="{Binding TrafficDisplay}"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FontSize="10" VerticalAlignment="Center"
|
||||
Margin="8,0" FlowDirection="LeftToRight"/>
|
||||
<!-- Compact actions -->
|
||||
<Grid Grid.Column="4"
|
||||
Width="76"
|
||||
VerticalAlignment="Center"
|
||||
FlowDirection="LeftToRight">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<CheckBox Style="{StaticResource CompactToggleSwitch}"
|
||||
IsChecked="{Binding IsEnabled, Mode=OneWay}"
|
||||
Command="{Binding DataContext.ToggleAppCommand,
|
||||
RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,4,0"/>
|
||||
|
||||
<!-- Toggle -->
|
||||
<CheckBox Grid.Column="3" Style="{StaticResource ToggleSwitch}"
|
||||
IsChecked="{Binding IsEnabled, Mode=OneWay}"
|
||||
Command="{Binding DataContext.ToggleAppCommand,
|
||||
RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
VerticalAlignment="Center" Margin="4,0"/>
|
||||
|
||||
<!-- Remove -->
|
||||
<Button Grid.Column="4" Style="{StaticResource DangerButton}"
|
||||
Content="حذف" FontSize="10" Padding="7,3"
|
||||
Command="{Binding DataContext.RemoveAppCommand,
|
||||
RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Button Style="{StaticResource CompactIconButton}"
|
||||
Content="×"
|
||||
ToolTip="حذف"
|
||||
Foreground="#FF8A80"
|
||||
Command="{Binding DataContext.RemoveAppCommand,
|
||||
RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</StackPanel>
|
||||
<Border Grid.Row="1"
|
||||
Background="#15121212"
|
||||
CornerRadius="6"
|
||||
Padding="4,1"
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Right">
|
||||
<TextBlock Text="{Binding TrafficDisplay}"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FontSize="8.5"
|
||||
FlowDirection="LeftToRight"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
@@ -161,46 +248,58 @@
|
||||
<ListView ItemsSource="{Binding FilteredAvailableApps}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
FlowDirection="LeftToRight"
|
||||
FlowDirection="{Binding AppFlowDirection}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border x:Name="itemBorder"
|
||||
Background="#0CFFFFFF" BorderBrush="#10FFFFFF" BorderThickness="1" CornerRadius="9"
|
||||
Padding="8,6" Margin="0,2" Cursor="Hand"
|
||||
Style="{StaticResource AppRowCard}"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="OnAvailableAppClick">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image Grid.Column="0" Source="{Binding Icon}"
|
||||
Width="24" Height="24" Margin="0,0,8,0"/>
|
||||
Width="26" Height="26"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
FontSize="12" TextTrimming="CharacterEllipsis"/>
|
||||
FontSize="10.8"
|
||||
FontWeight="Normal"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"
|
||||
TextOptions.TextHintingMode="Fixed"
|
||||
TextWrapping="Wrap"
|
||||
FlowDirection="LeftToRight"
|
||||
TextAlignment="Left"/>
|
||||
<TextBlock Text="{Binding ExecutableName}"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FontSize="9" FlowDirection="LeftToRight"/>
|
||||
FontSize="8.5"
|
||||
Margin="0,2,0,0"
|
||||
FlowDirection="LeftToRight"
|
||||
TextAlignment="Left"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Column="2" x:Name="addBadge"
|
||||
Background="#1AE8803A" CornerRadius="7"
|
||||
Padding="6,2" Margin="6,0,0,0">
|
||||
<TextBlock Text="افزودن" FontSize="9"
|
||||
Foreground="{StaticResource PrimaryBrush}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Button Grid.Column="4"
|
||||
x:Name="addBadge"
|
||||
Style="{StaticResource CompactIconButton}"
|
||||
Content="+"
|
||||
ToolTip="افزودن"
|
||||
Foreground="{StaticResource PrimaryBrush}"
|
||||
Click="OnAvailableAppAddClick"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<DataTemplate.Triggers>
|
||||
<Trigger SourceName="itemBorder" Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="itemBorder" Property="Background" Value="#18FFFFFF"/>
|
||||
<Setter TargetName="addBadge" Property="Background" Value="#40E07820"/>
|
||||
</Trigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -28,4 +28,14 @@ public partial class AppsTabView : System.Windows.Controls.UserControl
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAvailableAppAddClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element && element.DataContext is AppItemViewModel app
|
||||
&& DataContext is MainViewModel vm)
|
||||
{
|
||||
vm.AddAppToTunnel(app);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
<conv:EnumToVisibilityConverter x:Key="EnumToVis"/>
|
||||
</UserControl.Resources>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<ScrollViewer FlowDirection="{Binding AppFlowDirection}"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel HorizontalAlignment="Stretch" Margin="16,8,16,4">
|
||||
|
||||
<!-- ══ DISCONNECTED STATE: Show connection form ══ -->
|
||||
@@ -153,7 +155,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<StackPanel Orientation="Horizontal" FlowDirection="RightToLeft">
|
||||
<StackPanel Orientation="Horizontal" FlowDirection="{Binding DataContext.AppFlowDirection, RelativeSource={RelativeSource AncestorType=UserControl}}">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
@@ -187,7 +189,7 @@
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
FlowDirection="RightToLeft"
|
||||
FlowDirection="{Binding DataContext.AppFlowDirection, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource PrimaryButton}"
|
||||
Content="فعال"
|
||||
@@ -237,7 +239,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" FlowDirection="RightToLeft">
|
||||
<StackPanel Orientation="Horizontal" FlowDirection="{Binding AppFlowDirection}">
|
||||
<Ellipse Width="9" Height="9"
|
||||
Fill="{Binding StatusColor, Converter={StaticResource StringToColor}}"
|
||||
VerticalAlignment="Center"/>
|
||||
@@ -250,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"
|
||||
@@ -264,13 +265,13 @@
|
||||
MinWidth="160"
|
||||
Height="44"
|
||||
Padding="18,8"
|
||||
ToolTip="شروع اتصال با پروفایل انتخابشده">
|
||||
ToolTip="{Binding ConnectButtonToolTip}">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#10B981"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#8CFFFFFF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
@@ -290,18 +291,13 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#0EA371"/>
|
||||
<Setter Property="BorderBrush" Value="#CCFFFFFF"/>
|
||||
<Setter Property="Background" Value="#059669"/>
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding ConnectionState}" Value="{x:Static models:ConnectionState.Connecting}">
|
||||
<Setter Property="Background" Value="#D97706"/>
|
||||
<Setter Property="BorderBrush" Value="#80FFE3BF"/>
|
||||
<Setter Property="ToolTip" Value="لغو تلاش اتصال"/>
|
||||
<Setter Property="Background" Value="#F97316"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding ConnectionState}" Value="{x:Static models:ConnectionState.Error}">
|
||||
<Setter Property="Background" Value="#B91C1C"/>
|
||||
<Setter Property="BorderBrush" Value="#80FFD2D2"/>
|
||||
<Setter Property="ToolTip" Value="اتصال مجدد"/>
|
||||
<Setter Property="Background" Value="#DC2626"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
@@ -497,7 +493,7 @@
|
||||
Foreground="{Binding OpenVpnPrerequisiteColor, Converter={StaticResource StringToColor}}"
|
||||
FontSize="10"
|
||||
TextWrapping="Wrap"
|
||||
FlowDirection="RightToLeft"/>
|
||||
FlowDirection="{Binding AppFlowDirection}"/>
|
||||
<Button Grid.Column="2"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Content="دانلود OpenVPN"
|
||||
@@ -674,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>
|
||||
|
||||
@@ -849,9 +848,9 @@
|
||||
ToolTip="قطع اتصال فعلی">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#B42318"/>
|
||||
<Setter Property="BorderBrush" Value="#F97066"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Background" Value="#DC2626"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
@@ -864,12 +863,12 @@
|
||||
CornerRadius="16"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="#F04438"
|
||||
BlurRadius="16"
|
||||
Opacity="0.24"
|
||||
<DropShadowEffect Color="#B91C1C"
|
||||
BlurRadius="18"
|
||||
Opacity="0.28"
|
||||
ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
<Grid FlowDirection="RightToLeft">
|
||||
<Grid FlowDirection="{Binding AppFlowDirection}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
@@ -899,11 +898,10 @@
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="DisconnectButtonBorder" Property="Background" Value="#D92D20"/>
|
||||
<Setter TargetName="DisconnectButtonBorder" Property="BorderBrush" Value="#FDA29B"/>
|
||||
<Setter TargetName="DisconnectButtonBorder" Property="Background" Value="#B91C1C"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="DisconnectButtonBorder" Property="Background" Value="#912018"/>
|
||||
<Setter TargetName="DisconnectButtonBorder" Property="Background" Value="#991B1B"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -913,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 -->
|
||||
|
||||
@@ -921,14 +961,14 @@
|
||||
CornerRadius="10" Padding="10,8" Margin="0,8,0,0">
|
||||
<StackPanel>
|
||||
<TextBlock TextWrapping="Wrap" FontSize="10" LineHeight="17"
|
||||
FlowDirection="RightToLeft"
|
||||
FlowDirection="{Binding AppFlowDirection}"
|
||||
Foreground="{StaticResource TextPrimaryBrush}">
|
||||
<Run Text="💡 در حالت انتخابی، فقط برنامههای فعال در تب "/>
|
||||
<Run Text="«برنامهها»" FontWeight="SemiBold"/>
|
||||
<Run Text=" از تونل عبور میکنند؛ بقیه مستقیم میمانند."/>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap" FontSize="10" LineHeight="17"
|
||||
FlowDirection="RightToLeft"
|
||||
FlowDirection="{Binding AppFlowDirection}"
|
||||
Foreground="{StaticResource TextSecondaryBrush}">
|
||||
<Run Text="📌 برای تلگرام، واتساپ و برنامههای Store، "/>
|
||||
<Run Text="Microsoft Edge WebView2"
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<Window x:Class="AppTunnel.Views.DonationDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:services="clr-namespace:AppTunnel.Services"
|
||||
Title="حمایت از پروژه"
|
||||
Width="430"
|
||||
Height="520"
|
||||
MinWidth="390"
|
||||
MinHeight="440"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False"
|
||||
FlowDirection="{Binding Source={x:Static services:LocalizationService.Instance}, Path=FlowDirection}">
|
||||
|
||||
<Border Background="{StaticResource BackgroundBrush}"
|
||||
CornerRadius="16"
|
||||
BorderBrush="#34E8803A"
|
||||
BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="28" Opacity="0.65" ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="حمایت از پروژه"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="اگر TunnelX برایتان مفید بوده، میتوانید با PayPal یا کریپتو از توسعه آن حمایت کنید."
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
LineHeight="18"
|
||||
Margin="0,4,12,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Content="✕"
|
||||
Style="{StaticResource WindowControlButton}"
|
||||
Width="32"
|
||||
Height="28"
|
||||
FontSize="13"
|
||||
FlowDirection="LeftToRight"
|
||||
Click="OnCloseClick"/>
|
||||
</Grid>
|
||||
|
||||
<Button Grid.Row="1"
|
||||
Content="پرداخت با PayPal"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="14,9"
|
||||
Margin="0,0,0,12"
|
||||
Click="OnPayPalClick"/>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
Background="#0EFFFFFF"
|
||||
BorderBrush="#16FFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="10">
|
||||
<DockPanel LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Top"
|
||||
Text="پرداخت با کریپتو"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsControl x:Name="CryptoItemsControl">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#10121212"
|
||||
BorderBrush="#12FFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="9,8"
|
||||
Margin="0,0,0,7">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="{Binding Label}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="{Binding Address}"
|
||||
FontSize="10"
|
||||
FontFamily="Consolas"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
FlowDirection="LeftToRight"
|
||||
TextAlignment="Left"
|
||||
Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
Content="کپی"
|
||||
Tag="{Binding}"
|
||||
Style="{StaticResource SecondaryButton}"
|
||||
Padding="9,4"
|
||||
FontSize="10"
|
||||
MinWidth="44"
|
||||
VerticalAlignment="Center"
|
||||
Click="OnCopyCryptoClick"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="CopyStatusText"
|
||||
Grid.Row="3"
|
||||
Text=""
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,10,0,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using AppTunnel.Services;
|
||||
|
||||
namespace AppTunnel.Views;
|
||||
|
||||
public partial class DonationDialog : Window
|
||||
{
|
||||
public DonationDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
CryptoItemsControl.ItemsSource = CreateDonationAddresses();
|
||||
Loaded += (_, _) => LocalizationService.Instance.ApplyTo(this);
|
||||
}
|
||||
|
||||
private static ObservableCollection<CryptoDonationAddress> CreateDonationAddresses()
|
||||
{
|
||||
var t = LocalizationService.Instance.T;
|
||||
return
|
||||
[
|
||||
new(t("ترون / USDT روی TRC20"), "TNWV867fQDT6zpLunHgbeMjrN6ic63LQSu"),
|
||||
new(t("بیتکوین"), "bc1qgx3g47c458fu6smnpqpu0l05hha82rq2xjet4y"),
|
||||
new(t("اتریوم / USDT روی ERC20"), "0x72d94Bb250E8802441a0ED05686Ee925BC99Fef5"),
|
||||
new("TON", "UQD65oL2Vu2OJDSrwQ0wLLSw3g668SREMJ3VPW9k8b6Sy-Yf"),
|
||||
new("BNB Smart Chain", "0xE2a5b01cE2b3713D435Bc16d92eAdd88A82159f0"),
|
||||
new("Dogecoin", "DSZRNY65yF679uvjAh6sUAt6YiEEQHwKGb")
|
||||
];
|
||||
}
|
||||
|
||||
private void OnPayPalClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(AppInfo.PayPalDonateUrl)
|
||||
{
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CopyStatusText.Text = LocalizationService.Instance.Format("باز کردن لینک ناموفق بود: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCopyCryptoClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not System.Windows.Controls.Button { Tag: CryptoDonationAddress item })
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
System.Windows.Clipboard.SetText(item.Address);
|
||||
CopyStatusText.Text = LocalizationService.Instance.Format("{0} کپی شد", item.Label);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CopyStatusText.Text = LocalizationService.Instance.Format("کپی ناموفق بود: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CryptoDonationAddress(string Label, string Address);
|
||||
@@ -3,13 +3,53 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:services="clr-namespace:AppTunnel.Services">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Padding="0,14,0,12">
|
||||
<StackPanel Margin="16,0">
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="HelpCard" TargetType="Border" BasedOn="{StaticResource CardPanel}">
|
||||
<Setter Property="Padding" Value="14,12"/>
|
||||
<Setter Property="Margin" Value="0,0,0,10"/>
|
||||
</Style>
|
||||
|
||||
<!-- Overview -->
|
||||
<Border Style="{StaticResource CardPanel}">
|
||||
<Style x:Key="HelpTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,6"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HelpBody" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="LineHeight" Value="18"/>
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HelpChip" TargetType="Border">
|
||||
<Setter Property="Background" Value="#0EFFFFFF"/>
|
||||
<Setter Property="BorderBrush" Value="#14FFFFFF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="9"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HelpExpander" TargetType="Expander">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="#14FFFFFF"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0,4,0,0"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<ScrollViewer FlowDirection="{Binding AppFlowDirection}"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Padding="0,12,0,10">
|
||||
<StackPanel Margin="14,0">
|
||||
|
||||
<!-- Header -->
|
||||
<Border Style="{StaticResource HelpCard}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -17,23 +57,22 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="TunnelX"
|
||||
FontSize="22"
|
||||
<TextBlock Text="راهنمای TunnelX"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="Split tunneling برای برنامهها، مقصدها و حالت کل سیستم"
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
<TextBlock Text="شروع سریع، پروفایلها، بخشهای اپ و عیبیابی در یک صفحه ساده."
|
||||
Style="{StaticResource HelpBody}"
|
||||
Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Background="#1AE8803A"
|
||||
Background="#14E8803A"
|
||||
CornerRadius="8"
|
||||
Padding="10,5"
|
||||
VerticalAlignment="Center">
|
||||
Padding="9,4"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock Text="{x:Static services:AppInfo.VersionText}"
|
||||
FontSize="12"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FlowDirection="LeftToRight"/>
|
||||
@@ -41,106 +80,162 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Project + Support -->
|
||||
<Border Style="{StaticResource CardPanel}">
|
||||
<!-- Primary Actions -->
|
||||
<Border Style="{StaticResource HelpCard}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition Width="10"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="پروژه آزاد و حمایت"/>
|
||||
<TextBlock Text="{Binding AppCreatorText}"
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Margin="0,0,0,4"/>
|
||||
<TextBlock Text="{Binding AppGitHubUrl}"
|
||||
FontSize="10"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FlowDirection="LeftToRight"
|
||||
TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding AppLicenseText, StringFormat=لایسنس: {0}}"
|
||||
FontSize="10"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FlowDirection="LeftToRight"
|
||||
Margin="0,5,0,0"/>
|
||||
<TextBlock Text="{Binding DonatePayPalText}"
|
||||
FontSize="10"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FlowDirection="LeftToRight"
|
||||
Margin="0,5,0,0"/>
|
||||
<TextBlock Text="{Binding CryptoDonationText}"
|
||||
FontSize="10"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FontFamily="Consolas"
|
||||
FlowDirection="LeftToRight"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Vertical"
|
||||
VerticalAlignment="Center"
|
||||
MinWidth="150">
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
Content="باز کردن گیتهاب"
|
||||
Command="{Binding OpenGitHubCommand}"
|
||||
Padding="14,8"/>
|
||||
<Button Style="{StaticResource PrimaryButton}"
|
||||
Content="حمایت با پیپل"
|
||||
Command="{Binding OpenDonateCommand}"
|
||||
Padding="16,8"
|
||||
Margin="0,8,0,0"/>
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
Content="کپی اطلاعات حمایت"
|
||||
Command="{Binding CopyDonationInfoCommand}"
|
||||
Padding="14,8"
|
||||
Margin="0,8,0,0"/>
|
||||
</StackPanel>
|
||||
</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="پروژه و بروزرسانی" Style="{StaticResource HelpTitle}"/>
|
||||
<TextBlock Text="{Binding UpdateStatusText}"
|
||||
FontSize="11"
|
||||
Style="{StaticResource HelpBody}"/>
|
||||
<TextBlock Text="{Binding AppLicenseDisplayText}"
|
||||
FontSize="10"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
LineHeight="18"/>
|
||||
FlowDirection="LeftToRight"
|
||||
Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Vertical"
|
||||
VerticalAlignment="Center"
|
||||
MinWidth="150">
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
MinWidth="145"
|
||||
VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource PrimaryButton}"
|
||||
Content="{Binding UpdateButtonText}"
|
||||
Command="{Binding CheckForUpdatesCommand}"
|
||||
Padding="14,8"/>
|
||||
<Button Style="{StaticResource PrimaryButton}"
|
||||
Content="باز کردن صفحه انتشار"
|
||||
FontSize="11"
|
||||
Padding="12,7"/>
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
Content="صفحه انتشار"
|
||||
Command="{Binding OpenLatestReleaseCommand}"
|
||||
Padding="14,8"
|
||||
Margin="0,8,0,0"/>
|
||||
FontSize="11"
|
||||
Padding="12,7"
|
||||
Margin="0,6,0,0"/>
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
Content="GitHub"
|
||||
Command="{Binding OpenGitHubCommand}"
|
||||
FontSize="11"
|
||||
Padding="12,7"
|
||||
Margin="0,6,0,0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Quick Path -->
|
||||
<Border Style="{StaticResource CardPanel}">
|
||||
<!-- Quick Start -->
|
||||
<Border Style="{StaticResource HelpCard}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="شروع سریع"/>
|
||||
<TextBlock Text="شروع سریع" Style="{StaticResource HelpTitle}"/>
|
||||
<UniformGrid Columns="2" Rows="2">
|
||||
<Border Style="{StaticResource HelpChip}" Margin="0,0,5,5">
|
||||
<StackPanel>
|
||||
<TextBlock Text="۱. پروفایل" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="کانفیگ را بسازید و نوع اتصال را انتخاب کنید."
|
||||
Style="{StaticResource HelpBody}" FontSize="10" Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Style="{StaticResource HelpChip}" Margin="5,0,0,5">
|
||||
<StackPanel>
|
||||
<TextBlock Text="۲. برنامهها" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="برنامههای داخل تونل را انتخاب یا دستی اضافه کنید."
|
||||
Style="{StaticResource HelpBody}" FontSize="10" Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Style="{StaticResource HelpChip}" Margin="0,5,5,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="۳. قوانین" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="مقصدهای مستقیم یا اجباری را مشخص کنید."
|
||||
Style="{StaticResource HelpBody}" FontSize="10" Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Style="{StaticResource HelpChip}" Margin="5,5,0,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="۴. اتصال" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="وصل شوید و سلامت، IP و مصرف را بررسی کنید."
|
||||
Style="{StaticResource HelpBody}" FontSize="10" Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Profile Types -->
|
||||
<Border Style="{StaticResource HelpCard}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="نوع پروفایل" Style="{StaticResource HelpTitle}"/>
|
||||
<TextBlock Text="فقط فیلدهای مربوط به نوع انتخابشده را پر کنید. هر پروفایل برنامهها و قوانین مسیر خودش را نگه میدارد."
|
||||
Style="{StaticResource HelpBody}"/>
|
||||
|
||||
<Expander Header="L2TP/IPsec" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="برای اتصال VPN ویندوز. آدرس سرور، نام کاربری، رمز عبور و Pre-Shared Key لازم است. اگر وصل نشد، PSK، فایروال و تنظیمات VPN ویندوز را بررسی کنید."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="V2Ray / Xray" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="لینک یا JSON کانفیگ را وارد کنید یا از کلیپبورد پیست کنید. TunnelX معمولاً sing-box را اجرا میکند و برای قابلیتهایی مثل xhttp از Xray-core استفاده میکند."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="SOCKS5 / HTTP Proxy" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="برای پراکسی خارجی آماده. نوع پراکسی، آدرس، پورت و در صورت نیاز نام کاربری/رمز را وارد کنید. این با پراکسی داخلی 127.0.0.1 فرق دارد."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="OpenVPN" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="فایل ovpn را انتخاب کنید. OpenVPN Community باید جداگانه نصب باشد؛ OpenVPN Connect برای Split Tunneling مناسب نیست. اگر سرور رمز میخواهد، نام کاربری و رمز را در TunnelX وارد کنید."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- App Areas -->
|
||||
<Border Style="{StaticResource HelpCard}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="بخشهای اپ" Style="{StaticResource HelpTitle}"/>
|
||||
|
||||
<Expander Header="اتصال VPN" Style="{StaticResource HelpExpander}" IsExpanded="True">
|
||||
<TextBlock Text="پروفایل فعال، تست سرور، اتصال/قطع اتصال، IP خروجی، پینگ، مصرف و راهنمای پراکسی دستی اینجاست."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="برنامهها" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="از لیست برنامههای پیدا شده انتخاب کنید یا فایل exe را دستی اضافه کنید. برای Store/MSIX/WebView2 برنامه را باز نگه دارید و بروزرسانی لیست را بزنید."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="قوانین مسیر" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="«مستقیم بماند» یعنی مقصد از تونل عبور نکند. «از تونل عبور کند» یعنی مقصد حتی بدون انتخاب برنامه وارد تونل شود. دامنهها زیردامنهها را هم پوشش میدهند."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="تنظیمات" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="پورت پراکسی محلی، MTU خودکار، DNS Optimization، Game Mode، اجرای خودکار ویندوز و اتصال خودکار اینجاست."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="ترافیک و تاریخچه" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="مدت اتصال، IP، مصرف تونل، مصرف خارج تونل، سلامت Split Tunnel، مصرف برنامهها و تاریخچه اتصالها را نشان میدهد."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="جزئیات و لاگها" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="از دکمه جزئیات، لاگها را با فیلتر خطا، هشدار، DNS یا Route ببینید. قبل از ارسال عمومی لاگ، رمزها، کلیدها، UUID و endpoint خصوصی را حذف کنید."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Essentials -->
|
||||
<Border Style="{StaticResource HelpCard}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="نکات مهم" Style="{StaticResource HelpTitle}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -153,217 +248,89 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Grid.Column="0" Background="#11FFFFFF" CornerRadius="8" Padding="12,10">
|
||||
<Border Grid.Row="0" Grid.Column="0" Style="{StaticResource HelpChip}">
|
||||
<TextBlock Text="حالت عادی: فقط برنامههای انتخابشده و مقصدهای لزومی وارد تونل میشوند."
|
||||
Style="{StaticResource HelpBody}" FontSize="10"/>
|
||||
</Border>
|
||||
<Border Grid.Row="0" Grid.Column="2" Style="{StaticResource HelpChip}">
|
||||
<TextBlock Text="Full Route: کل سیستم وارد تونل میشود؛ استثناها میتوانند مستقیم بمانند."
|
||||
Style="{StaticResource HelpBody}" FontSize="10"/>
|
||||
</Border>
|
||||
<Border Grid.Row="2" Grid.Column="0" Style="{StaticResource HelpChip}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="۱. پروفایل" FontSize="12" FontWeight="SemiBold"
|
||||
<TextBlock Text="پراکسی داخلی"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="نوع اتصال را انتخاب کنید: L2TP، V2Ray/Xray، SOCKS5/HTTP Proxy یا اوپنویپیان. برای اسپلیت اوپنویپیان، نسخه Community لازم است؛ فایل .ovpn را انتخاب کنید و نام کاربری/رمز را در TunnelX وارد کنید."
|
||||
FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,5,0,0"/>
|
||||
<TextBlock Text="برای ابزارهایی که آدرس محلی میخواهند:"
|
||||
Style="{StaticResource HelpBody}" FontSize="10" Margin="0,3,0,0"/>
|
||||
<TextBlock Text="127.0.0.1:1080"
|
||||
FontSize="11"
|
||||
FontFamily="Consolas"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FlowDirection="LeftToRight"
|
||||
Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="0" Grid.Column="2" Background="#11FFFFFF" CornerRadius="8" Padding="12,10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="۲. تست سرور" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="قبل از اتصال، تست سرور را بزنید. برای TLS، handshake واقعی اندازهگیری میشود."
|
||||
FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2" Grid.Column="0" Background="#11FFFFFF" CornerRadius="8" Padding="12,10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="۳. برنامهها" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="برنامههایی که باید از تونل عبور کنند را در تب برنامهها فعال کنید."
|
||||
FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2" Grid.Column="2" Background="#11FFFFFF" CornerRadius="8" Padding="12,10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="۴. اتصال" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="بعد از اتصال، کارت سلامت ترافیک را برای Leak، DNS، IPv6 و Route بررسی کنید."
|
||||
FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
<Border Grid.Row="2" Grid.Column="2" Style="{StaticResource HelpChip}">
|
||||
<TextBlock Text="سلامت: Leak باید صفر باشد. DNS، IPv6 و Route را بعد از اتصال بررسی کنید."
|
||||
Style="{StaticResource HelpBody}" FontSize="10"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Connection Types -->
|
||||
<Border Style="{StaticResource CardPanel}">
|
||||
<!-- Troubleshooting -->
|
||||
<Border Style="{StaticResource HelpCard}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="راهنمای نوع اتصال"/>
|
||||
<TextBlock Text="L2TP/IPsec"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="آدرس سرور، نام کاربری، رمز عبور و Pre-Shared Key را وارد کنید؛ TunnelX اتصال ویندوز را ایجاد و مسیرها را مدیریت میکند."
|
||||
FontSize="11"
|
||||
LineHeight="18"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,4,0,10"/>
|
||||
<TextBlock Text="عیبیابی سریع" Style="{StaticResource HelpTitle}"/>
|
||||
|
||||
<TextBlock Text="V2Ray / Xray"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="لینک یا کانفیگ V2Ray/Xray را در فیلد کانفیگ وارد کنید. TunnelX با sing-box/Xray تونل را بالا میآورد و برنامههای انتخابی را از آن عبور میدهد."
|
||||
FontSize="11"
|
||||
LineHeight="18"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,4,0,10"/>
|
||||
<Expander Header="اتصال برقرار نمیشود" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="برنامه را با Administrator اجرا کنید. فایروال، آنتیویروس، آدرس سرور، پورت، رمزها، PSK، نصب OpenVPN Community و اعتبار کانفیگ را بررسی کنید."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<TextBlock Text="SOCKS5 / HTTP Proxy"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="اگر پراکسی خارجی آماده دارید، آدرس سرور، پورت و در صورت نیاز نام کاربری/رمز را در پروفایل پراکسی وارد کنید. این حالت با SOCKS5 داخلی روی 127.0.0.1 تفاوت دارد."
|
||||
FontSize="11"
|
||||
LineHeight="18"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,4,0,10"/>
|
||||
<Expander Header="ترافیک برنامه از تونل عبور نمیکند" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="برنامه را در تب برنامهها فعال کنید. اگر چندپردازشی است، برنامه را باز نگه دارید و لیست برنامهها را دوباره بارگذاری کنید."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<TextBlock Text="OpenVPN"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||
<TextBlock Text="TunnelX فایل اوپنویپیان را همراه خود توزیع نمیکند. برای اسپلیتتانلینگ باید نسخه Community نصب باشد، چون TunnelX باید فایل openvpn.exe را با کانفیگ سازگار با اسپلیت اجرا کند. نسخه Connect معمولاً مسیر و DNS را خودش تغییر میدهد و برای جدا کردن ترافیک برنامهها مناسب نیست. در تب اتصال فایل .ovpn را انتخاب کنید و اگر سرور نیاز دارد، نام کاربری و رمز را در TunnelX وارد کنید."
|
||||
FontSize="11"
|
||||
LineHeight="18"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
Margin="0,4,0,0"/>
|
||||
<Expander Header="پراکسی کار نمیکند" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="برای پروفایل پراکسی، آدرس، پورت، نوع و اطلاعات ورود را بررسی کنید. برای ابزارهای محلی، آدرس 127.0.0.1 و پورت تنظیمات را وارد کنید."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="DNS، IPv6 یا Leak غیرعادی است" Style="{StaticResource HelpExpander}">
|
||||
<TextBlock Text="یک بار قطع و وصل کنید تا مسیرها و قوانین DNS دوباره ساخته شوند. اگر مشکل ماند، لاگهای DNS و Route را بررسی کنید."
|
||||
Style="{StaticResource HelpBody}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Modes -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Style="{StaticResource CardPanel}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="حالت انتخابی"/>
|
||||
<TextBlock Text="فقط برنامههای انتخابشده و مقصدهای لزومی از تونل عبور میکنند. این حالت برای مصرف کمتر و کنترل دقیقتر پیشنهاد میشود."
|
||||
FontSize="11"
|
||||
LineHeight="19"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="2" Style="{StaticResource CardPanel}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="عبور کل سیستم"/>
|
||||
<TextBlock Text="وقتی Full Route روشن باشد، کل ترافیک ویندوز از تونل عبور میکند. برای تست یا زمانی که همه برنامهها باید VPN داشته باشند مناسب است."
|
||||
FontSize="11"
|
||||
LineHeight="19"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Rules + Apps -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Style="{StaticResource CardPanel}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="برنامهها"/>
|
||||
<TextBlock TextWrapping="Wrap" FontSize="11" LineHeight="19"
|
||||
Foreground="{StaticResource TextSecondaryBrush}">
|
||||
برنامههای Store/MSIX و WebView2 ممکن است چند پردازش داشته باشند. اگر اپی دیده نشد، آن را باز کنید و دوباره بارگذاری برنامهها را بزنید.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="2" Style="{StaticResource CardPanel}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="قوانین مسیر"/>
|
||||
<TextBlock TextWrapping="Wrap" FontSize="11" LineHeight="19"
|
||||
Foreground="{StaticResource TextSecondaryBrush}">
|
||||
استثناها مقصد را مستقیم نگه میدارند. لزومیها مقصد را حتی بدون انتخاب برنامه از تونل عبور میدهند. دامنه، IP و URL قابل استفاده است.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Local Proxy + Diagnostics -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Style="{StaticResource CardPanel}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="SOCKS5 داخلی"/>
|
||||
<TextBlock TextWrapping="Wrap" FontSize="11" LineHeight="19"
|
||||
Foreground="{StaticResource TextSecondaryBrush}">
|
||||
پروکسی داخلی روی 127.0.0.1 اجرا میشود و برای ابزارهایی است که آدرس پراکسی محلی میخواهند. پورت را قبل از اتصال انتخاب کنید؛ پورتهای سیستم، رزرو و پورتهای رایج توسعه محدود شدهاند.
|
||||
</TextBlock>
|
||||
<Border Background="#11FFFFFF" CornerRadius="6" Padding="8" Margin="0,8,0,0">
|
||||
<TextBlock Text="127.0.0.1:1080"
|
||||
FontSize="11"
|
||||
FontFamily="Consolas"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FlowDirection="LeftToRight"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="2" Style="{StaticResource CardPanel}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="کارت سلامت"/>
|
||||
<TextBlock TextWrapping="Wrap" FontSize="11" LineHeight="19"
|
||||
Foreground="{StaticResource TextSecondaryBrush}">
|
||||
Leak باید صفر باشد. DNS نشان میدهد درخواستهای DNS هدایت شدهاند. IPv6 blocked یعنی مسیرهای IPv6 ناخواسته بسته شدهاند. Route تعداد مسیرهای فعال و خطاها را نشان میدهد.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Troubleshooting -->
|
||||
<Border Style="{StaticResource CardPanel}">
|
||||
<!-- Support -->
|
||||
<Border Style="{StaticResource HelpCard}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="عیبیابی سریع"/>
|
||||
|
||||
<Expander Header="اتصال برقرار نمیشود" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,4,0,0">
|
||||
<TextBlock Text="دسترسی Administrator، فایروال، پورت SOCKS5، اطلاعات پراکسی و صحت کانفیگ را بررسی کنید. در کانفیگهای xhttp، هسته باید Xray باشد."
|
||||
TextWrapping="Wrap" FontSize="11" LineHeight="19"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="ترافیک برنامه از تونل عبور نمیکند" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,8,0,0">
|
||||
<TextBlock Text="برنامه را در تب برنامهها فعال کنید. برای اپهای Store، برنامه را باز نگه دارید و لیست برنامهها را دوباره بارگذاری کنید."
|
||||
TextWrapping="Wrap" FontSize="11" LineHeight="19"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
|
||||
<Expander Header="سایت داخلی باید مستقیم باز شود" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,8,0,0">
|
||||
<TextBlock Text="دامنه یا IP را به استثناها اضافه کنید. برای عبور اجباری یک مقصد از VPN، آن را به لزومیها اضافه کنید."
|
||||
TextWrapping="Wrap" FontSize="11" LineHeight="19"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,0"/>
|
||||
</Expander>
|
||||
<TextBlock Text="حمایت و تماس" Style="{StaticResource HelpTitle}"/>
|
||||
<TextBlock Text="{Binding AppCreatorText}" Style="{StaticResource HelpBody}"/>
|
||||
<TextBlock Text="{Binding AppGitHubUrl}"
|
||||
FontSize="10"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
FlowDirection="LeftToRight"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,5,0,0"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
Content="حمایت با پیپل"
|
||||
Command="{Binding OpenDonateCommand}"
|
||||
FontSize="11"
|
||||
Padding="12,7"/>
|
||||
<Button Style="{StaticResource SecondaryButton}"
|
||||
Content="کپی اطلاعات حمایت"
|
||||
Command="{Binding CopyDonationInfoCommand}"
|
||||
FontSize="11"
|
||||
Padding="12,7"
|
||||
Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
FlowDirection="RightToLeft"
|
||||
FlowDirection="{Binding Source={x:Static services:LocalizationService.Instance}, Path=FlowDirection}"
|
||||
MouseLeftButtonDown="OnTitleBarMouseDown">
|
||||
|
||||
<Border Background="{StaticResource BackgroundBrush}"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Windows;
|
||||
using AppTunnel.Services;
|
||||
|
||||
namespace AppTunnel.Views;
|
||||
|
||||
@@ -7,6 +8,7 @@ public partial class HelpWindow : Window
|
||||
public HelpWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => LocalizationService.Instance.ApplyTo(this);
|
||||
}
|
||||
|
||||
private void OnCloseClick(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="0,6,0,0">
|
||||
<ScrollViewer FlowDirection="{Binding AppFlowDirection}"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Padding="0,6,0,0">
|
||||
<StackPanel HorizontalAlignment="Stretch" Margin="10,0">
|
||||
|
||||
<!-- History Header -->
|
||||
@@ -35,10 +37,9 @@
|
||||
Background="#15E07820" BorderBrush="#22E8803A" BorderThickness="1" CornerRadius="9"
|
||||
Padding="12,8" Margin="0,10,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
|
||||
FlowDirection="LeftToRight">
|
||||
FlowDirection="{Binding AppFlowDirection}">
|
||||
<TextBlock Text="مجموع مصرف تونل: " FontSize="11"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
FlowDirection="RightToLeft"/>
|
||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||
<TextBlock Text="{Binding TotalHistoryData}" FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource WarningBrush}"/>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<Window x:Class="AppTunnel.Views.ModernDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:services="clr-namespace:AppTunnel.Services"
|
||||
Title="TunnelX"
|
||||
Width="340" Height="200"
|
||||
MinWidth="300" MinHeight="170"
|
||||
Width="360"
|
||||
MinWidth="330" MinHeight="220"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
FlowDirection="RightToLeft"
|
||||
FlowDirection="{Binding Source={x:Static services:LocalizationService.Instance}, Path=FlowDirection}"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize">
|
||||
|
||||
@@ -21,11 +23,11 @@
|
||||
<DropShadowEffect Color="Black" BlurRadius="30" Opacity="0.7" ShadowDepth="0"/>
|
||||
</Border.Effect>
|
||||
|
||||
<Grid Margin="18">
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
@@ -40,7 +42,9 @@
|
||||
FontSize="15" FontWeight="Bold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8"/>
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
<!-- Message -->
|
||||
<TextBlock x:Name="MessageText" Grid.Row="2"
|
||||
@@ -50,14 +54,15 @@
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,0,14"
|
||||
LineHeight="20"/>
|
||||
VerticalAlignment="Top"
|
||||
MaxWidth="310"
|
||||
Margin="0,0,0,18"
|
||||
LineHeight="22"/>
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
FlowDirection="RightToLeft">
|
||||
FlowDirection="{Binding Source={x:Static services:LocalizationService.Instance}, Path=FlowDirection}">
|
||||
<Button x:Name="PrimaryButton" Content="بله"
|
||||
Style="{StaticResource PrimaryButton}"
|
||||
MinWidth="80" Margin="0,0,6,0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Windows;
|
||||
using AppTunnel.Services;
|
||||
using Application = System.Windows.Application;
|
||||
|
||||
namespace AppTunnel.Views;
|
||||
@@ -14,6 +15,7 @@ public partial class ModernDialog : Window
|
||||
public ModernDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => LocalizationService.Instance.ApplyTo(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -24,11 +26,11 @@ public partial class ModernDialog : Window
|
||||
var dialog = new ModernDialog
|
||||
{
|
||||
Owner = owner ?? Application.Current.MainWindow,
|
||||
TitleText = { Text = title },
|
||||
MessageText = { Text = message },
|
||||
TitleText = { Text = LocalizationService.Instance.T(title) },
|
||||
MessageText = { Text = LocalizationService.Instance.T(message) },
|
||||
IconText = { Text = "⚠️" },
|
||||
PrimaryButton = { Content = "بله" },
|
||||
SecondaryButton = { Content = "خیر" }
|
||||
PrimaryButton = { Content = LocalizationService.Instance.T("بله") },
|
||||
SecondaryButton = { Content = LocalizationService.Instance.T("خیر") }
|
||||
};
|
||||
|
||||
dialog.ShowDialog();
|
||||
@@ -43,10 +45,10 @@ public partial class ModernDialog : Window
|
||||
var dialog = new ModernDialog
|
||||
{
|
||||
Owner = owner ?? Application.Current.MainWindow,
|
||||
TitleText = { Text = title },
|
||||
MessageText = { Text = message },
|
||||
TitleText = { Text = LocalizationService.Instance.T(title) },
|
||||
MessageText = { Text = LocalizationService.Instance.T(message) },
|
||||
IconText = { Text = "ℹ️" },
|
||||
PrimaryButton = { Content = "متوجه شدم", Visibility = Visibility.Visible },
|
||||
PrimaryButton = { Content = LocalizationService.Instance.T("متوجه شدم"), Visibility = Visibility.Visible },
|
||||
SecondaryButton = { Visibility = Visibility.Collapsed }
|
||||
};
|
||||
|
||||
@@ -61,10 +63,10 @@ public partial class ModernDialog : Window
|
||||
var dialog = new ModernDialog
|
||||
{
|
||||
Owner = owner ?? Application.Current.MainWindow,
|
||||
TitleText = { Text = title },
|
||||
MessageText = { Text = message },
|
||||
TitleText = { Text = LocalizationService.Instance.T(title) },
|
||||
MessageText = { Text = LocalizationService.Instance.T(message) },
|
||||
IconText = { Text = "✅" },
|
||||
PrimaryButton = { Content = "عالی", Visibility = Visibility.Visible },
|
||||
PrimaryButton = { Content = LocalizationService.Instance.T("عالی"), Visibility = Visibility.Visible },
|
||||
SecondaryButton = { Visibility = Visibility.Collapsed }
|
||||
};
|
||||
|
||||
@@ -79,10 +81,10 @@ public partial class ModernDialog : Window
|
||||
var dialog = new ModernDialog
|
||||
{
|
||||
Owner = owner ?? Application.Current.MainWindow,
|
||||
TitleText = { Text = title },
|
||||
MessageText = { Text = message },
|
||||
TitleText = { Text = LocalizationService.Instance.T(title) },
|
||||
MessageText = { Text = LocalizationService.Instance.T(message) },
|
||||
IconText = { Text = "❌" },
|
||||
PrimaryButton = { Content = "متوجه شدم", Visibility = Visibility.Visible },
|
||||
PrimaryButton = { Content = LocalizationService.Instance.T("متوجه شدم"), Visibility = Visibility.Visible },
|
||||
SecondaryButton = { Visibility = Visibility.Collapsed }
|
||||
};
|
||||
|
||||
@@ -97,10 +99,10 @@ public partial class ModernDialog : Window
|
||||
var dialog = new ModernDialog
|
||||
{
|
||||
Owner = owner ?? Application.Current.MainWindow,
|
||||
TitleText = { Text = title },
|
||||
MessageText = { Text = message },
|
||||
TitleText = { Text = LocalizationService.Instance.T(title) },
|
||||
MessageText = { Text = LocalizationService.Instance.T(message) },
|
||||
IconText = { Text = "⚠️" },
|
||||
PrimaryButton = { Content = "متوجه شدم", Visibility = Visibility.Visible },
|
||||
PrimaryButton = { Content = LocalizationService.Instance.T("متوجه شدم"), Visibility = Visibility.Visible },
|
||||
SecondaryButton = { Visibility = Visibility.Collapsed }
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="clr-namespace:AppTunnel.Converters"
|
||||
xmlns:models="clr-namespace:AppTunnel.Models"
|
||||
xmlns:services="clr-namespace:AppTunnel.Services"
|
||||
Title="Profile Editor"
|
||||
Width="500"
|
||||
Height="560"
|
||||
@@ -12,7 +13,7 @@
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
FlowDirection="RightToLeft"
|
||||
FlowDirection="{Binding Source={x:Static services:LocalizationService.Instance}, Path=FlowDirection}"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize">
|
||||
|
||||
@@ -72,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}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using AppTunnel.Models;
|
||||
using AppTunnel.Services;
|
||||
|
||||
namespace AppTunnel.Views;
|
||||
|
||||
@@ -12,8 +13,9 @@ public partial class ProfileEditorDialog : Window
|
||||
{
|
||||
_profile = profile;
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => LocalizationService.Instance.ApplyTo(this);
|
||||
DataContext = profile;
|
||||
DialogTitleText.Text = title;
|
||||
DialogTitleText.Text = LocalizationService.Instance.T(title);
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
@@ -38,8 +40,8 @@ public partial class ProfileEditorDialog : Window
|
||||
{
|
||||
var dialog = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "انتخاب فایل OpenVPN",
|
||||
Filter = "OpenVPN config (*.ovpn)|*.ovpn|All files (*.*)|*.*",
|
||||
Title = LocalizationService.Instance.T("انتخاب فایل OpenVPN"),
|
||||
Filter = LocalizationService.Instance.T("OpenVPN config (*.ovpn)|*.ovpn|All files (*.*)|*.*"),
|
||||
CheckFileExists = true,
|
||||
Multiselect = false
|
||||
};
|
||||
@@ -53,7 +55,7 @@ public partial class ProfileEditorDialog : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ValidationText.Text = $"خواندن فایل OpenVPN ناموفق بود: {ex.Message}";
|
||||
ValidationText.Text = LocalizationService.Instance.Format("خواندن فایل OpenVPN ناموفق بود: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +68,7 @@ public partial class ProfileEditorDialog : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ValidationText.Text = $"خواندن کلیپبورد ناموفق بود: {ex.Message}";
|
||||
ValidationText.Text = LocalizationService.Instance.Format("خواندن کلیپبورد ناموفق بود: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,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;
|
||||
@@ -96,26 +105,26 @@ public partial class ProfileEditorDialog : Window
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_profile.Name))
|
||||
{
|
||||
message = "نام پروفایل را وارد کنید";
|
||||
message = LocalizationService.Instance.T("نام پروفایل را وارد کنید");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (_profile.TunnelType)
|
||||
{
|
||||
case TunnelType.L2tpIpsec when string.IsNullOrWhiteSpace(_profile.ServerAddress):
|
||||
message = "آدرس سرور L2TP را وارد کنید";
|
||||
message = LocalizationService.Instance.T("آدرس سرور L2TP را وارد کنید");
|
||||
return false;
|
||||
case TunnelType.V2Ray when string.IsNullOrWhiteSpace(_profile.V2RayConfig):
|
||||
message = "کانفیگ V2Ray/Xray را وارد کنید";
|
||||
message = LocalizationService.Instance.T("کانفیگ V2Ray/Xray را وارد کنید");
|
||||
return false;
|
||||
case TunnelType.OpenVpn when string.IsNullOrWhiteSpace(_profile.OpenVpnConfig):
|
||||
message = "فایل OpenVPN (.ovpn) را انتخاب کنید";
|
||||
message = LocalizationService.Instance.T("فایل OpenVPN (.ovpn) را انتخاب کنید");
|
||||
return false;
|
||||
case TunnelType.SocksProxy when string.IsNullOrWhiteSpace(_profile.ProxyServerAddress):
|
||||
message = "آدرس سرور پراکسی را وارد کنید";
|
||||
message = LocalizationService.Instance.T("آدرس سرور پراکسی را وارد کنید");
|
||||
return false;
|
||||
case TunnelType.SocksProxy when _profile.ProxyPort <= 0 || _profile.ProxyPort > 65535:
|
||||
message = "پورت پراکسی باید بین 1 تا 65535 باشد";
|
||||
message = LocalizationService.Instance.T("پورت پراکسی باید بین 1 تا 65535 باشد");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -123,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;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<ScrollViewer FlowDirection="{Binding AppFlowDirection}"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel HorizontalAlignment="Stretch" Margin="16,8,16,4">
|
||||
|
||||
<!-- Local Proxy Settings -->
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 16 KiB |
@@ -2,10 +2,51 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.2.32 - 2026-05-18
|
||||
|
||||
### English
|
||||
|
||||
- Fixed VLESS REALITY configs in the sing-box path by enabling uTLS with the configured fingerprint, preventing startup failures that reported `uTLS is required by reality client`.
|
||||
- Replaced icon-only log-panel actions with compact localized text buttons for clearing logs, copying the latest error, and copying all logs.
|
||||
|
||||
### فارسی
|
||||
|
||||
- مشکل کانفیگهای VLESS REALITY در مسیر sing-box اصلاح شد؛ uTLS با fingerprint کانفیگ فعال میشود تا خطای `uTLS is required by reality client` هنگام شروع اتصال رخ ندهد.
|
||||
- دکمههای فقطآیکونی پنل لاگ با دکمههای متنی و فشرده جایگزین شدند تا پاک کردن لاگ، کپی آخرین خطا و کپی همه لاگها در فارسی و انگلیسی واضح باشد.
|
||||
|
||||
## 1.2.31 - 2026-05-18
|
||||
|
||||
- Update release packaging and connection UX
|
||||
- Update README contact and localization notes
|
||||
|
||||
## 1.2.30 - 2026-05-18
|
||||
|
||||
### English
|
||||
|
||||
- Added bilingual Persian/English UI switching with automatic system-language detection, persisted language selection, RTL/LTR layout handling, and localized dialogs, tray text, runtime status messages, help content, and profile/app/routing views.
|
||||
- Refined the desktop UI with adaptive window sizing, disabled maximize/system-menu fullscreen paths, smoother log-panel animation, polished dialogs, refreshed TunnelX branding, updated app icon, compact app-list rows, footer donation modal, and header update notification.
|
||||
- Improved V2Ray/Xray reliability by dynamically reserving free local ports for Xray SOCKS and sing-box mixed proxy inbounds instead of relying on fixed `2080/2081` ports.
|
||||
- Improved connection UX with more reliable exit-IP detection retries, consistent text-field behavior, better log window direction/localization, and localized README screenshots for Persian and English documentation.
|
||||
|
||||
### فارسی
|
||||
|
||||
- تغییر زبان فارسی/انگلیسی به برنامه اضافه شد؛ شامل تشخیص زبان سیستم، ذخیره زبان انتخابشده، رعایت RTL/LTR، و ترجمه دیالوگها، متن tray، وضعیتهای runtime، راهنما، پروفایلها، برنامهها و قوانین مسیر.
|
||||
- رابط کاربری دسکتاپ بهبود یافت؛ شامل اندازهگیری تطبیقی پنجره، جلوگیری از maximize/fullscreen و منوی Alt+Space، انیمیشن نرم پنل لاگ، دیالوگهای تمیزتر، برندینگ و آیکون جدید TunnelX، ردیفهای فشردهتر برنامهها، مودال حمایت مالی و دکمه اعلان بروزرسانی در هدر.
|
||||
- پایداری V2Ray/Xray بهتر شد؛ پورتهای داخلی Xray SOCKS و sing-box mixed proxy دیگر ثابت نیستند و بهصورت آزاد از سیستم رزرو میشوند تا خطای اشغال بودن `2080/2081` تکرار نشود.
|
||||
- تجربه اتصال بهتر شد؛ شامل تلاش دوباره برای دریافت IP خروجی، رفتار یکدست فیلدهای متنی، جهت و ترجمه بهتر پنجره لاگ، و استفاده از اسکرینشاتهای فارسی/انگلیسی در READMEهای مربوطه.
|
||||
|
||||
## 1.2.29 - 2026-05-17
|
||||
|
||||
### English
|
||||
|
||||
- Expanded the GitHub README with Russian and Simplified Chinese summaries for international users.
|
||||
- Expanded the in-app Persian Help tab with fuller guidance for profiles, connection types, routing rules, logs, updates, and troubleshooting.
|
||||
|
||||
### فارسی
|
||||
|
||||
- توضیحهای روسی و چینی سادهشده به README گیتهاب اضافه شد تا کاربران بینالمللی سریعتر با کاربرد برنامه آشنا شوند.
|
||||
- تب راهنمای فارسی داخل برنامه با توضیح کاملتر درباره پروفایلها، نوعهای اتصال، قوانین مسیر، لاگها، بروزرسانی و عیبیابی گسترش پیدا کرد.
|
||||
|
||||
## 1.2.28 - 2026-05-17
|
||||
|
||||
### English
|
||||
@@ -94,3 +135,6 @@
|
||||
- Improved leak logging and traffic accounting in recent internal builds.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
# TunnelX
|
||||
|
||||
فارسی | <span dir="ltr">[English](README.md)</span>
|
||||
فارسی | <span dir="ltr">[English](README.md)</span> | <span dir="ltr">[Русский](README.md#русский)</span> | <span dir="ltr">[简体中文](README.md#简体中文)</span>
|
||||
|
||||
<span dir="ltr">TunnelX</span> یک نرمافزار آزاد و رایگان برای ویندوز است که توسط **<span dir="ltr">MaxFan</span>** ساخته شده و برای مدیریت تونل، ویپیان و <span dir="ltr">Split Tunneling</span> استفاده میشود. این برنامه میتواند ترافیک برنامههای انتخابشده، مقصدهای مشخص، یا کل سیستم را از تونل عبور دهد و همزمان مسیر عادی شبکه را برای مقصدهای محلی یا مستثنیشده حفظ کند.
|
||||
<span dir="ltr">TunnelX</span> یک نرمافزار آزاد و رایگان برای ویندوز است که توسط **<span dir="ltr">MaxFan</span>** ساخته شده و برای مدیریت تونل، ویپیان و <span dir="ltr">Split Tunneling</span> استفاده میشود. این برنامه میتواند ترافیک برنامههای انتخابشده، مقصدهای مشخص، یا کل سیستم را از تونل عبور دهد و همزمان مسیر عادی شبکه را برای مقصدهای محلی یا مستثنیشده حفظ کند. رابط برنامه دوزبانه است، زبان سیستم را تشخیص میدهد و چینش راستبهچپ/چپبهراست را برای فارسی و انگلیسی رعایت میکند.
|
||||
|
||||
## کاربرد برنامه
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
- پروکسی <span dir="ltr">SOCKS5</span> محلی روی <span dir="ltr">`127.0.0.1`</span> برای ابزارهایی که تنظیم پروکسی داخلی دارند
|
||||
- تغییر مسیر <span dir="ltr">DNS</span>، مسدودسازی <span dir="ltr">IPv6</span>، محافظ نشت، عیبیابی <span dir="ltr">route</span> و تاریخچه مصرف تونل
|
||||
- مدیریت چند پروفایل، کپی/ویرایش کانفیگها، تست سرور، تشخیص <span dir="ltr">IP</span> خروجی و اعلان بروزرسانی
|
||||
- رابط کاربری فارسیمحور برای ویندوز
|
||||
- رابط کاربری فارسی و انگلیسی با تشخیص خودکار زبان، دکمه تغییر زبان و رعایت کامل راستبهچپ/چپبهراست
|
||||
- انتخاب پورت داخلی آزاد برای <span dir="ltr">V2Ray/Xray</span> تا خطاهای اشغال بودن پورتهای <span dir="ltr">`2080/2081`</span> کمتر شود
|
||||
|
||||
## شروع سریع
|
||||
|
||||
@@ -81,11 +82,11 @@
|
||||
|
||||
| داشبورد اتصال | تنظیم پروفایل و سرور |
|
||||
| --- | --- |
|
||||
| <img src="docs/ScreenShots/Screenshot%202026-05-12%20115349.png" alt="داشبورد اتصال TunnelX"> | <img src="docs/ScreenShots/Screenshot%202026-05-12%20115544.png" alt="تنظیم پروفایل و سرور TunnelX"> |
|
||||
| <img src="docs/ScreenShots/fa/connection-dashboard.png" alt="داشبورد اتصال TunnelX"> | <img src="docs/ScreenShots/fa/apps.png" alt="انتخاب برنامهها برای تونل در TunnelX"> |
|
||||
|
||||
| انتخاب برنامهها برای تونل | تنظیمات تونل |
|
||||
| قوانین مسیر | راهنما و عیبیابی |
|
||||
| --- | --- |
|
||||
| <img src="docs/ScreenShots/Screenshot%202026-05-12%20115646.png" alt="انتخاب برنامهها برای تونل در TunnelX"> | <img src="docs/ScreenShots/Screenshot%202026-05-12%20115718.png" alt="تنظیمات تونل در TunnelX"> |
|
||||
| <img src="docs/ScreenShots/fa/routing-rules.png" alt="قوانین مسیر در TunnelX"> | <img src="docs/ScreenShots/fa/help.png" alt="راهنما و عیبیابی در TunnelX"> |
|
||||
|
||||
## دانلود
|
||||
|
||||
@@ -131,9 +132,13 @@ dotnet publish AppTunnel\AppTunnel.csproj -c Release -r win-x64 --self-contained
|
||||
|
||||
<span dir="ltr">TunnelX</span> آزاد و رایگان است. حمایت مالی کاملا اختیاری است و فقط به نگهداری و توسعه پروژه کمک میکند.
|
||||
|
||||
برای ارتباط مستقیم، درخواست پشتیبانی، سفارشیسازی خصوصی یا سفارش توسعه، از طریق تلگرام پیام بدهید: <span dir="ltr">[t.me/maxifaan](https://t.me/maxifaan)</span>
|
||||
|
||||
خدمات پولی میتواند به صورت جداگانه برای پشتیبانی خصوصی، راهاندازی، بیلد اختصاصی، سفارشیسازی برای شرکتها، یا توسعه برنامهای مشابه ارائه شود. این خدمات پولی حقوقی را که مجوز <span dir="ltr">GPL</span> به کاربران میدهد محدود نمیکند.
|
||||
|
||||
گزینههای حمایت و راههای تماس از طریق <span dir="ltr">GitHub Sponsors/Funding</span> یا فایل <span dir="ltr">`docs/DONATE.md`</span> در دسترس هستند.
|
||||
پذیرش تبلیغات ثابت داخل <span dir="ltr">TunnelX</span> امکانپذیر است. تبلیغات بهصورت مستقیم با نگهدارنده هماهنگ میشود، از طریق شبکههای تبلیغاتی یا سایتهای واسط نمایش داده نمیشود و با هدف ساده، ثابت و امن ماندن تجربه کاربر انجام میشود.
|
||||
|
||||
گزینههای حمایت مالی از طریق <span dir="ltr">GitHub Sponsors/Funding</span> یا فایل <span dir="ltr">`docs/DONATE.md`</span> در دسترس هستند.
|
||||
|
||||
## نکته ایمنی و سلب مسئولیت
|
||||
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
# TunnelX
|
||||
|
||||
[فارسی](README.fa.md) | English
|
||||
[فارسی](README.fa.md) | English | [Русский](#русский) | [简体中文](#简体中文)
|
||||
|
||||
TunnelX is a free and open-source Windows split-tunneling client built by **MaxFan**. It routes selected apps, selected destinations, or the whole system through supported tunnel cores while keeping local and excluded destinations on the normal network path.
|
||||
TunnelX is a free and open-source Windows split-tunneling client built by **MaxFan**. It routes selected apps, selected destinations, or the whole system through supported tunnel cores while keeping local and excluded destinations on the normal network path. The app supports Persian and English UI modes with automatic system-language detection and correct RTL/LTR layout handling.
|
||||
|
||||
## Русский
|
||||
|
||||
TunnelX — бесплатный клиент split tunneling для Windows от **MaxFan**. Он позволяет направлять через VPN, V2Ray/Xray, OpenVPN или SOCKS5/HTTP Proxy только выбранные приложения, выбранные домены/IP или весь системный трафик. Интерфейс поддерживает персидский и английский языки с автоматическим выбором языка системы и корректным RTL/LTR отображением.
|
||||
|
||||
Основные возможности: профили L2TP/IPsec, V2Ray/Xray, SOCKS5/HTTP Proxy и OpenVPN Community; выбор приложений для туннеля; правила include/exclude для доменов и IP; режим Full Route; локальный прокси `127.0.0.1`; отображение публичного выходного IP; история трафика; защита от DNS/IPv6/leak проблем.
|
||||
|
||||
Для обычного использования скачайте последний standalone-файл из [GitHub Releases](https://github.com/MaxiFan/TunnelX/releases/latest), запустите TunnelX от имени Administrator, создайте профиль подключения, выберите приложения для туннеля и подключитесь. Отдельная установка .NET Runtime для standalone-сборки не требуется.
|
||||
|
||||
Связаться с автором можно в Telegram: [t.me/maxifaan](https://t.me/maxifaan).
|
||||
|
||||
## 简体中文
|
||||
|
||||
TunnelX 是由 **MaxFan** 构建的免费 Windows 分流隧道客户端。它可以只让选定的应用、指定的域名/IP,或整个系统流量通过 VPN、V2Ray/Xray、OpenVPN 或 SOCKS5/HTTP Proxy,同时让本地或排除的目标继续走普通网络。应用支持波斯语和英语界面,可自动检测系统语言并正确处理 RTL/LTR 布局。
|
||||
|
||||
主要功能包括:L2TP/IPsec、V2Ray/Xray、SOCKS5/HTTP Proxy 和 OpenVPN Community 配置文件;按应用分流;域名/IP include 与 exclude 规则;Full Route 全局模式;本地 `127.0.0.1` 代理;公网出口 IP 显示;流量历史;DNS、IPv6 与泄漏防护诊断。
|
||||
|
||||
普通用户可以从 [GitHub Releases](https://github.com/MaxiFan/TunnelX/releases/latest) 下载最新 standalone 版本,以 Administrator 权限运行 TunnelX,创建连接配置,选择需要进入隧道的应用,然后连接。standalone 版本不需要单独安装 .NET Runtime。
|
||||
|
||||
可通过 Telegram 联系作者:[t.me/maxifaan](https://t.me/maxifaan)。
|
||||
|
||||
## Features
|
||||
|
||||
@@ -15,7 +35,8 @@ TunnelX is a free and open-source Windows split-tunneling client built by **MaxF
|
||||
- Local SOCKS5 proxy for tools that need `127.0.0.1`
|
||||
- DNS redirect, IPv6 blocking, leak guard, route diagnostics, and traffic history
|
||||
- Multiple profiles, duplicate/edit flows, server tests, public exit IP detection, and release update checks
|
||||
- Persian-first Windows desktop UI
|
||||
- Persian and English desktop UI with automatic language detection, manual language switching, and correct RTL/LTR layout behavior
|
||||
- Dynamic local port selection for V2Ray/Xray internals to reduce `2080/2081` binding conflicts
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -75,11 +96,11 @@ Logs can contain process names, hostnames, IP addresses, ports, and connection s
|
||||
|
||||
| Connection dashboard | Profile and server setup |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
| App split tunneling | Tunnel settings |
|
||||
| Routing rules | Help and troubleshooting |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Download
|
||||
|
||||
@@ -121,9 +142,13 @@ TunnelX is licensed under **GPL-3.0-or-later**. Commercial use is allowed under
|
||||
|
||||
TunnelX is free and open-source. Donations are optional and help keep the project maintained.
|
||||
|
||||
For direct contact, support requests, private customization, or paid development work, message MaxFan on Telegram: [t.me/maxifaan](https://t.me/maxifaan).
|
||||
|
||||
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.
|
||||
|
||||
Use the GitHub funding button or see `docs/DONATE.md` for donation and contact options.
|
||||
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
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 88 KiB |