feat(wip): implement trip persistence through a local repository. Include loaded trips in the start page UI

This commit is contained in:
2025-12-30 00:51:40 +01:00
parent 81ed2fd8c3
commit 014b48591e
28 changed files with 2395 additions and 387 deletions

View File

@@ -1,50 +1,241 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:anyway/domain/entities/preferences.dart';
import 'package:anyway/presentation/providers/trip_provider.dart';
import 'package:anyway/presentation/pages/new_trip_preferences.dart';
import 'package:anyway/presentation/pages/trip_creation_flow.dart';
import 'package:anyway/domain/entities/landmark.dart';
import 'package:anyway/presentation/providers/landmark_providers.dart';
class NewTripPage extends StatefulWidget {
class NewTripPage extends ConsumerStatefulWidget {
const NewTripPage({super.key});
@override
_NewTripPageState createState() => _NewTripPageState();
ConsumerState<NewTripPage> createState() => _NewTripPageState();
}
class _NewTripPageState extends State<NewTripPage> {
class _NewTripPageState extends ConsumerState<NewTripPage> {
int _currentStep = 0;
bool _isCreating = false;
List<double>? _selectedStartLocation;
bool get _hasSelectedLocation => _selectedStartLocation != null;
Future<void> _pickLocation() async {
final result = await Navigator.of(context).push<List<double>>(
MaterialPageRoute(
builder: (_) =>
const TripLocationSelectionPage(autoNavigateToPreferences: false),
),
);
if (!mounted) return;
if (result != null) {
setState(() => _selectedStartLocation = result);
}
}
void _openPreferencesPage() {
if (!_hasSelectedLocation) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Select a start location first.')),
);
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
NewTripPreferencesPage(startLocation: _selectedStartLocation!),
),
);
}
void _showSelectLocationReminder() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Choose a start location to continue.')),
);
}
Widget _buildIntermediateLandmarks(List<Landmark> landmarks) {
return Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Fetched landmarks',
style: TextStyle(fontWeight: FontWeight.bold),
),
if (landmarks.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text(
'No landmarks fetched yet. Create a trip to load candidates.',
),
)
else
SizedBox(
height: 160,
child: ListView.separated(
itemCount: landmarks.length,
itemBuilder: (context, index) {
final lm = landmarks[index];
final coords = lm.location;
final subtitle = coords.length >= 2
? 'Lat ${coords[0].toStringAsFixed(4)}, Lon ${coords[1].toStringAsFixed(4)}'
: 'Coordinates unavailable';
return ListTile(
leading: const Icon(Icons.place),
title: Text(lm.name),
subtitle: Text(subtitle),
);
},
separatorBuilder: (context, index) =>
const Divider(height: 0),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final intermediateLandmarks = ref.watch(intermediateLandmarksProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Create New Trip'),
actions: [
IconButton(
tooltip: 'Preferences',
icon: const Icon(Icons.tune),
onPressed: _openPreferencesPage,
),
],
),
body: Stepper(
currentStep: _currentStep,
onStepContinue: () {
if (_currentStep < 1) {
setState(() {
_currentStep += 1;
});
}
},
onStepCancel: () {
if (_currentStep > 0) {
setState(() {
_currentStep -= 1;
});
}
},
steps: [
Step(
title: const Text('Select Location'),
content: const MapWithSearchField(), // Replace with your map module
isActive: _currentStep >= 0,
body: Column(
children: [
Expanded(
child: Stepper(
currentStep: _currentStep,
onStepContinue: () async {
if (_currentStep == 0) {
if (!_hasSelectedLocation) {
_showSelectLocationReminder();
return;
}
setState(() => _currentStep = 1);
return;
}
// final step: create trip with current preferences
if (_isCreating) return;
setState(() => _isCreating = true);
if (!_hasSelectedLocation) {
_showSelectLocationReminder();
setState(() {
_isCreating = false;
_currentStep = 0;
});
return;
}
// For now use a minimal Preferences object; UI should supply real values later.
final prefs = Preferences(
scores: {'sightseeing': 3, 'shopping': 1, 'nature': 2},
maxTimeMinutes: 120,
startLocation: _selectedStartLocation!,
);
final createTrip = ref.read(createTripProvider);
final messenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
try {
final trip = await createTrip(prefs);
// Show success and (later) navigate to trip viewer
messenger.showSnackBar(
SnackBar(content: Text('Trip created: ${trip.uuid}')),
);
navigator.pop();
} catch (e) {
messenger.showSnackBar(
SnackBar(content: Text('Failed to create trip: $e')),
);
} finally {
if (mounted) {
setState(() => _isCreating = false);
}
}
},
onStepCancel: () {
if (_currentStep > 0) {
setState(() {
_currentStep -= 1;
});
}
},
steps: [
Step(
title: const Text('Select Location'),
state: _hasSelectedLocation
? StepState.complete
: StepState.indexed,
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_hasSelectedLocation
? 'Selected: ${_selectedStartLocation![0].toStringAsFixed(4)}, ${_selectedStartLocation![1].toStringAsFixed(4)}'
: 'Pick the starting point for your trip.',
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(Icons.map),
label: Text(
_hasSelectedLocation
? 'Change location'
: 'Pick on map',
),
onPressed: _pickLocation,
),
),
],
),
isActive: _currentStep >= 0,
),
Step(
title: const Text('Choose Options'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
SizedBox(
height: 200,
child: Center(child: Text('Options placeholder')),
),
SizedBox(height: 8),
Text('Tap the tuner icon to fine-tune preferences.'),
],
),
isActive: _currentStep >= 1,
),
],
),
),
Step(
title: const Text('Choose Options'),
content: const OptionsList(), // Replace with your options module
isActive: _currentStep >= 1,
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _buildIntermediateLandmarks(intermediateLandmarks),
),
const SizedBox(height: 16),
],
),
);
}
}

View File

@@ -0,0 +1,161 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:anyway/domain/entities/preferences.dart';
import 'package:anyway/presentation/pages/trip_details_page.dart';
import 'package:anyway/presentation/providers/trip_provider.dart';
class NewTripPreferencesPage extends ConsumerStatefulWidget {
const NewTripPreferencesPage({super.key, required this.startLocation});
final List<double> startLocation;
@override
ConsumerState<NewTripPreferencesPage> createState() =>
_NewTripPreferencesPageState();
}
class _NewTripPreferencesPageState
extends ConsumerState<NewTripPreferencesPage> {
int _sightseeing = 3;
int _shopping = 1;
int _nature = 2;
int _maxTimeMinutes = 90;
bool _isCreating = false;
Widget _preferenceSlider(
String name,
int value,
ValueChanged<int> onChanged,
Icon icon,
) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: icon,
title: Text(name),
subtitle: Slider(
value: value.toDouble(),
min: 0,
max: 5,
divisions: 5,
label: value.toString(),
onChanged: (v) => onChanged(v.toInt()),
),
),
);
}
Widget _durationPicker() {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: const Icon(Icons.access_time),
title: const Text('Maximum trip duration'),
subtitle: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: Duration(minutes: _maxTimeMinutes),
minuteInterval: 15,
onTimerDurationChanged: (Duration newDuration) {
setState(() {
_maxTimeMinutes = newDuration.inMinutes;
});
},
),
),
);
}
Future<void> _onCreatePressed() async {
if (_isCreating) return;
setState(() => _isCreating = true);
final createTrip = ref.read(createTripProvider);
final prefs = Preferences(
scores: {
'sightseeing': _sightseeing,
'shopping': _shopping,
'nature': _nature,
},
maxTimeMinutes: _maxTimeMinutes,
startLocation: widget.startLocation,
);
try {
final trip = await createTrip(prefs);
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Trip created: ${trip.uuid}')));
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => TripDetailsPage(trip: trip)),
(route) => route.isFirst,
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to create trip: $e')));
} finally {
if (mounted) setState(() => _isCreating = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Trip Preferences')),
body: ListView(
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'Tell us about your ideal trip.',
style: TextStyle(fontSize: 18),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Icon(Icons.place),
const SizedBox(width: 8),
Text(
'Start: ${widget.startLocation[0].toStringAsFixed(4)}, ${widget.startLocation[1].toStringAsFixed(4)}',
),
],
),
),
_durationPicker(),
_preferenceSlider(
'Sightseeing',
_sightseeing,
(v) => setState(() => _sightseeing = v),
const Icon(Icons.camera_alt),
),
_preferenceSlider(
'Shopping',
_shopping,
(v) => setState(() => _shopping = v),
const Icon(Icons.shopping_bag),
),
_preferenceSlider(
'Nature',
_nature,
(v) => setState(() => _nature = v),
const Icon(Icons.park),
),
const SizedBox(height: 120), // padding for floating button
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _isCreating ? null : _onCreatePressed,
label: _isCreating
? const Text('Creating...')
: const Text('Create Trip'),
icon: const Icon(Icons.playlist_add),
),
);
}
}

View File

@@ -1,107 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:anyway/presentation/providers/onboarding_state_provider.dart';
import 'package:anyway/domain/entities/trip.dart';
import 'package:anyway/presentation/pages/login.dart';
import 'package:anyway/presentation/pages/onboarding.dart';
import 'package:anyway/presentation/pages/new_trip_preferences.dart';
import 'package:anyway/presentation/pages/trip_creation_flow.dart';
import 'package:anyway/presentation/pages/trip_details_page.dart';
import 'package:anyway/presentation/providers/trip_provider.dart';
import 'package:anyway/presentation/widgets/trip_summary_card.dart';
// Example providers (replace these with your actual providers)
// final onboardingStateProvider = Provider<bool>((ref) => true); // Replace with actual onboarding state logic
final authStateProvider = FutureProvider<bool>((ref) async => true); // Replace with actual auth state logic
final tripsAvailableProvider = FutureProvider<bool>((ref) async => false); // Replace with actual trips availability logic
// TODO - Replace with actual auth state logic
final authStateProvider = FutureProvider<bool>(
(ref) async => true,
);
class StartPage extends ConsumerWidget {
const StartPage({Key? key}) : super(key: key);
const StartPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// the home page is dependent on the state of the providers:
// - if the user is not onboarded, show the onboarding flow
// - if the user is not logged in, show the login page
// - if there are no trips available, show the trip creation page
// - else: show the overview page that shows the last trip
// the home page is dependent on the state of the providers:
// - if the user is not onboarded, show the onboarding flow
// - if the user is not logged in, show the login page
// - if there are no trips available, show the trip creation page
// - else: show the overview page that shows the last trip
final onboardingState = ref.watch(onboardingStateProvider);
final authState = ref.watch(authStateProvider);
final currentTrips = ref.watch(currentTripsProvider);
final onboardingState = ref.watch(onboardingStateProvider);
final authState = ref.watch(authStateProvider);
final tripsAvailable = ref.watch(tripsAvailableProvider);
return onboardingState.when(
data: (isOnboarded) {
if (!isOnboarded) {
return const OnboardingPage();
}
return onboardingState.when(
data: (isOnboarded) {
if (!isOnboarded) {
return const OnboardingPage();
}
return authState.when(
data: (isLoggedIn) {
if (!isLoggedIn) {
return const LoginPage();
}
return authState.when(
data: (isLoggedIn) {
if (!isLoggedIn) {
return const LoginPage();
}
return tripsAvailable.when(
data: (hasTrips) {
if (!hasTrips) {
return const TripCreationPage();
}
return const OverviewPage();
},
loading: () => const Scaffold(
body: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => Scaffold(
body: Center(child: Text('Error: $error')),
),
return currentTrips.when(
data: (trips) {
if (trips.isEmpty) {
return const TripLocationSelectionPage();
}
return TripsOverviewPage(trips: trips);
},
loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())),
error: (error, stack) => Scaffold(body: Center(child: Text('Error loading trips: $error'))),
);
},
loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())),
error: (error, stack) => Scaffold(body: Center(child: Text('Error: $error'))),
);
},
loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())),
error: (error, stack) => Scaffold(body: Center(child: Text('Error: $error'))),
);
}
}
// TODO - move to separate file
class TripsOverviewPage extends StatelessWidget {
const TripsOverviewPage({super.key, required this.trips});
final List<Trip> trips;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Your trips')),
body: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: trips.length,
separatorBuilder: (context, index) => const SizedBox(height: 16),
itemBuilder: (context, index) {
final trip = trips[index];
return TripSummaryCard(
trip: trip,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => TripDetailsPage(trip: trip)),
);
},
loading: () => const Scaffold(
body: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => Scaffold(
body: Center(child: Text('Error: $error')),
),
);
},
loading: () => const Scaffold(
body: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => Scaffold(
body: Center(child: Text('Error: $error')
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const TripLocationSelectionPage()));
},
icon: const Icon(Icons.map),
label: const Text('Plan your next trip'),
),
);
}
}
class TripCreationPage extends StatelessWidget {
const TripCreationPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Create a Trip')),
body: Center(
child: ElevatedButton.icon(
icon: const Icon(Icons.tune),
label: const Text('Set Preferences'),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const NewTripPreferencesPage()));
},
),
),
);
}
}
class OverviewPage extends StatelessWidget {
const OverviewPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Overview Page')),
);
}
}

View 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',
),
),
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:anyway/core/constants.dart';
import 'package:anyway/domain/entities/trip.dart';
import 'package:anyway/presentation/utils/trip_location_utils.dart';
import 'package:anyway/presentation/widgets/trip_details_panel.dart';
import 'package:anyway/presentation/widgets/trip_map.dart';
class TripDetailsPage extends StatefulWidget {
const TripDetailsPage({super.key, required this.trip});
final Trip trip;
@override
State<TripDetailsPage> createState() => _TripDetailsPageState();
}
class _TripDetailsPageState extends State<TripDetailsPage> {
late Future<bool> _locationPrefFuture;
late Future<TripLocaleInfo> _localeFuture;
@override
void initState() {
super.initState();
_locationPrefFuture = _loadLocationPreference();
_localeFuture = TripLocationUtils.resolveLocaleInfo(widget.trip);
}
Future<bool> _loadLocationPreference() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('useLocation') ?? true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: FutureBuilder<TripLocaleInfo>(
future: _localeFuture,
builder: (context, snapshot) {
final locale = snapshot.data;
final title = locale?.cityName ?? 'Trip overview';
return Text(title);
},
),
),
body: FutureBuilder<bool>(
future: _locationPrefFuture,
builder: (context, snapshot) {
final enableLocation = snapshot.data ?? false;
return SlidingUpPanel(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)),
parallaxEnabled: true,
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
panelBuilder: (controller) => TripDetailsPanel(trip: widget.trip, controller: controller),
body: TripMap(trip: widget.trip, showRoute: true, interactive: true, borderRadius: 0, enableMyLocation: enableLocation),
);
},
),
);
}
}