import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../api/api.dart'; import '../rid/rid_aircraft.dart'; import '../rid/rid_service.dart'; import 'flight_session.dart'; /// A logbook entry that could not be filed (offline): kept until it uploads. class PendingFlight { const PendingFlight(this.id, this.body, this.error); final String id; final Map body; final String error; String get summary => '${body['operationDate']} ${body['startTime']}–${body['endTime']}'; } /// Owns the current [FlightSession]: feeds it the paired aircraft's fixes, /// persists it, and files it to the logbook (queueing when offline). class SessionController extends ChangeNotifier { SessionController({required this.api, required this.rid}); final Api api; final RidService rid; FlightSession session = FlightSession(); final List pending = []; StreamSubscription? _sub; Timer? _tick; DateTime _lastSave = DateTime.fromMillisecondsSinceEpoch(0); SharedPreferences? _prefs; bool filing = false; Future init() async { _prefs = await SharedPreferences.getInstance(); final String? raw = _prefs!.getString('session'); if (raw != null) { try { session = FlightSession.decode(raw); } catch (_) { session = FlightSession(); } } final String? q = _prefs!.getString('pending_flights'); if (q != null) { try { for (final Object? e in jsonDecode(q) as List) { final Map m = (e! as Map).cast(); pending.add(PendingFlight('${m['id']}', (m['body'] as Map).cast(), '${m['error'] ?? ''}')); } } catch (_) {} } _sub = rid.pairedLocations.listen(_onFix); _tick = Timer.periodic(const Duration(seconds: 1), (_) { if (session.onTick(DateTime.now())) _save(force: true); // Elapsed time on screen. if (session.phase == SessionPhase.active || session.phase == SessionPhase.armed) notifyListeners(); }); notifyListeners(); } void _onFix(RidAircraft a) { if (session.ridSerial.isNotEmpty && a.serial != session.ridSerial) return; final bool changed = session.onFix(FixSample( at: a.lastLocationAt ?? DateTime.now(), airborne: a.isAirborne, lat: a.latitude, lon: a.longitude, height: a.height, speed: a.speedHorizontal, )); _save(force: changed); if (changed) notifyListeners(); } /// Briefing done: wait for take-off. void arm({ required String droneId, required String droneName, required String purpose, required String category, required String areaRoute, required String weather, }) { session = FlightSession() ..droneId = droneId ..droneName = droneName ..ridSerial = rid.pairing?.serial ?? '' ..purpose = purpose ..category = category ..areaRoute = areaRoute ..weather = weather; session.arm(DateTime.now()); _save(force: true); notifyListeners(); } void startNow() { session.start(DateTime.now()); _save(force: true); notifyListeners(); } void stopNow() { session.stop(DateTime.now()); _save(force: true); notifyListeners(); } void resume() { session.resume(); _save(force: true); notifyListeners(); } void discard() { session = FlightSession(); _save(force: true); notifyListeners(); } /// Saves review edits made on the landed screen. void edited() { _save(force: true); notifyListeners(); } /// Files the landed session. Returns null on success, else a message; a /// network failure queues the entry instead of losing it. Future file() async { final Map body = session.toFlightInput(); filing = true; notifyListeners(); try { await api.createFlight(body); session = FlightSession(); return null; } on ApiException catch (e) { if (!e.isNetwork) return e.message; // the pilot has to fix the entry pending.add(PendingFlight(DateTime.now().millisecondsSinceEpoch.toString(), body, e.message)); _savePending(); session = FlightSession(); return 'Offline: the entry is queued and will be filed when the server is reachable.'; } finally { filing = false; _save(force: true); notifyListeners(); } } /// Retries queued entries. Returns how many were filed. Future retryPending() async { int ok = 0; for (final PendingFlight p in List.of(pending)) { try { await api.createFlight(p.body); pending.remove(p); ok++; } on ApiException catch (e) { final int i = pending.indexOf(p); if (i >= 0) pending[i] = PendingFlight(p.id, p.body, e.message); if (e.isNetwork) break; } } _savePending(); notifyListeners(); return ok; } void dropPending(PendingFlight p) { pending.remove(p); _savePending(); notifyListeners(); } void _save({bool force = false}) { final DateTime now = DateTime.now(); // A fix a second would otherwise rewrite the whole track every second. if (!force && now.difference(_lastSave) < const Duration(seconds: 10)) return; _lastSave = now; if (session.phase == SessionPhase.idle) { _prefs?.remove('session'); } else { _prefs?.setString('session', session.encode()); } } void _savePending() { _prefs?.setString( 'pending_flights', jsonEncode(pending .map((PendingFlight p) => {'id': p.id, 'body': p.body, 'error': p.error}) .toList()), ); } @override void dispose() { _save(force: true); _sub?.cancel(); _tick?.cancel(); super.dispose(); } }