Files
PilotVault/Fly Agent App/lib/ui/shell.dart
T
tajniak81andClaude Opus 5.5 3dd31b257a Add the Fly Agent App, a Remote ID companion to DJI Fly
A Flutter app that runs next to DJI Fly without the DJI SDK. It receives the
aircraft's Remote ID (Bluetooth, BT5, Wi-Fi NaN and Beacon) in a foreground
service, streams the paired aircraft to the hub over /ws/device, and turns
take-off to landing into a logbook entry filed through /api/flights, queued
when offline. Covers phases A0-A3 of the Fly Agent plan; needs no server change.

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

99 lines
3.1 KiB
Dart

import 'package:flutter/material.dart';
import '../services.dart';
import '../theme.dart';
import '../uplink/uplink.dart';
import 'flight_tab.dart';
import 'logbook_tab.dart';
import 'nearby_tab.dart';
import 'settings_page.dart';
/// Signed-in home: Flight (session), Nearby (Remote ID receiver), Logbook.
class AgentShell extends StatefulWidget {
const AgentShell({super.key});
@override
State<AgentShell> createState() => _AgentShellState();
}
class _AgentShellState extends State<AgentShell> {
int _tab = 0;
bool _started = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_started) return;
_started = true;
// Signing in is what turns the Agent on: GPS, the receiver and the uplink
// all start here so the pilot only has to open DJI Fly.
final Services s = Services.of(context);
WidgetsBinding.instance.addPostFrameCallback((_) async {
await s.pilot.start();
if (!s.rid.running) {
try {
await s.rid.start();
} catch (_) {
// Shown on the Nearby tab; the session still works by hand.
}
}
s.uplink.enable(s.api.serverUrl);
});
}
@override
Widget build(BuildContext context) {
const List<String> titles = <String>['Flight', 'Nearby aircraft', 'Logbook'];
return Scaffold(
appBar: AppBar(
title: Text(titles[_tab]),
actions: <Widget>[
const _UplinkChip(),
IconButton(
tooltip: 'Settings',
icon: const Icon(Icons.settings_outlined),
onPressed: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SettingsPage())),
),
],
),
body: IndexedStack(index: _tab, children: const <Widget>[FlightTab(), NearbyTab(), LogbookTab()]),
bottomNavigationBar: NavigationBar(
selectedIndex: _tab,
onDestinationSelected: (int i) => setState(() => _tab = i),
destinations: const <NavigationDestination>[
NavigationDestination(icon: Icon(Icons.flight_takeoff), label: 'Flight'),
NavigationDestination(icon: Icon(Icons.radar), label: 'Nearby'),
NavigationDestination(icon: Icon(Icons.menu_book_outlined), label: 'Logbook'),
],
),
);
}
}
class _UplinkChip extends StatelessWidget {
const _UplinkChip();
@override
Widget build(BuildContext context) {
final Uplink uplink = Services.of(context).uplink;
return ListenableBuilder(
listenable: uplink,
builder: (BuildContext context, _) {
final (Color c, String t) = switch (uplink.status) {
UplinkStatus.live => (PV.ready, 'LIVE'),
UplinkStatus.connecting => (PV.inkMuted, 'IDLE'),
UplinkStatus.offline => (PV.caution, 'OFFLINE'),
UplinkStatus.off => (PV.inkMuted, 'OFF'),
};
return Tooltip(
message: 'Live uplink to PilotVault',
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(children: <Widget>[StatusDot(c), const SizedBox(width: 6), Text(t, style: PV.label)]),
),
);
},
);
}
}