import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/mission/drone_profile.dart'; import '../../services/database/mission_repository.dart'; import '../../services/sharing/drone_share_codec.dart'; import '../../services/sharing/mission_share_codec.dart'; import '../providers/active_drone_profile_provider.dart'; import '../providers/database_provider.dart'; import '../providers/mission_meta_provider.dart'; import '../providers/sharing_provider.dart'; import '../providers/wind_provider.dart'; import 'drone_profile_editor.dart'; enum _ManagementTab { missions, drones } /// Vollbild-Verwaltungsmenue fuer Missionen und Drohnenprofile (Doku 3.5/ /// 3.6/7.1/7.3), per Tabs umschaltbar - analog #missionsPanel im HTML- /// Demonstrator. Erreichbar ueber die Mission-/Drone-Chips in der /// Fusszeile, die jeweils mit dem passenden Starttab oeffnen. class MissionsDronesPanel extends ConsumerStatefulWidget { const MissionsDronesPanel.missions({super.key}) : _initialTab = _ManagementTab.missions; const MissionsDronesPanel.drones({super.key}) : _initialTab = _ManagementTab.drones; final _ManagementTab _initialTab; @override ConsumerState createState() => _MissionsDronesPanelState(); } class _MissionsDronesPanelState extends ConsumerState { late _ManagementTab _tab = widget._initialTab; @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: [ const Expanded( child: Text( 'Missions & Drones', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w700, ), ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close, color: Colors.white), ), ], ), ), Row( children: [ _tabButton('Missions', _ManagementTab.missions), _tabButton('Drones', _ManagementTab.drones), ], ), const Divider(color: Colors.white24, height: 1), Expanded( child: switch (_tab) { _ManagementTab.missions => _missionsTab(), _ManagementTab.drones => _dronesTab(), }, ), ], ), ), ); } Widget _tabButton(String label, _ManagementTab tab) { final active = _tab == tab; return Expanded( child: InkWell( onTap: () => setState(() => _tab = tab), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: active ? const Color(0xFF4D9DFF) : Colors.transparent, width: 2, ), ), ), child: Text( label, textAlign: TextAlign.center, style: TextStyle( color: active ? Colors.white : Colors.white60, fontWeight: FontWeight.w600, fontSize: 14, ), ), ), ), ); } // --- Missions --- Widget _missionsTab() { final summaries = ref.watch(missionSummariesProvider); final currentId = ref.watch(currentMissionMetaProvider).id; return Column( children: [ Padding( padding: const EdgeInsets.all(12), child: Row( children: [ _ToolbarButton( icon: Icons.add, label: 'New mission', onTap: () async { await ref.read(currentMissionMetaProvider.notifier).startNew(); if (mounted) Navigator.of(context).pop(); }, ), const SizedBox(width: 8), _ToolbarButton( icon: Icons.file_upload_outlined, label: 'Import', onTap: _importMission, ), ], ), ), Expanded( child: summaries.when( data: (missions) { if (missions.isEmpty) { return const Center( child: Text('No saved missions yet', style: TextStyle(color: Colors.white60, fontSize: 15)), ); } final sorted = [...missions] ..sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: sorted.length, itemBuilder: (context, index) => _missionRow(sorted[index], isActive: sorted[index].id == currentId), ); }, loading: () => const Center( child: CircularProgressIndicator(color: Color(0xFF4D9DFF)), ), error: (err, st) => Center( child: Text('Error loading missions: $err', style: const TextStyle(color: Color(0xFFE2574A))), ), ), ), ], ); } Widget _missionRow(MissionSummary m, {required bool isActive}) { return GestureDetector( onTap: () async { await ref.read(currentMissionMetaProvider.notifier).loadMission(m.id); await ref.read(headerWindProvider.notifier).refresh(); if (mounted) Navigator.of(context).pop(); }, child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: isActive ? const Color(0xFF4D9DFF).withValues(alpha: 0.28) : Colors.white.withValues(alpha: 0.04), border: isActive ? Border.all(color: const Color(0xFF4D9DFF)) : null, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Expanded( flex: 2, child: Text(m.name, style: const TextStyle(color: Colors.white)), ), Expanded( child: Text('${m.waypointCount} wp', style: const TextStyle(color: Colors.white70)), ), Expanded( child: Text(_formatDate(m.updatedAt), style: const TextStyle(color: Colors.white70)), ), IconButton( onPressed: () => _renameMission(m), icon: const Icon(Icons.edit, color: Colors.white70, size: 18), ), IconButton( onPressed: () => _shareMission(m), icon: const Icon(Icons.share, color: Color(0xFF4D9DFF), size: 18), ), IconButton( onPressed: () => ref.read(missionRepositoryProvider).delete(m.id), icon: const Icon(Icons.delete_outline, color: Color(0xFFE2574A), size: 18), ), ], ), ), ); } /// Teilt eine gespeicherte Mission als JSON-Datei ueber das native /// Share-Sheet (Doku 3.6, analog shareMission() im HTML-Demonstrator) - /// laedt zuerst die vollstaendige Wegpunktliste nach, da [MissionSummary] /// aus Speichergruenden nur die Anzahl fuehrt. Future _shareMission(MissionSummary m) async { final full = await ref.read(missionRepositoryProvider).load(m.id); if (full == null) return; final json = buildMissionExportJson(MissionExportData( name: full.name, placeName: full.placeName, createdAt: full.createdAt, waypoints: full.waypoints, )); await ref.read(sharingServiceProvider).shareJsonFile( json: json, filename: missionExportFilename(full.placeName ?? full.name), shareTitle: 'Mission: ${full.placeName ?? full.name}', ); } /// Importiert eine zuvor geteilte Missionsdatei als neue, eigenstaendige /// Mission (Doku 3.6, analog dem `change`-Handler von importMissionInput /// im HTML-Demonstrator) - ueberschreibt nie eine bestehende Mission. Future _importMission() async { final messenger = ScaffoldMessenger.of(context); final text = await ref.read(sharingServiceProvider).pickJsonFileText(); if (text == null) return; final ParsedMissionImport parsed; try { parsed = parseMissionImportJson(text); } on FormatException catch (e) { messenger.showSnackBar(SnackBar(content: Text(e.message))); return; } await ref.read(missionRepositoryProvider).upsert( name: parsed.name, placeName: parsed.placeName, waypoints: parsed.waypoints, ); if (parsed.versionMismatch != null) { messenger.showSnackBar(SnackBar( content: Text( 'Imported from a different app version (${parsed.versionMismatch}) – check carefully'), )); } else { messenger.showSnackBar(const SnackBar(content: Text('Mission imported'))); } } Future _renameMission(MissionSummary m) async { final controller = TextEditingController(text: m.name); final newName = await showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF1A1A1A), title: const Text('Rename mission', style: TextStyle(color: Colors.white)), content: TextField( controller: controller, autofocus: true, style: const TextStyle(color: Colors.white), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.of(context).pop(controller.text.trim()), child: const Text('Rename'), ), ], ), ); if (newName == null || newName.isEmpty) return; await ref.read(currentMissionMetaProvider.notifier).renameMission(m.id, newName); } String _formatDate(DateTime dt) => '${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year}'; // --- Drones --- Widget _dronesTab() { final profiles = ref.watch(droneProfilesProvider); final activeId = ref.watch(activeDroneProfileProvider).id; return Column( children: [ Padding( padding: const EdgeInsets.all(12), child: Row( children: [ _ToolbarButton( icon: Icons.add, label: 'New profile', onTap: () => Navigator.of(context).push(MaterialPageRoute( builder: (_) => const DroneProfileEditor(), )), ), const SizedBox(width: 8), _ToolbarButton( icon: Icons.file_upload_outlined, label: 'Import', onTap: _importDrone, ), ], ), ), Expanded( child: profiles.when( data: (drones) => ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: drones.length, itemBuilder: (context, index) => _droneRow(drones[index], drones, isActive: drones[index].id == activeId), ), loading: () => const Center( child: CircularProgressIndicator(color: Color(0xFF4D9DFF)), ), error: (err, st) => Center( child: Text('Error loading drones: $err', style: const TextStyle(color: Color(0xFFE2574A))), ), ), ), ], ); } Widget _droneRow(DroneProfile d, List all, {required bool isActive}) { return GestureDetector( onTap: () async { await ref.read(activeDroneProfileProvider.notifier).select(d); if (mounted) Navigator.of(context).pop(); }, child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: isActive ? const Color(0xFF4D9DFF).withValues(alpha: 0.28) : Colors.white.withValues(alpha: 0.04), border: isActive ? Border.all(color: const Color(0xFF4D9DFF)) : null, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(d.name, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w600)), Text(d.summary, style: const TextStyle(color: Colors.white60, fontSize: 12)), ], ), ), IconButton( onPressed: () => Navigator.of(context).push(MaterialPageRoute( builder: (_) => DroneProfileEditor(existing: d), )), icon: const Icon(Icons.edit, color: Colors.white70, size: 18), ), IconButton( onPressed: () => _shareDrone(d), icon: const Icon(Icons.share, color: Color(0xFF4D9DFF), size: 18), ), IconButton( onPressed: () => _deleteDrone(d, all), icon: const Icon(Icons.delete_outline, color: Color(0xFFE2574A), size: 18), ), ], ), ), ); } Future _deleteDrone(DroneProfile d, List all) async { if (all.length <= 1) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('At least one drone profile is required')), ); return; } final wasActive = ref.read(activeDroneProfileProvider).id == d.id; await ref.read(droneProfileRepositoryProvider).delete(d.id!); if (wasActive) { final remaining = all.where((p) => p.id != d.id).toList(); await ref.read(activeDroneProfileProvider.notifier).fallBackTo(remaining.first); } } /// Teilt ein Drohnenprofil als JSON-Datei ueber das native Share-Sheet /// (Doku 3.5, analog shareDrone() im HTML-Demonstrator). Future _shareDrone(DroneProfile d) async { final json = buildDroneExportJson(d); await ref.read(sharingServiceProvider).shareJsonFile( json: json, filename: droneExportFilename(d.name), shareTitle: 'Drone profile: ${d.name}', ); } /// Importiert ein zuvor geteiltes Drohnenprofil als neues, eigenstaendiges /// Profil (Doku 3.5, analog dem `change`-Handler von importDroneInput im /// HTML-Demonstrator) - ueberschreibt nie ein bestehendes Profil. Future _importDrone() async { final messenger = ScaffoldMessenger.of(context); final text = await ref.read(sharingServiceProvider).pickJsonFileText(); if (text == null) return; final ParsedDroneImport parsed; try { parsed = parseDroneImportJson(text); } on FormatException catch (e) { messenger.showSnackBar(SnackBar(content: Text(e.message))); return; } await ref.read(droneProfileRepositoryProvider).insert(parsed.profile); if (parsed.versionMismatch != null) { messenger.showSnackBar(SnackBar( content: Text( 'Imported from a different app version (${parsed.versionMismatch}) – check carefully'), )); } else { messenger.showSnackBar(const SnackBar(content: Text('Drone profile imported'))); } } } class _ToolbarButton extends StatelessWidget { const _ToolbarButton({required this.icon, required this.label, required this.onTap}); final IconData icon; final String label; final VoidCallback onTap; @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: const Color(0xFF4D9DFF).withValues(alpha: 0.2), border: Border.all(color: const Color(0xFF4D9DFF).withValues(alpha: 0.5)), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: const Color(0xFFCFE2FF)), const SizedBox(width: 6), Text(label, style: const TextStyle( color: Color(0xFFCFE2FF), fontWeight: FontWeight.w600, fontSize: 14)), ], ), ), ); } }