Files
PilotVault/Fly App/lib/ui/home_page.dart
T
tajniak81andClaude Opus 4.8 a4a2709456 Rebuild Fly App to design v2 with full DJI SDK integration
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>
2026-07-15 16:51:08 +02:00

286 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))),
]),
),
),
);
}
}