Adjust map view when adding/editing to account for bottom sheet

This commit is contained in:
stopflock
2025-08-29 20:09:42 -05:00
parent a8ac237317
commit 5c2bfbc76e
6 changed files with 175 additions and 33 deletions
+42 -4
View File
@@ -11,6 +11,7 @@ import '../widgets/add_node_sheet.dart';
import '../widgets/edit_node_sheet.dart';
import '../widgets/camera_provider_with_cache.dart';
import '../widgets/download_area_dialog.dart';
import '../widgets/measured_sheet.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@@ -24,6 +25,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final GlobalKey<MapViewState> _mapViewKey = GlobalKey<MapViewState>();
late final AnimatedMapController _mapController;
bool _editSheetShown = false;
// Track sheet heights for map padding
double _addSheetHeight = 0.0;
double _editSheetHeight = 0.0;
@override
void initState() {
@@ -78,9 +83,23 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
appState.startAddSession();
final session = appState.session!; // guaranteed nonnull now
_scaffoldKey.currentState!.showBottomSheet(
(ctx) => AddNodeSheet(session: session),
final controller = _scaffoldKey.currentState!.showBottomSheet(
(ctx) => MeasuredSheet(
onHeightChanged: (height) {
setState(() {
_addSheetHeight = height;
});
},
child: AddNodeSheet(session: session),
),
);
// Reset height when sheet is dismissed
controller.closed.then((_) {
setState(() {
_addSheetHeight = 0.0;
});
});
}
void _openEditNodeSheet() {
@@ -90,9 +109,23 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final session = appState.editSession!; // should be non-null when this is called
_scaffoldKey.currentState!.showBottomSheet(
(ctx) => EditNodeSheet(session: session),
final controller = _scaffoldKey.currentState!.showBottomSheet(
(ctx) => MeasuredSheet(
onHeightChanged: (height) {
setState(() {
_editSheetHeight = height;
});
},
child: EditNodeSheet(session: session),
),
);
// Reset height when sheet is dismissed
controller.closed.then((_) {
setState(() {
_editSheetHeight = 0.0;
});
});
}
@override
@@ -107,6 +140,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
_editSheetShown = false;
}
// Calculate bottom padding for map (90% of active sheet height)
final activeSheetHeight = _addSheetHeight > 0 ? _addSheetHeight : _editSheetHeight;
final mapBottomPadding = activeSheetHeight * 0.9;
return MultiProvider(
providers: [
ChangeNotifierProvider<CameraProviderWithCache>(create: (_) => CameraProviderWithCache()),
@@ -142,6 +179,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
key: _mapViewKey,
controller: _mapController,
followMeMode: appState.followMeMode,
bottomPadding: mapBottomPadding,
onUserGesture: () {
if (appState.followMeMode != FollowMeMode.off) {
appState.setFollowMeMode(FollowMeMode.off);
+21 -5
View File
@@ -29,7 +29,7 @@ class AddNodeSheet extends StatelessWidget {
}
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
final allowSubmit = submittableProfiles.isNotEmpty && session.profile.isSubmittable;
final allowSubmit = appState.isLoggedIn && submittableProfiles.isNotEmpty && session.profile.isSubmittable;
void _openRefineTags() async {
final result = await Navigator.push<OperatorProfile?>(
@@ -90,7 +90,7 @@ class AddNodeSheet extends StatelessWidget {
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: const [
children: [
Icon(Icons.info_outline, color: Colors.grey, size: 16),
SizedBox(width: 6),
Expanded(
@@ -102,11 +102,27 @@ class AddNodeSheet extends StatelessWidget {
],
),
),
if (submittableProfiles.isEmpty)
if (!appState.isLoggedIn)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: const [
children: [
Icon(Icons.info_outline, color: Colors.red, size: 20),
SizedBox(width: 6),
Expanded(
child: Text(
'You must be logged in to submit new nodes. Please log in via Settings.',
style: TextStyle(color: Colors.red, fontSize: 13),
),
),
],
),
)
else if (submittableProfiles.isEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.red, size: 20),
SizedBox(width: 6),
Expanded(
@@ -122,7 +138,7 @@ class AddNodeSheet extends StatelessWidget {
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: const [
children: [
Icon(Icons.info_outline, color: Colors.orange, size: 20),
SizedBox(width: 6),
Expanded(
+22 -6
View File
@@ -31,7 +31,7 @@ class EditNodeSheet extends StatelessWidget {
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
final isSandboxMode = appState.uploadMode == UploadMode.sandbox;
final allowSubmit = submittableProfiles.isNotEmpty && session.profile.isSubmittable && !isSandboxMode;
final allowSubmit = appState.isLoggedIn && submittableProfiles.isNotEmpty && session.profile.isSubmittable && !isSandboxMode;
void _openRefineTags() async {
final result = await Navigator.push<OperatorProfile?>(
@@ -97,7 +97,7 @@ class EditNodeSheet extends StatelessWidget {
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: const [
children: [
Icon(Icons.info_outline, color: Colors.grey, size: 16),
SizedBox(width: 6),
Expanded(
@@ -109,11 +109,27 @@ class EditNodeSheet extends StatelessWidget {
],
),
),
if (isSandboxMode)
if (!appState.isLoggedIn)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: const [
children: [
Icon(Icons.info_outline, color: Colors.red, size: 20),
SizedBox(width: 6),
Expanded(
child: Text(
'You must be logged in to edit nodes. Please log in via Settings.',
style: TextStyle(color: Colors.red, fontSize: 13),
),
),
],
),
)
else if (isSandboxMode)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.blue, size: 20),
SizedBox(width: 6),
Expanded(
@@ -129,7 +145,7 @@ class EditNodeSheet extends StatelessWidget {
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: const [
children: [
Icon(Icons.info_outline, color: Colors.red, size: 20),
SizedBox(width: 6),
Expanded(
@@ -145,7 +161,7 @@ class EditNodeSheet extends StatelessWidget {
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: const [
children: [
Icon(Icons.info_outline, color: Colors.orange, size: 20),
SizedBox(width: 6),
Expanded(
+1 -12
View File
@@ -132,18 +132,7 @@ class MapOverlays extends StatelessWidget {
),
),
// Fixed pin when adding or editing camera
if (session != null || editSession != null)
IgnorePointer(
child: Center(
child: Transform.translate(
offset: const Offset(0, kAddPinYOffset),
child: CameraIcon(
type: editSession != null ? CameraIconType.editing : CameraIconType.mock
),
),
),
),
],
);
}
+34 -6
View File
@@ -12,6 +12,7 @@ import '../models/node_profile.dart';
import '../models/tile_provider.dart';
import 'debouncer.dart';
import 'camera_provider_with_cache.dart';
import 'camera_icon.dart';
import 'map/camera_markers.dart';
import 'map/direction_cones.dart';
import 'map/map_overlays.dart';
@@ -30,10 +31,12 @@ class MapView extends StatefulWidget {
required this.controller,
required this.followMeMode,
required this.onUserGesture,
this.bottomPadding = 0.0,
});
final FollowMeMode followMeMode;
final VoidCallback onUserGesture;
final double bottomPadding;
@override
State<MapView> createState() => MapViewState();
@@ -244,11 +247,31 @@ class MapViewState extends State<MapView> {
// Build edit lines connecting original cameras to their edited positions
final editLines = _buildEditLines(cameras);
// Build center marker for add/edit sessions
final centerMarkers = <Marker>[];
if (session != null || editSession != null) {
try {
final center = _controller.mapController.camera.center;
centerMarkers.add(
Marker(
point: center,
width: kCameraIconDiameter,
height: kCameraIconDiameter,
child: CameraIcon(
type: editSession != null ? CameraIconType.editing : CameraIconType.mock,
),
),
);
} catch (_) {
// Controller not ready yet
}
}
return Stack(
children: [
PolygonLayer(polygons: overlays),
if (editLines.isNotEmpty) PolylineLayer(polylines: editLines),
MarkerLayer(markers: markers),
MarkerLayer(markers: [...markers, ...centerMarkers]),
],
);
}
@@ -256,11 +279,15 @@ class MapViewState extends State<MapView> {
return Stack(
children: [
FlutterMap(
key: ValueKey('map_${appState.offlineMode}_${appState.selectedTileType?.id ?? 'none'}_${_tileManager.mapRebuildKey}'),
mapController: _controller.mapController,
options: MapOptions(
initialCenter: _gpsController.currentLocation ?? _positionManager.initialLocation ?? LatLng(37.7749, -122.4194),
AnimatedPadding(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
padding: EdgeInsets.only(bottom: widget.bottomPadding),
child: FlutterMap(
key: ValueKey('map_${appState.offlineMode}_${appState.selectedTileType?.id ?? 'none'}_${_tileManager.mapRebuildKey}'),
mapController: _controller.mapController,
options: MapOptions(
initialCenter: _gpsController.currentLocation ?? _positionManager.initialLocation ?? LatLng(37.7749, -122.4194),
initialZoom: _positionManager.initialZoom ?? 15,
maxZoom: 19,
onPositionChanged: (pos, gesture) {
@@ -315,6 +342,7 @@ class MapViewState extends State<MapView> {
// backgroundColor removed in flutter_map >=8 (wrap in Container if needed)
),
],
),
),
// All map overlays (mode indicator, zoom, attribution, add pin)
+55
View File
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
/// Wrapper widget that measures its child's height and reports changes via callback
class MeasuredSheet extends StatefulWidget {
final Widget child;
final ValueChanged<double> onHeightChanged;
const MeasuredSheet({
super.key,
required this.child,
required this.onHeightChanged,
});
@override
State<MeasuredSheet> createState() => _MeasuredSheetState();
}
class _MeasuredSheetState extends State<MeasuredSheet> {
final GlobalKey _key = GlobalKey();
double _lastHeight = 0.0;
@override
void initState() {
super.initState();
// Schedule height measurement after first frame
WidgetsBinding.instance.addPostFrameCallback(_measureHeight);
}
void _measureHeight(Duration _) {
final renderBox = _key.currentContext?.findRenderObject() as RenderBox?;
if (renderBox != null) {
final height = renderBox.size.height;
if (height != _lastHeight) {
_lastHeight = height;
widget.onHeightChanged(height);
}
}
}
@override
Widget build(BuildContext context) {
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
WidgetsBinding.instance.addPostFrameCallback(_measureHeight);
return true;
},
child: SizeChangedLayoutNotifier(
child: Container(
key: _key,
child: widget.child,
),
),
);
}
}