import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:diagd/src/rust/api/diagnostics.dart'; class CrashesTab extends StatefulWidget { final List crashes; const CrashesTab({ super.key, required this.crashes, }); @override State createState() => _CrashesTabState(); } class _CrashesTabState extends State { 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), ), ), ), ], ), ); } }