import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class LogViewerScreen extends StatefulWidget { final String runId; final dynamic api; final ValueChanged? onStatusChanged; const LogViewerScreen({ super.key, required this.runId, required this.api, this.onStatusChanged, }); @override State createState() => _LogViewerScreenState(); } class _LogViewerScreenState extends State { String _log = ''; bool _loading = true; String? _error; Timer? _pollTimer; final ScrollController _scrollController = ScrollController(); bool _autoScroll = true; Future _loadLog() async { try { final log = await widget.api.getRunLog(widget.runId); final changed = log != _log; setState(() { _log = log; _loading = false; }); if (changed && _autoScroll && mounted) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 200), curve: Curves.easeOut, ); } } catch (e) { setState(() { _error = e.toString(); _loading = false; }); } } @override void initState() { super.initState(); _loadLog(); _pollTimer = Timer.periodic(const Duration(seconds: 2), (_) => _loadLog()); } @override void dispose() { _pollTimer?.cancel(); _scrollController.dispose(); super.dispose(); } Future _copyLog() async { await Clipboard.setData(ClipboardData(text: _log)); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Log copied to clipboard'))); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Run Log'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _loadLog, tooltip: 'Refresh', ), IconButton( icon: const Icon(Icons.copy), onPressed: _log.isNotEmpty ? _copyLog : null, tooltip: 'Copy', ), IconButton( icon: Icon( _autoScroll ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_up, ), onPressed: () => setState(() => _autoScroll = !_autoScroll), tooltip: _autoScroll ? 'Auto-scroll ON' : 'Auto-scroll OFF', ), ], ), body: _loading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center(child: Text(_error!)) : _log.isEmpty ? const Center( child: Text('Log is empty', style: TextStyle(color: Colors.grey)), ) : SelectionArea( child: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(16), child: Text( _log, style: const TextStyle( fontFamily: 'monospace', fontSize: 13, height: 1.5, ), ), ), ), ); } }