The icons (flag/ruler/clock) already convey what each value means, so the label was redundant. Gave the pill a stable Key since tests relied on the now-removed text to find and tap it.
582 lines
23 KiB
Dart
582 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(
|
|
// Testhaken (siehe widget_test.dart): seit dem Entfernen des
|
|
// "Details:"-Labels (Nutzerwunsch: Platz sparen in Plan- und
|
|
// Fly-Modus) gibt es keinen eindeutigen Text mehr, um die Pille in
|
|
// Tests anzutippen.
|
|
key: const Key('detailsPill'),
|
|
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 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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|