mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-18 14:45:07 +02:00
Adjust map view when adding/editing to account for bottom sheet
This commit is contained in:
@@ -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 non‑null 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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user