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>
117 lines
5.0 KiB
Dart
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('_', ' '),
|
|
};
|
|
}
|