云算力平台windows桌面应用
This commit is contained in:
392
lib/screens/config_editor_screen.dart
Normal file
392
lib/screens/config_editor_screen.dart
Normal file
@@ -0,0 +1,392 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
145
lib/screens/log_viewer_screen.dart
Normal file
145
lib/screens/log_viewer_screen.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/client_provider.dart';
|
||||
import '../services/log_service.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class LogViewerScreen extends StatefulWidget {
|
||||
const LogViewerScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LogViewerScreen> createState() => _LogViewerScreenState();
|
||||
}
|
||||
|
||||
class _LogViewerScreenState extends State<LogViewerScreen> {
|
||||
final LogService _logService = LogService();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
String _logs = '';
|
||||
StreamSubscription<String>? _logSubscription;
|
||||
StreamSubscription<String>? _clientLogSubscription;
|
||||
bool _autoScroll = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLogs();
|
||||
_startMonitoring();
|
||||
}
|
||||
|
||||
Future<void> _loadLogs() async {
|
||||
final logs = await _logService.getRecentLogs();
|
||||
setState(() {
|
||||
_logs = logs;
|
||||
});
|
||||
if (_autoScroll && _scrollController.hasClients) {
|
||||
_scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
void _startMonitoring() {
|
||||
// 监控日志文件
|
||||
_logSubscription = _logService.logStream.listen((log) {
|
||||
setState(() {
|
||||
_logs += '\n$log';
|
||||
});
|
||||
if (_autoScroll && _scrollController.hasClients) {
|
||||
_scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
// 监控客户端日志流
|
||||
final clientProvider = Provider.of<ClientProvider>(context, listen: false);
|
||||
_clientLogSubscription = clientProvider.logStream.listen((log) {
|
||||
setState(() {
|
||||
_logs += '\n$log';
|
||||
});
|
||||
if (_autoScroll && _scrollController.hasClients) {
|
||||
_scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logSubscription?.cancel();
|
||||
_clientLogSubscription?.cancel();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('日志查看'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_autoScroll ? Icons.arrow_downward : Icons.arrow_upward),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_autoScroll = !_autoScroll;
|
||||
});
|
||||
},
|
||||
tooltip: _autoScroll ? '关闭自动滚动' : '开启自动滚动',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadLogs,
|
||||
tooltip: '刷新日志',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () 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) {
|
||||
await _logService.clearLogs();
|
||||
_loadLogs();
|
||||
}
|
||||
},
|
||||
tooltip: '清理日志',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: SelectableText(
|
||||
_logs.isEmpty ? '暂无日志' : _logs,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
426
lib/screens/main_screen.dart
Normal file
426
lib/screens/main_screen.dart
Normal file
@@ -0,0 +1,426 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/client_provider.dart';
|
||||
import '../models/client_status.dart';
|
||||
import 'log_viewer_screen.dart';
|
||||
import 'config_editor_screen.dart';
|
||||
import 'mining_info_screen.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class MainScreen extends StatelessWidget {
|
||||
const MainScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('云算力平台客户端'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Consumer<ClientProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final clientInfo = provider.clientInfo;
|
||||
|
||||
if (provider.isLoading && clientInfo == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (clientInfo == null) {
|
||||
return const Center(child: Text('无法获取客户端信息'));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 状态指示器
|
||||
_buildStatusCard(clientInfo),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 基本信息卡片
|
||||
_buildInfoCard(clientInfo),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 版本更新提示
|
||||
if (provider.hasUpdate)
|
||||
_buildUpdateCard(context, provider),
|
||||
if (provider.hasUpdate) const SizedBox(height: 16),
|
||||
|
||||
// GPU信息卡片
|
||||
_buildGPUCard(clientInfo.gpus),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 操作按钮
|
||||
_buildActionButtons(context, clientInfo),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusCard(ClientInfo clientInfo) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: clientInfo.status.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'当前状态: ${clientInfo.status.displayName}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(ClientInfo clientInfo) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'基本信息',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
_buildInfoRow('版本号', clientInfo.version),
|
||||
_buildInfoRow('身份信息', clientInfo.auth),
|
||||
_buildInfoRow('机器码', clientInfo.machineCode),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGPUCard(List<GPUInfo> gpus) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'GPU信息',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
if (gpus.isEmpty)
|
||||
const Text('未检测到GPU')
|
||||
else
|
||||
...gpus.map((gpu) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text('GPU ${gpu.index}: ${gpu.displayName}'),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUpdateCard(BuildContext context, ClientProvider provider) {
|
||||
return Card(
|
||||
color: Colors.orange.shade50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.system_update, color: Colors.orange.shade700),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'发现新版本',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('当前版本: ${provider.clientInfo?.version ?? '未知'}'),
|
||||
Text('最新版本: ${provider.remoteVersion ?? '未知'}'),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _performUpdate(context, provider),
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text('立即更新'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _performUpdate(BuildContext context, ClientProvider provider) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认更新'),
|
||||
content: const Text(
|
||||
'更新将在下载完成后,下次启动时应用。\n'
|
||||
'更新过程中请勿关闭程序。\n\n'
|
||||
'确定要继续吗?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
// 显示进度对话框
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('正在下载更新...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final success = await provider.performUpdate();
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // 关闭进度对话框
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(success ? '更新成功' : '更新失败'),
|
||||
content: Text(
|
||||
success
|
||||
? '更新已下载完成,将在下次启动时应用。\n请重启程序以完成更新。'
|
||||
: '更新下载失败,请检查网络连接或稍后重试。',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context, ClientInfo clientInfo) {
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LogViewerScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.description),
|
||||
label: const Text('查看日志'),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ConfigEditorScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.settings),
|
||||
label: const Text('查看/修改配置'),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: clientInfo.status == ClientStatus.mining && clientInfo.miningInfo != null
|
||||
? () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MiningInfoScreen(miningInfo: clientInfo.miningInfo!),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.diamond),
|
||||
label: const Text('挖矿信息'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: clientInfo.status == ClientStatus.mining && clientInfo.miningInfo != null
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _restartClient(context),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('重启'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _exitApp(context),
|
||||
icon: const Icon(Icons.exit_to_app),
|
||||
label: const Text('退出程序'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _restartClient(BuildContext context) 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) {
|
||||
// 显示加载对话框,禁用所有操作
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => PopScope(
|
||||
canPop: false, // 禁止返回
|
||||
child: const AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('正在重启客户端...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final provider = Provider.of<ClientProvider>(context, listen: false);
|
||||
bool success = false;
|
||||
String errorMessage = '';
|
||||
|
||||
try {
|
||||
await provider.restart();
|
||||
success = true;
|
||||
} catch (e) {
|
||||
errorMessage = e.toString();
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // 关闭加载对话框
|
||||
|
||||
// 显示结果
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(success ? '客户端重启成功' : '客户端重启失败: $errorMessage'),
|
||||
backgroundColor: success ? Colors.green : Colors.red,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _exitApp(BuildContext context) 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) {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
239
lib/screens/mining_info_screen.dart
Normal file
239
lib/screens/mining_info_screen.dart
Normal file
@@ -0,0 +1,239 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:async';
|
||||
import '../models/client_status.dart';
|
||||
import '../core/mining_manager.dart';
|
||||
|
||||
class MiningInfoScreen extends StatefulWidget {
|
||||
final MiningInfo miningInfo;
|
||||
|
||||
const MiningInfoScreen({
|
||||
super.key,
|
||||
required this.miningInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MiningInfoScreen> createState() => _MiningInfoScreenState();
|
||||
}
|
||||
|
||||
class _MiningInfoScreenState extends State<MiningInfoScreen> {
|
||||
final MiningManager _miningManager = MiningManager();
|
||||
final ScrollController _logScrollController = ScrollController();
|
||||
final List<String> _minerLogs = [];
|
||||
StreamSubscription<String>? _logSubscription;
|
||||
bool _autoScroll = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startLogMonitoring();
|
||||
}
|
||||
|
||||
void _startLogMonitoring() {
|
||||
_logSubscription = _miningManager.minerLogStream.listen((log) {
|
||||
setState(() {
|
||||
_minerLogs.add(log);
|
||||
// 限制日志行数,避免内存溢出
|
||||
if (_minerLogs.length > 1000) {
|
||||
_minerLogs.removeAt(0);
|
||||
}
|
||||
});
|
||||
if (_autoScroll && _logScrollController.hasClients) {
|
||||
_scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
if (_logScrollController.hasClients) {
|
||||
_logScrollController.animateTo(
|
||||
_logScrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logSubscription?.cancel();
|
||||
_logScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final endTime = DateTime.fromMillisecondsSinceEpoch(widget.miningInfo.endTimestamp * 1000);
|
||||
final formatter = DateFormat('yyyy-MM-dd HH:mm:ss');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('挖矿信息'),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 挖矿任务信息卡片
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'当前挖矿任务',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
_buildInfoRow('币种', widget.miningInfo.coin),
|
||||
_buildInfoRow('算法', widget.miningInfo.algo),
|
||||
_buildInfoRow('矿池', widget.miningInfo.pool),
|
||||
_buildInfoRow('矿池地址', widget.miningInfo.poolUrl),
|
||||
_buildInfoRow('钱包地址', widget.miningInfo.walletAddress),
|
||||
_buildInfoRow('矿工号', widget.miningInfo.workerId),
|
||||
if (widget.miningInfo.miner != null)
|
||||
_buildInfoRow('挖矿软件', widget.miningInfo.miner!),
|
||||
if (widget.miningInfo.pid != null)
|
||||
_buildInfoRow('进程ID', widget.miningInfo.pid.toString()),
|
||||
_buildInfoRow(
|
||||
'结束时间',
|
||||
formatter.format(endTime),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTimeRemaining(endTime),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 挖矿日志卡片
|
||||
Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'挖矿日志',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(_autoScroll ? Icons.arrow_downward : Icons.arrow_upward),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_autoScroll = !_autoScroll;
|
||||
});
|
||||
},
|
||||
tooltip: _autoScroll ? '关闭自动滚动' : '开启自动滚动',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_minerLogs.clear();
|
||||
});
|
||||
},
|
||||
tooltip: '清空日志',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Container(
|
||||
height: 400,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _minerLogs.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'暂无日志输出',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
controller: _logScrollController,
|
||||
child: SelectableText(
|
||||
_minerLogs.join('\n'),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeRemaining(DateTime endTime) {
|
||||
final now = DateTime.now();
|
||||
final remaining = endTime.difference(now);
|
||||
|
||||
if (remaining.isNegative) {
|
||||
return const Card(
|
||||
color: Colors.red,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'任务已结束',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final hours = remaining.inHours;
|
||||
final minutes = remaining.inMinutes.remainder(60);
|
||||
final seconds = remaining.inSeconds.remainder(60);
|
||||
|
||||
return Card(
|
||||
color: Colors.green,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'剩余时间: ${hours}小时 ${minutes}分钟 ${seconds}秒',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user