import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../dji_service.dart'; import '../flight_model.dart'; import '../theme.dart'; import 'pv_icons.dart'; /// Aircraft settings — mirrors the v2 "Settings" mockup. Left tab column /// (Safety / Control / Camera / Transmission / About); Safety & Control rows /// drive real flight-controller setters. Values are held on the model and /// updated as the user changes them (sensible defaults until read back). class SettingsMenuPage extends StatefulWidget { const SettingsMenuPage({super.key, required this.model, required this.dji}); final FlightModel model; final DjiService dji; @override State createState() => _SettingsMenuPageState(); } class _SettingsMenuPageState extends State { DjiService get _dji => widget.dji; FlightModel get _m => widget.model; int _tab = 0; static const List<(String, String)> _tabs = <(String, String)>[ ('shield', 'Safety'), ('rc', 'Control'), ('aperture', 'Camera'), ('radio', 'Transmission'), ('drone', 'About'), ]; @override void initState() { super.initState(); // Seed defaults where nothing has been read back yet. _m.maxHeight ??= 120; _m.maxRadius ??= 500; _m.maxRadiusEnabled ??= false; _m.rthHeight ??= 100; _m.obstacleAvoidance ??= 'On'; _m.noviceMode ??= false; } 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}'); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF0A1120), body: SafeArea( child: AnimatedBuilder( animation: _m, builder: (BuildContext context, _) => Stack(children: [ Row(children: [ _tabColumn(), Expanded(child: _rightPanel()), ]), Positioned( top: 6, right: 8, child: GestureDetector( onTap: () => Navigator.of(context).maybePop(), child: Container( width: 30, height: 30, decoration: BoxDecoration(color: const Color(0x1FFFFFFF), borderRadius: BorderRadius.circular(9)), alignment: Alignment.center, child: const PVIcon('close', size: 17, color: Glass.ink), ), ), ), ]), ), ), ); } Widget _tabColumn() { return Container( width: 160, padding: const EdgeInsets.fromLTRB(12, 18, 12, 12), color: const Color(0x8C081020), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.only(left: 6, bottom: 10), child: Row(children: const [ PVIcon('settings', size: 18, color: Glass.ink), SizedBox(width: 8), Text('Settings', style: TextStyle(fontFamily: PV.fontSans, fontSize: 15, fontWeight: FontWeight.w700, color: Glass.ink)), ]), ), for (int i = 0; i < _tabs.length; i++) _tabBtn(i), ]), ); } Widget _tabBtn(int i) { final bool active = i == _tab; return GestureDetector( onTap: () => setState(() => _tab = i), child: Container( height: 38, margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration(color: active ? Glass.accent : Colors.transparent, borderRadius: BorderRadius.circular(10)), child: Row(children: [ PVIcon(_tabs[i].$1, size: 17, stroke: 1.7, color: Glass.ink), const SizedBox(width: 10), Text(_tabs[i].$2, style: const TextStyle(fontFamily: PV.fontSans, fontSize: 13, fontWeight: FontWeight.w600, color: Glass.ink)), ]), ), ); } Widget _rightPanel() { final (String eyebrow, List rows) = switch (_tab) { 0 => ('Flight Safety', _safetyRows()), 1 => ('Flight Control', _controlRows()), 2 => ('Camera', _cameraRows()), 3 => ('Transmission', _infoRows(<(String, String)>[('Channel Mode', 'Auto'), ('Frequency', '2.4 / 5.8 GHz'), ('Signal', 'HD 1080p')])), _ => ('About', _infoRows(<(String, String)>[ ('Model', _m.model ?? '—'), ('Serial Number', _m.serialNumber ?? '—'), ('Firmware', _m.firmwareVersion ?? '—'), ('Controller Firmware', _m.controllerFirmwareVersion ?? '—'), ('MSDK', _m.sdkVersion), ])), }; return Padding( padding: const EdgeInsets.fromLTRB(22, 20, 22, 20), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.only(bottom: 12), child: Text(eyebrow.toUpperCase(), style: const TextStyle(fontFamily: PV.fontMono, fontSize: 10, letterSpacing: 1.4, color: Color(0x8CEAF0FA))), ), Expanded(child: ListView(children: rows)), ]), ); } // ── Rows ──────────────────────────────────────────────────────────────── List _safetyRows() => [ _stepperRow('Max Altitude', '${_m.maxHeight} m', () => _editNumber('Max Altitude', _m.maxHeight ?? 120, 20, 500, 10, (int v) { setState(() => _m.maxHeight = v); _run('Max altitude', () => _dji.setMaxFlightHeight(v)); })), _toggleRow('Max Distance', _m.maxRadiusEnabled ?? false, (bool on) { setState(() => _m.maxRadiusEnabled = on); _run('Max distance', () => _dji.setMaxRadiusEnabled(on)); }), _stepperRow('Return-to-Home Alt.', '${_m.rthHeight} m', () => _editNumber('RTH Altitude', _m.rthHeight ?? 100, 20, 500, 10, (int v) { setState(() => _m.rthHeight = v); _run('RTH altitude', () => _dji.setGoHomeHeight(v)); })), _cycleRow('Obstacle Avoidance', _m.obstacleAvoidance ?? 'On', ['On', 'Off'], (String v) { setState(() => _m.obstacleAvoidance = v); _run('Obstacle avoidance', () => _dji.setObstacleAvoidance(v == 'On')); }), _toggleRow('Beginner Mode', _m.noviceMode ?? false, (bool on) { setState(() => _m.noviceMode = on); _run('Beginner mode', () => _dji.setNoviceMode(on)); }), _toggleRow('AR Home Point', _m.arHomePoint, (bool on) => setState(() => _m.arHomePoint = on)), ]; List _controlRows() => [ _stepperRow('Max Distance', '${_m.maxRadius} m', () => _editNumber('Max Distance', _m.maxRadius ?? 500, 50, 5000, 50, (int v) { setState(() => _m.maxRadius = v); _run('Max distance', () => _dji.setMaxFlightRadius(v)); })), _infoRow('Set Home to Current', 'Tap', onTap: () => _run('Set home', _dji.setHomeToCurrent)), _infoRow('Cancel Return-to-Home', 'Tap', onTap: () => _run('Cancel RTH', _dji.cancelGoHome)), ]; List _cameraRows() => [ _infoRow('Exposure', _m.exposureProgram == ExposureProgram.pro ? 'Pro' : 'Auto'), _infoRow('ISO', _m.iso ?? '—'), _infoRow('Shutter', _m.shutter ?? '—'), _infoRow('White Balance', _m.whiteBalance ?? 'Auto'), ]; List _infoRows(List<(String, String)> items) => [for (final (String l, String v) in items) _infoRow(l, v)]; Widget _rowShell({required Widget child}) => Container( height: 44, decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0x12FFFFFF)))), child: child, ); Widget _label(String l) => Text(l, style: const TextStyle(fontFamily: PV.fontSans, fontSize: 13.5, color: Glass.ink)); Widget _stepperRow(String label, String value, VoidCallback onTap) => _rowShell( child: InkWell( onTap: onTap, child: Row(children: [ Expanded(child: _label(label)), Text(value, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF8FB4F6))), const SizedBox(width: 4), const PVIcon('chevronRight', size: 15, color: Color(0xFF8FB4F6)), ]), ), ); Widget _infoRow(String label, String value, {VoidCallback? onTap}) => _rowShell( child: InkWell( onTap: onTap, child: Row(children: [ Expanded(child: _label(label)), Text(value, style: TextStyle(fontFamily: PV.fontMono, fontSize: 13, color: onTap == null ? const Color(0x99EAF0FA) : const Color(0xFF8FB4F6))), ]), ), ); Widget _cycleRow(String label, String value, List options, ValueChanged onChanged) => _rowShell( child: InkWell( onTap: () { final int i = (options.indexOf(value) + 1) % options.length; onChanged(options[i]); }, child: Row(children: [ Expanded(child: _label(label)), Text(value, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF8FB4F6))), const SizedBox(width: 4), const PVIcon('chevronRight', size: 15, color: Color(0xFF8FB4F6)), ]), ), ); Widget _toggleRow(String label, bool value, ValueChanged onChanged) => _rowShell( child: Row(children: [ Expanded(child: _label(label)), GestureDetector( onTap: () => onChanged(!value), child: Container( width: 40, height: 22, padding: const EdgeInsets.all(2), decoration: BoxDecoration(color: value ? const Color(0xFF3D7BF0) : const Color(0x26FFFFFF), borderRadius: BorderRadius.circular(999)), alignment: value ? Alignment.centerRight : Alignment.centerLeft, child: Container(width: 18, height: 18, decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle)), ), ), ]), ); Future _editNumber(String title, int initial, int min, int max, int step, ValueChanged onSet) async { int value = initial; await showDialog( context: context, builder: (BuildContext ctx) => StatefulBuilder( builder: (BuildContext ctx, StateSetter set) => AlertDialog( backgroundColor: const Color(0xFF10203F), title: Text(title, style: const TextStyle(color: Glass.ink, fontFamily: PV.fontSans, fontWeight: FontWeight.w700)), content: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton(onPressed: () => set(() => value = (value - step).clamp(min, max)), icon: const Icon(Icons.remove, color: Glass.ink)), Text('$value m', style: const TextStyle(fontFamily: PV.fontMono, fontSize: 22, fontWeight: FontWeight.w700, color: Glass.ink)), IconButton(onPressed: () => set(() => value = (value + step).clamp(min, max)), icon: const Icon(Icons.add, color: Glass.ink)), ]), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')), FilledButton(onPressed: () { Navigator.pop(ctx); onSet(value); }, child: const Text('Set')), ], ), ), ); } }