393 lines
14 KiB
Dart
393 lines
14 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
|||
|
|
import '../services/config_service.dart';
|
|||
|
|
import '../utils/ini_utils.dart';
|
|||
|
|
|
|||
|
|
class ConfigEditorScreen extends StatefulWidget {
|
|||
|
|
const ConfigEditorScreen({super.key});
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
State<ConfigEditorScreen> createState() => _ConfigEditorScreenState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _ConfigEditorScreenState extends State<ConfigEditorScreen> {
|
|||
|
|
final ConfigService _configService = ConfigService();
|
|||
|
|
bool _isLoading = false;
|
|||
|
|
bool _isModified = false;
|
|||
|
|
|
|||
|
|
// 配置项数据
|
|||
|
|
final Map<String, Map<String, TextEditingController>> _controllers = {};
|
|||
|
|
final Map<String, Map<String, bool>> _enabledFlags = {};
|
|||
|
|
final List<String> _sections = [];
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
void initState() {
|
|||
|
|
super.initState();
|
|||
|
|
_loadConfig();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<void> _loadConfig() async {
|
|||
|
|
setState(() {
|
|||
|
|
_isLoading = true;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
final content = await _configService.readConfig();
|
|||
|
|
if (content.isNotEmpty) {
|
|||
|
|
_parseConfig(content);
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
if (mounted) {
|
|||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|||
|
|
SnackBar(content: Text('加载配置失败: $e')),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
setState(() {
|
|||
|
|
_isLoading = false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void _parseConfig(String content) {
|
|||
|
|
final config = parseIni(content);
|
|||
|
|
_controllers.clear();
|
|||
|
|
_enabledFlags.clear();
|
|||
|
|
_sections.clear();
|
|||
|
|
|
|||
|
|
// 定义所有可能的配置项
|
|||
|
|
final configStructure = {
|
|||
|
|
'client': ['server_url', 'update_url'],
|
|||
|
|
'lolminer': ['path'],
|
|||
|
|
'rigel': ['path'],
|
|||
|
|
'bzminer': ['path'],
|
|||
|
|
'proxy': ['proxy'],
|
|||
|
|
'sustain': ['enabled', 'algo', 'coin', 'miner', 'pool_url', 'wallet', 'worker_id', 'pool_user', 'wallet_mining'],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (final section in configStructure.keys) {
|
|||
|
|
_sections.add(section);
|
|||
|
|
_controllers[section] = {};
|
|||
|
|
_enabledFlags[section] = {};
|
|||
|
|
|
|||
|
|
for (final option in configStructure[section]!) {
|
|||
|
|
final value = config.get(section, option) ?? '';
|
|||
|
|
_controllers[section]![option] = TextEditingController(text: value);
|
|||
|
|
_controllers[section]![option]!.addListener(() {
|
|||
|
|
setState(() {
|
|||
|
|
_isModified = true;
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
// 默认所有配置项都是禁用的(需要勾选才能编辑)
|
|||
|
|
_enabledFlags[section]![option] = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<void> _saveConfig() async {
|
|||
|
|
final confirmed = await showDialog<bool>(
|
|||
|
|
context: context,
|
|||
|
|
builder: (context) => AlertDialog(
|
|||
|
|
title: const Text('确认保存'),
|
|||
|
|
content: const Text('确定要保存配置文件吗?'),
|
|||
|
|
actions: [
|
|||
|
|
TextButton(
|
|||
|
|
onPressed: () => Navigator.pop(context, false),
|
|||
|
|
child: const Text('取消'),
|
|||
|
|
),
|
|||
|
|
TextButton(
|
|||
|
|
onPressed: () => Navigator.pop(context, true),
|
|||
|
|
child: const Text('确定'),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (confirmed == true) {
|
|||
|
|
setState(() {
|
|||
|
|
_isLoading = true;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
final content = _generateConfigContent();
|
|||
|
|
final success = await _configService.saveConfig(content);
|
|||
|
|
|
|||
|
|
if (mounted) {
|
|||
|
|
setState(() {
|
|||
|
|
_isLoading = false;
|
|||
|
|
_isModified = !success;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|||
|
|
SnackBar(
|
|||
|
|
content: Text(success ? '保存成功' : '保存失败'),
|
|||
|
|
backgroundColor: success ? Colors.green : Colors.red,
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
if (mounted) {
|
|||
|
|
setState(() {
|
|||
|
|
_isLoading = false;
|
|||
|
|
});
|
|||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|||
|
|
SnackBar(content: Text('保存失败: $e')),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
String _generateConfigContent() {
|
|||
|
|
final buffer = StringBuffer();
|
|||
|
|
|
|||
|
|
// 添加注释
|
|||
|
|
buffer.writeln('#请确认您的主机上安装了下列挖矿软件,确认后可以打开注释,并修改其路径,如果没有安装,请勿打开注释');
|
|||
|
|
buffer.writeln('#请使用双\\\\,否则可能无法解析出准确的路径');
|
|||
|
|
buffer.writeln();
|
|||
|
|
|
|||
|
|
for (final section in _sections) {
|
|||
|
|
if (section == 'client') {
|
|||
|
|
buffer.writeln('[client]');
|
|||
|
|
_writeOption(buffer, section, 'server_url');
|
|||
|
|
_writeOption(buffer, section, 'update_url');
|
|||
|
|
buffer.writeln();
|
|||
|
|
buffer.writeln('#请确认您的主机上安装了下列挖矿软件,确认后可以打开注释,并修改其路径,如果没有安装,请勿打开注释');
|
|||
|
|
buffer.writeln('#请使用双\\\\,否则可能无法解析出准确的路径');
|
|||
|
|
} else if (section == 'lolminer' || section == 'rigel' || section == 'bzminer') {
|
|||
|
|
buffer.writeln('[$section]');
|
|||
|
|
_writeOption(buffer, section, 'path', commentPrefix: '# ');
|
|||
|
|
} else if (section == 'proxy') {
|
|||
|
|
buffer.writeln();
|
|||
|
|
buffer.writeln('#如果您的网络无法直接连通各个矿池,需要使用各大矿池专用网络,请打开proxy的注释');
|
|||
|
|
buffer.writeln('#打开此注释后会使用各大矿池的专用网络,每笔订单额外增加1%的网络费用');
|
|||
|
|
buffer.writeln('[proxy]');
|
|||
|
|
_writeOption(buffer, section, 'proxy', commentPrefix: '# ');
|
|||
|
|
} else if (section == 'sustain') {
|
|||
|
|
buffer.writeln();
|
|||
|
|
buffer.writeln('#持续挖矿开关,即在矿机没有租约期间是否自行挖矿');
|
|||
|
|
buffer.writeln('#开启此选项启动客户端后,客户端会自动根据下面配置开启挖矿任务,直到云算力平台有人租赁本台GPU主机');
|
|||
|
|
buffer.writeln('#当该租约结束后,本台GPU主机会自动切回下方配置的挖矿任务');
|
|||
|
|
buffer.writeln('[sustain]');
|
|||
|
|
_writeOption(buffer, section, 'enabled', commentPrefix: '#');
|
|||
|
|
_writeOption(buffer, section, 'algo', commentPrefix: '#');
|
|||
|
|
_writeOption(buffer, section, 'coin', commentPrefix: '#');
|
|||
|
|
_writeOption(buffer, section, 'miner', commentPrefix: '#');
|
|||
|
|
_writeOption(buffer, section, 'pool_url', commentPrefix: '#');
|
|||
|
|
_writeOption(buffer, section, 'wallet', commentPrefix: '#');
|
|||
|
|
_writeOption(buffer, section, 'worker_id', commentPrefix: '#');
|
|||
|
|
_writeOption(buffer, section, 'pool_user', commentPrefix: '#');
|
|||
|
|
_writeOption(buffer, section, 'wallet_mining', commentPrefix: '#');
|
|||
|
|
}
|
|||
|
|
buffer.writeln();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return buffer.toString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void _writeOption(StringBuffer buffer, String section, String option, {String commentPrefix = ''}) {
|
|||
|
|
final controller = _controllers[section]?[option];
|
|||
|
|
final enabled = _enabledFlags[section]?[option] ?? false;
|
|||
|
|
final value = controller?.text ?? '';
|
|||
|
|
|
|||
|
|
if (value.isEmpty || !enabled) {
|
|||
|
|
// 如果值为空或未启用,使用注释形式
|
|||
|
|
if (option == 'path') {
|
|||
|
|
buffer.writeln('$commentPrefix$option=C:\\\\path\\\\${section}');
|
|||
|
|
} else if (option == 'proxy') {
|
|||
|
|
buffer.writeln('$commentPrefix$option=true');
|
|||
|
|
} else if (option == 'enabled') {
|
|||
|
|
buffer.writeln('$commentPrefix$option=true');
|
|||
|
|
} else if (option == 'algo') {
|
|||
|
|
buffer.writeln('$commentPrefix$option="算法"');
|
|||
|
|
} else if (option == 'coin') {
|
|||
|
|
buffer.writeln('$commentPrefix$option="币种"');
|
|||
|
|
} else if (option == 'miner') {
|
|||
|
|
buffer.writeln('$commentPrefix$option="挖矿软件名,此处使用的挖矿软件要使用上方已经配置路径的挖矿软件名,即bzminer/lolminer/rigel,只能填一个,自行选择"');
|
|||
|
|
} else if (option == 'pool_url') {
|
|||
|
|
buffer.writeln('$commentPrefix$option="挖矿地址"');
|
|||
|
|
} else if (option == 'wallet') {
|
|||
|
|
buffer.writeln('$commentPrefix$option="挖矿钱包"');
|
|||
|
|
} else if (option == 'worker_id') {
|
|||
|
|
buffer.writeln('$commentPrefix$option="矿工号"');
|
|||
|
|
} else if (option == 'pool_user') {
|
|||
|
|
buffer.writeln('$commentPrefix$option="挖矿账号名,f2pool/m2pool等不支持钱包挖矿的矿池需配置,其余支持钱包挖矿的矿池无需配置"');
|
|||
|
|
} else if (option == 'wallet_mining') {
|
|||
|
|
buffer.writeln('$commentPrefix$option=true #pool_user打开时同时打开本配置');
|
|||
|
|
} else {
|
|||
|
|
buffer.writeln('$commentPrefix$option=$value');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 如果值不为空且已启用,写入实际值
|
|||
|
|
if (option == 'path' || option == 'server_url' || option == 'update_url' ||
|
|||
|
|
option == 'pool_url' || option == 'wallet' || option == 'worker_id' ||
|
|||
|
|
option == 'pool_user' || option == 'algo' || option == 'coin' || option == 'miner') {
|
|||
|
|
buffer.writeln('$option=$value');
|
|||
|
|
} else {
|
|||
|
|
buffer.writeln('$option=$value');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
void dispose() {
|
|||
|
|
for (final section in _controllers.values) {
|
|||
|
|
for (final controller in section.values) {
|
|||
|
|
controller.dispose();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
super.dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
return Scaffold(
|
|||
|
|
appBar: AppBar(
|
|||
|
|
title: const Text('配置文件编辑'),
|
|||
|
|
actions: [
|
|||
|
|
if (_isModified)
|
|||
|
|
IconButton(
|
|||
|
|
icon: const Icon(Icons.save),
|
|||
|
|
onPressed: _isLoading ? null : _saveConfig,
|
|||
|
|
tooltip: '保存',
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
body: _isLoading && _controllers.isEmpty
|
|||
|
|
? const Center(child: CircularProgressIndicator())
|
|||
|
|
: SingleChildScrollView(
|
|||
|
|
padding: const EdgeInsets.all(16.0),
|
|||
|
|
child: Column(
|
|||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
|
children: [
|
|||
|
|
if (_isModified)
|
|||
|
|
Container(
|
|||
|
|
padding: const EdgeInsets.all(12.0),
|
|||
|
|
margin: const EdgeInsets.only(bottom: 16.0),
|
|||
|
|
decoration: BoxDecoration(
|
|||
|
|
color: Colors.orange.shade50,
|
|||
|
|
borderRadius: BorderRadius.circular(8.0),
|
|||
|
|
border: Border.all(color: Colors.orange),
|
|||
|
|
),
|
|||
|
|
child: Row(
|
|||
|
|
children: [
|
|||
|
|
Icon(Icons.info, color: Colors.orange.shade700),
|
|||
|
|
const SizedBox(width: 8),
|
|||
|
|
const Expanded(
|
|||
|
|
child: Text(
|
|||
|
|
'配置已修改,请点击保存按钮保存更改',
|
|||
|
|
style: TextStyle(fontWeight: FontWeight.w500),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
..._sections.map((section) => _buildSection(section)),
|
|||
|
|
const SizedBox(height: 16),
|
|||
|
|
ElevatedButton.icon(
|
|||
|
|
onPressed: _isLoading || !_isModified ? null : _saveConfig,
|
|||
|
|
icon: const Icon(Icons.save),
|
|||
|
|
label: const Text('保存配置'),
|
|||
|
|
style: ElevatedButton.styleFrom(
|
|||
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Widget _buildSection(String section) {
|
|||
|
|
final sectionNames = {
|
|||
|
|
'client': '客户端配置',
|
|||
|
|
'lolminer': 'LolMiner 配置',
|
|||
|
|
'rigel': 'Rigel 配置',
|
|||
|
|
'bzminer': 'BzMiner 配置',
|
|||
|
|
'proxy': '代理配置',
|
|||
|
|
'sustain': '持续挖矿配置',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return Card(
|
|||
|
|
margin: const EdgeInsets.only(bottom: 16.0),
|
|||
|
|
child: Padding(
|
|||
|
|
padding: const EdgeInsets.all(16.0),
|
|||
|
|
child: Column(
|
|||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
|
children: [
|
|||
|
|
Text(
|
|||
|
|
sectionNames[section] ?? section,
|
|||
|
|
style: const TextStyle(
|
|||
|
|
fontSize: 18,
|
|||
|
|
fontWeight: FontWeight.bold,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const Divider(),
|
|||
|
|
...(_controllers[section]?.keys.map((option) => _buildConfigField(section, option)) ?? []),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Widget _buildConfigField(String section, String option) {
|
|||
|
|
final controller = _controllers[section]?[option];
|
|||
|
|
final enabled = _enabledFlags[section]?[option] ?? false;
|
|||
|
|
|
|||
|
|
final fieldNames = {
|
|||
|
|
'server_url': '服务器地址',
|
|||
|
|
'update_url': '更新服务器地址',
|
|||
|
|
'path': '软件路径',
|
|||
|
|
'proxy': '启用代理',
|
|||
|
|
'enabled': '启用持续挖矿',
|
|||
|
|
'algo': '算法',
|
|||
|
|
'coin': '币种',
|
|||
|
|
'miner': '挖矿软件',
|
|||
|
|
'pool_url': '矿池地址',
|
|||
|
|
'wallet': '钱包地址',
|
|||
|
|
'worker_id': '矿工号',
|
|||
|
|
'pool_user': '矿池用户名',
|
|||
|
|
'wallet_mining': '钱包挖矿',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return Padding(
|
|||
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|||
|
|
child: Row(
|
|||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
|
children: [
|
|||
|
|
Checkbox(
|
|||
|
|
value: enabled,
|
|||
|
|
onChanged: (value) {
|
|||
|
|
setState(() {
|
|||
|
|
_enabledFlags[section]![option] = value ?? false;
|
|||
|
|
_isModified = true;
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
Expanded(
|
|||
|
|
child: Column(
|
|||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
|
children: [
|
|||
|
|
Text(
|
|||
|
|
fieldNames[option] ?? option,
|
|||
|
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: 4),
|
|||
|
|
TextField(
|
|||
|
|
controller: controller,
|
|||
|
|
enabled: enabled,
|
|||
|
|
decoration: InputDecoration(
|
|||
|
|
border: const OutlineInputBorder(),
|
|||
|
|
hintText: '请输入${fieldNames[option] ?? option}',
|
|||
|
|
filled: !enabled,
|
|||
|
|
fillColor: enabled ? null : Colors.grey.shade200,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|