Files
diagd/lib/widgets/hardware_tab.dart

1250 lines
49 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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',
),
),
),
],
),
);
}
}