The drone status pill and the mission pill previously sat in two equally sized Expanded regions. That squeezed the drone pill's FittedBox down to illegibility whenever its content grew (e.g. "disconnected"), while the mission pill often had unused space to spare. A plain flex-ratio rebalance (e.g. mission flex:1 vs drone flex:4) turned out to be the wrong tool: Expanded/Flexible always force a fixed fractional share regardless of actual content need, so it either starved the mission pill even for short names, or capped the drone pill below what it needed. bottom_stats_bar.dart: the drone-side content (status pill or drone-name chip + send/warning/settings buttons) is now a plain, non-flex Row child, exactly like _detailsPill()/_AltitudeProfileToggleButton already were - it always gets exactly the width its current content needs, never more, never less. The mission pill remains the sole Expanded element and absorbs whatever space is left, ellipsizing first if it's tight. widget_test.dart: two tests exercising the footer at the default 800x600 test viewport started hitting a real RenderFlex overflow, since that width was never realistic for this landscape-only app's footer content in the first place (previously masked by the drone pill silently shrinking via FittedBox). Set a realistic device-sized viewport (2424x1080, matching the Pixel_10a emulator) for just those two tests. Verified on the Pixel_10a emulator: "disconnected" now renders fully legible in the drone pill, and the mission pill still reads normally alongside it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
586 lines
23 KiB
Dart
586 lines
23 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.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 'drone_status_messages_panel.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;
|
|
|
|
/// Einheitliche Icon-/Textgroesse und Schriftgewicht fuer alle Pillen und
|
|
/// Knoepfe der Fusszeile (Doku: "text und symbol hoehe in allen pillen und
|
|
/// buttons der fusszeile genau so machen wie in der neuen drone pille.
|
|
/// einheitliche schriftart und gewicht verwenden") - uebernommen von der
|
|
/// zuvor eigens dafuer eingefuehrten Drohnen-Status-Pille. Eine eigene
|
|
/// Schriftart wird nicht gesetzt, alle Texte hier verwenden bereits ohne
|
|
/// explizites fontFamily das App-Theme, sind also schon einheitlich.
|
|
const _footerIconSize = 20.0;
|
|
const _footerFontSize = 14.0;
|
|
const _footerFontWeight = FontWeight.w600;
|
|
|
|
/// Statuszeile am unteren Bildschirmrand (HTML-Demonstrator: #bottomBar/
|
|
/// #statsRow). Details-Pille bleibt mittig und oeffnet die Wegpunktliste;
|
|
/// Mission-Chip links und Drohnen-Chip rechts oeffnen das Verwaltungsmenue
|
|
/// mit dem passenden Starttab (HTML-Demonstrator: #missionNameBtn/
|
|
/// #droneNameBtn). Warnungs- (Plan-Modus) bzw. Sende-Knopf (Fly-Modus,
|
|
/// siehe onSendTap) und Settings-Knopf sitzen neben dem Drohnen-Chip
|
|
/// rechts aussen. Der Hoehenprofil-Toggle (Bergkuppen-Icon) sitzt links
|
|
/// der Details-Pille.
|
|
class BottomStatsBar extends ConsumerWidget {
|
|
const BottomStatsBar({
|
|
super.key,
|
|
required this.waypointCount,
|
|
required this.totalDistanceM,
|
|
required this.totalDurationSec,
|
|
this.flyMissionStatus,
|
|
this.droneStatus,
|
|
required this.onTap,
|
|
required this.onSettingsTap,
|
|
this.warnings = const [],
|
|
this.onWarningsTap,
|
|
this.onSendTap,
|
|
this.sending = false,
|
|
});
|
|
|
|
final int waypointCount;
|
|
final double totalDistanceM;
|
|
final double totalDurationSec;
|
|
|
|
/// 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
|
|
/// Details-Pille).
|
|
final VoidCallback onSettingsTap;
|
|
|
|
/// Aktive Warnungen zur Mission (Doku 3.10) - der Button erscheint nur,
|
|
/// solange diese Liste nicht leer ist (HTML-Demonstrator: #warningsBtn,
|
|
/// per default versteckt).
|
|
final List<MissionWarning> warnings;
|
|
final VoidCallback? onWarningsTap;
|
|
|
|
/// Nur im Fly-Modus gesetzt (Doku: "im fly screen den warnings button
|
|
/// durch einen wp send button ersetzen") - wenn gesetzt, ersetzt der
|
|
/// Sende-Knopf (Papierflieger, horizontal, pink) den runden Warnungs-
|
|
/// Knopf komplett, unabhaengig von [warnings]. System Messages (Doku:
|
|
/// "aendere das warnings Menü in System Messages") bleiben im Fly-Modus
|
|
/// weiterhin ueber die Drohnen-Status-Pille erreichbar (dort als eigener
|
|
/// Tab), siehe DroneStatusMessagesPanel.
|
|
final VoidCallback? onSendTap;
|
|
|
|
/// Zeigt einen Ladeindikator statt des Papierflieger-Icons und ignoriert
|
|
/// Taps, waehrend ein Upload laeuft (verhindert doppeltes Abschicken).
|
|
final bool sending;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final missionName = ref.watch(currentMissionMetaProvider).name;
|
|
final droneName = ref.watch(activeDroneProfileProvider).name;
|
|
final isAltitudeProfileExpanded = ref.watch(altitudeProfileExpandedProvider);
|
|
|
|
// Gespiegelter Randabstand (wie in PlanScreen fuer die Drehraeder, Doku:
|
|
// "Symmetrischer Randabstand auf beiden Seiten"): die groessere der
|
|
// beiden echten Seiten-Insets wird auf BEIDE Seiten angewendet, statt
|
|
// SafeAreas Standardverhalten (jede Seite nur ihren eigenen, ggf. 0)
|
|
// zu uebernehmen - sonst waere z.B. eine Frontkamera-Aussparung im
|
|
// Querformat nur auf einer Seite der Fusszeile sichtbar beruecksichtigt,
|
|
// je nachdem wie herum das Geraet gehalten wird.
|
|
final safeEdgeInset = math.max(
|
|
MediaQuery.of(context).padding.left,
|
|
MediaQuery.of(context).padding.right,
|
|
);
|
|
|
|
// 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,
|
|
left: false,
|
|
right: false,
|
|
child: Padding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
12 + safeEdgeInset,
|
|
10,
|
|
12 + safeEdgeInset,
|
|
10,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Mission-Pille links bleibt das einzige flexible/absorbierende
|
|
// Element (Doku: "erlaube dass die Pille fuer den Drohnenstatus
|
|
// groesser skalieren kann ... und dafuer die Missionspille
|
|
// kleiner skalieren kann", "darauf achten dass drone status
|
|
// immer auf seinen inhalt skaliert") - vorher waren links UND
|
|
// rechts je ein gleich breites Expanded, wodurch die
|
|
// Drohnen-Pille bei langem Inhalt (z.B. "disconnected") per
|
|
// FittedBox bis zur Unlesbarkeit schrumpfte. Ein Flex-
|
|
// Verhaeltnis (z.B. 1:4) loest das NICHT sauber: Expanded/
|
|
// Flexible erzwingen so oder so eine feste Bruchteils-
|
|
// Zuteilung, die die Missionspille selbst bei kurzen Namen auf
|
|
// ihren (kleineren) Anteil einschraenkt, unabhaengig davon, ob
|
|
// die Drohnen-Seite diesen Platz ueberhaupt braucht. Die
|
|
// Drohnen-Seite ist deshalb jetzt bewusst NICHT mehr
|
|
// Expanded/Flexible, sondern wie _detailsPill()/
|
|
// _AltitudeProfileToggleButton ein normales, intrinsisch
|
|
// grosses Row-Kind - sie bekommt IMMER genau die Breite, die
|
|
// ihr aktueller Inhalt braucht, nie mehr und nie weniger. Die
|
|
// Missionspille als einziges Expanded-Element bekommt
|
|
// verlaesslich den kompletten Rest und schrumpft (ellipsiert)
|
|
// zuerst, wenn es eng wird.
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
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: _footerElementSpacing),
|
|
_AltitudeProfileToggleButton(
|
|
isExpanded: isAltitudeProfileExpanded,
|
|
onTap: () => ref
|
|
.read(altitudeProfileExpandedProvider.notifier)
|
|
.toggle(),
|
|
),
|
|
const SizedBox(width: _footerElementSpacing),
|
|
_detailsPill(),
|
|
const SizedBox(width: _footerElementSpacing),
|
|
// Drohnen-Pille + Warnungs-/Settings-Knopf rechts - bewusst kein
|
|
// Expanded/Flexible (siehe Kommentar oben): immer intrinsische
|
|
// Breite, FittedBox in _DroneStatusPill bleibt nur als letztes
|
|
// Sicherheitsnetz fuer wirklich extreme Faelle (z.B.
|
|
// Grossschrift-Bedienungshilfen), statt schon im Normalbetrieb
|
|
// zu greifen.
|
|
droneStatus != null
|
|
? _DroneStatusPill(
|
|
status: droneStatus!,
|
|
// Oeffnet im Fly-Modus das neue Status-Menue (Doku: "ein
|
|
// neues menue anlegen ... nach dem vorbild des menues
|
|
// fuer drones and missions") direkt auf dem Status-Tab,
|
|
// statt wie zuvor die Drohnenprofil-Verwaltung - Profile
|
|
// werden waehrend des Fluges ohnehin nicht gewechselt.
|
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (_) => const DroneStatusMessagesPanel.status(),
|
|
)),
|
|
)
|
|
: _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: _footerElementSpacing),
|
|
if (onSendTap != null) ...[
|
|
_SendButton(sending: sending, onTap: onSendTap),
|
|
const SizedBox(width: _footerElementSpacing),
|
|
] else if (warnings.isNotEmpty) ...[
|
|
_WarningButton(onTap: onWarningsTap),
|
|
const SizedBox(width: _footerElementSpacing),
|
|
],
|
|
_SettingsButton(onTap: onSettingsTap),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _detailsPill() {
|
|
final flyStatus = flyMissionStatus;
|
|
// Im Fly-Modus (flyStatus != null) ersetzen aktiver Wegpunkt sowie
|
|
// Distanz/Zeit dorthin die statischen Plan-Werte (Doku: "Live-Status
|
|
// der Mission in der Details-Pille, genau wie vorher aber mit aktivem
|
|
// Wegpunkt statt Gesamtzahl, Distanz/Zeit zum naechsten Wegpunkt statt
|
|
// Gesamtdistanz/-dauer").
|
|
final waypointText = flyStatus != null
|
|
? '${flyStatus.activeWaypointNumber}/${flyStatus.waypointCount}'
|
|
: '$waypointCount WP';
|
|
final distanceText = flyStatus != null
|
|
? (flyStatus.distanceToActiveWaypointM != null
|
|
? '${flyStatus.distanceToActiveWaypointM!.round()} m'
|
|
: '—')
|
|
: '${totalDistanceM.round()} m';
|
|
final durationText = flyStatus != null
|
|
? (flyStatus.etaToActiveWaypointSec != null
|
|
? _fmtDuration(flyStatus.etaToActiveWaypointSec!)
|
|
: '—')
|
|
: _fmtDuration(totalDurationSec);
|
|
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
height: _footerElementHeight,
|
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(_footerElementHeight / 2),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text(
|
|
'Details:',
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontWeight: _footerFontWeight,
|
|
fontSize: _footerFontSize,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
const Icon(Icons.flag, size: _footerIconSize, color: Colors.white),
|
|
const SizedBox(width: 4),
|
|
Text(waypointText,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: _footerFontSize,
|
|
fontWeight: _footerFontWeight,
|
|
)),
|
|
const SizedBox(width: 14),
|
|
const Icon(Icons.straighten, size: _footerIconSize, color: Colors.white),
|
|
const SizedBox(width: 4),
|
|
Text(distanceText,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: _footerFontSize,
|
|
fontWeight: _footerFontWeight,
|
|
)),
|
|
const SizedBox(width: 14),
|
|
const Icon(Icons.access_time, size: _footerIconSize, color: Colors.white),
|
|
const SizedBox(width: 4),
|
|
Text(durationText,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: _footerFontSize,
|
|
fontWeight: _footerFontWeight,
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _fmtDuration(double seconds) {
|
|
final total = seconds.round();
|
|
final minutes = total ~/ 60;
|
|
final secs = total % 60;
|
|
return '$minutes:${secs.toString().padLeft(2, '0')}';
|
|
}
|
|
}
|
|
|
|
/// Runder Knopf links der Details-Pille zum Ein-/Ausklappen des
|
|
/// Mini-Hoehenprofils. Zeigt eine stilisierte Bergkuppe: ausgeklappt steht
|
|
/// sie kopfueber (Zacken zeigen nach unten - signalisiert "einklappen"),
|
|
/// eingeklappt (Standard, um moeglichst viel Kartenflaeche frei zu lassen)
|
|
/// richtig herum (Zacken zeigen nach oben - signalisiert "ausklappen").
|
|
class _AltitudeProfileToggleButton extends StatelessWidget {
|
|
const _AltitudeProfileToggleButton({
|
|
required this.isExpanded,
|
|
required this.onTap,
|
|
});
|
|
|
|
final bool isExpanded;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: _footerElementHeight,
|
|
height: _footerElementHeight,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.white.withValues(alpha: 0.12),
|
|
),
|
|
child: Center(
|
|
child: MountainPeakIcon(
|
|
size: _footerIconSize,
|
|
color: Colors.white,
|
|
upsideDown: isExpanded,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Kleiner, roter Rundknopf fuer aktive Warnungen (Doku 3.10, HTML-
|
|
/// Demonstrator: #warningsBtn) - oeffnet die Warnungsliste.
|
|
class _WarningButton extends StatelessWidget {
|
|
const _WarningButton({required this.onTap});
|
|
|
|
final VoidCallback? onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: _footerElementHeight,
|
|
height: _footerElementHeight,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: const Color(0xFFE2574A).withValues(alpha: 0.18),
|
|
border: Border.all(color: const Color(0xFFE2574A).withValues(alpha: 0.5)),
|
|
),
|
|
child: const Icon(Icons.warning_amber_rounded, color: Color(0xFFFFB3A8), size: _footerIconSize),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Runder Sende-Knopf im Fly-Modus (Doku: "im fly screen den warnings
|
|
/// button durch einen wp send button ersetzen. verwende unser paperplane
|
|
/// shape. ausrichtung horizontal und pink gefuellt") - ersetzt dort den
|
|
/// Warnungs-Knopf komplett, siehe BottomStatsBar.onSendTap.
|
|
class _SendButton extends StatelessWidget {
|
|
const _SendButton({required this.sending, required this.onTap});
|
|
|
|
final bool sending;
|
|
final VoidCallback? onTap;
|
|
|
|
static const _pink = Color(0xFFFF2E93);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: sending ? null : onTap,
|
|
child: Container(
|
|
width: _footerElementHeight,
|
|
height: _footerElementHeight,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.white.withValues(alpha: 0.12),
|
|
),
|
|
child: Center(
|
|
child: sending
|
|
? const SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(strokeWidth: 2, color: _pink),
|
|
)
|
|
// Papierflieger liegend statt der Spitze-nach-oben-
|
|
// Kartenmarker-Orientierung (Doku: "ausrichtung horizontal") -
|
|
// um 90 Grad im Uhrzeigersinn gedreht zeigt die Nase nach
|
|
// rechts, wie ein abgeschickter/wegfliegender Flieger.
|
|
: Transform.rotate(
|
|
angle: math.pi / 2,
|
|
child: const PaperPlaneIcon(size: _footerIconSize, color: _pink),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Runder Zahnrad-Knopf rechts der Details-Pille - oeffnet das
|
|
/// Settings-Menue (SettingsScreen).
|
|
class _SettingsButton extends StatelessWidget {
|
|
const _SettingsButton({required this.onTap});
|
|
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: _footerElementHeight,
|
|
height: _footerElementHeight,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.white.withValues(alpha: 0.12),
|
|
),
|
|
child: const Icon(Icons.settings, color: Colors.white, size: _footerIconSize),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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.disconnected: 'disconnected',
|
|
DroneState.error: 'error',
|
|
};
|
|
|
|
/// Nur 3 Farben fuer 6 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 fuer Emergency UND fehlenden Telemetrie-Strom
|
|
/// (Doku: "rote Drohne ... Status text in rot disconnected").
|
|
static const _stateColors = {
|
|
DroneState.idle: DmcColors.pafAmber,
|
|
DroneState.ready: DmcColors.flyModeColor,
|
|
DroneState.armed: DmcColors.pafAmber,
|
|
DroneState.waypoint: DmcColors.flyModeColor,
|
|
DroneState.disconnected: DmcColors.warnRed,
|
|
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.
|
|
// Zustandstext folgt erst ganz am Ende hinter den beiden
|
|
// Qualitaets-Symbolen (Doku: "text fuer drone state als
|
|
// letztes hinter den symbolen anzeigen"). Icon-/Textgroessen
|
|
// an die Pillenhoehe angeglichen (Doku: "hoehe der pille
|
|
// besser ausnutzen") statt der vorher recht kleinen 12-15px.
|
|
PaperPlaneIcon(size: _footerIconSize, color: stateColor),
|
|
const SizedBox(width: 8),
|
|
Icon(Icons.signal_cellular_alt,
|
|
size: _footerIconSize, color: _signalColors[status.connectionQuality]),
|
|
const SizedBox(width: 8),
|
|
Icon(Icons.satellite_alt,
|
|
size: _footerIconSize, color: _signalColors[status.positionQuality]),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
_stateLabels[status.state]!,
|
|
style: TextStyle(
|
|
color: stateColor,
|
|
fontWeight: _footerFontWeight,
|
|
fontSize: _footerFontSize,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// Kein fester maxWidth mehr - die Pille darf die verfuegbare Breite
|
|
// ihres Elternwidgets nutzen (Align/Flexible am Aufrufort begrenzen sie
|
|
// bereits auf die dort tatsaechlich verfuegbare Breite).
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
height: _footerElementHeight,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
borderRadius: BorderRadius.circular(_footerElementHeight / 2),
|
|
),
|
|
child: Text(
|
|
label,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontWeight: _footerFontWeight,
|
|
fontSize: _footerFontSize,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|