Skip to content

Commit c84cc55

Browse files
committed
✨ Add location suggestions, loading indicators, and transport mode UX improvements
📝 Summary: This commit addresses three user-reported issues in the PH Fare Calculator app: 1. Map picker search field now shows location suggestions 2. All location input fields display loading indicators during search 3. Transport modes default to disabled with new selection modal and quick toggle 🔧 Changes: 🗺️ Map Picker Location Suggestions: - Replaced placeholder TextField with Autocomplete<Location> widget - Integrated GeocodingService for location search - Added selection logic to move map camera to chosen location ⏳ Loading Indicators: - Added ValueNotifier<bool> pattern for tracking search state - Display 16px CircularProgressIndicator in input suffix while fetching - Applied to both main screen (origin/destination) and map picker search 🚌 Transport Mode Improvements: - Changed default for all transport modes to disabled for new users - Added hasSetTransportModePreferences() to distinguish new vs existing users - Created TransportModeSelectionModal with Material 3 bottom sheet design - Modal shows modes grouped by category (Road, Rail, Water) - Added quick-access "Modes" button in TravelOptionsBar with badge count - Modal appears when calculating fare with no modes enabled 🧪 Tests: - Updated MockSettingsService and FakeSettingsService with new interface - Fixed flaky test finder in main_screen_test.dart - All 289 tests pass
1 parent a1b4b27 commit c84cc55

10 files changed

Lines changed: 1065 additions & 119 deletions

lib/src/presentation/screens/main_screen.dart

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import 'package:latlong2/latlong.dart';
66
import '../../core/di/injection.dart';
77
import '../../l10n/app_localizations.dart';
88
import '../../models/connectivity_status.dart';
9+
import '../../models/fare_formula.dart';
10+
import '../../repositories/fare_repository.dart';
911
import '../../services/connectivity/connectivity_service.dart';
1012
import '../../services/fare_comparison_service.dart';
13+
import '../../services/settings_service.dart';
1114
import '../controllers/main_screen_controller.dart';
1215
import '../widgets/main_screen/calculate_fare_button.dart';
1316
import '../widgets/main_screen/error_message_banner.dart';
@@ -19,6 +22,7 @@ import '../widgets/main_screen/main_screen_app_bar.dart';
1922
import '../widgets/main_screen/map_preview.dart';
2023
import '../widgets/main_screen/offline_status_banner.dart';
2124
import '../widgets/main_screen/passenger_bottom_sheet.dart';
25+
import '../widgets/main_screen/transport_mode_selection_modal.dart';
2226
import '../widgets/main_screen/travel_options_bar.dart';
2327
import 'map_picker_screen.dart';
2428

@@ -34,20 +38,34 @@ class MainScreen extends StatefulWidget {
3438
class _MainScreenState extends State<MainScreen> {
3539
late final MainScreenController _controller;
3640
late final ConnectivityService _connectivityService;
41+
late final SettingsService _settingsService;
42+
late final FareRepository _fareRepository;
3743
final TextEditingController _originTextController = TextEditingController();
3844
final TextEditingController _destinationTextController =
3945
TextEditingController();
4046
StreamSubscription<ConnectivityStatus>? _connectivitySubscription;
4147
ConnectivityStatus _connectivityStatus = ConnectivityStatus.online;
4248

49+
/// Number of enabled transport modes (not hidden).
50+
int _enabledModesCount = 0;
51+
52+
/// Total number of available transport modes.
53+
int _totalModesCount = 0;
54+
55+
/// Cached list of all formulas for modal display.
56+
List<FareFormula> _allFormulas = [];
57+
4358
@override
4459
void initState() {
4560
super.initState();
4661
_controller = MainScreenController();
4762
_connectivityService = getIt<ConnectivityService>();
63+
_settingsService = getIt<SettingsService>();
64+
_fareRepository = getIt<FareRepository>();
4865
_controller.addListener(_onControllerChanged);
4966
_initializeData();
5067
_initConnectivity();
68+
_loadTransportModeCounts();
5169
}
5270

5371
Future<void> _initConnectivity() async {
@@ -61,6 +79,35 @@ class _MainScreenState extends State<MainScreen> {
6179
});
6280
}
6381

82+
/// Loads transport mode counts for the quick-access button badge.
83+
Future<void> _loadTransportModeCounts() async {
84+
try {
85+
final allFormulas = await _fareRepository.getAllFormulas();
86+
final hiddenModes = await _settingsService.getHiddenTransportModes();
87+
88+
// Count unique mode-subtype combinations
89+
final allModeKeys = <String>{};
90+
for (final formula in allFormulas) {
91+
allModeKeys.add('${formula.mode}::${formula.subType}');
92+
}
93+
94+
final enabledCount = allModeKeys
95+
.where((key) => !hiddenModes.contains(key))
96+
.length;
97+
98+
if (mounted) {
99+
setState(() {
100+
_allFormulas = allFormulas;
101+
_totalModesCount = allModeKeys.length;
102+
_enabledModesCount = enabledCount;
103+
});
104+
}
105+
} catch (e) {
106+
// Silently handle error - the button will show 0/0
107+
debugPrint('Failed to load transport mode counts: $e');
108+
}
109+
}
110+
64111
@override
65112
void dispose() {
66113
_connectivitySubscription?.cancel();
@@ -152,6 +199,9 @@ class _MainScreenState extends State<MainScreen> {
152199
sortCriteria: _controller.sortCriteria,
153200
onPassengerTap: _showPassengerBottomSheet,
154201
onSortChanged: _controller.setSortCriteria,
202+
enabledModesCount: _enabledModesCount,
203+
totalModesCount: _totalModesCount,
204+
onTransportModesTap: _handleTransportModesTap,
155205
),
156206
const SizedBox(height: 16),
157207
// Map height: 280 when no fare results (40% larger), 200 when showing results
@@ -169,7 +219,7 @@ class _MainScreenState extends State<MainScreen> {
169219
CalculateFareButton(
170220
canCalculate: _controller.canCalculate,
171221
isCalculating: _controller.isCalculating,
172-
onPressed: _controller.calculateFare,
222+
onPressed: _handleCalculateFare,
173223
),
174224
if (_controller.errorMessage != null) ...[
175225
const SizedBox(height: 16),
@@ -292,4 +342,85 @@ class _MainScreenState extends State<MainScreen> {
292342
);
293343
}
294344
}
345+
346+
/// Handles the transport modes quick-access button tap.
347+
/// Shows the transport mode selection modal and updates the count after.
348+
Future<void> _handleTransportModesTap() async {
349+
// Ensure we have formulas loaded
350+
if (_allFormulas.isEmpty) {
351+
await _loadTransportModeCounts();
352+
}
353+
354+
if (!mounted || _allFormulas.isEmpty) return;
355+
356+
await TransportModeSelectionModal.show(
357+
context: context,
358+
settingsService: _settingsService,
359+
availableFormulas: _allFormulas,
360+
);
361+
362+
// Refresh the count after modal closes
363+
await _loadTransportModeCounts();
364+
}
365+
366+
/// Handles fare calculation with transport mode check.
367+
/// If no transport modes are enabled, shows the mode selection modal first.
368+
Future<void> _handleCalculateFare() async {
369+
// Check if user has set transport mode preferences
370+
final hasSetPreferences = await _settingsService
371+
.hasSetTransportModePreferences();
372+
373+
if (!hasSetPreferences) {
374+
// User hasn't set any preferences - show the modal
375+
final allFormulas = await _fareRepository.getAllFormulas();
376+
377+
if (!mounted) return;
378+
379+
final confirmed = await TransportModeSelectionModal.show(
380+
context: context,
381+
settingsService: _settingsService,
382+
availableFormulas: allFormulas,
383+
);
384+
385+
// Refresh the count after modal closes
386+
await _loadTransportModeCounts();
387+
388+
if (!confirmed) {
389+
// User cancelled - don't proceed with fare calculation
390+
return;
391+
}
392+
} else {
393+
// User has set preferences, check if any modes are actually enabled
394+
final hiddenModes = await _settingsService.getHiddenTransportModes();
395+
final allFormulas = await _fareRepository.getAllFormulas();
396+
397+
// Check if ALL modes are hidden
398+
final allModesHidden = allFormulas.every((formula) {
399+
final modeSubTypeKey = '${formula.mode}::${formula.subType}';
400+
return hiddenModes.contains(modeSubTypeKey);
401+
});
402+
403+
if (allModesHidden) {
404+
// All modes are hidden - show the modal
405+
if (!mounted) return;
406+
407+
final confirmed = await TransportModeSelectionModal.show(
408+
context: context,
409+
settingsService: _settingsService,
410+
availableFormulas: allFormulas,
411+
);
412+
413+
// Refresh the count after modal closes
414+
await _loadTransportModeCounts();
415+
416+
if (!confirmed) {
417+
// User cancelled - don't proceed with fare calculation
418+
return;
419+
}
420+
}
421+
}
422+
423+
// Proceed with fare calculation
424+
await _controller.calculateFare();
425+
}
295426
}

0 commit comments

Comments
 (0)