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

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;
}