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 createState() => _CameraSettingsPageState(); } class _CameraSettingsPageState extends State { 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)> _dials = <(String, String, List)>[ ('ISO', 'iso', ['AUTO', '100', '200', '400', '800', '1600', '3200', '6400']), ('Shutter', 'shutter', ['1/2000', '1/1000', '1/500', '1/240', '1/120', '1/60', '1/30', '1/15', '1/8']), ('Aperture', 'aperture', ['f/2.8', 'f/4', 'f/5.6', 'f/8', 'f/11']), ('EV', 'ev', ['-2.0', '-1.3', '-0.7', '-0.3', '0.0', '+0.3', '+0.7', '+1.3', '+2.0']), ('WB', 'wb', ['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 _run(String label, Future 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 _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 _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: [ 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: [ _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 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: [ Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ 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: [ 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: [ 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; }