drone status pill in footer for fly mode implemented. footer layout and sizes homogenized
This commit is contained in:
@@ -3,13 +3,29 @@ import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../domain/mission/mission_stats.dart';
|
||||
import '../../domain/mission/mission_warnings.dart';
|
||||
import '../../domain/mission/mission_stats.dart';
|
||||
import '../../domain/telemetry/drone_status.dart';
|
||||
import '../dmc_colors.dart';
|
||||
import '../providers/active_drone_profile_provider.dart';
|
||||
import '../providers/altitude_profile_provider.dart';
|
||||
import '../providers/mission_meta_provider.dart';
|
||||
import 'missions_drones_panel.dart';
|
||||
import 'mountain_peak_icon.dart';
|
||||
import 'paper_plane_icon.dart';
|
||||
|
||||
/// Einheitliche Hoehe fuer alle Pillen/Knoepfe der Fusszeile (Doku: "fuer
|
||||
/// alle Pillen und Buttons die gleiche Hoehe wie bei der Details-Pille
|
||||
/// verwenden") - vorher hatten Text-Pillen (implizit aus Padding+Zeilen-
|
||||
/// hoehe) und die runden Knoepfe (fest 34px) leicht unterschiedliche
|
||||
/// Hoehen, was in der Fusszeile ein unruhiges Bild ergab.
|
||||
const _footerElementHeight = 36.0;
|
||||
|
||||
/// Einheitlicher Abstand zwischen allen Pillen/Knoepfen der Fusszeile
|
||||
/// (Doku: "vereinheitliche den Abstand zwischen Buttons und Pillen") -
|
||||
/// vorher fehlte er stellenweise ganz (z.B. zwischen Details- und
|
||||
/// Drohnen-Pille), an anderen Stellen war er fest auf 6px verdrahtet.
|
||||
const _footerElementSpacing = 8.0;
|
||||
|
||||
/// Statuszeile am unteren Bildschirmrand (HTML-Demonstrator: #bottomBar/
|
||||
/// #statsRow). Details-Pille bleibt mittig und oeffnet die Wegpunktliste;
|
||||
@@ -27,6 +43,7 @@ class BottomStatsBar extends ConsumerWidget {
|
||||
required this.totalDistanceM,
|
||||
required this.totalDurationSec,
|
||||
this.flyMissionStatus,
|
||||
this.droneStatus,
|
||||
required this.onTap,
|
||||
required this.onSettingsTap,
|
||||
this.warnings = const [],
|
||||
@@ -40,6 +57,11 @@ class BottomStatsBar extends ConsumerWidget {
|
||||
/// Siehe MissionFooterBar-Doku - nur im Fly-Modus gesetzt, ersetzt dann
|
||||
/// die statischen Felder in der Details-Pille.
|
||||
final FlyMissionStatus? flyMissionStatus;
|
||||
|
||||
/// Nur im Fly-Modus gesetzt (Doku: "im fly modus zeigt die drone pille
|
||||
/// den zustand der drohne") - ersetzt dort den Drohnennamen-Chip durch
|
||||
/// die 3-Icon-Statusanzeige (Verbindung/Position/Zustand).
|
||||
final DroneStatus? droneStatus;
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// Oeffnet das Settings-Menue (runder Zahnrad-Knopf rechts der
|
||||
@@ -108,14 +130,16 @@ class BottomStatsBar extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _footerElementSpacing),
|
||||
_AltitudeProfileToggleButton(
|
||||
isExpanded: isAltitudeProfileExpanded,
|
||||
onTap: () => ref
|
||||
.read(altitudeProfileExpandedProvider.notifier)
|
||||
.toggle(),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: _footerElementSpacing),
|
||||
_detailsPill(),
|
||||
const SizedBox(width: _footerElementSpacing),
|
||||
// Drohnen-Pille + Warnungs-/Settings-Knopf rechts. Die Pille
|
||||
// sitzt in Flexible, damit sie bei langen Namen zu Gunsten der
|
||||
// beiden festgroessen Rundknoepfe schrumpft/ellipsiert, statt
|
||||
@@ -127,19 +151,31 @@ class BottomStatsBar extends ConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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(),
|
||||
)),
|
||||
),
|
||||
child: droneStatus != null
|
||||
? _DroneStatusPill(
|
||||
status: droneStatus!,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (_) => const MissionsDronesPanel.drones(),
|
||||
)),
|
||||
)
|
||||
: _NameChip(
|
||||
// Gleicher Hintergrund wie die Details-Pille
|
||||
// (Doku: "gleichen Hintergrund wie bei der
|
||||
// Detail Pille fuer die Drohne Pille
|
||||
// verwenden") statt des vorherigen blauen
|
||||
// Farbtons.
|
||||
label: 'Drone: $droneName',
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
textColor: Colors.white,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (_) => const MissionsDronesPanel.drones(),
|
||||
)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: _footerElementSpacing),
|
||||
if (warnings.isNotEmpty) ...[
|
||||
_WarningButton(onTap: onWarningsTap),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: _footerElementSpacing),
|
||||
],
|
||||
_SettingsButton(onTap: onSettingsTap),
|
||||
],
|
||||
@@ -176,10 +212,11 @@ class BottomStatsBar extends ConsumerWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 9),
|
||||
height: _footerElementHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(_footerElementHeight / 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -240,8 +277,8 @@ class _AltitudeProfileToggleButton extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
width: _footerElementHeight,
|
||||
height: _footerElementHeight,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
@@ -270,8 +307,8 @@ class _WarningButton extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
width: _footerElementHeight,
|
||||
height: _footerElementHeight,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFFE2574A).withValues(alpha: 0.18),
|
||||
@@ -295,8 +332,8 @@ class _SettingsButton extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
width: _footerElementHeight,
|
||||
height: _footerElementHeight,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
@@ -307,6 +344,100 @@ class _SettingsButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ersetzt im Fly-Modus die Drohnen-Namen-Pille (Doku: "im fly modus zeigt
|
||||
/// die drone pille den zustand der drohne. das beinhaltet diese 3
|
||||
/// elemente: connection quality ..., position quality ..., drone state
|
||||
/// ..."). Farbcodierung siehe computeDroneStatus in
|
||||
/// domain/telemetry/drone_status.dart; das Papierflieger-Icon zeigt
|
||||
/// zusaetzlich einen Kurztext (idle/ready/armed/waypoint/error).
|
||||
class _DroneStatusPill extends StatelessWidget {
|
||||
const _DroneStatusPill({required this.status, required this.onTap});
|
||||
|
||||
final DroneStatus status;
|
||||
final VoidCallback onTap;
|
||||
|
||||
static const _stateLabels = {
|
||||
DroneState.idle: 'idle',
|
||||
DroneState.ready: 'ready',
|
||||
DroneState.armed: 'armed',
|
||||
DroneState.waypoint: 'waypoint',
|
||||
DroneState.error: 'error',
|
||||
};
|
||||
|
||||
/// Nur 3 Farben fuer 5 Zustaende (Doku: "papierflieger icon mit
|
||||
/// farbcodierung (gruen/gelb/rot)") - gruen fuer die beiden Zustaende, in
|
||||
/// denen alles nominal laeuft (bereit zum Start bzw. aktiv unterwegs),
|
||||
/// gelb fuer die beiden "Achtung"-Uebergangszustaende (am Boden wartend
|
||||
/// bzw. bereits scharf aber ohne aktive Wegpunktnavigation, z.B. manuell
|
||||
/// oder im Hold), rot ausschliesslich fuer Emergency.
|
||||
static const _stateColors = {
|
||||
DroneState.idle: DmcColors.pafAmber,
|
||||
DroneState.ready: DmcColors.flyModeColor,
|
||||
DroneState.armed: DmcColors.pafAmber,
|
||||
DroneState.waypoint: DmcColors.flyModeColor,
|
||||
DroneState.error: DmcColors.warnRed,
|
||||
};
|
||||
|
||||
static const _signalColors = {
|
||||
SignalLevel.green: DmcColors.flyModeColor,
|
||||
SignalLevel.yellow: DmcColors.pafAmber,
|
||||
SignalLevel.red: DmcColors.warnRed,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final stateColor = _stateColors[status.state]!;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: _footerElementHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(_footerElementHeight / 2),
|
||||
),
|
||||
// FittedBox statt eines festen Layouts: die Pille sitzt in einem
|
||||
// Flexible neben Warnungs-/Settings-Knopf (BottomStatsBar) und hat
|
||||
// je nach Bildschirmbreite/Drohnennamenslaenge unterschiedlich viel
|
||||
// Platz - ohne Skalierung wuerde 3 Icons + Statustext auf schmalen
|
||||
// Screens einen RenderFlex-Overflow ausloesen statt einfach etwas
|
||||
// kleiner zu werden.
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Papierflieger-Symbol ganz links in der Pille (Doku: "fuer
|
||||
// das drohnen symbol ... unsere standard paperplane form
|
||||
// verwenden und nach ganz links innerhalb der pille
|
||||
// anordnen") - unsere Standardform (dieselben Koordinaten
|
||||
// wie DroneMarkerIcon/DroneFollowIcon), hier einfarbig nach
|
||||
// Zustand statt der zweifarbigen/pinken Kartenmarker-Variante.
|
||||
PaperPlaneIcon(size: 15, color: stateColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_stateLabels[status.state]!,
|
||||
style: TextStyle(
|
||||
color: stateColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Icon(Icons.signal_cellular_alt,
|
||||
size: 15, color: _signalColors[status.connectionQuality]),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.satellite_alt,
|
||||
size: 14, color: _signalColors[status.positionQuality]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NameChip extends StatelessWidget {
|
||||
const _NameChip({
|
||||
required this.label,
|
||||
@@ -328,10 +459,12 @@ class _NameChip extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
height: _footerElementHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(_footerElementHeight / 2),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
|
||||
Reference in New Issue
Block a user