import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// One thing the pilot checks by hand before a flight (plan P9 pre-flight /// checklist). The aircraft's own checks are `AircraftChecklist`'s live rows. @immutable class PreflightItem { const PreflightItem(this.id, this.group, this.title, this.detail); final String id; final String group; final String title; final String detail; } /// The manual checks, in walk-around order. const List preflightItems = [ PreflightItem('site', 'Site', 'Airspace and site checked', 'Fly zones on the map, local restrictions, permission for the take-off spot, and people who are not part of the flight kept clear.'), PreflightItem('weather', 'Site', 'Weather within limits', 'Wind below the aircraft\'s limit at height, no rain or fog, and enough daylight for the whole flight.'), PreflightItem('takeoffArea', 'Site', 'Take-off area clear', 'Flat, open, away from metal and magnetic interference, with room to land.'), PreflightItem('props', 'Aircraft', 'Propellers secure and undamaged', 'No nicks or cracks; each one locked on the right motor.'), PreflightItem('frame', 'Aircraft', 'Arms unfolded, gimbal clamp off', 'Frame undamaged, lens and vision sensors clean.'), PreflightItem('batteries', 'Aircraft', 'Batteries charged and seated', 'Aircraft battery clicked in and warm enough; remote controller and phone charged.'), PreflightItem('sdcard', 'Aircraft', 'SD card in, with space', 'Format it in the camera if it is full of old flights you have already saved.'), PreflightItem('operatorId', 'Paperwork', 'Operator ID on the aircraft', 'Your registration number is displayed and your certificate and insurance are valid.'), PreflightItem('rth', 'Plan', 'Return-to-home height set', 'Above the tallest obstacle between the flight area and home.'), PreflightItem('crew', 'Plan', 'Observer briefed, phone on flight mode', 'Everyone involved knows the plan, and calls cannot cover the live view.'), ]; /// The pilot's ticks for the current session, kept on the phone so a restart /// mid-preparation keeps them. A session goes stale after [ttl]: the next /// flight day starts unticked. class PreflightChecklist extends ChangeNotifier { PreflightChecklist({ DateTime Function()? clock, this.ttl = const Duration(hours: 6), Future Function()? prefs, this.items = preflightItems, }) : _clock = clock ?? DateTime.now, _prefs = prefs ?? SharedPreferences.getInstance; static const String _key = 'fly.preflightChecklist'; final DateTime Function() _clock; final Duration ttl; final Future Function() _prefs; final List items; Set _ticked = {}; DateTime? _startedAt; bool isTicked(PreflightItem i) => _ticked.contains(i.id); int get done => items.where(isTicked).length; bool get complete => done == items.length; /// The groups in order of first appearance. List get groups => {for (final PreflightItem i in items) i.group}.toList(); Future load() async { try { final String? raw = (await _prefs()).getString(_key); final Object? j = raw == null ? null : jsonDecode(raw); if (j is! Map) return; final Object? at = j['at']; final DateTime? started = at is num ? DateTime.fromMillisecondsSinceEpoch(at.toInt()) : null; if (started == null || _clock().difference(started) > ttl) return; _startedAt = started; _ticked = {for (final Object? id in (j['ticked'] as List? ?? const [])) if (id is String) id}; notifyListeners(); } catch (_) { // Nothing saved: start unticked. } } void toggle(PreflightItem i) { final DateTime now = _clock(); if (_startedAt == null || now.difference(_startedAt!) > ttl) { _ticked = {}; _startedAt = now; } if (!_ticked.remove(i.id)) _ticked.add(i.id); notifyListeners(); _save(); } void reset() { _ticked = {}; _startedAt = null; notifyListeners(); _save(); } Future _save() async { try { final SharedPreferences p = await _prefs(); if (_startedAt == null) { await p.remove(_key); } else { await p.setString(_key, jsonEncode({'at': _startedAt!.millisecondsSinceEpoch, 'ticked': _ticked.toList()..sort()})); } } catch (_) {} } }