flutter_singbox_vpn 1.1.3 copy "flutter_singbox_vpn: ^1.1.3" to clipboard
flutter_singbox_vpn: ^1.1.3 copied to clipboard

A Flutter plugin for Sing-Box VPN by TecClub. Provides complete VPN functionality with traffic stats, logging, and per-app tunneling.

example/lib/main.dart

import 'dart:developer';

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';

import 'package:flutter/services.dart';
import 'package:flutter_singbox_vpn/flutter_singbox.dart';
import 'package:permission_handler/permission_handler.dart';
import 'per_app_tunneling_page.dart';

// Global instance of FlutterSingbox to share across the app
final FlutterSingbox singboxPlugin = FlutterSingbox();

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      routes: {
        '/': (context) => const HomePage(),
        '/per_app_tunneling': (context) =>
            PerAppTunnelingPage(singbox: singboxPlugin),
      },
      initialRoute: '/',
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String _platformVersion = 'Unknown';
  String _vpnStatus = 'Stopped';
  final _configController = TextEditingController();

  // Traffic stats
  String _uploadSpeed = '0 B/s';
  String _downloadSpeed = '0 B/s';
  String _uploadTotal = '0 B';
  String _downloadTotal = '0 B';
  String _sessionTotal = '0 B';
  int _connectionsIn = 0;
  int _connectionsOut = 0;

  // Logs
  final List<String> _logs = [];
  final ScrollController _logScrollController = ScrollController();
  bool _showLogs = false;

  // Using the global singbox instance
  final _flutterSingboxPlugin = singboxPlugin;
  StreamSubscription? _statusSubscription;
  StreamSubscription? _trafficSubscription;
  StreamSubscription? _logSubscription;

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

  @override
  void dispose() {
    _statusSubscription?.cancel();
    _trafficSubscription?.cancel();
    _logSubscription?.cancel();
    _configController.dispose();
    _logScrollController.dispose();
    super.dispose();
  }

  // Initialize the plugin and set up listeners
  Future<void> initPlugin() async {
    try {
      // // Get platform version
      // final platformVersion =
      //     await _flutterSingboxPlugin.getPlatformVersion() ??
      //     'Unknown platform version';

      // Get current VPN status and log it
      Permission.notification.request();
      _flutterSingboxPlugin.setNotificationTitle('SafePro VPN');
      _flutterSingboxPlugin.setNotificationDescription(
        'Your secure VPN connection is active',
      );
      final vpnStatus = await _flutterSingboxPlugin.getVPNStatus();
      print('Initial VPN status: $vpnStatus');

      // Get saved config
      await _flutterSingboxPlugin.getConfig();

      // Listen for status changes
      _statusSubscription = _flutterSingboxPlugin.onStatusChanged.listen((
        event,
      ) {
        if (event['status'] != null) {
          print('VPN status update: ${event['status']}');
          setState(() {
            _vpnStatus = event['status'] as String;
          });
        }
      });

      // Listen for traffic updates
      _trafficSubscription = _flutterSingboxPlugin.onTrafficUpdate.listen((
        stats,
      ) {
        setState(() {
          _uploadSpeed = stats['formattedUplinkSpeed'] as String;
          _downloadSpeed = stats['formattedDownlinkSpeed'] as String;
          _uploadTotal = stats['formattedUplinkTotal'] as String;
          _downloadTotal = stats['formattedDownlinkTotal'] as String;
          _sessionTotal = stats['formattedSessionTotal'] as String;
          _connectionsIn = stats['connectionsIn'] as int;
          _connectionsOut = stats['connectionsOut'] as int;
        });
      });

      // Listen for log messages
      _logSubscription = _flutterSingboxPlugin.onLogMessage.listen((event) {
        log('Log event: $event');
        if (event['type'] == 'clear') {
          setState(() {
            _logs.clear();
          });
        } else if (event['type'] == 'log' && event['message'] != null) {
          setState(() {
            _logs.add(event['message'] as String);
            // Keep only last 200 logs
            while (_logs.length > 200) {
              _logs.removeAt(0);
            }
          });
          // Auto-scroll to bottom
          if (_logScrollController.hasClients) {
            Future.delayed(const Duration(milliseconds: 50), () {
              _logScrollController.animateTo(
                _logScrollController.position.maxScrollExtent,
                duration: const Duration(milliseconds: 100),
                curve: Curves.easeOut,
              );
            });
          }
        }
      });

      // Update state with initial values
      if (mounted) {
        setState(() {
          _platformVersion = "16";
          _vpnStatus = vpnStatus;
          _configController.text = _formatJson(sampleConfig);
        });
      }
    } on PlatformException catch (e) {
      debugPrint('Error initializing plugin: ${e.message}');
    }
  }

  // Format JSON string for better readability
  String _formatJson(String jsonStr) {
    try {
      var jsonObj = json.decode(jsonStr);
      return const JsonEncoder.withIndent('  ').convert(jsonObj);
    } catch (e) {
      return jsonStr;
    }
  }

  // Save configuration
  Future<void> _saveConfig() async {
    try {
      final success = await _flutterSingboxPlugin.saveConfig(
        _configController.text,
      );
      if (success) {
        // ScaffoldMessenger.of(context).showSnackBar(
        //   const SnackBar(content: Text('Configuration saved successfully'))
        // );
      } else {
        log('Failed to save configuration');
        // ScaffoldMessenger.of(context).showSnackBar(
        //   const SnackBar(content: Text('Failed to save configuration')),
        // );
      }
    } on PlatformException catch (e) {
      log('Error: ${e.message}');
    }
  }

  // Start VPN connection
  Future<void> _startVPN() async {
    try {
      await _saveConfig();
      final success = await _flutterSingboxPlugin.startVPN();
      if (!success) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('Failed to start VPN')));
      }
    } on PlatformException catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Error: ${e.message}')));
    }
  }

  // Stop VPN connection
  Future<void> _stopVPN() async {
    try {
      final success = await _flutterSingboxPlugin.stopVPN();
      if (!success) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('Failed to stop VPN')));
      }
    } on PlatformException catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Error: ${e.message}')));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SingBox VPN Plugin Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Platform info
            Text('Running on: $_platformVersion'),
            const SizedBox(height: 20),

            // Status card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'VPN Status: $_vpnStatus',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 10),

                    // VPN control buttons
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                      children: [
                        ElevatedButton(
                          onPressed: _vpnStatus == VPNStatus.STOPPED
                              ? _startVPN
                              : null,
                          child: const Text('Connect VPN'),
                        ),
                        ElevatedButton(
                          onPressed: _vpnStatus == VPNStatus.STARTED
                              ? _stopVPN
                              : null,
                          child: const Text('Disconnect VPN'),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 20),

            // Traffic stats - only show when connected
            if (_vpnStatus == VPNStatus.STARTED)
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Traffic Statistics',
                        style: Theme.of(context).textTheme.titleMedium,
                      ),
                      const SizedBox(height: 10),

                      // Session total (highlighted)
                      Container(
                        width: double.infinity,
                        padding: const EdgeInsets.all(10),
                        decoration: BoxDecoration(
                          color: Theme.of(context).colorScheme.primaryContainer,
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: Center(
                          child: Text(
                            'Session Data Usage: $_sessionTotal',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              color: Theme.of(
                                context,
                              ).colorScheme.onPrimaryContainer,
                            ),
                          ),
                        ),
                      ),
                      const SizedBox(height: 15),

                      // Speeds
                      _buildStatRow('Upload Speed:', _uploadSpeed),
                      _buildStatRow('Download Speed:', _downloadSpeed),
                      const Divider(),

                      // Totals
                      _buildStatRow('Upload Total:', _uploadTotal),
                      _buildStatRow('Download Total:', _downloadTotal),
                      const Divider(),

                      // Connections
                      _buildStatRow(
                        'Connections In:',
                        _connectionsIn.toString(),
                      ),
                      _buildStatRow(
                        'Connections Out:',
                        _connectionsOut.toString(),
                      ),
                    ],
                  ),
                ),
              ),
            const SizedBox(height: 20),

            // Logs Card - show when VPN is connected or when we have logs
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          'Service Logs',
                          style: Theme.of(context).textTheme.titleMedium,
                        ),
                        Row(
                          children: [
                            IconButton(
                              icon: Icon(
                                _showLogs
                                    ? Icons.visibility_off
                                    : Icons.visibility,
                              ),
                              onPressed: () {
                                setState(() {
                                  _showLogs = !_showLogs;
                                });
                              },
                              tooltip: _showLogs ? 'Hide Logs' : 'Show Logs',
                            ),
                            IconButton(
                              icon: const Icon(Icons.delete),
                              onPressed: () async {
                                await _flutterSingboxPlugin.clearLogs();
                                setState(() {
                                  _logs.clear();
                                });
                              },
                              tooltip: 'Clear Logs',
                            ),
                          ],
                        ),
                      ],
                    ),
                    if (_showLogs) ...[
                      const SizedBox(height: 10),
                      Container(
                        height: 300,
                        decoration: BoxDecoration(
                          color: Colors.black87,
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: _logs.isEmpty
                            ? const Center(
                                child: Text(
                                  'No logs yet. Connect VPN to see logs.',
                                  style: TextStyle(color: Colors.white54),
                                ),
                              )
                            : ListView.builder(
                                controller: _logScrollController,
                                padding: const EdgeInsets.all(8),
                                itemCount: _logs.length,
                                itemBuilder: (context, index) {
                                  final log = _logs[index];
                                  // Color logs based on content
                                  Color logColor = Colors.white70;
                                  if (log.contains('error') ||
                                      log.contains('Error') ||
                                      log.contains('ERROR')) {
                                    logColor = Colors.red;
                                  } else if (log.contains('warn') ||
                                      log.contains('Warn') ||
                                      log.contains('WARN')) {
                                    logColor = Colors.orange;
                                  } else if (log.contains('info') ||
                                      log.contains('Info') ||
                                      log.contains('INFO')) {
                                    logColor = Colors.lightBlue;
                                  }
                                  return Padding(
                                    padding: const EdgeInsets.symmetric(
                                      vertical: 1,
                                    ),
                                    child: Text(
                                      log,
                                      style: TextStyle(
                                        color: logColor,
                                        fontFamily: 'monospace',
                                        fontSize: 11,
                                      ),
                                    ),
                                  );
                                },
                              ),
                      ),
                    ],
                  ],
                ),
              ),
            ),
            const SizedBox(height: 20),

            // Configuration
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'SingBox Configuration',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 10),
                    TextField(
                      controller: _configController,
                      maxLines: 8,
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        hintText: 'Enter SingBox JSON configuration',
                      ),
                    ),
                    const SizedBox(height: 10),
                    Center(
                      child: ElevatedButton(
                        onPressed: _saveConfig,
                        child: const Text('Save Configuration'),
                      ),
                    ),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 20),

            // Per-App Tunneling Configuration Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Per-App Tunneling',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 10),
                    const Text(
                      'Configure which apps use the VPN tunnel and which bypass it.',
                    ),
                    const SizedBox(height: 10),
                    Center(
                      child: ElevatedButton.icon(
                        onPressed: () {
                          Navigator.pushNamed(context, '/per_app_tunneling');
                        },
                        icon: const Icon(Icons.app_registration),
                        label: const Text('Configure Per-App Tunneling'),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  // Helper method to build stat rows
  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)),
        ],
      ),
    );
  }
}

String sampleConfig = '''
{
    "log": {
        "level": "debug",
        "disabled": false,
        "timestamp": true
    },
    "dns": {
        "final": "local-dns",
        "rules": [
            {
                "clash_mode": "Global",
                "server": "proxy-dns",
                "source_ip_cidr": [
                    "172.19.0.0/30"
                ]
            },
            {
                "server": "proxy-dns",
                "source_ip_cidr": [
                    "172.19.0.0/30"
                ]
            },
            {
                "clash_mode": "Direct",
                "server": "direct-dns"
            }
        ],
        "servers": [
            {
                "address": "tls://208.67.222.123",
                "address_resolver": "local-dns",
                "detour": "proxy",
                "tag": "proxy-dns"
            },
            {
                "address": "local",
                "detour": "direct",
                "tag": "local-dns"
            },
            {
                "address": "rcode://success",
                "tag": "block"
            },
            {
                "address": "local",
                "detour": "direct",
                "tag": "direct-dns"
            }
        ],
        "strategy": "prefer_ipv4"
    },
    "inbounds": [
        {
            "type": "tun",
            "tag": "tun-in",
            "interface_name": "tun0",
            "inet4_address": "172.19.0.1/30",
            "mtu": 1400,
            "auto_route": true,
            "strict_route": true,
            "stack": "mixed",
            "sniff": true,
            "sniff_override_destination": false,
            "domain_strategy": "ipv4_only"
        }
    ],
    "outbounds": [
        {
            "tag": "proxy",
            "type": "selector",
            "outbounds": [
                "chained"
            ]
        },
        {
            "type": "wireguard",
            "tag": "chained",
            "server": "205.198.86.198",
            "server_port": 443,
            "local_address": [
                "10.0.0.3/16"
            ],
            "private_key": "qI1BqT25ylggqfCaL7CncBDLxEo3+urBaop18Rx7oH0=",
            "peer_public_key": "tlcyqr3tGZwhZHJkGE51NMtzMw1zbeifCe699MEg1lU=",
            "mtu": 1280
        },
        {
            "tag": "dns-out",
            "type": "dns"
        },
        {
            "tag": "direct",
            "type": "direct"
        }
    ],
    "route": {
        "auto_detect_interface": true,
        "final": "proxy",
        "rules": [
            {
                "clash_mode": "Direct",
                "outbound": "direct"
            },
            {
                "clash_mode": "Global",
                "outbound": "proxy"
            },
            {
                "protocol": "dns",
                "outbound": "dns-out"
            }
        ]
    }
}
''';
3
likes
140
points
370
downloads

Publisher

verified publishertecclubx.com

Weekly Downloads

A Flutter plugin for Sing-Box VPN by TecClub. Provides complete VPN functionality with traffic stats, logging, and per-app tunneling.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_singbox_vpn

Packages that implement flutter_singbox_vpn