import 'package:flutter/material.dart'; import '../brew/brew_client.dart'; /// Runs a [BrewTask] and shows its output as it streams in. class TaskDialog extends StatefulWidget { const TaskDialog({super.key, required this.task}); final BrewTask task; @override State createState() => _TaskDialogState(); } class _TaskDialogState extends State { final _scroll = ScrollController(); @override void initState() { super.initState(); widget.task.addListener(_onOutput); widget.task.run(); } @override void dispose() { widget.task.removeListener(_onOutput); _scroll.dispose(); super.dispose(); } void _onOutput() { if (!mounted) return; // Follow the output, unless the user scrolled up to read something. final follow = !_scroll.hasClients || _scroll.position.pixels >= _scroll.position.maxScrollExtent - 40; setState(() {}); if (follow) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scroll.hasClients) { _scroll.jumpTo(_scroll.position.maxScrollExtent); } }); } } @override Widget build(BuildContext context) { final task = widget.task; final scheme = Theme.of(context).colorScheme; final Widget status = task.running ? const SizedBox.square( dimension: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : task.succeeded ? Icon(Icons.check_circle, color: scheme.primary) : Icon(Icons.error, color: scheme.error); return AlertDialog( title: Row( children: [ status, const SizedBox(width: 12), Expanded(child: Text(task.title)), ], ), content: SizedBox( width: 720, height: 420, child: Container( decoration: BoxDecoration( color: scheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.all(12), child: SingleChildScrollView( controller: _scroll, child: SelectableText( task.lines.join('\n'), style: const TextStyle(fontFamily: 'Menlo', fontSize: 12), ), ), ), ), actions: [ if (!task.running && !task.succeeded) Padding( padding: const EdgeInsets.only(right: 8), child: Text( 'Exited with code ${task.exitCode}', style: TextStyle(color: scheme.error), ), ), if (task.running) TextButton(onPressed: task.cancel, child: const Text('Cancel')) else FilledButton( onPressed: () => Navigator.pop(context), child: const Text('Close'), ), ], ); } }