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>
159 lines
5.4 KiB
Dart
159 lines
5.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import '../dji_service.dart';
|
|
import '../flight_model.dart';
|
|
import '../theme.dart';
|
|
import 'flight_overlay_scaffold.dart';
|
|
import 'pv_icons.dart';
|
|
|
|
/// Capture-mode selector — mirrors the v2 "Capture modes" mockup. Photo/Video
|
|
/// modes drive the camera directly; QuickShots + Intelligent modes start the
|
|
/// matching SDK mission (Phase 5 native — until then they report "unavailable").
|
|
class CaptureModesPage extends StatefulWidget {
|
|
const CaptureModesPage({super.key, required this.model, required this.dji});
|
|
|
|
final FlightModel model;
|
|
final DjiService dji;
|
|
|
|
@override
|
|
State<CaptureModesPage> createState() => _CaptureModesPageState();
|
|
}
|
|
|
|
class _CaptureModesPageState extends State<CaptureModesPage> {
|
|
DjiService get _dji => widget.dji;
|
|
FlightModel get _m => widget.model;
|
|
|
|
// (icon, label, wire) — wire drives the SDK call per group.
|
|
static const List<(String, String, List<(String, String, String)>)> _groups =
|
|
<(String, String, List<(String, String, String)>)>[
|
|
('Photo', 'photo', <(String, String, String)>[
|
|
('image', 'Single', 'SINGLE'),
|
|
('aperture', 'AEB', 'AEB'),
|
|
('burst', 'Burst', 'BURST'),
|
|
('timer', 'Timed', 'INTERVAL'),
|
|
('hdr', 'HDR', 'HDR'),
|
|
]),
|
|
('Video', 'video', <(String, String, String)>[
|
|
('video', 'Normal', 'NORMAL'),
|
|
('slowmo', 'Slow-Mo', 'SLOW_MOTION'),
|
|
('hyperlapse', 'Hyperlapse', 'HYPERLAPSE'),
|
|
]),
|
|
('QuickShots', 'quick', <(String, String, String)>[
|
|
('dronie', 'Dronie', 'Dronie'),
|
|
('rocket', 'Rocket', 'Rocket'),
|
|
('circle', 'Circle', 'Circle'),
|
|
('helix', 'Helix', 'Helix'),
|
|
('boomerang', 'Boomerang', 'Boomerang'),
|
|
('asteroid', 'Asteroid', 'Asteroid'),
|
|
]),
|
|
('Intelligent', 'smart', <(String, String, String)>[
|
|
('master', 'MasterShot', 'MasterShot'),
|
|
('pano', 'Pano', 'PANORAMA'),
|
|
('crosshair', 'Track', 'TRACK'),
|
|
]),
|
|
];
|
|
|
|
String? _selected;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selected = _m.shootPhotoMode ?? 'SINGLE';
|
|
}
|
|
|
|
void _snack(String msg) {
|
|
ScaffoldMessenger.of(context)
|
|
..clearSnackBars()
|
|
..showSnackBar(SnackBar(content: Text(msg)));
|
|
}
|
|
|
|
Future<void> _run(String label, Future<void> Function() action) async {
|
|
try {
|
|
await action();
|
|
} catch (e) {
|
|
_snack('$label: ${e is PlatformException ? (e.message ?? e.code) : e}');
|
|
}
|
|
}
|
|
|
|
Future<void> _pick(String group, String label, String wire) async {
|
|
setState(() => _selected = wire);
|
|
switch (group) {
|
|
case 'photo':
|
|
_m.captureMode = CaptureMode.photo;
|
|
_m.shootPhotoMode = wire;
|
|
await _run('Photo mode', () => _dji.setCameraMode('photo'));
|
|
await _run('Photo mode', () => _dji.setShootPhotoMode(wire));
|
|
break;
|
|
case 'video':
|
|
_m.captureMode = CaptureMode.video;
|
|
await _run('Video mode', () => _dji.setCameraMode('video'));
|
|
if (wire != 'NORMAL') _snack('$label is applied on the aircraft camera');
|
|
break;
|
|
case 'quick':
|
|
await _run('QuickShot', () => _dji.startQuickShot(wire));
|
|
break;
|
|
case 'smart':
|
|
if (wire == 'PANORAMA') {
|
|
_m.captureMode = CaptureMode.photo;
|
|
await _run('Pano', () => _dji.setCameraMode('photo'));
|
|
await _run('Pano', () => _dji.setShootPhotoMode('PANORAMA'));
|
|
} else if (wire == 'MasterShot') {
|
|
await _run('MasterShot', () => _dji.startQuickShot('MasterShot'));
|
|
} else {
|
|
_snack('Track: draw a box around a subject in the flight view');
|
|
}
|
|
break;
|
|
}
|
|
if (mounted) _m.bump();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FlightOverlayScaffold(
|
|
title: 'Capture modes',
|
|
body: ListView(
|
|
children: <Widget>[
|
|
for (final (String label, String group, List<(String, String, String)> items) in _groups) ...<Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
|
child: Text(label.toUpperCase(),
|
|
style: const TextStyle(fontFamily: PV.fontMono, fontSize: 10, letterSpacing: 1.4, color: Color(0x8CEAF0FA))),
|
|
),
|
|
Wrap(spacing: 10, runSpacing: 10, children: <Widget>[
|
|
for (final (String ic, String l, String wire) in items) _cell(group, ic, l, wire),
|
|
]),
|
|
const SizedBox(height: 18),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _cell(String group, String icon, String label, String wire) {
|
|
final bool sel = _selected == wire;
|
|
return GestureDetector(
|
|
onTap: () => _pick(group, label, wire),
|
|
child: SizedBox(
|
|
width: 66,
|
|
child: Column(children: <Widget>[
|
|
Container(
|
|
width: 46, height: 46,
|
|
decoration: BoxDecoration(
|
|
color: sel ? const Color(0x403D7BF0) : const Color(0x0DFFFFFF),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: sel ? const Color(0xFF5B93F5) : Glass.hairline, width: sel ? 2 : 1),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: PVIcon(icon, size: 20, stroke: 1.7, color: sel ? const Color(0xFF8FB4F6) : Glass.ink),
|
|
),
|
|
const SizedBox(height: 5),
|
|
Text(label,
|
|
maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(fontFamily: PV.fontMono, fontSize: 9.5, color: Color(0xCCEAF0FA))),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|