import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:iperf3gui/domain/models/iperf3_event.dart'; import 'package:iperf3gui/domain/models/iperf3_result.dart'; import 'package:iperf3gui/domain/models/iperf3_session.dart'; import 'package:iperf3gui/domain/models/test_configuration.dart'; import 'package:iperf3gui/domain/services/iperf3_runner.dart'; import 'package:iperf3gui/ui/features/test_run/iperf3_event_formatter.dart'; /// State of the test run. enum TestRunState { idle, running, completed, error } /// ViewModel for managing test run state. /// /// Presentation only. What happened during a run is accumulated by /// [Iperf3Session] in the domain layer; this class subscribes to /// [Iperf3Runner.events], feeds that session, keeps a formatted transcript for /// display, and decides when to repaint. class TestRunViewModel extends ChangeNotifier { final Iperf3Runner iperf3Service; /// How often to notify listeners while output is arriving. iperf3 can report /// several times a second across many streams, and each rebuild re-renders /// the whole transcript — so repaints are coalesced instead of one per event. static const Duration displayInterval = Duration(milliseconds: 250); TestRunViewModel({required this.iperf3Service}) { _eventSubscription = iperf3Service.events.listen(_onEvent); } StreamSubscription? _eventSubscription; Timer? _repaintTimer; bool _dirty = false; TestRunState _state = TestRunState.idle; TestRunState get state => _state; Iperf3Result? _result; Iperf3Result? get result => _result; TestConfiguration? _currentConfig; TestConfiguration? get currentConfig => _currentConfig; final StringBuffer _transcript = StringBuffer(); /// Human-readable output accumulated so far. Display only — the structured /// record of the run is [session]. String get output => _transcript.toString(); final Iperf3Session _session = Iperf3Session(); /// The accumulated run: interval series, totals, errors and their statistics. Iperf3Session get session => _session; /// Whether a test is currently in flight. bool get isRunning => _state == TestRunState.running; /// Whether the most recent finished run ended because the user stopped it. bool get wasStopped => _result?.stopped ?? false; /// Start a test with the given configuration. Future runTest(TestConfiguration config) async { if (_state == TestRunState.running) return; if (!config.isValid) { _resetRunData(); _transcript .writeln('Invalid configuration: check the server address and port.'); _state = TestRunState.error; notifyListeners(); return; } _resetRunData(); _state = TestRunState.running; _currentConfig = config; notifyListeners(); try { final result = await iperf3Service.run(config); _result = result; if (_transcript.isEmpty && result.rawOutput.isNotEmpty) { _transcript.write(result.rawOutput); } _state = result.exitCode == 0 ? TestRunState.completed : TestRunState.error; } catch (e) { _transcript.writeln('Test failed: $e'); _state = TestRunState.error; } finally { _stopRepaintTimer(); notifyListeners(); } } /// Request that a running test stop early. Future stopTest() async { if (_state != TestRunState.running) return; await iperf3Service.stop(); } /// Reset to idle state. void reset() { _resetRunData(); _state = TestRunState.idle; _currentConfig = null; notifyListeners(); } void _resetRunData() { _result = null; _transcript.clear(); _session.clear(); _dirty = false; } void _onEvent(Iperf3Event event) { _session.add(event); final line = formatIperf3Event(event); if (line != null && line.isNotEmpty) { _transcript.writeln(line); } _scheduleRepaint(); } /// Coalesce repaints: mark dirty and let a timer flush at [displayInterval]. void _scheduleRepaint() { _dirty = true; _repaintTimer ??= Timer.periodic(displayInterval, (_) { if (!_dirty) return; _dirty = false; notifyListeners(); }); } void _stopRepaintTimer() { _repaintTimer?.cancel(); _repaintTimer = null; _dirty = false; } @override void dispose() { _stopRepaintTimer(); _eventSubscription?.cancel(); iperf3Service.dispose(); super.dispose(); } }