From 27c404687acad8913c100b06d38704ccfba63b1b Mon Sep 17 00:00:00 2001 From: stopflock Date: Wed, 22 Oct 2025 18:46:48 -0500 Subject: [PATCH] Fix north-up UX --- lib/widgets/compass_indicator.dart | 34 +++++++++++++++++++++++++---- lib/widgets/map/gps_controller.dart | 13 +++++++++++ lib/widgets/map/map_overlays.dart | 1 - lib/widgets/map_view.dart | 9 ++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/widgets/compass_indicator.dart b/lib/widgets/compass_indicator.dart index bee5283..518aa6d 100644 --- a/lib/widgets/compass_indicator.dart +++ b/lib/widgets/compass_indicator.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -21,10 +22,16 @@ class CompassIndicator extends StatefulWidget { } class _CompassIndicatorState extends State { + Timer? _animationTimer; + + @override + void dispose() { + _animationTimer?.cancel(); + super.dispose(); + } @override Widget build(BuildContext context) { - return Consumer( - builder: (context, appState, child) { + final appState = context.watch(); // Get current map rotation in degrees double rotationDegrees = 0.0; try { @@ -46,6 +53,10 @@ class _CompassIndicatorState extends State { onTap: isDisabled ? null : () { // Animate to north-up orientation try { + // Cancel any existing animation timer + _animationTimer?.cancel(); + + // Start animation widget.mapController.animateTo( dest: widget.mapController.mapController.camera.center, zoom: widget.mapController.mapController.camera.zoom, @@ -53,6 +64,23 @@ class _CompassIndicatorState extends State { duration: const Duration(milliseconds: 500), curve: Curves.easeOut, ); + + // Start timer to force compass updates during animation + // Update every 16ms (~60fps) for smooth visual rotation + _animationTimer = Timer.periodic(const Duration(milliseconds: 16), (timer) { + if (mounted) { + setState(() {}); + } + }); + + // Stop the timer after animation completes (with small buffer) + Timer(const Duration(milliseconds: 550), () { + _animationTimer?.cancel(); + _animationTimer = null; + if (mounted) { + setState(() {}); // Final update to ensure correct end state + } + }); } catch (_) { // Controller not ready, ignore } @@ -132,7 +160,5 @@ class _CompassIndicatorState extends State { ), ), ); - }, - ); } } \ No newline at end of file diff --git a/lib/widgets/map/gps_controller.dart b/lib/widgets/map/gps_controller.dart index 2bec741..caf8edb 100644 --- a/lib/widgets/map/gps_controller.dart +++ b/lib/widgets/map/gps_controller.dart @@ -46,6 +46,7 @@ class GpsController { required FollowMeMode newMode, required FollowMeMode oldMode, required AnimatedMapController controller, + VoidCallback? onMapMovedProgrammatically, }) { debugPrint('[GpsController] Follow-me mode changed: $oldMode → $newMode'); @@ -62,6 +63,7 @@ class GpsController { duration: kFollowMeAnimationDuration, curve: Curves.easeOut, ); + onMapMovedProgrammatically?.call(); } else if (newMode == FollowMeMode.rotating) { // When switching to rotating mode, reset to north-up first controller.animateTo( @@ -71,6 +73,7 @@ class GpsController { duration: kFollowMeAnimationDuration, curve: Curves.easeOut, ); + onMapMovedProgrammatically?.call(); } } catch (e) { debugPrint('[GpsController] MapController not ready for follow-me change: $e'); @@ -89,6 +92,8 @@ class GpsController { int proximityAlertDistance = 200, List nearbyNodes = const [], List enabledProfiles = const [], + // Optional callback when map is moved programmatically + VoidCallback? onMapMovedProgrammatically, }) { final latLng = LatLng(position.latitude, position.longitude); @@ -121,6 +126,9 @@ class GpsController { duration: kFollowMeAnimationDuration, curve: Curves.easeOut, ); + + // Notify that we moved the map programmatically (for node refresh) + onMapMovedProgrammatically?.call(); } else if (followMeMode == FollowMeMode.rotating) { // Follow position and rotation based on heading final heading = position.heading; @@ -137,6 +145,9 @@ class GpsController { duration: kFollowMeAnimationDuration, curve: Curves.easeOut, ); + + // Notify that we moved the map programmatically (for node refresh) + onMapMovedProgrammatically?.call(); } } catch (e) { debugPrint('[GpsController] MapController not ready for position animation: $e'); @@ -155,6 +166,7 @@ class GpsController { required int Function() getProximityAlertDistance, required List Function() getNearbyNodes, required List Function() getEnabledProfiles, + VoidCallback? onMapMovedProgrammatically, }) async { final perm = await Geolocator.requestPermission(); @@ -180,6 +192,7 @@ class GpsController { proximityAlertDistance: proximityAlertDistance, nearbyNodes: nearbyNodes, enabledProfiles: enabledProfiles, + onMapMovedProgrammatically: onMapMovedProgrammatically, ); }); } diff --git a/lib/widgets/map/map_overlays.dart b/lib/widgets/map/map_overlays.dart index ac62f54..9b8a676 100644 --- a/lib/widgets/map/map_overlays.dart +++ b/lib/widgets/map/map_overlays.dart @@ -18,7 +18,6 @@ class MapOverlays extends StatelessWidget { final EditNodeSession? editSession; final String? attribution; // Attribution for current tile provider final VoidCallback? onSearchPressed; // Callback for search button - const MapOverlays({ super.key, required this.mapController, diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 25f53bd..c0ac06d 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -78,6 +78,8 @@ class MapViewState extends State { // State for proximity alert banner bool _showProximityBanner = false; + + @override void initState() { @@ -180,6 +182,10 @@ class MapViewState extends State { } return []; }, + onMapMovedProgrammatically: () { + // Refresh nodes when GPS controller moves the map + _refreshNodesFromProvider(); + }, ); @@ -302,6 +308,9 @@ class MapViewState extends State { newMode: widget.followMeMode, oldMode: oldWidget.followMeMode, controller: _controller, + onMapMovedProgrammatically: () { + _refreshNodesFromProvider(); + }, ); } }