Rebuild the Flutter/DJI-MSDK-V4 Fly App to the v2 UI kit and wire the full SDK surface behind it. Native (Kotlin): split DjiSdkBridge into a method/event router delegating to per-subsystem SubBridge helpers sharing a BridgeCtx — FlightController (takeoff/land/RTH + rich telemetry), Camera (mode/record/photo/exposure), Gimbal, Mission (Waypoint + ActiveTrack; QuickShots via ActiveTrack QUICK_SHOT), Media (MediaManager list/thumbnail/download), and optional DJI account login. Manifest gains scoped media permissions. Flutter: ten screens under lib/ui/ (Flight HUD, capture modes, camera settings, settings menu, map+waypoints, home, album, academy, profile, routes/flight logs), driven by an expanded FlightModel. New PVIcon renders the kit's SVG paths via flutter_svg; map uses flutter_map + latlong2. Pin transitive androidx.core/browser down to SDK-35-compatible versions so the newer plugins don't force AGP 8.9.1 onto the DJI toolchain. Verified with `flutter build apk --debug` (compiles Dart + all Kotlin); runtime behaviour is untested here — it needs a physical DJI-connected device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
286 lines
10 KiB
Dart
286 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../flight_model.dart';
|
||
import '../theme.dart';
|
||
import '../uploader.dart';
|
||
import 'pv_icons.dart';
|
||
|
||
/// Portrait launch screen — mirrors the v2 "Home" mockups (connected /
|
||
/// disconnected). Brand header + avatar, aircraft card, GO FLY, and a 2×2 tile
|
||
/// grid (Album / Academy / Routes / Flight logs).
|
||
class HomeScreen extends StatelessWidget {
|
||
const HomeScreen({
|
||
super.key,
|
||
required this.model,
|
||
required this.onGoFly,
|
||
required this.onConnect,
|
||
required this.onSettings,
|
||
required this.onTile,
|
||
});
|
||
|
||
final FlightModel model;
|
||
final VoidCallback onGoFly;
|
||
final VoidCallback onConnect;
|
||
final VoidCallback onSettings;
|
||
final void Function(String tile) onTile;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final PVScheme s = PVScheme.of(context);
|
||
return Scaffold(
|
||
backgroundColor: s.bgApp,
|
||
body: SafeArea(
|
||
child: AnimatedBuilder(
|
||
animation: model,
|
||
builder: (BuildContext context, _) {
|
||
final bool connected = model.connected;
|
||
return Column(
|
||
children: <Widget>[
|
||
_brandRow(s),
|
||
Expanded(
|
||
child: Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||
child: connected ? _connectedCard(s) : _disconnectedCard(s),
|
||
),
|
||
),
|
||
),
|
||
if (connected)
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||
child: _goFly(s),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 28),
|
||
child: _tiles(s),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _brandRow(PVScheme s) {
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 0),
|
||
child: Row(
|
||
children: <Widget>[
|
||
const PvBrandMark(size: 22),
|
||
const SizedBox(width: 8),
|
||
Text.rich(TextSpan(children: <TextSpan>[
|
||
TextSpan(text: 'Pilot', style: _brand(s.textSecondary, FontWeight.w500)),
|
||
TextSpan(text: 'Vault', style: _brand(s.textPrimary, FontWeight.w700)),
|
||
TextSpan(text: ' Fly', style: _brand(s.accent, FontWeight.w500)),
|
||
])),
|
||
const Spacer(),
|
||
GestureDetector(
|
||
onTap: onSettings,
|
||
child: Container(
|
||
width: 32,
|
||
height: 32,
|
||
decoration: BoxDecoration(color: s.surfaceInset, shape: BoxShape.circle),
|
||
alignment: Alignment.center,
|
||
child: PVIcon('user', size: 17, color: s.textSecondary),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
TextStyle _brand(Color c, FontWeight w) => TextStyle(
|
||
fontFamily: PV.fontSans, fontSize: 19, fontWeight: w, letterSpacing: -0.38, color: c);
|
||
|
||
Widget _connectedCard(PVScheme s) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: s.surface,
|
||
borderRadius: BorderRadius.circular(18),
|
||
border: Border.all(color: s.border),
|
||
boxShadow: s.shadowSm,
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: <Widget>[
|
||
Row(children: <Widget>[
|
||
Container(width: 7, height: 7, decoration: BoxDecoration(color: s.success, shape: BoxShape.circle)),
|
||
const SizedBox(width: 6),
|
||
Text('CONNECTED',
|
||
style: TextStyle(fontFamily: PV.fontMono, fontSize: 11, letterSpacing: 1.1, fontWeight: FontWeight.w700, color: s.successFg)),
|
||
]),
|
||
const SizedBox(height: 14),
|
||
Row(children: <Widget>[
|
||
Container(
|
||
width: 56, height: 56,
|
||
decoration: BoxDecoration(color: s.accentSoft, borderRadius: BorderRadius.circular(14)),
|
||
alignment: Alignment.center,
|
||
child: PVIcon('drone', size: 30, stroke: 1.6, color: s.accent),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: <Widget>[
|
||
Text(model.model ?? 'Aircraft',
|
||
style: TextStyle(fontFamily: PV.fontSans, fontSize: 18, fontWeight: FontWeight.w700, color: s.textPrimary)),
|
||
const SizedBox(height: 2),
|
||
Text(model.firmwareVersion == null ? 'MSDK · ${model.sdkVersion}' : 'FW ${model.firmwareVersion}',
|
||
style: TextStyle(fontFamily: PV.fontMono, fontSize: 12, color: s.textSecondary)),
|
||
],
|
||
),
|
||
),
|
||
]),
|
||
const SizedBox(height: 14),
|
||
Row(children: <Widget>[
|
||
_chip(s, 'battery', model.batteryPercent == null ? '—' : '${model.batteryPercent}%'),
|
||
const SizedBox(width: 8),
|
||
_chip(s, 'sdcard', model.sdRemainingMB == null ? '— GB' : '${(model.sdRemainingMB! / 1024).toStringAsFixed(0)} GB'),
|
||
const SizedBox(width: 8),
|
||
_chip(s, 'link', _linkLabel(model.upload)),
|
||
]),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _disconnectedCard(PVScheme s) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: s.surface,
|
||
borderRadius: BorderRadius.circular(18),
|
||
border: Border.all(color: s.borderStrong, style: BorderStyle.solid),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
Container(
|
||
width: 64, height: 64,
|
||
decoration: BoxDecoration(color: s.surfaceInset, borderRadius: BorderRadius.circular(18)),
|
||
alignment: Alignment.center,
|
||
child: PVIcon('drone', size: 34, stroke: 1.5, color: s.textTertiary),
|
||
),
|
||
const SizedBox(height: 14),
|
||
Text('No aircraft connected',
|
||
style: TextStyle(fontFamily: PV.fontSans, fontSize: 17, fontWeight: FontWeight.w700, color: s.textPrimary)),
|
||
const SizedBox(height: 4),
|
||
Text('Power on your aircraft and remote controller, then connect to begin.',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontFamily: PV.fontSans, fontSize: 13, height: 1.5, color: s.textSecondary)),
|
||
const SizedBox(height: 16),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 50,
|
||
child: FilledButton(
|
||
onPressed: onConnect,
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: s.accent, foregroundColor: Colors.white, elevation: 0,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||
),
|
||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||
const PVIcon('link', size: 18, color: Colors.white),
|
||
const SizedBox(width: 8),
|
||
const Text('Connect aircraft', style: TextStyle(fontFamily: PV.fontSans, fontSize: 15, fontWeight: FontWeight.w700)),
|
||
]),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
String _linkLabel(UploadStatus u) {
|
||
switch (u) {
|
||
case UploadStatus.connected:
|
||
return 'Streaming';
|
||
case UploadStatus.connecting:
|
||
return 'Linking…';
|
||
case UploadStatus.error:
|
||
return 'Retrying';
|
||
case UploadStatus.disabled:
|
||
return 'RC linked';
|
||
}
|
||
}
|
||
|
||
Widget _chip(PVScheme s, String icon, String value) {
|
||
return Expanded(
|
||
child: Container(
|
||
height: 34,
|
||
decoration: BoxDecoration(color: s.surfaceInset, borderRadius: BorderRadius.circular(9)),
|
||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||
PVIcon(icon, size: 14, color: s.textTertiary),
|
||
const SizedBox(width: 6),
|
||
Flexible(child: Text(value, overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(fontFamily: PV.fontMono, fontSize: 11.5, color: s.textSecondary))),
|
||
]),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _goFly(PVScheme s) {
|
||
return SizedBox(
|
||
height: 58,
|
||
child: FilledButton(
|
||
onPressed: onGoFly,
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: s.accent, foregroundColor: Colors.white, elevation: 0,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
),
|
||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: const <Widget>[
|
||
PVIcon('play2', size: 20, color: Colors.white, fill: true),
|
||
SizedBox(width: 10),
|
||
Text('GO FLY', style: TextStyle(fontFamily: PV.fontSans, fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: 0.4)),
|
||
]),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _tiles(PVScheme s) {
|
||
const List<(String, String)> tiles = <(String, String)>[
|
||
('album', 'Album'), ('academy', 'Academy'), ('route', 'Routes'), ('gauge', 'Flight logs'),
|
||
];
|
||
return Column(children: <Widget>[
|
||
Row(children: <Widget>[
|
||
Expanded(child: _tile(s, tiles[0])), const SizedBox(width: 12), Expanded(child: _tile(s, tiles[1])),
|
||
]),
|
||
const SizedBox(height: 12),
|
||
Row(children: <Widget>[
|
||
Expanded(child: _tile(s, tiles[2])), const SizedBox(width: 12), Expanded(child: _tile(s, tiles[3])),
|
||
]),
|
||
]);
|
||
}
|
||
|
||
Widget _tile(PVScheme s, (String, String) t) {
|
||
return Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
borderRadius: BorderRadius.circular(14),
|
||
onTap: () => onTile(t.$2),
|
||
child: Container(
|
||
height: 56,
|
||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||
decoration: BoxDecoration(
|
||
color: s.surface, borderRadius: BorderRadius.circular(14),
|
||
border: Border.all(color: s.border), boxShadow: s.shadowXs,
|
||
),
|
||
child: Row(children: <Widget>[
|
||
Container(
|
||
width: 32, height: 32,
|
||
decoration: BoxDecoration(color: s.accentSoft, borderRadius: BorderRadius.circular(9)),
|
||
alignment: Alignment.center,
|
||
child: PVIcon(t.$1, size: 17, color: s.accentSoftFg),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Flexible(child: Text(t.$2, overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(fontFamily: PV.fontSans, fontSize: 14, fontWeight: FontWeight.w600, color: s.textPrimary))),
|
||
]),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|