import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'badnote_log.dart'; import 'diagnostic_export.dart'; import '../editor/canvas/input_diagnostics.dart'; import '../editor/input/diagnostic_logger.dart'; import '../editor/input/pen_input_service.dart'; /// Shared diagnostics chrome: overlay readout + export action. /// Mount on any document surface (note / PDF / PPT / board). class DiagnosticChrome extends StatefulWidget { const DiagnosticChrome({ super.key, required this.child, this.initiallyVisible = false, }); final Widget child; final bool initiallyVisible; @override State createState() => DiagnosticChromeState(); } class DiagnosticChromeState extends State { late bool _visible = widget.initiallyVisible; bool _exporting = false; String? _lastExportPath; bool get isVisible => _visible; void toggle() { setState(() { _visible = !_visible; if (_visible) { DiagnosticLogger.instance.start(); InputDiagnostics.instance.reset(); } else { DiagnosticLogger.instance.stop(); } }); } Future exportPack() async { if (_exporting) return; setState(() => _exporting = true); try { final result = await DiagnosticExport.instance.exportPack(); if (!mounted) return; setState(() => _lastExportPath = result.zipPath); await Clipboard.setData(ClipboardData(text: result.zipPath)); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '诊断包已导出 (${result.bytes} bytes)\n路径已复制到剪贴板', ), duration: const Duration(seconds: 5), ), ); } catch (e) { BadNoteLog.instance.error(LogSubsystem.diag, 'export_failed', fields: { 'error': '$e', }); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('导出失败: $e')), ); } finally { if (mounted) setState(() => _exporting = false); } } @override Widget build(BuildContext context) { return Stack( fit: StackFit.expand, children: [ widget.child, if (_visible) Positioned( left: 8, right: 8, bottom: 8, child: Material( elevation: 6, borderRadius: BorderRadius.circular(8), color: Colors.black.withValues(alpha: 0.82), child: Padding( padding: const EdgeInsets.all(10), child: DefaultTextStyle( style: const TextStyle( color: Colors.white, fontSize: 11, fontFamily: 'monospace', height: 1.35, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ListenableBuilder( listenable: InputDiagnostics.instance, builder: (context, _) { return Text( '${InputDiagnostics.instance.summary()}\n' '${PenInputService.instance.debugSummary}\n' 'log: ${BadNoteLog.instance.path ?? "(starting…)"}\n' 'session: ${BadNoteLog.instance.sessionId}' '${_lastExportPath != null ? "\nlast zip: $_lastExportPath" : ""}', ); }, ), const SizedBox(height: 8), Row( children: [ TextButton( onPressed: () => InputDiagnostics.instance.reset(), child: const Text('Reset', style: TextStyle(color: Colors.white70)), ), TextButton( onPressed: _exporting ? null : exportPack, child: Text( _exporting ? 'Exporting…' : 'Export pack', style: const TextStyle(color: Colors.lightGreenAccent), ), ), TextButton( onPressed: toggle, child: const Text('Hide', style: TextStyle(color: Colors.white54)), ), ], ), ], ), ), ), ), ), ], ); } } /// Compact icon button for app bars / toolbars. class DiagnosticToggleButton extends StatelessWidget { const DiagnosticToggleButton({ super.key, required this.onToggle, required this.onExport, }); final VoidCallback onToggle; final VoidCallback onExport; @override Widget build(BuildContext context) { return PopupMenuButton( tooltip: 'Diagnostics', icon: const Icon(Icons.bug_report_outlined), onSelected: (v) { if (v == 'toggle') onToggle(); if (v == 'export') onExport(); }, itemBuilder: (context) => const [ PopupMenuItem(value: 'toggle', child: Text('Toggle overlay')), PopupMenuItem(value: 'export', child: Text('Export diagnostic pack')), ], ); } }