Files
PilotVault/Fly App/lib/ui/fly/right/zoom_column.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

115 lines
4.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)),
),
),
);
}
}