import 'package:flutter/widgets.dart'; import 'product_profile.dart'; /// One capability-gated piece of UI: a HUD element, settings row or mode tile. /// /// Its [requires] predicate is the only place that decides whether the element /// exists on the connected product — GO 4 does the same with per-product `gone` /// views. Gate a widget with [Requires]; list every feature in /// [HudFeatures.all] so the debug screen and the widget tests see it. class HudFeature { const HudFeature(this.id, this.label, this.group, this.requires); final String id; final String label; /// Where the element lives (`Status bar`, `Camera`, …), for the debug screen. final String group; final bool Function(ProductProfile p) requires; bool enabledFor(ProductProfile p) => requires(p); } abstract final class HudFeatures { // ── Status bar ──────────────────────────────────────────────────────────── static final HudFeature rcHdSignal = HudFeature( 'status.rcHdSignal', 'RC + HD signal bars', 'Status bar', (ProductProfile p) => p.hasRcLink); static final HudFeature wifiSignal = HudFeature( 'status.wifiSignal', 'Wi-Fi signal', 'Status bar', (ProductProfile p) => p.hasWifiLink); static final HudFeature visionIndicator = HudFeature( 'status.vision', 'Vision system icon', 'Status bar', (ProductProfile p) => p.hasVps); static final HudFeature masterSlaveBadge = HudFeature( 'status.masterSlave', 'Master / slave RC badge', 'Status bar', (ProductProfile p) => p.hasMasterSlaveRc); static final HudFeature rtkStatus = HudFeature( 'status.rtk', 'RTK status', 'Status bar', (ProductProfile p) => p.hasRtk); // ── Battery ─────────────────────────────────────────────────────────────── static final HudFeature singleBattery = HudFeature( 'battery.single', 'Single battery bar', 'Battery', (ProductProfile p) => p.batteryCount == 1); static final HudFeature dualBattery = HudFeature( 'battery.dual', 'Dual battery bars', 'Battery', (ProductProfile p) => p.batteryCount == 2); static final HudFeature multiBattery = HudFeature( 'battery.multi', 'Multi-battery summary', 'Battery', (ProductProfile p) => p.batteryCount > 2); static final HudFeature nonSmartBattery = HudFeature( 'battery.nonSmart', 'Cell-voltage actions (non-smart)', 'Battery', (ProductProfile p) => p.batteryKind == BatteryKind.nonSmart); // ── Camera ──────────────────────────────────────────────────────────────── static final HudFeature cameraControls = HudFeature( 'camera.controls', 'Shutter + mode column', 'Camera', (ProductProfile p) => p.hasCamera); static final HudFeature apertureReadout = HudFeature( 'camera.aperture', 'Aperture readout + wheel', 'Camera', (ProductProfile p) => p.camera?.adjustableApertureSupported == true); static final HudFeature zoomColumn = HudFeature( 'camera.zoom', 'Zoom column', 'Camera', (ProductProfile p) => p.hasZoom); static final HudFeature ssdStatus = HudFeature( 'camera.ssd', 'SSD status', 'Camera', (ProductProfile p) => p.hasSsd); static final HudFeature thermalReadout = HudFeature( 'camera.thermal', 'Thermal readouts', 'Camera', (ProductProfile p) => p.hasThermal); static final HudFeature spotMetering = HudFeature( 'camera.metering', 'Tap-to-meter', 'Camera', (ProductProfile p) => p.camera?.meteringSupported == true); static final HudFeature tapFocus = HudFeature( 'camera.focus', 'Tap-to-focus', 'Camera', (ProductProfile p) => p.camera?.adjustableFocalPointSupported == true); static final HudFeature playback = HudFeature('camera.playback', 'Playback button', 'Camera', (ProductProfile p) => p.camera?.mediaDownloadSupported == true || p.camera?.playbackSupported == true); static final HudFeature histogram = HudFeature( 'camera.histogram', 'Histogram', 'Camera', (ProductProfile p) => p.hasCamera); // ── Gimbal ──────────────────────────────────────────────────────────────── static final HudFeature gimbalPitch = HudFeature( 'gimbal.pitch', 'Gimbal pitch slider', 'Gimbal', (ProductProfile p) => p.gimbal?.pitchAdjustable == true); static final HudFeature gimbalYaw = HudFeature( 'gimbal.yaw', 'Gimbal yaw (drag, recenter, Free mode)', 'Gimbal', (ProductProfile p) => p.gimbal?.yawAdjustable == true); static final HudFeature gimbalPitchExtension = HudFeature( 'gimbal.pitchExtension', 'Extend pitch to +30°', 'Gimbal', (ProductProfile p) => p.gimbal?.pitchRangeExtensionSupported == true); static final HudFeature gimbalDrag = HudFeature( 'gimbal.drag', 'Drag on video to aim', 'Gimbal', (ProductProfile p) => p.gimbal?.pitchAdjustable == true); static final HudFeature gimbalSettings = HudFeature( 'gimbal.settings', 'Gimbal settings (mode, recenter)', 'Gimbal', (ProductProfile p) => p.hasGimbal); static final HudFeature gimbalRollAdjust = HudFeature( 'gimbal.rollAdjust', 'Adjust gimbal roll', 'Gimbal', (ProductProfile p) => p.hasGimbal); static final HudFeature gimbalCalibration = HudFeature( 'gimbal.calibration', 'Auto calibrate gimbal', 'Gimbal', (ProductProfile p) => p.hasGimbal); static final HudFeature gimbalAdvanced = HudFeature('gimbal.advanced', 'Gimbal speed / smoothness', 'Gimbal', (ProductProfile p) => p.gimbal?.controller.isNotEmpty == true); // ── Vision / control ────────────────────────────────────────────────────── static final HudFeature vpsReadout = HudFeature( 'telemetry.vps', 'VPS height', 'Telemetry', (ProductProfile p) => p.hasVps); static final HudFeature visionRadar = HudFeature( 'overlay.radar', 'Vision radar', 'Overlays', (ProductProfile p) => p.hasObstacleSensing); static final HudFeature virtualSticks = HudFeature( 'control.virtualSticks', 'On-screen sticks', 'Control', (ProductProfile p) => p.supportsVirtualStick); /// Precision take-off records the spot with the downward cameras. static final HudFeature precisionTakeoff = HudFeature( 'control.precisionTakeoff', 'Precision Takeoff option', 'Control', (ProductProfile p) => p.hasVps); /// ADS-B In: the receiver's own reports decide at runtime; the profile only rules out non-aircraft. static final HudFeature airSense = HudFeature( 'overlay.airSense', 'AirSense (ADS-B) warnings', 'Overlays', (ProductProfile p) => p.isAircraft); /// DJI GEO zones: the database is app-level, every aircraft gets it. static final HudFeature flyZones = HudFeature( 'overlay.flyZones', 'Fly zones (GEO) warnings + map', 'Overlays', (ProductProfile p) => p.isAircraft); static final HudFeature simulator = HudFeature( 'control.simulator', 'Simulator toggle', 'Control', (ProductProfile p) => p.hasSimulator); // ── Settings sheet (plan P6) ────────────────────────────────────────────── static final HudFeature settingsVision = HudFeature( 'settings.vision', 'Vision settings tab', 'Settings', (ProductProfile p) => p.hasVps); static final HudFeature settingsObstacle = HudFeature( 'settings.obstacle', 'Obstacle avoidance settings', 'Settings', (ProductProfile p) => p.hasObstacleSensing); /// Transmission rows address the link's own key class, so each set is gated /// on the link type. /// Gesture and palm control ride on ActiveTrack's vision. static final HudFeature settingsGestures = HudFeature('settings.gestures', 'Gesture & palm control', 'Settings', (ProductProfile p) => p.supportsMode(IntelligentMode.activeTrack)); static final HudFeature linkOcuSync = HudFeature( 'settings.ocusync', 'OcuSync transmission settings', 'Settings', (ProductProfile p) => p.link == LinkType.ocusync); static final HudFeature linkLightbridge = HudFeature('settings.lightbridge', 'Lightbridge transmission settings', 'Settings', (ProductProfile p) => p.link == LinkType.lightbridge); static final HudFeature linkWifi = HudFeature( 'settings.wifi', 'Wi-Fi transmission settings', 'Settings', (ProductProfile p) => p.link == LinkType.wifi); // ── Calibration & maintenance wizards (plan P6b) ────────────────────────── // Runtime support (`CalibrationState`) can still drop a wizard the profile allows. static final HudFeature calibrationCompass = HudFeature( 'calibration.compass', 'Compass calibration', 'Calibration', (ProductProfile p) => p.isAircraft); static final HudFeature calibrationImu = HudFeature( 'calibration.imu', 'IMU calibration', 'Calibration', (ProductProfile p) => p.isAircraft); static final HudFeature calibrationRc = HudFeature( 'calibration.rc', 'Remote controller calibration', 'Calibration', (ProductProfile p) => p.remoteController != null); static final HudFeature gimbalAdjust = HudFeature( 'calibration.gimbalAdjust', 'Adjust gimbal (horizon)', 'Calibration', (ProductProfile p) => p.hasGimbal); static final HudFeature calibrationLens = HudFeature('calibration.lens', 'Lens calibration', 'Calibration', (ProductProfile p) => p.camera?.interchangeableLensSupported == true); static final HudFeature sensorCleaning = HudFeature('calibration.sensorCleaning', 'Sensor cleaning', 'Calibration', (ProductProfile p) => p.camera?.interchangeableLensSupported == true); static final HudFeature calibrationGravityCenter = HudFeature( 'calibration.gravityCenter', 'Center of gravity calibration', 'Calibration', (ProductProfile p) => p.isAircraft); static final HudFeature calibrationPropeller = HudFeature('calibration.propeller', 'Propeller calibration', 'Calibration', (ProductProfile p) => p.flightController?.propellerCalibrationSupported == true); static final HudFeature visionSensorGuide = HudFeature( 'calibration.vision', 'Vision sensor calibration (guide)', 'Calibration', (ProductProfile p) => p.hasVps); // ── Intelligent flight modes (plan P7) ──────────────────────────────────── static final HudFeature modePicker = HudFeature( 'mode.picker', 'Intelligent Flight Modes button', 'Intelligent modes', (ProductProfile p) => p.modes.isNotEmpty); static final List modes = [ for (final IntelligentMode m in IntelligentMode.values) HudFeature('mode.${m.name}', _modeLabel(m), 'Intelligent modes', (ProductProfile p) => p.supportsMode(m)), ]; static final List all = [ rcHdSignal, wifiSignal, visionIndicator, masterSlaveBadge, rtkStatus, singleBattery, dualBattery, multiBattery, nonSmartBattery, cameraControls, apertureReadout, zoomColumn, ssdStatus, thermalReadout, spotMetering, tapFocus, playback, histogram, gimbalPitch, gimbalYaw, gimbalPitchExtension, gimbalDrag, gimbalSettings, gimbalRollAdjust, gimbalCalibration, gimbalAdvanced, vpsReadout, visionRadar, virtualSticks, airSense, flyZones, precisionTakeoff, simulator, settingsVision, settingsObstacle, settingsGestures, linkOcuSync, linkLightbridge, linkWifi, calibrationCompass, calibrationImu, calibrationRc, gimbalAdjust, calibrationLens, sensorCleaning, calibrationGravityCenter, calibrationPropeller, visionSensorGuide, modePicker, ...modes, ]; static String _modeLabel(IntelligentMode m) => switch (m) { IntelligentMode.waypoint => 'Waypoints', IntelligentMode.waypointV2 => 'Waypoints V2', IntelligentMode.pointOfInterest => 'Point of Interest', IntelligentMode.activeTrack => 'ActiveTrack', IntelligentMode.tapFly => 'TapFly', IntelligentMode.followMe => 'Follow Me', IntelligentMode.quickShots => 'QuickShots', IntelligentMode.courseLock => 'Course Lock', IntelligentMode.homeLock => 'Home Lock', IntelligentMode.tripod => 'Tripod', IntelligentMode.hyperlapse => 'Hyperlapse', }; } /// Makes the active [ProductProfile] available to every [Requires] below it. class ProductProfileScope extends InheritedWidget { const ProductProfileScope({super.key, required this.profile, required super.child}); final ProductProfile profile; /// The nearest scope's profile, or [ProductProfile.disconnected] when none. static ProductProfile of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()?.profile ?? ProductProfile.disconnected; @override bool updateShouldNotify(ProductProfileScope oldWidget) => !identical(profile, oldWidget.profile); } /// Shows [child] only when [feature] is enabled for the scope's profile. /// /// With no product connected there is nothing to be capable *of*, so the gate /// is open by default and the HUD keeps its full layout as a preview; pass /// `whenDisconnected: false` for elements that must never appear without a /// product. class Requires extends StatelessWidget { const Requires(this.feature, {super.key, required this.child, this.fallback, this.whenDisconnected = true}); final HudFeature feature; final Widget child; final Widget? fallback; final bool whenDisconnected; @override Widget build(BuildContext context) { final ProductProfile p = ProductProfileScope.of(context); final bool on = p.connected ? feature.enabledFor(p) : whenDisconnected; return on ? child : (fallback ?? const SizedBox.shrink()); } }