Files
PilotVault/Fly App/lib/ui/fly/top/camera_info_strip.dart
T
tajniak81andClaude Opus 5.5 c1770fff73 Give the Fly App GO 4-style camera controls
One settings table in CameraBridge watches every camera value into the
telemetry tick and backs a generic setCameraValue, so the tabbed camera
menu (Exposure / Photo / Video / Other) is built from row specs and the
camera's own capability ranges. Writes show at once and fall back to what
the camera reports. Adds the quick exposure panel (Auto/A/S/M, ISO slider,
shutter and aperture wheels, EV strip, metered EV in M), a new camera
column with record timer and interval/pano progress, a zoom column for
zoom cameras, tap-to-focus / tap-to-meter, and a live histogram.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:57:38 +02:00

117 lines
5.0 KiB
Dart

import 'package:flutter/material.dart';
import '../../../fly/hud_features.dart';
import '../../../fly/hud_geometry.dart';
import '../../../fly/state/camera_state.dart';
import '../../../theme.dart';
import '../hud_kit.dart';
/// Row 2 of the flight HUD (plan P2): storage · ISO · shutter · aperture · EV ·
/// REC time or what is left to shoot · colour profile · AE lock — plus the SSD
/// chip and the thermal spot reading on the cameras that have them.
class CameraInfoStrip extends StatelessWidget {
const CameraInfoStrip({super.key, required this.camera, this.onTap});
final CameraState camera;
/// Opens the quick exposure panel (plan P4), as tapping GO 4's parameter bar does.
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Requires(
HudFeatures.cameraControls,
child: ListenableBuilder(
listenable: camera,
builder: (BuildContext context, _) {
final CameraState c = camera;
final StorageInfo? sd = c.storage;
final SsdInfo? ssd = c.ssd;
final String? colour = _colourLabel(c.colorProfile);
return GlassPill(
key: const ValueKey<String>('camera-info'),
height: 22,
onTap: onTap,
child: Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
_sd(sd),
Requires(HudFeatures.ssdStatus, whenDisconnected: false, child: _ssd(ssd)),
_field('ISO', c.iso),
_field('SHUTTER', c.shutter),
Requires(HudFeatures.apertureReadout, child: _field('F', c.aperture?.replaceFirst('f/', ''))),
_field('EV', c.ev),
Requires(HudFeatures.thermalReadout, whenDisconnected: false, child: _field('SPOT', _celsius(c.thermalSpot))),
_divider(),
_capture(c),
if (colour != null) ...<Widget>[_divider(), Text(colour, style: HudText.monoSmall)],
if (c.aeLock == true) ...<Widget>[
_divider(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(color: Glass.accent, borderRadius: BorderRadius.circular(4)),
child: Text('AE-L', style: HudText.monoSmall.copyWith(color: Colors.white, fontWeight: FontWeight.w700)),
),
],
]),
);
},
),
);
}
Widget _sd(StorageInfo? sd) {
final String text = sd == null ? 'SD —' : (sd.status == 'OK' ? 'SD' : sd.status);
final Color color = sd == null ? HudText.dim : (sd.usable ? Glass.ink : HudColors.danger);
return Text(text, style: HudText.monoSmall.copyWith(color: color, fontWeight: FontWeight.w700));
}
Widget _ssd(SsdInfo? ssd) {
final String text = ssd == null ? 'SSD —' : (ssd.status == 'OK' ? 'SSD' : ssd.status);
final Color color = ssd == null ? HudText.dim : (ssd.usable ? Glass.ink : HudColors.danger);
return Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(text, style: HudText.monoSmall.copyWith(color: color, fontWeight: FontWeight.w700)),
);
}
/// REC dot + elapsed time while recording; otherwise what the card still
/// holds — shots in photo mode, recording time in video mode.
Widget _capture(CameraState c) {
if (c.isRecording) {
return Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Glass.rec, shape: BoxShape.circle)),
const SizedBox(width: 4),
Text(formatDuration(c.recordSeconds), style: HudText.monoSmall.copyWith(color: Glass.rec, fontWeight: FontWeight.w700)),
]);
}
final StorageInfo? sd = c.storage;
// On SSD cameras the video goes to the SSD, so that is the time that counts.
final int? recordLeft = c.ssd?.availableRecordingSeconds ?? sd?.availableRecordingSeconds;
final String left = c.isVideoMode
? formatDuration(recordLeft)
: (sd?.availableCaptureCount == null ? '—' : '${sd!.availableCaptureCount}');
return Text(left, style: HudText.monoSmall);
}
Widget _field(String label, String? value) => Padding(
padding: const EdgeInsets.only(left: 8),
child: Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
Text(label, style: HudText.label),
const SizedBox(width: 3),
Text(value ?? '—', style: HudText.monoSmall),
]),
);
Widget _divider() => Container(width: 1, height: 10, margin: const EdgeInsets.symmetric(horizontal: 7), color: HudColors.off);
static String? _celsius(double? t) => t == null ? null : '${t.toStringAsFixed(1)}°C';
/// `NONE` is the default look and not worth a chip; the rest read as GO 4 names them.
static String? _colourLabel(String? p) => switch (p) {
null || 'NONE' || 'UNKNOWN' => null,
'D_LOG' => 'D-Log',
'D_CINELIKE' => 'D-Cinelike',
'HLG' => 'HLG',
_ => p.replaceAll('_', ' '),
};
}