Files
diagd/lib/widgets/crashes_tab.dart
T

487 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:diagd/src/rust/api/diagnostics.dart';
class CrashesTab extends StatefulWidget {
final List<CrashReportInfo> crashes;
const CrashesTab({
super.key,
required this.crashes,
});
@override
State<CrashesTab> createState() => _CrashesTabState();
}
class _CrashesTabState extends State<CrashesTab> {
int _selectedCrashIndex = 0;
@override
Widget build(BuildContext context) {
if (widget.crashes.isEmpty) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 550),
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF238636).withAlpha(80)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF238636).withAlpha(30),
shape: BoxShape.circle,
),
child: const Icon(
Icons.verified_rounded,
color: Color(0xFF3FB950),
size: 44,
),
),
const SizedBox(height: 18),
const Text(
'Аварийных падений не зафиксировано',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(height: 8),
const Text(
'В системных журналах coredumpctl, /var/crash, dmesg и crash reporter отсутствуют записи о падениях процессов по сигналам SIGSEGV, SIGABRT или сбоям ядра.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: Color(0xFF8B949E), height: 1.4),
),
],
),
),
);
}
final selectedCrash = widget.crashes[_selectedCrashIndex.clamp(0, widget.crashes.length - 1)];
return LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 850;
if (isWide) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column: Crash list
SizedBox(
width: 340,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildListHeader(),
const SizedBox(height: 10),
Expanded(
child: ListView.separated(
itemCount: widget.crashes.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final crash = widget.crashes[index];
final isSelected = index == _selectedCrashIndex;
return _buildCrashListItem(crash, isSelected, () {
setState(() {
_selectedCrashIndex = index;
});
});
},
),
),
],
),
),
const SizedBox(width: 20),
// Right Column: Crash details
Expanded(
child: SingleChildScrollView(
child: _buildCrashDetails(context, selectedCrash),
),
),
],
),
);
} else {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildListHeader(),
const SizedBox(height: 10),
SizedBox(
height: 180,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: widget.crashes.length,
separatorBuilder: (context, index) => const SizedBox(width: 10),
itemBuilder: (context, index) {
final crash = widget.crashes[index];
final isSelected = index == _selectedCrashIndex;
return SizedBox(
width: 260,
child: _buildCrashListItem(crash, isSelected, () {
setState(() {
_selectedCrashIndex = index;
});
}),
);
},
),
),
const SizedBox(height: 20),
_buildCrashDetails(context, selectedCrash),
],
),
);
}
},
);
}
Widget _buildListHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'ЖУРНАЛ АВАРИЙНЫХ СБОЕВ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(25),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFF85149).withAlpha(70)),
),
child: Text(
'${widget.crashes.length}',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
],
);
}
Widget _buildCrashListItem(CrashReportInfo crash, bool isSelected, VoidCallback onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF21262D) : const Color(0xFF161B22),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? const Color(0xFFF85149) : const Color(0xFF30363D),
width: isSelected ? 1.5 : 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(30),
borderRadius: BorderRadius.circular(4),
),
child: Text(
crash.signalOrCode,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
crash.processName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
),
],
),
const SizedBox(height: 6),
Text(
crash.timestamp,
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
),
if (crash.pid != null) ...[
const SizedBox(height: 2),
Text(
'PID: ${crash.pid}',
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E), fontFamily: 'monospace'),
),
],
],
),
),
);
}
Widget _buildCrashDetails(BuildContext context, CrashReportInfo crash) {
final debugCmd = crash.dumpPath != null && crash.dumpPath!.isNotEmpty
? 'gdb ${crash.processName} ${crash.dumpPath!}'
: (crash.pid != null ? 'coredumpctl debug ${crash.pid}' : 'coredumpctl info ${crash.processName}');
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.bug_report_rounded, color: Color(0xFFF85149), size: 24),
const SizedBox(width: 10),
Text(
crash.processName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFF85149).withAlpha(25),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFF85149).withAlpha(70)),
),
child: Text(
crash.signalOrCode,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFFF85149),
),
),
),
],
),
if (crash.pid != null)
Text(
'PID: ${crash.pid}',
style: const TextStyle(
fontSize: 13,
color: Color(0xFF8B949E),
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 16),
// Metadata Grid
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF21262D)),
),
child: Column(
children: [
_buildMetaRow('Время падения:', crash.timestamp),
if (crash.faultModule != null && crash.faultModule!.isNotEmpty)
_buildMetaRow('Сбойный модуль:', crash.faultModule!),
if (crash.dumpPath != null && crash.dumpPath!.isNotEmpty) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: 130,
child: Text(
'Файл Core Dump:',
style: TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
),
Expanded(
child: Text(
crash.dumpPath!,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF58A6FF),
fontFamily: 'monospace',
),
),
),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 14, color: Color(0xFF8B949E)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: 'Копировать путь к дампу',
onPressed: () {
Clipboard.setData(ClipboardData(text: crash.dumpPath!));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Путь к Core Dump скопирован'), duration: Duration(seconds: 2)),
);
},
),
],
),
],
],
),
),
const SizedBox(height: 20),
// Debug CLI Command
const Text(
'КОМАНДА ОТЛАДКИ (GDB / COREDUMPCTL)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Row(
children: [
const Text(
'\$ ',
style: TextStyle(color: Color(0xFF3FB950), fontWeight: FontWeight.bold),
),
Expanded(
child: Text(
debugCmd,
style: const TextStyle(
color: Color(0xFFE6EDF3),
fontFamily: 'monospace',
fontSize: 13,
),
),
),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF8B949E)),
tooltip: 'Копировать команду',
onPressed: () {
Clipboard.setData(ClipboardData(text: debugCmd));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Команда отладки скопирована'), duration: Duration(seconds: 2)),
);
},
),
],
),
),
const SizedBox(height: 20),
// Stack Trace / Backtrace
const Text(
'СТЕК ВЫЗОВОВ (BACKTRACE / CRASH TRACE)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF21262D)),
),
child: crash.stackTraceSnippet.isNotEmpty
? SelectableText(
crash.stackTraceSnippet,
style: const TextStyle(
color: Color(0xFFE6EDF3),
fontFamily: 'monospace',
fontSize: 12,
height: 1.4,
),
)
: const Text(
'Трассировка стека недоступна в данном дампе памяти.',
style: TextStyle(color: Color(0xFF8B949E), fontSize: 12),
),
),
],
),
);
}
Widget _buildMetaRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
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),
),
),
),
],
),
);
}
}