maps/mobile/lib/features/map/presentation/screens/map_screen.dart
2026-03-30 09:22:16 +02:00

157 lines
5.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/api/api_client.dart';
import '../../providers/map_provider.dart';
import '../widgets/map_controls.dart';
import '../widgets/place_card.dart';
class MapScreen extends ConsumerStatefulWidget {
const MapScreen({super.key});
@override
ConsumerState<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends ConsumerState<MapScreen> {
late final MapController _mapController;
@override
void initState() {
super.initState();
_mapController = MapController();
}
@override
void dispose() {
_mapController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mapState = ref.watch(mapProvider);
final apiClient = ref.watch(apiClientProvider);
final tileUrl = '${apiClient.baseUrl}/tiles/openmaptiles/{z}/{x}/{y}.pbf';
// Listen for zoom/center changes from the provider and move the map.
ref.listen<MapState>(mapProvider, (previous, next) {
if (previous?.center != next.center || previous?.zoom != next.zoom) {
_mapController.move(next.center, next.zoom);
}
});
return Scaffold(
body: Stack(
children: [
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: mapState.center,
initialZoom: mapState.zoom,
minZoom: 0,
maxZoom: 18,
onPositionChanged: (position, hasGesture) {
if (hasGesture) {
ref.read(mapProvider.notifier).updateCamera(
position.center,
position.zoom,
);
}
},
onTap: (tapPosition, point) {
ref.read(mapProvider.notifier).clearSelectedPlace();
},
),
children: [
TileLayer(
urlTemplate: tileUrl,
tileProvider: CancellableNetworkTileProvider(),
userAgentPackageName: 'com.privacymaps.app',
),
if (mapState.currentLocation != null)
MarkerLayer(
markers: [
Marker(
point: mapState.currentLocation!,
width: 24,
height: 24,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
),
),
],
),
],
),
// Search bar at the top
Positioned(
top: MediaQuery.of(context).padding.top + 8,
left: 12,
right: 12,
child: GestureDetector(
onTap: () => context.push('/search'),
child: Card(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(Icons.search,
color: Theme.of(context).colorScheme.onSurfaceVariant),
const SizedBox(width: 12),
Expanded(
child: Text(
'Search places...',
style:
Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => context.push('/settings'),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
),
),
),
),
// Map controls
const MapControls(),
// Place card bottom sheet
const PlaceCard(),
// Offline button
Positioned(
left: 16,
bottom: 120,
child: FloatingActionButton.small(
heroTag: 'offline',
onPressed: () => context.push('/offline'),
child: const Icon(Icons.download),
),
),
],
),
);
}
}