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>
116 lines
3.6 KiB
Dart
116 lines
3.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// PilotVault mobile tokens — the same values as the Fly App's `PV` (Signal
|
|
/// Blue over Vault Navy, Space Grotesk for chrome, Space Mono for data).
|
|
class PV {
|
|
PV._();
|
|
|
|
static const String fontSans = 'Space Grotesk';
|
|
static const String fontMono = 'Space Mono';
|
|
|
|
static const Color navy = Color(0xFF0F1E3D);
|
|
static const Color accent = Color(0xFF3D7BF0);
|
|
|
|
static const Color ready = Color(0xFF1F8A5B);
|
|
static const Color caution = Color(0xFFD9852B);
|
|
static const Color warning = Color(0xFFD64545);
|
|
|
|
static const Color surface0 = Color(0xFFEEF0F3);
|
|
static const Color surface1 = Color(0xFFFFFFFF);
|
|
static const Color surface2 = Color(0xFFF6F7F9);
|
|
|
|
static const Color ink = navy;
|
|
static const Color inkSecondary = Color(0xFF5A6B85);
|
|
static const Color inkMuted = Color(0xFF97A1B0);
|
|
static const Color line = Color(0xFFDCE0E7);
|
|
|
|
static const double radius = 14;
|
|
|
|
static const TextStyle telemetry = TextStyle(
|
|
fontFamily: fontMono,
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.w700,
|
|
height: 1.0,
|
|
color: ink,
|
|
fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
|
|
);
|
|
static const TextStyle label = TextStyle(
|
|
fontFamily: fontMono,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.5,
|
|
color: inkMuted,
|
|
);
|
|
static const TextStyle mono = TextStyle(
|
|
fontFamily: fontMono,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: ink,
|
|
fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
|
|
);
|
|
static const TextStyle caption = TextStyle(fontFamily: fontSans, fontSize: 12, color: inkSecondary);
|
|
|
|
static ThemeData theme() {
|
|
final ColorScheme scheme = ColorScheme.fromSeed(
|
|
seedColor: accent,
|
|
primary: accent,
|
|
surface: surface1,
|
|
error: warning,
|
|
);
|
|
return ThemeData(
|
|
colorScheme: scheme,
|
|
fontFamily: fontSans,
|
|
scaffoldBackgroundColor: surface0,
|
|
appBarTheme: const AppBarTheme(
|
|
backgroundColor: surface1,
|
|
foregroundColor: ink,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0.5,
|
|
titleTextStyle: TextStyle(fontFamily: fontSans, fontSize: 18, fontWeight: FontWeight.w600, color: ink),
|
|
),
|
|
cardTheme: CardThemeData(
|
|
color: surface1,
|
|
elevation: 0,
|
|
margin: EdgeInsets.zero,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(radius),
|
|
side: const BorderSide(color: line),
|
|
),
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: surface2,
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: line)),
|
|
enabledBorder:
|
|
OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: line)),
|
|
),
|
|
navigationBarTheme: const NavigationBarThemeData(backgroundColor: surface1, indicatorColor: Color(0x263D7BF0)),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Mono ALL-CAPS section label.
|
|
class SectionLabel extends StatelessWidget {
|
|
const SectionLabel(this.text, {super.key});
|
|
final String text;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Padding(
|
|
padding: const EdgeInsets.fromLTRB(4, 16, 4, 8),
|
|
child: Text(text.toUpperCase(), style: PV.label),
|
|
);
|
|
}
|
|
|
|
class StatusDot extends StatelessWidget {
|
|
const StatusDot(this.color, {super.key, this.size = 8});
|
|
final Color color;
|
|
final double size;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
);
|
|
}
|