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>
115 lines
4.1 KiB
Dart
115 lines
4.1 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../../../dji_service.dart';
|
||
import '../../../fly/hud_features.dart';
|
||
import '../../../fly/product_profile.dart';
|
||
import '../../../fly/state/camera_state.dart';
|
||
import '../../../theme.dart';
|
||
import '../hud_kit.dart';
|
||
|
||
/// T / W / factor / 1× for zoom cameras (plan P4, GO 4's
|
||
/// `camera_zoom_focus_view`). Gate it with [HudFeatures.zoomColumn].
|
||
///
|
||
/// Optical-only zoom cameras (Z30) zoom continuously while T or W is held;
|
||
/// hybrid cameras (Mavic 2 Zoom) step through the focal range — an eighth per
|
||
/// press — because the SDK's continuous zoom stops at the optical end.
|
||
class ZoomColumn extends StatelessWidget {
|
||
const ZoomColumn({super.key, required this.camera, required this.dji, required this.onError});
|
||
|
||
final CameraState camera;
|
||
final DjiService dji;
|
||
final void Function(String label, Object error) onError;
|
||
|
||
Future<void> _run(String label, Future<void> Function() call) async {
|
||
try {
|
||
await call();
|
||
} catch (e) {
|
||
onError(label, e);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final CameraProfile? c = ProductProfileScope.of(context).camera;
|
||
final bool continuous = c != null && c.opticalZoomSupported && !c.hybridZoomSupported;
|
||
return ListenableBuilder(
|
||
listenable: camera,
|
||
builder: (BuildContext context, _) {
|
||
final ZoomInfo? z = camera.zoom;
|
||
Widget button(String label, bool zoomIn) {
|
||
final Key key = ValueKey<String>(zoomIn ? 'zoom-in' : 'zoom-out');
|
||
if (continuous) {
|
||
return _ZoomButton(
|
||
key: key,
|
||
label: label,
|
||
onPress: () => _run('Zoom', () => dji.startContinuousZoom(zoomIn ? 'in' : 'out')),
|
||
onRelease: () => _run('Zoom', dji.stopContinuousZoom),
|
||
);
|
||
}
|
||
return _ZoomButton(
|
||
key: key,
|
||
label: label,
|
||
onTap: z == null ? null : () => _run('Zoom', () => dji.setZoomFocalLength(z.stepped(zoomIn))),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
width: 40,
|
||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||
decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(10)),
|
||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||
button('T', true),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
z == null ? '—' : '${z.factor.toStringAsFixed(1)}×',
|
||
key: const ValueKey<String>('zoom-factor'),
|
||
style: HudText.monoSmall.copyWith(fontWeight: FontWeight.w700, color: z?.digital == true ? HudColors.caution : Glass.ink),
|
||
),
|
||
const SizedBox(height: 4),
|
||
button('W', false),
|
||
const SizedBox(height: 6),
|
||
_ZoomButton(
|
||
key: const ValueKey<String>('zoom-reset'),
|
||
label: '1×',
|
||
small: true,
|
||
onTap: z == null || z.focalLength == z.min ? null : () => _run('Zoom', () => dji.setZoomFocalLength(z.min)),
|
||
),
|
||
]),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ZoomButton extends StatelessWidget {
|
||
const _ZoomButton({super.key, required this.label, this.onTap, this.onPress, this.onRelease, this.small = false});
|
||
|
||
final String label;
|
||
final VoidCallback? onTap;
|
||
final VoidCallback? onPress;
|
||
final VoidCallback? onRelease;
|
||
final bool small;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final bool enabled = onTap != null || onPress != null;
|
||
return Opacity(
|
||
opacity: enabled ? 1 : 0.4,
|
||
child: GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: onTap,
|
||
onTapDown: onPress == null ? null : (_) => onPress!(),
|
||
onTapUp: onRelease == null ? null : (_) => onRelease!(),
|
||
onTapCancel: onRelease,
|
||
child: Container(
|
||
width: 32,
|
||
height: small ? 24 : 32,
|
||
decoration: BoxDecoration(color: Glass.pillStrong, borderRadius: BorderRadius.circular(8), border: Border.all(color: Glass.hairline)),
|
||
alignment: Alignment.center,
|
||
child: Text(label, style: (small ? HudText.monoSmall : HudText.value).copyWith(fontWeight: FontWeight.w700)),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|