diff --git a/lib/main.dart b/lib/main.dart index e986457..11725c0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,21 +2,34 @@ import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:process_run/shell.dart'; import 'dart:io'; +import 'dart:convert'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; void main() { runApp(NaiveProxyApp()); } -class NaiveProxyApp extends StatelessWidget { - const NaiveProxyApp({super.key}); +class ServerConfig { + final String name; + final String configPath; + final int? ping; + ServerConfig({ + required this.name, + required this.configPath, + this.ping + }); +} + +class NaiveProxyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'NaiveProxy启动器', + title: 'NaiveProxy Launcher', theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, + colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple), + useMaterial3: true, ), home: NaiveProxyHomePage(), ); @@ -24,89 +37,152 @@ class NaiveProxyApp extends StatelessWidget { } class NaiveProxyHomePage extends StatefulWidget { - const NaiveProxyHomePage({super.key}); - @override _NaiveProxyHomePageState createState() => _NaiveProxyHomePageState(); } class _NaiveProxyHomePageState extends State { - String? _configFilePath; - String _status = '未选择配置文件'; + List _servers = []; + ServerConfig? _selectedServer; + bool _isProxyRunning = false; + Process? _naiveProcess; + List _logs = []; + ScrollController _logController = ScrollController(); - Future _pickConfigFile() async { - FilePickerResult? result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['json'], - ); + @override + void initState() { + super.initState(); + _loadServers(); + } - if (result != null) { - setState(() { - _configFilePath = result.files.single.path; - _status = '已选择文件: ${_configFilePath!.split('/').last}'; - }); + Future _loadServers() async { + // 获取配置目录 + Directory configDir = await _getNaiveFlyConfigDirectory(); + + // 确保目录存在 + if (!configDir.existsSync()) { + configDir.createSync(recursive: true); + } + + // 读取所有JSON文件 + List files = configDir.listSync() + .where((file) => file.path.endsWith('.json')) + .toList(); + + setState(() { + _servers = files.map((file) { + String name = path.basenameWithoutExtension(file.path); + return ServerConfig( + name: name, + configPath: file.path + ); + }).toList(); + }); + } + + Future _getNaiveFlyConfigDirectory() async { + if (Platform.isWindows) { + final appDataDir = Platform.environment['APPDATA']; + return Directory(path.join(appDataDir!, 'NaiveFly')); + } else { + // 假设是Unix类系统(Linux/macOS) + final homeDir = Platform.environment['HOME']; + return Directory(path.join(homeDir!, '.config', 'naivefly')); } } Future _launchNaiveProxy() async { - if (_configFilePath == null) { - _showErrorDialog('请先导入配置文件'); + if (_isProxyRunning) { + _stopNaiveProxy(); + return; + } + + if (_selectedServer == null) { + _showErrorDialog('Please select a server'); return; } try { // 检查naiveproxy是否已安装 - var shell = Shell(); - var whichResult = await shell.run('which naive'); - + var whichResult = await Shell().run('which naive'); + if (whichResult.isEmpty) { - _showErrorDialog('NaiveProxy未安装'); + _showErrorDialog('NaiveProxy is not installed'); return; } - // 尝试执行naive - var result = await shell.run('naive $_configFilePath'); + // 启动naive进程 + _naiveProcess = await Process.start('naive', [_selectedServer!.configPath]); - // 显示执行结果 - _showResultDialog('NaiveProxy已启动', result.outText); + setState(() { + _isProxyRunning = true; + _logs.clear(); + _addLog('NaiveProxy started: ${_selectedServer!.name}'); + }); + + // 监听进程输出 + _naiveProcess!.stdout.listen((List event) { + setState(() { + _addLog('Output: ${String.fromCharCodes(event)}'); + }); + }); + + // 监听进程错误输出 + _naiveProcess!.stderr.listen((List event) { + setState(() { + _addLog('Message: ${String.fromCharCodes(event)}'); + }); + }); + + // 监听进程退出 + _naiveProcess!.exitCode.then((int code) { + setState(() { + _isProxyRunning = false; + _addLog('NaiveProxy exited with code: $code'); + }); + }); } catch (e) { - _showErrorDialog('启动失败:${e.toString()}'); + _showErrorDialog('Launch failed: ${e.toString()}'); } } + void _stopNaiveProxy() { + if (_naiveProcess != null) { + _naiveProcess!.kill(); + setState(() { + _isProxyRunning = false; + _addLog('NaiveProxy manually stopped'); + }); + } + } + + void _addLog(String message) { + setState(() { + _logs.add(message); + // 自动滚动到底部 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_logController.hasClients) { + _logController.animateTo( + _logController.position.maxScrollExtent, + duration: Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + }); + } + void _showErrorDialog(String message) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('错误'), + title: Text('Error'), content: Text(message), actions: [ TextButton( - child: Text('确定'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - } - - void _showResultDialog(String title, String message) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text(title), - content: SingleChildScrollView( - child: Text(message), - ), - actions: [ - TextButton( - child: Text('确定'), + child: Text('OK'), onPressed: () { Navigator.of(context).pop(); }, @@ -121,43 +197,90 @@ class _NaiveProxyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('NaiveProxy启动器'), + title: Text('NaiveProxy Launcher'), centerTitle: true, + actions: [ + IconButton( + icon: Icon(Icons.refresh), + onPressed: _loadServers, + ) + ], ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 300, - child: Text( - _status, - style: TextStyle(fontSize: 16), - textAlign: TextAlign.center, - ), + body: Column( + children: [ + // 服务器列表 + Container( + height: 200, + child: ListView.builder( + itemCount: _servers.length, + itemBuilder: (context, index) { + ServerConfig server = _servers[index]; + return ListTile( + title: Text(server.name), + subtitle: Text(server.configPath), + trailing: Text('${server.ping ?? 'N/A'} ms', + style: TextStyle(color: Colors.green), + ), + selected: _selectedServer == server, + onTap: () { + setState(() { + _selectedServer = server; + }); + }, + ); + }, ), - SizedBox(height: 20), - ElevatedButton.icon( - icon: Icon(Icons.file_upload), - label: Text('导入配置文件'), - onPressed: _pickConfigFile, - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), - ), - ), - SizedBox(height: 20), - ElevatedButton.icon( - icon: Icon(Icons.rocket_launch), - label: Text('启动NaiveProxy'), + ), + + // 启动按钮 + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton.icon( + icon: Icon(_isProxyRunning ? Icons.stop : Icons.rocket_launch), + label: Text(_isProxyRunning ? 'Stop Proxy' : 'Start NaiveProxy'), onPressed: _launchNaiveProxy, style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, + backgroundColor: _isProxyRunning ? Colors.red : Colors.green, padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), ), - ], - ), + ), + + // 日志输出区域 + Expanded( + child: Container( + margin: EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: ListView.builder( + controller: _logController, + itemCount: _logs.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + _logs[index], + style: TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ); + }, + ), + ), + ), + ], ), ); } -} \ No newline at end of file + + @override + void dispose() { + _stopNaiveProxy(); + _logController.dispose(); + super.dispose(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7f336f6..150b500 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -177,13 +177,69 @@ packages: source: hosted version: "1.16.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + url: "https://pub.dev" + source: hosted + version: "2.2.16" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -301,6 +357,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.12.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cf6cae3..93264be 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,9 @@ dependencies: file_picker: ^9.2.2 process_run: ^1.2.4 + path_provider: ^2.1.5 + path: ^1.9.1 + # system_tray: ^2.0.3 dev_dependencies: flutter_test: sdk: flutter