From 2db557a3b41a4b6f88457a6e23cf2829be9366a5 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:36:17 +0200 Subject: [PATCH] Retire the classic flight screen and stop the start-up location grant being dropped MainActivity now makes the app's only start-up permission request and tells Dart when it finishes; the phone-location stream waits for that instead of asking in parallel, which made Android drop the location grant. The P3 simulator run passed, so the pre-P2 FlightControlPage, the pages only it used and its Settings toggle are gone. The app smoke test checks the home screen again instead of the login gate removed in July. Co-Authored-By: Claude Opus 5.5 --- .../com/pilotvault/flyapp/DjiSdkBridge.kt | 9 + .../com/pilotvault/flyapp/MainActivity.kt | 15 +- Fly App/lib/fly/fly_prefs.dart | 11 - Fly App/lib/main.dart | 31 +- Fly App/lib/ui/camera_settings_page.dart | 236 -------- Fly App/lib/ui/flight_control_page.dart | 573 ------------------ Fly App/lib/ui/fly/bottom/minimap.dart | 2 +- Fly App/lib/ui/fly/fly_screen.dart | 2 +- .../fly/panels/settings/settings_sheet.dart | 9 - Fly App/lib/ui/map_page.dart | 238 -------- Fly App/test/widget_test.dart | 12 +- 11 files changed, 50 insertions(+), 1088 deletions(-) delete mode 100644 Fly App/lib/ui/camera_settings_page.dart delete mode 100644 Fly App/lib/ui/flight_control_page.dart delete mode 100644 Fly App/lib/ui/map_page.dart diff --git a/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/DjiSdkBridge.kt b/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/DjiSdkBridge.kt index 10be3ff..32f2db4 100644 --- a/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/DjiSdkBridge.kt +++ b/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/DjiSdkBridge.kt @@ -198,6 +198,15 @@ class DjiSdkBridge( mainHandler.post { eventSink?.success(event) } } + /** + * MainActivity's start-up permission request finished. Dart must not ask for + * location itself while that dialog is up (Android allows one request at a + * time and drops the other's grant), so it waits for this event instead. + */ + fun onPermissionsResult(locationGranted: Boolean) { + emit(mapOf("type" to "permissions", "location" to locationGranted)) + } + // ── DJI SDK registration & product lifecycle ───────────────────────────────── private fun registerApp() { diff --git a/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/MainActivity.kt b/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/MainActivity.kt index 7d01232..facc6d9 100644 --- a/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/MainActivity.kt +++ b/Fly App/android/app/src/main/kotlin/com/pilotvault/flyapp/MainActivity.kt @@ -31,7 +31,9 @@ class MainActivity : FlutterFragmentActivity() { /** * The DJI SDK needs these dangerous permissions granted before it can connect * to / communicate with a product. We request them up front from the host - * Activity so the Flutter UI can stay focused on the SDK flow. + * Activity so the Flutter UI can stay focused on the SDK flow. This is the + * app's only start-up permission request: Dart waits for the result (see + * [DjiSdkBridge.onPermissionsResult]) rather than asking in parallel. */ private fun requestDjiPermissions() { val missing = REQUIRED_PERMISSIONS.filter { @@ -42,6 +44,17 @@ class MainActivity : FlutterFragmentActivity() { } } + @Deprecated("Deprecated in Java") + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode != PERMISSION_REQUEST_CODE) return + val location = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == + PackageManager.PERMISSION_GRANTED + bridge?.onPermissionsResult(location) + } + companion object { private const val PERMISSION_REQUEST_CODE = 12321 diff --git a/Fly App/lib/fly/fly_prefs.dart b/Fly App/lib/fly/fly_prefs.dart index c4dca24..c96089b 100644 --- a/Fly App/lib/fly/fly_prefs.dart +++ b/Fly App/lib/fly/fly_prefs.dart @@ -31,7 +31,6 @@ enum LongPressAction { /// Device-local switches for the flight screen, persisted across launches. abstract final class FlyPrefs { - static const String _classicKey = 'fly.classicHud'; static const String _perfKey = 'fly.perfOverlay'; static const String _gridKey = 'fly.grid'; static const String _centerKey = 'fly.centerPoint'; @@ -52,10 +51,6 @@ abstract final class FlyPrefs { static const String _lensCalibratedKey = 'fly.lensCalibrated'; static const String _tutorialsKey = 'fly.modeTutorials'; - /// Open the pre-P2 `FlightControlPage` instead of the HUD v2 — kept - /// reachable until P3's safety flows pass their simulator run (plan P2/P3). - static final ValueNotifier classicHud = ValueNotifier(false); - /// Flutter's performance overlay on the flight screen, for the P2 "no jank at /// 10 Hz" check. static final ValueNotifier perfOverlay = ValueNotifier(false); @@ -114,7 +109,6 @@ abstract final class FlyPrefs { static Future load() async { try { _prefs = await SharedPreferences.getInstance(); - classicHud.value = _prefs!.getBool(_classicKey) ?? false; perfOverlay.value = _prefs!.getBool(_perfKey) ?? false; grid.value = _byName(GridStyle.values, _prefs!.getString(_gridKey)) ?? GridStyle.off; centerPoint.value = _prefs!.getBool(_centerKey) ?? true; @@ -139,11 +133,6 @@ abstract final class FlyPrefs { } } - static Future setClassicHud(bool on) async { - classicHud.value = on; - await _prefs?.setBool(_classicKey, on); - } - static Future setPerfOverlay(bool on) async { perfOverlay.value = on; await _prefs?.setBool(_perfKey, on); diff --git a/Fly App/lib/main.dart b/Fly App/lib/main.dart index a427433..c54291a 100644 --- a/Fly App/lib/main.dart +++ b/Fly App/lib/main.dart @@ -19,7 +19,6 @@ import 'pb_auth.dart'; import 'theme.dart'; import 'ui/academy_page.dart'; import 'ui/album_page.dart'; -import 'ui/flight_control_page.dart'; import 'ui/flight_logs_page.dart'; import 'ui/fly/fly_screen.dart'; import 'ui/home_page.dart'; @@ -108,16 +107,19 @@ class _HomePageState extends State { /// server as a telemetry field — a location fallback for the Web App's auto /// bounding box when the drone has no fix. Best-effort: silently gives up if /// location services or permission are unavailable. + /// + /// It never asks for permission: MainActivity asks for every start-up + /// permission in one request, and a second request in parallel makes Android + /// drop the location grant. A `permissions` event retries once that is done. Future _startPhoneLocation() async { + if (_phoneLocSub != null) return; try { if (!await Geolocator.isLocationServiceEnabled()) return; - LocationPermission perm = await Geolocator.checkPermission(); - if (perm == LocationPermission.denied) { - perm = await Geolocator.requestPermission(); - } + final LocationPermission perm = await Geolocator.checkPermission(); if (perm == LocationPermission.denied || perm == LocationPermission.deniedForever) { return; } + if (_phoneLocSub != null) return; _phoneLocSub = Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, // country-level is all the bbox needs @@ -161,6 +163,9 @@ class _HomePageState extends State { // Telemetry, camera, diagnostics and mission state go to the sub-models. if (_telemetry.apply(event)) return; switch (event['type'] as String?) { + case 'permissions': + if (event['location'] == true) _startPhoneLocation(); + return; case 'registration': switch (event['state'] as String?) { case 'registering': @@ -380,15 +385,13 @@ class _HomePageState extends State { void _goFly() { Navigator.of(context).push(MaterialPageRoute( - builder: (_) => FlyPrefs.classicHud.value - ? FlightControlPage(model: _model, dji: _dji) - : FlyScreen( - model: _model, - dji: _dji, - onOpenFlightLogs: () => Navigator.of(context).push(MaterialPageRoute( - builder: (_) => FlightLogsPage(log: _flightLog, locator: _locator, model: _model), - )), - ), + builder: (_) => FlyScreen( + model: _model, + dji: _dji, + onOpenFlightLogs: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => FlightLogsPage(log: _flightLog, locator: _locator, model: _model), + )), + ), )); } diff --git a/Fly App/lib/ui/camera_settings_page.dart b/Fly App/lib/ui/camera_settings_page.dart deleted file mode 100644 index 0008c0e..0000000 --- a/Fly App/lib/ui/camera_settings_page.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../dji_service.dart'; -import '../flight_model.dart'; -import '../theme.dart'; -import 'flight_overlay_scaffold.dart'; -import 'pv_icons.dart'; - -/// Pro exposure controls — mirrors the v2 "Camera settings" mockup. Auto/Pro -/// tabs switch exposure program; the five dials (ISO / shutter / aperture / EV / -/// WB) each drive the matching SDK setter, and the value scrubber picks a value. -class CameraSettingsPage extends StatefulWidget { - const CameraSettingsPage({super.key, required this.model, required this.dji}); - - final FlightModel model; - final DjiService dji; - - @override - State createState() => _CameraSettingsPageState(); -} - -class _CameraSettingsPageState extends State { - DjiService get _dji => widget.dji; - FlightModel get _m => widget.model; - - // (key, icon, values). Values mirror the kit scrubber; the SDK rejects any it - // doesn't support (surfaced as a snackbar), so a superset is safe. - static const List<(String, String, List)> _dials = <(String, String, List)>[ - ('ISO', 'iso', ['AUTO', '100', '200', '400', '800', '1600', '3200', '6400']), - ('Shutter', 'shutter', ['1/2000', '1/1000', '1/500', '1/240', '1/120', '1/60', '1/30', '1/15', '1/8']), - ('Aperture', 'aperture', ['f/2.8', 'f/4', 'f/5.6', 'f/8', 'f/11']), - ('EV', 'ev', ['-2.0', '-1.3', '-0.7', '-0.3', '0.0', '+0.3', '+0.7', '+1.3', '+2.0']), - ('WB', 'wb', ['AUTO', '2700K', '4000K', '5200K', '5800K', '6500K']), - ]; - - int _active = 0; // selected dial - - bool get _pro => _m.exposureProgram == ExposureProgram.pro; - - void _snack(String msg) { - ScaffoldMessenger.of(context) - ..clearSnackBars() - ..showSnackBar(SnackBar(content: Text(msg))); - } - - Future _run(String label, Future Function() action) async { - try { - await action(); - } catch (e) { - _snack('$label: ${e is PlatformException ? (e.message ?? e.code) : e}'); - } - } - - String? _valueFor(String key) => switch (key) { - 'ISO' => _m.iso, - 'Shutter' => _m.shutter, - 'Aperture' => _m.aperture, - 'EV' => _m.ev, - 'WB' => _m.whiteBalance, - _ => null, - }; - - Future _setValue(String key, String value) async { - setState(() { - switch (key) { - case 'ISO': - _m.iso = value; - case 'Shutter': - _m.shutter = value; - case 'Aperture': - _m.aperture = value; - case 'EV': - _m.ev = value; - case 'WB': - _m.whiteBalance = value; - } - }); - switch (key) { - case 'ISO': - await _run('ISO', () => _dji.setISO(value)); - case 'Shutter': - await _run('Shutter', () => _dji.setShutterSpeed(value)); - case 'Aperture': - await _run('Aperture', () => _dji.setAperture(value)); - case 'EV': - await _run('EV', () => _dji.setEV(value)); - case 'WB': - await _run('White balance', () => _dji.setWhiteBalance(value)); - } - } - - Future _setProgram(bool pro) async { - setState(() => _m.exposureProgram = pro ? ExposureProgram.pro : ExposureProgram.auto); - await _run('Exposure', () => _dji.setExposureProgram(pro ? 'pro' : 'auto')); - } - - @override - Widget build(BuildContext context) { - return FlightOverlayScaffold( - title: 'Camera settings', - trailing: _autoProTabs(), - body: Column( - children: [ - Align( - alignment: Alignment.centerRight, - child: Container( - width: 132, height: 48, - padding: const EdgeInsets.all(6), - decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(10)), - child: const CustomPaint(size: Size.infinite, painter: _HistogramPainter()), - ), - ), - const Spacer(), - _dialRail(), - ], - ), - ); - } - - Widget _autoProTabs() { - return Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(10)), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - _tab('Auto', !_pro, () => _setProgram(false)), - _tab('Pro', _pro, () => _setProgram(true)), - ]), - ); - } - - Widget _tab(String label, bool active, VoidCallback onTap) { - return GestureDetector( - onTap: onTap, - child: Container( - height: 28, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration(color: active ? Glass.accent : Colors.transparent, borderRadius: BorderRadius.circular(7)), - alignment: Alignment.center, - child: Text(label, style: const TextStyle(fontFamily: PV.fontSans, fontSize: 12, fontWeight: FontWeight.w700, color: Glass.ink)), - ), - ); - } - - Widget _dialRail() { - final (String key, _, List values) = _dials[_active]; - final String? current = _valueFor(key); - return Container( - padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), - decoration: BoxDecoration( - color: Glass.pill, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0x1AFFFFFF)), - ), - child: Column(children: [ - Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - for (int i = 0; i < _dials.length; i++) _dial(i), - ]), - const SizedBox(height: 12), - Opacity( - opacity: _pro ? 1 : 0.4, - child: IgnorePointer( - ignoring: !_pro, - child: SizedBox( - height: 30, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - for (final String v in values) _scrubValue(key, v, v == current), - ], - ), - ), - ), - ), - ]), - ); - } - - Widget _dial(int i) { - final (String key, String icon, _) = _dials[i]; - final bool sel = i == _active; - final String value = _valueFor(key) ?? '—'; - return GestureDetector( - onTap: () => setState(() => _active = i), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - PVIcon(icon, size: 17, color: sel ? const Color(0xFF8FB4F6) : const Color(0xFF8FA0BE)), - const SizedBox(height: 4), - Text(key, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 9.5, letterSpacing: 0.8, color: Color(0x99EAF0FA))), - Text(value, style: TextStyle(fontFamily: PV.fontMono, fontSize: 15, fontWeight: FontWeight.w700, color: sel ? const Color(0xFF8FB4F6) : Glass.ink)), - ]), - ); - } - - Widget _scrubValue(String key, String v, bool sel) { - return GestureDetector( - onTap: () => _setValue(key, v), - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text(v, - style: TextStyle( - fontFamily: PV.fontMono, - fontSize: sel ? 14 : 12, - fontWeight: sel ? FontWeight.w700 : FontWeight.w400, - color: sel ? const Color(0xFF5B93F5) : const Color(0x99EAF0FA), - )), - ), - ); - } -} - -class _HistogramPainter extends CustomPainter { - const _HistogramPainter(); - @override - void paint(Canvas canvas, Size size) { - final Path p = Path() - ..moveTo(0, size.height) - ..quadraticBezierTo(size.width * 0.2, size.height * 0.2, size.width * 0.4, size.height * 0.6) - ..quadraticBezierTo(size.width * 0.6, size.height * 0.05, size.width * 0.8, size.height * 0.5) - ..quadraticBezierTo(size.width * 0.9, size.height * 0.8, size.width, size.height * 0.9); - canvas.drawPath( - Path.from(p) - ..lineTo(size.width, size.height) - ..lineTo(0, size.height) - ..close(), - Paint()..color = const Color(0x407FE0B0), - ); - canvas.drawPath(p, Paint() - ..color = const Color(0xFF7FE0B0) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.4); - } - - @override - bool shouldRepaint(covariant _HistogramPainter oldDelegate) => false; -} diff --git a/Fly App/lib/ui/flight_control_page.dart b/Fly App/lib/ui/flight_control_page.dart deleted file mode 100644 index 9e64fee..0000000 --- a/Fly App/lib/ui/flight_control_page.dart +++ /dev/null @@ -1,573 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../dji_service.dart'; -import '../flight_model.dart'; -import '../fly/hud_features.dart'; -import '../fly/product_profile.dart'; -import '../theme.dart'; -import 'camera_settings_page.dart'; -import 'capture_modes_page.dart'; -import 'dji_video_view.dart'; -import 'fly/panels/settings/settings_sheet.dart'; -import 'map_page.dart'; -import 'pv_icons.dart'; - -/// Landscape live-flight overlay — mirrors the v2 "Main flight view" mockup. -/// Live camera feed behind a glass HUD; real telemetry bound throughout, and -/// the shutter / mode switch / gimbal slider / RTH / take-off controls issue -/// real SDK commands via [DjiService]. -/// -/// Retired by the HUD v2 (`fly/fly_screen.dart`, plan P2); still reachable via -/// Settings → About → Classic flight screen until the P3 safety flows land. -class FlightControlPage extends StatefulWidget { - const FlightControlPage({super.key, required this.model, required this.dji}); - - final FlightModel model; - final DjiService dji; - - @override - State createState() => _FlightControlPageState(); -} - -class _FlightControlPageState extends State { - Timer? _recTimer; - int _recSeconds = 0; - // Gimbal pitch slider: fraction 0 (top, +30°) … 1 (bottom, −90°). - double _gimbalFrac = 0.24; - bool _grid = false; - - DjiService get _dji => widget.dji; - FlightModel get _m => widget.model; - - @override - void initState() { - super.initState(); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - } - - @override - void dispose() { - _recTimer?.cancel(); - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - super.dispose(); - } - - String _fmt(int s) => '${(s ~/ 60).toString().padLeft(2, '0')}:${(s % 60).toString().padLeft(2, '0')}'; - - void _snack(String msg) { - if (!mounted) return; - ScaffoldMessenger.of(context) - ..clearSnackBars() - ..showSnackBar(SnackBar(content: Text(msg))); - } - - Future _run(String label, Future Function() action) async { - try { - await action(); - } catch (e) { - _snack('$label failed: ${_reason(e)}'); - } - } - - String _reason(Object e) => e is PlatformException ? (e.message ?? e.code) : e.toString(); - - // ── Commands ─────────────────────────────────────────────────────────────── - Future _toggleShutter() async { - if (_m.captureMode == CaptureMode.video) { - if (_m.isRecording) { - _stopRecTimer(); - await _run('Stop recording', _dji.stopRecordVideo); - } else { - _startRecTimer(); - await _run('Start recording', _dji.startRecordVideo); - } - } else { - await _run('Shoot photo', _dji.startShootPhoto); - } - } - - void _startRecTimer() { - _recSeconds = 0; - _recTimer?.cancel(); - _recTimer = Timer.periodic(const Duration(seconds: 1), (_) { - if (mounted) setState(() => _recSeconds++); - }); - } - - void _stopRecTimer() { - _recTimer?.cancel(); - _recTimer = null; - } - - Future _setCaptureMode(CaptureMode mode) async { - setState(() => _m.captureMode = mode); - final String wire = mode == CaptureMode.video ? 'video' : 'photo'; - await _run('Set camera mode', () => _dji.setCameraMode(wire)); - } - - Future _rth() async { - final bool ok = await _confirm('Return to Home', 'The aircraft will fly back to its recorded home point and land.'); - if (ok) await _run('Return to Home', _dji.startGoHome); - } - - Future _toggleTakeoff() async { - if (_m.isFlying == true) { - final bool ok = await _confirm('Land now', 'The aircraft will descend and land at its current position.'); - if (ok) await _run('Land', _dji.land); - } else { - final bool ok = await _confirm('Take off', 'The aircraft will take off and hover at ~1.2 m.'); - if (ok) await _run('Take off', _dji.takeOff); - } - } - - Future _confirm(String title, String body) async { - final bool? r = await showDialog( - context: context, - builder: (BuildContext ctx) => AlertDialog( - backgroundColor: const Color(0xFF10203F), - title: Text(title, style: const TextStyle(color: Glass.ink, fontFamily: PV.fontSans, fontWeight: FontWeight.w700)), - content: Text(body, style: const TextStyle(color: Color(0xFF8FA0BE), fontFamily: PV.fontSans)), - actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), - FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(title)), - ], - ), - ); - return r ?? false; - } - - /// Fallback pitch range (+30° … −90°) until the profile reports the gimbal's. - static const AxisRange _defaultPitch = AxisRange(-90, 30); - - void _onGimbalDrag(double frac) { - setState(() => _gimbalFrac = frac.clamp(0.0, 1.0)); - final AxisRange r = _m.profile.gimbal?.pitch ?? _defaultPitch; - final double pitch = r.max - _gimbalFrac * (r.max - r.min); // max (top) … min (bottom) - _run('Gimbal', () => _dji.rotateGimbalPitch(pitch)); - } - - void _openCaptureModes() { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => CaptureModesPage(model: _m, dji: _dji), - )); - } - - void _openCameraSettings() { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => CameraSettingsPage(model: _m, dji: _dji), - )); - } - - void _openSettingsMenu() { - showFlySettings(context, model: _m, dji: _dji, onError: (String label, Object e) => _snack('$label failed: $e')); - } - - void _openMap() { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => MapPage(model: _m, dji: _dji), - )); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFF0A1120), - body: AnimatedBuilder( - animation: _m, - builder: (BuildContext context, _) { - return ProductProfileScope( - profile: _m.profile, - child: Stack( - children: [ - Positioned.fill( - child: _m.connected - ? const DjiVideoView() - : const CustomPaint(painter: _FeedPainter()), - ), - if (_grid) const Positioned.fill(child: IgnorePointer(child: CustomPaint(painter: _GridPainter()))), - const Center(child: PVIcon('crosshair', size: 30, stroke: 1.1, color: Color(0xB3FFFFFF))), - Positioned(top: 12, left: 14, right: 14, child: _topBar()), - Positioned(left: 14, top: 58, child: _leftRail()), - Positioned(left: 70, top: 58, bottom: 96, child: Requires(HudFeatures.gimbalPitch, child: _gimbalSlider())), - Positioned(right: 16, top: 0, bottom: 0, child: Center(child: Requires(HudFeatures.cameraControls, child: _cameraControls()))), - Positioned(left: 14, bottom: 12, child: _minimap()), - Positioned(bottom: 14, left: 0, right: 0, child: Center(child: _telemetry())), - Positioned(right: 92, bottom: 20, child: _rthButton()), - Positioned(right: 92, bottom: 72, child: _takeoffButton()), - ], - ), - ); - }, - ), - ); - } - - // ── Top bar ────────────────────────────────────────────────────────────── - Widget _topBar() { - final String mode = (_m.flightMode != null && _m.flightMode!.isNotEmpty) ? _m.flightMode! : 'N'; - return Row(children: [ - GestureDetector( - onTap: () => Navigator.of(context).maybePop(), - child: _pill(child: const PVIcon('chevronLeft', size: 16, color: Glass.ink)), - ), - const SizedBox(width: 8), - Container( - height: 26, - padding: const EdgeInsets.symmetric(horizontal: 11), - decoration: BoxDecoration(color: Glass.accent, borderRadius: BorderRadius.circular(8)), - alignment: Alignment.center, - child: Text(mode, style: const TextStyle(fontFamily: PV.fontSans, fontSize: 12, fontWeight: FontWeight.w700, letterSpacing: 0.4, color: Colors.white)), - ), - const SizedBox(width: 8), - _pill(child: Row(mainAxisSize: MainAxisSize.min, children: [ - const PVIcon('satellite', size: 14, color: Glass.sat), - const SizedBox(width: 4), - _mono(_m.satellites?.toString() ?? '0'), - ])), - const SizedBox(width: 8), - Requires(HudFeatures.visionIndicator, child: Padding( - padding: const EdgeInsets.only(right: 8), - child: _pill(child: const PVIcon('obstacle', size: 13, color: Glass.sat)), - )), - _pill(child: Row(mainAxisSize: MainAxisSize.min, children: [ - const PVIcon('radio', size: 14, color: Glass.ink), - const SizedBox(width: 4), - Requires(HudFeatures.wifiSignal, whenDisconnected: false, fallback: _mono('HD'), child: _mono('Wi-Fi')), - ])), - const Spacer(), - _pill(child: Row(mainAxisSize: MainAxisSize.min, children: [ - _mono('REC'), - const SizedBox(width: 6), - Container(width: 7, height: 7, decoration: BoxDecoration(color: _m.isRecording ? Glass.rec : const Color(0x66D64545), shape: BoxShape.circle)), - const SizedBox(width: 6), - _mono(_fmt(_m.isRecording ? _recSeconds : 0)), - ])), - const SizedBox(width: 8), - _pill(child: Row(mainAxisSize: MainAxisSize.min, children: [ - const PVIcon('battery', size: 16, color: Glass.sat), - const SizedBox(width: 4), - _mono(_m.batteryPercent == null ? '—' : '${_m.batteryPercent}%'), - ])), - const SizedBox(width: 8), - GestureDetector(onTap: _openSettingsMenu, child: _pill(child: const PVIcon('more', size: 16, color: Glass.ink))), - ]); - } - - Widget _leftRail() { - return Column(children: [ - _sideBtn('gimbal', active: true, onTap: _openCameraSettings), - const SizedBox(height: 10), - _sideBtn('sun', onTap: _openCameraSettings), - const SizedBox(height: 10), - _sideBtn('aperture', onTap: _openCaptureModes), - const SizedBox(height: 10), - _sideBtn('grid', active: _grid, onTap: () => setState(() => _grid = !_grid)), - ]); - } - - // ── Reusable glass pieces ──────────────────────────────────────────────── - Widget _pill({required Widget child}) { - return Container( - height: 26, - padding: const EdgeInsets.symmetric(horizontal: 10), - decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(8)), - alignment: Alignment.center, - child: child, - ); - } - - Widget _mono(String t) => Text(t, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 12, color: Glass.ink)); - - Widget _sideBtn(String icon, {bool active = false, VoidCallback? onTap}) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 42, height: 42, - decoration: BoxDecoration( - color: active ? Glass.accent : Glass.pill, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Glass.hairline), - ), - alignment: Alignment.center, - child: PVIcon(icon, size: 20, stroke: 1.8, color: Glass.ink), - ), - ); - } - - Widget _gimbalSlider() { - return LayoutBuilder(builder: (BuildContext context, BoxConstraints c) { - final double h = c.maxHeight; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onVerticalDragUpdate: (DragUpdateDetails d) => _onGimbalDrag(d.localPosition.dy / h), - onTapDown: (TapDownDetails d) => _onGimbalDrag(d.localPosition.dy / h), - child: SizedBox( - width: 16, - child: Stack(children: [ - Align( - alignment: Alignment.topCenter, - child: Container(width: 6, height: h, decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(6))), - ), - Positioned( - top: (_gimbalFrac * h - 8).clamp(0.0, h - 16), - left: 0, - child: const _Thumb(), - ), - ]), - ), - ); - }); - } - - Widget _cameraControls() { - return Column(mainAxisSize: MainAxisSize.min, children: [ - GestureDetector( - onTap: _openCameraSettings, - child: Container( - width: 46, height: 46, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0x99FFFFFF), width: 2), - gradient: const LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF2A4E86), Color(0xFF12201A)]), - ), - ), - ), - const SizedBox(height: 14), - GestureDetector( - onTap: _toggleShutter, - child: Container( - width: 62, height: 62, - decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: const Color(0xD9FFFFFF), width: 4)), - child: Center( - child: _m.captureMode == CaptureMode.video && _m.isRecording - ? Container(width: 24, height: 24, decoration: BoxDecoration(color: Glass.rec, borderRadius: BorderRadius.circular(5))) - : Container(width: 46, height: 46, decoration: BoxDecoration(color: _m.captureMode == CaptureMode.video ? Glass.rec : Colors.white, shape: BoxShape.circle)), - ), - ), - ), - const SizedBox(height: 14), - Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(10)), - child: Column(children: [ - _modeBtn('image', CaptureMode.photo), - _modeBtn('video', CaptureMode.video), - _modeBtn('film', CaptureMode.pano), - ]), - ), - ]); - } - - Widget _modeBtn(String icon, CaptureMode mode) { - final bool active = _m.captureMode == mode; - return GestureDetector( - onTap: () => _setCaptureMode(mode), - child: Container( - width: 40, height: 30, - margin: const EdgeInsets.symmetric(vertical: 1), - decoration: BoxDecoration(color: active ? Glass.accent : Colors.transparent, borderRadius: BorderRadius.circular(7)), - alignment: Alignment.center, - child: PVIcon(icon, size: 17, stroke: 1.8, color: Glass.ink), - ), - ); - } - - Widget _minimap() { - final String rth = _m.homeDistance == null ? 'RTH —' : 'RTH ${_m.homeDistance!.toStringAsFixed(0)}m'; - return GestureDetector( - onTap: _openMap, - child: Container( - width: 148, height: 78, - decoration: BoxDecoration(color: Glass.pillStrong, borderRadius: BorderRadius.circular(12), border: Border.all(color: Glass.hairline)), - child: Stack(children: [ - const Positioned.fill(child: CustomPaint(painter: _MinimapPainter())), - Positioned(top: 6, left: 8, child: Row(children: [ - const PVIcon('home', size: 12, color: Glass.ink), - const SizedBox(width: 5), - Text(rth, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 10, color: Glass.ink)), - ])), - ]), - ), - ); - } - - Widget _telemetry() { - String f(double? v, {int d = 1}) => v == null ? '—' : v.toStringAsFixed(d); - final List<(String, String, String)> fields = <(String, String, String)>[ - ('H', f(_m.altitude), 'm'), - ('D', f(_m.homeDistance, d: 0), 'm'), - ('H.S', f(_m.horizontalSpeed), 'm/s'), - ('V.S', f(_m.verticalSpeed), 'm/s'), - ]; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), - decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(12)), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < fields.length; i++) ...[ - if (i > 0) const SizedBox(width: 20), - Column(mainAxisSize: MainAxisSize.min, children: [ - Text(fields[i].$1, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 10, letterSpacing: 0.8, color: Color(0x99EAF0FA))), - const SizedBox(height: 2), - Text.rich(TextSpan(children: [ - TextSpan(text: fields[i].$2, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 15, fontWeight: FontWeight.w700, color: Glass.ink)), - TextSpan(text: ' ${fields[i].$3}', style: const TextStyle(fontFamily: PV.fontMono, fontSize: 10, color: Color(0x99EAF0FA))), - ])), - ]), - ], - ]), - ); - } - - Widget _rthButton() { - return GestureDetector( - onTap: _rth, - child: Container( - width: 44, height: 44, - decoration: BoxDecoration(color: Glass.pillStrong, shape: BoxShape.circle, border: Border.all(color: const Color(0x2EFFFFFF))), - alignment: Alignment.center, - child: const PVIcon('rth', size: 20, color: Glass.ink), - ), - ); - } - - Widget _takeoffButton() { - final bool flying = _m.isFlying == true; - return GestureDetector( - onTap: _toggleTakeoff, - child: Container( - width: 44, height: 44, - decoration: BoxDecoration(color: flying ? const Color(0xE6D64545) : Glass.accent, shape: BoxShape.circle, border: Border.all(color: const Color(0x2EFFFFFF))), - alignment: Alignment.center, - child: PVIcon(flying ? 'home' : 'takeoff', size: 20, color: Colors.white), - ), - ); - } -} - -class _Thumb extends StatelessWidget { - const _Thumb(); - @override - Widget build(BuildContext context) { - return Container( - width: 16, height: 16, - decoration: const BoxDecoration( - color: Glass.ink, shape: BoxShape.circle, - boxShadow: [BoxShadow(color: Color(0x80000000), blurRadius: 3, offset: Offset(0, 1))], - ), - ); - } -} - -/// Rule-of-thirds grid overlay toggled from the left rail. -class _GridPainter extends CustomPainter { - const _GridPainter(); - @override - void paint(Canvas canvas, Size size) { - final Paint p = Paint() - ..color = const Color(0x33FFFFFF) - ..strokeWidth = 1; - for (int i = 1; i < 3; i++) { - final double x = size.width * i / 3, y = size.height * i / 3; - canvas.drawLine(Offset(x, 0), Offset(x, size.height), p); - canvas.drawLine(Offset(0, y), Offset(size.width, y), p); - } - } - - @override - bool shouldRepaint(covariant _GridPainter oldDelegate) => false; -} - -/// Painted placeholder camera feed: graded sky→ground, perspective grid, haze, -/// distant buildings, and a yellow tracked-subject bracket. -class _FeedPainter extends CustomPainter { - const _FeedPainter(); - - @override - void paint(Canvas canvas, Size size) { - final double w = size.width, h = size.height; - final double horizon = h * 0.52; - - final Rect full = Offset.zero & size; - final Paint sky = Paint() - ..shader = const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFF2A4E86), Color(0xFF3E6199), Color(0xFF1C2A1E), Color(0xFF0E1710)], - stops: [0.0, 0.51, 0.54, 1.0], - ).createShader(full); - canvas.drawRect(full, sky); - - final Paint haze = Paint() - ..shader = LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [const Color(0x808FB4D8), const Color(0x008FB4D8)], - ).createShader(Rect.fromLTWH(0, horizon - 30, w, 60)); - canvas.drawRect(Rect.fromLTWH(0, horizon - 30, w, 60), haze); - - final Paint bld = Paint()..color = const Color(0xE612201A); - void building(double x, double y, double bw, double bh) => canvas.drawRect(Rect.fromLTWH(x * w, horizon + y, bw, bh), bld); - building(0.10, -40, 46, 40); - building(0.17, -52, 30, 52); - building(0.74, -46, 54, 46); - building(0.83, -34, 34, 34); - - final Paint grid = Paint() - ..color = const Color(0x297FE0B0) - ..strokeWidth = 1; - for (int i = 1; i <= 5; i++) { - final double t = i / 5.0; - final double y = horizon + (h - horizon) * t * t; - canvas.drawLine(Offset(0, y), Offset(w, y), grid); - } - final double vx = w / 2; - for (int k = -6; k <= 6; k += 2) { - final double bx = vx + k * (w * 0.16); - canvas.drawLine(Offset(vx + k * 10, horizon), Offset(bx, h), grid); - } - - final Paint subj = Paint() - ..color = Glass.subject - ..style = PaintingStyle.stroke - ..strokeWidth = 2; - const double bxw = 118, bxh = 82; - final Rect box = Rect.fromCenter(center: Offset(vx, horizon + 8), width: bxw, height: bxh); - const double c = 14; - canvas.drawPath(Path()..moveTo(box.left + c, box.top)..lineTo(box.left, box.top)..lineTo(box.left, box.top + c), subj); - canvas.drawPath(Path()..moveTo(box.right - c, box.top)..lineTo(box.right, box.top)..lineTo(box.right, box.top + c), subj); - canvas.drawPath(Path()..moveTo(box.left + c, box.bottom)..lineTo(box.left, box.bottom)..lineTo(box.left, box.bottom - c), subj); - canvas.drawPath(Path()..moveTo(box.right - c, box.bottom)..lineTo(box.right, box.bottom)..lineTo(box.right, box.bottom - c), subj); - } - - @override - bool shouldRepaint(covariant _FeedPainter oldDelegate) => false; -} - -class _MinimapPainter extends CustomPainter { - const _MinimapPainter(); - - @override - void paint(Canvas canvas, Size size) { - final Path route = Path() - ..moveTo(20, 60) - ..cubicTo(50, 40, 70, 30, 120, 24); - final Paint line = Paint() - ..color = const Color(0xFF5B93F5) - ..style = PaintingStyle.stroke - ..strokeWidth = 2; - canvas.drawPath(route, line); - canvas.drawCircle(const Offset(20, 60), 4, Paint()..color = Glass.sat); - canvas.drawCircle(const Offset(120, 24), 4, Paint()..color = const Color(0xFF5B93F5)); - } - - @override - bool shouldRepaint(covariant _MinimapPainter oldDelegate) => false; -} diff --git a/Fly App/lib/ui/fly/bottom/minimap.dart b/Fly App/lib/ui/fly/bottom/minimap.dart index 66d0bb5..3772150 100644 --- a/Fly App/lib/ui/fly/bottom/minimap.dart +++ b/Fly App/lib/ui/fly/bottom/minimap.dart @@ -34,7 +34,7 @@ class MinimapController extends ChangeNotifier { } } -/// The map layer: OpenStreetMap tiles (like `map_page.dart`), the flown trail, +/// The map layer: OpenStreetMap tiles, the flown trail, /// home, pilot and the aircraft. Used both as the small picture-in-picture and, /// after the swap, full-bleed behind the HUD — where it is also interactive. /// diff --git a/Fly App/lib/ui/fly/fly_screen.dart b/Fly App/lib/ui/fly/fly_screen.dart index 2313065..03decd4 100644 --- a/Fly App/lib/ui/fly/fly_screen.dart +++ b/Fly App/lib/ui/fly/fly_screen.dart @@ -69,7 +69,7 @@ import 'top/camera_info_strip.dart'; import 'top/smart_battery_bar.dart'; import 'top/status_bar.dart'; -/// The flight HUD v2 (plan P2), replacing [FlightControlPage]. +/// The flight HUD v2 (plan P2). /// /// A stack of layers in `fpv_litchi`'s order — video → visual overlays → map → /// camera column → gimbal → top bar → bottom attitude → left rail → minimap — diff --git a/Fly App/lib/ui/fly/panels/settings/settings_sheet.dart b/Fly App/lib/ui/fly/panels/settings/settings_sheet.dart index 2e4ef6d..6c314e0 100644 --- a/Fly App/lib/ui/fly/panels/settings/settings_sheet.dart +++ b/Fly App/lib/ui/fly/panels/settings/settings_sheet.dart @@ -515,15 +515,6 @@ class _FlySettingsSheetState extends State { value: 'View', onTap: () => _push(TelemetryInspectorPage(model: _m, dji: widget.dji)), ), - ValueListenableBuilder( - valueListenable: FlyPrefs.classicHud, - builder: (BuildContext context, bool on, _) => SwitchTile( - label: 'Classic flight screen', - hint: 'Takes effect the next time the flight screen opens', - value: on, - onChanged: FlyPrefs.setClassicHud, - ), - ), ValueListenableBuilder( valueListenable: FlyPrefs.perfOverlay, builder: (BuildContext context, bool on, _) => diff --git a/Fly App/lib/ui/map_page.dart b/Fly App/lib/ui/map_page.dart deleted file mode 100644 index 8b14238..0000000 --- a/Fly App/lib/ui/map_page.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; - -import '../dji_service.dart'; -import '../flight_model.dart'; -import '../fly/modes/waypoint_mission.dart'; -import '../theme.dart'; -import 'pv_icons.dart'; - -/// Map & waypoints — mirrors the v2 "Map & waypoints" mockup. Real OpenStreetMap -/// tiles (no API key); tapping the map in Pin mode drops real-GPS waypoints, and -/// "Run route" uploads + starts a Waypoint mission via [DjiService]. -class MapPage extends StatefulWidget { - const MapPage({super.key, required this.model, required this.dji}); - - final FlightModel model; - final DjiService dji; - - @override - State createState() => _MapPageState(); -} - -class _MapPageState extends State { - final MapController _map = MapController(); - final List _waypoints = []; - bool _pinMode = true; - double _altitude = 50; - double _speed = 8; - - DjiService get _dji => widget.dji; - FlightModel get _m => widget.model; - - LatLng get _fallback => const LatLng(37.7749, -122.4194); - LatLng? get _drone => (_m.latitude != null && _m.longitude != null) ? LatLng(_m.latitude!, _m.longitude!) : null; - LatLng? get _home => (_m.homeLatitude != null && _m.homeLongitude != null) ? LatLng(_m.homeLatitude!, _m.homeLongitude!) : null; - - @override - void initState() { - super.initState(); - SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - } - - void _snack(String msg) { - ScaffoldMessenger.of(context) - ..clearSnackBars() - ..showSnackBar(SnackBar(content: Text(msg))); - } - - void _onTap(TapPosition pos, LatLng latlng) { - if (!_pinMode) return; - setState(() => _waypoints.add(latlng)); - } - - Future _runRoute() async { - if (_waypoints.length < 2) { - _snack('Drop at least 2 waypoints first'); - return; - } - final WaypointMissionSpec mission = WaypointMissionSpec( - points: [for (final LatLng p in _waypoints) WaypointSpec(at: p, altitude: _altitude)], - cruiseSpeed: _speed, - ); - try { - await _dji.uploadWaypointMission(mission.toBridge()); - await _dji.startWaypointMission(); - if (mounted) _snack('Route running — ${_waypoints.length} waypoints'); - } catch (e) { - if (mounted) _snack('Route: ${e is PlatformException ? (e.message ?? e.code) : e}'); - } - } - - @override - Widget build(BuildContext context) { - final LatLng center = _drone ?? _home ?? _fallback; - return Scaffold( - backgroundColor: const Color(0xFF0E1726), - body: Stack(children: [ - FlutterMap( - mapController: _map, - options: MapOptions(initialCenter: center, initialZoom: 16, onTap: _onTap), - children: [ - TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.pilotvault.flyapp', - ), - if (_waypoints.length >= 2) - PolylineLayer(polylines: >[ - Polyline(points: _waypoints, strokeWidth: 3, color: const Color(0xFF5B93F5)), - ]), - MarkerLayer(markers: _markers()), - ], - ), - SafeArea( - child: Stack(children: [ - Positioned(top: 12, left: 14, child: _back()), - Positioned(left: 14, top: 58, child: _toolRail()), - Positioned(right: 16, top: 58, child: _routePanel()), - ]), - ), - ]), - ); - } - - List _markers() { - final List m = []; - for (int i = 0; i < _waypoints.length; i++) { - m.add(Marker( - point: _waypoints[i], - width: 26, height: 26, - child: Container( - decoration: const BoxDecoration(color: Color(0xFF3D7BF0), shape: BoxShape.circle), - alignment: Alignment.center, - child: Text('${i + 1}', style: const TextStyle(fontFamily: PV.fontMono, fontSize: 12, fontWeight: FontWeight.w700, color: Colors.white)), - ), - )); - } - if (_home != null) { - m.add(Marker(point: _home!, width: 24, height: 24, child: const _Dot(Color(0xFF7FE0B0), 'home'))); - } - if (_drone != null) { - m.add(Marker(point: _drone!, width: 24, height: 24, child: const _Dot(Color(0xFFF4C542), 'drone'))); - } - return m; - } - - Widget _back() { - return GestureDetector( - onTap: () => Navigator.of(context).maybePop(), - child: Container( - height: 30, width: 30, - decoration: BoxDecoration(color: Glass.pill, borderRadius: BorderRadius.circular(8)), - alignment: Alignment.center, - child: const PVIcon('chevronLeft', size: 18, color: Glass.ink), - ), - ); - } - - Widget _toolRail() { - return Column(children: [ - _tool('pin', _pinMode, () => setState(() => _pinMode = true)), - const SizedBox(height: 10), - _tool('route', false, () => setState(_waypoints.clear)), - const SizedBox(height: 10), - _tool('home', false, () { if (_home != null) _map.move(_home!, 16); }), - const SizedBox(height: 10), - _tool('crosshair', false, () { if (_drone != null) _map.move(_drone!, 16); }), - ]); - } - - Widget _tool(String icon, bool active, VoidCallback onTap) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 42, height: 42, - decoration: BoxDecoration(color: active ? Glass.accent : Glass.pill, borderRadius: BorderRadius.circular(12), border: Border.all(color: Glass.hairline)), - alignment: Alignment.center, - child: PVIcon(icon, size: 20, stroke: 1.8, color: Glass.ink), - ), - ); - } - - Widget _routePanel() { - return Container( - width: 200, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: Glass.pillStrong, borderRadius: BorderRadius.circular(14), border: Border.all(color: Glass.hairline)), - child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - const Text('Waypoint route', style: TextStyle(fontFamily: PV.fontSans, fontSize: 13, fontWeight: FontWeight.w700, color: Glass.ink)), - const SizedBox(height: 8), - if (_waypoints.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Text('Tap the map to drop waypoints', style: TextStyle(fontFamily: PV.fontMono, fontSize: 10.5, color: Color(0x99EAF0FA))), - ) - else - ...List.generate(_waypoints.length, (int i) => Padding( - padding: const EdgeInsets.symmetric(vertical: 5), - child: Row(children: [ - Container( - width: 22, height: 22, - decoration: const BoxDecoration(color: Color(0xE63D7BF0), shape: BoxShape.circle), - alignment: Alignment.center, - child: Text('${i + 1}', style: const TextStyle(fontFamily: PV.fontMono, fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white)), - ), - const SizedBox(width: 9), - Text('Alt ${_altitude.toInt()}m · ${_speed.toInt()} m/s', style: const TextStyle(fontFamily: PV.fontMono, fontSize: 10.5, color: Color(0xCCEAF0FA))), - ]), - )), - const SizedBox(height: 8), - _slider('Alt', _altitude, 20, 120, (double v) => setState(() => _altitude = v)), - _slider('Speed', _speed, 2, 15, (double v) => setState(() => _speed = v)), - const SizedBox(height: 8), - SizedBox( - height: 36, - child: FilledButton( - onPressed: _runRoute, - style: FilledButton.styleFrom(backgroundColor: const Color(0xFF3D7BF0), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: const [ - PVIcon('play2', size: 15, color: Colors.white, fill: true), - SizedBox(width: 6), - Text('Run route', style: TextStyle(fontFamily: PV.fontSans, fontSize: 13, fontWeight: FontWeight.w700)), - ]), - ), - ), - ]), - ); - } - - Widget _slider(String label, double value, double min, double max, ValueChanged onChanged) { - return Row(children: [ - SizedBox(width: 38, child: Text(label, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 10, color: Color(0x99EAF0FA)))), - Expanded( - child: SliderTheme( - data: SliderThemeData(trackHeight: 2, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6)), - child: Slider(value: value, min: min, max: max, activeColor: const Color(0xFF5B93F5), inactiveColor: const Color(0x33FFFFFF), onChanged: onChanged), - ), - ), - SizedBox(width: 26, child: Text('${value.toInt()}', textAlign: TextAlign.right, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 10, color: Glass.ink))), - ]); - } -} - -class _Dot extends StatelessWidget { - const _Dot(this.color, this.icon); - final Color color; - final String icon; - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration(color: color, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2)), - alignment: Alignment.center, - child: PVIcon(icon, size: 12, color: const Color(0xFF05060A)), - ); - } -} diff --git a/Fly App/test/widget_test.dart b/Fly App/test/widget_test.dart index 5af8e12..69d3006 100644 --- a/Fly App/test/widget_test.dart +++ b/Fly App/test/widget_test.dart @@ -1,13 +1,17 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:pilotvault_fly/main.dart'; void main() { - testWidgets('Shows the login screen when signed out', - (WidgetTester tester) async { + testWidgets('Opens on the home screen with no aircraft connected', (WidgetTester tester) async { + SharedPreferences.setMockInitialValues({}); await tester.pumpWidget(const DjiSampleApp()); + await tester.pump(); - expect(find.text('Sign in'), findsWidgets); - expect(find.text('Sign in to continue'), findsOneWidget); + expect(find.text('No aircraft connected'), findsOneWidget); + expect(find.text('Connect aircraft'), findsOneWidget); + expect(find.byType(MaterialApp), findsOneWidget); }); }