import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../debug/log_store.dart'; import '../theme/tokens.dart'; /// Beta-testing console: shows the app's captured `print`/`debugPrint` output /// and uncaught errors (see [LogStore], wired up in `main`). Read-only view /// with copy-all / clear / follow controls — no debugger needed on-device. class DebugScreen extends StatefulWidget { const DebugScreen({super.key}); @override State createState() => _DebugScreenState(); } class _DebugScreenState extends State { static const _errorColor = Color(0xFFE06C75); final _controller = ScrollController(); /// When true, new lines keep the view pinned to the bottom (tail -f style). /// Flipped off automatically when the user scrolls up to read history. bool _follow = true; @override void initState() { super.initState(); _controller.addListener(_onScroll); LogStore.instance.addListener(_onLog); } @override void dispose() { LogStore.instance.removeListener(_onLog); _controller.removeListener(_onScroll); _controller.dispose(); super.dispose(); } void _onScroll() { if (!_controller.hasClients) return; final atBottom = _controller.offset >= _controller.position.maxScrollExtent - 24; if (atBottom != _follow) setState(() => _follow = atBottom); } void _onLog() { if (!mounted) return; setState(() {}); if (_follow) { WidgetsBinding.instance.addPostFrameCallback((_) => _jumpToBottom()); } } void _jumpToBottom() { if (!_controller.hasClients) return; _controller.jumpTo(_controller.position.maxScrollExtent); } Future _copyAll() async { await Clipboard.setData(ClipboardData(text: LogStore.instance.asText())); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Log copied to clipboard')), ); } @override Widget build(BuildContext context) { return Container( color: TimbreColors.background, child: Column( children: [ _header(), Expanded( child: ListenableBuilder( listenable: LogStore.instance, builder: (context, _) { final entries = LogStore.instance.entries; if (entries.isEmpty) { return Center( child: Text( 'No output captured yet.', style: TextStyle(color: TimbreColors.dimmed), ), ); } return Scrollbar( controller: _controller, child: ListView.builder( controller: _controller, padding: const EdgeInsets.symmetric( horizontal: TimbreSpacing.lg, vertical: TimbreSpacing.sm, ), itemCount: entries.length, itemBuilder: (context, i) => _line(entries[i]), ), ); }, ), ), ], ), ); } Widget _header() { return Container( decoration: BoxDecoration( color: TimbreColors.surface, border: Border(bottom: BorderSide(color: TimbreColors.border)), ), padding: const EdgeInsets.symmetric( horizontal: TimbreSpacing.lg, vertical: TimbreSpacing.sm, ), child: Row( children: [ Expanded( child: ListenableBuilder( listenable: LogStore.instance, builder: (context, _) => Text( 'console · ${LogStore.instance.length} lines', style: TextStyle(color: TimbreColors.dimmed, fontSize: 12), ), ), ), _action( _follow ? Icons.vertical_align_bottom : Icons.pause, _follow ? 'follow' : 'paused', () { setState(() => _follow = !_follow); if (_follow) _jumpToBottom(); }, active: _follow, ), _action(Icons.copy, 'copy', _copyAll), _action(Icons.delete_outline, 'clear', LogStore.instance.clear), ], ), ); } Widget _action(IconData icon, String label, VoidCallback onTap, {bool active = false}) { final accent = Theme.of(context).colorScheme.primary; final color = active ? accent : TimbreColors.dimmed; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.md), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: color), const SizedBox(width: TimbreSpacing.xs), Text(label, style: TextStyle(color: color, fontSize: 12)), ], ), ), ); } Widget _line(LogEntry e) { final isError = e.level == LogLevel.error; return Padding( padding: const EdgeInsets.only(bottom: 2), child: Text.rich( TextSpan( children: [ TextSpan( text: '${e.timeLabel} ', style: TextStyle(color: TimbreColors.dimmed, fontSize: 11), ), TextSpan( text: e.text, style: TextStyle( color: isError ? _errorColor : TimbreColors.foreground, fontSize: 12, height: 1.35, ), ), ], ), ), ); } }