From 0b812bdaeaef11e4bb5a4a8af6eeadb0eb4d32ec Mon Sep 17 00:00:00 2001 From: stopflock Date: Sun, 14 Jun 2026 21:20:17 -0500 Subject: [PATCH] long press to add node --- assets/changelog.json | 5 +- lib/dev_config.dart | 1 + .../coordinators/map_interaction_handler.dart | 47 +++++++++++++++++++ lib/screens/home_screen.dart | 17 +++++++ lib/widgets/map_view.dart | 11 +++++ 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/assets/changelog.json b/assets/changelog.json index c9ffc1a..6607d80 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,7 +1,8 @@ { "2.10.2": { "content": [ - "• Fixed Flutter deprecation warnings for profile reordering" + "• Added tap and hold gesture on empty map area to add a node at that location", + "• Added ability to accept direct links to a specific node for callbacks from e.g. FlockHopper" ] }, "2.10.1": { @@ -13,7 +14,7 @@ }, "2.10.0": { "content": [ - "• Simplified profile FOVs; there is now only a checkbox for '360'", + "• Simplified profile FOVs; there is now only a checkbox for '360'" ] }, "2.9.2": { diff --git a/lib/dev_config.dart b/lib/dev_config.dart index 903b92a..0d16a25 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -96,6 +96,7 @@ const int kOsmApiMinZoomLevel = 13; // Minimum zoom for OSM API bbox queries (sa const int kMinZoomForNodeEditingSheets = 16; // Minimum zoom to open add/edit node sheets const int kMinZoomForOfflineDownload = 10; // Minimum zoom to download offline areas (prevents large area crashes) const Duration kMarkerTapTimeout = Duration(milliseconds: 250); +const Duration kMapLongPressTimeout = Duration(milliseconds: 600); // Duration to trigger "add node here" on empty map area const Duration kDebounceCameraRefresh = Duration(milliseconds: 500); // Pre-fetch area configuration diff --git a/lib/screens/coordinators/map_interaction_handler.dart b/lib/screens/coordinators/map_interaction_handler.dart index 4076b35..60f2f43 100644 --- a/lib/screens/coordinators/map_interaction_handler.dart +++ b/lib/screens/coordinators/map_interaction_handler.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_animations/flutter_map_animations.dart'; +import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../../app_state.dart'; @@ -125,4 +126,50 @@ class MapInteractionHandler { // Clear suspected location selection appState.clearSuspectedLocationSelection(); } + + /// Handle long press on empty map area (add node here) + void handleMapLongPress({ + required BuildContext context, + required LatLng tapLocation, + required AnimatedMapController mapController, + required VoidCallback onAddNode, + }) { + final appState = context.read(); + + debugPrint('[MapInteractionHandler] Long press at: ${tapLocation.latitude}, ${tapLocation.longitude}'); + + // Check if we should handle this long press + // Don't handle if any sheet is open or if in search mode + if (appState.isInSearchMode || appState.showingOverview) { + debugPrint('[MapInteractionHandler] Long press ignored - in search/navigation mode'); + return; + } + + // Don't handle if any session is active (add/edit sheets are open) + if (appState.session != null || appState.editSession != null) { + debugPrint('[MapInteractionHandler] Long press ignored - node sheet already open'); + return; + } + + // Disable follow-me when user long presses + appState.setFollowMeMode(FollowMeMode.off); + + // First, center the map on the tap location + try { + mapController.animateTo( + dest: tapLocation, + zoom: mapController.mapController.camera.zoom, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } catch (e) { + debugPrint('[MapInteractionHandler] Could not center map on long press location: $e'); + } + + // After a short delay to let the map center, open the add node sheet + // This matches the existing pattern used for node taps + Future.delayed(const Duration(milliseconds: 350), () { + onAddNode(); + }); + } } \ No newline at end of file diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index c84eeab..bbd632e 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_animations/flutter_map_animations.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../app_state.dart'; @@ -289,6 +290,21 @@ class _HomeScreenState extends State with TickerProviderStateMixin { ); } + void _onMapLongPress(LatLng location) { + // Don't handle long press if tag sheet is open + if (_sheetCoordinator.tagSheetHeight > 0) { + debugPrint('[HomeScreen] Long press ignored - tag sheet is open'); + return; + } + + _mapInteractionHandler.handleMapLongPress( + context: context, + tapLocation: location, + mapController: _mapController, + onAddNode: _openAddNodeSheet, + ); + } + void _handleNodeDeepLink(OsmNode node) { try { _mapController.animateTo( @@ -520,6 +536,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { // Re-render when location status changes (for follow-me button state) setState(() {}); }, + onMapLongPress: _onMapLongPress, onUserGesture: () { // Only clear selected node if tag sheet is not open // This prevents nodes from losing their grey-out when map is moved while viewing tags diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 2f2e015..c7abcad 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -43,6 +43,7 @@ class MapView extends StatefulWidget { this.onSearchPressed, this.onNodeLimitChanged, this.onLocationStatusChanged, + this.onMapLongPress, }); final FollowMeMode followMeMode; @@ -54,6 +55,7 @@ class MapView extends StatefulWidget { final VoidCallback? onSearchPressed; final void Function(bool isLimited)? onNodeLimitChanged; final VoidCallback? onLocationStatusChanged; + final void Function(LatLng)? onMapLongPress; @override State createState() => MapViewState(); @@ -514,6 +516,15 @@ class MapViewState extends State { _dataManager.showZoomWarningIfNeeded(context, pos.zoom, appState.uploadMode); } }, + onTap: (tapPosition, point) { + // Handle tap on empty map area - currently no action needed + debugPrint('[MapView] Tap at: $point'); + }, + onLongPress: (tapPosition, point) { + // Handle long press on empty map area - add node here + debugPrint('[MapView] Long press at: $point'); + widget.onMapLongPress?.call(point); + }, ), children: [ _tileManager.buildTileLayer(