import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/mission/drone_profile.dart'; import '../providers/active_drone_profile_provider.dart'; import '../providers/database_provider.dart'; import 'swipe_value_input.dart'; /// Formular zum Anlegen/Bearbeiten eines Drohnenprofils (Doku 3.5/7.3). /// Numerische Felder als Swipe-Chips (HTML-Demonstrator: SwipeValueInput) - /// vertikales Ziehen aendert den Wert in grossen Schritten, horizontales /// Ziehen in kleinen Schritten; nur der Name bleibt ein normales Textfeld. class DroneProfileEditor extends ConsumerStatefulWidget { const DroneProfileEditor({super.key, this.existing}); /// Null fuer ein neues Profil (vorbefuellt mit T1-Ranger-Standardwerten). final DroneProfile? existing; @override ConsumerState createState() => _DroneProfileEditorState(); } class _DroneProfileEditorState extends ConsumerState { final _formKey = GlobalKey(); late final TextEditingController _nameController; late final Map _initialValues; late Map _fieldValues; // Waehrend eine Swipe-Geste auf einem Chip aktiv ist, wird das Formular // nicht scrollbar (Doku 3.5) - sonst gewinnt die ListView-eigene // Scroll-Geste das Gesture-Arena gegen den Chip, siehe SwipeValueInput. bool _scrolling = true; // Min/Max/Schrittweiten 1:1 aus DRONE_EDITOR_GROUPS im HTML-Demonstrator // uebernommen. static const _groups = [ ( title: 'Speed (m/s)', fields: [ (key: 'minSpeed', label: 'Min (stall)', min: 3.0, max: 40.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: 'm/s'), (key: 'cruiseSpeed', label: 'Cruise', min: 3.0, max: 50.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: 'm/s'), (key: 'maxSpeed', label: 'Max', min: 5.0, max: 80.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: 'm/s'), ], ), ( title: 'Radii (m)', fields: [ (key: 'minTurnRadius', label: 'Min turn', min: 5.0, max: 500.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: 'm'), (key: 'loiterRadius', label: 'Loiter', min: 20.0, max: 1000.0, stepBig: 10.0, stepSmall: 5.0, decimals: 0, unit: 'm'), (key: 'catchRadius', label: 'Waypoint catch', min: 5.0, max: 500.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: 'm'), ], ), ( title: 'Climb / Descent (m/s)', fields: [ (key: 'maxClimbRate', label: 'Max climb', min: 0.5, max: 20.0, stepBig: 1.0, stepSmall: 0.5, decimals: 1, unit: 'm/s'), (key: 'maxDescentRate', label: 'Max descent', min: 0.5, max: 20.0, stepBig: 1.0, stepSmall: 0.5, decimals: 1, unit: 'm/s'), ], ), ( title: 'Limits', fields: [ (key: 'maxRangeM', label: 'Range', min: 500.0, max: 200000.0, stepBig: 1000.0, stepSmall: 100.0, decimals: 0, unit: 'm'), (key: 'maxEnduranceMin', label: 'Endurance', min: 5.0, max: 600.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: 'min'), (key: 'maxAltitudeM', label: 'Ceiling', min: 50.0, max: 10000.0, stepBig: 100.0, stepSmall: 10.0, decimals: 0, unit: 'm'), ], ), // Ampel-Schwellwerte fuer die farbcodierten Telemetriefelder im // Drohnen-Status-Menue (Doku: "Schwellwerte fuer die Ampel-Kodierung im // Drohnenprofil anlegen", siehe domain/telemetry/telemetry_field_status.dart) - // je Feld Gruen-/Rot-Schwelle als eigene Gruppe, analog Climb/Descent oben. ( title: 'GPS satellites alarm', fields: [ (key: 'gpsSatGreenMin', label: 'Green from', min: 0.0, max: 20.0, stepBig: 2.0, stepSmall: 1.0, decimals: 0, unit: 'sat'), (key: 'gpsSatRedMin', label: 'Red below', min: 0.0, max: 20.0, stepBig: 2.0, stepSmall: 1.0, decimals: 0, unit: 'sat'), ], ), ( title: 'GPS precision (HDOP) alarm', fields: [ (key: 'hdopGreenMax', label: 'Green up to', min: 0.5, max: 10.0, stepBig: 1.0, stepSmall: 0.1, decimals: 1, unit: ''), (key: 'hdopRedMax', label: 'Red above', min: 0.5, max: 20.0, stepBig: 1.0, stepSmall: 0.1, decimals: 1, unit: ''), ], ), ( title: 'Current draw alarm (A)', fields: [ (key: 'currentGreenMaxA', label: 'Green up to', min: 1.0, max: 200.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: 'A'), (key: 'currentRedMaxA', label: 'Red above', min: 1.0, max: 300.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: 'A'), ], ), ( title: 'Battery voltage alarm (V/cell)', fields: [ (key: 'batteryCellCount', label: 'Cells', min: 1.0, max: 16.0, stepBig: 1.0, stepSmall: 1.0, decimals: 0, unit: 'S'), (key: 'batteryVoltageGreenMinPerCellV', label: 'Green from', min: 2.5, max: 4.3, stepBig: 0.2, stepSmall: 0.05, decimals: 2, unit: 'V'), (key: 'batteryVoltageRedMinPerCellV', label: 'Red below', min: 2.5, max: 4.3, stepBig: 0.2, stepSmall: 0.05, decimals: 2, unit: 'V'), ], ), ( title: 'RX link quality alarm (%)', fields: [ (key: 'rxQualityGreenMin', label: 'Green from', min: 0.0, max: 100.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: '%'), (key: 'rxQualityRedMin', label: 'Red below', min: 0.0, max: 100.0, stepBig: 10.0, stepSmall: 1.0, decimals: 0, unit: '%'), ], ), ( title: 'RX SNR alarm (dB)', fields: [ (key: 'rxSnrGreenMinDb', label: 'Green from', min: -30.0, max: 30.0, stepBig: 5.0, stepSmall: 1.0, decimals: 0, unit: 'dB'), (key: 'rxSnrRedMinDb', label: 'Red below', min: -30.0, max: 30.0, stepBig: 5.0, stepSmall: 1.0, decimals: 0, unit: 'dB'), ], ), ]; double _fieldValue(DroneProfile p, String key) => switch (key) { 'minSpeed' => p.minSpeed, 'cruiseSpeed' => p.cruiseSpeed, 'maxSpeed' => p.maxSpeed, 'minTurnRadius' => p.minTurnRadius, 'loiterRadius' => p.loiterRadius, 'catchRadius' => p.catchRadius, 'maxClimbRate' => p.maxClimbRate, 'maxDescentRate' => p.maxDescentRate, 'maxRangeM' => p.maxRangeM, 'maxEnduranceMin' => p.maxEnduranceMin, 'maxAltitudeM' => p.maxAltitudeM, 'gpsSatGreenMin' => p.gpsSatGreenMin, 'gpsSatRedMin' => p.gpsSatRedMin, 'hdopGreenMax' => p.hdopGreenMax, 'hdopRedMax' => p.hdopRedMax, 'currentGreenMaxA' => p.currentGreenMaxA, 'currentRedMaxA' => p.currentRedMaxA, 'batteryCellCount' => p.batteryCellCount.toDouble(), 'batteryVoltageGreenMinPerCellV' => p.batteryVoltageGreenMinPerCellV, 'batteryVoltageRedMinPerCellV' => p.batteryVoltageRedMinPerCellV, 'rxQualityGreenMin' => p.rxQualityGreenMin, 'rxQualityRedMin' => p.rxQualityRedMin, 'rxSnrGreenMinDb' => p.rxSnrGreenMinDb, 'rxSnrRedMinDb' => p.rxSnrRedMinDb, _ => throw ArgumentError(key), }; @override void initState() { super.initState(); final base = widget.existing ?? DroneProfile.t1Ranger(); _nameController = TextEditingController( text: widget.existing != null ? base.name : 'New profile', ); _initialValues = { for (final group in _groups) for (final field in group.fields) field.key: _fieldValue(base, field.key), }; _fieldValues = Map.of(_initialValues); } @override void dispose() { _nameController.dispose(); super.dispose(); } Future _save() async { if (!_formKey.currentState!.validate()) return; double v(String key) => _fieldValues[key]!; final profile = DroneProfile( id: widget.existing?.id, name: _nameController.text.trim(), minSpeed: v('minSpeed'), cruiseSpeed: v('cruiseSpeed'), maxSpeed: v('maxSpeed'), minTurnRadius: v('minTurnRadius'), loiterRadius: v('loiterRadius'), catchRadius: v('catchRadius'), maxClimbRate: v('maxClimbRate'), maxDescentRate: v('maxDescentRate'), maxRangeM: v('maxRangeM'), maxEnduranceMin: v('maxEnduranceMin'), maxAltitudeM: v('maxAltitudeM'), gpsSatGreenMin: v('gpsSatGreenMin'), gpsSatRedMin: v('gpsSatRedMin'), hdopGreenMax: v('hdopGreenMax'), hdopRedMax: v('hdopRedMax'), currentGreenMaxA: v('currentGreenMaxA'), currentRedMaxA: v('currentRedMaxA'), batteryCellCount: v('batteryCellCount').round(), batteryVoltageGreenMinPerCellV: v('batteryVoltageGreenMinPerCellV'), batteryVoltageRedMinPerCellV: v('batteryVoltageRedMinPerCellV'), rxQualityGreenMin: v('rxQualityGreenMin'), rxQualityRedMin: v('rxQualityRedMin'), rxSnrGreenMinDb: v('rxSnrGreenMinDb'), rxSnrRedMinDb: v('rxSnrRedMinDb'), ); final repo = ref.read(droneProfileRepositoryProvider); if (profile.id == null) { final id = await repo.insert(profile); await ref.read(activeDroneProfileProvider.notifier).select(profile.copyWith(id: id)); } else { await repo.update(profile); await ref.read(activeDroneProfileProvider.notifier).reloadIfActive(profile.id!); } if (mounted) Navigator.of(context).pop(); } @override Widget build(BuildContext context) { return Material( color: const Color(0xEB080808), child: SafeArea( child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 8, 12), child: Row( children: [ Expanded( child: Text( widget.existing == null ? 'New drone profile' : 'Edit drone profile', style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w700, ), ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close, color: Colors.white), ), ], ), ), const Divider(color: Colors.white24, height: 1), Expanded( child: Form( key: _formKey, child: ListView( physics: _scrolling ? const ClampingScrollPhysics() : const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(16), children: [ TextFormField( controller: _nameController, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(labelText: 'Name'), validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null, ), for (final group in _groups) ...[ const SizedBox(height: 20), Text( group.title, style: const TextStyle( color: Colors.white70, fontWeight: FontWeight.w700, fontSize: 13, ), ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final field in group.fields) Expanded( child: Padding( padding: const EdgeInsets.only(right: 8), child: SwipeValueInput( label: field.label, value: _fieldValues[field.key]!, initialValue: _initialValues[field.key]!, min: field.min, max: field.max, stepBig: field.stepBig, stepSmall: field.stepSmall, decimals: field.decimals, unit: field.unit, onChanged: (v) => setState(() => _fieldValues[field.key] = v), onDragActiveChanged: (active) => setState(() => _scrolling = !active), ), ), ), ], ), ], const SizedBox(height: 24), FilledButton( onPressed: _save, child: const Text('Save'), ), ], ), ), ), ], ), ), ); } }