1250 lines
49 KiB
Dart
1250 lines
49 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:diagd/src/rust/api/diagnostics.dart';
|
||
|
||
class HardwareTab extends StatelessWidget {
|
||
final HardwareInfo hardware;
|
||
|
||
const HardwareTab({super.key, required this.hardware});
|
||
|
||
static String _formatBytes(BigInt bytes) {
|
||
final b = bytes.toDouble();
|
||
if (b >= 1024 * 1024 * 1024 * 1024) {
|
||
return '${(b / (1024 * 1024 * 1024 * 1024)).toStringAsFixed(1)} TB';
|
||
} else if (b >= 1024 * 1024 * 1024) {
|
||
return '${(b / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||
} else if (b >= 1024 * 1024) {
|
||
return '${(b / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||
} else if (b >= 1024) {
|
||
return '${(b / 1024).toStringAsFixed(1)} KB';
|
||
}
|
||
return '$bytes B';
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return SingleChildScrollView(
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_buildHeaderBanner(),
|
||
const SizedBox(height: 20),
|
||
_buildCpuAndMotherboardRow(context),
|
||
const SizedBox(height: 20),
|
||
_buildMemoryAndSwapSection(),
|
||
const SizedBox(height: 20),
|
||
_buildGpuSection(),
|
||
const SizedBox(height: 20),
|
||
_buildStorageSection(),
|
||
const SizedBox(height: 20),
|
||
_buildNetworkSection(),
|
||
const SizedBox(height: 20),
|
||
_buildThermalsAndBatterySection(),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildHeaderBanner() {
|
||
return Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF1F6FEB).withAlpha(40),
|
||
borderRadius: BorderRadius.circular(10),
|
||
border: Border.all(color: const Color(0xFF1F6FEB).withAlpha(100)),
|
||
),
|
||
child: const Icon(Icons.memory_rounded, color: Color(0xFF58A6FF), size: 28),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text(
|
||
'Полный аппаратный профиль системы',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'${hardware.motherboard.vendor} ${hardware.motherboard.productName} • ${hardware.cpu.modelName}',
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
color: Color(0xFF8B949E),
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
_buildChassisBadge(hardware.motherboard.chassisType),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildChassisBadge(String chassis) {
|
||
IconData icon = Icons.computer_rounded;
|
||
final lower = chassis.toLowerCase();
|
||
if (lower.contains('notebook') ||
|
||
lower.contains('laptop') ||
|
||
lower.contains('portable')) {
|
||
icon = Icons.laptop_rounded;
|
||
} else if (lower.contains('server') ||
|
||
lower.contains('rack')) {
|
||
icon = Icons.dns_rounded;
|
||
}
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, size: 16, color: const Color(0xFF8B949E)),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
chassis,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCpuAndMotherboardRow(BuildContext context) {
|
||
return LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final isWide = constraints.maxWidth > 900;
|
||
if (isWide) {
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(child: _buildCpuCard()),
|
||
const SizedBox(width: 20),
|
||
Expanded(child: _buildMotherboardCard()),
|
||
],
|
||
);
|
||
} else {
|
||
return Column(
|
||
children: [
|
||
_buildCpuCard(),
|
||
const SizedBox(height: 20),
|
||
_buildMotherboardCard(),
|
||
],
|
||
);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildCpuCard() {
|
||
final cpu = hardware.cpu;
|
||
return Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.speed_rounded, color: Color(0xFF58A6FF), size: 20),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'Процессор (CPU)',
|
||
style: TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Text(
|
||
cpu.architecture,
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
fontFamily: 'monospace',
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFF58A6FF),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
Text(
|
||
cpu.modelName,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
const Divider(color: Color(0xFF21262D), height: 1),
|
||
const SizedBox(height: 12),
|
||
_buildInfoRow('Ядра / Потоки:', '${cpu.physicalCores} физич. / ${cpu.logicalThreads} логич.'),
|
||
_buildInfoRow(
|
||
'Тактовая частота:',
|
||
'${cpu.currentFrequencyMhz != null ? "${cpu.currentFrequencyMhz!.toStringAsFixed(0)} MHz" : "N/A"} '
|
||
'${cpu.baseFrequencyMhz != null ? "(база: ${cpu.baseFrequencyMhz!.toStringAsFixed(0)} MHz)" : ""}',
|
||
),
|
||
_buildInfoRow('Кэш L1 / L2 / L3:', '${cpu.l1Cache} / ${cpu.l2Cache} / ${cpu.l3Cache}'),
|
||
_buildInfoRow('Виртуализация:', cpu.virtualization ?? 'Отсутствует / Отключена'),
|
||
_buildInfoRow('Микрокод:', cpu.microcode),
|
||
if (cpu.flags.isNotEmpty) ...[
|
||
const SizedBox(height: 10),
|
||
const Text(
|
||
'Инструкции & Флаги:',
|
||
style: TextStyle(fontSize: 12, color: Color(0xFF8B949E), fontWeight: FontWeight.w500),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Wrap(
|
||
spacing: 6,
|
||
runSpacing: 6,
|
||
children: cpu.flags.take(16).map((flag) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(4),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Text(
|
||
flag,
|
||
style: const TextStyle(fontSize: 10, fontFamily: 'monospace', color: Color(0xFFC9D1D9)),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
if (cpu.flags.length > 16)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Text(
|
||
'+ еще ${cpu.flags.length - 16} флагов',
|
||
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildMotherboardCard() {
|
||
final mb = hardware.motherboard;
|
||
return Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Row(
|
||
children: [
|
||
Icon(Icons.developer_board_rounded, color: Color(0xFF58A6FF), size: 20),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
'Материнская плата & BIOS',
|
||
style: TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
Text(
|
||
'${mb.vendor} ${mb.productName}',
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
const Divider(color: Color(0xFF21262D), height: 1),
|
||
const SizedBox(height: 12),
|
||
_buildInfoRow('Производитель:', mb.vendor),
|
||
_buildInfoRow('Модель платы:', mb.productName),
|
||
_buildInfoRow('Ревизия / Версия:', mb.version),
|
||
_buildInfoRow('Тип шасси:', mb.chassisType),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'Параметры BIOS / UEFI:',
|
||
style: TextStyle(fontSize: 12, color: Color(0xFF8B949E), fontWeight: FontWeight.w500),
|
||
),
|
||
const SizedBox(height: 6),
|
||
_buildInfoRow('Разработчик BIOS:', mb.biosVendor),
|
||
_buildInfoRow('Версия BIOS:', mb.biosVersion),
|
||
_buildInfoRow('Дата релиза BIOS:', mb.biosDate),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildMemoryAndSwapSection() {
|
||
final mem = hardware.memory;
|
||
final total = mem.totalBytes.toDouble();
|
||
final used = mem.usedBytes.toDouble();
|
||
final ramPercent = total > 0 ? (used / total).clamp(0.0, 1.0) : 0.0;
|
||
|
||
final swapTotal = mem.swapTotalBytes.toDouble();
|
||
final swapUsed = mem.swapUsedBytes.toDouble();
|
||
final swapPercent = swapTotal > 0 ? (swapUsed / swapTotal).clamp(0.0, 1.0) : 0.0;
|
||
|
||
final hasEccErrors = mem.edacCeCount > BigInt.zero || mem.edacUeCount > BigInt.zero;
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.straighten_rounded, color: Color(0xFF58A6FF), size: 20),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'Оперативная память (RAM) & Подкачка (Swap)',
|
||
style: TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: hasEccErrors ? const Color(0xFFF85149).withAlpha(30) : const Color(0xFF3FB950).withAlpha(30),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(color: hasEccErrors ? const Color(0xFFF85149) : const Color(0xFF3FB950)),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
hasEccErrors ? Icons.warning_amber_rounded : Icons.check_circle_outline_rounded,
|
||
size: 14,
|
||
color: hasEccErrors ? const Color(0xFFF85149) : const Color(0xFF3FB950),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
hasEccErrors
|
||
? 'EDAC Ошибки: ${mem.edacCeCount} CE / ${mem.edacUeCount} UE'
|
||
: 'ECC / EDAC: Ошибок нет',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: hasEccErrors ? const Color(0xFFF85149) : const Color(0xFF3FB950),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
// RAM Bar
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'RAM: ${_formatBytes(mem.usedBytes)} из ${_formatBytes(mem.totalBytes)} занято (${(ramPercent * 100).toStringAsFixed(1)}%)',
|
||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFE6EDF3)),
|
||
),
|
||
Text(
|
||
'Доступно: ${_formatBytes(mem.availableBytes)}',
|
||
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(6),
|
||
child: LinearProgressIndicator(
|
||
value: ramPercent,
|
||
minHeight: 10,
|
||
backgroundColor: const Color(0xFF21262D),
|
||
valueColor: AlwaysStoppedAnimation<Color>(
|
||
ramPercent > 0.9 ? const Color(0xFFF85149) : (ramPercent > 0.75 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
// Swap Bar
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
swapTotal > 0
|
||
? 'Swap: ${_formatBytes(mem.swapUsedBytes)} из ${_formatBytes(mem.swapTotalBytes)} занято (${(swapPercent * 100).toStringAsFixed(1)}%)'
|
||
: 'Swap: Раздел подкачки не настроен (0 B)',
|
||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFE6EDF3)),
|
||
),
|
||
],
|
||
),
|
||
if (swapTotal > 0) ...[
|
||
const SizedBox(height: 8),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(6),
|
||
child: LinearProgressIndicator(
|
||
value: swapPercent,
|
||
minHeight: 8,
|
||
backgroundColor: const Color(0xFF21262D),
|
||
valueColor: AlwaysStoppedAnimation<Color>(
|
||
swapPercent > 0.8 ? const Color(0xFFF85149) : const Color(0xFF58A6FF),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildGpuSection() {
|
||
return Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.videogame_asset_rounded, color: Color(0xFF58A6FF), size: 20),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'Графические ускорители (GPU) [${hardware.gpus.length}]',
|
||
style: const TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
if (hardware.gpus.isEmpty)
|
||
const Text('Дискретные или интегрированные GPU не обнаружены', style: TextStyle(color: Color(0xFF8B949E)))
|
||
else
|
||
...hardware.gpus.map((gpu) {
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 10),
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(10),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: const Icon(Icons.tv_rounded, color: Color(0xFF58A6FF), size: 22),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
gpu.model,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'Вендор: ${gpu.vendor} • Слот PCI: ${gpu.pciSlot}',
|
||
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Text(
|
||
'Драйвер: ${gpu.driver}',
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
fontFamily: 'monospace',
|
||
color: Color(0xFF3FB950),
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStorageSection() {
|
||
return Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.storage_rounded, color: Color(0xFF58A6FF), size: 20),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'Накопители данных (NVMe / SSD / HDD) [${hardware.disks.length}]',
|
||
style: const TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
if (hardware.disks.isEmpty)
|
||
const Text('Дисковые накопители не обнаружены', style: TextStyle(color: Color(0xFF8B949E)))
|
||
else
|
||
...hardware.disks.map((disk) {
|
||
return _buildDiskCard(disk);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDiskCard(DiskInfo disk) {
|
||
final isSsd = disk.diskType.contains('SSD') || disk.diskType.contains('NVMe');
|
||
final healthVal = disk.healthPercent ?? (disk.smartStatus == 'PASSED' ? 100 : (disk.smartStatus == 'FAILED' ? 30 : 100));
|
||
final wearVal = disk.wearPercent ?? (100 - healthVal);
|
||
final healthProgress = (healthVal / 100.0).clamp(0.0, 1.0);
|
||
|
||
final Color healthColor = healthVal >= 80
|
||
? const Color(0xFF3FB950)
|
||
: (healthVal >= 50 ? const Color(0xFFD29922) : const Color(0xFFF85149));
|
||
|
||
IconData diskIcon = Icons.album_rounded;
|
||
Color typeColor = const Color(0xFFD29922);
|
||
if (disk.diskType.contains('NVMe')) {
|
||
diskIcon = Icons.flash_on_rounded;
|
||
typeColor = const Color(0xFFBC8CFF);
|
||
} else if (disk.diskType.contains('SSD')) {
|
||
diskIcon = Icons.save_rounded;
|
||
typeColor = const Color(0xFF58A6FF);
|
||
}
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(10),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Header: Icon, Name, Model, Badges
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(10),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Icon(diskIcon, color: typeColor, size: 24),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Wrap(
|
||
crossAxisAlignment: WrapCrossAlignment.center,
|
||
spacing: 8,
|
||
runSpacing: 4,
|
||
children: [
|
||
Text(
|
||
disk.name,
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w700,
|
||
fontFamily: 'monospace',
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
Text(
|
||
disk.model,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFFC9D1D9),
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: typeColor.withAlpha(30),
|
||
borderRadius: BorderRadius.circular(4),
|
||
border: Border.all(color: typeColor.withAlpha(120)),
|
||
),
|
||
child: Text(
|
||
disk.rotationalSpeedRpm != null && disk.rotationalSpeedRpm! > 0
|
||
? '${disk.diskType} • ${disk.rotationalSpeedRpm} RPM'
|
||
: disk.diskType,
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: typeColor,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'Объем: ${_formatBytes(disk.sizeBytes)}'
|
||
'${disk.serial.isNotEmpty ? " • S/N: ${disk.serial}" : ""}',
|
||
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
// SMART & Temp Badges
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: disk.smartStatus == 'PASSED'
|
||
? const Color(0xFF3FB950).withAlpha(30)
|
||
: (disk.smartStatus == 'FAILED'
|
||
? const Color(0xFFF85149).withAlpha(30)
|
||
: const Color(0xFF30363D)),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(
|
||
color: disk.smartStatus == 'PASSED'
|
||
? const Color(0xFF3FB950)
|
||
: (disk.smartStatus == 'FAILED'
|
||
? const Color(0xFFF85149)
|
||
: const Color(0xFF8B949E)),
|
||
),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
disk.smartStatus == 'PASSED'
|
||
? Icons.check_circle_outline_rounded
|
||
: (disk.smartStatus == 'FAILED'
|
||
? Icons.error_outline_rounded
|
||
: Icons.help_outline_rounded),
|
||
size: 13,
|
||
color: disk.smartStatus == 'PASSED'
|
||
? const Color(0xFF3FB950)
|
||
: (disk.smartStatus == 'FAILED'
|
||
? const Color(0xFFF85149)
|
||
: const Color(0xFF8B949E)),
|
||
),
|
||
const SizedBox(width: 5),
|
||
Text(
|
||
'SMART: ${disk.smartStatus}',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w700,
|
||
color: disk.smartStatus == 'PASSED'
|
||
? const Color(0xFF3FB950)
|
||
: (disk.smartStatus == 'FAILED'
|
||
? const Color(0xFFF85149)
|
||
: const Color(0xFF8B949E)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (disk.temperatureC != null) ...[
|
||
const SizedBox(height: 6),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(4),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
Icons.thermostat_rounded,
|
||
size: 13,
|
||
color: disk.temperatureC! > 55 ? const Color(0xFFF85149) : const Color(0xFF3FB950),
|
||
),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
'${disk.temperatureC}°C',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: disk.temperatureC! > 55 ? const Color(0xFFF85149) : const Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
|
||
// ПОЛОСКА ИЗНОСА И ЗДОРОВЬЯ НАКОПИТЕЛЯ
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
isSsd ? Icons.speed_rounded : Icons.health_and_safety_rounded,
|
||
size: 15,
|
||
color: healthColor,
|
||
),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
isSsd
|
||
? 'Здоровье SSD: $healthVal% • Износ ячеек памяти: $wearVal%'
|
||
: 'Состояние HDD: ${healthVal >= 90 ? "Отличное" : "Внимание"} ($healthVal%)',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
color: healthColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
Text(
|
||
isSsd ? 'Остаточный ресурс: $healthVal%' : 'Надежность: $healthVal%',
|
||
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(5),
|
||
child: LinearProgressIndicator(
|
||
value: healthProgress,
|
||
minHeight: 8,
|
||
backgroundColor: const Color(0xFF21262D),
|
||
valueColor: AlwaysStoppedAnimation<Color>(healthColor),
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
// Метрики накопителя
|
||
Wrap(
|
||
spacing: 16,
|
||
runSpacing: 6,
|
||
children: [
|
||
if (disk.powerOnHours != null)
|
||
_buildDiskMetric(
|
||
Icons.timer_outlined,
|
||
'Наработка',
|
||
'${disk.powerOnHours} ч. (${(disk.powerOnHours!.toDouble() / 24.0).toStringAsFixed(0)} дн.)',
|
||
),
|
||
if (disk.powerCycleCount != null)
|
||
_buildDiskMetric(
|
||
Icons.power_settings_new_rounded,
|
||
'Включений',
|
||
'${disk.powerCycleCount} раз',
|
||
),
|
||
_buildDiskMetric(
|
||
Icons.warning_amber_rounded,
|
||
'Бэд-секторы',
|
||
'${disk.badSectorsCount ?? 0}',
|
||
highlightColor: (disk.badSectorsCount != null && disk.badSectorsCount! > BigInt.zero)
|
||
? const Color(0xFFF85149)
|
||
: const Color(0xFF3FB950),
|
||
),
|
||
if (disk.totalWrittenTb != null)
|
||
_buildDiskMetric(
|
||
Icons.data_exploration_rounded,
|
||
'Записано (TBW)',
|
||
'${disk.totalWrittenTb!.toStringAsFixed(2)} TB',
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// РАЗДЕЛЫ И ТОЧКИ МОНТИРОВАНИЯ
|
||
if (disk.partitions.isNotEmpty) ...[
|
||
const SizedBox(height: 12),
|
||
const Text(
|
||
'Разделы и файловые системы:',
|
||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF8B949E)),
|
||
),
|
||
const SizedBox(height: 6),
|
||
...disk.partitions.map((part) {
|
||
final pTotal = part.sizeBytes.toDouble();
|
||
final pUsed = part.usedBytes.toDouble();
|
||
final pPercent = pTotal > 0 && pUsed > 0 ? (pUsed / pTotal).clamp(0.0, 1.0) : 0.0;
|
||
final isMounted = part.mountPoint != null && part.mountPoint!.isNotEmpty;
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 6),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(
|
||
part.name,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
fontFamily: 'monospace',
|
||
color: Color(0xFF58A6FF),
|
||
),
|
||
),
|
||
if (part.fsType.isNotEmpty) ...[
|
||
const SizedBox(width: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
part.fsType,
|
||
style: const TextStyle(fontSize: 10, color: Color(0xFF8B949E)),
|
||
),
|
||
),
|
||
],
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
isMounted ? '➔ ${part.mountPoint}' : '• не смонтирован',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: isMounted ? const Color(0xFFE6EDF3) : const Color(0xFF8B949E),
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
isMounted && pUsed > 0
|
||
? '${_formatBytes(part.usedBytes)} из ${_formatBytes(part.sizeBytes)} (${(pPercent * 100).toStringAsFixed(0)}%)'
|
||
: _formatBytes(part.sizeBytes),
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFFC9D1D9),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
if (isMounted && pUsed > 0) ...[
|
||
const SizedBox(height: 6),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(3),
|
||
child: LinearProgressIndicator(
|
||
value: pPercent,
|
||
minHeight: 4,
|
||
backgroundColor: const Color(0xFF21262D),
|
||
valueColor: AlwaysStoppedAnimation<Color>(
|
||
pPercent > 0.9
|
||
? const Color(0xFFF85149)
|
||
: (pPercent > 0.75 ? const Color(0xFFD29922) : const Color(0xFF58A6FF)),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDiskMetric(IconData icon, String label, String value, {Color? highlightColor}) {
|
||
return Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, size: 13, color: highlightColor ?? const Color(0xFF8B949E)),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
'$label: ',
|
||
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
|
||
),
|
||
Text(
|
||
value,
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: highlightColor ?? const Color(0xFFE6EDF3),
|
||
fontFamily: 'monospace',
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildNetworkSection() {
|
||
return Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.settings_ethernet_rounded, color: Color(0xFF58A6FF), size: 20),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'Сетевые интерфейсы [${hardware.networkAdapters.length}]',
|
||
style: const TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
if (hardware.networkAdapters.isEmpty)
|
||
const Text('Сетевые интерфейсы не обнаружены', style: TextStyle(color: Color(0xFF8B949E)))
|
||
else
|
||
...hardware.networkAdapters.map((nic) {
|
||
final isUp = nic.state.toUpperCase() == 'UP';
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 10),
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(10),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Icon(
|
||
nic.ifType.contains('Wi-Fi') || nic.ifType.contains('Wireless')
|
||
? Icons.wifi_rounded
|
||
: Icons.lan_rounded,
|
||
color: isUp ? const Color(0xFF3FB950) : const Color(0xFF8B949E),
|
||
size: 22,
|
||
),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(
|
||
nic.interfaceName,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w700,
|
||
fontFamily: 'monospace',
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
nic.ifType,
|
||
style: const TextStyle(fontSize: 10, color: Color(0xFF8B949E)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'MAC: ${nic.macAddress} • Драйвер: ${nic.driver} ${nic.speedMbps != null ? "• ${nic.speedMbps} Mbps" : ""}',
|
||
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: isUp ? const Color(0xFF3FB950).withAlpha(30) : const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(color: isUp ? const Color(0xFF3FB950) : const Color(0xFF30363D)),
|
||
),
|
||
child: Text(
|
||
nic.state.toUpperCase(),
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w700,
|
||
color: isUp ? const Color(0xFF3FB950) : const Color(0xFF8B949E),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildThermalsAndBatterySection() {
|
||
final therm = hardware.thermals;
|
||
final bat = hardware.battery;
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161B22),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Row(
|
||
children: [
|
||
Icon(Icons.thermostat_rounded, color: Color(0xFF58A6FF), size: 20),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
'Температурный режим & Электропитание',
|
||
style: TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text('Температура CPU (макс.)', style: TextStyle(fontSize: 12, color: Color(0xFF8B949E))),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
therm.maxCpuTempC > 0 ? '${therm.maxCpuTempC.toStringAsFixed(1)} °C' : 'Н/Д',
|
||
style: TextStyle(
|
||
fontSize: 22,
|
||
fontWeight: FontWeight.w700,
|
||
color: therm.maxCpuTempC > 80
|
||
? const Color(0xFFF85149)
|
||
: (therm.maxCpuTempC > 65 ? const Color(0xFFD29922) : const Color(0xFF3FB950)),
|
||
),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
therm.isActivelyThrottling
|
||
? 'ВНИМАНИЕ: Троттлинг активен!'
|
||
: (therm.totalThrottleEvents > BigInt.zero
|
||
? 'Событий троттлинга: ${therm.totalThrottleEvents}'
|
||
: 'Троттлинг не фиксировался'),
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: therm.isActivelyThrottling ? const Color(0xFFF85149) : const Color(0xFF8B949E),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
if (bat != null) ...[
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF21262D),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFF30363D)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'Батарея: ${bat.name}',
|
||
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
||
),
|
||
Text(
|
||
bat.status,
|
||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF58A6FF)),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
'${bat.capacityPercent}%',
|
||
style: const TextStyle(
|
||
fontSize: 22,
|
||
fontWeight: FontWeight.w700,
|
||
color: Color(0xFFE6EDF3),
|
||
),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
'Здоровье: ${bat.healthPercent.toStringAsFixed(1)}% • Циклов: ${bat.cycleCount} ${bat.isDegraded ? "• Износ высок!" : ""}',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: bat.isDegraded ? const Color(0xFFF85149) : const Color(0xFF8B949E),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildInfoRow(String label, String value) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 6),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(
|
||
width: 150,
|
||
child: Text(
|
||
label,
|
||
style: const TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Text(
|
||
value,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
color: Color(0xFFE6EDF3),
|
||
fontFamily: 'monospace',
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|