import 'package:flutter/material.dart'; import '../dmc_colors.dart'; /// Schmaler Batteriebalken ueber der Fusszeile im Fly-Modus (Doku: /// "Balken ueber die gesamte Breite, feste Farbzonen von links: 20% rot /// (sofort landen), 30% orange (landen), Rest gruen"). Nur sichtbar, /// solange Telemetrie vorliegt - siehe Aufrufstelle in MissionFooterBar, /// die dafuer sorgt, dass die Anzeige im Plan-Modus (kein [percent]) /// entfaellt. /// /// Die Farbzonen sind an FESTEN Positionen im Balken verankert, gemessen /// vom LINKEN Rand (0-20% rot, 20-30% orange, 30-100% gruen) - "30% /// orange" bedeutet also, dass die orangene Zone bis zur 30%-Marke reicht /// (nur 10 Prozentpunkte weiter als rot), nicht dass sie selbst 30 Punkte /// breit ist. Die gefuellte Laenge (= [percent] der Gesamtbreite) schrumpft /// mit sinkendem Ladestand von rechts nach links, bis irgendwann nur noch /// der orangene bzw. rote Bereich links uebrig bleibt. Berechnet die je /// Zone tatsaechlich sichtbare Breite direkt in Dart (statt ueber Align. /// widthFactor + ClipRect zu clippen) - robuster gegenueber Layout- /// Eigenheiten und leichter nachvollziehbar. Farben rot/gruen bewusst /// identisch zur Kopfleiste (DmcColors.warnRed/flyModeColor), Orange aus /// der sonst im Rest der App bereits verwendeten Amber-Farbe /// (DmcColors.pafAmber) - der Screenshot in design/ diente nur als /// Layout-Vorlage, nicht als Farbvorgabe. class BatteryIndicatorBar extends StatelessWidget { const BatteryIndicatorBar({super.key, required this.percent}); /// 0-100, ungeklemmter Rohwert aus TelemetryFrame.batteryPercent. final int percent; /// Absolute Positionen (vom linken Rand aus), an denen rot in orange und /// orange in gruen uebergeht - siehe Klassendoku. static const _redZoneEndFraction = 0.20; static const _amberZoneEndFraction = 0.30; static const _barHeight = 10.0; static const _pillHeight = 22.0; static const _pillMinWidth = 46.0; /// Durchsichtiger als der Fusszeilen-Hintergrund (0.72 in /// MissionFooterBar) - der Balken liegt ueber diesem bereits halb- /// opaken Hintergrund, ohne eigene zusaetzliche Abdunkelung waere er /// also automatisch schon genauso transparent; dieser niedrige Wert /// sorgt dafuer, dass er sogar noch etwas heller/durchsichtiger wirkt. static const _trackAlpha = 0.12; static const _fillAlpha = 0.55; static const _pillNeutralAlpha = 0.55; static const _pillWarningAlpha = 0.8; @override Widget build(BuildContext context) { final clampedPercent = percent.clamp(0, 100); return SizedBox( height: _pillHeight, width: double.infinity, child: LayoutBuilder( builder: (context, constraints) { final totalWidth = constraints.maxWidth; final fillWidth = totalWidth * clampedPercent / 100; final redZoneEnd = totalWidth * _redZoneEndFraction; final amberZoneEnd = totalWidth * _amberZoneEndFraction; final redZoneWidth = redZoneEnd; final amberZoneWidth = amberZoneEnd - redZoneEnd; final greenZoneWidth = totalWidth - amberZoneEnd; // Je Zone nur so viel sichtbar wie vom aktuellen Fuellstand noch // "uebrig" ist, sobald die vorherigen Zonen bereits voll // abgedeckt sind - siehe Klassendoku. final redVisible = fillWidth.clamp(0.0, redZoneWidth); final amberVisible = (fillWidth - redZoneWidth).clamp(0.0, amberZoneWidth); final greenVisible = (fillWidth - redZoneWidth - amberZoneWidth).clamp(0.0, greenZoneWidth); final pillLeft = (fillWidth - _pillMinWidth / 2).clamp( 0.0, (totalWidth - _pillMinWidth) < 0 ? 0.0 : totalWidth - _pillMinWidth, ); // Faerbt die Prozent-Pille passend zur Zone, in der der aktuelle // Ladestand liegt (Doku: "sobald die Pille den farbigen [Warn-] // Bereich erreicht, auch die Pille entsprechend faerben") - im // gruenen (normalen) Bereich bleibt sie neutral dunkel, um nicht // staendig unnoetig Aufmerksamkeit zu ziehen. final Color pillColor; if (clampedPercent <= _redZoneEndFraction * 100) { pillColor = DmcColors.warnRed.withValues(alpha: _pillWarningAlpha); } else if (clampedPercent <= _amberZoneEndFraction * 100) { pillColor = DmcColors.pafAmber.withValues(alpha: _pillWarningAlpha); } else { pillColor = Colors.black.withValues(alpha: _pillNeutralAlpha); } return Stack( clipBehavior: Clip.none, children: [ // Transparenter Hintergrund fuer den ungefuellten Rest (Doku: // "Transparenz des Hintergrunds hoeher machen als in der // Fusszeile") - nur der dunkle Fusszeilen-Hintergrund scheint // durch. Positioned( left: 0, right: 0, top: (_pillHeight - _barHeight) / 2, height: _barHeight, child: ColoredBox(color: Colors.black.withValues(alpha: _trackAlpha)), ), Positioned( left: 0, top: (_pillHeight - _barHeight) / 2, height: _barHeight, child: Row( children: [ ColoredBox( color: DmcColors.warnRed.withValues(alpha: _fillAlpha), child: SizedBox(width: redVisible, height: _barHeight), ), ColoredBox( color: DmcColors.pafAmber.withValues(alpha: _fillAlpha), child: SizedBox(width: amberVisible, height: _barHeight), ), ColoredBox( color: DmcColors.flyModeColor.withValues(alpha: _fillAlpha), child: SizedBox(width: greenVisible, height: _barHeight), ), ], ), ), Positioned( left: pillLeft, child: Container( height: _pillHeight, constraints: const BoxConstraints(minWidth: _pillMinWidth), padding: const EdgeInsets.symmetric(horizontal: 6), alignment: Alignment.center, decoration: BoxDecoration( color: pillColor, borderRadius: BorderRadius.circular(_pillHeight / 2), ), child: Text( '$clampedPercent%', style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600, ), ), ), ), ], ); }, ), ); } }