2026-01-22 15:14:27 +08:00
|
|
|
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(
|
2026-01-23 16:11:20 +08:00
|
|
|
onPressed: (clientInfo.status == ClientStatus.mining || clientInfo.status == ClientStatus.sustainingMining) && clientInfo.miningInfo != null
|
2026-01-22 15:14:27 +08:00
|
|
|
? () {
|
|
|
|
|
Navigator.push(
|
|
|
|
|
context,
|
|
|
|
|
MaterialPageRoute(
|
2026-01-23 16:11:20 +08:00
|
|
|
builder: (_) => MiningInfoScreen(
|
|
|
|
|
miningInfo: clientInfo.miningInfo!,
|
|
|
|
|
isSustainMining: clientInfo.status == ClientStatus.sustainingMining,
|
|
|
|
|
),
|
2026-01-22 15:14:27 +08:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
icon: const Icon(Icons.diamond),
|
|
|
|
|
label: const Text('挖矿信息'),
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
2026-01-23 16:11:20 +08:00
|
|
|
backgroundColor: ((clientInfo.status == ClientStatus.mining || clientInfo.status == ClientStatus.sustainingMining) && clientInfo.miningInfo != null)
|
2026-01-22 15:14:27 +08:00
|
|
|
? 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) {
|
2026-01-29 10:44:29 +08:00
|
|
|
// 先优雅关闭客户端和挖矿进程,再退出程序
|
|
|
|
|
final provider = Provider.of<ClientProvider>(context, listen: false);
|
|
|
|
|
await provider.shutdown();
|
2026-01-22 15:14:27 +08:00
|
|
|
exit(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|