Files
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

140 lines
5.2 KiB
Dart

import 'package:flutter/material.dart';
import '../api/api.dart';
import '../services.dart';
import '../theme.dart';
import '../uplink/uplink.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool? _batteryExempt;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_checkBattery();
}
Future<void> _checkBattery() async {
try {
final bool v = await Services.of(context).rid.isIgnoringBatteryOptimizations();
if (mounted) setState(() => _batteryExempt = v);
} catch (_) {}
}
@override
Widget build(BuildContext context) {
final Services s = Services.of(context);
final Api api = s.api;
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListenableBuilder(
listenable: Listenable.merge(<Listenable>[api, s.uplink, s.rid]),
builder: (BuildContext context, _) => ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
const SectionLabel('Account'),
Card(
child: Column(
children: <Widget>[
ListTile(
leading: const Icon(Icons.person_outline),
title: Text(api.me?.email ?? api.email),
subtitle: Text(<String>[
if (api.me?.role.isNotEmpty ?? false) api.me!.role,
if (api.me?.orgName.isNotEmpty ?? false) api.me!.orgName,
].join(' · ')),
),
ListTile(
leading: const Icon(Icons.dns_outlined),
title: const Text('API server'),
subtitle: Text(api.serverUrl),
),
ListTile(
leading: const Icon(Icons.logout, color: PV.warning),
title: const Text('Sign out'),
onTap: () async {
s.uplink.disable();
await api.signOut();
if (context.mounted) Navigator.of(context).pop();
},
),
],
),
),
const SectionLabel('Live uplink'),
Card(
child: Column(
children: <Widget>[
SwitchListTile(
title: const Text('Stream the paired aircraft to PilotVault'),
subtitle: Text(_uplinkText(s.uplink)),
value: s.uplink.status != UplinkStatus.off,
onChanged: (bool on) => on ? s.uplink.enable(api.serverUrl) : s.uplink.disable(),
),
],
),
),
const SectionLabel('Pairing'),
Card(
child: ListTile(
leading: const Icon(Icons.link),
title: Text(s.rid.pairing?.droneName ?? 'No aircraft paired'),
subtitle: Text(s.rid.pairing?.serial ?? 'Pair one under Nearby'),
trailing: s.rid.pairing == null
? null
: TextButton(onPressed: s.rid.unpair, child: const Text('Unpair')),
),
),
const SectionLabel('Background'),
Card(
child: Column(
children: <Widget>[
ListTile(
leading: Icon(Icons.battery_saver_outlined, color: _batteryExempt == false ? PV.caution : PV.ready),
title: const Text('Battery optimisation'),
subtitle: Text(_batteryExempt == null
? '…'
: _batteryExempt!
? 'Exempt: Android will not stop the receiver'
: 'Android may stop the receiver while DJI Fly is in front'),
trailing: _batteryExempt == false
? TextButton(
onPressed: () async {
await s.rid.requestIgnoreBatteryOptimizations();
await Future<void>.delayed(const Duration(seconds: 1));
_checkBattery();
},
child: const Text('Exempt'),
)
: null,
),
const ListTile(
leading: Icon(Icons.wifi_find),
title: Text('Wi-Fi Beacon updates'),
subtitle: Text('Android limits Wi-Fi scans to 4 per 2 minutes. For faster Beacon updates turn off '
'"Wi-Fi scan throttling" in Developer options.'),
),
],
),
),
],
),
),
);
}
String _uplinkText(Uplink u) => switch (u.status) {
UplinkStatus.off => 'Off',
UplinkStatus.connecting => 'Waiting for the paired aircraft',
UplinkStatus.live => 'Live as ${u.deviceId} · ${u.sent} sent',
UplinkStatus.offline => 'Server unreachable · ${u.buffered} buffered',
};
}