reality configured
This commit is contained in:
1 parent
efcfe6e706
commit
2086f9ded2
11 files changed
+205
-209
No files matched your search
@@ -78,7 +78,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
||||
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
final adWatched = await ad.showAd('main_ad');
|
||||
final adWatched = await ad.showAd('main');
|
||||
if (!adWatched && AdManager.adsEnabled) {
|
||||
setState(() => _busy = false);
|
||||
return;
|
||||
|
||||
@@ -14,7 +14,7 @@ class StatusBar extends StatelessWidget {
|
||||
builder: (context, vpn, timer, _) {
|
||||
final isConnected = vpn.status == VpnStatus.connected;
|
||||
final isConnecting = vpn.status == VpnStatus.connecting;
|
||||
final serverHealthy = isConnected && timer.hasSyncedOnce;
|
||||
final serverHealthy = vpn.serverReachable || (isConnected && timer.hasSyncedOnce);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
@@ -55,11 +55,9 @@ class StatusBar extends StatelessWidget {
|
||||
// ── Server health dot (right) ────────────────────────────
|
||||
OnlineDot(
|
||||
isOnline: serverHealthy,
|
||||
label: isConnected && !timer.hasSyncedOnce
|
||||
? 'Syncing…'
|
||||
: serverHealthy
|
||||
? 'Online'
|
||||
: 'Offline',
|
||||
label: isConnected
|
||||
? (timer.hasSyncedOnce ? 'Online' : 'Syncing…')
|
||||
: (vpn.serverReachable ? 'Ready' : 'Offline'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -24,6 +24,12 @@ class TimerBox extends StatelessWidget {
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textWhite,
|
||||
letterSpacing: 2,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: AppColors.accent50,
|
||||
blurRadius: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,22 +3,29 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Rename this file to `app_config.dart` and fill in your real values.
|
||||
// Do not commit your real `app_config.dart` to GitHub!
|
||||
//
|
||||
// Most Reality tunnel fields (vless_ip, vless_port, reality_pbk, reality_sid,
|
||||
// reality_sni, reality_fp) come from the server's /session/status response.
|
||||
// The constants below are fallbacks — the server is the source of truth.
|
||||
// =============================================================================
|
||||
|
||||
abstract final class AppConfig {
|
||||
AppConfig._();
|
||||
|
||||
// ── Server ─────────────────────────────────────────────────────────────
|
||||
static const String serverDomain = 'yourdomain.com'; // REPLACE
|
||||
|
||||
static const String vlessPath = '/tunnel'; // REPLACE if different
|
||||
// ── Server (API) ──────────────────────────────────────────────────────
|
||||
static const String serverDomain = 'yourdomain.com'; // ◄── REPLACE
|
||||
|
||||
static String get hivemindApiBase => 'https://$serverDomain/api';
|
||||
|
||||
// ── VLESS ──────────────────────────────────────────────────────────────
|
||||
static const int serverPort = 443;
|
||||
static const String vlessSecurity = 'tls';
|
||||
static const String vlessType = 'ws';
|
||||
// ── Reality (VPN tunnel) ──────────────────────────────────────────────
|
||||
// These are fallbacks — normally overridden by /session/status response.
|
||||
static const String realitySni = 'www.microsoft.com';
|
||||
static const String realityFp = 'chrome';
|
||||
|
||||
// These are authoritative — never sent by the server.
|
||||
static const String vlessSecurity = 'reality';
|
||||
static const String vlessType = 'tcp';
|
||||
static const String vlessFlow = 'xtls-rprx-vision';
|
||||
|
||||
// ── AdMob ──────────────────────────────────────────────────────────────
|
||||
static const String adUnitId = 'ca-app-pub-0000000000000000/0000000000'; // REPLACE
|
||||
|
||||
@@ -61,11 +61,23 @@ class HivemindService {
|
||||
debugPrint('[HivemindService] Nonce mismatch — retrying…');
|
||||
} else if (data['active'] == true && data['vless_uuid'] != null) {
|
||||
final vlessUuid = data['vless_uuid'];
|
||||
final vlessPath = data['vless_path'] ?? AppConfig.vlessPath;
|
||||
final vlessUrl = 'vless://$vlessUuid@${AppConfig.serverDomain}:${AppConfig.serverPort}'
|
||||
'?path=${Uri.encodeComponent(vlessPath)}'
|
||||
'&security=${AppConfig.vlessSecurity}'
|
||||
|
||||
// Reality tunnel fields — server is source of truth, AppConfig is fallback.
|
||||
final vlessIp = data['vless_ip'] ?? AppConfig.serverDomain;
|
||||
final vlessPort = data['vless_port'] ?? 8443;
|
||||
final pbk = data['reality_pbk'] ?? '';
|
||||
final sid = data['reality_sid'] ?? '';
|
||||
final sni = data['reality_sni'] ?? AppConfig.realitySni;
|
||||
final fp = data['reality_fp'] ?? AppConfig.realityFp;
|
||||
|
||||
final vlessUrl = 'vless://$vlessUuid@$vlessIp:$vlessPort'
|
||||
'?security=${AppConfig.vlessSecurity}'
|
||||
'&type=${AppConfig.vlessType}'
|
||||
'&flow=${AppConfig.vlessFlow}'
|
||||
'&pbk=${Uri.encodeComponent(pbk)}'
|
||||
'&sni=${Uri.encodeComponent(sni)}'
|
||||
'&sid=${Uri.encodeComponent(sid)}'
|
||||
'&fp=${Uri.encodeComponent(fp)}'
|
||||
'#ReVoltVPN';
|
||||
_expectedNonce = null;
|
||||
return vlessUrl;
|
||||
@@ -82,6 +94,16 @@ class HivemindService {
|
||||
throw Exception('Session not activated. Server callback may have timed out.');
|
||||
}
|
||||
|
||||
static Future<bool> checkHealth() async {
|
||||
try {
|
||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/health');
|
||||
final response = await http.get(url).timeout(const Duration(seconds: 3));
|
||||
return response.statusCode == 200;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> verifySession() async {
|
||||
try {
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
|
||||
@@ -14,7 +14,6 @@ class SessionTimer extends ChangeNotifier {
|
||||
final VpnConnection vpnConnection;
|
||||
|
||||
int _remainingSeconds = 0;
|
||||
int _quotaBytes = 0;
|
||||
int _usedBytes = 0;
|
||||
|
||||
bool _hasSyncedOnce = false;
|
||||
@@ -22,9 +21,10 @@ class SessionTimer extends ChangeNotifier {
|
||||
bool _isDisconnecting = false;
|
||||
bool _userInitiatedStop = false;
|
||||
|
||||
String? _currentPath;
|
||||
bool _isThrottled = false;
|
||||
bool _pathSwitchInProgress = false;
|
||||
|
||||
int? _currentPort;
|
||||
bool _portSwitchInProgress = false;
|
||||
|
||||
static const int _maxConsecutiveFailures = 3;
|
||||
|
||||
@@ -48,9 +48,7 @@ class SessionTimer extends ChangeNotifier {
|
||||
bool get isExpired => _remainingSeconds <= 0 && !isRunning;
|
||||
bool get hasSyncedOnce => _hasSyncedOnce;
|
||||
|
||||
int get quotaBytes => _quotaBytes;
|
||||
int get usedBytes => _usedBytes;
|
||||
int get remainingBytes => _quotaBytes > _usedBytes ? _quotaBytes - _usedBytes : 0;
|
||||
double get currentSpeedKBps => _currentSpeedKBps;
|
||||
|
||||
String get formatted {
|
||||
@@ -60,27 +58,6 @@ class SessionTimer extends ChangeNotifier {
|
||||
return '$h:$m:$s';
|
||||
}
|
||||
|
||||
String get formattedDataRemaining {
|
||||
if (_quotaBytes == 0) return '0.00 GB';
|
||||
final gb = remainingBytes / (1024 * 1024 * 1024);
|
||||
return '${gb.toStringAsFixed(2)} GB';
|
||||
}
|
||||
|
||||
String get formattedDataUsed {
|
||||
if (_usedBytes == 0) return '0.00 MB';
|
||||
if (_usedBytes > 1024 * 1024 * 1024) {
|
||||
final gb = _usedBytes / (1024 * 1024 * 1024);
|
||||
return '${gb.toStringAsFixed(2)} GB';
|
||||
}
|
||||
final mb = _usedBytes / (1024 * 1024);
|
||||
return '${mb.toStringAsFixed(2)} MB';
|
||||
}
|
||||
|
||||
double get progress {
|
||||
if (_quotaBytes == 0) return 0.0;
|
||||
return (_quotaBytes - _usedBytes) / _quotaBytes;
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
|
||||
/// Fires whenever VpnConnection state changes.
|
||||
@@ -113,7 +90,6 @@ class SessionTimer extends ChangeNotifier {
|
||||
|
||||
Future<void> start(String adType) async {
|
||||
_remainingSeconds = 0;
|
||||
_quotaBytes = 0;
|
||||
_usedBytes = 0;
|
||||
_lastUsedBytes = 0;
|
||||
_currentSpeedKBps = 0.0;
|
||||
@@ -123,6 +99,8 @@ class SessionTimer extends ChangeNotifier {
|
||||
_offlineSeconds = 0;
|
||||
_isDisconnecting = false;
|
||||
_userInitiatedStop = false;
|
||||
_currentPort = null;
|
||||
_portSwitchInProgress = false;
|
||||
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), _tick);
|
||||
@@ -227,7 +205,6 @@ class SessionTimer extends ChangeNotifier {
|
||||
}
|
||||
|
||||
_remainingSeconds = data['expires_in_seconds'] ?? _remainingSeconds;
|
||||
_quotaBytes = data['quota_bytes'] ?? _quotaBytes;
|
||||
_usedBytes = data['used_bytes'] ?? _usedBytes;
|
||||
|
||||
|
||||
@@ -240,27 +217,27 @@ class SessionTimer extends ChangeNotifier {
|
||||
_lastUsedBytes = _usedBytes;
|
||||
|
||||
|
||||
// ── Detect path changes (throttle engage / disengage) ──────
|
||||
final serverPath = data['vless_path'] as String?;
|
||||
if (serverPath != null &&
|
||||
_currentPath != null &&
|
||||
serverPath != _currentPath &&
|
||||
!_pathSwitchInProgress) {
|
||||
debugPrint('[Timer] Path changed $_currentPath → $serverPath — reconnecting…');
|
||||
_currentPath = serverPath;
|
||||
// ── Detect port change (throttle engage / disengage) ──────
|
||||
final serverPort = data['vless_port'];
|
||||
if (serverPort != null &&
|
||||
_currentPort != null &&
|
||||
serverPort != _currentPort &&
|
||||
!_portSwitchInProgress) {
|
||||
debugPrint('[Timer] Port changed $_currentPort → $serverPort — reconnecting…');
|
||||
_currentPort = serverPort;
|
||||
_isThrottled = data['is_throttled'] ?? false;
|
||||
_pathSwitchInProgress = true;
|
||||
_portSwitchInProgress = true;
|
||||
|
||||
_timer?.cancel();
|
||||
_isDisconnecting = true; // block _stopInternal during relocation
|
||||
_isDisconnecting = true;
|
||||
await vpnConnection.disconnect(skipCleanup: true);
|
||||
await vpnConnection.connect(skipAdBypass: true, quickReconnect: true);
|
||||
|
||||
_resumeTicking();
|
||||
_pathSwitchInProgress = false;
|
||||
_portSwitchInProgress = false;
|
||||
return;
|
||||
}
|
||||
_currentPath = serverPath;
|
||||
_currentPort = serverPort;
|
||||
_isThrottled = data['is_throttled'] ?? false;
|
||||
|
||||
_consecutiveFailures = 0;
|
||||
|
||||
@@ -26,6 +26,11 @@ class VpnConnection extends ChangeNotifier {
|
||||
bool _isStartupRestoration = false;
|
||||
bool get isStartupRestoration => _isStartupRestoration;
|
||||
|
||||
bool _serverReachable = false;
|
||||
bool get serverReachable => _serverReachable;
|
||||
|
||||
Timer? _healthTimer;
|
||||
|
||||
late final FlutterVless _vless;
|
||||
bool _initialized = false;
|
||||
|
||||
@@ -53,6 +58,10 @@ class VpnConnection extends ChangeNotifier {
|
||||
debugPrint('[VPN] VLESS init error (expected on emulator): $e');
|
||||
}
|
||||
|
||||
// Periodic server health check
|
||||
_checkHealth();
|
||||
_healthTimer = Timer.periodic(const Duration(seconds: 30), (_) => _checkHealth());
|
||||
|
||||
// Check if VPN was already running (app killed and reopened)
|
||||
try {
|
||||
final coreVersion = await _vless.getCoreVersion();
|
||||
@@ -62,9 +71,6 @@ class VpnConnection extends ChangeNotifier {
|
||||
if (delay > 0) {
|
||||
_isStartupRestoration = true;
|
||||
_setStatus(VpnStatus.connected, 'Secured');
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
_isStartupRestoration = false;
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -75,12 +81,14 @@ class VpnConnection extends ChangeNotifier {
|
||||
_setStatus(VpnStatus.connected, 'Secured');
|
||||
break;
|
||||
case VlessConnectionState.disconnected:
|
||||
_isStartupRestoration = false;
|
||||
_setStatus(VpnStatus.disconnected, 'Tap to connect');
|
||||
break;
|
||||
case VlessConnectionState.connecting:
|
||||
_setStatus(VpnStatus.connecting, 'Establishing tunnel…');
|
||||
break;
|
||||
case VlessConnectionState.disconnecting:
|
||||
_isStartupRestoration = false;
|
||||
_setStatus(VpnStatus.disconnecting, 'Tearing down…');
|
||||
break;
|
||||
case VlessConnectionState.unknown:
|
||||
@@ -234,8 +242,14 @@ class VpnConnection extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _checkHealth() async {
|
||||
_serverReachable = await HivemindService.checkHealth();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_healthTimer?.cancel();
|
||||
if (_status == VpnStatus.connected || _status == VpnStatus.connecting) {
|
||||
try {
|
||||
_vless.stopVless();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
@@ -22,6 +23,15 @@ void main() async {
|
||||
systemNavigationBarIconBrightness: Brightness.light,
|
||||
));
|
||||
|
||||
FlutterError.onError = (details) {
|
||||
debugPrint('[FlutterError] ${details.exception}');
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
debugPrint('[PlatformError] $error\n$stack');
|
||||
return true;
|
||||
};
|
||||
|
||||
await ConsentManager.requestConsentIfNeeded();
|
||||
await MobileAds.instance.initialize();
|
||||
|
||||
|
||||
Reference in new issue
Block a user