v2ray_myanmar 1.0.0
v2ray_myanmar: ^1.0.0 copied to clipboard
V2Ray/Xray VPN plugin with PRO features (Myanmar edition)
// 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'),
),
],
),
),
],
),
),
),
);
}
}