Files
PilotVault/Fly App/lib/fly/flight_log.dart
T
tajniak81andClaude Opus 5.5 91ca150a35 Give the Fly App GO 4-style playback, flight records, Find My Drone and live streaming
Playback: the flight screen's playback button opens the album over the
live view, so a video tapped there plays on the aircraft into the feed
with a transport bar. Long press selects files to download or delete
(delete asks first); one file opens in a viewer with the camera's
preview. Leaving the album puts the camera back in its capture mode.

Flight records: every flight is recorded from take-off to landing (1 Hz
track, max height / distance / speed, distance flown, battery used) and
kept on the phone. When it lands while signed in, it is uploaded to the
PilotVault logbook through the existing /api/drones/auto and
/api/flights endpoints as an automatic-path entry with the track as
GeoJSON. Simulator flights are never uploaded; flights recorded while
signed out retry on sign-in. Flight logs lists them with their upload
state and a track map, and the profile stats come from real flights.

Find My Drone: the aircraft's last fix is kept across lost links and
restarts, and shown against the phone's own GPS with distance, direction
and a hand-off to the maps app.

Live streaming: a new LiveStreamBridge streams the main camera to an
RTMP address set in Settings > General, with a LIVE pill on the flight
screen that turns amber when the uplink falls behind and stops on tap.

Pre-flight checklist (manual checks plus the aircraft's live status)
and an in-app Academy with six short guides that work offline.

None of this has run on hardware yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:07:10 +02:00

154 lines
4.7 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'flight_record.dart';
import 'flight_record_sync.dart';
import 'fly_prefs.dart';
/// Where recorded flights live on the phone.
abstract class FlightRecordStore {
Future<List<FlightRecord>> loadAll();
Future<void> save(FlightRecord r);
Future<void> delete(String id);
}
/// One JSON file per flight under the app's documents directory
/// (`flight_records/<id>.json`); an unreadable file is skipped, not fatal.
class FileFlightRecordStore implements FlightRecordStore {
FileFlightRecordStore({Future<Directory> Function()? root}) : _root = root ?? getApplicationDocumentsDirectory;
final Future<Directory> Function() _root;
Directory? _dir;
Future<Directory> _folder() async {
if (_dir != null) return _dir!;
final Directory d = Directory('${(await _root()).path}${Platform.pathSeparator}flight_records');
if (!await d.exists()) await d.create(recursive: true);
return _dir = d;
}
File _file(Directory d, String id) => File('${d.path}${Platform.pathSeparator}$id.json');
@override
Future<List<FlightRecord>> loadAll() async {
final Directory d = await _folder();
final List<FlightRecord> out = <FlightRecord>[];
await for (final FileSystemEntity f in d.list()) {
if (f is! File || !f.path.endsWith('.json')) continue;
try {
final Object? j = jsonDecode(await f.readAsString());
if (j is Map) out.add(FlightRecord.fromJson(Map<String, dynamic>.from(j)));
} catch (_) {
// A half-written file from a crash: skip it.
}
}
return out;
}
@override
Future<void> save(FlightRecord r) async {
final File f = _file(await _folder(), r.id);
// Write then rename, so a crash never leaves a truncated record.
final File tmp = File('${f.path}.tmp');
await tmp.writeAsString(jsonEncode(r.toJson()), flush: true);
await tmp.rename(f.path);
}
@override
Future<void> delete(String id) async {
final File f = _file(await _folder(), id);
if (await f.exists()) await f.delete();
}
}
/// The phone's flight records (plan P9): every flight the [FlightRecorder]
/// finishes is saved here and, while signed in and
/// [FlyPrefs.autoUploadFlights] is on, uploaded to the API Server's logbook.
/// Flights that could not be uploaded stay pending and retry on [uploadPending].
class FlightLog extends ChangeNotifier {
FlightLog({required this.store, required this.remote});
final FlightRecordStore store;
final LogbookRemote remote;
List<FlightRecord> _records = <FlightRecord>[];
bool _loaded = false;
final Set<String> _uploading = <String>{};
/// Newest first.
List<FlightRecord> get records => List<FlightRecord>.unmodifiable(_records);
bool get loaded => _loaded;
bool isUploading(FlightRecord r) => _uploading.contains(r.id);
/// Real flights not in the logbook yet.
List<FlightRecord> get pending => <FlightRecord>[for (final FlightRecord r in _records) if (!r.uploaded && !r.simulated) r];
/// The latest flight, which is where Find My Drone looks first.
FlightRecord? get latest => _records.isEmpty ? null : _records.first;
Future<void> load() async {
try {
_records = await store.loadAll();
} catch (_) {
_records = <FlightRecord>[];
}
_sort();
_loaded = true;
notifyListeners();
}
void _sort() => _records.sort((FlightRecord a, FlightRecord b) => b.start.compareTo(a.start));
/// A flight the recorder just finished.
Future<void> add(FlightRecord r) async {
_records.removeWhere((FlightRecord x) => x.id == r.id);
_records.add(r);
_sort();
notifyListeners();
await _save(r);
if (FlyPrefs.autoUploadFlights.value && remote.available && !r.simulated) await upload(r);
}
Future<void> _save(FlightRecord r) async {
try {
await store.save(r);
} catch (_) {
// Storage full or gone: the record lives in memory until the app closes.
}
}
Future<FlightRecord> upload(FlightRecord r) async {
if (r.uploaded || !_uploading.add(r.id)) return r;
notifyListeners();
try {
await uploadFlightRecord(r, remote);
await _save(r);
} finally {
_uploading.remove(r.id);
notifyListeners();
}
return r;
}
/// Retries every pending flight; answers how many made it.
Future<int> uploadPending() async {
if (!remote.available) return 0;
int ok = 0;
for (final FlightRecord r in pending) {
if ((await upload(r)).uploaded) ok++;
}
return ok;
}
Future<void> delete(FlightRecord r) async {
_records.removeWhere((FlightRecord x) => x.id == r.id);
notifyListeners();
try {
await store.delete(r.id);
} catch (_) {}
}
}