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'), ], ), ]; 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, _ => 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'), ); 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'), ), ], ), ), ), ], ), ), ); } }