flutter_vpnengine 1.0.5+11 copy "flutter_vpnengine: ^1.0.5+11" to clipboard
flutter_vpnengine: ^1.0.5+11 copied to clipboard

VPN Client Engine Flutter plugin

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:vpnclient_engine_flutter/vpnclient_engine_flutter.dart';
import 'vpn_test.dart';
import 'vpn_state_example.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'VPN Client Engine Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
        brightness: Brightness.dark,
      ),
      home: const VPNClientDemo(),
    );
  }
}

class VPNClientDemo extends StatefulWidget {
  const VPNClientDemo({super.key});

  @override
  VPNClientDemoState createState() => VPNClientDemoState();
}

class VPNClientDemoState extends State<VPNClientDemo> {
  ConnectionStatus _connectionStatus = ConnectionStatus.disconnected;
  String _currentServer = 'Not Connected';
  List<Server> _servers = [];
  List<String> _loadedSubscriptions = [];
  SessionStatistics _sessionStatistics = SessionStatistics(
    dataInBytes: 0,
    dataOutBytes: 0,
  );
  bool _isLoading = false;

  final TextEditingController _subscriptionUrlController = TextEditingController();
  final PageController _pageController = PageController();

  @override
  void initState() {
    super.initState();
    _initializeEngine();
  }

  void _initializeEngine() {
    VPNclientEngine.initialize();
    VPNclientEngine.onConnectionStatusChanged.listen(_updateConnectionStatus);
    VPNclientEngine.onPingResult.listen(_updatePingResult);
    VPNclientEngine.onDataUsageUpdated.listen(_updateSessionStatistics);
  }

  void _updateConnectionStatus(ConnectionStatus status) {
    setState(() {
      _connectionStatus = status;
    });
  }

  void _updatePingResult(PingResult result) {
    // Update server latency in the list
    setState(() {
      if (result.serverIndex < _servers.length) {
        // Create a new Server instance with updated latency
        _servers[result.serverIndex] = Server(
          address: _servers[result.serverIndex].address,
          latency: result.latencyInMs,
          location: _servers[result.serverIndex].location,
          isPreferred: _servers[result.serverIndex].isPreferred,
          config: _servers[result.serverIndex].config,
        );
      }
    });
  }

  void _updateSessionStatistics(SessionStatistics stats) {
    setState(() {
      _sessionStatistics = stats;
    });
  }

  Future<void> _loadSubscription() async {
    if (_subscriptionUrlController.text.isEmpty) {
      _showSnackBar('Please enter a subscription URL');
      return;
    }

    setState(() {
      _isLoading = true;
    });

    try {
      await VPNclientEngine.loadSubscriptions(
        subscriptionLinks: [_subscriptionUrlController.text],
      );
      
      setState(() {
        _loadedSubscriptions.add(_subscriptionUrlController.text);
        _subscriptionUrlController.clear();
      });
      
      await _refreshServers();
      _showSnackBar('Subscription loaded successfully');
    } catch (e) {
      _showSnackBar('Failed to load subscription: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _refreshServers() async {
    setState(() {
      _servers = VPNclientEngine.getServerList();
    });
  }

  Future<void> _connectToServer(int serverIndex) async {
    if (_loadedSubscriptions.isEmpty) {
      _showSnackBar('Please load a subscription first');
      return;
    }

    setState(() {
      _isLoading = true;
    });

    try {
      await VPNclientEngine.connect(subscriptionIndex: 0, serverIndex: serverIndex);
      setState(() {
        _currentServer = _servers[serverIndex].address;
      });
      _showSnackBar('Connecting to ${_servers[serverIndex].address}');
    } catch (e) {
      _showSnackBar('Failed to connect: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _disconnect() async {
    setState(() {
      _isLoading = true;
    });

    try {
      await VPNclientEngine.disconnect();
      setState(() {
        _currentServer = 'Not Connected';
      });
      _showSnackBar('Disconnected');
    } catch (e) {
      _showSnackBar('Failed to disconnect: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  void _pingServer(int serverIndex) {
    VPNclientEngine.pingServer(subscriptionIndex: 0, index: serverIndex);
  }

  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('VPN Client Engine'),
        elevation: 0,
        backgroundColor: Colors.transparent,
      ),
      body: PageView(
        controller: _pageController,
        children: [
          _buildHomePage(),
          _buildServersPage(),
          _buildSettingsPage(),
          const VpnTestPage(),
          const VpnStateExample(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: 0,
        onTap: (index) => _pageController.animateToPage(
          index,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeInOut,
        ),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.list), label: 'Servers'),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
          BottomNavigationBarItem(icon: Icon(Icons.science), label: 'Test'),
          BottomNavigationBarItem(icon: Icon(Icons.vpn_key), label: 'New API'),
        ],
      ),
    );
  }

  Widget _buildHomePage() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        children: [
          // Connection Status Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                children: [
                  Row(
                    children: [
                      Container(
                        width: 12,
                        height: 12,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: _getStatusColor(),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Text(
                        _getStatusText(),
                        style: Theme.of(context).textTheme.titleMedium,
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Text(
                    _currentServer,
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          
          // Connection Controls
          Row(
            children: [
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: _connectionStatus == ConnectionStatus.connected 
                    ? null 
                    : _isLoading 
                      ? null 
                      : () => _connectToServer(0),
                  icon: const Icon(Icons.power_settings_new),
                  label: const Text('Connect'),
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: _connectionStatus == ConnectionStatus.connected 
                    ? _disconnect 
                    : null,
                  icon: const Icon(Icons.stop),
                  label: const Text('Disconnect'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 24),
          
          // Statistics Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Session Statistics',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 8),
                  _buildStatRow('Duration', _formatDuration(_sessionStatistics.sessionDuration)),
                  _buildStatRow('Data In', _formatBytes(_sessionStatistics.dataInBytes)),
                  _buildStatRow('Data Out', _formatBytes(_sessionStatistics.dataOutBytes)),
                ],
              ),
            ),
          ),
          const SizedBox(height: 24),
          
          // Subscription Management
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Subscription Management',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 16),
                  TextField(
                    controller: _subscriptionUrlController,
                    decoration: const InputDecoration(
                      hintText: 'Enter subscription URL',
                      border: OutlineInputBorder(),
                    ),
                  ),
                  const SizedBox(height: 8),
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton(
                      onPressed: _isLoading ? null : _loadSubscription,
                      child: _isLoading 
                        ? const SizedBox(
                            height: 20,
                            width: 20,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          )
                        : const Text('Load Subscription'),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildServersPage() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                'Available Servers',
                style: Theme.of(context).textTheme.headlineSmall,
              ),
              TextButton.icon(
                onPressed: _refreshServers,
                icon: const Icon(Icons.refresh),
                label: const Text('Refresh'),
              ),
            ],
          ),
          const SizedBox(height: 16),
          if (_servers.isEmpty)
            Expanded(
              child: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(
                      Icons.cloud_off,
                      size: 64,
                      color: Colors.grey[400],
                    ),
                    const SizedBox(height: 16),
                    Text(
                      'No servers available',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    Text(
                      'Load a subscription to see available servers',
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                  ],
                ),
              ),
            )
          else
            Expanded(
              child: ListView.builder(
                itemCount: _servers.length,
                itemBuilder: (context, index) {
                  final server = _servers[index];
                  final isConnected = _currentServer == server.address;
                  
                  return Card(
                    margin: const EdgeInsets.only(bottom: 8),
                    child: ListTile(
                      leading: Icon(
                        isConnected ? Icons.check_circle : Icons.cloud,
                        color: isConnected ? Colors.green : null,
                      ),
                      title: Text(server.address),
                      subtitle: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          if (server.location != null)
                            Text('Location: ${server.location}'),
                          if (server.latency != null)
                            Text('Latency: ${server.latency}ms'),
                        ],
                      ),
                      trailing: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          IconButton(
                            onPressed: () => _pingServer(index),
                            icon: const Icon(Icons.speed),
                            tooltip: 'Ping Server',
                          ),
                          IconButton(
                            onPressed: isConnected 
                              ? null 
                              : () => _connectToServer(index),
                            icon: Icon(
                              isConnected ? Icons.check : Icons.power_settings_new,
                            ),
                            tooltip: isConnected ? 'Connected' : 'Connect',
                          ),
                        ],
                      ),
                    ),
                  );
                },
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildSettingsPage() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Settings',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 16),
          Card(
            child: Column(
              children: [
                ListTile(
                  leading: const Icon(Icons.auto_awesome),
                  title: const Text('Auto Connect'),
                  subtitle: const Text('Automatically connect on app launch'),
                  trailing: Switch(
                    value: false, // TODO: Implement state management
                    onChanged: (value) {
                      VPNclientEngine.setAutoConnect(enable: value);
                    },
                  ),
                ),
                ListTile(
                  leading: const Icon(Icons.security),
                  title: const Text('Kill Switch'),
                  subtitle: const Text('Block internet when VPN disconnects'),
                  trailing: Switch(
                    value: false, // TODO: Implement state management
                    onChanged: (value) {
                      VPNclientEngine.setKillSwitch(enable: value);
                    },
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 16),
          Card(
            child: Column(
              children: [
                ListTile(
                  leading: const Icon(Icons.route),
                  title: const Text('Routing Rules'),
                  subtitle: const Text('Configure app and domain routing'),
                  onTap: () => _showRoutingRulesDialog(),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildStatRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
        ],
      ),
    );
  }

  Color _getStatusColor() {
    switch (_connectionStatus) {
      case ConnectionStatus.connected:
        return Colors.green;
      case ConnectionStatus.connecting:
        return Colors.orange;
      case ConnectionStatus.error:
        return Colors.red;
      case ConnectionStatus.disconnected:
      default:
        return Colors.grey;
    }
  }

  String _getStatusText() {
    switch (_connectionStatus) {
      case ConnectionStatus.connected:
        return 'Connected';
      case ConnectionStatus.connecting:
        return 'Connecting...';
      case ConnectionStatus.error:
        return 'Error';
      case ConnectionStatus.disconnected:
      default:
        return 'Disconnected';
    }
  }

  String _formatDuration(Duration? duration) {
    if (duration == null) return '0:00';
    final minutes = duration.inMinutes;
    final seconds = duration.inSeconds % 60;
    return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  }

  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';
  }

  void _showRoutingRulesDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Routing Rules'),
        content: const Text('Routing rules configuration will be implemented here.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}