feat(wip): implement trip persistence through a local repository. Include loaded trips in the start page UI
This commit is contained in:
287
frontend/lib/presentation/pages/trip_creation_flow.dart
Normal file
287
frontend/lib/presentation/pages/trip_creation_flow.dart
Normal file
@@ -0,0 +1,287 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import 'package:anyway/core/constants.dart';
|
||||
import 'package:anyway/presentation/pages/new_trip_preferences.dart';
|
||||
|
||||
class TripLocationSelectionPage extends StatefulWidget {
|
||||
const TripLocationSelectionPage({
|
||||
super.key,
|
||||
this.autoNavigateToPreferences = true,
|
||||
});
|
||||
|
||||
final bool autoNavigateToPreferences;
|
||||
|
||||
@override
|
||||
State<TripLocationSelectionPage> createState() =>
|
||||
_TripLocationSelectionPageState();
|
||||
}
|
||||
|
||||
class _TripLocationSelectionPageState extends State<TripLocationSelectionPage> {
|
||||
final CameraPosition _initialCameraPosition = const CameraPosition(
|
||||
// TODO - maybe Paris is not the best default?
|
||||
target: LatLng(48.8566, 2.3522),
|
||||
zoom: 11.0,
|
||||
);
|
||||
|
||||
GoogleMapController? _mapController;
|
||||
LatLng? _selectedLocation;
|
||||
bool _useLocation = true;
|
||||
bool _loadingPreferences = true;
|
||||
bool _isSearchingAddress = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
static const Map<String, List<double>> _debugLocations = {
|
||||
'paris': [48.8575, 2.3514],
|
||||
'london': [51.5074, -0.1278],
|
||||
'new york': [40.7128, -74.006],
|
||||
'tokyo': [35.6895, 139.6917],
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLocationPreference();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadLocationPreference() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final useLocation = prefs.getBool('useLocation') ?? true;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_useLocation = useLocation;
|
||||
_loadingPreferences = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _onLongPress(LatLng location) {
|
||||
setState(() {
|
||||
_selectedLocation = location;
|
||||
});
|
||||
_mapController?.animateCamera(CameraUpdate.newLatLng(location));
|
||||
}
|
||||
|
||||
void _setSelectedLocationFromCoords(double lat, double lng) {
|
||||
final latLng = LatLng(lat, lng);
|
||||
setState(() {
|
||||
_selectedLocation = latLng;
|
||||
});
|
||||
_mapController?.animateCamera(CameraUpdate.newLatLng(latLng));
|
||||
}
|
||||
|
||||
Future<void> _useCurrentLocation() async {
|
||||
try {
|
||||
var permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.denied) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Location permission denied.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Location permission permanently denied.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final position = await Geolocator.getCurrentPosition();
|
||||
if (!mounted) return;
|
||||
_setSelectedLocationFromCoords(position.latitude, position.longitude);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Unable to get current location: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _searchForLocation(String rawQuery) async {
|
||||
final query = rawQuery.trim();
|
||||
if (query.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isSearchingAddress = true);
|
||||
try {
|
||||
List<Location> locations = [];
|
||||
if (GeocodingPlatform.instance != null) {
|
||||
locations = await locationFromAddress(query);
|
||||
}
|
||||
|
||||
Location? selected;
|
||||
if (locations.isNotEmpty) {
|
||||
selected = locations.first;
|
||||
} else {
|
||||
final fallback = _debugLocations[query.toLowerCase()];
|
||||
if (fallback != null) {
|
||||
selected = Location(
|
||||
latitude: fallback[0],
|
||||
longitude: fallback[1],
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selected == null) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('No results for "$query".')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
_setSelectedLocationFromCoords(selected.latitude, selected.longitude);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Search failed: $e')));
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isSearchingAddress = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<Marker> get _markers => _selectedLocation == null
|
||||
? <Marker>{}
|
||||
: {
|
||||
Marker(
|
||||
markerId: const MarkerId('new-trip-start'),
|
||||
position: _selectedLocation!,
|
||||
infoWindow: const InfoWindow(title: 'Trip start'),
|
||||
),
|
||||
};
|
||||
|
||||
void _confirmLocation() {
|
||||
if (_selectedLocation == null) return;
|
||||
final startLocation = <double>[
|
||||
_selectedLocation!.latitude,
|
||||
_selectedLocation!.longitude,
|
||||
];
|
||||
|
||||
if (!widget.autoNavigateToPreferences) {
|
||||
Navigator.of(context).pop(startLocation);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => NewTripPreferencesPage(startLocation: startLocation),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_loadingPreferences) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Choose Start Location')),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Choose Start Location')),
|
||||
body: Stack(
|
||||
children: [
|
||||
GoogleMap(
|
||||
onMapCreated: (controller) => _mapController = controller,
|
||||
initialCameraPosition: _initialCameraPosition,
|
||||
onLongPress: _onLongPress,
|
||||
markers: _markers,
|
||||
cloudMapId: MAP_ID,
|
||||
mapToolbarEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
myLocationEnabled: _useLocation,
|
||||
),
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Column(
|
||||
children: [
|
||||
SearchBar(
|
||||
controller: _searchController,
|
||||
hintText: 'Enter a city or long-press on the map',
|
||||
leading: const Icon(Icons.search),
|
||||
onSubmitted: _searchForLocation,
|
||||
trailing: [
|
||||
IconButton(
|
||||
icon: _isSearchingAddress
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.send),
|
||||
onPressed: _isSearchingAddress
|
||||
? null
|
||||
: () => _searchForLocation(_searchController.text),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_useLocation)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.my_location),
|
||||
label: const Text('Use current location'),
|
||||
onPressed: _useCurrentLocation,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
_selectedLocation == null
|
||||
? 'Long-press anywhere to drop the starting point.'
|
||||
: 'Selected: ${_selectedLocation!.latitude.toStringAsFixed(4)}, ${_selectedLocation!.longitude.toStringAsFixed(4)}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: _selectedLocation == null
|
||||
? null
|
||||
: FloatingActionButton.extended(
|
||||
onPressed: _confirmLocation,
|
||||
icon: widget.autoNavigateToPreferences
|
||||
? const Icon(Icons.tune)
|
||||
: const Icon(Icons.check),
|
||||
label: Text(
|
||||
widget.autoNavigateToPreferences
|
||||
? 'Select Preferences'
|
||||
: 'Use this location',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user