feat: Initial commit

This commit is contained in:
cialloo
2024-07-20 12:45:47 +08:00
commit 0ae1adeb32
40 changed files with 3400 additions and 0 deletions

64
lib/about_page.dart Normal file
View File

@@ -0,0 +1,64 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPage extends StatelessWidget {
const AboutPage({super.key});
@override
Widget build(BuildContext context) {
TextStyle defaultStyle = const TextStyle(fontSize: 16.0);
TextStyle linkStyle = const TextStyle(color: Colors.blue);
return Center(
child: SelectableText.rich(
TextSpan(
style: defaultStyle,
children: <TextSpan>[
const TextSpan(
text: '项目基于 n2n , 一款优秀的跨平台开源p2p VPN软件\n',
),
const TextSpan(
text: 'winui_n2n软件开源地址 ',
),
TextSpan(
text: 'https://github.com/moemoequte/winui_n2n\n',
style: linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(
Uri.parse('https://github.com/moemoequte/winui_n2n'));
},
),
const TextSpan(
text: '分发和使用请遵循 GPL-3.0 license 开源协议\n\n',
),
const TextSpan(
text: '作者信息\n',
),
const TextSpan(text: '网站 '),
TextSpan(
text: 'https://www.cialloo.com\n',
style: linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(Uri.parse('https://www.cialloo.com'));
},
),
const TextSpan(text: '邮箱 '),
TextSpan(
text: 'admin@cialloo.com',
style: linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(Uri(
scheme: 'mailto',
path: 'admin@cialloo.com',
));
},
),
],
),
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'dart:ui';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:winui_n2n/edge_state.dart';
import 'package:winui_n2n/home_page.dart';
class ApplicationExitControl extends StatefulWidget {
const ApplicationExitControl({super.key});
@override
State<ApplicationExitControl> createState() => _ApplicationExitControlState();
}
class _ApplicationExitControlState extends State<ApplicationExitControl> {
late final AppLifecycleListener _listener;
@override
void initState() {
super.initState();
_listener = AppLifecycleListener(
onExitRequested: _handleExitRequest,
);
}
@override
void dispose() {
_listener.dispose();
super.dispose();
}
Future<AppExitResponse> _handleExitRequest() async {
if (EdgeState.instance.isRunning && EdgeState.instance.process != null) {
if (EdgeState.instance.process!.kill()) {
EdgeState.instance.isRunning = false;
EdgeState.instance.process = null;
} else {
// TODO: Handle abnormal close.
return AppExitResponse.cancel;
}
}
// Uninstall tap device before exit
final findTapResult = await Process.run(
"./tools/driver/devcon.exe",
["hwids", "tap0901"],
);
if (!findTapResult.stdout
.toString()
.contains('No matching devices found.')) {
// Unstall tap device
await Process.run(
"./tools/driver/devcon.exe",
[
"remove",
"tap0901",
],
);
}
return AppExitResponse.exit;
}
@override
Widget build(BuildContext context) {
return const HomePage();
}
}

442
lib/control_page.dart Normal file
View File

@@ -0,0 +1,442 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:winui_n2n/edge_state.dart';
import 'package:winui_n2n/saved_connection.dart';
import 'package:winui_n2n/shared_pref_singleton.dart';
class ControlPage extends StatefulWidget {
const ControlPage({super.key});
@override
State<ControlPage> createState() => _ControlPageState();
}
class _ControlPageState extends State<ControlPage> {
late TextEditingController _supernodeController;
late TextEditingController _communityController;
late TextEditingController _keyController;
late TextEditingController _selfAddressController;
late TextEditingController _configNameController;
bool _edgeConnecting = false;
@override
void initState() {
super.initState();
_supernodeController = TextEditingController();
_communityController = TextEditingController();
_keyController = TextEditingController();
_selfAddressController = TextEditingController();
_configNameController = TextEditingController();
}
@override
void dispose() {
_supernodeController.dispose();
_communityController.dispose();
_keyController.dispose();
_selfAddressController.dispose();
_configNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 300,
child: TextField(
controller: _supernodeController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '主服务器',
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 300,
child: TextField(
controller: _communityController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '网络社区',
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 300,
child: TextField(
controller: _keyController,
obscureText: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '社区密码',
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 300,
child: TextField(
controller: _selfAddressController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '我的地址',
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () => showDialog<String>(
context: context,
builder: (BuildContext context) => Dialog(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 350,
child: TextField(
controller: _configNameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '给这个配置备注一个名字',
),
),
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
List<dynamic> nodeList = jsonDecode(
SharedPrefSingleton().savedConnection);
List<SavedConnection> nodes = nodeList
.cast<Map<String, dynamic>>()
.map((nodeData) =>
SavedConnection.fromJson(nodeData))
.toList();
nodes.add(SavedConnection(
_configNameController.text,
_supernodeController.text,
_communityController.text,
_keyController.text,
_selfAddressController.text,
));
List<Map<String, dynamic>> nodeMaps = nodes
.map((node) => node.toJson())
.toList();
String jsonString = jsonEncode(nodeMaps);
SharedPrefSingleton()
.setSavedConnection(jsonString);
Navigator.pop(context);
return;
},
child: const Text("保存")),
TextButton(
onPressed: () {
Navigator.pop(context);
return;
},
child: const Text('取消'),
),
],
),
],
),
),
),
),
child: const Text('保存配置'),
),
ElevatedButton(
onPressed: _edgeConnecting
? null
: () async {
if (EdgeState.instance.isRunning) {
// Turn on firewall
if (SharedPrefSingleton().autoFirewall) {
await Process.run(
"netsh.exe",
[
"advfirewall",
"set",
"allprofiles",
"state",
"on",
],
);
}
// Kill the edge process
if (EdgeState.instance.process != null) {
if (EdgeState.instance.process!.kill()) {
EdgeState.instance.isRunning = false;
EdgeState.instance.process = null;
setState(() {});
}
}
} else if (!EdgeState.instance.isRunning) {
// Block user click
setState(() {
_edgeConnecting = true;
});
// Check for tap device
final findTapResult = await Process.run(
"./tools/driver/devcon.exe",
["hwids", "tap0901"],
);
if (findTapResult.stdout
.toString()
.contains('No matching devices found.')) {
// Install tap device
EdgeState.instance.logger
.addLog("Tap device not found, installing tap");
debugPrint("Tap device not found, installing tap");
final installTapResult = await Process.run(
"./tools/driver/devcon.exe",
[
"install",
"./tools/driver/OemVista.inf",
"tap0901",
],
);
if (installTapResult.stdout.toString().contains(
"Tap driver installed successfully.") ||
installTapResult.stdout.toString().contains(
"Drivers installed successfully.")) {
EdgeState.instance.logger
.addLog("Tap driver installed successfully.");
debugPrint("Tap driver installed successfully.");
} else {
EdgeState.instance.logger
.addLog("Tap driver install failed.");
debugPrint("Tap driver install failed.");
}
} else {
// Ignore.
EdgeState.instance.logger
.addLog("Tap device already installed");
debugPrint("Tap device already installed");
}
// Close firewall
if (SharedPrefSingleton().autoFirewall) {
await Process.run(
"netsh.exe",
[
"advfirewall",
"set",
"allprofiles",
"state",
"off",
],
);
}
// Start the edge
// edge -c <community> -k <communityKey> -a <selfAddress> -r -l <remoteSuperNodeAddress>
Process.start(
"./tools/edge/edge.exe",
[
"-l",
_supernodeController.text,
"-c",
_communityController.text,
"-k",
_keyController.text,
"-a",
_selfAddressController.text,
"-r",
],
).then((process) async {
EdgeState.instance.isRunning = true;
EdgeState.instance.process = process;
EdgeState.instance.logger
.addLog("edge.exe starting");
debugPrint("edge.exe starting");
setState(() {
_edgeConnecting = false;
});
process.stdout
.transform(const SystemEncoding().decoder)
.listen((data) {
debugPrint('stdout: $data');
EdgeState.instance.logger.addLog(data);
});
await process.exitCode;
EdgeState.instance.isRunning = false;
EdgeState.instance.process = null;
EdgeState.instance.logger
.addLog("edge.exe closing");
debugPrint("edge.exe closing");
if (!mounted) return;
setState(() {});
});
}
},
child: EdgeState.instance.isRunning
? const Text("断开连接")
: const Text("开始连接"),
),
TextButton(
onPressed: () {
SavedConnection? selectedSavedConnection;
showDialog<String>(
context: context,
builder: (BuildContext context) => Dialog(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Builder(builder: (context) {
String jsonString =
SharedPrefSingleton().savedConnection;
List<dynamic> nodeList = jsonDecode(jsonString);
List<SavedConnection> nodes = nodeList
.cast<Map<String, dynamic>>()
.map((nodeData) =>
SavedConnection.fromJson(nodeData))
.toList();
return StatefulBuilder(builder:
(BuildContext context,
StateSetter setState) {
return DropdownButton(
value: selectedSavedConnection,
items: nodes.map<
DropdownMenuItem<
SavedConnection>>(
(SavedConnection value) {
return DropdownMenuItem<
SavedConnection>(
value: value,
child: Text(value.name),
);
}).toList(),
onChanged: (value) {
setState(
() {
selectedSavedConnection = value!;
},
);
});
});
}),
const SizedBox(height: 15),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
if (selectedSavedConnection != null) {
_supernodeController.text =
selectedSavedConnection!.supernode;
_communityController.text =
selectedSavedConnection!.community;
_keyController.text =
selectedSavedConnection!
.communityKey;
_selfAddressController.text =
selectedSavedConnection!
.selfAddress;
}
Navigator.pop(context);
return;
},
child: const Text('使用'),
),
TextButton(
onPressed: () {
if (selectedSavedConnection == null) {
Navigator.pop(context);
return;
}
String jsonString =
SharedPrefSingleton().savedConnection;
List<dynamic> nodeList =
jsonDecode(jsonString);
List<SavedConnection> nodes = nodeList
.cast<Map<String, dynamic>>()
.map((nodeData) =>
SavedConnection.fromJson(
nodeData))
.toList();
nodes.removeWhere(
(element) {
return element.name ==
selectedSavedConnection!.name;
},
);
List<Map<String, dynamic>> nodeMaps =
nodes
.map((node) => node.toJson())
.toList();
SharedPrefSingleton()
.setSavedConnection(
jsonEncode(nodeMaps))
.then((onValue) {
Navigator.pop(context);
return;
});
},
child: const Text('删除'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
return;
},
child: const Text("取消"),
),
],
),
],
),
),
),
);
},
child: const Text("使用配置")),
],
),
),
],
);
}
}

20
lib/edge_state.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'dart:io';
import 'package:flutter/material.dart';
class EdgeState {
EdgeState._internal();
static final EdgeState instance = EdgeState._internal();
bool isRunning = false;
Process? process;
EdgeLogger logger = EdgeLogger();
}
class EdgeLogger with ChangeNotifier {
List<String> logList = [];
void addLog(String log) {
logList.add(log);
notifyListeners();
}
}

97
lib/home_page.dart Normal file
View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:winui_n2n/control_page.dart';
import 'package:winui_n2n/about_page.dart';
import 'package:winui_n2n/logger_page.dart';
import 'package:winui_n2n/main.dart';
import 'package:winui_n2n/setting_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _selectedIndex = 0;
Widget _homePageShow() {
if (_selectedIndex == 0) {
return const ControlPage();
} else if (_selectedIndex == 1) {
return const AboutPage();
} else if (_selectedIndex == 2) {
return const LoggerPage();
} else if (_selectedIndex == 3) {
return const SettingPage();
}
// Must contain a default widget.
return const Text("Default");
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
selectedIndex: _selectedIndex,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: NavigationRailLabelType.all,
trailing: Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: IconButton(
icon:
MainApp.of(context).getCurrentTheme == ThemeMode.light
? const Icon(Icons.dark_mode)
: const Icon(Icons.light_mode),
onPressed: () {
MainApp.of(context).getCurrentTheme == ThemeMode.light
? MainApp.of(context).changeTheme(ThemeMode.dark)
: MainApp.of(context).changeTheme(ThemeMode.light);
setState(() {});
}),
),
),
),
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('主页'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.book),
label: Text('关于'),
),
NavigationRailDestination(
icon: Icon(Icons.pause_circle_filled),
selectedIcon: Icon(Icons.pause_circle),
label: Text('日志'),
),
NavigationRailDestination(
icon: Icon(Icons.star_border),
selectedIcon: Icon(Icons.star),
label: Text('设置'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: _homePageShow(),
),
],
),
);
}
}

22
lib/logger_page.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:winui_n2n/edge_state.dart';
class LoggerPage extends StatelessWidget {
const LoggerPage({super.key});
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: EdgeState.instance.logger,
builder: (context, child) {
return ListView(
children: EdgeState.instance.logger.logList.map<SelectableText>(
(e) {
return SelectableText(e);
},
).toList(),
);
},
);
}
}

77
lib/main.dart Normal file
View File

@@ -0,0 +1,77 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'package:winui_n2n/application_exit_control.dart';
import 'package:winui_n2n/edge_state.dart';
import 'package:winui_n2n/shared_pref_singleton.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
size: Size(630, 420),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.normal,
windowButtonVisibility: false,
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
await initSingleton();
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
static _MainAppState of(BuildContext context) {
return context.findAncestorStateOfType<_MainAppState>()!;
}
}
class _MainAppState extends State<MainApp> {
// false = dark, true = light
ThemeMode _themeMode =
SharedPrefSingleton().appTheme ? ThemeMode.light : ThemeMode.dark;
ThemeMode get getCurrentTheme => _themeMode;
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
darkTheme: ThemeData.dark(),
themeMode: _themeMode,
home: const Scaffold(
body: ApplicationExitControl(),
),
);
}
void changeTheme(ThemeMode mode) {
mode == ThemeMode.light
? SharedPrefSingleton().setAppTheme(true).then((onValue) {
setState(() {
_themeMode = mode;
});
})
: SharedPrefSingleton().setAppTheme(false).then((onValue) {
setState(() {
_themeMode = mode;
});
});
}
}
Future<void> initSingleton() async {
EdgeState.instance;
await SharedPrefSingleton().initialize();
}

27
lib/saved_connection.dart Normal file
View File

@@ -0,0 +1,27 @@
class SavedConnection {
final String name;
final String supernode;
final String community;
final String communityKey;
final String selfAddress;
SavedConnection(this.name, this.supernode, this.community, this.communityKey,
this.selfAddress);
factory SavedConnection.fromJson(Map<String, dynamic> json) =>
SavedConnection(
json['name'] as String,
json['supernode'] as String,
json['community'] as String,
json['communityKey'] as String,
json['selfAddress'] as String,
);
Map<String, dynamic> toJson() => {
'name': name,
'supernode': supernode,
'community': community,
'communityKey': communityKey,
'selfAddress': selfAddress,
};
}

48
lib/setting_page.dart Normal file
View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:winui_n2n/shared_pref_singleton.dart';
class SettingPage extends StatefulWidget {
const SettingPage({super.key});
@override
State<SettingPage> createState() => _SettingPageState();
}
class _SettingPageState extends State<SettingPage> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('自动设置防火墙'),
Switch(
value: SharedPrefSingleton().autoFirewall,
onChanged: (value) {
final setting = !SharedPrefSingleton().autoFirewall;
SharedPrefSingleton().setAutoFirewall(setting).then((ok) {
setState(() {});
});
},
),
],
),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(
const SnackBar(
content: Text("功能暂未开发, 敬请期待~"),
duration: Duration(seconds: 2),
),
);
},
child: const Text("检查更新"),
),
],
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:shared_preferences/shared_preferences.dart';
class SharedPrefSingleton {
static final SharedPrefSingleton _instance = SharedPrefSingleton._internal();
factory SharedPrefSingleton() => _instance;
SharedPrefSingleton._internal();
late SharedPreferences _pref;
Future<void> initialize() async {
_pref = await SharedPreferences.getInstance();
}
Future<bool> setAutoFirewall(bool ok) => _pref.setBool("auto_firewall", ok);
bool get autoFirewall => _pref.getBool("auto_firewall") ?? true;
Future<bool> setAppTheme(bool ok) => _pref.setBool("app_theme", ok);
// false = dark, true = light
bool get appTheme => _pref.getBool("app_theme") ?? false;
Future<bool> setSavedConnection(String config) =>
_pref.setString("saved_connection", config);
String get savedConnection => _pref.getString("saved_connection") ?? "[]";
}