import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import '../models/connection_state_enum.dart'; import 'settings_screen.dart'; import 'logs_screen.dart'; import 'app_routing_screen.dart'; import 'qr_scanner_screen.dart'; class HomeScreen extends StatefulWidget { final SharedPreferences prefs; const HomeScreen({super.key, required this.prefs}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State with TickerProviderStateMixin { static const platform = MethodChannel('com.ospab.ostp/vpn'); ConnectionStateEnum _state = ConnectionStateEnum.disconnected; Timer? _pollTimer; Timer? _uptimeTimer; int _uptimeSecs = 0; String _serverAddr = '127.0.0.1:443'; String _accessKey = 'default_key'; String _download = '0 B'; String _upload = '0 B'; late AnimationController _pulseController; late AnimationController _spinController; bool _isCheckingPing = false; String _pingText = 'Target Ping: -- ms'; Color _pingColor = Colors.white54; @override void initState() { super.initState(); _loadSettings(); _pulseController = AnimationController( vsync: this, duration: const Duration(seconds: 2), ); _spinController = AnimationController( vsync: this, duration: const Duration(seconds: 4), ); _checkInitialState(); } Future _checkInitialState() async { try { final isRunning = await platform.invokeMethod('isRunning'); if (isRunning == true && mounted) { _setConnected(); } } catch (e) { debugPrint("Failed to check initial state: $e"); } } void _loadSettings() { setState(() { _serverAddr = widget.prefs.getString('server_addr') ?? '127.0.0.1:443'; _accessKey = widget.prefs.getString('access_key') ?? ''; }); _updateLatestConfigJson(); } void _updateLatestConfigJson() { final exDomains = widget.prefs.getString('ex_domains') ?? ''; final exIps = widget.prefs.getString('ex_ips') ?? ''; final exProcesses = widget.prefs.getString('ex_processes') ?? ''; final debugMode = widget.prefs.getBool('debug_mode') ?? false; final transportMode = widget.prefs.getString('transport_mode') ?? 'udp'; final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com'; final wss = widget.prefs.getBool('wss') ?? false; final mtu = widget.prefs.getString('mtu') ?? '1140'; final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false; final muxSessions = widget.prefs.getString('mux_sessions') ?? '2'; final dnsServer = widget.prefs.getString('dns_server'); final effectiveDnsServer = (dnsServer == null || dnsServer.isEmpty) ? '1.1.1.1' : dnsServer; final tunStack = 'ostp'; final appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass'; final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? []; final localBind = widget.prefs.getString('local_bind') ?? '127.0.0.1:1088'; final configMap = { "mode": "client", "debug": debugMode, "ostp": { "server_addr": _serverAddr, "local_bind_addr": "0.0.0.0:0", "access_key": _accessKey, "handshake_timeout_ms": 10000, "io_timeout_ms": 5000, "mtu": int.tryParse(mtu) ?? 1140, }, "local_proxy": { "bind_addr": localBind, "connect_timeout_ms": 15000, }, "transport": { "mode": transportMode, "stealth_sni": stealthSni, "wss": wss, }, "multiplex": { "enabled": muxEnabled, "sessions": int.tryParse(muxSessions) ?? 2, }, "reality": { "enabled": widget.prefs.getBool('reality_enabled') ?? false, "dest": "", "private_key": "", "pbk": widget.prefs.getString('pbk') ?? "", "sid": widget.prefs.getString('sid') ?? "", "sni_list": [] }, "tun": { "enable": true, "stack": tunStack }, "exclusions": { "domains": exDomains.split('\n').where((s) => s.trim().isNotEmpty).toList(), "ips": exIps.split('\n').where((s) => s.trim().isNotEmpty).toList(), "processes": exProcesses.split('\n').where((s) => s.trim().isNotEmpty).toList(), }, "app_rules": { "mode": appRoutingMode, "packages": appRoutingPackages, }, "dns_server": effectiveDnsServer, "tun_stack": tunStack }; widget.prefs.setString('latest_config_json', jsonEncode(configMap)); platform.invokeMethod('saveConfig', { "configJson": jsonEncode(configMap) }); } @override void dispose() { _pollTimer?.cancel(); _uptimeTimer?.cancel(); _pulseController.dispose(); _spinController.dispose(); super.dispose(); } Future _toggleConnection() async { if (_state == ConnectionStateEnum.disconnected) { if (_serverAddr.isEmpty || _accessKey.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please configure Server and Key in Settings')), ); return; } setState(() { _state = ConnectionStateEnum.connecting; }); _pulseController.repeat(reverse: true); _spinController.repeat(); final dnsServer = widget.prefs.getString('dns_server'); final effectiveDnsServer = (dnsServer == null || dnsServer.isEmpty) ? '1.1.1.1' : dnsServer; final exDomains = widget.prefs.getString('ex_domains') ?? ''; final exIps = widget.prefs.getString('ex_ips') ?? ''; final exProcesses = widget.prefs.getString('ex_processes') ?? ''; final debugMode = widget.prefs.getBool('debug_mode') ?? false; final transportMode = widget.prefs.getString('transport_mode') ?? 'udp'; final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com'; final wss = widget.prefs.getBool('wss') ?? false; final mtu = widget.prefs.getString('mtu') ?? '1140'; final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false; final muxSessions = widget.prefs.getString('mux_sessions') ?? '2'; final tunStack = 'ostp'; final appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass'; final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? []; final localBind = widget.prefs.getString('local_bind') ?? '127.0.0.1:1088'; final configMap = { "mode": "client", "debug": debugMode, "ostp": { "server_addr": _serverAddr, "local_bind_addr": "0.0.0.0:0", "access_key": _accessKey, "handshake_timeout_ms": 10000, "io_timeout_ms": 5000, "mtu": int.tryParse(mtu) ?? 1140, }, "local_proxy": { "bind_addr": localBind, "connect_timeout_ms": 15000, }, "transport": { "mode": transportMode, "stealth_sni": stealthSni, "wss": wss, }, "multiplex": { "enabled": muxEnabled, "sessions": int.tryParse(muxSessions) ?? 2, }, "reality": { "enabled": widget.prefs.getBool('reality_enabled') ?? false, "dest": "", "private_key": "", "pbk": widget.prefs.getString('pbk') ?? "", "sid": widget.prefs.getString('sid') ?? "", "sni_list": [] }, "tun": { "enable": true, "stack": tunStack }, "exclusions": { "domains": exDomains.split('\n').where((s) => s.trim().isNotEmpty).toList(), "ips": exIps.split('\n').where((s) => s.trim().isNotEmpty).toList(), "processes": exProcesses.split('\n').where((s) => s.trim().isNotEmpty).toList(), }, "app_rules": { "mode": appRoutingMode, "packages": appRoutingPackages, }, "dns_server": dnsServer, "tun_stack": tunStack }; widget.prefs.setString('latest_config_json', jsonEncode(configMap)); try { await platform.invokeMethod('saveConfig', { "configJson": jsonEncode(configMap) }); await platform.invokeMethod('startTunnel', { "configJson": jsonEncode(configMap) }); bool started = false; for (int i = 0; i < 10; i++) { await Future.delayed(const Duration(milliseconds: 500)); final isRunning = await platform.invokeMethod('isRunning'); if (isRunning == true) { started = true; break; } } if (started) { _setConnected(); } else { _setDisconnected(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to connect. Check logs for details.')), ); } } } catch (e, stackTrace) { debugPrint("Failed to start tunnel: $e\n$stackTrace"); _setDisconnected(); if (mounted) { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Error', style: TextStyle(color: Colors.redAccent)), content: SingleChildScrollView( child: SelectableText(e.toString(), style: const TextStyle(fontFamily: 'monospace', fontSize: 12)), ), actions: [ TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: e.toString())); ScaffoldMessenger.of(ctx).showSnackBar(const SnackBar(content: Text('Copied!'))); }, child: const Text('Copy'), ), TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Close'), ), ], ), ); } } } else { try { await platform.invokeMethod('stopTunnel'); } catch (e) { debugPrint("Stop error: $e"); } _setDisconnected(); } } Future _runAutoMode() async { final mtus = [1500, 1350, 1280, 1140]; final modes = [ {'t': 'udp', 'w': false, 'r': false}, {'t': 'uot', 'w': false, 'r': false}, {'t': 'uot', 'w': true, 'r': false}, {'t': 'uot', 'w': false, 'r': true}, ]; if (_serverAddr.isEmpty || _accessKey.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please configure Server and Key first')), ); return; } for (var mode in modes) { for (var mtu in mtus) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Testing: ${mode['t']} | WSS: ${mode['w']} | XTLS: ${mode['r']} | MTU: $mtu'), duration: const Duration(seconds: 2)), ); // Update prefs await widget.prefs.setString('mtu', mtu.toString()); await widget.prefs.setString('transport_mode', mode['t'] as String); await widget.prefs.setBool('wss', mode['w'] as bool); await widget.prefs.setBool('reality_enabled', mode['r'] as bool); _updateLatestConfigJson(); setState(() { _state = ConnectionStateEnum.connecting; }); _pulseController.repeat(reverse: true); _spinController.repeat(); try { final configJson = widget.prefs.getString('latest_config_json') ?? '{}'; await platform.invokeMethod('startTunnel', {"configJson": configJson}); bool started = false; for (int i = 0; i < 10; i++) { await Future.delayed(const Duration(milliseconds: 500)); final isRunning = await platform.invokeMethod('isRunning'); if (isRunning == true) { started = true; break; } } if (started) { _setConnected(); // Wait to see if connection is stable and ping is successful await Future.delayed(const Duration(seconds: 3)); try { final metricsJson = await platform.invokeMethod('getMetrics'); if (metricsJson != null && metricsJson.isNotEmpty) { final Map parsed = jsonDecode(metricsJson); final rttMs = parsed['rtt_ms'] as int? ?? 0; if (rttMs > 0) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Success! Found working config: ${mode['t']} (MTU $mtu)')), ); } return; // Stop on first working config } } } catch (e) { // Ignore metrics error } // Connection seems unstable or no ping, stop and try next await platform.invokeMethod('stopTunnel'); _setDisconnected(); } else { _setDisconnected(); } } catch (e) { _setDisconnected(); } } } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Auto search finished. No working config found.')), ); } } void _setConnected() { if (!mounted) return; setState(() { _state = ConnectionStateEnum.connected; }); _pulseController.stop(); _pulseController.value = 1.0; _uptimeSecs = 0; _uptimeTimer?.cancel(); _uptimeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) return; setState(() => _uptimeSecs++); }); _startPollingMetrics(); } void _startPollingMetrics() { _pollTimer?.cancel(); _pollTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { if (!mounted) return; try { final metricsJson = await platform.invokeMethod('getMetrics'); if (metricsJson != null && metricsJson.isNotEmpty) { final Map parsed = jsonDecode(metricsJson); final bytesSent = parsed['bytes_sent'] as int? ?? 0; final bytesRecv = parsed['bytes_recv'] as int? ?? 0; final connState = parsed['connection_state'] as int? ?? 2; final rttMs = parsed['rtt_ms'] as int? ?? 0; if (connState == 0 && _state != ConnectionStateEnum.disconnected) { try { await platform.invokeMethod('stopTunnel'); } catch (e) { debugPrint("Failed to stop background tunnel: $e"); } _setDisconnected(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Connection failed. Check logs for details.')), ); } return; } if (mounted) { setState(() { _download = _formatBytes(bytesRecv); _upload = _formatBytes(bytesSent); if (rttMs > 0 && !_isCheckingPing) { _pingText = 'Server Ping: $rttMs ms'; if (rttMs < 100) { _pingColor = const Color(0xFF22D3A5); } else if (rttMs < 250) { _pingColor = Colors.amberAccent; } else { _pingColor = Colors.redAccent; } } }); } } } catch (e) { debugPrint("Failed to get metrics: $e"); } }); } String _formatBytes(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; } Future _checkConnectionLatency() async { if (_state != ConnectionStateEnum.connected) return; setState(() { _isCheckingPing = true; _pingText = 'Updating...'; _pingColor = Colors.white70; }); await Future.delayed(const Duration(milliseconds: 500)); if (mounted) { setState(() { _isCheckingPing = false; }); } } void _setDisconnected() { if (!mounted) return; setState(() { _state = ConnectionStateEnum.disconnected; _download = '0 B'; _upload = '0 B'; _pingText = 'Target Ping: -- ms'; _pingColor = Colors.white54; _isCheckingPing = false; }); _pulseController.stop(); _pulseController.value = 0.0; _spinController.stop(); _uptimeTimer?.cancel(); _pollTimer?.cancel(); } String _formatTime(int s) { final h = s ~/ 3600; final m = (s % 3600) ~/ 60; final sec = s % 60; final pad = (int n) => n.toString().padLeft(2, '0'); return h > 0 ? '$h:${pad(m)}:${pad(sec)}' : '${pad(m)}:${pad(sec)}'; } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( body: Stack( children: [ Positioned( top: -150, right: -100, child: Container( width: 400, height: 400, decoration: BoxDecoration( shape: BoxShape.circle, color: theme.colorScheme.primary.withOpacity(0.15), ), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100), child: Container(), ), ), ), Positioned( bottom: -100, left: -100, child: Container( width: 350, height: 350, decoration: BoxDecoration( shape: BoxShape.circle, color: theme.colorScheme.secondary.withOpacity(0.1), ), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100), child: Container(), ), ), ), SafeArea( child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: IntrinsicHeight( child: Column( children: [ _buildTopBar(theme), Expanded(child: _buildStage(theme)), _buildMetricsBar(theme), ], ), ), ), ); }, ), ), ], ), ); } Widget _buildTopBar(ThemeData theme) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ AnimatedContainer( duration: const Duration(milliseconds: 300), width: 12, height: 12, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: _state == ConnectionStateEnum.connected ? theme.colorScheme.secondary : theme.colorScheme.primary, boxShadow: [ BoxShadow( color: _state == ConnectionStateEnum.connected ? theme.colorScheme.secondary.withOpacity(0.5) : theme.colorScheme.primary.withOpacity(0.5), blurRadius: 10, ) ] ), ), const SizedBox(width: 12), const Text( 'OSTP', style: TextStyle( fontSize: 22, fontWeight: FontWeight.w800, letterSpacing: 2.5, color: Colors.white, ), ), ], ), Row( children: [ IconButton( iconSize: 30, icon: const Icon(Icons.auto_mode_rounded, color: Colors.white), onPressed: () { if (_state != ConnectionStateEnum.disconnected) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Disconnect first to run Auto mode')), ); return; } _runAutoMode(); }, ), IconButton( iconSize: 30, icon: const Icon(Icons.settings_outlined, color: Colors.white), onPressed: () async { await Navigator.push( context, MaterialPageRoute(builder: (context) => SettingsScreen(prefs: widget.prefs)), ); _loadSettings(); }, ) ], ) ], ), ); } Widget _buildStage(ThemeData theme) { Color getAccentColor() { if (_state == ConnectionStateEnum.connected) return theme.colorScheme.secondary; return theme.colorScheme.primary; } return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 260, height: 260, child: Stack( alignment: Alignment.center, children: [ if (_state != ConnectionStateEnum.disconnected) RotationTransition( turns: _spinController, child: Container( width: 240, height: 240, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: getAccentColor().withOpacity(0.25), width: 2.0, ), ), ), ), if (_state != ConnectionStateEnum.disconnected) RotationTransition( turns: ReverseAnimation(_spinController), child: Container( width: 200, height: 200, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: getAccentColor().withOpacity(0.15), width: 1.5, ), ), ), ), AnimatedBuilder( animation: _pulseController, builder: (context, child) { return Container( width: 140, height: 140, decoration: BoxDecoration( shape: BoxShape.circle, color: theme.colorScheme.surface, border: Border.all( color: _state == ConnectionStateEnum.disconnected ? Colors.white.withOpacity(0.15) : getAccentColor(), width: 3, ), boxShadow: [ if (_state != ConnectionStateEnum.disconnected) BoxShadow( color: getAccentColor().withOpacity(0.4 * (_state == ConnectionStateEnum.connected ? 1.0 : _pulseController.value)), blurRadius: 40, spreadRadius: 8, ) ] ), child: child, ); }, child: Material( color: Colors.transparent, child: InkWell( customBorder: const CircleBorder(), onTap: _toggleConnection, child: Icon( Icons.power_settings_new_rounded, size: 60, color: _state == ConnectionStateEnum.disconnected ? Colors.white54 : getAccentColor(), ), ), ), ), ], ), ), const SizedBox(height: 40), Text( _state == ConnectionStateEnum.disconnected ? 'Disconnected' : _state == ConnectionStateEnum.connecting ? 'Connecting...' : 'Connected', style: TextStyle( fontSize: 26, fontWeight: FontWeight.w700, color: _state == ConnectionStateEnum.disconnected ? Colors.white70 : getAccentColor(), ), ), const SizedBox(height: 8), Text( _state == ConnectionStateEnum.connected ? _formatTime(_uptimeSecs) : 'Tap to protect your traffic', style: const TextStyle( fontSize: 16, color: Colors.white54, ), ), const SizedBox(height: 30), AnimatedOpacity( opacity: _state == ConnectionStateEnum.connected ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.08), borderRadius: BorderRadius.circular(30), border: Border.all(color: Colors.white.withOpacity(0.15)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.dns_rounded, size: 18, color: Colors.white70), const SizedBox(width: 10), Text( _serverAddr, style: const TextStyle( fontFamily: 'monospace', fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white70, ), ), ], ), ), const SizedBox(height: 16), Container( margin: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.03), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white.withOpacity(0.06)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'CONNECTION TEST', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white38, letterSpacing: 0.8, ), ), const SizedBox(height: 4), Text( _pingText, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: _pingColor, ), ), ], ), _isCheckingPing ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white70), ) : TextButton.icon( onPressed: _checkConnectionLatency, icon: Icon(Icons.speed_rounded, size: 16, color: theme.colorScheme.primary), label: Text( 'Test Ping', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 13, color: theme.colorScheme.primary, ), ), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), backgroundColor: theme.colorScheme.primary.withOpacity(0.1), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ], ), ), ], ), ) ], ); } Widget _buildMetricsBar(ThemeData theme) { return Container( padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), decoration: BoxDecoration( color: Colors.white.withOpacity(0.04), border: Border(top: BorderSide(color: Colors.white.withOpacity(0.08))), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildMetricItem(Icons.arrow_downward_rounded, 'Download', _download, theme.colorScheme.secondary), Container(width: 1, height: 40, color: Colors.white.withOpacity(0.15)), _buildMetricItem(Icons.arrow_upward_rounded, 'Upload', _upload, theme.colorScheme.primary), ], ), ); } Widget _buildMetricItem(IconData icon, String label, String value, Color color) { return Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 20, color: color), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label.toUpperCase(), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: Colors.white54, letterSpacing: 0.8, ), ), const SizedBox(height: 4), Text( value, style: const TextStyle( fontFamily: 'monospace', fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white, ), ), ], ) ], ); } }