Files
PilotVault/Fly App/lib/ui/capture_modes_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

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))),
]),
),
);
}
}