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>
140 lines
5.2 KiB
Dart
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',
|
|
};
|
|
}
|