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>
237 lines
8.0 KiB
Dart
237 lines
8.0 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';
|
|
|
|
/// Pro exposure controls — mirrors the v2 "Camera settings" mockup. Auto/Pro
|
|
/// tabs switch exposure program; the five dials (ISO / shutter / aperture / EV /
|
|
/// WB) each drive the matching SDK setter, and the value scrubber picks a value.
|
|
class CameraSettingsPage extends StatefulWidget {
|
|
const CameraSettingsPage({super.key, required this.model, required this.dji});
|
|
|
|
final FlightModel model;
|
|
final DjiService dji;
|
|
|
|
@override
|
|
State<CameraSettingsPage> createState() => _CameraSettingsPageState();
|
|
}
|
|
|
|
class _CameraSettingsPageState extends State<CameraSettingsPage> {
|
|
DjiService get _dji => widget.dji;
|
|
FlightModel get _m => widget.model;
|
|
|
|
// (key, icon, values). Values mirror the kit scrubber; the SDK rejects any it
|
|
// doesn't support (surfaced as a snackbar), so a superset is safe.
|
|
static const List<(String, String, List<String>)> _dials = <(String, String, List<String>)>[
|
|
('ISO', 'iso', <String>['AUTO', '100', '200', '400', '800', '1600', '3200', '6400']),
|
|
('Shutter', 'shutter', <String>['1/2000', '1/1000', '1/500', '1/240', '1/120', '1/60', '1/30', '1/15', '1/8']),
|
|
('Aperture', 'aperture', <String>['f/2.8', 'f/4', 'f/5.6', 'f/8', 'f/11']),
|
|
('EV', 'ev', <String>['-2.0', '-1.3', '-0.7', '-0.3', '0.0', '+0.3', '+0.7', '+1.3', '+2.0']),
|
|
('WB', 'wb', <String>['AUTO', '2700K', '4000K', '5200K', '5800K', '6500K']),
|
|
];
|
|
|
|
int _active = 0; // selected dial
|
|
|
|
bool get _pro => _m.exposureProgram == ExposureProgram.pro;
|
|
|
|
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}');
|
|
}
|
|
}
|
|
|
|
String? _valueFor(String key) => switch (key) {
|
|
'ISO' => _m.iso,
|
|
'Shutter' => _m.shutter,
|
|
'Aperture' => _m.aperture,
|
|
'EV' => _m.ev,
|
|
'WB' => _m.whiteBalance,
|
|
_ => null,
|
|
};
|
|
|
|
Future<void> _setValue(String key, String value) async {
|
|
setState(() {
|
|
switch (key) {
|
|
case 'ISO':
|
|
_m.iso = value;
|
|
case 'Shutter':
|
|
_m.shutter = value;
|
|
case 'Aperture':
|
|
_m.aperture = value;
|
|
case 'EV':
|
|
_m.ev = value;
|
|
case 'WB':
|
|
_m.whiteBalance = value;
|
|
}
|
|
});
|
|
switch (key) {
|
|
case 'ISO':
|
|
await _run('ISO', () => _dji.setISO(value));
|
|
case 'Shutter':
|
|
await _run('Shutter', () => _dji.setShutterSpeed(value));
|
|
case 'Aperture':
|
|
await _run('Aperture', () => _dji.setAperture(value));
|
|
case 'EV':
|
|
await _run('EV', () => _dji.setEV(value));
|
|
case 'WB':
|
|
await _run('White balance', () => _dji.setWhiteBalance(value));
|
|
}
|
|
}
|
|
|
|
Future<void> _setProgram(bool pro) async {
|
|
setState(() => _m.exposureProgram = pro ? ExposureProgram.pro : ExposureProgram.auto);
|
|
await _run('Exposure', () => _dji.setExposureProgram(pro ? 'pro' : 'auto'));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FlightOverlayScaffold(
|
|
title: 'Camera settings',
|
|
trailing: _autoProTabs(),
|
|
body: Column(
|
|
children: <Widget>[
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Container(
|
|
width: 132, height: 48,
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(10)),
|
|
child: const CustomPaint(size: Size.infinite, painter: _HistogramPainter()),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
_dialRail(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _autoProTabs() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(3),
|
|
decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(10)),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
|
_tab('Auto', !_pro, () => _setProgram(false)),
|
|
_tab('Pro', _pro, () => _setProgram(true)),
|
|
]),
|
|
);
|
|
}
|
|
|
|
Widget _tab(String label, bool active, VoidCallback onTap) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
height: 28,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: BoxDecoration(color: active ? Glass.accent : Colors.transparent, borderRadius: BorderRadius.circular(7)),
|
|
alignment: Alignment.center,
|
|
child: Text(label, style: const TextStyle(fontFamily: PV.fontSans, fontSize: 12, fontWeight: FontWeight.w700, color: Glass.ink)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _dialRail() {
|
|
final (String key, _, List<String> values) = _dials[_active];
|
|
final String? current = _valueFor(key);
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
|
|
decoration: BoxDecoration(
|
|
color: Glass.pill,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0x1AFFFFFF)),
|
|
),
|
|
child: Column(children: <Widget>[
|
|
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[
|
|
for (int i = 0; i < _dials.length; i++) _dial(i),
|
|
]),
|
|
const SizedBox(height: 12),
|
|
Opacity(
|
|
opacity: _pro ? 1 : 0.4,
|
|
child: IgnorePointer(
|
|
ignoring: !_pro,
|
|
child: SizedBox(
|
|
height: 30,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
children: <Widget>[
|
|
for (final String v in values) _scrubValue(key, v, v == current),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
Widget _dial(int i) {
|
|
final (String key, String icon, _) = _dials[i];
|
|
final bool sel = i == _active;
|
|
final String value = _valueFor(key) ?? '—';
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _active = i),
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
|
PVIcon(icon, size: 17, color: sel ? const Color(0xFF8FB4F6) : const Color(0xFF8FA0BE)),
|
|
const SizedBox(height: 4),
|
|
Text(key, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 9.5, letterSpacing: 0.8, color: Color(0x99EAF0FA))),
|
|
Text(value, style: TextStyle(fontFamily: PV.fontMono, fontSize: 15, fontWeight: FontWeight.w700, color: sel ? const Color(0xFF8FB4F6) : Glass.ink)),
|
|
]),
|
|
);
|
|
}
|
|
|
|
Widget _scrubValue(String key, String v, bool sel) {
|
|
return GestureDetector(
|
|
onTap: () => _setValue(key, v),
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
child: Text(v,
|
|
style: TextStyle(
|
|
fontFamily: PV.fontMono,
|
|
fontSize: sel ? 14 : 12,
|
|
fontWeight: sel ? FontWeight.w700 : FontWeight.w400,
|
|
color: sel ? const Color(0xFF5B93F5) : const Color(0x99EAF0FA),
|
|
)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HistogramPainter extends CustomPainter {
|
|
const _HistogramPainter();
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final Path p = Path()
|
|
..moveTo(0, size.height)
|
|
..quadraticBezierTo(size.width * 0.2, size.height * 0.2, size.width * 0.4, size.height * 0.6)
|
|
..quadraticBezierTo(size.width * 0.6, size.height * 0.05, size.width * 0.8, size.height * 0.5)
|
|
..quadraticBezierTo(size.width * 0.9, size.height * 0.8, size.width, size.height * 0.9);
|
|
canvas.drawPath(
|
|
Path.from(p)
|
|
..lineTo(size.width, size.height)
|
|
..lineTo(0, size.height)
|
|
..close(),
|
|
Paint()..color = const Color(0x407FE0B0),
|
|
);
|
|
canvas.drawPath(p, Paint()
|
|
..color = const Color(0xFF7FE0B0)
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1.4);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _HistogramPainter oldDelegate) => false;
|
|
}
|