From aaec7f0f72fdd0633eaad6ff0d716ec12d999fb4 Mon Sep 17 00:00:00 2001 From: Constantin Leue <77660791+Consti3D@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:59:09 +0200 Subject: [PATCH] Add mission/drone profile management with autosave and persistence Vervollstaendigt Doku 3.5/3.6/7.1/7.3/7.5: ein Vollbild-Verwaltungsmenue fuer Missionen und Drohnenprofile (per Tabs umschaltbar, wie im HTML- Demonstrator #missionsPanel gemeinsam mit der Profilliste), Autosave der aktuell bearbeiteten Mission, Drift-Persistenz statt der bisherigen In-Memory-only currentMissionProvider, sowie Anzeige/Zugang ueber neue Chips in der Fusszeile (HTML-Demonstrator: #missionNameBtn/#droneNameBtn). Datenbank (services/database/): Missions-Tabelle um updatedAt ergaenzt, neue DroneProfiles- und AppSettingsTable-Tabellen (schemaVersion 2 mit onUpgrade-Migration). Drei Repositories kapseln Drift-Zugriff + JSON-(De-)Serialisierung der Wegpunktliste: MissionRepository, DroneProfileRepository, AppSettingsRepository - je mit Unit-Tests gegen eine In-Memory-Datenbank (NativeDatabase.memory()). Provider: CurrentMissionMetaNotifier haelt Name/id der aktuellen Mission und autosaved sie 800ms-debounced (identisch zum HTML-Demonstrator: scheduleAutosave()/flushPendingAutosave()) - inklusive Wiederherstellung des zuletzt bearbeiteten Autosave-Drafts beim App-Start (Doku 7.5). ActiveDroneProfileNotifier haelt das aktive Profil, seedet beim ersten Start automatisch T1 Ranger und merkt sich die Auswahl ueber AppSettingsTable neustart-fest. UI: MissionsDronesPanel (Missions-Tab: Liste mit Umbenennen/Loeschen/ Laden; Drones-Tab: Liste mit Bearbeiten/Loeschen/Auswaehlen, faellt beim Loeschen des aktiven Profils auf das naechste zurueck). DroneProfileEditor als Formular (bewusste Vereinfachung gegenueber den Swipe-Gesten-Chips des HTML-Demonstrators - bei elf Feldern ist ein normales Formular auf einem Mobilgeraet zugaenglicher). Rename per AlertDialog+TextField statt Browser-prompt(). Aktives Drohnenprofil ist jetzt tatsaechlich wirksam statt nur eine feste Anzeige: Speed-/Alt-Drehrad-Grenzen, Fangradius neuer Wegpunkte und die Flugpfad-Machbarkeitspruefung (Kurvenradius, Steig-/Sinkrate) in PlanScreen und WaypointListPanel lesen jetzt activeDroneProfileProvider statt der bisherigen statischen DefaultDroneProfile-Konstanten (die als T1-Ranger-Seed-Werte weiterleben). Beim Verifizieren zwei echte Bugs in BottomStatsBar gefunden und gefixt: ein Stack+Align-ohne-Factor blaehte sich auf unbegrenzte Groesse auf und verschob die Details-Pille aus ihrer sichtbaren Position (durch zwei gleich grosse Expanded-Bereiche ersetzt); zwei ConstrainedBox-Chips nebeneinander verursachten auf schmaleren Breiten einen RenderFlex- Overflow (durch Flexible ersetzt). Widget-Tests: AppShell/PlanScreen initialisieren beim Start jetzt die echte Datenbank - alle Tests, die AppShell pumpen, ueberschreiben appDatabaseProvider testweise mit einer In-Memory-Instanz. Ausserdem mussten mehrere Tests den neuen 800ms-Autosave-Timer abwarten (wie zuvor schon beim WaypointChip-Flash-Timer etabliert), damit nach Testende kein Timer mehr aussteht. Verifiziert: flutter analyze (0 issues), flutter test (59/59, davon 14 neue Repository-Tests), manuell auf Pixel_10a-Emulator - Wegpunkte werden automatisch als "New mission" gespeichert und erscheinen in der Liste, Umbenennen/Laden/Loeschen funktionieren, neues Drohnenprofil mit abweichender Max-Speed wird angelegt+aktiviert+in der Fusszeile angezeigt, Loeschen des aktiven Profils faellt auf das verbleibende zurueck. Co-Authored-By: Claude Sonnet 5 --- app/lib/domain/mission/drone_profile.dart | 96 + app/lib/services/database/app_database.dart | 51 +- app/lib/services/database/app_database.g.dart | 1667 ++++++++++++++++- .../database/app_settings_repository.dart | 42 + .../database/drone_profile_repository.dart | 80 + .../services/database/mission_repository.dart | 149 ++ .../active_drone_profile_provider.dart | 62 + app/lib/ui/providers/database_provider.dart | 36 + .../ui/providers/mission_meta_provider.dart | 122 ++ app/lib/ui/screens/plan/plan_screen.dart | 41 +- app/lib/ui/widgets/bottom_stats_bar.dart | 180 +- app/lib/ui/widgets/drone_profile_editor.dart | 218 +++ app/lib/ui/widgets/missions_drones_panel.dart | 373 ++++ app/lib/ui/widgets/waypoint_list_panel.dart | 38 +- .../app_settings_repository_test.dart | 52 + .../drone_profile_repository_test.dart | 67 + .../services/mission_repository_test.dart | 104 + app/test/widget_test.dart | 70 +- 18 files changed, 3372 insertions(+), 76 deletions(-) create mode 100644 app/lib/domain/mission/drone_profile.dart create mode 100644 app/lib/services/database/app_settings_repository.dart create mode 100644 app/lib/services/database/drone_profile_repository.dart create mode 100644 app/lib/services/database/mission_repository.dart create mode 100644 app/lib/ui/providers/active_drone_profile_provider.dart create mode 100644 app/lib/ui/providers/database_provider.dart create mode 100644 app/lib/ui/providers/mission_meta_provider.dart create mode 100644 app/lib/ui/widgets/drone_profile_editor.dart create mode 100644 app/lib/ui/widgets/missions_drones_panel.dart create mode 100644 app/test/services/app_settings_repository_test.dart create mode 100644 app/test/services/drone_profile_repository_test.dart create mode 100644 app/test/services/mission_repository_test.dart diff --git a/app/lib/domain/mission/drone_profile.dart b/app/lib/domain/mission/drone_profile.dart new file mode 100644 index 0000000..0903f81 --- /dev/null +++ b/app/lib/domain/mission/drone_profile.dart @@ -0,0 +1,96 @@ +/// Ein Drohnen-Profil mit Flugleistungsdaten (Architektur-Doku 3.5/7.3): +/// FC-Typ ist bewusst NICHT Teil dieses MVP-Profils (nur iNAV/MSP im +/// Scope, siehe CLAUDE.md) - anders als im HTML-Demonstrator, der bereits +/// auf MAVLink vorbereitet. +class DroneProfile { + const DroneProfile({ + this.id, + required this.name, + required this.minSpeed, + required this.cruiseSpeed, + required this.maxSpeed, + required this.minTurnRadius, + required this.loiterRadius, + required this.catchRadius, + required this.maxClimbRate, + required this.maxDescentRate, + required this.maxRangeM, + required this.maxEnduranceMin, + required this.maxAltitudeM, + }); + + /// Null fuer ein noch nicht gespeichertes (neu angelegtes) Profil. + final int? id; + final String name; + final double minSpeed; + final double cruiseSpeed; + final double maxSpeed; + final double minTurnRadius; + final double loiterRadius; + final double catchRadius; + final double maxClimbRate; + final double maxDescentRate; + final double maxRangeM; + final double maxEnduranceMin; + final double maxAltitudeM; + + /// T1 Ranger (Heewing) - Standardprofil (Doku 3.5), analog DEFAULT_DRONE + /// im HTML-Demonstrator. + static const _t1RangerName = 'T1 Ranger'; + + factory DroneProfile.t1Ranger() => const DroneProfile( + name: _t1RangerName, + minSpeed: 13, + cruiseSpeed: 15, + maxSpeed: 25, + minTurnRadius: 35, + loiterRadius: 100, + catchRadius: 60, + maxClimbRate: 5, + maxDescentRate: 6, + maxRangeM: 15000, + maxEnduranceMin: 45, + maxAltitudeM: 2000, + ); + + /// Einzeilige Zusammenfassung fuer die Profilliste (Doku 3.5), analog + /// droneSummary() im HTML-Demonstrator. + String get summary { + final rangeKm = (maxRangeM / 100).round() / 10; + return '$minSpeed–$cruiseSpeed–$maxSpeed m/s · R${minTurnRadius.round()}m · ' + '↑${maxClimbRate.round()}/↓${maxDescentRate.round()} m/s · ${rangeKm}km · ' + '${maxEnduranceMin.round()}min · ceil ${maxAltitudeM.round()}m'; + } + + DroneProfile copyWith({ + int? id, + String? name, + double? minSpeed, + double? cruiseSpeed, + double? maxSpeed, + double? minTurnRadius, + double? loiterRadius, + double? catchRadius, + double? maxClimbRate, + double? maxDescentRate, + double? maxRangeM, + double? maxEnduranceMin, + double? maxAltitudeM, + }) { + return DroneProfile( + id: id ?? this.id, + name: name ?? this.name, + minSpeed: minSpeed ?? this.minSpeed, + cruiseSpeed: cruiseSpeed ?? this.cruiseSpeed, + maxSpeed: maxSpeed ?? this.maxSpeed, + minTurnRadius: minTurnRadius ?? this.minTurnRadius, + loiterRadius: loiterRadius ?? this.loiterRadius, + catchRadius: catchRadius ?? this.catchRadius, + maxClimbRate: maxClimbRate ?? this.maxClimbRate, + maxDescentRate: maxDescentRate ?? this.maxDescentRate, + maxRangeM: maxRangeM ?? this.maxRangeM, + maxEnduranceMin: maxEnduranceMin ?? this.maxEnduranceMin, + maxAltitudeM: maxAltitudeM ?? this.maxAltitudeM, + ); + } +} diff --git a/app/lib/services/database/app_database.dart b/app/lib/services/database/app_database.dart index 96874ce..471671e 100644 --- a/app/lib/services/database/app_database.dart +++ b/app/lib/services/database/app_database.dart @@ -17,17 +17,64 @@ class Missions extends Table { TextColumn get location => text().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => + dateTime().withDefault(currentDateAndTime)(); TextColumn get waypointsJson => text()(); BoolColumn get isDirty => boolean().withDefault(const Constant(true))(); TextColumn get serverId => text().nullable()(); } -@DriftDatabase(tables: [Missions]) +/// Drohnen-Profile (Doku 3.5/7.3) - FC-Typ bewusst nicht Teil des Schemas, +/// siehe DroneProfile-Domainklasse. +class DroneProfiles extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + RealColumn get minSpeed => real()(); + RealColumn get cruiseSpeed => real()(); + RealColumn get maxSpeed => real()(); + RealColumn get minTurnRadius => real()(); + RealColumn get loiterRadius => real()(); + RealColumn get catchRadius => real()(); + RealColumn get maxClimbRate => real()(); + RealColumn get maxDescentRate => real()(); + RealColumn get maxRangeM => real()(); + RealColumn get maxEnduranceMin => real()(); + RealColumn get maxAltitudeM => real()(); +} + +/// Einzeilige Einstellungstabelle (id fest 0) fuer app-weite Auswahl, die +/// einen Neustart ueberleben soll: zuletzt aktives Drohnenprofil und die +/// zuletzt bearbeitete (autosaved) Mission, um sie beim naechsten Start +/// fortzusetzen (Doku 7.5: Autosave/Mission-Drafts). +class AppSettingsTable extends Table { + IntColumn get id => integer()(); + IntColumn get activeDroneProfileId => integer().nullable()(); + IntColumn get currentMissionId => integer().nullable()(); + + @override + Set get primaryKey => {id}; +} + +@DriftDatabase(tables: [Missions, DroneProfiles, AppSettingsTable]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); + AppDatabase.forTesting(super.executor); + @override - int get schemaVersion => 1; + int get schemaVersion => 2; + + @override + MigrationStrategy get migration => MigrationStrategy( + onCreate: (m) => m.createAll(), + onUpgrade: (m, from, to) async { + if (from < 2) { + await m.addColumn(missions, missions.updatedAt); + await m.createTable(droneProfiles); + await m.createTable(appSettingsTable); + } + }, + ); static LazyDatabase _openConnection() { return LazyDatabase(() async { diff --git a/app/lib/services/database/app_database.g.dart b/app/lib/services/database/app_database.g.dart index fb01165..f32706d 100644 --- a/app/lib/services/database/app_database.g.dart +++ b/app/lib/services/database/app_database.g.dart @@ -53,6 +53,18 @@ class $MissionsTable extends Missions with TableInfo<$MissionsTable, Mission> { requiredDuringInsert: false, defaultValue: currentDateAndTime, ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); static const VerificationMeta _waypointsJsonMeta = const VerificationMeta( 'waypointsJson', ); @@ -96,6 +108,7 @@ class $MissionsTable extends Missions with TableInfo<$MissionsTable, Mission> { name, location, createdAt, + updatedAt, waypointsJson, isDirty, serverId, @@ -135,6 +148,12 @@ class $MissionsTable extends Missions with TableInfo<$MissionsTable, Mission> { createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), ); } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } if (data.containsKey('waypoints_json')) { context.handle( _waypointsJsonMeta, @@ -183,6 +202,10 @@ class $MissionsTable extends Missions with TableInfo<$MissionsTable, Mission> { DriftSqlType.dateTime, data['${effectivePrefix}created_at'], )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, waypointsJson: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}waypoints_json'], @@ -209,6 +232,7 @@ class Mission extends DataClass implements Insertable { final String name; final String? location; final DateTime createdAt; + final DateTime updatedAt; final String waypointsJson; final bool isDirty; final String? serverId; @@ -217,6 +241,7 @@ class Mission extends DataClass implements Insertable { required this.name, this.location, required this.createdAt, + required this.updatedAt, required this.waypointsJson, required this.isDirty, this.serverId, @@ -230,6 +255,7 @@ class Mission extends DataClass implements Insertable { map['location'] = Variable(location); } map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); map['waypoints_json'] = Variable(waypointsJson); map['is_dirty'] = Variable(isDirty); if (!nullToAbsent || serverId != null) { @@ -246,6 +272,7 @@ class Mission extends DataClass implements Insertable { ? const Value.absent() : Value(location), createdAt: Value(createdAt), + updatedAt: Value(updatedAt), waypointsJson: Value(waypointsJson), isDirty: Value(isDirty), serverId: serverId == null && nullToAbsent @@ -264,6 +291,7 @@ class Mission extends DataClass implements Insertable { name: serializer.fromJson(json['name']), location: serializer.fromJson(json['location']), createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), waypointsJson: serializer.fromJson(json['waypointsJson']), isDirty: serializer.fromJson(json['isDirty']), serverId: serializer.fromJson(json['serverId']), @@ -277,6 +305,7 @@ class Mission extends DataClass implements Insertable { 'name': serializer.toJson(name), 'location': serializer.toJson(location), 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), 'waypointsJson': serializer.toJson(waypointsJson), 'isDirty': serializer.toJson(isDirty), 'serverId': serializer.toJson(serverId), @@ -288,6 +317,7 @@ class Mission extends DataClass implements Insertable { String? name, Value location = const Value.absent(), DateTime? createdAt, + DateTime? updatedAt, String? waypointsJson, bool? isDirty, Value serverId = const Value.absent(), @@ -296,6 +326,7 @@ class Mission extends DataClass implements Insertable { name: name ?? this.name, location: location.present ? location.value : this.location, createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, waypointsJson: waypointsJson ?? this.waypointsJson, isDirty: isDirty ?? this.isDirty, serverId: serverId.present ? serverId.value : this.serverId, @@ -306,6 +337,7 @@ class Mission extends DataClass implements Insertable { name: data.name.present ? data.name.value : this.name, location: data.location.present ? data.location.value : this.location, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, waypointsJson: data.waypointsJson.present ? data.waypointsJson.value : this.waypointsJson, @@ -321,6 +353,7 @@ class Mission extends DataClass implements Insertable { ..write('name: $name, ') ..write('location: $location, ') ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') ..write('waypointsJson: $waypointsJson, ') ..write('isDirty: $isDirty, ') ..write('serverId: $serverId') @@ -334,6 +367,7 @@ class Mission extends DataClass implements Insertable { name, location, createdAt, + updatedAt, waypointsJson, isDirty, serverId, @@ -346,6 +380,7 @@ class Mission extends DataClass implements Insertable { other.name == this.name && other.location == this.location && other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && other.waypointsJson == this.waypointsJson && other.isDirty == this.isDirty && other.serverId == this.serverId); @@ -356,6 +391,7 @@ class MissionsCompanion extends UpdateCompanion { final Value name; final Value location; final Value createdAt; + final Value updatedAt; final Value waypointsJson; final Value isDirty; final Value serverId; @@ -364,6 +400,7 @@ class MissionsCompanion extends UpdateCompanion { this.name = const Value.absent(), this.location = const Value.absent(), this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), this.waypointsJson = const Value.absent(), this.isDirty = const Value.absent(), this.serverId = const Value.absent(), @@ -373,6 +410,7 @@ class MissionsCompanion extends UpdateCompanion { required String name, this.location = const Value.absent(), this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), required String waypointsJson, this.isDirty = const Value.absent(), this.serverId = const Value.absent(), @@ -383,6 +421,7 @@ class MissionsCompanion extends UpdateCompanion { Expression? name, Expression? location, Expression? createdAt, + Expression? updatedAt, Expression? waypointsJson, Expression? isDirty, Expression? serverId, @@ -392,6 +431,7 @@ class MissionsCompanion extends UpdateCompanion { if (name != null) 'name': name, if (location != null) 'location': location, if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, if (waypointsJson != null) 'waypoints_json': waypointsJson, if (isDirty != null) 'is_dirty': isDirty, if (serverId != null) 'server_id': serverId, @@ -403,6 +443,7 @@ class MissionsCompanion extends UpdateCompanion { Value? name, Value? location, Value? createdAt, + Value? updatedAt, Value? waypointsJson, Value? isDirty, Value? serverId, @@ -412,6 +453,7 @@ class MissionsCompanion extends UpdateCompanion { name: name ?? this.name, location: location ?? this.location, createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, waypointsJson: waypointsJson ?? this.waypointsJson, isDirty: isDirty ?? this.isDirty, serverId: serverId ?? this.serverId, @@ -433,6 +475,9 @@ class MissionsCompanion extends UpdateCompanion { if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } if (waypointsJson.present) { map['waypoints_json'] = Variable(waypointsJson.value); } @@ -452,6 +497,7 @@ class MissionsCompanion extends UpdateCompanion { ..write('name: $name, ') ..write('location: $location, ') ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') ..write('waypointsJson: $waypointsJson, ') ..write('isDirty: $isDirty, ') ..write('serverId: $serverId') @@ -460,15 +506,1079 @@ class MissionsCompanion extends UpdateCompanion { } } +class $DroneProfilesTable extends DroneProfiles + with TableInfo<$DroneProfilesTable, DroneProfile> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $DroneProfilesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _minSpeedMeta = const VerificationMeta( + 'minSpeed', + ); + @override + late final GeneratedColumn minSpeed = GeneratedColumn( + 'min_speed', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _cruiseSpeedMeta = const VerificationMeta( + 'cruiseSpeed', + ); + @override + late final GeneratedColumn cruiseSpeed = GeneratedColumn( + 'cruise_speed', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _maxSpeedMeta = const VerificationMeta( + 'maxSpeed', + ); + @override + late final GeneratedColumn maxSpeed = GeneratedColumn( + 'max_speed', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _minTurnRadiusMeta = const VerificationMeta( + 'minTurnRadius', + ); + @override + late final GeneratedColumn minTurnRadius = GeneratedColumn( + 'min_turn_radius', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _loiterRadiusMeta = const VerificationMeta( + 'loiterRadius', + ); + @override + late final GeneratedColumn loiterRadius = GeneratedColumn( + 'loiter_radius', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _catchRadiusMeta = const VerificationMeta( + 'catchRadius', + ); + @override + late final GeneratedColumn catchRadius = GeneratedColumn( + 'catch_radius', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _maxClimbRateMeta = const VerificationMeta( + 'maxClimbRate', + ); + @override + late final GeneratedColumn maxClimbRate = GeneratedColumn( + 'max_climb_rate', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _maxDescentRateMeta = const VerificationMeta( + 'maxDescentRate', + ); + @override + late final GeneratedColumn maxDescentRate = GeneratedColumn( + 'max_descent_rate', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _maxRangeMMeta = const VerificationMeta( + 'maxRangeM', + ); + @override + late final GeneratedColumn maxRangeM = GeneratedColumn( + 'max_range_m', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _maxEnduranceMinMeta = const VerificationMeta( + 'maxEnduranceMin', + ); + @override + late final GeneratedColumn maxEnduranceMin = GeneratedColumn( + 'max_endurance_min', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + static const VerificationMeta _maxAltitudeMMeta = const VerificationMeta( + 'maxAltitudeM', + ); + @override + late final GeneratedColumn maxAltitudeM = GeneratedColumn( + 'max_altitude_m', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + minSpeed, + cruiseSpeed, + maxSpeed, + minTurnRadius, + loiterRadius, + catchRadius, + maxClimbRate, + maxDescentRate, + maxRangeM, + maxEnduranceMin, + maxAltitudeM, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'drone_profiles'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('min_speed')) { + context.handle( + _minSpeedMeta, + minSpeed.isAcceptableOrUnknown(data['min_speed']!, _minSpeedMeta), + ); + } else if (isInserting) { + context.missing(_minSpeedMeta); + } + if (data.containsKey('cruise_speed')) { + context.handle( + _cruiseSpeedMeta, + cruiseSpeed.isAcceptableOrUnknown( + data['cruise_speed']!, + _cruiseSpeedMeta, + ), + ); + } else if (isInserting) { + context.missing(_cruiseSpeedMeta); + } + if (data.containsKey('max_speed')) { + context.handle( + _maxSpeedMeta, + maxSpeed.isAcceptableOrUnknown(data['max_speed']!, _maxSpeedMeta), + ); + } else if (isInserting) { + context.missing(_maxSpeedMeta); + } + if (data.containsKey('min_turn_radius')) { + context.handle( + _minTurnRadiusMeta, + minTurnRadius.isAcceptableOrUnknown( + data['min_turn_radius']!, + _minTurnRadiusMeta, + ), + ); + } else if (isInserting) { + context.missing(_minTurnRadiusMeta); + } + if (data.containsKey('loiter_radius')) { + context.handle( + _loiterRadiusMeta, + loiterRadius.isAcceptableOrUnknown( + data['loiter_radius']!, + _loiterRadiusMeta, + ), + ); + } else if (isInserting) { + context.missing(_loiterRadiusMeta); + } + if (data.containsKey('catch_radius')) { + context.handle( + _catchRadiusMeta, + catchRadius.isAcceptableOrUnknown( + data['catch_radius']!, + _catchRadiusMeta, + ), + ); + } else if (isInserting) { + context.missing(_catchRadiusMeta); + } + if (data.containsKey('max_climb_rate')) { + context.handle( + _maxClimbRateMeta, + maxClimbRate.isAcceptableOrUnknown( + data['max_climb_rate']!, + _maxClimbRateMeta, + ), + ); + } else if (isInserting) { + context.missing(_maxClimbRateMeta); + } + if (data.containsKey('max_descent_rate')) { + context.handle( + _maxDescentRateMeta, + maxDescentRate.isAcceptableOrUnknown( + data['max_descent_rate']!, + _maxDescentRateMeta, + ), + ); + } else if (isInserting) { + context.missing(_maxDescentRateMeta); + } + if (data.containsKey('max_range_m')) { + context.handle( + _maxRangeMMeta, + maxRangeM.isAcceptableOrUnknown(data['max_range_m']!, _maxRangeMMeta), + ); + } else if (isInserting) { + context.missing(_maxRangeMMeta); + } + if (data.containsKey('max_endurance_min')) { + context.handle( + _maxEnduranceMinMeta, + maxEnduranceMin.isAcceptableOrUnknown( + data['max_endurance_min']!, + _maxEnduranceMinMeta, + ), + ); + } else if (isInserting) { + context.missing(_maxEnduranceMinMeta); + } + if (data.containsKey('max_altitude_m')) { + context.handle( + _maxAltitudeMMeta, + maxAltitudeM.isAcceptableOrUnknown( + data['max_altitude_m']!, + _maxAltitudeMMeta, + ), + ); + } else if (isInserting) { + context.missing(_maxAltitudeMMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + DroneProfile map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return DroneProfile( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + minSpeed: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}min_speed'], + )!, + cruiseSpeed: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}cruise_speed'], + )!, + maxSpeed: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}max_speed'], + )!, + minTurnRadius: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}min_turn_radius'], + )!, + loiterRadius: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}loiter_radius'], + )!, + catchRadius: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}catch_radius'], + )!, + maxClimbRate: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}max_climb_rate'], + )!, + maxDescentRate: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}max_descent_rate'], + )!, + maxRangeM: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}max_range_m'], + )!, + maxEnduranceMin: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}max_endurance_min'], + )!, + maxAltitudeM: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}max_altitude_m'], + )!, + ); + } + + @override + $DroneProfilesTable createAlias(String alias) { + return $DroneProfilesTable(attachedDatabase, alias); + } +} + +class DroneProfile extends DataClass implements Insertable { + final int id; + final String name; + final double minSpeed; + final double cruiseSpeed; + final double maxSpeed; + final double minTurnRadius; + final double loiterRadius; + final double catchRadius; + final double maxClimbRate; + final double maxDescentRate; + final double maxRangeM; + final double maxEnduranceMin; + final double maxAltitudeM; + const DroneProfile({ + required this.id, + required this.name, + required this.minSpeed, + required this.cruiseSpeed, + required this.maxSpeed, + required this.minTurnRadius, + required this.loiterRadius, + required this.catchRadius, + required this.maxClimbRate, + required this.maxDescentRate, + required this.maxRangeM, + required this.maxEnduranceMin, + required this.maxAltitudeM, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['min_speed'] = Variable(minSpeed); + map['cruise_speed'] = Variable(cruiseSpeed); + map['max_speed'] = Variable(maxSpeed); + map['min_turn_radius'] = Variable(minTurnRadius); + map['loiter_radius'] = Variable(loiterRadius); + map['catch_radius'] = Variable(catchRadius); + map['max_climb_rate'] = Variable(maxClimbRate); + map['max_descent_rate'] = Variable(maxDescentRate); + map['max_range_m'] = Variable(maxRangeM); + map['max_endurance_min'] = Variable(maxEnduranceMin); + map['max_altitude_m'] = Variable(maxAltitudeM); + return map; + } + + DroneProfilesCompanion toCompanion(bool nullToAbsent) { + return DroneProfilesCompanion( + id: Value(id), + name: Value(name), + minSpeed: Value(minSpeed), + cruiseSpeed: Value(cruiseSpeed), + maxSpeed: Value(maxSpeed), + minTurnRadius: Value(minTurnRadius), + loiterRadius: Value(loiterRadius), + catchRadius: Value(catchRadius), + maxClimbRate: Value(maxClimbRate), + maxDescentRate: Value(maxDescentRate), + maxRangeM: Value(maxRangeM), + maxEnduranceMin: Value(maxEnduranceMin), + maxAltitudeM: Value(maxAltitudeM), + ); + } + + factory DroneProfile.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return DroneProfile( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + minSpeed: serializer.fromJson(json['minSpeed']), + cruiseSpeed: serializer.fromJson(json['cruiseSpeed']), + maxSpeed: serializer.fromJson(json['maxSpeed']), + minTurnRadius: serializer.fromJson(json['minTurnRadius']), + loiterRadius: serializer.fromJson(json['loiterRadius']), + catchRadius: serializer.fromJson(json['catchRadius']), + maxClimbRate: serializer.fromJson(json['maxClimbRate']), + maxDescentRate: serializer.fromJson(json['maxDescentRate']), + maxRangeM: serializer.fromJson(json['maxRangeM']), + maxEnduranceMin: serializer.fromJson(json['maxEnduranceMin']), + maxAltitudeM: serializer.fromJson(json['maxAltitudeM']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'minSpeed': serializer.toJson(minSpeed), + 'cruiseSpeed': serializer.toJson(cruiseSpeed), + 'maxSpeed': serializer.toJson(maxSpeed), + 'minTurnRadius': serializer.toJson(minTurnRadius), + 'loiterRadius': serializer.toJson(loiterRadius), + 'catchRadius': serializer.toJson(catchRadius), + 'maxClimbRate': serializer.toJson(maxClimbRate), + 'maxDescentRate': serializer.toJson(maxDescentRate), + 'maxRangeM': serializer.toJson(maxRangeM), + 'maxEnduranceMin': serializer.toJson(maxEnduranceMin), + 'maxAltitudeM': serializer.toJson(maxAltitudeM), + }; + } + + DroneProfile copyWith({ + int? id, + String? name, + double? minSpeed, + double? cruiseSpeed, + double? maxSpeed, + double? minTurnRadius, + double? loiterRadius, + double? catchRadius, + double? maxClimbRate, + double? maxDescentRate, + double? maxRangeM, + double? maxEnduranceMin, + double? maxAltitudeM, + }) => DroneProfile( + id: id ?? this.id, + name: name ?? this.name, + minSpeed: minSpeed ?? this.minSpeed, + cruiseSpeed: cruiseSpeed ?? this.cruiseSpeed, + maxSpeed: maxSpeed ?? this.maxSpeed, + minTurnRadius: minTurnRadius ?? this.minTurnRadius, + loiterRadius: loiterRadius ?? this.loiterRadius, + catchRadius: catchRadius ?? this.catchRadius, + maxClimbRate: maxClimbRate ?? this.maxClimbRate, + maxDescentRate: maxDescentRate ?? this.maxDescentRate, + maxRangeM: maxRangeM ?? this.maxRangeM, + maxEnduranceMin: maxEnduranceMin ?? this.maxEnduranceMin, + maxAltitudeM: maxAltitudeM ?? this.maxAltitudeM, + ); + DroneProfile copyWithCompanion(DroneProfilesCompanion data) { + return DroneProfile( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + minSpeed: data.minSpeed.present ? data.minSpeed.value : this.minSpeed, + cruiseSpeed: data.cruiseSpeed.present + ? data.cruiseSpeed.value + : this.cruiseSpeed, + maxSpeed: data.maxSpeed.present ? data.maxSpeed.value : this.maxSpeed, + minTurnRadius: data.minTurnRadius.present + ? data.minTurnRadius.value + : this.minTurnRadius, + loiterRadius: data.loiterRadius.present + ? data.loiterRadius.value + : this.loiterRadius, + catchRadius: data.catchRadius.present + ? data.catchRadius.value + : this.catchRadius, + maxClimbRate: data.maxClimbRate.present + ? data.maxClimbRate.value + : this.maxClimbRate, + maxDescentRate: data.maxDescentRate.present + ? data.maxDescentRate.value + : this.maxDescentRate, + maxRangeM: data.maxRangeM.present ? data.maxRangeM.value : this.maxRangeM, + maxEnduranceMin: data.maxEnduranceMin.present + ? data.maxEnduranceMin.value + : this.maxEnduranceMin, + maxAltitudeM: data.maxAltitudeM.present + ? data.maxAltitudeM.value + : this.maxAltitudeM, + ); + } + + @override + String toString() { + return (StringBuffer('DroneProfile(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('minSpeed: $minSpeed, ') + ..write('cruiseSpeed: $cruiseSpeed, ') + ..write('maxSpeed: $maxSpeed, ') + ..write('minTurnRadius: $minTurnRadius, ') + ..write('loiterRadius: $loiterRadius, ') + ..write('catchRadius: $catchRadius, ') + ..write('maxClimbRate: $maxClimbRate, ') + ..write('maxDescentRate: $maxDescentRate, ') + ..write('maxRangeM: $maxRangeM, ') + ..write('maxEnduranceMin: $maxEnduranceMin, ') + ..write('maxAltitudeM: $maxAltitudeM') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + minSpeed, + cruiseSpeed, + maxSpeed, + minTurnRadius, + loiterRadius, + catchRadius, + maxClimbRate, + maxDescentRate, + maxRangeM, + maxEnduranceMin, + maxAltitudeM, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DroneProfile && + other.id == this.id && + other.name == this.name && + other.minSpeed == this.minSpeed && + other.cruiseSpeed == this.cruiseSpeed && + other.maxSpeed == this.maxSpeed && + other.minTurnRadius == this.minTurnRadius && + other.loiterRadius == this.loiterRadius && + other.catchRadius == this.catchRadius && + other.maxClimbRate == this.maxClimbRate && + other.maxDescentRate == this.maxDescentRate && + other.maxRangeM == this.maxRangeM && + other.maxEnduranceMin == this.maxEnduranceMin && + other.maxAltitudeM == this.maxAltitudeM); +} + +class DroneProfilesCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value minSpeed; + final Value cruiseSpeed; + final Value maxSpeed; + final Value minTurnRadius; + final Value loiterRadius; + final Value catchRadius; + final Value maxClimbRate; + final Value maxDescentRate; + final Value maxRangeM; + final Value maxEnduranceMin; + final Value maxAltitudeM; + const DroneProfilesCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.minSpeed = const Value.absent(), + this.cruiseSpeed = const Value.absent(), + this.maxSpeed = const Value.absent(), + this.minTurnRadius = const Value.absent(), + this.loiterRadius = const Value.absent(), + this.catchRadius = const Value.absent(), + this.maxClimbRate = const Value.absent(), + this.maxDescentRate = const Value.absent(), + this.maxRangeM = const Value.absent(), + this.maxEnduranceMin = const Value.absent(), + this.maxAltitudeM = const Value.absent(), + }); + DroneProfilesCompanion.insert({ + this.id = const Value.absent(), + required String name, + required double minSpeed, + required double cruiseSpeed, + required double maxSpeed, + required double minTurnRadius, + required double loiterRadius, + required double catchRadius, + required double maxClimbRate, + required double maxDescentRate, + required double maxRangeM, + required double maxEnduranceMin, + required double maxAltitudeM, + }) : name = Value(name), + minSpeed = Value(minSpeed), + cruiseSpeed = Value(cruiseSpeed), + maxSpeed = Value(maxSpeed), + minTurnRadius = Value(minTurnRadius), + loiterRadius = Value(loiterRadius), + catchRadius = Value(catchRadius), + maxClimbRate = Value(maxClimbRate), + maxDescentRate = Value(maxDescentRate), + maxRangeM = Value(maxRangeM), + maxEnduranceMin = Value(maxEnduranceMin), + maxAltitudeM = Value(maxAltitudeM); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? minSpeed, + Expression? cruiseSpeed, + Expression? maxSpeed, + Expression? minTurnRadius, + Expression? loiterRadius, + Expression? catchRadius, + Expression? maxClimbRate, + Expression? maxDescentRate, + Expression? maxRangeM, + Expression? maxEnduranceMin, + Expression? maxAltitudeM, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (minSpeed != null) 'min_speed': minSpeed, + if (cruiseSpeed != null) 'cruise_speed': cruiseSpeed, + if (maxSpeed != null) 'max_speed': maxSpeed, + if (minTurnRadius != null) 'min_turn_radius': minTurnRadius, + if (loiterRadius != null) 'loiter_radius': loiterRadius, + if (catchRadius != null) 'catch_radius': catchRadius, + if (maxClimbRate != null) 'max_climb_rate': maxClimbRate, + if (maxDescentRate != null) 'max_descent_rate': maxDescentRate, + if (maxRangeM != null) 'max_range_m': maxRangeM, + if (maxEnduranceMin != null) 'max_endurance_min': maxEnduranceMin, + if (maxAltitudeM != null) 'max_altitude_m': maxAltitudeM, + }); + } + + DroneProfilesCompanion copyWith({ + Value? id, + Value? name, + Value? minSpeed, + Value? cruiseSpeed, + Value? maxSpeed, + Value? minTurnRadius, + Value? loiterRadius, + Value? catchRadius, + Value? maxClimbRate, + Value? maxDescentRate, + Value? maxRangeM, + Value? maxEnduranceMin, + Value? maxAltitudeM, + }) { + return DroneProfilesCompanion( + id: id ?? this.id, + name: name ?? this.name, + minSpeed: minSpeed ?? this.minSpeed, + cruiseSpeed: cruiseSpeed ?? this.cruiseSpeed, + maxSpeed: maxSpeed ?? this.maxSpeed, + minTurnRadius: minTurnRadius ?? this.minTurnRadius, + loiterRadius: loiterRadius ?? this.loiterRadius, + catchRadius: catchRadius ?? this.catchRadius, + maxClimbRate: maxClimbRate ?? this.maxClimbRate, + maxDescentRate: maxDescentRate ?? this.maxDescentRate, + maxRangeM: maxRangeM ?? this.maxRangeM, + maxEnduranceMin: maxEnduranceMin ?? this.maxEnduranceMin, + maxAltitudeM: maxAltitudeM ?? this.maxAltitudeM, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (minSpeed.present) { + map['min_speed'] = Variable(minSpeed.value); + } + if (cruiseSpeed.present) { + map['cruise_speed'] = Variable(cruiseSpeed.value); + } + if (maxSpeed.present) { + map['max_speed'] = Variable(maxSpeed.value); + } + if (minTurnRadius.present) { + map['min_turn_radius'] = Variable(minTurnRadius.value); + } + if (loiterRadius.present) { + map['loiter_radius'] = Variable(loiterRadius.value); + } + if (catchRadius.present) { + map['catch_radius'] = Variable(catchRadius.value); + } + if (maxClimbRate.present) { + map['max_climb_rate'] = Variable(maxClimbRate.value); + } + if (maxDescentRate.present) { + map['max_descent_rate'] = Variable(maxDescentRate.value); + } + if (maxRangeM.present) { + map['max_range_m'] = Variable(maxRangeM.value); + } + if (maxEnduranceMin.present) { + map['max_endurance_min'] = Variable(maxEnduranceMin.value); + } + if (maxAltitudeM.present) { + map['max_altitude_m'] = Variable(maxAltitudeM.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('DroneProfilesCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('minSpeed: $minSpeed, ') + ..write('cruiseSpeed: $cruiseSpeed, ') + ..write('maxSpeed: $maxSpeed, ') + ..write('minTurnRadius: $minTurnRadius, ') + ..write('loiterRadius: $loiterRadius, ') + ..write('catchRadius: $catchRadius, ') + ..write('maxClimbRate: $maxClimbRate, ') + ..write('maxDescentRate: $maxDescentRate, ') + ..write('maxRangeM: $maxRangeM, ') + ..write('maxEnduranceMin: $maxEnduranceMin, ') + ..write('maxAltitudeM: $maxAltitudeM') + ..write(')')) + .toString(); + } +} + +class $AppSettingsTableTable extends AppSettingsTable + with TableInfo<$AppSettingsTableTable, AppSettingsTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $AppSettingsTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _activeDroneProfileIdMeta = + const VerificationMeta('activeDroneProfileId'); + @override + late final GeneratedColumn activeDroneProfileId = GeneratedColumn( + 'active_drone_profile_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _currentMissionIdMeta = const VerificationMeta( + 'currentMissionId', + ); + @override + late final GeneratedColumn currentMissionId = GeneratedColumn( + 'current_mission_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + activeDroneProfileId, + currentMissionId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'app_settings_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('active_drone_profile_id')) { + context.handle( + _activeDroneProfileIdMeta, + activeDroneProfileId.isAcceptableOrUnknown( + data['active_drone_profile_id']!, + _activeDroneProfileIdMeta, + ), + ); + } + if (data.containsKey('current_mission_id')) { + context.handle( + _currentMissionIdMeta, + currentMissionId.isAcceptableOrUnknown( + data['current_mission_id']!, + _currentMissionIdMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + AppSettingsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AppSettingsTableData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + activeDroneProfileId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}active_drone_profile_id'], + ), + currentMissionId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}current_mission_id'], + ), + ); + } + + @override + $AppSettingsTableTable createAlias(String alias) { + return $AppSettingsTableTable(attachedDatabase, alias); + } +} + +class AppSettingsTableData extends DataClass + implements Insertable { + final int id; + final int? activeDroneProfileId; + final int? currentMissionId; + const AppSettingsTableData({ + required this.id, + this.activeDroneProfileId, + this.currentMissionId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || activeDroneProfileId != null) { + map['active_drone_profile_id'] = Variable(activeDroneProfileId); + } + if (!nullToAbsent || currentMissionId != null) { + map['current_mission_id'] = Variable(currentMissionId); + } + return map; + } + + AppSettingsTableCompanion toCompanion(bool nullToAbsent) { + return AppSettingsTableCompanion( + id: Value(id), + activeDroneProfileId: activeDroneProfileId == null && nullToAbsent + ? const Value.absent() + : Value(activeDroneProfileId), + currentMissionId: currentMissionId == null && nullToAbsent + ? const Value.absent() + : Value(currentMissionId), + ); + } + + factory AppSettingsTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AppSettingsTableData( + id: serializer.fromJson(json['id']), + activeDroneProfileId: serializer.fromJson( + json['activeDroneProfileId'], + ), + currentMissionId: serializer.fromJson(json['currentMissionId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'activeDroneProfileId': serializer.toJson(activeDroneProfileId), + 'currentMissionId': serializer.toJson(currentMissionId), + }; + } + + AppSettingsTableData copyWith({ + int? id, + Value activeDroneProfileId = const Value.absent(), + Value currentMissionId = const Value.absent(), + }) => AppSettingsTableData( + id: id ?? this.id, + activeDroneProfileId: activeDroneProfileId.present + ? activeDroneProfileId.value + : this.activeDroneProfileId, + currentMissionId: currentMissionId.present + ? currentMissionId.value + : this.currentMissionId, + ); + AppSettingsTableData copyWithCompanion(AppSettingsTableCompanion data) { + return AppSettingsTableData( + id: data.id.present ? data.id.value : this.id, + activeDroneProfileId: data.activeDroneProfileId.present + ? data.activeDroneProfileId.value + : this.activeDroneProfileId, + currentMissionId: data.currentMissionId.present + ? data.currentMissionId.value + : this.currentMissionId, + ); + } + + @override + String toString() { + return (StringBuffer('AppSettingsTableData(') + ..write('id: $id, ') + ..write('activeDroneProfileId: $activeDroneProfileId, ') + ..write('currentMissionId: $currentMissionId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, activeDroneProfileId, currentMissionId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AppSettingsTableData && + other.id == this.id && + other.activeDroneProfileId == this.activeDroneProfileId && + other.currentMissionId == this.currentMissionId); +} + +class AppSettingsTableCompanion extends UpdateCompanion { + final Value id; + final Value activeDroneProfileId; + final Value currentMissionId; + const AppSettingsTableCompanion({ + this.id = const Value.absent(), + this.activeDroneProfileId = const Value.absent(), + this.currentMissionId = const Value.absent(), + }); + AppSettingsTableCompanion.insert({ + this.id = const Value.absent(), + this.activeDroneProfileId = const Value.absent(), + this.currentMissionId = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? activeDroneProfileId, + Expression? currentMissionId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (activeDroneProfileId != null) + 'active_drone_profile_id': activeDroneProfileId, + if (currentMissionId != null) 'current_mission_id': currentMissionId, + }); + } + + AppSettingsTableCompanion copyWith({ + Value? id, + Value? activeDroneProfileId, + Value? currentMissionId, + }) { + return AppSettingsTableCompanion( + id: id ?? this.id, + activeDroneProfileId: activeDroneProfileId ?? this.activeDroneProfileId, + currentMissionId: currentMissionId ?? this.currentMissionId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (activeDroneProfileId.present) { + map['active_drone_profile_id'] = Variable( + activeDroneProfileId.value, + ); + } + if (currentMissionId.present) { + map['current_mission_id'] = Variable(currentMissionId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AppSettingsTableCompanion(') + ..write('id: $id, ') + ..write('activeDroneProfileId: $activeDroneProfileId, ') + ..write('currentMissionId: $currentMissionId') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); late final $MissionsTable missions = $MissionsTable(this); + late final $DroneProfilesTable droneProfiles = $DroneProfilesTable(this); + late final $AppSettingsTableTable appSettingsTable = $AppSettingsTableTable( + this, + ); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [missions]; + List get allSchemaEntities => [ + missions, + droneProfiles, + appSettingsTable, + ]; } typedef $$MissionsTableCreateCompanionBuilder = @@ -477,6 +1587,7 @@ typedef $$MissionsTableCreateCompanionBuilder = required String name, Value location, Value createdAt, + Value updatedAt, required String waypointsJson, Value isDirty, Value serverId, @@ -487,6 +1598,7 @@ typedef $$MissionsTableUpdateCompanionBuilder = Value name, Value location, Value createdAt, + Value updatedAt, Value waypointsJson, Value isDirty, Value serverId, @@ -521,6 +1633,11 @@ class $$MissionsTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get waypointsJson => $composableBuilder( column: $table.waypointsJson, builder: (column) => ColumnFilters(column), @@ -566,6 +1683,11 @@ class $$MissionsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get waypointsJson => $composableBuilder( column: $table.waypointsJson, builder: (column) => ColumnOrderings(column), @@ -603,6 +1725,9 @@ class $$MissionsTableAnnotationComposer GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumn get waypointsJson => $composableBuilder( column: $table.waypointsJson, builder: (column) => column, @@ -647,6 +1772,7 @@ class $$MissionsTableTableManager Value name = const Value.absent(), Value location = const Value.absent(), Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), Value waypointsJson = const Value.absent(), Value isDirty = const Value.absent(), Value serverId = const Value.absent(), @@ -655,6 +1781,7 @@ class $$MissionsTableTableManager name: name, location: location, createdAt: createdAt, + updatedAt: updatedAt, waypointsJson: waypointsJson, isDirty: isDirty, serverId: serverId, @@ -665,6 +1792,7 @@ class $$MissionsTableTableManager required String name, Value location = const Value.absent(), Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), required String waypointsJson, Value isDirty = const Value.absent(), Value serverId = const Value.absent(), @@ -673,6 +1801,7 @@ class $$MissionsTableTableManager name: name, location: location, createdAt: createdAt, + updatedAt: updatedAt, waypointsJson: waypointsJson, isDirty: isDirty, serverId: serverId, @@ -699,10 +1828,546 @@ typedef $$MissionsTableProcessedTableManager = Mission, PrefetchHooks Function() >; +typedef $$DroneProfilesTableCreateCompanionBuilder = + DroneProfilesCompanion Function({ + Value id, + required String name, + required double minSpeed, + required double cruiseSpeed, + required double maxSpeed, + required double minTurnRadius, + required double loiterRadius, + required double catchRadius, + required double maxClimbRate, + required double maxDescentRate, + required double maxRangeM, + required double maxEnduranceMin, + required double maxAltitudeM, + }); +typedef $$DroneProfilesTableUpdateCompanionBuilder = + DroneProfilesCompanion Function({ + Value id, + Value name, + Value minSpeed, + Value cruiseSpeed, + Value maxSpeed, + Value minTurnRadius, + Value loiterRadius, + Value catchRadius, + Value maxClimbRate, + Value maxDescentRate, + Value maxRangeM, + Value maxEnduranceMin, + Value maxAltitudeM, + }); + +class $$DroneProfilesTableFilterComposer + extends Composer<_$AppDatabase, $DroneProfilesTable> { + $$DroneProfilesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get minSpeed => $composableBuilder( + column: $table.minSpeed, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get cruiseSpeed => $composableBuilder( + column: $table.cruiseSpeed, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maxSpeed => $composableBuilder( + column: $table.maxSpeed, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get minTurnRadius => $composableBuilder( + column: $table.minTurnRadius, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get loiterRadius => $composableBuilder( + column: $table.loiterRadius, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get catchRadius => $composableBuilder( + column: $table.catchRadius, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maxClimbRate => $composableBuilder( + column: $table.maxClimbRate, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maxDescentRate => $composableBuilder( + column: $table.maxDescentRate, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maxRangeM => $composableBuilder( + column: $table.maxRangeM, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maxEnduranceMin => $composableBuilder( + column: $table.maxEnduranceMin, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maxAltitudeM => $composableBuilder( + column: $table.maxAltitudeM, + builder: (column) => ColumnFilters(column), + ); +} + +class $$DroneProfilesTableOrderingComposer + extends Composer<_$AppDatabase, $DroneProfilesTable> { + $$DroneProfilesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get minSpeed => $composableBuilder( + column: $table.minSpeed, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get cruiseSpeed => $composableBuilder( + column: $table.cruiseSpeed, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maxSpeed => $composableBuilder( + column: $table.maxSpeed, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get minTurnRadius => $composableBuilder( + column: $table.minTurnRadius, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get loiterRadius => $composableBuilder( + column: $table.loiterRadius, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get catchRadius => $composableBuilder( + column: $table.catchRadius, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maxClimbRate => $composableBuilder( + column: $table.maxClimbRate, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maxDescentRate => $composableBuilder( + column: $table.maxDescentRate, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maxRangeM => $composableBuilder( + column: $table.maxRangeM, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maxEnduranceMin => $composableBuilder( + column: $table.maxEnduranceMin, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maxAltitudeM => $composableBuilder( + column: $table.maxAltitudeM, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$DroneProfilesTableAnnotationComposer + extends Composer<_$AppDatabase, $DroneProfilesTable> { + $$DroneProfilesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get minSpeed => + $composableBuilder(column: $table.minSpeed, builder: (column) => column); + + GeneratedColumn get cruiseSpeed => $composableBuilder( + column: $table.cruiseSpeed, + builder: (column) => column, + ); + + GeneratedColumn get maxSpeed => + $composableBuilder(column: $table.maxSpeed, builder: (column) => column); + + GeneratedColumn get minTurnRadius => $composableBuilder( + column: $table.minTurnRadius, + builder: (column) => column, + ); + + GeneratedColumn get loiterRadius => $composableBuilder( + column: $table.loiterRadius, + builder: (column) => column, + ); + + GeneratedColumn get catchRadius => $composableBuilder( + column: $table.catchRadius, + builder: (column) => column, + ); + + GeneratedColumn get maxClimbRate => $composableBuilder( + column: $table.maxClimbRate, + builder: (column) => column, + ); + + GeneratedColumn get maxDescentRate => $composableBuilder( + column: $table.maxDescentRate, + builder: (column) => column, + ); + + GeneratedColumn get maxRangeM => + $composableBuilder(column: $table.maxRangeM, builder: (column) => column); + + GeneratedColumn get maxEnduranceMin => $composableBuilder( + column: $table.maxEnduranceMin, + builder: (column) => column, + ); + + GeneratedColumn get maxAltitudeM => $composableBuilder( + column: $table.maxAltitudeM, + builder: (column) => column, + ); +} + +class $$DroneProfilesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $DroneProfilesTable, + DroneProfile, + $$DroneProfilesTableFilterComposer, + $$DroneProfilesTableOrderingComposer, + $$DroneProfilesTableAnnotationComposer, + $$DroneProfilesTableCreateCompanionBuilder, + $$DroneProfilesTableUpdateCompanionBuilder, + ( + DroneProfile, + BaseReferences<_$AppDatabase, $DroneProfilesTable, DroneProfile>, + ), + DroneProfile, + PrefetchHooks Function() + > { + $$DroneProfilesTableTableManager(_$AppDatabase db, $DroneProfilesTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$DroneProfilesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DroneProfilesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DroneProfilesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value minSpeed = const Value.absent(), + Value cruiseSpeed = const Value.absent(), + Value maxSpeed = const Value.absent(), + Value minTurnRadius = const Value.absent(), + Value loiterRadius = const Value.absent(), + Value catchRadius = const Value.absent(), + Value maxClimbRate = const Value.absent(), + Value maxDescentRate = const Value.absent(), + Value maxRangeM = const Value.absent(), + Value maxEnduranceMin = const Value.absent(), + Value maxAltitudeM = const Value.absent(), + }) => DroneProfilesCompanion( + id: id, + name: name, + minSpeed: minSpeed, + cruiseSpeed: cruiseSpeed, + maxSpeed: maxSpeed, + minTurnRadius: minTurnRadius, + loiterRadius: loiterRadius, + catchRadius: catchRadius, + maxClimbRate: maxClimbRate, + maxDescentRate: maxDescentRate, + maxRangeM: maxRangeM, + maxEnduranceMin: maxEnduranceMin, + maxAltitudeM: maxAltitudeM, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required String name, + required double minSpeed, + required double cruiseSpeed, + required double maxSpeed, + required double minTurnRadius, + required double loiterRadius, + required double catchRadius, + required double maxClimbRate, + required double maxDescentRate, + required double maxRangeM, + required double maxEnduranceMin, + required double maxAltitudeM, + }) => DroneProfilesCompanion.insert( + id: id, + name: name, + minSpeed: minSpeed, + cruiseSpeed: cruiseSpeed, + maxSpeed: maxSpeed, + minTurnRadius: minTurnRadius, + loiterRadius: loiterRadius, + catchRadius: catchRadius, + maxClimbRate: maxClimbRate, + maxDescentRate: maxDescentRate, + maxRangeM: maxRangeM, + maxEnduranceMin: maxEnduranceMin, + maxAltitudeM: maxAltitudeM, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$DroneProfilesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $DroneProfilesTable, + DroneProfile, + $$DroneProfilesTableFilterComposer, + $$DroneProfilesTableOrderingComposer, + $$DroneProfilesTableAnnotationComposer, + $$DroneProfilesTableCreateCompanionBuilder, + $$DroneProfilesTableUpdateCompanionBuilder, + ( + DroneProfile, + BaseReferences<_$AppDatabase, $DroneProfilesTable, DroneProfile>, + ), + DroneProfile, + PrefetchHooks Function() + >; +typedef $$AppSettingsTableTableCreateCompanionBuilder = + AppSettingsTableCompanion Function({ + Value id, + Value activeDroneProfileId, + Value currentMissionId, + }); +typedef $$AppSettingsTableTableUpdateCompanionBuilder = + AppSettingsTableCompanion Function({ + Value id, + Value activeDroneProfileId, + Value currentMissionId, + }); + +class $$AppSettingsTableTableFilterComposer + extends Composer<_$AppDatabase, $AppSettingsTableTable> { + $$AppSettingsTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get activeDroneProfileId => $composableBuilder( + column: $table.activeDroneProfileId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get currentMissionId => $composableBuilder( + column: $table.currentMissionId, + builder: (column) => ColumnFilters(column), + ); +} + +class $$AppSettingsTableTableOrderingComposer + extends Composer<_$AppDatabase, $AppSettingsTableTable> { + $$AppSettingsTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get activeDroneProfileId => $composableBuilder( + column: $table.activeDroneProfileId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get currentMissionId => $composableBuilder( + column: $table.currentMissionId, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$AppSettingsTableTableAnnotationComposer + extends Composer<_$AppDatabase, $AppSettingsTableTable> { + $$AppSettingsTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get activeDroneProfileId => $composableBuilder( + column: $table.activeDroneProfileId, + builder: (column) => column, + ); + + GeneratedColumn get currentMissionId => $composableBuilder( + column: $table.currentMissionId, + builder: (column) => column, + ); +} + +class $$AppSettingsTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $AppSettingsTableTable, + AppSettingsTableData, + $$AppSettingsTableTableFilterComposer, + $$AppSettingsTableTableOrderingComposer, + $$AppSettingsTableTableAnnotationComposer, + $$AppSettingsTableTableCreateCompanionBuilder, + $$AppSettingsTableTableUpdateCompanionBuilder, + ( + AppSettingsTableData, + BaseReferences< + _$AppDatabase, + $AppSettingsTableTable, + AppSettingsTableData + >, + ), + AppSettingsTableData, + PrefetchHooks Function() + > { + $$AppSettingsTableTableTableManager( + _$AppDatabase db, + $AppSettingsTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$AppSettingsTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$AppSettingsTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$AppSettingsTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value activeDroneProfileId = const Value.absent(), + Value currentMissionId = const Value.absent(), + }) => AppSettingsTableCompanion( + id: id, + activeDroneProfileId: activeDroneProfileId, + currentMissionId: currentMissionId, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + Value activeDroneProfileId = const Value.absent(), + Value currentMissionId = const Value.absent(), + }) => AppSettingsTableCompanion.insert( + id: id, + activeDroneProfileId: activeDroneProfileId, + currentMissionId: currentMissionId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$AppSettingsTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $AppSettingsTableTable, + AppSettingsTableData, + $$AppSettingsTableTableFilterComposer, + $$AppSettingsTableTableOrderingComposer, + $$AppSettingsTableTableAnnotationComposer, + $$AppSettingsTableTableCreateCompanionBuilder, + $$AppSettingsTableTableUpdateCompanionBuilder, + ( + AppSettingsTableData, + BaseReferences< + _$AppDatabase, + $AppSettingsTableTable, + AppSettingsTableData + >, + ), + AppSettingsTableData, + PrefetchHooks Function() + >; class $AppDatabaseManager { final _$AppDatabase _db; $AppDatabaseManager(this._db); $$MissionsTableTableManager get missions => $$MissionsTableTableManager(_db, _db.missions); + $$DroneProfilesTableTableManager get droneProfiles => + $$DroneProfilesTableTableManager(_db, _db.droneProfiles); + $$AppSettingsTableTableTableManager get appSettingsTable => + $$AppSettingsTableTableTableManager(_db, _db.appSettingsTable); } diff --git a/app/lib/services/database/app_settings_repository.dart b/app/lib/services/database/app_settings_repository.dart new file mode 100644 index 0000000..76414fb --- /dev/null +++ b/app/lib/services/database/app_settings_repository.dart @@ -0,0 +1,42 @@ +import 'package:drift/drift.dart'; + +import 'app_database.dart'; + +/// Zugriff auf die Einzeilen-Einstellungstabelle (Doku 7.5): welches +/// Drohnenprofil zuletzt aktiv war und welche Mission zuletzt bearbeitet +/// wurde, um beide nach einem Neustart wiederherzustellen. +class AppSettingsRepository { + AppSettingsRepository(this._db); + + final AppDatabase _db; + + static const _rowId = 0; + + Future<({int? activeDroneProfileId, int? currentMissionId})> load() async { + final row = await (_db.select(_db.appSettingsTable) + ..where((s) => s.id.equals(_rowId))) + .getSingleOrNull(); + return ( + activeDroneProfileId: row?.activeDroneProfileId, + currentMissionId: row?.currentMissionId, + ); + } + + Future setActiveDroneProfileId(int? id) async { + await _upsert(AppSettingsTableCompanion( + id: const Value(_rowId), + activeDroneProfileId: Value(id), + )); + } + + Future setCurrentMissionId(int? id) async { + await _upsert(AppSettingsTableCompanion( + id: const Value(_rowId), + currentMissionId: Value(id), + )); + } + + Future _upsert(AppSettingsTableCompanion companion) async { + await _db.into(_db.appSettingsTable).insertOnConflictUpdate(companion); + } +} diff --git a/app/lib/services/database/drone_profile_repository.dart b/app/lib/services/database/drone_profile_repository.dart new file mode 100644 index 0000000..1f97d94 --- /dev/null +++ b/app/lib/services/database/drone_profile_repository.dart @@ -0,0 +1,80 @@ +import 'package:drift/drift.dart'; + +import '../../domain/mission/drone_profile.dart' as domain; +import 'app_database.dart'; + +/// Persistenz-Zugriff fuer Drohnenprofile (Doku 3.5/7.3). +/// +/// `domain.DroneProfile` (das reine Datenmodell) und `DroneProfile` (die von +/// Drift aus der `DroneProfiles`-Tabelle generierte Zeilenklasse) heissen +/// gleich - hier daher qualifiziert importiert, um beide zu unterscheiden. +class DroneProfileRepository { + DroneProfileRepository(this._db); + + final AppDatabase _db; + + Stream> watchAll() { + return _db.select(_db.droneProfiles).watch().map( + (rows) => [for (final row in rows) _fromRow(row)], + ); + } + + Future> loadAll() async { + final rows = await _db.select(_db.droneProfiles).get(); + return [for (final row in rows) _fromRow(row)]; + } + + /// Legt beim allerersten Start ein T1-Ranger-Standardprofil an, falls + /// noch keines existiert (analog `if (!drones) drones=[DEFAULT_DRONE]` + /// im HTML-Demonstrator). Gibt dessen id zurueck. + Future ensureSeeded() async { + final existing = await loadAll(); + if (existing.isNotEmpty) return existing.first.id!; + return insert(domain.DroneProfile.t1Ranger()); + } + + Future insert(domain.DroneProfile profile) { + return _db.into(_db.droneProfiles).insert(_toCompanion(profile)); + } + + Future update(domain.DroneProfile profile) async { + await (_db.update(_db.droneProfiles)..where((d) => d.id.equals(profile.id!))) + .write(_toCompanion(profile)); + } + + Future delete(int id) async { + await (_db.delete(_db.droneProfiles)..where((d) => d.id.equals(id))).go(); + } + + domain.DroneProfile _fromRow(DroneProfile row) => domain.DroneProfile( + id: row.id, + name: row.name, + minSpeed: row.minSpeed, + cruiseSpeed: row.cruiseSpeed, + maxSpeed: row.maxSpeed, + minTurnRadius: row.minTurnRadius, + loiterRadius: row.loiterRadius, + catchRadius: row.catchRadius, + maxClimbRate: row.maxClimbRate, + maxDescentRate: row.maxDescentRate, + maxRangeM: row.maxRangeM, + maxEnduranceMin: row.maxEnduranceMin, + maxAltitudeM: row.maxAltitudeM, + ); + + DroneProfilesCompanion _toCompanion(domain.DroneProfile profile) => + DroneProfilesCompanion( + name: Value(profile.name), + minSpeed: Value(profile.minSpeed), + cruiseSpeed: Value(profile.cruiseSpeed), + maxSpeed: Value(profile.maxSpeed), + minTurnRadius: Value(profile.minTurnRadius), + loiterRadius: Value(profile.loiterRadius), + catchRadius: Value(profile.catchRadius), + maxClimbRate: Value(profile.maxClimbRate), + maxDescentRate: Value(profile.maxDescentRate), + maxRangeM: Value(profile.maxRangeM), + maxEnduranceMin: Value(profile.maxEnduranceMin), + maxAltitudeM: Value(profile.maxAltitudeM), + ); +} diff --git a/app/lib/services/database/mission_repository.dart b/app/lib/services/database/mission_repository.dart new file mode 100644 index 0000000..65c557b --- /dev/null +++ b/app/lib/services/database/mission_repository.dart @@ -0,0 +1,149 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; + +import '../../domain/waypoint/flat_waypoint_list.dart'; +import 'app_database.dart'; + +/// Kopfzeilen-taugliche Zusammenfassung einer gespeicherten Mission, ohne +/// die komplette Wegpunktliste zu materialisieren (fuer die Missionsliste +/// im Verwaltungsmenue, Doku 3.6/7.1). +class MissionSummary { + const MissionSummary({ + required this.id, + required this.name, + required this.placeName, + required this.waypointCount, + required this.updatedAt, + }); + + final int id; + final String name; + final String? placeName; + final int waypointCount; + final DateTime updatedAt; +} + +class LoadedMission { + const LoadedMission({ + required this.name, + required this.placeName, + required this.waypoints, + }); + + final String name; + final String? placeName; + final List waypoints; +} + +/// Persistenz-Zugriff fuer Missionen (Doku 4.15): kapselt die Drift-Tabelle +/// und die JSON-(De-)Serialisierung der Wegpunktliste - die Domain-Schicht +/// (Waypoint) kennt bewusst kein JSON, siehe flat_waypoint_list.dart. +class MissionRepository { + MissionRepository(this._db); + + final AppDatabase _db; + + Stream> watchSummaries() { + return _db.select(_db.missions).watch().map((rows) { + final summaries = [ + for (final row in rows) + MissionSummary( + id: row.id, + name: row.name, + placeName: row.location, + waypointCount: + (jsonDecode(row.waypointsJson) as List).length, + updatedAt: row.updatedAt, + ), + ]; + return summaries; + }); + } + + Future load(int id) async { + final row = await (_db.select(_db.missions) + ..where((m) => m.id.equals(id))) + .getSingleOrNull(); + if (row == null) return null; + final decoded = jsonDecode(row.waypointsJson) as List; + return LoadedMission( + name: row.name, + placeName: row.location, + waypoints: [ + for (final entry in decoded) _waypointFromJson(entry as Map), + ], + ); + } + + /// Legt eine neue Mission an oder ueberschreibt die bestehende mit [id] - + /// analog saveCurrentMission()/Mission.toRecord() im HTML-Demonstrator + /// (find-or-insert nach id). Gibt die (ggf. neu vergebene) id zurueck. + Future upsert({ + int? id, + required String name, + String? placeName, + required List waypoints, + }) async { + final waypointsJson = jsonEncode([for (final w in waypoints) _waypointToJson(w)]); + final companion = MissionsCompanion( + name: Value(name), + location: Value(placeName), + updatedAt: Value(DateTime.now()), + waypointsJson: Value(waypointsJson), + ); + if (id == null) { + return _db.into(_db.missions).insert(companion); + } + final existing = await (_db.select(_db.missions) + ..where((m) => m.id.equals(id))) + .getSingleOrNull(); + if (existing == null) { + return _db.into(_db.missions).insert(companion.copyWith(id: Value(id))); + } + await (_db.update(_db.missions)..where((m) => m.id.equals(id))) + .write(companion); + return id; + } + + Future rename(int id, String name) async { + await (_db.update(_db.missions)..where((m) => m.id.equals(id))).write( + MissionsCompanion(name: Value(name), updatedAt: Value(DateTime.now())), + ); + } + + Future delete(int id) async { + await (_db.delete(_db.missions)..where((m) => m.id.equals(id))).go(); + } + + Map _waypointToJson(Waypoint w) => { + 'lat': w.lat, + 'lon': w.lon, + 'altitudeM': w.altitudeM, + 'speedMs': w.speedMs, + 'catchRadiusM': w.catchRadiusM, + 'action': w.action.name, + 'jumpTargetIndex': w.jumpTargetIndex, + 'jumpRepeatCount': w.jumpRepeatCount, + 'windSpeedMs': w.windSpeedMs, + 'windDirFromDeg': w.windDirFromDeg, + 'windElevationM': w.windElevationM, + }; + + Waypoint _waypointFromJson(Map json) => Waypoint( + lat: (json['lat'] as num).toDouble(), + lon: (json['lon'] as num).toDouble(), + altitudeM: (json['altitudeM'] as num).toDouble(), + speedMs: (json['speedMs'] as num).toDouble(), + catchRadiusM: (json['catchRadiusM'] as num).toDouble(), + action: WaypointAction.values.firstWhere( + (a) => a.name == json['action'], + orElse: () => WaypointAction.none, + ), + jumpTargetIndex: json['jumpTargetIndex'] as int?, + jumpRepeatCount: json['jumpRepeatCount'] as int?, + windSpeedMs: (json['windSpeedMs'] as num?)?.toDouble(), + windDirFromDeg: (json['windDirFromDeg'] as num?)?.toDouble(), + windElevationM: (json['windElevationM'] as num?)?.toDouble(), + ); +} diff --git a/app/lib/ui/providers/active_drone_profile_provider.dart b/app/lib/ui/providers/active_drone_profile_provider.dart new file mode 100644 index 0000000..aaf30a3 --- /dev/null +++ b/app/lib/ui/providers/active_drone_profile_provider.dart @@ -0,0 +1,62 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../domain/mission/drone_profile.dart'; +import 'database_provider.dart'; + +/// Haelt das aktuell aktive Drohnenprofil (Doku 3.5/7.3) - bestimmt u.a. den +/// gueltigen Geschwindigkeits-/Hoehenbereich der Drehraeder sowie die +/// Flugpfad-Machbarkeitspruefung (Kurvenradius, Steig-/Sinkrate). +/// +/// T1-Ranger-Konstanten dienen nur als Startwert, bevor initialize() die +/// Datenbank gelesen hat (verhindert einen Zustand ganz ohne Profil waehrend +/// des asynchronen Ladens). +class ActiveDroneProfileNotifier extends Notifier { + @override + DroneProfile build() => DroneProfile.t1Ranger(); + + /// Stellt sicher, dass mindestens ein Profil in der Datenbank existiert, + /// und laedt das zuletzt aktive (oder das erste vorhandene) Profil - + /// analog dem drones/activeDrone-Bootstrap im HTML-Demonstrator. + Future initialize() async { + final droneRepo = ref.read(droneProfileRepositoryProvider); + final settingsRepo = ref.read(appSettingsRepositoryProvider); + + final seededId = await droneRepo.ensureSeeded(); + final settings = await settingsRepo.load(); + final all = await droneRepo.loadAll(); + + final activeId = settings.activeDroneProfileId ?? seededId; + final active = all.firstWhere( + (p) => p.id == activeId, + orElse: () => all.first, + ); + state = active; + if (settings.activeDroneProfileId != active.id) { + await settingsRepo.setActiveDroneProfileId(active.id); + } + } + + Future select(DroneProfile profile) async { + state = profile; + await ref.read(appSettingsRepositoryProvider).setActiveDroneProfileId(profile.id); + } + + /// Nach dem Bearbeiten des aktiven Profils dessen Werte neu einlesen. + Future reloadIfActive(int id) async { + if (state.id != id) return; + final all = await ref.read(droneProfileRepositoryProvider).loadAll(); + final updated = all.where((p) => p.id == id); + if (updated.isNotEmpty) state = updated.first; + } + + /// Nach dem Loeschen des aktiven Profils auf das erste verbleibende + /// zurueckfallen (analog `if (wasActive) applyDroneProfile(drones[0])`). + Future fallBackTo(DroneProfile profile) async { + state = profile; + await ref.read(appSettingsRepositoryProvider).setActiveDroneProfileId(profile.id); + } +} + +final activeDroneProfileProvider = + NotifierProvider( + ActiveDroneProfileNotifier.new); diff --git a/app/lib/ui/providers/database_provider.dart b/app/lib/ui/providers/database_provider.dart new file mode 100644 index 0000000..dc229ea --- /dev/null +++ b/app/lib/ui/providers/database_provider.dart @@ -0,0 +1,36 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../domain/mission/drone_profile.dart'; +import '../../services/database/app_database.dart' hide DroneProfile; +import '../../services/database/app_settings_repository.dart'; +import '../../services/database/drone_profile_repository.dart'; +import '../../services/database/mission_repository.dart'; + +/// App-lebenslange Drift-Datenbankinstanz (Doku 4.15). +final appDatabaseProvider = Provider((ref) { + final db = AppDatabase(); + ref.onDispose(db.close); + return db; +}); + +final missionRepositoryProvider = Provider( + (ref) => MissionRepository(ref.watch(appDatabaseProvider)), +); + +final droneProfileRepositoryProvider = Provider( + (ref) => DroneProfileRepository(ref.watch(appDatabaseProvider)), +); + +final appSettingsRepositoryProvider = Provider( + (ref) => AppSettingsRepository(ref.watch(appDatabaseProvider)), +); + +/// Reaktive Missionsliste fuers Verwaltungsmenue (Doku 3.6/7.1). +final missionSummariesProvider = StreamProvider( + (ref) => ref.watch(missionRepositoryProvider).watchSummaries(), +); + +/// Reaktive Drohnenprofil-Liste fuers Verwaltungsmenue (Doku 3.5/7.3). +final droneProfilesProvider = StreamProvider>( + (ref) => ref.watch(droneProfileRepositoryProvider).watchAll(), +); diff --git a/app/lib/ui/providers/mission_meta_provider.dart b/app/lib/ui/providers/mission_meta_provider.dart new file mode 100644 index 0000000..c83b286 --- /dev/null +++ b/app/lib/ui/providers/mission_meta_provider.dart @@ -0,0 +1,122 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'current_mission_provider.dart'; +import 'database_provider.dart'; + +/// Metadaten der aktuell in Bearbeitung befindlichen Mission - ergaenzt +/// currentMissionProvider (das nur die Wegpunkte haelt) um Name/Ort und die +/// Datenbank-id, sobald die Mission mindestens einmal gespeichert wurde. +class MissionMeta { + const MissionMeta({this.id, this.name = 'New mission', this.placeName}); + + /// Null, solange die Mission noch nie (auto-)gespeichert wurde. + final int? id; + final String name; + final String? placeName; + + MissionMeta copyWith({int? id, String? name, String? placeName}) { + return MissionMeta( + id: id ?? this.id, + name: name ?? this.name, + placeName: placeName ?? this.placeName, + ); + } +} + +/// Haelt Name/id der aktuellen Mission und autosaved sie ueber +/// MissionRepository (Doku 7.5), sobald sich die Wegpunktliste aendert - +/// analog scheduleAutosave()/flushPendingAutosave() im HTML-Demonstrator +/// (dort 800ms debounce, hier identisch uebernommen). +class CurrentMissionMetaNotifier extends Notifier { + Timer? _autosaveTimer; + bool _suppressAutosave = false; + + @override + MissionMeta build() { + ref.listen(currentMissionProvider, (previous, next) { + if (!_suppressAutosave) _scheduleAutosave(); + }); + ref.onDispose(() => _autosaveTimer?.cancel()); + return const MissionMeta(); + } + + /// Laedt die zuletzt bearbeitete Mission (falls vorhanden) beim App-Start + /// wieder, damit ein Autosave-Draft einen Neustart ueberlebt (Doku 7.5). + Future restoreLastSession() async { + final settings = await ref.read(appSettingsRepositoryProvider).load(); + final missionId = settings.currentMissionId; + if (missionId == null) return; + final loaded = await ref.read(missionRepositoryProvider).load(missionId); + if (loaded == null) return; + + _suppressAutosave = true; + ref.read(currentMissionProvider.notifier).replaceAll(loaded.waypoints); + _suppressAutosave = false; + state = MissionMeta(id: missionId, name: loaded.name, placeName: loaded.placeName); + } + + /// Analog startNewMission() im HTML-Demonstrator: sichert zuerst noch + /// ausstehende Aenderungen der bisherigen Mission, dann leerer Neustart. + Future startNew() async { + await _flushNow(); + _suppressAutosave = true; + ref.read(currentMissionProvider.notifier).replaceAll(const []); + _suppressAutosave = false; + state = const MissionMeta(); + await ref.read(appSettingsRepositoryProvider).setCurrentMissionId(null); + } + + Future loadMission(int id) async { + await _flushNow(); + final loaded = await ref.read(missionRepositoryProvider).load(id); + if (loaded == null) return; + + _suppressAutosave = true; + ref.read(currentMissionProvider.notifier).replaceAll(loaded.waypoints); + _suppressAutosave = false; + state = MissionMeta(id: id, name: loaded.name, placeName: loaded.placeName); + await ref.read(appSettingsRepositoryProvider).setCurrentMissionId(id); + } + + /// Benennt eine gespeicherte Mission um (Doku 3.6: nur ueber die + /// Missionsliste moeglich, dort hat jede Zeile immer bereits eine id). + /// Aktualisiert zusaetzlich den eigenen Zustand, falls [id] die gerade + /// aktive Mission ist. + Future renameMission(int id, String name) async { + await ref.read(missionRepositoryProvider).rename(id, name); + if (state.id == id) { + state = state.copyWith(name: name); + } + } + + void _scheduleAutosave() { + if (ref.read(currentMissionProvider).isEmpty) return; + _autosaveTimer?.cancel(); + _autosaveTimer = Timer(const Duration(milliseconds: 800), () { + unawaited(_flushNow()); + }); + } + + Future _flushNow() async { + _autosaveTimer?.cancel(); + _autosaveTimer = null; + final waypoints = ref.read(currentMissionProvider); + if (waypoints.isEmpty) return; + final id = await ref.read(missionRepositoryProvider).upsert( + id: state.id, + name: state.name, + placeName: state.placeName, + waypoints: waypoints, + ); + if (state.id == null) { + state = state.copyWith(id: id); + } + await ref.read(appSettingsRepositoryProvider).setCurrentMissionId(id); + } +} + +final currentMissionMetaProvider = + NotifierProvider( + CurrentMissionMetaNotifier.new); diff --git a/app/lib/ui/screens/plan/plan_screen.dart b/app/lib/ui/screens/plan/plan_screen.dart index bd76519..b071a3b 100644 --- a/app/lib/ui/screens/plan/plan_screen.dart +++ b/app/lib/ui/screens/plan/plan_screen.dart @@ -9,8 +9,10 @@ import 'package:latlong2/latlong.dart'; import '../../../domain/mission/default_drone_profile.dart'; import '../../../domain/mission/route_geometry.dart'; import '../../../domain/waypoint/flat_waypoint_list.dart'; +import '../../providers/active_drone_profile_provider.dart'; import '../../providers/current_mission_provider.dart'; import '../../providers/map_controller_provider.dart'; +import '../../providers/mission_meta_provider.dart'; import '../../providers/wind_provider.dart'; import '../../widgets/bottom_stats_bar.dart'; import '../../widgets/halo_menu.dart'; @@ -72,8 +74,28 @@ class _PlanScreenState extends ConsumerState { // Erstbefuellung der Kopfleisten-Wind-Pille (Doku 3.8) - erst nach dem // ersten Frame, da die Kartenkamera (Fallback-Referenzort ohne // Wegpunkte) vor dem initialen Layout noch nicht bereit ist. - WidgetsBinding.instance - .addPostFrameCallback((_) => ref.read(headerWindProvider.notifier).refresh()); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + ref.read(headerWindProvider.notifier).refresh(); + // Bootstrap der Persistenz (Doku 4.15/7.5): mindestens ein + // Drohnenprofil sicherstellen/laden, danach ggf. den zuletzt + // bearbeiteten Autosave-Draft wiederherstellen. + await ref.read(activeDroneProfileProvider.notifier).initialize(); + await ref.read(currentMissionMetaProvider.notifier).restoreLastSession(); + if (mounted) { + setState(() { + _curSpeed = ref.read(activeDroneProfileProvider).cruiseSpeed; + }); + } + }); + // Wechselt der Nutzer waehrend der Bearbeitung das aktive Drohnenprofil, + // muss die aktuell eingestellte Soll-Geschwindigkeit in dessen + // gueltigen Bereich geklemmt werden (sonst zeigt das Drehrad einen + // Wert ausserhalb seiner eigenen Skala). + ref.listenManual(activeDroneProfileProvider, (previous, next) { + final clamped = _curSpeed.clamp(next.minSpeed, next.maxSpeed); + if (clamped != _curSpeed) setState(() => _curSpeed = clamped); + }); } @override @@ -167,7 +189,7 @@ class _PlanScreenState extends ConsumerState { lon: center.longitude, altitudeM: _curAlt, speedMs: _curSpeed, - catchRadiusM: DefaultDroneProfile.catchRadiusM, + catchRadiusM: ref.read(activeDroneProfileProvider).catchRadius, ), ); if (waypoints.isEmpty) { @@ -325,12 +347,13 @@ class _PlanScreenState extends ConsumerState { Widget build(BuildContext context) { final waypoints = ref.watch(currentMissionProvider); final showPerWaypointWind = ref.watch(headerWindProvider).showPerWaypoint; + final droneProfile = ref.watch(activeDroneProfileProvider); final routeGeometry = buildRouteGeometry( waypoints, - minTurnRadius: DefaultDroneProfile.minTurnRadius, - maxClimbRate: DefaultDroneProfile.maxClimbRate, - maxDescentRate: DefaultDroneProfile.maxDescentRate, + minTurnRadius: droneProfile.minTurnRadius, + maxClimbRate: droneProfile.maxClimbRate, + maxDescentRate: droneProfile.maxDescentRate, ); final missionStats = _missionStats(waypoints); final anyBad = routeGeometry.vertexBad.any((bad) => bad) || @@ -414,7 +437,7 @@ class _PlanScreenState extends ConsumerState { child: ValueWheel( value: _curAlt, min: DefaultDroneProfile.altMin, - max: DefaultDroneProfile.altMax, + max: droneProfile.maxAltitudeM, step: DefaultDroneProfile.altStep, pxPerStep: _altPxPerStep, majorEvery: _altMajorEvery, @@ -433,8 +456,8 @@ class _PlanScreenState extends ConsumerState { right: 14 + safeEdgeInset, child: ValueWheel( value: _curSpeed, - min: DefaultDroneProfile.speedMin, - max: DefaultDroneProfile.speedMax, + min: droneProfile.minSpeed, + max: droneProfile.maxSpeed, step: DefaultDroneProfile.speedStep, pxPerStep: _speedPxPerStep, majorEvery: _speedMajorEvery, diff --git a/app/lib/ui/widgets/bottom_stats_bar.dart b/app/lib/ui/widgets/bottom_stats_bar.dart index c18ecdb..049c7ca 100644 --- a/app/lib/ui/widgets/bottom_stats_bar.dart +++ b/app/lib/ui/widgets/bottom_stats_bar.dart @@ -1,11 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../providers/active_drone_profile_provider.dart'; +import '../providers/mission_meta_provider.dart'; +import 'missions_drones_panel.dart'; /// Statuszeile am unteren Bildschirmrand (HTML-Demonstrator: #bottomBar/ -/// #statsRow). Antippen oeffnet die Wegpunktliste. Mission-/Drohnen-Name -/// und der Send-Button sind bewusst nicht Teil dieses Schritts - die -/// dafuer noetige Missionsverwaltung bzw. Upload-Anbindung existiert noch -/// nicht. -class BottomStatsBar extends StatelessWidget { +/// #statsRow). Details-Pille (mittig) oeffnet die Wegpunktliste; die +/// Mission-/Drohnen-Chips (links) oeffnen das Verwaltungsmenue mit dem +/// passenden Starttab (HTML-Demonstrator: #missionNameBtn/#droneNameBtn). +/// Der Send-Button ist bewusst nicht Teil dieses Schritts - die dafuer +/// noetige Upload-Anbindung existiert noch nicht. +class BottomStatsBar extends ConsumerWidget { const BottomStatsBar({ super.key, required this.waypointCount, @@ -20,50 +26,97 @@ class BottomStatsBar extends StatelessWidget { final VoidCallback onTap; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final missionName = ref.watch(currentMissionMetaProvider).name; + final droneName = ref.watch(activeDroneProfileProvider).name; + + // Row statt Stack: ein Align/Center ohne width-/heightFactor nimmt sich + // immer den gesamten verfuegbaren Platz seines Elternwidgets (auch wenn + // sein Kind viel kleiner ist) - in einem Stack ohne begrenzende + // Positioned-Kinder blaeht das dessen eigene Groesse unkontrolliert auf + // und verschiebt die Details-Pille aus ihrer sichtbaren Position. + // Zwei gleich grosse Expanded-Bereiche links/rechts der Pille halten sie + // dagegen zuverlaessig zentriert. return SafeArea( top: false, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Center( - child: GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 9), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Details:', - style: TextStyle( - color: Colors.white70, - fontWeight: FontWeight.w600, - fontSize: 14, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + child: Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Flexible( + child: _NameChip( + label: 'Mission: $missionName', + color: Colors.white.withValues(alpha: 0.12), + textColor: Colors.white, + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => const MissionsDronesPanel.missions(), + )), + ), ), - ), - const SizedBox(width: 10), - const Icon(Icons.flag, size: 14, color: Colors.white), - const SizedBox(width: 4), - Text('$waypointCount WP', - style: const TextStyle(color: Colors.white, fontSize: 14)), - const SizedBox(width: 14), - const Icon(Icons.straighten, size: 14, color: Colors.white), - const SizedBox(width: 4), - Text('${totalDistanceM.round()} m', - style: const TextStyle(color: Colors.white, fontSize: 14)), - const SizedBox(width: 14), - const Icon(Icons.access_time, size: 14, color: Colors.white), - const SizedBox(width: 4), - Text(_fmtDuration(totalDurationSec), - style: const TextStyle(color: Colors.white, fontSize: 14)), - ], + const SizedBox(width: 6), + Flexible( + child: _NameChip( + label: 'Drone: $droneName', + color: const Color(0xFF4D9DFF).withValues(alpha: 0.18), + textColor: const Color(0xFFCFE2FF), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => const MissionsDronesPanel.drones(), + )), + ), + ), + ], + ), ), ), - ), + _detailsPill(), + const Expanded(child: SizedBox.shrink()), + ], + ), + ), + ); + } + + Widget _detailsPill() { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 9), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Details:', + style: TextStyle( + color: Colors.white70, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(width: 10), + const Icon(Icons.flag, size: 14, color: Colors.white), + const SizedBox(width: 4), + Text('$waypointCount WP', + style: const TextStyle(color: Colors.white, fontSize: 14)), + const SizedBox(width: 14), + const Icon(Icons.straighten, size: 14, color: Colors.white), + const SizedBox(width: 4), + Text('${totalDistanceM.round()} m', + style: const TextStyle(color: Colors.white, fontSize: 14)), + const SizedBox(width: 14), + const Icon(Icons.access_time, size: 14, color: Colors.white), + const SizedBox(width: 4), + Text(_fmtDuration(totalDurationSec), + style: const TextStyle(color: Colors.white, fontSize: 14)), + ], ), ), ); @@ -76,3 +129,44 @@ class BottomStatsBar extends StatelessWidget { return '$minutes:${secs.toString().padLeft(2, '0')}'; } } + +class _NameChip extends StatelessWidget { + const _NameChip({ + required this.label, + required this.color, + required this.textColor, + required this.onTap, + }); + + final String label; + final Color color; + final Color textColor; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(14), + ), + child: Text( + label, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/ui/widgets/drone_profile_editor.dart b/app/lib/ui/widgets/drone_profile_editor.dart new file mode 100644 index 0000000..ea49a0f --- /dev/null +++ b/app/lib/ui/widgets/drone_profile_editor.dart @@ -0,0 +1,218 @@ +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'; + +/// Formular zum Anlegen/Bearbeiten eines Drohnenprofils (Doku 3.5/7.3). +/// Vereinfacht gegenueber dem HTML-Demonstrator: normale Zahlenfelder statt +/// Swipe-Gesten-Chips (SwipeValueInput) - bei elf Feldern in Gruppen ist ein +/// Formular auf einem Mobilgeraet die zugaenglichere Eingabemethode. +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 _fieldControllers; + + static const _groups = [ + ( + title: 'Speed (m/s)', + fields: [ + ('minSpeed', 'Min (stall)'), + ('cruiseSpeed', 'Cruise'), + ('maxSpeed', 'Max'), + ], + ), + ( + title: 'Radii (m)', + fields: [ + ('minTurnRadius', 'Min turn'), + ('loiterRadius', 'Loiter'), + ('catchRadius', 'Waypoint catch'), + ], + ), + ( + title: 'Climb / Descent (m/s)', + fields: [ + ('maxClimbRate', 'Max climb'), + ('maxDescentRate', 'Max descent'), + ], + ), + ( + title: 'Limits', + fields: [ + ('maxRangeM', 'Range (m)'), + ('maxEnduranceMin', 'Endurance (min)'), + ('maxAltitudeM', 'Ceiling (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', + ); + _fieldControllers = { + for (final group in _groups) + for (final (key, _) in group.fields) + key: TextEditingController(text: _fieldValue(base, key).toString()), + }; + } + + @override + void dispose() { + _nameController.dispose(); + for (final c in _fieldControllers.values) { + c.dispose(); + } + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + double v(String key) => double.parse(_fieldControllers[key]!.text); + + 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( + 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( + children: [ + for (final (key, label) in group.fields) + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: TextFormField( + controller: _fieldControllers[key], + style: const TextStyle(color: Colors.white), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + decoration: InputDecoration(labelText: label), + validator: (v) => + double.tryParse(v ?? '') == null ? 'Number' : null, + ), + ), + ), + ], + ), + ], + const SizedBox(height: 24), + FilledButton( + onPressed: _save, + child: const Text('Save'), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/ui/widgets/missions_drones_panel.dart b/app/lib/ui/widgets/missions_drones_panel.dart new file mode 100644 index 0000000..c68612a --- /dev/null +++ b/app/lib/ui/widgets/missions_drones_panel.dart @@ -0,0 +1,373 @@ +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 '../providers/active_drone_profile_provider.dart'; +import '../providers/database_provider.dart'; +import '../providers/mission_meta_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: Align( + alignment: Alignment.centerLeft, + child: _ToolbarButton( + icon: Icons.add, + label: 'New mission', + onTap: () async { + await ref.read(currentMissionMetaProvider.notifier).startNew(); + if (mounted) Navigator.of(context).pop(); + }, + ), + ), + ), + 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: () => ref.read(missionRepositoryProvider).delete(m.id), + icon: const Icon(Icons.delete_outline, + color: Color(0xFFE2574A), size: 18), + ), + ], + ), + ), + ); + } + + 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: Align( + alignment: Alignment.centerLeft, + child: _ToolbarButton( + icon: Icons.add, + label: 'New profile', + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => const DroneProfileEditor(), + )), + ), + ), + ), + 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: () => _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); + } + } +} + +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)), + ], + ), + ), + ); + } +} diff --git a/app/lib/ui/widgets/waypoint_list_panel.dart b/app/lib/ui/widgets/waypoint_list_panel.dart index d6f3bb1..0c8b463 100644 --- a/app/lib/ui/widgets/waypoint_list_panel.dart +++ b/app/lib/ui/widgets/waypoint_list_panel.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/mission/default_drone_profile.dart'; +import '../../domain/mission/drone_profile.dart'; import '../../domain/mission/route_geometry.dart'; import '../../domain/waypoint/flat_waypoint_list.dart'; import '../../domain/wind/wind_math.dart'; +import '../providers/active_drone_profile_provider.dart'; import '../providers/current_mission_provider.dart'; import '../providers/wind_provider.dart'; import 'full_value_chart.dart'; @@ -134,11 +136,12 @@ class _WaypointListPanelState extends ConsumerState { @override Widget build(BuildContext context) { final waypoints = ref.watch(currentMissionProvider); + final droneProfile = ref.watch(activeDroneProfileProvider); final routeGeometry = buildRouteGeometry( waypoints, - minTurnRadius: DefaultDroneProfile.minTurnRadius, - maxClimbRate: DefaultDroneProfile.maxClimbRate, - maxDescentRate: DefaultDroneProfile.maxDescentRate, + minTurnRadius: droneProfile.minTurnRadius, + maxClimbRate: droneProfile.maxClimbRate, + maxDescentRate: droneProfile.maxDescentRate, ); return Material( @@ -190,13 +193,13 @@ class _WaypointListPanelState extends ConsumerState { const Divider(color: Colors.white24, height: 1), Expanded( child: switch (_tab) { - _PanelTab.list => _listTab(waypoints, routeGeometry), + _PanelTab.list => _listTab(waypoints, routeGeometry, droneProfile), _PanelTab.altitude => _chartTab( waypoints, routeGeometry, values: [for (final w in waypoints) w.altitudeM], min: DefaultDroneProfile.altMin, - max: DefaultDroneProfile.altMax, + max: droneProfile.maxAltitudeM, step: DefaultDroneProfile.altStep, pxPerUnit: 5, unit: ' m', @@ -206,8 +209,8 @@ class _WaypointListPanelState extends ConsumerState { waypoints, routeGeometry, values: [for (final w in waypoints) w.speedMs], - min: DefaultDroneProfile.speedMin, - max: DefaultDroneProfile.speedMax, + min: droneProfile.minSpeed, + max: droneProfile.maxSpeed, step: DefaultDroneProfile.speedStep, pxPerUnit: 22, unit: ' m/s', @@ -290,7 +293,11 @@ class _WaypointListPanelState extends ConsumerState { ); } - Widget _listTab(List waypoints, RouteGeometry routeGeometry) { + Widget _listTab( + List waypoints, + RouteGeometry routeGeometry, + DroneProfile droneProfile, + ) { return Column( children: [ if (waypoints.isNotEmpty) @@ -322,14 +329,19 @@ class _WaypointListPanelState extends ConsumerState { itemExtent: _rowExtent, itemCount: waypoints.length, itemBuilder: (context, index) => - _row(waypoints[index], index, routeGeometry), + _row(waypoints[index], index, routeGeometry, droneProfile), ), ), ], ); } - Widget _row(Waypoint wp, int index, RouteGeometry routeGeometry) { + Widget _row( + Waypoint wp, + int index, + RouteGeometry routeGeometry, + DroneProfile droneProfile, + ) { final isEditing = _rowEditIndex == index; final isActive = widget.activeIndex == index; final isBad = index < routeGeometry.vertexBad.length && @@ -371,7 +383,7 @@ class _WaypointListPanelState extends ConsumerState { value: wp.altitudeM, unit: ' m', min: DefaultDroneProfile.altMin, - max: DefaultDroneProfile.altMax, + max: droneProfile.maxAltitudeM, step: DefaultDroneProfile.altStep, onChanged: (v) => widget.onAltitudeChanged(index, v), ) @@ -383,8 +395,8 @@ class _WaypointListPanelState extends ConsumerState { ? WaypointChip( value: wp.speedMs, unit: ' m/s', - min: DefaultDroneProfile.speedMin, - max: DefaultDroneProfile.speedMax, + min: droneProfile.minSpeed, + max: droneProfile.maxSpeed, step: DefaultDroneProfile.speedStep, onChanged: (v) => widget.onSpeedChanged(index, v), ) diff --git a/app/test/services/app_settings_repository_test.dart b/app/test/services/app_settings_repository_test.dart new file mode 100644 index 0000000..3d389da --- /dev/null +++ b/app/test/services/app_settings_repository_test.dart @@ -0,0 +1,52 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:dmc_app/services/database/app_database.dart'; +import 'package:dmc_app/services/database/app_settings_repository.dart'; + +void main() { + late AppDatabase db; + late AppSettingsRepository repository; + + setUp(() { + db = AppDatabase.forTesting(NativeDatabase.memory()); + repository = AppSettingsRepository(db); + }); + + tearDown(() => db.close()); + + test('load ohne vorherige Werte liefert beides null', () async { + final settings = await repository.load(); + expect(settings.activeDroneProfileId, isNull); + expect(settings.currentMissionId, isNull); + }); + + test('setActiveDroneProfileId und setCurrentMissionId sind unabhaengig voneinander', + () async { + await repository.setActiveDroneProfileId(3); + await repository.setCurrentMissionId(7); + + final settings = await repository.load(); + expect(settings.activeDroneProfileId, 3); + expect(settings.currentMissionId, 7); + }); + + test('erneutes Setzen eines Werts laesst den anderen unangetastet', () async { + await repository.setActiveDroneProfileId(1); + await repository.setCurrentMissionId(2); + + await repository.setCurrentMissionId(9); + + final settings = await repository.load(); + expect(settings.activeDroneProfileId, 1); + expect(settings.currentMissionId, 9); + }); + + test('Wert kann auf null zurueckgesetzt werden', () async { + await repository.setCurrentMissionId(5); + await repository.setCurrentMissionId(null); + + final settings = await repository.load(); + expect(settings.currentMissionId, isNull); + }); +} diff --git a/app/test/services/drone_profile_repository_test.dart b/app/test/services/drone_profile_repository_test.dart new file mode 100644 index 0000000..dc8c9e4 --- /dev/null +++ b/app/test/services/drone_profile_repository_test.dart @@ -0,0 +1,67 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:dmc_app/domain/mission/drone_profile.dart'; +import 'package:dmc_app/services/database/app_database.dart' hide DroneProfile; +import 'package:dmc_app/services/database/drone_profile_repository.dart'; + +void main() { + late AppDatabase db; + late DroneProfileRepository repository; + + setUp(() { + db = AppDatabase.forTesting(NativeDatabase.memory()); + repository = DroneProfileRepository(db); + }); + + tearDown(() => db.close()); + + test('ensureSeeded legt beim ersten Aufruf T1 Ranger an', () async { + final id = await repository.ensureSeeded(); + + final all = await repository.loadAll(); + expect(all, hasLength(1)); + expect(all.single.id, id); + expect(all.single.name, 'T1 Ranger'); + expect(all.single.cruiseSpeed, 15); + }); + + test('ensureSeeded ist ein no-op, wenn bereits Profile existieren', () async { + await repository.insert(DroneProfile.t1Ranger().copyWith(name: 'Custom')); + + await repository.ensureSeeded(); + + final all = await repository.loadAll(); + expect(all, hasLength(1)); + expect(all.single.name, 'Custom'); + }); + + test('insert/update/delete rundtrip', () async { + final id = await repository.insert( + DroneProfile.t1Ranger().copyWith(name: 'Racer', maxSpeed: 40), + ); + + var all = await repository.loadAll(); + expect(all.single.maxSpeed, 40); + + await repository.update(all.single.copyWith(maxSpeed: 45)); + all = await repository.loadAll(); + expect(all.single.maxSpeed, 45); + expect(all.single.id, id); + + await repository.delete(id); + expect(await repository.loadAll(), isEmpty); + }); + + test('watchAll gibt Aenderungen reaktiv weiter', () async { + final stream = repository.watchAll(); + final emissions = []; + final sub = stream.listen((profiles) => emissions.add(profiles.length)); + addTearDown(sub.cancel); + + await repository.insert(DroneProfile.t1Ranger()); + await pumpEventQueue(); + + expect(emissions, contains(1)); + }); +} diff --git a/app/test/services/mission_repository_test.dart b/app/test/services/mission_repository_test.dart new file mode 100644 index 0000000..cf0620f --- /dev/null +++ b/app/test/services/mission_repository_test.dart @@ -0,0 +1,104 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart'; +import 'package:dmc_app/services/database/app_database.dart'; +import 'package:dmc_app/services/database/mission_repository.dart'; + +void main() { + late AppDatabase db; + late MissionRepository repository; + + setUp(() { + db = AppDatabase.forTesting(NativeDatabase.memory()); + repository = MissionRepository(db); + }); + + tearDown(() => db.close()); + + const waypoints = [ + Waypoint(lat: 52.0, lon: 4.0, altitudeM: 60, speedMs: 15, catchRadiusM: 60), + Waypoint( + lat: 52.01, + lon: 4.01, + altitudeM: 80, + speedMs: 18, + catchRadiusM: 60, + action: WaypointAction.loiter, + ), + ]; + + test('upsert ohne id legt eine neue Mission an', () async { + final id = await repository.upsert( + name: 'Amsterdam', + placeName: 'Amsterdam', + waypoints: waypoints, + ); + + final loaded = await repository.load(id); + expect(loaded, isNotNull); + expect(loaded!.name, 'Amsterdam'); + expect(loaded.placeName, 'Amsterdam'); + expect(loaded.waypoints, hasLength(2)); + expect(loaded.waypoints[1].action, WaypointAction.loiter); + expect(loaded.waypoints[1].speedMs, 18); + }); + + test('upsert mit bestehender id ueberschreibt statt zu duplizieren', () async { + final id = await repository.upsert(name: 'Draft', waypoints: waypoints); + + await repository.upsert( + id: id, + name: 'Draft', + waypoints: [waypoints.first], + ); + + final summaries = await repository.watchSummaries().first; + expect(summaries, hasLength(1)); + expect(summaries.single.waypointCount, 1); + }); + + test('rename aendert nur den Namen, nicht die Wegpunkte', () async { + final id = await repository.upsert(name: 'Old name', waypoints: waypoints); + + await repository.rename(id, 'New name'); + + final loaded = await repository.load(id); + expect(loaded!.name, 'New name'); + expect(loaded.waypoints, hasLength(2)); + }); + + test('delete entfernt die Mission aus der Liste', () async { + final id = await repository.upsert(name: 'To delete', waypoints: waypoints); + + await repository.delete(id); + + final loaded = await repository.load(id); + expect(loaded, isNull); + expect(await repository.watchSummaries().first, isEmpty); + }); + + test('load fuer unbekannte id liefert null', () async { + expect(await repository.load(999), isNull); + }); + + test('Windfelder werden mitgespeichert und wiederhergestellt', () async { + const withWind = Waypoint( + lat: 52.0, + lon: 4.0, + altitudeM: 60, + speedMs: 15, + catchRadiusM: 60, + windSpeedMs: 5.5, + windDirFromDeg: 210, + windElevationM: 2, + ); + final id = await repository.upsert(name: 'Windy', waypoints: [withWind]); + + final loaded = await repository.load(id); + + expect(loaded!.waypoints.single.windSpeedMs, 5.5); + expect(loaded.waypoints.single.windDirFromDeg, 210); + expect(loaded.waypoints.single.windElevationM, 2); + }); +} diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index 4eb5b4d..b3b7478 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -1,3 +1,4 @@ +import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -6,8 +7,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:dmc_app/app_mode/app_mode_cubit.dart'; import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart'; +import 'package:dmc_app/services/database/app_database.dart' hide DroneProfile, Mission; import 'package:dmc_app/ui/app_shell.dart'; import 'package:dmc_app/ui/providers/current_mission_provider.dart'; +import 'package:dmc_app/ui/providers/database_provider.dart'; import 'package:dmc_app/ui/widgets/full_value_chart.dart'; import 'package:dmc_app/ui/widgets/halo_menu.dart'; import 'package:dmc_app/ui/widgets/header_wind_pill.dart'; @@ -19,7 +22,18 @@ import 'package:dmc_app/ui/widgets/waypoint_list_panel.dart'; import 'package:dmc_app/ui/widgets/wind_marker_pill.dart'; import 'package:latlong2/latlong.dart'; +// AppShell/PlanScreen initialisieren beim Start die Drift-Datenbank (Doku +// 4.15/7.5: Drohnenprofil-Bootstrap, Autosave-Restore) - in Widget-Tests +// durch eine In-Memory-Instanz ersetzt, damit dafuer nicht das +// path_provider-Plugin (echtes Dateisystem) benoetigt wird. +AppDatabase _testDatabase() => AppDatabase.forTesting(NativeDatabase.memory()); + +ProviderContainer _testContainer() => ProviderContainer( + overrides: [appDatabaseProvider.overrideWithValue(_testDatabase())], + ); + Widget _wrap(Widget child) => ProviderScope( + overrides: [appDatabaseProvider.overrideWithValue(_testDatabase())], child: BlocProvider( create: (_) => AppModeCubit(), child: MaterialApp(home: child), @@ -27,7 +41,7 @@ Widget _wrap(Widget child) => ProviderScope( ); Future _pumpWithContainer(WidgetTester tester) async { - final container = ProviderContainer(); + final container = _testContainer(); await tester.pumpWidget( UncontrolledProviderScope( container: container, @@ -73,7 +87,7 @@ void main() { testWidgets('Reticle-Button fuegt einen Wegpunkt am Kartenzentrum hinzu', (tester) async { - final container = ProviderContainer(); + final container = _testContainer(); addTearDown(container.dispose); await tester.pumpWidget( @@ -96,12 +110,16 @@ void main() { expect(waypoints, hasLength(1)); expect(waypoints.single.lat, closeTo(52.3676, 0.0001)); expect(waypoints.single.lon, closeTo(4.9041, 0.0001)); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets( 'Zweiter Tap auf den frisch gesetzten Wegpunkt oeffnet das Halo-Menue', (tester) async { - final container = ProviderContainer(); + final container = _testContainer(); addTearDown(container.dispose); await tester.pumpWidget( @@ -124,11 +142,15 @@ void main() { await tester.tap(find.byType(ReticleButton)); await tester.pump(); expect(find.byType(HaloMenu), findsOneWidget); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets('Halo-Remove entfernt den Wegpunkt und schliesst das Menue', (tester) async { - final container = ProviderContainer(); + final container = _testContainer(); addTearDown(container.dispose); await tester.pumpWidget( @@ -155,11 +177,15 @@ void main() { expect(container.read(currentMissionProvider), isEmpty); expect(find.byType(HaloMenu), findsNothing); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets('Halo-Loiter setzt die Execute-Aktion und schliesst das Menue', (tester) async { - final container = ProviderContainer(); + final container = _testContainer(); addTearDown(container.dispose); await tester.pumpWidget( @@ -184,6 +210,10 @@ void main() { final waypoints = container.read(currentMissionProvider); expect(waypoints.single.action, WaypointAction.loiter); expect(find.byType(HaloMenu), findsNothing); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets('Bottom-Stats-Bar oeffnet die leere Wegpunktliste', @@ -219,6 +249,10 @@ void main() { await tester.pump(const Duration(milliseconds: 200)); expect(find.byType(WaypointListPanel), findsNothing); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets('Papierkorb-Icon in der Liste entfernt den Wegpunkt', @@ -238,6 +272,10 @@ void main() { expect(container.read(currentMissionProvider), isEmpty); expect(find.text('No waypoints yet'), findsOneWidget); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets( @@ -267,6 +305,10 @@ void main() { final waypoints = container.read(currentMissionProvider); expect(waypoints.single.altitudeM, greaterThan(60)); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets( @@ -298,6 +340,10 @@ void main() { final speed = container.read(currentMissionProvider).single.speedMs; expect(speed, greaterThan(startSpeed)); expect(speed, lessThan(startSpeed + 4)); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets( @@ -321,6 +367,10 @@ void main() { final waypoints = container.read(currentMissionProvider); expect(waypoints.single.altitudeM, greaterThan(60)); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets( @@ -344,6 +394,10 @@ void main() { final waypoints = container.read(currentMissionProvider); expect(waypoints.single.speedMs, greaterThan(15)); + + // Autosave debounced ueber 800ms - abwarten, damit nach Testende kein + // Timer mehr aussteht (Doku 7.5). + await tester.pump(const Duration(milliseconds: 900)); }); testWidgets('MapSearchControls zeigt Suchfeld und Home-/Fit-Buttons', @@ -391,7 +445,7 @@ void main() { testWidgets( 'Kameraposition bleibt beim Wechsel Plan -> Fly -> Plan erhalten', (tester) async { - final container = ProviderContainer(); + final container = _testContainer(); final appModeCubit = AppModeCubit(); addTearDown(() { container.dispose(); @@ -434,7 +488,7 @@ void main() { testWidgets( 'Wegpunktliste scrollt beim Oeffnen automatisch zum aktiven Wegpunkt', (tester) async { - final container = ProviderContainer(); + final container = _testContainer(); addTearDown(container.dispose); final notifier = container.read(currentMissionProvider.notifier); @@ -476,7 +530,7 @@ void main() { testWidgets( 'Wind-Marker an Wegpunkten erscheinen erst nach Antippen der Kopfleisten-Pille', (tester) async { - final container = ProviderContainer(); + final container = _testContainer(); addTearDown(container.dispose); // Am Kartenzentrum platziert (siehe MissionMap._initialCenter), damit