v2ray_myanmar 1.0.0 copy "v2ray_myanmar: ^1.0.0" to clipboard
v2ray_myanmar: ^1.0.0 copied to clipboard

V2Ray/Xray VPN plugin with PRO features (Myanmar edition)

example/lib/main.dart

// ignore_for_file: use_build_context_synchronously

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:v2ray_myanmar/v2ray_myanmar.dart';
import 'package:shared_preferences/shared_preferences.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter V2Ray',
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark,
        inputDecorationTheme: const InputDecorationTheme(
          border: OutlineInputBorder(),
        ),
      ),
      debugShowCheckedModeBanner: false,
      home: const Scaffold(body: HomePage()),
    );
  }
}

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

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

class _HomePageState extends State<HomePage> {
  var v2rayStatus = ValueNotifier<V2RayStatus>(V2RayStatus());
  late final V2rayMyanmar flutterV2ray = V2rayMyanmar(
    onStatusChanged: (status) {
      v2rayStatus.value = status;
    },
  );
  final config = TextEditingController();
  bool proxyOnly = false;
  final bypassSubnetController = TextEditingController();
  List<String> bypassSubnets = [];
  // PRO controls
  final allowedAppsController = TextEditingController();
  List<String> allowedApps = [];
  final routesController = TextEditingController();
  List<String> routes = [];
  final excludeRoutesController = TextEditingController();
  List<String> excludeRoutes = [];
  final dnsServersController = TextEditingController();
  List<String> dnsServers = [];
  bool enableIPv6 = true;
  bool enableFakeDNS = false;
  bool showSpeedInNotification = true;
  final fakeIpPoolController = TextEditingController(text: '198.18.0.0/15');
  final fakeIpPoolV6Controller = TextEditingController();
  bool preferDoH = false;
  final dohUrlController = TextEditingController(
    text: 'https://dns.google/dns-query',
  );
  final dohBootstrapController = TextEditingController(
    text: '8.8.8.8\n1.1.1.1',
  );
  final failoverThresholdMsController = TextEditingController(text: '5000');
  final healthCheckIntervalMsController = TextEditingController(text: '10000');
  String? coreVersion;
  String? currentMode;

  String remark = "Default Remark";
  final List<String> failoverConfigs = [];

  void connect() async {
    if (await flutterV2ray.requestPermission()) {
      try {
        await flutterV2ray.startV2Ray(
          remark: remark,
          config: config.text,
          showSpeedInNotification: showSpeedInNotification,
          failoverConfigs: failoverConfigs.isEmpty ? null : failoverConfigs,
          failoverThresholdMs: int.tryParse(
            failoverThresholdMsController.text.trim(),
          ),
          healthCheckIntervalMs: int.tryParse(
            healthCheckIntervalMsController.text.trim(),
          ),
          proxyOnly: proxyOnly,
          bypassSubnets: bypassSubnets,
          allowedApps: allowedApps.isEmpty ? null : allowedApps,
          routes: routes.isEmpty ? null : routes,
          excludeRoutes: excludeRoutes.isEmpty ? null : excludeRoutes,
          dnsServers: dnsServers.isEmpty ? null : dnsServers,
          enableWatchdog: true,
          preferDoH: preferDoH,
          dohUrl: preferDoH ? dohUrlController.text : null,
          dohBootstrap:
              preferDoH
                  ? dohBootstrapController.text
                      .split('\n')
                      .map((e) => e.trim())
                      .where((e) => e.isNotEmpty)
                      .toList()
                  : null,
          enableFakeDNS: enableFakeDNS,
          fakeIpPool: enableFakeDNS ? fakeIpPoolController.text.trim() : null,
          fakeIpPoolV6:
              enableFakeDNS ? fakeIpPoolV6Controller.text.trim() : null,
          enableIPv6: enableIPv6,
          notificationDisconnectButtonName: "DISCONNECT",
        );
      } catch (e) {
        if (mounted) {
          ScaffoldMessenger.of(
            context,
          ).showSnackBar(SnackBar(content: Text('Start failed: $e')));
        }
      }
    } else {
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('Permission Denied')));
      }
    }
  }

  Map<String, dynamic> _collectSettings() {
    return {
      'remark': remark,
      'config': config.text,
      'failoverConfigs': failoverConfigs,
      'failoverThresholdMs': int.tryParse(
        failoverThresholdMsController.text.trim(),
      ),
      'healthCheckIntervalMs': int.tryParse(
        healthCheckIntervalMsController.text.trim(),
      ),
      'proxyOnly': proxyOnly,
      'enableIPv6': enableIPv6,
      'enableFakeDNS': enableFakeDNS,
      'preferDoH': preferDoH,
      'fakeIpPool': fakeIpPoolController.text.trim(),
      'fakeIpPoolV6': fakeIpPoolV6Controller.text.trim(),
      'dohUrl': dohUrlController.text.trim(),
      'dohBootstrap': dohBootstrapController.text,
      'allowedApps': allowedApps,
      'routes': routes,
      'excludeRoutes': excludeRoutes,
      'dnsServers': dnsServers,
      'bypassSubnets': bypassSubnets,
    };
  }

  Future<void> _exportSettingsToClipboard() async {
    try {
      final jsonStr = const JsonEncoder.withIndent(
        '  ',
      ).convert(_collectSettings());
      await Clipboard.setData(ClipboardData(text: jsonStr));
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Exported settings to clipboard')),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Export failed: $e')));
      }
    }
  }

  Future<void> _importSettingsFromClipboard() async {
    if (!await Clipboard.hasStrings()) return;
    try {
      final raw = (await Clipboard.getData('text/plain'))?.text?.trim() ?? '';
      if (raw.isEmpty) return;
      final Map<String, dynamic> data = jsonDecode(raw);
      setState(() {
        remark = (data['remark'] ?? remark).toString();
        config.text = (data['config'] ?? config.text).toString();
        failoverConfigs
          ..clear()
          ..addAll(
            ((data['failoverConfigs'] as List?)
                    ?.map((e) => e.toString())
                    .toList()) ??
                [],
          );
        final ft = data['failoverThresholdMs'];
        final hi = data['healthCheckIntervalMs'];
        if (ft is int) failoverThresholdMsController.text = '$ft';
        if (hi is int) healthCheckIntervalMsController.text = '$hi';
        proxyOnly = (data['proxyOnly'] ?? proxyOnly) as bool;
        enableIPv6 = (data['enableIPv6'] ?? enableIPv6) as bool;
        enableFakeDNS = (data['enableFakeDNS'] ?? enableFakeDNS) as bool;
        preferDoH = (data['preferDoH'] ?? preferDoH) as bool;
        fakeIpPoolController.text =
            (data['fakeIpPool'] ?? fakeIpPoolController.text).toString();
        fakeIpPoolV6Controller.text = (data['fakeIpPoolV6'] ?? '').toString();
        dohUrlController.text =
            (data['dohUrl'] ?? dohUrlController.text).toString();
        dohBootstrapController.text =
            (data['dohBootstrap'] ?? dohBootstrapController.text).toString();
        allowedApps =
            ((data['allowedApps'] as List?)
                ?.map((e) => e.toString())
                .toList()) ??
            allowedApps;
        routes =
            ((data['routes'] as List?)?.map((e) => e.toString()).toList()) ??
            routes;
        excludeRoutes =
            ((data['excludeRoutes'] as List?)
                ?.map((e) => e.toString())
                .toList()) ??
            excludeRoutes;
        dnsServers =
            ((data['dnsServers'] as List?)
                ?.map((e) => e.toString())
                .toList()) ??
            dnsServers;
        bypassSubnets =
            ((data['bypassSubnets'] as List?)
                ?.map((e) => e.toString())
                .toList()) ??
            bypassSubnets;
      });
      await _persistFailoversAndSettings();
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Imported settings from clipboard')),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Import failed: $e')));
      }
    }
  }

  void importConfig() async {
    if (await Clipboard.hasStrings()) {
      try {
        final String link =
            (await Clipboard.getData('text/plain'))?.text?.trim() ?? '';
        final V2RayURL v2rayURL = V2rayMyanmar.parseFromURL(link);
        remark = v2rayURL.remark;
        config.text = v2rayURL.getFullConfiguration();
        await _persistFailoversAndSettings();
        if (mounted) {
          ScaffoldMessenger.of(
            context,
          ).showSnackBar(const SnackBar(content: Text('Success')));
        }
      } catch (error) {
        if (mounted) {
          ScaffoldMessenger.of(
            context,
          ).showSnackBar(SnackBar(content: Text('Error: $error')));
        }
      }
    }
  }

  void importFailoverFromClipboard() async {
    if (await Clipboard.hasStrings()) {
      try {
        final String link =
            (await Clipboard.getData('text/plain'))?.text?.trim() ?? '';
        final V2RayURL v2rayURL = V2rayMyanmar.parseFromURL(link);
        final String full = v2rayURL.getFullConfiguration();
        failoverConfigs.add(full);
        await _persistFailoversAndSettings();
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Added failover from clipboard')),
          );
        }
      } catch (error) {
        if (mounted) {
          ScaffoldMessenger.of(
            context,
          ).showSnackBar(SnackBar(content: Text('Error: $error')));
        }
      }
    }
  }

  void delay() async {
    late int delay;
    if (v2rayStatus.value.state == 'CONNECTED') {
      delay = await flutterV2ray.getConnectedServerDelay();
    } else {
      delay = await flutterV2ray.getServerDelay(config: config.text);
    }
    if (!mounted) return;
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text('${delay}ms')));
  }

  void bypassSubnet() {
    bypassSubnetController.text = bypassSubnets.join("\n");
    showDialog(
      context: context,
      builder:
          (context) => Dialog(
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Text('Subnets:', style: TextStyle(fontSize: 16)),
                  const SizedBox(height: 5),
                  TextFormField(
                    controller: bypassSubnetController,
                    maxLines: 5,
                    minLines: 5,
                  ),
                  const SizedBox(height: 5),
                  ElevatedButton(
                    onPressed: () async {
                      bypassSubnets = bypassSubnetController.text.trim().split(
                        '\n',
                      );
                      if (bypassSubnets.first.isEmpty) {
                        bypassSubnets = [];
                      }
                      await _persistFailoversAndSettings();
                      if (context.mounted) Navigator.of(context).pop();
                    },
                    child: const Text('Submit'),
                  ),
                ],
              ),
            ),
          ),
    );
  }

  void editAllowedApps() {
    allowedAppsController.text = allowedApps.join("\n");
    showDialog(
      context: context,
      builder:
          (context) => Dialog(
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Text('Allowed Apps (package names):'),
                  const SizedBox(height: 5),
                  TextFormField(
                    controller: allowedAppsController,
                    maxLines: 6,
                    minLines: 6,
                  ),
                  const SizedBox(height: 5),
                  ElevatedButton(
                    onPressed: () async {
                      allowedApps =
                          allowedAppsController.text
                              .split('\n')
                              .map((e) => e.trim())
                              .where((e) => e.isNotEmpty)
                              .toList();
                      await _persistFailoversAndSettings();
                      if (context.mounted) Navigator.of(context).pop();
                    },
                    child: const Text('Submit'),
                  ),
                ],
              ),
            ),
          ),
    );
  }

  void editRoutes() {
    routesController.text = routes.join("\n");
    excludeRoutesController.text = excludeRoutes.join("\n");
    showDialog(
      context: context,
      builder:
          (context) => Dialog(
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Text('Routes (CIDRs, e.g. 0.0.0.0/0, ::/0):'),
                  const SizedBox(height: 5),
                  TextFormField(
                    controller: routesController,
                    maxLines: 5,
                    minLines: 5,
                  ),
                  const SizedBox(height: 10),
                  const Text('Exclude Routes (CIDRs to skip):'),
                  const SizedBox(height: 5),
                  TextFormField(
                    controller: excludeRoutesController,
                    maxLines: 3,
                    minLines: 3,
                  ),
                  const SizedBox(height: 5),
                  ElevatedButton(
                    onPressed: () async {
                      routes =
                          routesController.text
                              .split('\n')
                              .map((e) => e.trim())
                              .where((e) => e.isNotEmpty)
                              .toList();
                      excludeRoutes =
                          excludeRoutesController.text
                              .split('\n')
                              .map((e) => e.trim())
                              .where((e) => e.isNotEmpty)
                              .toList();
                      await _persistFailoversAndSettings();
                      if (context.mounted) Navigator.of(context).pop();
                    },
                    child: const Text('Submit'),
                  ),
                ],
              ),
            ),
          ),
    );
  }

  void editDnsServers() {
    dnsServersController.text = dnsServers.join("\n");
    showDialog(
      context: context,
      builder:
          (context) => Dialog(
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Text('DNS Servers (one per line):'),
                  const SizedBox(height: 5),
                  TextFormField(
                    controller: dnsServersController,
                    maxLines: 4,
                    minLines: 4,
                  ),
                  const SizedBox(height: 5),
                  ElevatedButton(
                    onPressed: () async {
                      dnsServers =
                          dnsServersController.text
                              .split('\n')
                              .map((e) => e.trim())
                              .where((e) => e.isNotEmpty)
                              .toList();
                      await _persistFailoversAndSettings();
                      if (context.mounted) Navigator.of(context).pop();
                    },
                    child: const Text('Submit'),
                  ),
                ],
              ),
            ),
          ),
    );
  }

  @override
  void initState() {
    super.initState();
    _loadPersisted();
    flutterV2ray
        .initializeV2Ray(
          notificationIconResourceType: "mipmap",
          notificationIconResourceName: "ic_launcher",
        )
        .then((value) async {
          coreVersion = await flutterV2ray.getCoreVersion();
          currentMode = await flutterV2ray.getMode();
          setState(() {});
        });
  }

  Future<void> _loadPersisted() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final list = prefs.getStringList('failoverConfigs') ?? [];
      setState(() {
        failoverConfigs.clear();
        failoverConfigs.addAll(list);
        final ft = prefs.getInt('failoverThresholdMs');
        final hi = prefs.getInt('healthCheckIntervalMs');
        if (ft != null) failoverThresholdMsController.text = '$ft';
        if (hi != null) healthCheckIntervalMsController.text = '$hi';
        // Toggles
        enableIPv6 = prefs.getBool('enableIPv6') ?? enableIPv6;
        enableFakeDNS = prefs.getBool('enableFakeDNS') ?? enableFakeDNS;
        preferDoH = prefs.getBool('preferDoH') ?? preferDoH;
        proxyOnly = prefs.getBool('proxyOnly') ?? proxyOnly;
        // Texts
        fakeIpPoolController.text =
            prefs.getString('fakeIpPool') ?? fakeIpPoolController.text;
        fakeIpPoolV6Controller.text = prefs.getString('fakeIpPoolV6') ?? '';
        dohUrlController.text =
            prefs.getString('dohUrl') ?? dohUrlController.text;
        dohBootstrapController.text =
            prefs.getString('dohBootstrap') ?? dohBootstrapController.text;
        // Lists
        allowedApps = prefs.getStringList('allowedApps') ?? allowedApps;
        routes = prefs.getStringList('routes') ?? routes;
        excludeRoutes = prefs.getStringList('excludeRoutes') ?? excludeRoutes;
        dnsServers = prefs.getStringList('dnsServers') ?? dnsServers;
        bypassSubnets = prefs.getStringList('bypassSubnets') ?? bypassSubnets;
      });
    } catch (_) {}
  }

  Future<void> _persistFailoversAndSettings() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setStringList('failoverConfigs', failoverConfigs);
      final ft = int.tryParse(failoverThresholdMsController.text.trim());
      final hi = int.tryParse(healthCheckIntervalMsController.text.trim());
      if (ft != null) await prefs.setInt('failoverThresholdMs', ft);
      if (hi != null) await prefs.setInt('healthCheckIntervalMs', hi);
      // Toggles
      await prefs.setBool('enableIPv6', enableIPv6);
      await prefs.setBool('enableFakeDNS', enableFakeDNS);
      await prefs.setBool('preferDoH', preferDoH);
      await prefs.setBool('proxyOnly', proxyOnly);
      // Texts
      await prefs.setString('fakeIpPool', fakeIpPoolController.text.trim());
      await prefs.setString('fakeIpPoolV6', fakeIpPoolV6Controller.text.trim());
      await prefs.setString('dohUrl', dohUrlController.text.trim());
      await prefs.setString('dohBootstrap', dohBootstrapController.text);
      // Lists
      await prefs.setStringList('allowedApps', allowedApps);
      await prefs.setStringList('routes', routes);
      await prefs.setStringList('excludeRoutes', excludeRoutes);
      await prefs.setStringList('dnsServers', dnsServers);
      await prefs.setStringList('bypassSubnets', bypassSubnets);
    } catch (_) {}
  }

  @override
  void dispose() {
    config.dispose();
    bypassSubnetController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const SizedBox(height: 5),
              const Text(
                'V2Ray Config (json):',
                style: TextStyle(fontSize: 16),
              ),
              const SizedBox(height: 5),
              TextFormField(controller: config, maxLines: 10, minLines: 10),
              const SizedBox(height: 10),
              ValueListenableBuilder(
                valueListenable: v2rayStatus,
                builder: (context, value, child) {
                  return Column(
                    children: [
                      Text(value.state),
                      const SizedBox(height: 10),
                      Text(value.duration),
                      const SizedBox(height: 10),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          const Text('Speed:'),
                          const SizedBox(width: 10),
                          Text(value.uploadSpeed.toString()),
                          const Text('↑'),
                          const SizedBox(width: 10),
                          Text(value.downloadSpeed.toString()),
                          const Text('↓'),
                        ],
                      ),
                      const SizedBox(height: 10),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          const Text('Traffic:'),
                          const SizedBox(width: 10),
                          Text(value.upload.toString()),
                          const Text('↑'),
                          const SizedBox(width: 10),
                          Text(value.download.toString()),
                          const Text('↓'),
                        ],
                      ),
                      const SizedBox(height: 10),
                      Text('Core Version: $coreVersion'),
                      const SizedBox(height: 6),
                      Text('Mode: ${currentMode ?? '-'}'),
                    ],
                  );
                },
              ),
              const SizedBox(height: 10),
              Padding(
                padding: const EdgeInsets.all(5.0),
                child: Wrap(
                  spacing: 5,
                  runSpacing: 5,
                  children: [
                    ElevatedButton(
                      onPressed: connect,
                      child: const Text('Connect'),
                    ),
                    ElevatedButton(
                      onPressed: () => flutterV2ray.stopV2Ray(),
                      child: const Text('Disconnect'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        setState(() => proxyOnly = !proxyOnly);
                        await _persistFailoversAndSettings();
                      },
                      child: Text(proxyOnly ? 'Proxy Only' : 'VPN Mode'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        setState(() => enableIPv6 = !enableIPv6);
                        await _persistFailoversAndSettings();
                      },
                      child: Text(enableIPv6 ? 'IPv6: ON' : 'IPv6: OFF'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        setState(() => enableFakeDNS = !enableFakeDNS);
                        await _persistFailoversAndSettings();
                      },
                      child: Text(
                        enableFakeDNS ? 'FakeDNS: ON' : 'FakeDNS: OFF',
                      ),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        setState(() => preferDoH = !preferDoH);
                        await _persistFailoversAndSettings();
                      },
                      child: Text(preferDoH ? 'DoH: ON' : 'DoH: OFF'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        setState(
                          () =>
                              showSpeedInNotification =
                                  !showSpeedInNotification,
                        );
                      },
                      child: Text(
                        showSpeedInNotification
                            ? 'Notif Speed: ON'
                            : 'Notif Speed: OFF',
                      ),
                    ),
                    ElevatedButton(
                      onPressed: importConfig,
                      child: const Text(
                        'Import from v2ray share link (clipboard)',
                      ),
                    ),
                    ElevatedButton(
                      onPressed: importFailoverFromClipboard,
                      child: Text(
                        'Add Failover (now: ${failoverConfigs.length})',
                      ),
                    ),
                    ElevatedButton(
                      onPressed: delay,
                      child: const Text('Server Delay'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        final state = await flutterV2ray.getState();
                        final mode = await flutterV2ray.getMode();
                        if (!mounted) return;
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('State: $state\nMode: $mode')),
                        );
                      },
                      child: const Text('Show State/Mode'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        try {
                          final current =
                              (await flutterV2ray.getMode()).toUpperCase();
                          final next =
                              current.contains('PROXY')
                                  ? 'VPN_TUN'
                                  : 'PROXY_ONLY';
                          final applied = await flutterV2ray.setMode(next);
                          setState(() => currentMode = applied);
                          if (!mounted) return;
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text('Switched mode to: $applied'),
                            ),
                          );
                        } catch (e) {
                          if (!mounted) return;
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text('Mode switch failed: $e')),
                          );
                        }
                      },
                      child: const Text('Toggle Mode'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        final ports = await flutterV2ray.getLocalProxyPorts();
                        if (!mounted) return;
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text(
                              'SOCKS5: ${ports['localSocksPort'] ?? '-'}  HTTP: ${ports['localHttpPort'] ?? '-'}',
                            ),
                          ),
                        );
                      },
                      child: const Text('Show Local Ports'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        final granted = await flutterV2ray.checkPermission();
                        if (!mounted) return;
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text(
                              'VPN permission: ${granted ? 'GRANTED' : 'NOT GRANTED'}',
                            ),
                          ),
                        );
                      },
                      child: const Text('Check VPN Permission'),
                    ),
                    ElevatedButton(
                      onPressed: _exportSettingsToClipboard,
                      child: const Text('Export Settings'),
                    ),
                    ElevatedButton(
                      onPressed: _importSettingsFromClipboard,
                      child: const Text('Import Settings (clipboard)'),
                    ),
                    ElevatedButton(
                      onPressed: bypassSubnet,
                      child: const Text('Bypass Subnet'),
                    ),
                    ElevatedButton(
                      onPressed: editAllowedApps,
                      child: const Text('Allowed Apps'),
                    ),
                    ElevatedButton(
                      onPressed: editRoutes,
                      child: const Text('Routes / Exclude'),
                    ),
                    ElevatedButton(
                      onPressed: editDnsServers,
                      child: const Text('DNS Servers'),
                    ),
                    ElevatedButton(
                      onPressed: () {
                        showDialog(
                          context: context,
                          builder:
                              (context) => Dialog(
                                child: Padding(
                                  padding: const EdgeInsets.all(8.0),
                                  child: Column(
                                    mainAxisSize: MainAxisSize.min,
                                    crossAxisAlignment:
                                        CrossAxisAlignment.start,
                                    children: [
                                      const Text('Health/Failover Settings'),
                                      const SizedBox(height: 8),
                                      TextFormField(
                                        controller:
                                            failoverThresholdMsController,
                                        keyboardType: TextInputType.number,
                                        decoration: const InputDecoration(
                                          labelText: 'Failover threshold (ms)',
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      TextFormField(
                                        controller:
                                            healthCheckIntervalMsController,
                                        keyboardType: TextInputType.number,
                                        decoration: const InputDecoration(
                                          labelText:
                                              'Health check interval (ms)',
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      Align(
                                        alignment: Alignment.centerRight,
                                        child: Row(
                                          mainAxisSize: MainAxisSize.min,
                                          children: [
                                            TextButton(
                                              onPressed: () async {
                                                await _persistFailoversAndSettings();
                                                if (context.mounted) {
                                                  Navigator.of(context).pop();
                                                }
                                              },
                                              child: const Text('Save'),
                                            ),
                                            const SizedBox(width: 8),
                                            ElevatedButton(
                                              onPressed:
                                                  () =>
                                                      Navigator.of(
                                                        context,
                                                      ).pop(),
                                              child: const Text('Close'),
                                            ),
                                          ],
                                        ),
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                        );
                      },
                      child: const Text('Failover/Health Settings'),
                    ),
                    ElevatedButton(
                      onPressed: () {
                        showDialog(
                          context: context,
                          builder:
                              (context) => Dialog(
                                child: Padding(
                                  padding: const EdgeInsets.all(8.0),
                                  child: Column(
                                    mainAxisSize: MainAxisSize.min,
                                    crossAxisAlignment:
                                        CrossAxisAlignment.start,
                                    children: [
                                      const Text('Failover Configs'),
                                      const SizedBox(height: 8),
                                      SizedBox(
                                        width: 400,
                                        height: 240,
                                        child: ListView.builder(
                                          itemCount: failoverConfigs.length,
                                          itemBuilder: (context, index) {
                                            final preview =
                                                failoverConfigs[index];
                                            return ListTile(
                                              dense: true,
                                              title: Text('Failover #$index'),
                                              subtitle: Text(
                                                preview.length > 80
                                                    ? '${preview.substring(0, 80)}...'
                                                    : preview,
                                              ),
                                              trailing: IconButton(
                                                icon: const Icon(Icons.delete),
                                                onPressed: () {
                                                  setState(() {
                                                    failoverConfigs.removeAt(
                                                      index,
                                                    );
                                                  });
                                                  _persistFailoversAndSettings();
                                                  Navigator.of(context).pop();
                                                },
                                              ),
                                            );
                                          },
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      Row(
                                        mainAxisAlignment:
                                            MainAxisAlignment.spaceBetween,
                                        children: [
                                          Text(
                                            'Total: ${failoverConfigs.length}',
                                          ),
                                          ElevatedButton(
                                            onPressed: () async {
                                              importFailoverFromClipboard();
                                              if (context.mounted) {
                                                Navigator.of(context).pop();
                                              }
                                            },
                                            child: const Text(
                                              'Add from Clipboard',
                                            ),
                                          ),
                                        ],
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                        );
                      },
                      child: const Text('Manage Failovers'),
                    ),
                    ElevatedButton(
                      onPressed: () async {
                        final diag = await flutterV2ray.getDiagnostics();
                        if (!mounted) return;
                        showDialog(
                          context: context,
                          builder:
                              (context) => Dialog(
                                child: Padding(
                                  padding: const EdgeInsets.all(8.0),
                                  child: Column(
                                    mainAxisSize: MainAxisSize.min,
                                    crossAxisAlignment:
                                        CrossAxisAlignment.start,
                                    children: [
                                      const Text(
                                        'Diagnostics',
                                        style: TextStyle(
                                          fontWeight: FontWeight.bold,
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      SingleChildScrollView(
                                        child: Text(
                                          const JsonEncoder.withIndent(
                                            '  ',
                                          ).convert(diag),
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      Align(
                                        alignment: Alignment.centerRight,
                                        child: Row(
                                          mainAxisSize: MainAxisSize.min,
                                          children: [
                                            TextButton(
                                              onPressed: () async {
                                                await Clipboard.setData(
                                                  ClipboardData(
                                                    text:
                                                        const JsonEncoder.withIndent(
                                                          '  ',
                                                        ).convert(diag),
                                                  ),
                                                );
                                                if (context.mounted) {
                                                  Navigator.of(context).pop();
                                                }
                                              },
                                              child: const Text('Copy'),
                                            ),
                                            const SizedBox(width: 8),
                                            ElevatedButton(
                                              onPressed:
                                                  () =>
                                                      Navigator.of(
                                                        context,
                                                      ).pop(),
                                              child: const Text('Close'),
                                            ),
                                          ],
                                        ),
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                        );
                      },
                      child: const Text('Diagnostics'),
                    ),
                    ElevatedButton(
                      onPressed: () {
                        showDialog(
                          context: context,
                          builder:
                              (context) => Dialog(
                                child: Padding(
                                  padding: const EdgeInsets.all(8.0),
                                  child: Column(
                                    mainAxisSize: MainAxisSize.min,
                                    crossAxisAlignment:
                                        CrossAxisAlignment.start,
                                    children: [
                                      const Text(
                                        'FakeDNS Pools (IPv4 / IPv6 optional):',
                                      ),
                                      const SizedBox(height: 8),
                                      TextFormField(
                                        controller: fakeIpPoolController,
                                        decoration: const InputDecoration(
                                          labelText:
                                              'IPv4 Pool (e.g., 198.18.0.0/15)',
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      TextFormField(
                                        controller: fakeIpPoolV6Controller,
                                        decoration: const InputDecoration(
                                          labelText: 'IPv6 Pool (optional)',
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      const Text('DoH Settings:'),
                                      const SizedBox(height: 8),
                                      TextFormField(
                                        controller: dohUrlController,
                                        decoration: const InputDecoration(
                                          labelText: 'DoH URL',
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      TextFormField(
                                        controller: dohBootstrapController,
                                        maxLines: 3,
                                        minLines: 3,
                                        decoration: const InputDecoration(
                                          labelText:
                                              'DoH Bootstrap IPs (one per line)',
                                        ),
                                      ),
                                      const SizedBox(height: 8),
                                      Align(
                                        alignment: Alignment.centerRight,
                                        child: Row(
                                          mainAxisSize: MainAxisSize.min,
                                          children: [
                                            TextButton(
                                              onPressed: () async {
                                                await _persistFailoversAndSettings();
                                                if (context.mounted) {
                                                  Navigator.of(context).pop();
                                                }
                                              },
                                              child: const Text('Save'),
                                            ),
                                            const SizedBox(width: 8),
                                            ElevatedButton(
                                              onPressed:
                                                  () =>
                                                      Navigator.of(
                                                        context,
                                                      ).pop(),
                                              child: const Text('Close'),
                                            ),
                                          ],
                                        ),
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                        );
                      },
                      child: const Text('FakeDNS / DoH Settings'),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}