From afff401e503188143bff533ed35f4b94bae6fe5e Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:39:44 -0400 Subject: [PATCH 01/15] Added SheetNavigationManager!!! --- lib/screens/map_screen.dart | 548 ++++++++------------- lib/services/sheet_navigation_manager.dart | 308 ++++++++++++ 2 files changed, 508 insertions(+), 348 deletions(-) create mode 100644 lib/services/sheet_navigation_manager.dart diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 8ccee2d..f93a289 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -7,6 +7,7 @@ import 'dart:math' as math; import 'package:bluebus/globals.dart'; import 'package:bluebus/providers/theme_provider.dart'; import 'package:bluebus/screens/new_features_screen.dart'; +import 'package:bluebus/services/sheet_navigation_manager.dart'; import 'package:bluebus/widgets/building_sheet.dart'; import 'package:bluebus/widgets/bus_sheet.dart'; import 'package:bluebus/widgets/dialog.dart'; @@ -152,7 +153,8 @@ class _MaizeBusCoreState extends State { int _routesFingerprint = 0; // store persistent bottom sheet controller - PersistentBottomSheetController? _bottomSheetController; + // PersistentBottomSheetController? _bottomSheetController; + SheetNavigationManager? sheetNavigationManager; // GoogleMaps styles String _darkMapStyle = "{}"; @@ -170,6 +172,19 @@ class _MaizeBusCoreState extends State { super.initState(); _setupConnectivityMonitoring(); + sheetNavigationManager = SheetNavigationManager( + context: context, + addFavoriteStop: _addFavoriteStop, + onDirectionsChangeSelection: onDirectionsChangeSelection, + onSelectJourney: onSelectJourney, + onDirectionsResolved: onDirectionsResolved, + onRouteSelectorApply: onRouteSelectorApply, + onSearch: onSearch, + onSelectStop: onSelectStop, + onUnfavorite: onUnfavorite, + removeFavoriteStop: _removeFavoriteStop + ); + WidgetsBinding.instance.addPostFrameCallback((_) { try { _busProviderRef = Provider.of(context, listen: false); @@ -797,12 +812,19 @@ class _MaizeBusCoreState extends State { Haptics.vibrate(HapticsType.light); } catch (e) {} - _showStopSheet( + sheetNavigationManager?.showStopSheet( stop.id, stop.name, stop.location.latitude, stop.location.longitude, ); + + // _showStopSheet( + // stop.id, + // stop.name, + // stop.location.latitude, + // stop.location.longitude, + // ); }, rotation: stop.rotation, anchor: Offset(0.5, 0.5), @@ -955,7 +977,8 @@ class _MaizeBusCoreState extends State { try { Haptics.vibrate(HapticsType.light); } catch (e) {} - _showBusSheet(bus.id); + sheetNavigationManager?.showBusSheet(bus.id); + // _showBusSheet(bus.id); }, ); }) @@ -989,7 +1012,8 @@ class _MaizeBusCoreState extends State { icon: busIcon!, rotation: bus.heading, anchor: const Offset(0.5, 0.5), - onTap: () => _showBusSheet(bus.id), + // onTap: () => _showBusSheet(bus.id), + onTap: () => sheetNavigationManager?.showBusSheet(bus.id) ), ); } @@ -1115,242 +1139,14 @@ class _MaizeBusCoreState extends State { icon: icon, rotation: bus.heading, anchor: const Offset(0.5, 0.5), - onTap: () => _showBusSheet(bus.id), - ); - } - - void _showBusRoutesModal(List allRouteLines) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return RouteSelectorModal( - availableRoutes: _availableRoutes, - initialSelectedRoutes: _selectedRoutes, - onApply: (Set newSelection) async { - if (newSelection.difference(_selectedRoutes).isNotEmpty || - _selectedRoutes.difference(newSelection).isNotEmpty) { - setState(() { - _selectedRoutes.clear(); - _selectedRoutes.addAll(newSelection); - }); - _updateDisplayedRoutes(); - - // Save the new selection - await _saveSelectedRoutes(); - } - }, - canVibrate: canVibrate, - ); - }, - ); - } - - void _showSearchSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return SearchSheet( - onSearch: (Location location, bool isBusStop, String stopID) { - final searchCoordinates = location.latlng; - - // Clear any existing search location marker first - _removeSearchLocationMarker(); - - // null-proofing - if (searchCoordinates != null) { - if (isBusStop) { - _centerOnLocation( - false, - searchCoordinates.latitude, - searchCoordinates.longitude, - ); - _showStopSheet( - stopID, - location.name, - searchCoordinates.latitude, - searchCoordinates.longitude, - ); - } else { - _centerOnLocation( - false, - searchCoordinates.latitude, - searchCoordinates.longitude, - ); - _showBuildingSheet(location); - } - } else { - // Location has no coordinates - } - }, - ); - }, + // onTap: () => _showBusSheet(bus.id), + onTap: () => sheetNavigationManager?.showBusSheet(bus.id) ); } - void _showBuildingSheet(Location place) { - // Show red pin at the location - _showSearchLocationMarker(place.latlng!.latitude, place.latlng!.longitude); - - _bottomSheetController = showBottomSheet( - context: context, - enableDrag: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return BuildingSheet( - building: place, - onGetDirections: (Location location) { - Map? start; - Map? end = { - 'lat': place.latlng!.latitude, - 'lon': place.latlng!.longitude, - }; - - _showDirectionsSheet( - start, - end, - "Current Location", - place.name, - false, - ); - }, - ); - }, - ); - } - - void _showDirectionsSheet( - Map? start, - Map? end, - String startLoc, - String endLoc, - bool dontUseLocation, - ) { - _bottomSheetController = showBottomSheet( - context: context, - enableDrag: true, - - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return DraggableScrollableSheet( - initialChildSize: 0.5, - maxChildSize: 0.9, - minChildSize: 0, - expand: false, - snap: true, - snapSizes: [0.5, 0.9], - builder: (context, scrollController) { - return DirectionsSheet( - origin: start, - dest: end, - useOrigin: dontUseLocation, - originName: startLoc, - destName: endLoc, // true = start changed, false = end changed - onChangeSelection: (Location location, bool startChanged) { - // Clear any existing search location marker before showing new destination - _removeSearchLocationMarker(); - - if (startChanged) { - // Show red pin for new start location if it's a building (not bus stop) - if (!location.isBusStop) { - _showSearchLocationMarker( - location.latlng!.latitude, - location.latlng!.longitude, - ); - } - - _showDirectionsSheet( - { - 'lat': location.latlng!.latitude, - 'lon': location.latlng!.longitude, - }, - end, - location.name, - endLoc, - true, - ); - } else { - // Show red pin for new destination if it's a building (non-bus stop) - if (!location.isBusStop) { - _showSearchLocationMarker( - location.latlng!.latitude, - location.latlng!.longitude, - ); - } - - _showDirectionsSheet( - start, - { - 'lat': location.latlng!.latitude, - 'lon': location.latlng!.longitude, - }, - startLoc, - location.name, - dontUseLocation, - ); - } - }, - onSelectJourney: (journey) { - _displayJourneyOnMap( - journey, - getColor(context, ColorType.opposite), - ); - }, - onResolved: (orig, dest) { - // Cache resolved coordinates for virtual origin/destination resolution - _lastJourneyRequestOrigin = orig; - _lastJourneyRequestDest = dest; - }, - scrollController: scrollController, - ); - }, - ); - }, - ); - } - - _showJourneySheetOnReopen() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return DraggableScrollableSheet( - initialChildSize: 0.9, - minChildSize: 0, - maxChildSize: 0.9, - snap: true, - expand: false, - - builder: (BuildContext context, ScrollController scrollController) { - return Container( - decoration: BoxDecoration( - color: getColor(context, ColorType.background), - borderRadius: BorderRadius.vertical(top: Radius.circular(30)), - ), - child: ListView( - controller: scrollController, - padding: const EdgeInsets.all(20), - shrinkWrap: true, - children: [ - Text( - 'Steps', - style: TextStyle(fontSize: 30, fontWeight: FontWeight.w700), - ), - SizedBox(height: 15), - JourneyBody(journey: currDisplayed), - ], - ), - ); - }, - ); - }, - ); - } + + // Display a Journey on the map void _displayJourneyOnMap(Journey journey, Color walkLineColor) async { currDisplayed = journey; @@ -1786,110 +1582,7 @@ class _MaizeBusCoreState extends State { } } - void _showBusSheet(String busID) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - backgroundColor: Colors.transparent, - builder: (context) => Container( - child: DraggableScrollableSheet( - initialChildSize: 0.85, - maxChildSize: 0.85, - snap: true, - - builder: (BuildContext context, ScrollController scrollController) { - return BusSheet( - busID: busID, - scrollController: scrollController, - onSelectStop: (name, id) { - Navigator.pop(context); // Close the current modal - LatLng? latLong = getLatLongFromStopID(id); - if (latLong != null) { - _showStopSheet(id, name, latLong.latitude, latLong.longitude); - } else { - showMaizebusOKDialog( - contextIn: context, - title: const Text("Error"), - content: const Text("Couldn't load stop."), - ); - } - }, - ); - }, - ), - ), - ); - } - - void _showFavoritesSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return FavoritesSheet( - onSelectStop: (name, id) { - LatLng? latLong = getLatLongFromStopID(id); - if (latLong != null) { - _showStopSheet(id, name, latLong.latitude, latLong.longitude); - } else { - showMaizebusOKDialog( - contextIn: context, - title: const Text('Error'), - content: const Text('Couldn\'t load stop.'), - ); - } - }, - onUnfavorite: (stpid) { - // update in memory and marker icons immediately - setState(() { - _favoriteStops.remove(stpid); - }); - _setStopFavorited(stpid, false); - }, - ); - }, - ); - } - - void _showStopSheet(String stopID, String stopName, double lat, double long) { - final busProvider = Provider.of(context, listen: false); - - showModalBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return StopSheet( - stopID: stopID, - stopName: stopName, - onFavorite: _addFavoriteStop, - onUnFavorite: _removeFavoriteStop, - showBusSheet: (busId) { - // When someone clicks "See all stops for this bus" this callback runs - debugPrint("Got 'See all stops' click for Bus ${busId}"); - Navigator.pop(context); // Close the current modal - _showBusSheet(busId); - }, - busProvider: busProvider, - onGetDirections: () { - Map? start; - Map? end = {'lat': lat, 'lon': long}; - - _showDirectionsSheet( - start, - end, - "Current Location", - stopName, - false, - ); - }, - ); - }, - ).then((_) {}); - } + // lighter function for when we need to get location // over and over without constantly doing a full @@ -2000,6 +1693,151 @@ class _MaizeBusCoreState extends State { } } + void onSearch(Location location, bool isBusStop, String stopID) { + final searchCoordinates = location.latlng; + + // Clear any existing search location marker first + _removeSearchLocationMarker(); + + // null-proofing + if (searchCoordinates != null) { + if (isBusStop) { + _centerOnLocation( + false, + searchCoordinates.latitude, + searchCoordinates.longitude, + ); + sheetNavigationManager?.showStopSheet( + stopID, + location.name, + searchCoordinates.latitude, + searchCoordinates.longitude, + ); + // _showStopSheet( + // stopID, + // location.name, + // searchCoordinates.latitude, + // searchCoordinates.longitude, + // ); + } else { + _centerOnLocation( + false, + searchCoordinates.latitude, + searchCoordinates.longitude, + ); + _showSearchLocationMarker( + location.latlng!.latitude, + location.latlng!.longitude + ); + sheetNavigationManager?.showBuildingSheet(location); + // _showBuildingSheet(location); + } + } else { + // Location has no coordinates + } + } + + void onSelectStop(name, id) { + LatLng? latLong = getLatLongFromStopID(id); + if (latLong != null) { + sheetNavigationManager?.showStopSheet(id, name, latLong.latitude, latLong.longitude); + } else { + showMaizebusOKDialog( + contextIn: context, + title: const Text('Error'), + content: const Text('Couldn\'t load stop.'), + ); + } + } + + void onUnfavorite(String stpid) { + // update in memory and marker icons immediately + setState(() { + _favoriteStops.remove(stpid); + }); + _setStopFavorited(stpid, false); + } + + void onRouteSelectorApply(Set newSelection) async { + if (newSelection.difference(_selectedRoutes).isNotEmpty || + _selectedRoutes.difference(newSelection).isNotEmpty) { + setState(() { + _selectedRoutes.clear(); + _selectedRoutes.addAll(newSelection); + }); + _updateDisplayedRoutes(); + + // Save the new selection + await _saveSelectedRoutes(); + } + } + + void onDirectionsChangeSelection( + Location location, + bool startChanged, + Map? start, + Map? end, + String startLoc, + String endLoc, + bool dontUseLocation + ) { + // Clear any existing search location marker before showing new destination + _removeSearchLocationMarker(); + + if (startChanged) { + // Show red pin for new start location if it's a building (not bus stop) + if (!location.isBusStop) { + _showSearchLocationMarker( + location.latlng!.latitude, + location.latlng!.longitude, + ); + } + + sheetNavigationManager?.showDirectionsSheet( + { + 'lat': location.latlng!.latitude, + 'lon': location.latlng!.longitude, + }, + end, + location.name, + endLoc, + true, + ); + } else { + // Show red pin for new destination if it's a building (non-bus stop) + if (!location.isBusStop) { + _showSearchLocationMarker( + location.latlng!.latitude, + location.latlng!.longitude, + ); + } + + sheetNavigationManager?.showDirectionsSheet( + start, + { + 'lat': location.latlng!.latitude, + 'lon': location.latlng!.longitude, + }, + startLoc, + location.name, + dontUseLocation, + ); + } + } + + void onSelectJourney(Journey journey) { + _displayJourneyOnMap( + journey, + getColor(context, ColorType.opposite), + ); + } + + void onDirectionsResolved(Map orig, Map dest) { + // Cache resolved coordinates for virtual origin/destination resolution + _lastJourneyRequestOrigin = orig; + _lastJourneyRequestDest = dest; + } + @override Widget build(BuildContext context) { // Only update bus markers when buses change @@ -2077,10 +1915,13 @@ class _MaizeBusCoreState extends State { // If showing a persistent bottom sheet, close it. // Fix android back button for buildings sheet and journey sheet (doesn't work without this) - if (_bottomSheetController != null) { - _bottomSheetController!.close(); - _bottomSheetController = null; - _removeSearchLocationMarker(); + // if (_bottomSheetController != null) { + // _bottomSheetController!.close(); + // _bottomSheetController = null; + // _removeSearchLocationMarker(); + // } + if (sheetNavigationManager!.isBottomSheetControllerAlive()) { + } }, child: Stack( @@ -2577,7 +2418,10 @@ class _MaizeBusCoreState extends State { ), ), child: ElevatedButton.icon( - onPressed: _showJourneySheetOnReopen, + onPressed: () { + sheetNavigationManager?.showJourneySheetOnReopen(currDisplayed); + }, + // onPressed: _showJourneySheetOnReopen, style: ElevatedButton.styleFrom( backgroundColor: getColor( context, @@ -2698,9 +2542,15 @@ class _MaizeBusCoreState extends State { HapticsType.light, ); } - _showBusRoutesModal( + sheetNavigationManager?.showBusRoutesModal( busProvider.routes, + _availableRoutes, + _selectedRoutes, + canVibrate ); + // _showBusRoutesModal( + // busProvider.routes, + // ); }, heroTag: 'routes_fab', elevation: @@ -2748,7 +2598,8 @@ class _MaizeBusCoreState extends State { HapticsType.light, ); } - _showFavoritesSheet(); + sheetNavigationManager?.showFavoritesSheet(); + // _showFavoritesSheet(); }, heroTag: 'favorites_fab', elevation: 0, @@ -2795,7 +2646,8 @@ class _MaizeBusCoreState extends State { HapticsType.light, ); } - _showSearchSheet(); + sheetNavigationManager?.showSearchSheet(); + // _showSearchSheet(); }, style: ElevatedButton.styleFrom( alignment: Alignment.centerLeft, diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart new file mode 100644 index 0000000..99bd1fd --- /dev/null +++ b/lib/services/sheet_navigation_manager.dart @@ -0,0 +1,308 @@ +import 'package:bluebus/constants.dart'; +import 'package:bluebus/globals.dart'; +import 'package:bluebus/models/bus_route_line.dart'; +import 'package:bluebus/models/journey.dart'; +import 'package:bluebus/providers/bus_provider.dart'; +import 'package:bluebus/widgets/building_sheet.dart'; +import 'package:bluebus/widgets/bus_sheet.dart'; +import 'package:bluebus/widgets/dialog.dart'; +import 'package:bluebus/widgets/directions_sheet.dart'; +import 'package:bluebus/widgets/favorites_sheet.dart'; +import 'package:bluebus/widgets/journey_results_widget.dart'; +import 'package:bluebus/widgets/route_selector_modal.dart'; +import 'package:bluebus/widgets/search_sheet_main.dart'; +import 'package:bluebus/widgets/stop_sheet.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:provider/provider.dart'; + +class SheetNavigationManager { + PersistentBottomSheetController? _bottomSheetController; + + BuildContext context; + Future Function(String stpid, String name) addFavoriteStop; + Function( + Location location, + bool startChanged, + Map? start, + Map? end, + String startLoc, + String endLoc, + bool dontUseLocation + ) onDirectionsChangeSelection; + Function(Journey) onSelectJourney; + Function(Map, Map dest) onDirectionsResolved; + Function(Set) onRouteSelectorApply; + Function(Location location, bool isBusStop, String stopID) onSearch; + Function(String name, String id) onSelectStop; + Function(String stpid) onUnfavorite; + Future Function(String stpid, String name) removeFavoriteStop; + + SheetNavigationManager({ + required this.context, + required this.addFavoriteStop, + required this.onDirectionsChangeSelection, + required this.onSelectJourney, + required this.onDirectionsResolved, + required this.onRouteSelectorApply, + required this.onSearch, + required this.onSelectStop, + required this.onUnfavorite, + required this.removeFavoriteStop + }); + + bool isBottomSheetControllerAlive() { + return _bottomSheetController != null; + } + + void resetBottomSheetController() { + _bottomSheetController!.close(); + _bottomSheetController = null; + } + + void showBuildingSheet(Location place) { + // Show red pin at the location + // _showSearchLocationMarker(place.latlng!.latitude, place.latlng!.longitude); + + _bottomSheetController = showBottomSheet( + context: context, + enableDrag: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return BuildingSheet( + building: place, + onGetDirections: (Location location) { + Map? start; + Map? end = { + 'lat': place.latlng!.latitude, + 'lon': place.latlng!.longitude, + }; + + showDirectionsSheet( + start, + end, + "Current Location", + place.name, + false + ); + }, + ); + }, + ); + } + + void showDirectionsSheet( + Map? start, + Map? end, + String startLoc, + String endLoc, + bool dontUseLocation, + ) { + _bottomSheetController = showBottomSheet( + context: context, + enableDrag: true, + + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.5, + maxChildSize: 0.9, + minChildSize: 0, + expand: false, + snap: true, + snapSizes: [0.5, 0.9], + builder: (context, scrollController) { + return DirectionsSheet( + origin: start, + dest: end, + useOrigin: dontUseLocation, + originName: startLoc, + destName: endLoc, // true = start changed, false = end changed + onChangeSelection: (Location location, bool startChanged) { + onDirectionsChangeSelection( + location, + startChanged, + start, + end, + startLoc, + endLoc, + dontUseLocation + ); + }, + onSelectJourney: onSelectJourney, + onResolved: onDirectionsResolved, + scrollController: scrollController, + ); + }, + ); + }, + ); + } + + void showJourneySheetOnReopen(Journey currDisplayed) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.9, + minChildSize: 0, + maxChildSize: 0.9, + snap: true, + expand: false, + + builder: (BuildContext context, ScrollController scrollController) { + return Container( + decoration: BoxDecoration( + color: getColor(context, ColorType.background), + borderRadius: BorderRadius.vertical(top: Radius.circular(30)), + ), + child: ListView( + controller: scrollController, + padding: const EdgeInsets.all(20), + shrinkWrap: true, + children: [ + Text( + 'Steps', + style: TextStyle(fontSize: 30, fontWeight: FontWeight.w700), + ), + SizedBox(height: 15), + JourneyBody(journey: currDisplayed), + ], + ), + ); + }, + ); + }, + ); + } + + void showBusRoutesModal( + List allRouteLines, + List> availableRoutes, + Set selectedRoutes, + bool canVibrate + ) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return RouteSelectorModal( + availableRoutes: availableRoutes, + initialSelectedRoutes: selectedRoutes, + onApply: onRouteSelectorApply, + canVibrate: canVibrate, + ); + }, + ); + } + + void showSearchSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return SearchSheet( + onSearch: onSearch, + ); + }, + ); + } + + void showBusSheet(String busID) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + child: DraggableScrollableSheet( + initialChildSize: 0.85, + maxChildSize: 0.85, + snap: true, + + builder: (BuildContext context, ScrollController scrollController) { + return BusSheet( + busID: busID, + scrollController: scrollController, + onSelectStop: (name, id) { + Navigator.pop(context); // Close the current modal + LatLng? latLong = getLatLongFromStopID(id); + if (latLong != null) { + showStopSheet(id, name, latLong.latitude, latLong.longitude); + } else { + showMaizebusOKDialog( + contextIn: context, + title: const Text("Error"), + content: const Text("Couldn't load stop."), + ); + } + }, + ); + }, + ), + ), + ); + } + + void showFavoritesSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return FavoritesSheet( + onSelectStop: onSelectStop, + onUnfavorite: onUnfavorite, + ); + }, + ); + } + + void showStopSheet( + String stopID, + String stopName, + double lat, + double long, + ) { + final busProvider = Provider.of(context, listen: false); + + showModalBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return StopSheet( + stopID: stopID, + stopName: stopName, + onFavorite: addFavoriteStop, + onUnFavorite: removeFavoriteStop, + showBusSheet: (busId) { + // When someone clicks "See all stops for this bus" this callback runs + debugPrint("Got 'See all stops' click for Bus ${busId}"); + Navigator.pop(context); // Close the current modal + showBusSheet(busId); + }, + busProvider: busProvider, + onGetDirections: () { + Map? start; + Map? end = {'lat': lat, 'lon': long}; + + showDirectionsSheet( + start, + end, + "Current Location", + stopName, + false, + ); + }, + ); + }, + ).then((_) {}); + } + +} \ No newline at end of file From 0b86bea132e86a75a7869d9004af39ddf5618f90 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:28:06 -0400 Subject: [PATCH 02/15] First attempt at adding back button Things are still VERY broken, but it kind of works right now. What's still broken * The modal decides it wants to be the entire screen height after the first click * Swiping down doesn't work yet We'll keep chipping away at it --- lib/main.dart | 2 + lib/screens/map_screen.dart | 12 +- lib/services/sheet_navigation_manager.dart | 194 ++++++++++++++++++--- 3 files changed, 182 insertions(+), 26 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 4d99735..4d3bb6c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,8 @@ import 'services/bus_repository.dart'; import 'providers/bus_provider.dart'; import 'providers/theme_provider.dart'; +// TODO: Add descriptive comments to everything + // This function initializes the Flutter app and runs the MainApp widget void main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index f93a289..b17b456 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -812,7 +812,7 @@ class _MaizeBusCoreState extends State { Haptics.vibrate(HapticsType.light); } catch (e) {} - sheetNavigationManager?.showStopSheet( + sheetNavigationManager?.showStopSheetFromMap( stop.id, stop.name, stop.location.latitude, @@ -977,7 +977,7 @@ class _MaizeBusCoreState extends State { try { Haptics.vibrate(HapticsType.light); } catch (e) {} - sheetNavigationManager?.showBusSheet(bus.id); + sheetNavigationManager?.showBusSheetFromMap(bus.id); // _showBusSheet(bus.id); }, ); @@ -1013,7 +1013,7 @@ class _MaizeBusCoreState extends State { rotation: bus.heading, anchor: const Offset(0.5, 0.5), // onTap: () => _showBusSheet(bus.id), - onTap: () => sheetNavigationManager?.showBusSheet(bus.id) + onTap: () => sheetNavigationManager?.showBusSheetFromMap(bus.id) ), ); } @@ -1140,7 +1140,7 @@ class _MaizeBusCoreState extends State { rotation: bus.heading, anchor: const Offset(0.5, 0.5), // onTap: () => _showBusSheet(bus.id), - onTap: () => sheetNavigationManager?.showBusSheet(bus.id) + onTap: () => sheetNavigationManager?.showBusSheetFromMap(bus.id) ); } @@ -1707,7 +1707,7 @@ class _MaizeBusCoreState extends State { searchCoordinates.latitude, searchCoordinates.longitude, ); - sheetNavigationManager?.showStopSheet( + sheetNavigationManager?.showStopSheetFromMap( stopID, location.name, searchCoordinates.latitude, @@ -1740,7 +1740,7 @@ class _MaizeBusCoreState extends State { void onSelectStop(name, id) { LatLng? latLong = getLatLongFromStopID(id); if (latLong != null) { - sheetNavigationManager?.showStopSheet(id, name, latLong.latitude, latLong.longitude); + sheetNavigationManager?.showStopSheetFromMap(id, name, latLong.latitude, latLong.longitude); } else { showMaizebusOKDialog( contextIn: context, diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index 99bd1fd..8dc3eaf 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -16,6 +16,60 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:provider/provider.dart'; +// class BackStackStep { +// SheetNavigationManager? navigationManager; +// void recall() {} +// } + +// class BusSheetBackStackStep { +// SheetNavigationManager? navigationManager; +// String busId; +// BusSheetBackStackStep({ +// required this.navigationManager, +// required this.busId +// }); +// void recall() { +// navigationManager?.showBusSheet(busId); +// } +// } + +// class StopSheetBackStackStep { +// SheetNavigationManager? navigationManager; +// String stopID; +// String stopName; +// double lat; +// double long; + +// StopSheetBackStackStep({ +// required this.navigationManager, +// required this.stopID, +// required this.stopName, +// required this.lat, +// required this.long +// }); + +// void recall() { + +// } +// } + +class SheetNavigator extends StatelessWidget { + final Widget initialSheet; + SheetNavigator({ + required this.initialSheet + }); + + @override + Widget build(BuildContext context) { + return Navigator( + onGenerateRoute: (settings) { + return MaterialPageRoute(builder: (context) => initialSheet); + }, + ); + } + +} + class SheetNavigationManager { PersistentBottomSheetController? _bottomSheetController; @@ -38,6 +92,9 @@ class SheetNavigationManager { Function(String stpid) onUnfavorite; Future Function(String stpid, String name) removeFavoriteStop; + // List backStack; + // bool was_sheet_closed_programatically = false; // Flag to know whether the last-closed sheet was closed programatically (e.g. while executing a back gesture) versus if the user swiped it away + SheetNavigationManager({ required this.context, required this.addFavoriteStop, @@ -60,6 +117,10 @@ class SheetNavigationManager { _bottomSheetController = null; } + // void onAnySheetDismissed() { + // backStack.clear(); + // } + void showBuildingSheet(Location place) { // Show red pin at the location // _showSearchLocationMarker(place.latlng!.latitude, place.latlng!.longitude); @@ -212,27 +273,64 @@ class SheetNavigationManager { ); } - void showBusSheet(String busID) { + // void onAnySheetClosed() { + + // was_sheet_closed_programatically = false; // Reset to our default assumption that the user closed the sheet, unless this flag is set to true + // } + + // TODO: Add a flag to show whether the modal was closed programatically + // i.e. before closing the model programatically on line 262, set this variable + // and then in onClosing check it. If the user swiped it away, clear the back stack + + void showBusSheetFromSheet(String busID, BuildContext parentContext, ScrollController scrollController) { + Navigator.of(parentContext).push( + MaterialPageRoute(builder: (context) => + BusSheet( + busID: busID, + scrollController: scrollController, + onSelectStop: (name, id) { + + // Navigator.pop(context); // Close the current modal + LatLng? latLong = getLatLongFromStopID(id); + if (latLong != null) { + // showStopSheet(id, name, latLong.latitude, latLong.longitude); + showStopSheetFromSheet(id, name, latLong.latitude, latLong.longitude, context, scrollController); + } else { + showMaizebusOKDialog( + contextIn: context, + title: const Text("Error"), + content: const Text("Couldn't load stop."), + ); + } + }, + ) + ) + ); + } + + void showBusSheetFromMap(String busID) { showModalBottomSheet( context: context, isScrollControlled: true, isDismissible: true, backgroundColor: Colors.transparent, - builder: (context) => Container( - child: DraggableScrollableSheet( + builder: (context) => SheetNavigator( + initialSheet: DraggableScrollableSheet( initialChildSize: 0.85, maxChildSize: 0.85, snap: true, - + builder: (BuildContext context, ScrollController scrollController) { return BusSheet( busID: busID, scrollController: scrollController, onSelectStop: (name, id) { - Navigator.pop(context); // Close the current modal + + // Navigator.pop(context); // Close the current modal LatLng? latLong = getLatLongFromStopID(id); if (latLong != null) { - showStopSheet(id, name, latLong.latitude, latLong.longitude); + // showStopSheet(id, name, latLong.latitude, latLong.longitude); + showStopSheetFromSheet(id, name, latLong.latitude, latLong.longitude, context, scrollController); } else { showMaizebusOKDialog( contextIn: context, @@ -243,7 +341,7 @@ class SheetNavigationManager { }, ); }, - ), + ) ), ); } @@ -262,30 +360,29 @@ class SheetNavigationManager { ); } - void showStopSheet( + void showStopSheetFromSheet( String stopID, String stopName, double lat, double long, + BuildContext parentContext, // Context needed to reference the "shell" outside this stop sheet that switches between contents + ScrollController scrollController ) { final busProvider = Provider.of(context, listen: false); - showModalBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return StopSheet( + Navigator.of(parentContext).push( + MaterialPageRoute(builder: (context) => + StopSheet( stopID: stopID, stopName: stopName, onFavorite: addFavoriteStop, onUnFavorite: removeFavoriteStop, showBusSheet: (busId) { // When someone clicks "See all stops for this bus" this callback runs - debugPrint("Got 'See all stops' click for Bus ${busId}"); - Navigator.pop(context); // Close the current modal - showBusSheet(busId); + // debugPrint("Got 'See all stops' click for Bus ${busId}"); + // Navigator.pop(context); // Close the current modal + // showBusSheetFromMap(busId); + showBusSheetFromSheet(busId, parentContext, scrollController); }, busProvider: busProvider, onGetDirections: () { @@ -300,8 +397,65 @@ class SheetNavigationManager { false, ); }, - ); - }, + ) + ) + ); + } + + void showStopSheetFromMap( + String stopID, + String stopName, + double lat, + double long, + ) { + final busProvider = Provider.of(context, listen: false); + + showModalBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => SheetNavigator(initialSheet: DraggableScrollableSheet( + initialChildSize: 0.85, + maxChildSize: 0.85, + snap: true, + builder: (BuildContext context, ScrollController scrollController) { + return SheetNavigator( + initialSheet: StopSheet( + stopID: stopID, + stopName: stopName, + onFavorite: addFavoriteStop, + onUnFavorite: removeFavoriteStop, + showBusSheet: (busId) { + // When someone clicks "See all stops for this bus" this callback runs + debugPrint("Got 'See all stops' click for Bus ${busId}"); + // Navigator.pop(context); // Close the current modal + // showBusSheetFromMap(busId); + // TODO: Fix this here + showBusSheetFromSheet(busId, context, scrollController); + }, + busProvider: busProvider, + onGetDirections: () { + Map? start; + Map? end = {'lat': lat, 'lon': long}; + + showDirectionsSheet( + start, + end, + "Current Location", + stopName, + false, + ); + }, + ) + ); + } + + ) + ) + + + , ).then((_) {}); } From f97d8807a16faa509332008baed9b02f1b24bf6a Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:55:15 -0400 Subject: [PATCH 03/15] Removed extra Navigator, implemented simpler back stack --- lib/services/sheet_navigation_manager.dart | 345 +++++++++++++-------- 1 file changed, 221 insertions(+), 124 deletions(-) diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index 8dc3eaf..efa8317 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -53,23 +53,87 @@ import 'package:provider/provider.dart'; // } // } -class SheetNavigator extends StatelessWidget { - final Widget initialSheet; - SheetNavigator({ - required this.initialSheet - }); - +class SheetNavigatorState extends State { + List _stack = []; + int oldStackLength = 0; // Used to track the reverse animation + + void pushWidget(Widget sheet) { + // debugPrint("GOT PUSHWIDGET CALL! ${sheet}"); + setState(() { + _stack.add(sheet); + }); + } + + @override + void initState() { + super.initState(); + _stack.add(widget.getInitialSheetFromOutside(widget.scrollController, pushWidget)); + } + + @override Widget build(BuildContext context) { - return Navigator( - onGenerateRoute: (settings) { - return MaterialPageRoute(builder: (context) => initialSheet); - }, - ); + + return PopScope( + canPop: _stack.length <= 1, + onPopInvokedWithResult: (didPop, result) { + // debugPrint("Pop invoked!"); + if (!didPop) { + setState(() {_stack.removeLast();}); + } + }, child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + final isEntering = child.key == ValueKey(_stack.length); + final isGoingBackwards = oldStackLength > _stack.length; + oldStackLength = _stack.length; + + return SlideTransition( + position: Tween( + begin: + NEXT STEPS TODO: Figure out these animations + isGoingBackwards ? ( + isEntering ? + const Offset(-1, 0.0) : + const Offset(1, 0.0) + ) : isEntering ? + const Offset(1, 0.0) : + const Offset(-1, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)), + child: child, + ); + }, + child: KeyedSubtree( + key: ValueKey(_stack.length), + child: SizedBox.expand(child: _stack.last), + ) + + ) + ); + // return Navigator( + // onGenerateRoute: (settings) { + // return MaterialPageRoute( + // builder: (context) => initialSheet + // ); + // }, + // ); } } +class SheetNavigator extends StatefulWidget { + final Function(ScrollController, Function(Widget)) getInitialSheetFromOutside; + final ScrollController scrollController; + SheetNavigator({ + required this.scrollController, + required this.getInitialSheetFromOutside + }); + + @override + State createState() => SheetNavigatorState(); +} + class SheetNavigationManager { PersistentBottomSheetController? _bottomSheetController; @@ -282,29 +346,55 @@ class SheetNavigationManager { // i.e. before closing the model programatically on line 262, set this variable // and then in onClosing check it. If the user swiped it away, clear the back stack - void showBusSheetFromSheet(String busID, BuildContext parentContext, ScrollController scrollController) { - Navigator.of(parentContext).push( - MaterialPageRoute(builder: (context) => - BusSheet( - busID: busID, - scrollController: scrollController, - onSelectStop: (name, id) { + // void showBusSheetFromSheet(String busID, BuildContext parentContext, ScrollController scrollController) { + // Navigator.of(parentContext).push( + // PageRouteBuilder( + // opaque: false, + // pageBuilder: (context, animation, secondaryAnimation) => + // BusSheet( + // busID: busID, + // scrollController: scrollController, + // onSelectStop: (name, id) { - // Navigator.pop(context); // Close the current modal - LatLng? latLong = getLatLongFromStopID(id); - if (latLong != null) { - // showStopSheet(id, name, latLong.latitude, latLong.longitude); - showStopSheetFromSheet(id, name, latLong.latitude, latLong.longitude, context, scrollController); - } else { - showMaizebusOKDialog( - contextIn: context, - title: const Text("Error"), - content: const Text("Couldn't load stop."), - ); - } - }, - ) - ) + // // Navigator.pop(context); // Close the current modal + // LatLng? latLong = getLatLongFromStopID(id); + // if (latLong != null) { + // // showStopSheet(id, name, latLong.latitude, latLong.longitude); + // showStopSheetFromSheet(id, name, latLong.latitude, latLong.longitude, context, scrollController); + // } else { + // showMaizebusOKDialog( + // contextIn: context, + // title: const Text("Error"), + // content: const Text("Couldn't load stop."), + // ); + // } + // }, + // ) + // ) + // ); + // } + + BusSheet getBusSheet(String busID, ScrollController scrollController, Function(Widget) pushNewSheet) { + return BusSheet( + busID: busID, + scrollController: scrollController, + onSelectStop: (name, id) { + + // Navigator.pop(context); // Close the current modal + LatLng? latLong = getLatLongFromStopID(id); + if (latLong != null) { + // showStopSheet(id, name, latLong.latitude, latLong.longitude); + pushNewSheet(getStopSheet(id, name, latLong.latitude, latLong.longitude, pushNewSheet, scrollController)); + + // showStopSheetFromSheet(id, name, latLong.latitude, latLong.longitude, context, scrollController); + } else { + showMaizebusOKDialog( + contextIn: context, + title: const Text("Error"), + content: const Text("Couldn't load stop."), + ); + } + }, ); } @@ -314,36 +404,27 @@ class SheetNavigationManager { isScrollControlled: true, isDismissible: true, backgroundColor: Colors.transparent, - builder: (context) => SheetNavigator( - initialSheet: DraggableScrollableSheet( + // builder: (context) => SheetNavigator( + // initialSheet: + builder: (context) => DraggableScrollableSheet( initialChildSize: 0.85, maxChildSize: 0.85, snap: true, - builder: (BuildContext context, ScrollController scrollController) { - return BusSheet( - busID: busID, + builder: + + (BuildContext context, ScrollController scrollController) { + return Container( + + child: SheetNavigator( scrollController: scrollController, - onSelectStop: (name, id) { - - // Navigator.pop(context); // Close the current modal - LatLng? latLong = getLatLongFromStopID(id); - if (latLong != null) { - // showStopSheet(id, name, latLong.latitude, latLong.longitude); - showStopSheetFromSheet(id, name, latLong.latitude, latLong.longitude, context, scrollController); - } else { - showMaizebusOKDialog( - contextIn: context, - title: const Text("Error"), - content: const Text("Couldn't load stop."), - ); - } - }, + getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushNewSheet) => getBusSheet(busID, scrollControllerLocal, pushNewSheet) + ) ); }, ) - ), - ); + ); + // ); } void showFavoritesSheet() { @@ -360,45 +441,81 @@ class SheetNavigationManager { ); } - void showStopSheetFromSheet( - String stopID, - String stopName, - double lat, - double long, - BuildContext parentContext, // Context needed to reference the "shell" outside this stop sheet that switches between contents - ScrollController scrollController - ) { - final busProvider = Provider.of(context, listen: false); + // void showStopSheetFromSheet( + // String stopID, + // String stopName, + // double lat, + // double long, + // BuildContext parentContext, // Context needed to reference the "shell" outside this stop sheet that switches between contents + // ScrollController scrollController + // ) { + // final busProvider = Provider.of(context, listen: false); + + // Navigator.of(parentContext).push( + // PageRouteBuilder( + // opaque: false, + // pageBuilder: (context, animation, secondaryAnimation) => + // // MaterialPageRoute(builder: (context) => + // StopSheet( + // stopID: stopID, + // stopName: stopName, + // onFavorite: addFavoriteStop, + // onUnFavorite: removeFavoriteStop, + // showBusSheet: (busId) { + // // When someone clicks "See all stops for this bus" this callback runs + // // debugPrint("Got 'See all stops' click for Bus ${busId}"); + // // Navigator.pop(context); // Close the current modal + // // showBusSheetFromMap(busId); + // showBusSheetFromSheet(busId, parentContext, scrollController); + // }, + // busProvider: busProvider, + // onGetDirections: () { + // Map? start; + // Map? end = {'lat': lat, 'lon': long}; + + // showDirectionsSheet( + // start, + // end, + // "Current Location", + // stopName, + // false, + // ); + // }, + // ) + // ) + // ); + // } - Navigator.of(parentContext).push( - MaterialPageRoute(builder: (context) => - StopSheet( - stopID: stopID, - stopName: stopName, - onFavorite: addFavoriteStop, - onUnFavorite: removeFavoriteStop, - showBusSheet: (busId) { - // When someone clicks "See all stops for this bus" this callback runs - // debugPrint("Got 'See all stops' click for Bus ${busId}"); - // Navigator.pop(context); // Close the current modal - // showBusSheetFromMap(busId); - showBusSheetFromSheet(busId, parentContext, scrollController); - }, - busProvider: busProvider, - onGetDirections: () { - Map? start; - Map? end = {'lat': lat, 'lon': long}; + StopSheet getStopSheet(String stopID, String stopName, double lat, double long, Function(Widget) pushSheet, ScrollController scrollControllerLocal) { + final busProvider = Provider.of(context, listen: false); - showDirectionsSheet( - start, - end, - "Current Location", - stopName, - false, - ); - }, - ) - ) + return StopSheet( + stopID: stopID, + stopName: stopName, + onFavorite: addFavoriteStop, + onUnFavorite: removeFavoriteStop, + showBusSheet: (busId) { + // When someone clicks "See all stops for this bus" this callback runs + debugPrint("Got 'See all stops' click for Bus ${busId}"); + // Navigator.pop(context); // Close the current modal + // showBusSheetFromMap(busId); + // TODO: Fix this here + // showBusSheetFromSheet(busId, context, scrollController); + pushSheet(getBusSheet(busId, scrollControllerLocal, pushSheet)); + }, + busProvider: busProvider, + onGetDirections: () { + Map? start; + Map? end = {'lat': lat, 'lon': long}; + + showDirectionsSheet( + start, + end, + "Current Location", + stopName, + false, + ); + }, ); } @@ -408,51 +525,31 @@ class SheetNavigationManager { double lat, double long, ) { - final busProvider = Provider.of(context, listen: false); + showModalBottomSheet( context: context, isDismissible: true, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (context) => SheetNavigator(initialSheet: DraggableScrollableSheet( - initialChildSize: 0.85, - maxChildSize: 0.85, + // builder: (context) => SheetNavigator(initialSheet: + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.95, + maxChildSize: 0.95, snap: true, builder: (BuildContext context, ScrollController scrollController) { return SheetNavigator( - initialSheet: StopSheet( - stopID: stopID, - stopName: stopName, - onFavorite: addFavoriteStop, - onUnFavorite: removeFavoriteStop, - showBusSheet: (busId) { - // When someone clicks "See all stops for this bus" this callback runs - debugPrint("Got 'See all stops' click for Bus ${busId}"); - // Navigator.pop(context); // Close the current modal - // showBusSheetFromMap(busId); - // TODO: Fix this here - showBusSheetFromSheet(busId, context, scrollController); - }, - busProvider: busProvider, - onGetDirections: () { - Map? start; - Map? end = {'lat': lat, 'lon': long}; - - showDirectionsSheet( - start, - end, - "Current Location", - stopName, - false, - ); - }, - ) + scrollController: scrollController, + getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushSheet) { + return getStopSheet(stopID, stopName, lat, long, pushSheet, scrollControllerLocal); + } ); + + } ) - ) + // ) , From 2b26784785065966fd82b7c63bff1946fb949cba Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:03:06 -0400 Subject: [PATCH 04/15] Finally got animations looking good! Also figured out why the bus sheet was so jank--it made a doubly nested DraggableScrollableSheet --- lib/services/sheet_navigation_manager.dart | 55 +- lib/widgets/stop_sheet.dart | 916 +++++++++++---------- lib/widgets/upcoming_stops_widget.dart | 83 +- 3 files changed, 545 insertions(+), 509 deletions(-) diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index efa8317..ac73523 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -56,11 +56,14 @@ import 'package:provider/provider.dart'; class SheetNavigatorState extends State { List _stack = []; int oldStackLength = 0; // Used to track the reverse animation + bool isGoingBackwards = false; + + // NEXT STEPS TODO: Make isGoingBackwards a *state variable*. Every time a widget is pushed or popped, update the isGoingBackwards variable void pushWidget(Widget sheet) { - // debugPrint("GOT PUSHWIDGET CALL! ${sheet}"); setState(() { _stack.add(sheet); + isGoingBackwards = false; }); } @@ -78,32 +81,57 @@ class SheetNavigatorState extends State { canPop: _stack.length <= 1, onPopInvokedWithResult: (didPop, result) { // debugPrint("Pop invoked!"); + if (!didPop) { - setState(() {_stack.removeLast();}); + setState(() { + oldStackLength = _stack.length; + _stack.removeLast(); + isGoingBackwards = true; + }); } + // isGoingBackwards = oldStackLength > _stack.length; + }, child: AnimatedSwitcher( duration: const Duration(milliseconds: 400), transitionBuilder: (Widget child, Animation animation) { final isEntering = child.key == ValueKey(_stack.length); - final isGoingBackwards = oldStackLength > _stack.length; - oldStackLength = _stack.length; - + return SlideTransition( position: Tween( begin: - NEXT STEPS TODO: Figure out these animations + // NEXT STEPS TODO: Figure out these animations isGoingBackwards ? ( isEntering ? - const Offset(-1, 0.0) : - const Offset(1, 0.0) + const Offset(-1, 0.0) : const Offset(1, 0.0) ) : isEntering ? - const Offset(1, 0.0) : - const Offset(-1, 0.0), + const Offset(1, 0.0) : const Offset(-1, 0.0), end: Offset.zero, - ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)), + ).animate( + CurvedAnimation( + parent: animation, + curve: (isGoingBackwards && !isEntering) ? Curves.easeInOut : Curves.ease + ) + ), child: child, ); }, + layoutBuilder: (currentChild, previousChildren) { + if (isGoingBackwards) { + return Stack( + children: [ + if (currentChild != null) currentChild, + ...previousChildren, + + ] + ); + } + return Stack( + children: [ + ...previousChildren, + if (currentChild != null) currentChild + ] + ); + }, child: KeyedSubtree( key: ValueKey(_stack.length), child: SizedBox.expand(child: _stack.last), @@ -494,6 +522,7 @@ class SheetNavigationManager { stopName: stopName, onFavorite: addFavoriteStop, onUnFavorite: removeFavoriteStop, + scrollController: scrollControllerLocal, showBusSheet: (busId) { // When someone clicks "See all stops for this bus" this callback runs debugPrint("Got 'See all stops' click for Bus ${busId}"); @@ -534,8 +563,8 @@ class SheetNavigationManager { backgroundColor: Colors.transparent, // builder: (context) => SheetNavigator(initialSheet: builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.95, - maxChildSize: 0.95, + initialChildSize: 0.85, + maxChildSize: 0.85, snap: true, builder: (BuildContext context, ScrollController scrollController) { return SheetNavigator( diff --git a/lib/widgets/stop_sheet.dart b/lib/widgets/stop_sheet.dart index a8ef262..7fed9fc 100644 --- a/lib/widgets/stop_sheet.dart +++ b/lib/widgets/stop_sheet.dart @@ -28,6 +28,7 @@ class StopSheet extends StatefulWidget { final void Function() onGetDirections; final void Function(String) showBusSheet; final BusProvider busProvider; + final ScrollController scrollController; // Scroll controller of the DraggableScrollableSheet (the parent Widget) StopSheet({ Key? key, @@ -38,6 +39,7 @@ class StopSheet extends StatefulWidget { required this.onGetDirections, required this.showBusSheet, required this.busProvider, + required this.scrollController }) : super(key: key); @override @@ -303,511 +305,513 @@ class _StopSheetState extends State { @override Widget build(BuildContext context) { return Stack( - children: [ - // stacking the sheet on top of a gesture detector so you can close it by tapping out of it - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - Navigator.of(context).pop(); - }, - child: Container(), + children: [ + // stacking the sheet on top of a gesture detector so you can close it by tapping out of it + Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + Navigator.of(context).pop(); + }, + child: Container(), + ), ), - ), - FutureBuilder( - future: loadedStopData, - builder: (context, snapshot) { - List arrivingBuses = []; + FutureBuilder( + future: loadedStopData, + builder: (context, snapshot) { + List arrivingBuses = []; - if (snapshot.hasData) { - arrivingBuses = snapshot.data!.$1; - arrivingBuses.sort( - (lhs, rhs) => (int.tryParse(lhs.prediction) ?? 0).compareTo(int.tryParse(rhs.prediction) ?? 0) - ); - if (_isFavorited == null) { - _isFavorited = snapshot.data!.$2; + if (snapshot.hasData) { + arrivingBuses = snapshot.data!.$1; + arrivingBuses.sort( + (lhs, rhs) => (int.tryParse(lhs.prediction) ?? 0).compareTo(int.tryParse(rhs.prediction) ?? 0) + ); + if (_isFavorited == null) { + _isFavorited = snapshot.data!.$2; + } } - } - double initialSize = 0.9; + double initialSize = 0.9; - if (snapshot.hasData) { - final itemCount = arrivingBuses.length; + // if (snapshot.hasData) { + // final itemCount = arrivingBuses.length; - // edge case - if (itemCount == 0) { - if (imageBusStop) { - initialSize = 0.8; - } else { - initialSize = 0.5; - } - } - } else { - // A fixed initial size for loading or error states. - initialSize = 0.4; - if (imageBusStop) { - initialSize = 0.6; - } - } + // // edge case + // if (itemCount == 0) { + // if (imageBusStop) { + // initialSize = 0.8; + // } else { + // initialSize = 0.5; + // } + // } + // } else { + // // A fixed initial size for loading or error states. + // initialSize = 0.4; + // if (imageBusStop) { + // initialSize = 0.6; + // } + // } - // we know image dimensions, so we can use the width to find the height - // with a lil simple math - double heightOfImage = (imageBusStop)? ((MediaQuery.sizeOf(context).width) * 0.54345703125) : 0; + // we know image dimensions, so we can use the width to find the height + // with a lil simple math + double heightOfImage = (imageBusStop)? ((MediaQuery.sizeOf(context).width) * 0.54345703125) : 0; - double paddingBelowButtons = globalBottomPadding; + double paddingBelowButtons = globalBottomPadding; - return DraggableScrollableSheet( - initialChildSize: initialSize, - minChildSize: 0.0, // leave at 0.0 to allow full dismissal - maxChildSize: initialSize, - snap: true, - snapSizes: [initialSize], - builder: (BuildContext context, ScrollController scrollController) { - return Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: getColor(context, ColorType.background), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30), + // return DraggableScrollableSheet( + // initialChildSize: initialSize, + // minChildSize: 0.0, // leave at 0.0 to allow full dismissal + // maxChildSize: initialSize, + // snap: true, + // snapSizes: [initialSize], + // builder: (BuildContext context, ScrollController scrollController) { + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: getColor(context, ColorType.background), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + boxShadow: [ + SheetBoxShadow + ] ), - boxShadow: [ - SheetBoxShadow - ] - ), - // overflow box lets the stuff inside not shrink when page is being closed - child: OverflowBox( - alignment: Alignment.topCenter, - maxHeight: MediaQuery.of(context).size.height * initialSize, - // In this stack, - // First (bottom) layer is image, which sometimes doesn't exist - // Second layer is another stack. Inside that stack is: - // first layer: the the main body and content - // second layer is a box with a gradient - // third layer is the buttons themselves - child: Stack( - children: [ - (imageBusStop)? - // Image of bus stop if it exists - ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30), - ), - child: Image.asset( - imagePath, - fit: BoxFit.cover, + // overflow box lets the stuff inside not shrink when page is being closed + child: OverflowBox( + alignment: Alignment.topCenter, + maxHeight: MediaQuery.of(context).size.height * initialSize, + // In this stack, + // First (bottom) layer is image, which sometimes doesn't exist + // Second layer is another stack. Inside that stack is: + // first layer: the the main body and content + // second layer is a box with a gradient + // third layer is the buttons themselves + child: Stack( + children: [ + (imageBusStop)? + // Image of bus stop if it exists + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), ), - ) - // bus stop image does not exist, use empty widget - : SizedBox.shrink(), - - Stack( - children: [ - // yes this column only has one thing. yes this is - // the only way it works becuase the stack won't play - // nice without it. Thank you flutter - Column( - children: [ - Expanded( - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - controller: scrollController, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // spacer for image with gradient - Container( - height: heightOfImage, - decoration: BoxDecoration( - gradient: getStopHeroImageGradient(context) + child: Image.asset( + imagePath, + fit: BoxFit.cover, + ), + ) + // bus stop image does not exist, use empty widget + : SizedBox.shrink(), + + Stack( + children: [ + // yes this column only has one thing. yes this is + // the only way it works becuase the stack won't play + // nice without it. Thank you flutter + Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + controller: widget.scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // spacer for image with gradient + Container( + height: heightOfImage, + decoration: BoxDecoration( + gradient: getStopHeroImageGradient(context) + ), ), - ), - - // wrapped in container to add background color - Container( - color: getColor(context, ColorType.background), - child: Column( - children: [ - // header - Padding( - padding: EdgeInsets.only( - top: (imageBusStop) ? 0 : 20, - left: 20, - right: 20, - ), - child: Row( - children: [ - Expanded( - child: Text( - widget.stopName, - style: TextStyle( - fontFamily: 'Urbanist', - fontWeight: FontWeight.w700, - fontSize: 30, - height: 1.1, + + // wrapped in container to add background color + Container( + color: getColor(context, ColorType.background), + child: Column( + children: [ + // header + Padding( + padding: EdgeInsets.only( + top: (imageBusStop) ? 0 : 20, + left: 20, + right: 20, + ), + child: Row( + children: [ + Expanded( + child: Text( + widget.stopName, + style: TextStyle( + fontFamily: 'Urbanist', + fontWeight: FontWeight.w700, + fontSize: 30, + height: 1.1, + ), ), ), - ), - - SizedBox(width: 15), - - Column( - children: [ - IntrinsicWidth( - child: Container( - height: 25, - decoration: BoxDecoration( - color: Colors.amber, - borderRadius: - BorderRadius.circular(7), - ), - child: Center( - child: Padding( - padding: - const EdgeInsets.symmetric( - horizontal: 5, - ), - child: MediaQuery( - data: MediaQuery.of(context) - .copyWith( - textScaler: - TextScaler.linear( - 1.0, - ), + + SizedBox(width: 15), + + Column( + children: [ + IntrinsicWidth( + child: Container( + height: 25, + decoration: BoxDecoration( + color: Colors.amber, + borderRadius: + BorderRadius.circular(7), + ), + child: Center( + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 5, + ), + child: MediaQuery( + data: MediaQuery.of(context) + .copyWith( + textScaler: + TextScaler.linear( + 1.0, + ), + ), + child: Text( + widget.stopID, + style: TextStyle( + color: Colors.black, + fontFamily: 'Urbanist', + fontWeight: + FontWeight.w700, + fontSize: 17, ), - child: Text( - widget.stopID, - style: TextStyle( - color: Colors.black, - fontFamily: 'Urbanist', - fontWeight: - FontWeight.w700, - fontSize: 17, ), ), ), ), ), ), + ], + ), + ], + ), + ), + + SizedBox(height: 20), + + // loading text and button + Material( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Row( + children: [ + SizedBox(width: 2), + Text( + "Next bus departures", + style: TextStyle( + fontFamily: 'Urbanist', + fontWeight: FontWeight.w400, + fontSize: 20, + ), + ), + SizedBox(width: 5), + RefreshButton( + loading: snapshot.connectionState == ConnectionState.waiting, + onTap: _refreshData ), ], ), - ], + ), ), - ), - - SizedBox(height: 20), - - // loading text and button - Material( - color: Colors.transparent, - child: Padding( + + + // main page + Padding( padding: const EdgeInsets.symmetric( - horizontal: 20, + horizontal: 0, ), - child: Row( - children: [ - SizedBox(width: 2), - Text( - "Next bus departures", - style: TextStyle( - fontFamily: 'Urbanist', - fontWeight: FontWeight.w400, - fontSize: 20, - ), - ), - SizedBox(width: 5), - RefreshButton( - loading: snapshot.connectionState == ConnectionState.waiting, - onTap: _refreshData - ), - ], - ), - ), - ), - - - // main page - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 0, - ), - child: - (snapshot.connectionState == - ConnectionState.waiting) - ? Center(child: const SizedBox()) - : (snapshot.hasData) - ? Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - (arrivingBuses.length == 0) - ? - Center( - child: Column( - children: [ - SizedBox(height: 50,), - Icon( - Icons.no_transfer, - size: 80, - color: Color.fromARGB(255, 150, 150, 150), - ), - Text( - "no buses arriving", - style: TextStyle( + child: + (snapshot.connectionState == + ConnectionState.waiting) + ? const SizedBox() + : (snapshot.hasData) + ? Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + (arrivingBuses.length == 0) + ? + Center( + child: Column( + children: [ + SizedBox(height: 50,), + Icon( + Icons.no_transfer, + size: 80, color: Color.fromARGB(255, 150, 150, 150), - fontWeight: FontWeight.bold ), - ), - ], - ), - ) - - : - SizedBox(height: 10), + Text( + "no buses arriving", + style: TextStyle( + color: Color.fromARGB(255, 150, 150, 150), + fontWeight: FontWeight.bold + ), + ), + ], + ), + ) + + : + SizedBox(height: 10), + + Column( + mainAxisSize: MainAxisSize.max, + children: [ + ListView.separated( + controller: widget.scrollController, + shrinkWrap: true, + physics: + NeverScrollableScrollPhysics(), + itemCount: arrivingBuses.length, + itemBuilder: (context, index) { + BusWithPrediction bus = + arrivingBuses[index]; + + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 575), + delay: const Duration(milliseconds: 100), + child: FadeInAnimation( + child: ExpandableStopWidget( + routeId: bus.id, + vehicleId: bus.vehicleId, + busId: bus.id, + busPrediction: + bus.prediction, + busDirection: bus.direction, + stopId: widget.stopID, + showBusSheet: + widget.showBusSheet, + busProvider: + widget.busProvider, + ) + ) - Column( - mainAxisSize: MainAxisSize.max, - children: [ - ListView.separated( - controller: scrollController, - shrinkWrap: true, - physics: - NeverScrollableScrollPhysics(), - itemCount: arrivingBuses.length, - itemBuilder: (context, index) { - BusWithPrediction bus = - arrivingBuses[index]; - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 575), - delay: const Duration(milliseconds: 100), - child: FadeInAnimation( - child: ExpandableStopWidget( - routeId: bus.id, - vehicleId: bus.vehicleId, - busId: bus.id, - busPrediction: - bus.prediction, - busDirection: bus.direction, - stopId: widget.stopID, - showBusSheet: - widget.showBusSheet, - busProvider: - widget.busProvider, - ) - ) + ); - ); - - - }, - separatorBuilder: (context, index) { - return Divider( - height: 0, - indent: 20, - endIndent: 20, - thickness: 1, - ); - }, - ), - - SizedBox(height: paddingBelowButtons + 20,) - ], + }, + separatorBuilder: (context, index) { + return Divider( + height: 0, + indent: 20, + endIndent: 20, + thickness: 1, + ); + }, + ), + + SizedBox(height: paddingBelowButtons + 20,) + ], + ), + ], + ) + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, ), - ], - ) - : Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - child: Text( - "Can't load data. Check your internet connection and try refreshing", - style: TextStyle( - fontFamily: 'Urbanist', - fontWeight: FontWeight.w400, - fontSize: 20, + child: Text( + "Can't load data. Check your internet connection and try refreshing", + style: TextStyle( + fontFamily: 'Urbanist', + fontWeight: FontWeight.w400, + fontSize: 20, + ), ), ), - ), - ), - ], - ), - ) - ], - ) + ), + ], + ), + ) + ], + ) + ), ), - ), - ], - ), - - // white box with gradient that the buttons sit on - Column( - children: [ - Spacer(), // another spacer to stick this to the bottom - - Container( - height: paddingBelowButtons + 65, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - getColor(context, ColorType.backgroundGradientStart), // transparent - Color.lerp(getColor(context, ColorType.backgroundGradientStart), getColor(context, ColorType.background), 0.5)!, // half-way color - getColor(context, ColorType.background), // full color - ], - stops: [0, 0.4, 1] + ], + ), + + // white box with gradient that the buttons sit on + Column( + children: [ + Spacer(), // another spacer to stick this to the bottom + + Container( + height: paddingBelowButtons + 65, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + getColor(context, ColorType.backgroundGradientStart), // transparent + Color.lerp(getColor(context, ColorType.backgroundGradientStart), getColor(context, ColorType.background), 0.5)!, // half-way color + getColor(context, ColorType.background), // full color + ], + stops: [0, 0.4, 1] + ), ), ), - ), - ], - ), - - - // bottom buttons - Column( - children: [ - Spacer(), // sticks buttons to bottom - - Padding( - padding: EdgeInsets.symmetric(horizontal: globalLeftRightPadding), - child: Row( - children: [ - ElevatedButton.icon( - onPressed: () { - Navigator.pop(context); - widget.onGetDirections(); - }, - style: ElevatedButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - backgroundColor: getColor(context, ColorType.importantButtonBackground), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + ], + ), + + + // bottom buttons + Column( + children: [ + Spacer(), // sticks buttons to bottom + + Padding( + padding: EdgeInsets.symmetric(horizontal: globalLeftRightPadding), + child: Row( + children: [ + ElevatedButton.icon( + onPressed: () { + Navigator.pop(context); + widget.onGetDirections(); + }, + style: ElevatedButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: getColor(context, ColorType.importantButtonBackground), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + elevation: 0 ), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - elevation: 0 - ), - icon: Icon( - Icons.directions, - color: getColor(context, ColorType.importantButtonText), - size: 20, - ), - label: Text( - 'Get Directions', - style: TextStyle( + icon: Icon( + Icons.directions, color: getColor(context, ColorType.importantButtonText), - fontSize: 16, - fontWeight: FontWeight.w600, + size: 20, + ), + label: Text( + 'Get Directions', + style: TextStyle( + color: getColor(context, ColorType.importantButtonText), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + Spacer(), + + ElevatedButton( + onPressed: () { + // Read the current state + final bool currentStatus = _isFavorited ?? false; + + // Call the appropriate function + if (currentStatus){ + widget.onUnFavorite(widget.stopID, widget.stopName); + } else { + widget.onFavorite(widget.stopID, widget.stopName); + } + + // Update the UI immediately + setState(() { + _isFavorited = !currentStatus; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getColor(context, ColorType.secondaryButtonBackground), + shape: CircleBorder(), + shadowColor: Colors.black, + padding: EdgeInsets.zero, + minimumSize: Size(0,0), + fixedSize: Size(40,40), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + elevation: 0 ), - ), - ), - - Spacer(), - - ElevatedButton( - onPressed: () { - // Read the current state - final bool currentStatus = _isFavorited ?? false; - - // Call the appropriate function - if (currentStatus){ - widget.onUnFavorite(widget.stopID, widget.stopName); - } else { - widget.onFavorite(widget.stopID, widget.stopName); - } - - // Update the UI immediately - setState(() { - _isFavorited = !currentStatus; - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: getColor(context, ColorType.secondaryButtonBackground), - shape: CircleBorder(), - shadowColor: Colors.black, - padding: EdgeInsets.zero, - minimumSize: Size(0,0), - fixedSize: Size(40,40), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - elevation: 0 + child: Icon( + (_isFavorited ?? false)? Icons.favorite : Icons.favorite_border, + color: (_isFavorited ?? false)? Colors.red : getColor(context, ColorType.secondaryButtonText), + size: 20, + ), ), - child: Icon( - (_isFavorited ?? false)? Icons.favorite : Icons.favorite_border, - color: (_isFavorited ?? false)? Colors.red : getColor(context, ColorType.secondaryButtonText), - size: 20, - ), - ), - - SizedBox(width: 10,), - - ElevatedButton( - onPressed: () { - if (arrivingBuses.isEmpty) { - return; - } - - showDialog( - context: context, - builder: (context) { - return Dialog( - - backgroundColor: getColor(context, ColorType.background), - - + + SizedBox(width: 10,), - constraints: BoxConstraints( - minWidth: 0.0, - minHeight: 0.0, - ), - child: ReminderForm( - stpid: widget.stopID, - - activeRoutes: arrivingBuses - .fold([], (xs, x) => xs.contains(x.id) ? xs : xs + [x.id]), - ), - ); + ElevatedButton( + onPressed: () { + if (arrivingBuses.isEmpty) { + return; } - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: getColor(context, ColorType.secondaryButtonBackground), - shape: CircleBorder(), - shadowColor: Colors.black, - padding: EdgeInsets.zero, - minimumSize: Size(0,0), - fixedSize: Size(40,40), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - elevation: 0 + + showDialog( + context: context, + builder: (context) { + return Dialog( + + backgroundColor: getColor(context, ColorType.background), + + + + constraints: BoxConstraints( + minWidth: 0.0, + minHeight: 0.0, + ), + child: ReminderForm( + stpid: widget.stopID, + + activeRoutes: arrivingBuses + .fold([], (xs, x) => xs.contains(x.id) ? xs : xs + [x.id]), + ), + ); + } + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getColor(context, ColorType.secondaryButtonBackground), + shape: CircleBorder(), + shadowColor: Colors.black, + padding: EdgeInsets.zero, + minimumSize: Size(0,0), + fixedSize: Size(40,40), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + elevation: 0 + ), + child: Icon( + (arrivingBuses.isEmpty)? Icons.notifications_off_outlined : Icons.notifications_none, + color: getColor(context, ColorType.secondaryButtonText), + size: 20.0, + ) ), - child: Icon( - (arrivingBuses.isEmpty)? Icons.notifications_off_outlined : Icons.notifications_none, - color: getColor(context, ColorType.secondaryButtonText), - size: 20.0, - ) - ), - ], + ], + ), ), - ), - - SizedBox(height: paddingBelowButtons,) - ], - ), - ], - ), - ], + + SizedBox(height: paddingBelowButtons,) + ], + ), + ], + ), + ], + ), ), - ), - ); - }, - ); - }, - ), - ], + ); + // }, + // ); + }, + ), + ], + ); } } diff --git a/lib/widgets/upcoming_stops_widget.dart b/lib/widgets/upcoming_stops_widget.dart index e8ce6df..2c79091 100644 --- a/lib/widgets/upcoming_stops_widget.dart +++ b/lib/widgets/upcoming_stops_widget.dart @@ -514,7 +514,7 @@ class _UpcomingStopsWidgetState extends State { }); } - GestureDetector getUpcomingStopRow( // TODO: Add a lineTopColor and lineBottomColor attribute to the constructor and pass those in from the loop (so that it works when the bus color changes) + Material getUpcomingStopRow( // TODO: Add a lineTopColor and lineBottomColor attribute to the constructor and pass those in from the loop (so that it works when the bus color changes) int lineTopStyle, int lineBottomStyle, bool isKeyStop, @@ -545,50 +545,53 @@ class _UpcomingStopsWidgetState extends State { } - return GestureDetector( - onTap: () { - onBusStopClick?.call(stop.name, stop.id); - }, - child: Row( - children: [ - CustomPaint( - size: const Size(40, 40), - painter: UpcomingStopIconPainter( - lineTopStyle, - lineBottomStyle, - isKeyStop, - // widget.color, - topColor, - isDarkMode(context) + return Material( + color: Colors.transparent, + child: InkWell( + onTap: (onBusStopClick == null) ? null : () { // If onTap is null, the "ripple" effect won't show. + onBusStopClick?.call(stop.name, stop.id); + }, + child: Row( + children: [ + CustomPaint( + size: const Size(40, 40), + painter: UpcomingStopIconPainter( + lineTopStyle, + lineBottomStyle, + isKeyStop, + // widget.color, + topColor, + isDarkMode(context) + ), ), - ), - Expanded( - child: Text( - stop.name, - style: TextStyle( - fontSize: 15.0, - fontWeight: isKeyStop ? FontWeight.bold : FontWeight.normal, - height: 1.15, + Expanded( + child: Text( + stop.name, + style: TextStyle( + fontSize: 15.0, + fontWeight: isKeyStop ? FontWeight.bold : FontWeight.normal, + height: 1.15, + ), + ), - ), - ), - SizedBox(width: 15), + SizedBox(width: 15), - (stop.prediction != null) ? Text( - predictionText, - style: TextStyle( - fontSize: 15.0, - ) - ) : - SizedBox.shrink(), - - (onBusStopClick != null) - ? const Icon(Icons.chevron_right, color: Colors.grey, size: 20) - : const SizedBox.shrink(), - ], - ), + (stop.prediction != null) ? Text( + predictionText, + style: TextStyle( + fontSize: 15.0, + ) + ) : + SizedBox.shrink(), + + (onBusStopClick != null) + ? const Icon(Icons.chevron_right, color: Colors.grey, size: 20) + : const SizedBox.shrink(), + ], + ), + ) ); } From 944329fc9142046d68d6edfe3f79eeac519b12f4 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:16:39 -0400 Subject: [PATCH 05/15] Cleaned up SheetNavigationManager --- lib/services/sheet_navigation_manager.dart | 282 +++++---------------- 1 file changed, 65 insertions(+), 217 deletions(-) diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index ac73523..a009303 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -16,50 +16,14 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:provider/provider.dart'; -// class BackStackStep { -// SheetNavigationManager? navigationManager; -// void recall() {} -// } - -// class BusSheetBackStackStep { -// SheetNavigationManager? navigationManager; -// String busId; -// BusSheetBackStackStep({ -// required this.navigationManager, -// required this.busId -// }); -// void recall() { -// navigationManager?.showBusSheet(busId); -// } -// } - -// class StopSheetBackStackStep { -// SheetNavigationManager? navigationManager; -// String stopID; -// String stopName; -// double lat; -// double long; - -// StopSheetBackStackStep({ -// required this.navigationManager, -// required this.stopID, -// required this.stopName, -// required this.lat, -// required this.long -// }); - -// void recall() { - -// } -// } - +// SheetNavigator is a custom widget that allows multiple sheets to be displayed, one after another. +// Useful if the user is navigating through many Sheets (e.g. BusSheet to StopSheet to BusSheet) +// SheetNavigator manages its back stack, so the Android back button (and whatever back buttons you add to the UI) go backwards through history with a nice card animation class SheetNavigatorState extends State { List _stack = []; - int oldStackLength = 0; // Used to track the reverse animation + int oldStackLength = 0; bool isGoingBackwards = false; - // NEXT STEPS TODO: Make isGoingBackwards a *state variable*. Every time a widget is pushed or popped, update the isGoingBackwards variable - void pushWidget(Widget sheet) { setState(() { _stack.add(sheet); @@ -76,12 +40,10 @@ class SheetNavigatorState extends State { @override Widget build(BuildContext context) { - return PopScope( canPop: _stack.length <= 1, onPopInvokedWithResult: (didPop, result) { - // debugPrint("Pop invoked!"); - + // When the Android back button is pressed (or the current widget gets closed programatically), it's routed here and SheetNavigator takes the current sheet off the back stack if (!didPop) { setState(() { oldStackLength = _stack.length; @@ -89,63 +51,55 @@ class SheetNavigatorState extends State { isGoingBackwards = true; }); } - // isGoingBackwards = oldStackLength > _stack.length; }, child: AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: (Widget child, Animation animation) { - final isEntering = child.key == ValueKey(_stack.length); - - return SlideTransition( - position: Tween( - begin: - // NEXT STEPS TODO: Figure out these animations - isGoingBackwards ? ( - isEntering ? - const Offset(-1, 0.0) : const Offset(1, 0.0) - ) : isEntering ? - const Offset(1, 0.0) : const Offset(-1, 0.0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: (isGoingBackwards && !isEntering) ? Curves.easeInOut : Curves.ease - ) - ), - child: child, - ); - }, - layoutBuilder: (currentChild, previousChildren) { - if (isGoingBackwards) { - return Stack( - children: [ - if (currentChild != null) currentChild, - ...previousChildren, - - ] - ); - } + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + final isEntering = child.key == ValueKey(_stack.length); + + return SlideTransition( + // All this logic serves to make the forward and backward card animations look nice + position: Tween( + begin: + isGoingBackwards ? ( + isEntering ? + const Offset(-1, 0.0) : const Offset(1, 0.0) + ) : isEntering ? + const Offset(1, 0.0) : const Offset(-1, 0.0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animation, + curve: (isGoingBackwards && !isEntering) ? Curves.easeInOut : Curves.ease + ) + ), + child: child, + ); + }, + layoutBuilder: (currentChild, previousChildren) { + if (isGoingBackwards) { + // If the user is going backwards through the back stack, display the stack in reverse order so it looks like the top card is lifting off the stack return Stack( children: [ + if (currentChild != null) currentChild, ...previousChildren, - if (currentChild != null) currentChild ] ); - }, - child: KeyedSubtree( - key: ValueKey(_stack.length), - child: SizedBox.expand(child: _stack.last), - ) - + } + return Stack( // Forward order + children: [ + ...previousChildren, + if (currentChild != null) currentChild + ] + ); + }, + child: KeyedSubtree( + key: ValueKey(_stack.length), + child: SizedBox.expand(child: _stack.last), ) - ); - // return Navigator( - // onGenerateRoute: (settings) { - // return MaterialPageRoute( - // builder: (context) => initialSheet - // ); - // }, - // ); + + ) + ); } } @@ -162,10 +116,17 @@ class SheetNavigator extends StatefulWidget { State createState() => SheetNavigatorState(); } +// SheetNavigationManager is responsible for all the sheets that open up from the map screen. +// This code used to all live in map_screen.dart, but it was very unweildy, so I +// wrapped this into its own class whose job it is to manage which sheets are shown when. +// All map_screen has to do is provide some callbacks (onSelectJourney, onSelectStop, etc) +// and SheetNavigationManager figures out the history, back stack, DraggableScrollableSheet stuff, etc. class SheetNavigationManager { PersistentBottomSheetController? _bottomSheetController; BuildContext context; + + // These are all callbacks that BusSheet, StopSheet, etc. pass back to map_screen Future Function(String stpid, String name) addFavoriteStop; Function( Location location, @@ -184,9 +145,6 @@ class SheetNavigationManager { Function(String stpid) onUnfavorite; Future Function(String stpid, String name) removeFavoriteStop; - // List backStack; - // bool was_sheet_closed_programatically = false; // Flag to know whether the last-closed sheet was closed programatically (e.g. while executing a back gesture) versus if the user swiped it away - SheetNavigationManager({ required this.context, required this.addFavoriteStop, @@ -209,13 +167,7 @@ class SheetNavigationManager { _bottomSheetController = null; } - // void onAnySheetDismissed() { - // backStack.clear(); - // } - void showBuildingSheet(Location place) { - // Show red pin at the location - // _showSearchLocationMarker(place.latlng!.latitude, place.latlng!.longitude); _bottomSheetController = showBottomSheet( context: context, @@ -365,56 +317,15 @@ class SheetNavigationManager { ); } - // void onAnySheetClosed() { - - // was_sheet_closed_programatically = false; // Reset to our default assumption that the user closed the sheet, unless this flag is set to true - // } - - // TODO: Add a flag to show whether the modal was closed programatically - // i.e. before closing the model programatically on line 262, set this variable - // and then in onClosing check it. If the user swiped it away, clear the back stack - - // void showBusSheetFromSheet(String busID, BuildContext parentContext, ScrollController scrollController) { - // Navigator.of(parentContext).push( - // PageRouteBuilder( - // opaque: false, - // pageBuilder: (context, animation, secondaryAnimation) => - // BusSheet( - // busID: busID, - // scrollController: scrollController, - // onSelectStop: (name, id) { - - // // Navigator.pop(context); // Close the current modal - // LatLng? latLong = getLatLongFromStopID(id); - // if (latLong != null) { - // // showStopSheet(id, name, latLong.latitude, latLong.longitude); - // showStopSheetFromSheet(id, name, latLong.latitude, latLong.longitude, context, scrollController); - // } else { - // showMaizebusOKDialog( - // contextIn: context, - // title: const Text("Error"), - // content: const Text("Couldn't load stop."), - // ); - // } - // }, - // ) - // ) - // ); - // } - BusSheet getBusSheet(String busID, ScrollController scrollController, Function(Widget) pushNewSheet) { return BusSheet( busID: busID, scrollController: scrollController, onSelectStop: (name, id) { - - // Navigator.pop(context); // Close the current modal + // When the user clicks on a specific bus stop to open the details (StopSheet) page LatLng? latLong = getLatLongFromStopID(id); if (latLong != null) { - // showStopSheet(id, name, latLong.latitude, latLong.longitude); pushNewSheet(getStopSheet(id, name, latLong.latitude, latLong.longitude, pushNewSheet, scrollController)); - - // showStopSheetFromSheet(id, name, latLong.latitude, latLong.longitude, context, scrollController); } else { showMaizebusOKDialog( contextIn: context, @@ -432,27 +343,20 @@ class SheetNavigationManager { isScrollControlled: true, isDismissible: true, backgroundColor: Colors.transparent, - // builder: (context) => SheetNavigator( - // initialSheet: - builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.85, - maxChildSize: 0.85, - snap: true, - - builder: - - (BuildContext context, ScrollController scrollController) { - return Container( - + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.85, + maxChildSize: 0.85, + snap: true, + builder: (BuildContext context, ScrollController scrollController) { + return Container( child: SheetNavigator( scrollController: scrollController, getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushNewSheet) => getBusSheet(busID, scrollControllerLocal, pushNewSheet) ) - ); - }, - ) - ); - // ); + ); + }, + ) + ); } void showFavoritesSheet() { @@ -469,50 +373,6 @@ class SheetNavigationManager { ); } - // void showStopSheetFromSheet( - // String stopID, - // String stopName, - // double lat, - // double long, - // BuildContext parentContext, // Context needed to reference the "shell" outside this stop sheet that switches between contents - // ScrollController scrollController - // ) { - // final busProvider = Provider.of(context, listen: false); - - // Navigator.of(parentContext).push( - // PageRouteBuilder( - // opaque: false, - // pageBuilder: (context, animation, secondaryAnimation) => - // // MaterialPageRoute(builder: (context) => - // StopSheet( - // stopID: stopID, - // stopName: stopName, - // onFavorite: addFavoriteStop, - // onUnFavorite: removeFavoriteStop, - // showBusSheet: (busId) { - // // When someone clicks "See all stops for this bus" this callback runs - // // debugPrint("Got 'See all stops' click for Bus ${busId}"); - // // Navigator.pop(context); // Close the current modal - // // showBusSheetFromMap(busId); - // showBusSheetFromSheet(busId, parentContext, scrollController); - // }, - // busProvider: busProvider, - // onGetDirections: () { - // Map? start; - // Map? end = {'lat': lat, 'lon': long}; - - // showDirectionsSheet( - // start, - // end, - // "Current Location", - // stopName, - // false, - // ); - // }, - // ) - // ) - // ); - // } StopSheet getStopSheet(String stopID, String stopName, double lat, double long, Function(Widget) pushSheet, ScrollController scrollControllerLocal) { final busProvider = Provider.of(context, listen: false); @@ -525,11 +385,6 @@ class SheetNavigationManager { scrollController: scrollControllerLocal, showBusSheet: (busId) { // When someone clicks "See all stops for this bus" this callback runs - debugPrint("Got 'See all stops' click for Bus ${busId}"); - // Navigator.pop(context); // Close the current modal - // showBusSheetFromMap(busId); - // TODO: Fix this here - // showBusSheetFromSheet(busId, context, scrollController); pushSheet(getBusSheet(busId, scrollControllerLocal, pushSheet)); }, busProvider: busProvider, @@ -548,20 +403,18 @@ class SheetNavigationManager { ); } + // Shows a stop sheet from the map by creating a new DraggableScrollableSheet void showStopSheetFromMap( String stopID, String stopName, double lat, double long, ) { - - showModalBottomSheet( context: context, isDismissible: true, isScrollControlled: true, backgroundColor: Colors.transparent, - // builder: (context) => SheetNavigator(initialSheet: builder: (context) => DraggableScrollableSheet( initialChildSize: 0.85, maxChildSize: 0.85, @@ -574,14 +427,9 @@ class SheetNavigationManager { } ); - } ) - // ) - - - , ).then((_) {}); } From d0500bc256425b25ed71aea4fd719fc46d7ae53d Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:18:00 -0400 Subject: [PATCH 06/15] Cleaned up map_screen --- lib/screens/map_screen.dart | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index b17b456..945f613 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -818,13 +818,6 @@ class _MaizeBusCoreState extends State { stop.location.latitude, stop.location.longitude, ); - - // _showStopSheet( - // stop.id, - // stop.name, - // stop.location.latitude, - // stop.location.longitude, - // ); }, rotation: stop.rotation, anchor: Offset(0.5, 0.5), @@ -978,7 +971,6 @@ class _MaizeBusCoreState extends State { Haptics.vibrate(HapticsType.light); } catch (e) {} sheetNavigationManager?.showBusSheetFromMap(bus.id); - // _showBusSheet(bus.id); }, ); }) @@ -1012,7 +1004,6 @@ class _MaizeBusCoreState extends State { icon: busIcon!, rotation: bus.heading, anchor: const Offset(0.5, 0.5), - // onTap: () => _showBusSheet(bus.id), onTap: () => sheetNavigationManager?.showBusSheetFromMap(bus.id) ), ); @@ -1139,7 +1130,6 @@ class _MaizeBusCoreState extends State { icon: icon, rotation: bus.heading, anchor: const Offset(0.5, 0.5), - // onTap: () => _showBusSheet(bus.id), onTap: () => sheetNavigationManager?.showBusSheetFromMap(bus.id) ); } @@ -1713,12 +1703,6 @@ class _MaizeBusCoreState extends State { searchCoordinates.latitude, searchCoordinates.longitude, ); - // _showStopSheet( - // stopID, - // location.name, - // searchCoordinates.latitude, - // searchCoordinates.longitude, - // ); } else { _centerOnLocation( false, @@ -1730,7 +1714,6 @@ class _MaizeBusCoreState extends State { location.latlng!.longitude ); sheetNavigationManager?.showBuildingSheet(location); - // _showBuildingSheet(location); } } else { // Location has no coordinates @@ -2421,7 +2404,6 @@ class _MaizeBusCoreState extends State { onPressed: () { sheetNavigationManager?.showJourneySheetOnReopen(currDisplayed); }, - // onPressed: _showJourneySheetOnReopen, style: ElevatedButton.styleFrom( backgroundColor: getColor( context, @@ -2548,9 +2530,6 @@ class _MaizeBusCoreState extends State { _selectedRoutes, canVibrate ); - // _showBusRoutesModal( - // busProvider.routes, - // ); }, heroTag: 'routes_fab', elevation: @@ -2599,7 +2578,6 @@ class _MaizeBusCoreState extends State { ); } sheetNavigationManager?.showFavoritesSheet(); - // _showFavoritesSheet(); }, heroTag: 'favorites_fab', elevation: 0, @@ -2647,7 +2625,6 @@ class _MaizeBusCoreState extends State { ); } sheetNavigationManager?.showSearchSheet(); - // _showSearchSheet(); }, style: ElevatedButton.styleFrom( alignment: Alignment.centerLeft, From 4bb3a7c67527889d6fcc8d5599bcb7d6a7a7be86 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:45:17 -0400 Subject: [PATCH 07/15] Tried (unsuccessfully) to manage ScrollController The two Sheets are still fighting over the same ScrollController. Unfortunate. --- lib/services/sheet_navigation_manager.dart | 149 +++++++++++++++++---- lib/widgets/bus_sheet.dart | 60 ++++++++- lib/widgets/stop_sheet.dart | 95 +++++++++++-- lib/widgets/upcoming_stops_widget.dart | 5 - 4 files changed, 265 insertions(+), 44 deletions(-) diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index a009303..ef5ac4c 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -16,56 +16,138 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:provider/provider.dart'; +// This whole Factory thing is kind of a mess +// * In order for SheetNavigator to show two sheets at a time + +abstract class NavigableSheet extends Widget { + // These scroll controller methods are necessary so that when two widgets occupy the same Sheet + // (i.e. when one sheet is displayed on top of another via SheetNavigator), both Sheets don't + // fight over the scroll controller and make scrolling janky. The SheetNavigator feeds a "dummy" + // scroll controller when this sheet is displayed behind something else. + void setShouldUseScrollController(bool shouldUseScrollControler); + // void setCustomScrollController(ScrollController newScrollController); + // void clearCustomScrollController(); +} + // SheetNavigator is a custom widget that allows multiple sheets to be displayed, one after another. // Useful if the user is navigating through many Sheets (e.g. BusSheet to StopSheet to BusSheet) // SheetNavigator manages its back stack, so the Android back button (and whatever back buttons you add to the UI) go backwards through history with a nice card animation class SheetNavigatorState extends State { - List _stack = []; + List _stack = []; int oldStackLength = 0; bool isGoingBackwards = false; + int numSteps = 0; // Used for the Dismissable key + bool shouldAnimate = true; // Used to prevent a "double animation" happening after the user swipes away a sheet - void pushWidget(Widget sheet) { + void pushWidget(NavigableSheet sheet) { setState(() { + shouldAnimate = true; _stack.add(sheet); isGoingBackwards = false; + numSteps++; + updateScrollControllers(); + }); + } + + void popWidget() { + setState(() { + oldStackLength = _stack.length; + _stack.removeLast(); + isGoingBackwards = true; + numSteps++; + updateScrollControllers(); }); } + void updateScrollControllers() { + // debugPrint("******* UPDATING SCROLL CONTROLLERS"); + // _stack.last.setShouldUseScrollController(true); + // // _stack.last.setCustomScrollController(widget.scrollController); + // if (_stack.length > 1) { + // _stack[_stack.length - 2].setShouldUseScrollController(false); + // // _stack[_stack.length - 2].setCustomScrollController(ScrollController()); + // // Give the background element a dummy ScrollController so it doesn't fight the user when + // // trying to close the modal + // } + } + + void popWidgetNoAnimation() { + // shouldAnimate = false; + // popWidget(); // setState() called inside here + debugPrint("popWidgetNoAnimation call!"); + + setState(() {shouldAnimate = false;}); + setState(() { + oldStackLength = _stack.length; + _stack.removeLast(); + isGoingBackwards = true; + numSteps++; + updateScrollControllers(); + }); + + + // WidgetsBinding.instance.addPostFrameCallback((Duration d) { + // debugPrint("Post-frame callback reached!"); + // setState(() { + // oldStackLength = _stack.length; + // _stack.removeLast(); + // isGoingBackwards = true; + // numSteps++; + // }); + // }); + + + + // WidgetsBinding.instance.addPostFrameCallback((Duration d) { + // debugPrint("Post-frame callback reached!"); + // setState(() { + // oldStackLength = _stack.length; + // _stack.removeLast(); + // isGoingBackwards = true; + // numSteps++; + // }); + // }); + + } + @override void initState() { super.initState(); _stack.add(widget.getInitialSheetFromOutside(widget.scrollController, pushWidget)); } - @override Widget build(BuildContext context) { + + return PopScope( canPop: _stack.length <= 1, onPopInvokedWithResult: (didPop, result) { // When the Android back button is pressed (or the current widget gets closed programatically), it's routed here and SheetNavigator takes the current sheet off the back stack if (!didPop) { - setState(() { - oldStackLength = _stack.length; - _stack.removeLast(); - isGoingBackwards = true; - }); + shouldAnimate = true; // User clicked back button so we need a back animation + popWidget(); } }, child: AnimatedSwitcher( - duration: const Duration(milliseconds: 400), + duration: Duration(milliseconds: 400), transitionBuilder: (Widget child, Animation animation) { + debugPrint("Building transition, shouldAnimate is ${shouldAnimate}"); final isEntering = child.key == ValueKey(_stack.length); return SlideTransition( // All this logic serves to make the forward and backward card animations look nice position: Tween( begin: - isGoingBackwards ? ( - isEntering ? - const Offset(-1, 0.0) : const Offset(1, 0.0) - ) : isEntering ? - const Offset(1, 0.0) : const Offset(-1, 0.0), + !shouldAnimate ? ( + Offset.zero + ) + : isGoingBackwards ? ( + isEntering ? + const Offset(-1, 0.0) : const Offset(1, 0.0) + ) : isEntering ? + const Offset(1, 0.0) + : const Offset(-1, 0.0), end: Offset.zero, ).animate( CurvedAnimation( @@ -95,17 +177,38 @@ class SheetNavigatorState extends State { }, child: KeyedSubtree( key: ValueKey(_stack.length), - child: SizedBox.expand(child: _stack.last), + child: _stack.length > 1 ? + Stack( + children: [ + // IgnorePointer( + // child: SizedBox.expand(child: _stack[_stack.length - 2]) + // ), + //SizedBox.expand(), + SizedBox.expand(child: (_stack.length - 2 >= 0) ? _stack[_stack.length - 2] : null), + Dismissible( + key: ValueKey(numSteps), + direction: DismissDirection.startToEnd, + onDismissed: (DismissDirection d) { + popWidgetNoAnimation(); + }, + child: SizedBox.expand(child: _stack.last) + ) + ] + + // NEXT STEPS TODO: Figure out why the stacked sheets fight scrolling so much. Do they both hang on to the same ScrollController? + + ) + : SizedBox.expand(child: _stack.last) + ), ) - - ) - ); + ); + //); } } class SheetNavigator extends StatefulWidget { - final Function(ScrollController, Function(Widget)) getInitialSheetFromOutside; + final Function(ScrollController, Function(NavigableSheet)) getInitialSheetFromOutside; final ScrollController scrollController; SheetNavigator({ required this.scrollController, @@ -317,7 +420,7 @@ class SheetNavigationManager { ); } - BusSheet getBusSheet(String busID, ScrollController scrollController, Function(Widget) pushNewSheet) { + BusSheet getBusSheet(String busID, ScrollController scrollController, Function(NavigableSheet) pushNewSheet) { return BusSheet( busID: busID, scrollController: scrollController, @@ -351,7 +454,7 @@ class SheetNavigationManager { return Container( child: SheetNavigator( scrollController: scrollController, - getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushNewSheet) => getBusSheet(busID, scrollControllerLocal, pushNewSheet) + getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(NavigableSheet) pushNewSheet) => getBusSheet(busID, scrollControllerLocal, pushNewSheet) ) ); }, @@ -374,7 +477,7 @@ class SheetNavigationManager { } - StopSheet getStopSheet(String stopID, String stopName, double lat, double long, Function(Widget) pushSheet, ScrollController scrollControllerLocal) { + StopSheet getStopSheet(String stopID, String stopName, double lat, double long, Function(NavigableSheet) pushSheet, ScrollController scrollControllerLocal) { final busProvider = Provider.of(context, listen: false); return StopSheet( @@ -422,7 +525,7 @@ class SheetNavigationManager { builder: (BuildContext context, ScrollController scrollController) { return SheetNavigator( scrollController: scrollController, - getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushSheet) { + getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(NavigableSheet) pushSheet) { return getStopSheet(stopID, stopName, lat, long, pushSheet, scrollControllerLocal); } ); diff --git a/lib/widgets/bus_sheet.dart b/lib/widgets/bus_sheet.dart index 7249238..00aa353 100644 --- a/lib/widgets/bus_sheet.dart +++ b/lib/widgets/bus_sheet.dart @@ -1,5 +1,6 @@ import 'package:bluebus/services/bus_info_service.dart'; import 'package:bluebus/services/bus_repository.dart'; +import 'package:bluebus/services/sheet_navigation_manager.dart'; import 'package:flutter/material.dart'; import '../constants.dart'; import '../models/bus.dart'; @@ -15,27 +16,65 @@ bool isNumber(String? s) { return false; } -class BusSheet extends StatefulWidget { +class BusSheet extends StatefulWidget implements NavigableSheet { final String busID; final ScrollController scrollController; final void Function(String name, String id) onSelectStop; + final GlobalKey<_BusSheetState> stateKey; - const BusSheet({ - Key? key, + BusSheet._({ + // Key? key, + // Key? stateKey, + required this.stateKey, required this.busID, required this.onSelectStop, required this.scrollController, - }) : super(key: key); + }) : super(key: stateKey); + + factory BusSheet({ + required String busID, + required void Function(String, String) onSelectStop, + required ScrollController scrollController, + }) { + final key = GlobalKey<_BusSheetState>(); + return BusSheet._(stateKey: key, busID: busID, onSelectStop: onSelectStop, scrollController: scrollController); + } @override State createState() { return _BusSheetState(); } + + @override + void setShouldUseScrollController(bool shouldUseScrollController) { + // debugPrint("BusSheet: Got setCustomScrollController call with ${newScrollController}"); + stateKey.currentState?.setShouldUseScrollController(shouldUseScrollController); + } } class _BusSheetState extends State { late Bus? currBus = BusRepository.getBus(widget.busID); late Future> futureBusStops; + bool shouldUseScrollControler = true; + + // The shouldUseScrollControler parameter necessary so that when two widgets occupy the same Sheet + // (i.e. when one sheet is displayed on top of another via SheetNavigator), both Sheets don't + // fight over the scroll controller and make scrolling janky. The SheetNavigator feeds a "dummy" + // scroll controller when this sheet is displayed behind something else + void setShouldUseScrollController(bool shouldUseScrollControlerIn) { + debugPrint("BusSheet: INSIDE: setShouldUseScrollController() call: $shouldUseScrollControlerIn"); + setState(() { + shouldUseScrollControler = shouldUseScrollControlerIn; + }); + } + ScrollController? getScrollController() { + if (!shouldUseScrollControler) { + debugPrint("getScrollController() call, returning null"); + return null; + } + debugPrint("getScrollController call(), returning the vanilla one"); + return widget.scrollController; + } @override void initState() { @@ -49,10 +88,13 @@ class _BusSheetState extends State { // This accounts for that. if (currBus == null) return Text("Bus not found"); + // return Text("HI"); + + debugPrint("BusSheet: Building..."); + final bus = currBus!; - debugPrint(" currBus is ${currBus?.routeId}"); - return Container( + Widget output = Container( decoration: BoxDecoration( color: getColor(context, ColorType.background), borderRadius: const BorderRadius.only( @@ -71,7 +113,9 @@ class _BusSheetState extends State { bottom: 0, ), child: SingleChildScrollView( - controller: widget.scrollController, + // controller: widget.scrollController, + // controller: getScrollController(), + controller: null, child: Column( mainAxisSize: MainAxisSize.min, @@ -116,6 +160,8 @@ class _BusSheetState extends State { ), ), ); + debugPrint("BusSheet: Finished building"); + return output; } } diff --git a/lib/widgets/stop_sheet.dart b/lib/widgets/stop_sheet.dart index 7fed9fc..b68984e 100644 --- a/lib/widgets/stop_sheet.dart +++ b/lib/widgets/stop_sheet.dart @@ -2,6 +2,7 @@ import 'package:bluebus/globals.dart'; import 'package:bluebus/providers/bus_provider.dart'; import 'package:bluebus/services/bus_info_service.dart'; import 'package:bluebus/services/incoming_bus_reminder_service.dart'; +import 'package:bluebus/services/sheet_navigation_manager.dart'; import 'package:bluebus/widgets/dialog.dart'; import 'package:bluebus/widgets/refresh_button.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,7 @@ bool isRide(String? s) { return false; } -class StopSheet extends StatefulWidget { +class StopSheet extends StatefulWidget implements NavigableSheet { final String stopID; final String stopName; final Future Function(String, String) onFavorite; @@ -29,9 +30,15 @@ class StopSheet extends StatefulWidget { final void Function(String) showBusSheet; final BusProvider busProvider; final ScrollController scrollController; // Scroll controller of the DraggableScrollableSheet (the parent Widget) + // final GlobalKey<_StopSheetState> stateKey; - StopSheet({ - Key? key, + @override + void setShouldUseScrollController(bool shouldUseScrollController) { + // debugPrint("BusSheet: Got setCustomScrollController call with ${newScrollController}"); + // stateKey.currentState?.setShouldUseScrollController(shouldUseScrollController); + } + + const StopSheet({super.key, required this.stopID, required this.stopName, required this.onFavorite, @@ -40,7 +47,48 @@ class StopSheet extends StatefulWidget { required this.showBusSheet, required this.busProvider, required this.scrollController - }) : super(key: key); + }); + + // StopSheet._({ + // required this.stateKey, + // required this.stopID, + // required this.stopName, + // required this.onFavorite, + // required this.onUnFavorite, + // required this.onGetDirections, + // required this.showBusSheet, + // required this.busProvider, + // required this.scrollController + // }) : super(key: stateKey); + + // Yes, I know this is a mess. I'm sorry. + // The reason this needs to be here: + // * When used in a SheetNavigator, two sheets are displayed on top of each other for smooth transitions + // * If both sheets are using the ScrollController, they fight each other and make it difficult for the user to swipe the sheet away + // * Thus, SheetNavigationManager needs to call StopSheet.setShouldUseScrollController to tell it whether it should use the scroll controller (i.e. if it's in the foreground) or not + // * Unfortunately, in order to make StopSheet able to call a method inside StopSheetState, Flutter requires that you have a GlobalKey<_StopSheetState>, and for that you need to make a factory. Womp womp. + // factory StopSheet({ + // required String stopID, + // required String stopName, + // required Future Function(String, String) onFavorite, + // required Future Function(String, String) onUnFavorite, + // required Function() onGetDirections, + // required Function(String) showBusSheet, + // required BusProvider busProvider, + // required ScrollController scrollController, + // }) { + // final key = GlobalKey<_StopSheetState>(); + // return StopSheet._( + // stateKey: key, + // stopID: stopID, + // stopName: stopName, + // onFavorite: onFavorite, + // onUnFavorite: onUnFavorite, + // onGetDirections: onGetDirections, + // showBusSheet: showBusSheet, + // busProvider: busProvider, + // scrollController: scrollController); + // } @override State createState() => _StopSheetState(); @@ -261,6 +309,29 @@ class _StopSheetState extends State { late bool imageBusStop; late String imagePath; + bool shouldUseScrollControler = true; + + // The shouldUseScrollControler parameter necessary so that when two widgets occupy the same Sheet + // (i.e. when one sheet is displayed on top of another via SheetNavigator), both Sheets don't + // fight over the scroll controller and make scrolling janky. The SheetNavigator feeds a "dummy" + // scroll controller when this sheet is displayed behind something else + void setShouldUseScrollController(bool shouldUseScrollControlerIn) { + // debugPrint("StopSheet: INSIDE: setShouldUseScrollController() call: $shouldUseScrollControlerIn"); + // setState(() { + // shouldUseScrollControler = shouldUseScrollControlerIn; + // }); + } + ScrollController? getScrollController() { + return null; + if (!shouldUseScrollControler) { + return null; + } + debugPrint(" customScrollController is null, returning the vanilla one"); + return widget.scrollController; + } + + + @override void initState() { super.initState(); @@ -304,6 +375,8 @@ class _StopSheetState extends State { @override Widget build(BuildContext context) { + debugPrint("StopSheet: Building..."); + // return Text("It÷ works!"); return Stack( children: [ // stacking the sheet on top of a gesture detector so you can close it by tapping out of it @@ -356,7 +429,7 @@ class _StopSheetState extends State { // with a lil simple math double heightOfImage = (imageBusStop)? ((MediaQuery.sizeOf(context).width) * 0.54345703125) : 0; - double paddingBelowButtons = globalBottomPadding; + double paddingBelowButtons = globalBottomPadding; // TODO: Figure out why these buttons are pushed so far down // return DraggableScrollableSheet( // initialChildSize: initialSize, @@ -381,7 +454,8 @@ class _StopSheetState extends State { // overflow box lets the stuff inside not shrink when page is being closed child: OverflowBox( alignment: Alignment.topCenter, - maxHeight: MediaQuery.of(context).size.height * initialSize, + maxHeight: null, + // maxHeight: MediaQuery.of(context).size.height * initialSize, // In this stack, // First (bottom) layer is image, which sometimes doesn't exist // Second layer is another stack. Inside that stack is: @@ -416,7 +490,9 @@ class _StopSheetState extends State { Expanded( child: SingleChildScrollView( physics: const ClampingScrollPhysics(), - controller: widget.scrollController, + // controller: widget.scrollController, + // controller: getScrollController(), + controller: null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -575,7 +651,8 @@ class _StopSheetState extends State { mainAxisSize: MainAxisSize.max, children: [ ListView.separated( - controller: widget.scrollController, + // controller: widget.scrollController,\ + // controller: getScrollController(), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), @@ -619,7 +696,7 @@ class _StopSheetState extends State { }, ), - SizedBox(height: paddingBelowButtons + 20,) + SizedBox(height: paddingBelowButtons + 40,) // The +40 allows enough space for "See all stops for this bus" button to be clickable ], ), ], diff --git a/lib/widgets/upcoming_stops_widget.dart b/lib/widgets/upcoming_stops_widget.dart index 2c79091..8bd8b95 100644 --- a/lib/widgets/upcoming_stops_widget.dart +++ b/lib/widgets/upcoming_stops_widget.dart @@ -444,9 +444,7 @@ class _UpcomingStopsWidgetState extends State { var result; try { - debugPrint("Loading data......"); result = await fetchNextBusStops(widget.vehicleId!); - debugPrint(" Got result! $result"); } catch (e) { debugPrint("Error getting stops: $e"); return; @@ -495,9 +493,6 @@ class _UpcomingStopsWidgetState extends State { } if (filter_check_passed) { - debugPrint( - " ${result[i].name} found after both conditions met, adding...", - ); results_filtered.add(result[i]); } } From 46cce650f2b5955037014d9e3718eb42d76f8dc0 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:08:57 -0400 Subject: [PATCH 08/15] Finally fixed it with a nifty InheritedWidget! --- lib/services/sheet_navigation_manager.dart | 63 +++++++++------ lib/widgets/bus_sheet.dart | 58 +++----------- lib/widgets/stop_sheet.dart | 89 +++------------------- 3 files changed, 60 insertions(+), 150 deletions(-) diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index ef5ac4c..ccc2af0 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -16,30 +16,33 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:provider/provider.dart'; -// This whole Factory thing is kind of a mess -// * In order for SheetNavigator to show two sheets at a time - -abstract class NavigableSheet extends Widget { - // These scroll controller methods are necessary so that when two widgets occupy the same Sheet - // (i.e. when one sheet is displayed on top of another via SheetNavigator), both Sheets don't - // fight over the scroll controller and make scrolling janky. The SheetNavigator feeds a "dummy" - // scroll controller when this sheet is displayed behind something else. - void setShouldUseScrollController(bool shouldUseScrollControler); - // void setCustomScrollController(ScrollController newScrollController); - // void clearCustomScrollController(); +class SheetNavigationContext extends InheritedWidget { + final ScrollController scrollController; + final bool shouldShowShadow; + + const SheetNavigationContext({required this.scrollController, required this.shouldShowShadow, required super.child}); + + // The static accessor - this is the "of(context)" pattern + static SheetNavigationContext? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(SheetNavigationContext old) => scrollController != old.scrollController; } // SheetNavigator is a custom widget that allows multiple sheets to be displayed, one after another. // Useful if the user is navigating through many Sheets (e.g. BusSheet to StopSheet to BusSheet) // SheetNavigator manages its back stack, so the Android back button (and whatever back buttons you add to the UI) go backwards through history with a nice card animation class SheetNavigatorState extends State { - List _stack = []; + List _stack = []; int oldStackLength = 0; bool isGoingBackwards = false; int numSteps = 0; // Used for the Dismissable key bool shouldAnimate = true; // Used to prevent a "double animation" happening after the user swipes away a sheet + final ScrollController dummyScrollController = ScrollController(); - void pushWidget(NavigableSheet sheet) { + void pushWidget(Widget sheet) { setState(() { shouldAnimate = true; _stack.add(sheet); @@ -116,6 +119,12 @@ class SheetNavigatorState extends State { _stack.add(widget.getInitialSheetFromOutside(widget.scrollController, pushWidget)); } + @override + void dispose() { + dummyScrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { @@ -184,21 +193,23 @@ class SheetNavigatorState extends State { // child: SizedBox.expand(child: _stack[_stack.length - 2]) // ), //SizedBox.expand(), - SizedBox.expand(child: (_stack.length - 2 >= 0) ? _stack[_stack.length - 2] : null), + SizedBox.expand(child: (_stack.length - 2 >= 0) ? + SheetNavigationContext(child: _stack[_stack.length - 2], shouldShowShadow: false, scrollController: dummyScrollController) + : null), Dismissible( key: ValueKey(numSteps), direction: DismissDirection.startToEnd, onDismissed: (DismissDirection d) { popWidgetNoAnimation(); }, - child: SizedBox.expand(child: _stack.last) + child: SizedBox.expand(child: SheetNavigationContext(child: _stack.last, shouldShowShadow: true, scrollController: widget.scrollController)) ) ] // NEXT STEPS TODO: Figure out why the stacked sheets fight scrolling so much. Do they both hang on to the same ScrollController? ) - : SizedBox.expand(child: _stack.last) + : SizedBox.expand(child: SheetNavigationContext(child: _stack.last, shouldShowShadow: true, scrollController: widget.scrollController,)) ), ) ); @@ -208,7 +219,7 @@ class SheetNavigatorState extends State { } class SheetNavigator extends StatefulWidget { - final Function(ScrollController, Function(NavigableSheet)) getInitialSheetFromOutside; + final Function(ScrollController, Function(Widget)) getInitialSheetFromOutside; final ScrollController scrollController; SheetNavigator({ required this.scrollController, @@ -420,7 +431,7 @@ class SheetNavigationManager { ); } - BusSheet getBusSheet(String busID, ScrollController scrollController, Function(NavigableSheet) pushNewSheet) { + BusSheet getBusSheet(String busID, ScrollController? scrollController, Function(Widget) pushNewSheet) { return BusSheet( busID: busID, scrollController: scrollController, @@ -428,7 +439,8 @@ class SheetNavigationManager { // When the user clicks on a specific bus stop to open the details (StopSheet) page LatLng? latLong = getLatLongFromStopID(id); if (latLong != null) { - pushNewSheet(getStopSheet(id, name, latLong.latitude, latLong.longitude, pushNewSheet, scrollController)); + // pushNewSheet(getStopSheet(id, name, latLong.latitude, latLong.longitude, pushNewSheet, scrollController)); + pushNewSheet(getStopSheet(id, name, latLong.latitude, latLong.longitude, pushNewSheet, null)); } else { showMaizebusOKDialog( contextIn: context, @@ -454,7 +466,8 @@ class SheetNavigationManager { return Container( child: SheetNavigator( scrollController: scrollController, - getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(NavigableSheet) pushNewSheet) => getBusSheet(busID, scrollControllerLocal, pushNewSheet) + getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushNewSheet) => getBusSheet(busID, null, pushNewSheet) + // getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushNewSheet) => getBusSheet(busID, scrollControllerLocal, pushNewSheet) ) ); }, @@ -477,7 +490,7 @@ class SheetNavigationManager { } - StopSheet getStopSheet(String stopID, String stopName, double lat, double long, Function(NavigableSheet) pushSheet, ScrollController scrollControllerLocal) { + StopSheet getStopSheet(String stopID, String stopName, double lat, double long, Function(Widget) pushSheet, ScrollController? scrollControllerLocal) { final busProvider = Provider.of(context, listen: false); return StopSheet( @@ -488,7 +501,8 @@ class SheetNavigationManager { scrollController: scrollControllerLocal, showBusSheet: (busId) { // When someone clicks "See all stops for this bus" this callback runs - pushSheet(getBusSheet(busId, scrollControllerLocal, pushSheet)); + // pushSheet(getBusSheet(busId, scrollControllerLocal, pushSheet)); + pushSheet(getBusSheet(busId, null, pushSheet)); }, busProvider: busProvider, onGetDirections: () { @@ -525,8 +539,9 @@ class SheetNavigationManager { builder: (BuildContext context, ScrollController scrollController) { return SheetNavigator( scrollController: scrollController, - getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(NavigableSheet) pushSheet) { - return getStopSheet(stopID, stopName, lat, long, pushSheet, scrollControllerLocal); + getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushSheet) { + // return getStopSheet(stopID, stopName, lat, long, pushSheet, scrollControllerLocal); + return getStopSheet(stopID, stopName, lat, long, pushSheet, null); } ); diff --git a/lib/widgets/bus_sheet.dart b/lib/widgets/bus_sheet.dart index 00aa353..b58b40f 100644 --- a/lib/widgets/bus_sheet.dart +++ b/lib/widgets/bus_sheet.dart @@ -16,65 +16,27 @@ bool isNumber(String? s) { return false; } -class BusSheet extends StatefulWidget implements NavigableSheet { +class BusSheet extends StatefulWidget { final String busID; - final ScrollController scrollController; + ScrollController? scrollController; final void Function(String name, String id) onSelectStop; - final GlobalKey<_BusSheetState> stateKey; - BusSheet._({ - // Key? key, - // Key? stateKey, - required this.stateKey, + BusSheet({ required this.busID, required this.onSelectStop, - required this.scrollController, - }) : super(key: stateKey); - - factory BusSheet({ - required String busID, - required void Function(String, String) onSelectStop, - required ScrollController scrollController, - }) { - final key = GlobalKey<_BusSheetState>(); - return BusSheet._(stateKey: key, busID: busID, onSelectStop: onSelectStop, scrollController: scrollController); - } + this.scrollController, // Note: scrollController should be null ONLY if it's inside a SheetNavigator (in which case the SheetNavigator provides it through SheetNavigationContext). + }); @override State createState() { return _BusSheetState(); } - @override - void setShouldUseScrollController(bool shouldUseScrollController) { - // debugPrint("BusSheet: Got setCustomScrollController call with ${newScrollController}"); - stateKey.currentState?.setShouldUseScrollController(shouldUseScrollController); - } } class _BusSheetState extends State { late Bus? currBus = BusRepository.getBus(widget.busID); late Future> futureBusStops; - bool shouldUseScrollControler = true; - - // The shouldUseScrollControler parameter necessary so that when two widgets occupy the same Sheet - // (i.e. when one sheet is displayed on top of another via SheetNavigator), both Sheets don't - // fight over the scroll controller and make scrolling janky. The SheetNavigator feeds a "dummy" - // scroll controller when this sheet is displayed behind something else - void setShouldUseScrollController(bool shouldUseScrollControlerIn) { - debugPrint("BusSheet: INSIDE: setShouldUseScrollController() call: $shouldUseScrollControlerIn"); - setState(() { - shouldUseScrollControler = shouldUseScrollControlerIn; - }); - } - ScrollController? getScrollController() { - if (!shouldUseScrollControler) { - debugPrint("getScrollController() call, returning null"); - return null; - } - debugPrint("getScrollController call(), returning the vanilla one"); - return widget.scrollController; - } @override void initState() { @@ -94,16 +56,16 @@ class _BusSheetState extends State { final bus = currBus!; - Widget output = Container( + Widget output = AnimatedContainer( + duration: Duration(milliseconds: 500), decoration: BoxDecoration( color: getColor(context, ColorType.background), borderRadius: const BorderRadius.only( topLeft: Radius.circular(30), topRight: Radius.circular(30), ), - boxShadow: [ - SheetBoxShadow - ] + boxShadow: (SheetNavigationContext.of(context) != null && !SheetNavigationContext.of(context)!.shouldShowShadow) ? null : [SheetBoxShadow] + ), child: Padding( padding: const EdgeInsets.only( @@ -115,7 +77,7 @@ class _BusSheetState extends State { child: SingleChildScrollView( // controller: widget.scrollController, // controller: getScrollController(), - controller: null, + controller: widget.scrollController ?? SheetNavigationContext.of(context)?.scrollController, child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/stop_sheet.dart b/lib/widgets/stop_sheet.dart index b68984e..e20a9aa 100644 --- a/lib/widgets/stop_sheet.dart +++ b/lib/widgets/stop_sheet.dart @@ -21,7 +21,7 @@ bool isRide(String? s) { return false; } -class StopSheet extends StatefulWidget implements NavigableSheet { +class StopSheet extends StatefulWidget { final String stopID; final String stopName; final Future Function(String, String) onFavorite; @@ -29,14 +29,7 @@ class StopSheet extends StatefulWidget implements NavigableSheet { final void Function() onGetDirections; final void Function(String) showBusSheet; final BusProvider busProvider; - final ScrollController scrollController; // Scroll controller of the DraggableScrollableSheet (the parent Widget) - // final GlobalKey<_StopSheetState> stateKey; - - @override - void setShouldUseScrollController(bool shouldUseScrollController) { - // debugPrint("BusSheet: Got setCustomScrollController call with ${newScrollController}"); - // stateKey.currentState?.setShouldUseScrollController(shouldUseScrollController); - } + final ScrollController? scrollController; // Scroll controller of the DraggableScrollableSheet (the parent Widget) const StopSheet({super.key, required this.stopID, @@ -46,49 +39,9 @@ class StopSheet extends StatefulWidget implements NavigableSheet { required this.onGetDirections, required this.showBusSheet, required this.busProvider, - required this.scrollController + this.scrollController // Note: scrollController should be null ONLY if it's inside a SheetNavigator (in which case the SheetNavigator provides it through SheetNavigationContext). }); - // StopSheet._({ - // required this.stateKey, - // required this.stopID, - // required this.stopName, - // required this.onFavorite, - // required this.onUnFavorite, - // required this.onGetDirections, - // required this.showBusSheet, - // required this.busProvider, - // required this.scrollController - // }) : super(key: stateKey); - - // Yes, I know this is a mess. I'm sorry. - // The reason this needs to be here: - // * When used in a SheetNavigator, two sheets are displayed on top of each other for smooth transitions - // * If both sheets are using the ScrollController, they fight each other and make it difficult for the user to swipe the sheet away - // * Thus, SheetNavigationManager needs to call StopSheet.setShouldUseScrollController to tell it whether it should use the scroll controller (i.e. if it's in the foreground) or not - // * Unfortunately, in order to make StopSheet able to call a method inside StopSheetState, Flutter requires that you have a GlobalKey<_StopSheetState>, and for that you need to make a factory. Womp womp. - // factory StopSheet({ - // required String stopID, - // required String stopName, - // required Future Function(String, String) onFavorite, - // required Future Function(String, String) onUnFavorite, - // required Function() onGetDirections, - // required Function(String) showBusSheet, - // required BusProvider busProvider, - // required ScrollController scrollController, - // }) { - // final key = GlobalKey<_StopSheetState>(); - // return StopSheet._( - // stateKey: key, - // stopID: stopID, - // stopName: stopName, - // onFavorite: onFavorite, - // onUnFavorite: onUnFavorite, - // onGetDirections: onGetDirections, - // showBusSheet: showBusSheet, - // busProvider: busProvider, - // scrollController: scrollController); - // } @override State createState() => _StopSheetState(); @@ -309,29 +262,6 @@ class _StopSheetState extends State { late bool imageBusStop; late String imagePath; - bool shouldUseScrollControler = true; - - // The shouldUseScrollControler parameter necessary so that when two widgets occupy the same Sheet - // (i.e. when one sheet is displayed on top of another via SheetNavigator), both Sheets don't - // fight over the scroll controller and make scrolling janky. The SheetNavigator feeds a "dummy" - // scroll controller when this sheet is displayed behind something else - void setShouldUseScrollController(bool shouldUseScrollControlerIn) { - // debugPrint("StopSheet: INSIDE: setShouldUseScrollController() call: $shouldUseScrollControlerIn"); - // setState(() { - // shouldUseScrollControler = shouldUseScrollControlerIn; - // }); - } - ScrollController? getScrollController() { - return null; - if (!shouldUseScrollControler) { - return null; - } - debugPrint(" customScrollController is null, returning the vanilla one"); - return widget.scrollController; - } - - - @override void initState() { super.initState(); @@ -438,7 +368,8 @@ class _StopSheetState extends State { // snap: true, // snapSizes: [initialSize], // builder: (BuildContext context, ScrollController scrollController) { - return Container( + return AnimatedContainer( + duration: Duration(milliseconds: 500), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: getColor(context, ColorType.background), @@ -446,9 +377,10 @@ class _StopSheetState extends State { topLeft: Radius.circular(30), topRight: Radius.circular(30), ), - boxShadow: [ - SheetBoxShadow - ] + boxShadow: (SheetNavigationContext.of(context) != null && !SheetNavigationContext.of(context)!.shouldShowShadow) ? null : [SheetBoxShadow] + // boxShadow: [ + // SheetBoxShadow + // ] ), // overflow box lets the stuff inside not shrink when page is being closed @@ -492,7 +424,7 @@ class _StopSheetState extends State { physics: const ClampingScrollPhysics(), // controller: widget.scrollController, // controller: getScrollController(), - controller: null, + controller: widget.scrollController ?? SheetNavigationContext.of(context)?.scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -653,6 +585,7 @@ class _StopSheetState extends State { ListView.separated( // controller: widget.scrollController,\ // controller: getScrollController(), + controller: widget.scrollController ?? SheetNavigationContext.of(context)?.scrollController, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), From 3742efa31967dcd04c8238e054ec8f6022359c80 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:53:37 -0400 Subject: [PATCH 09/15] Fixed shadows and cleaned up code --- lib/constants.dart | 17 + lib/services/sheet_navigation_manager.dart | 263 +++---- lib/widgets/bus_sheet.dart | 14 +- lib/widgets/stop_sheet.dart | 842 ++++++++++----------- 4 files changed, 541 insertions(+), 595 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 0935f71..22f5856 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -415,6 +415,7 @@ class Loadpoint { Loadpoint(this.message, this.step); } +// Standard shadow behind a sheet const SheetBoxShadow = BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.2), offset: const Offset( @@ -424,3 +425,19 @@ const SheetBoxShadow = BoxShadow( blurRadius: 100.0, spreadRadius: 40.0, ); + +// Modified shadow that only appears behind the left side of a sheet +// This is used for sheets inside a SheetNavigator so the foreground +// sheet stands out from the background sheet. +// The SheetBoxShadow is applied behind everything, but the only shadow +// applied to the sheets inside the navigator is SheetBoxLeftShadow +const SheetBoxLeftShadow = BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.2), + offset: const Offset( + 0.0, + 30.0, + ), + blurRadius: 40.0, + spreadRadius: 0.0, +); + diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index ccc2af0..2fec4a9 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -16,11 +16,30 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:provider/provider.dart'; +enum ShadowType { + noShadow, + fadeInShadow, + fullShadow +} + +typedef PushSheet = void Function(Widget sheet); + +// A custom context that allows the ScrollController to be assigned dynamically. +// +// For the transition, SheetNavigator stacks two sheets on top of each other +// (i.e. a StopSheet on top of a BusSheet) so you can see the bottom sheet while +// you were swiping away the top sheet. +// +// Problem was--because both sheets are dispayed at the same time, both had +// access to the DraggableScrollableSheet's ScrollController, and the background +// sheet would fight the foreground sheet when the user tried to swipe the +// modal down. Thus, SheetNavigationContext hangs on to the ScrollController +// and dishes it out (or not) as necessary to whatever sheets it contains to +// prevent conflicts. class SheetNavigationContext extends InheritedWidget { final ScrollController scrollController; - final bool shouldShowShadow; - const SheetNavigationContext({required this.scrollController, required this.shouldShowShadow, required super.child}); + const SheetNavigationContext({required this.scrollController, required super.child}); // The static accessor - this is the "of(context)" pattern static SheetNavigationContext? of(BuildContext context) { @@ -31,16 +50,28 @@ class SheetNavigationContext extends InheritedWidget { bool updateShouldNotify(SheetNavigationContext old) => scrollController != old.scrollController; } +class SheetNavigator extends StatefulWidget { + final Function(ScrollController, PushSheet) initialSheetBuilder; + final ScrollController scrollController; + SheetNavigator({ + required this.scrollController, + required this.initialSheetBuilder + }); + + @override + State createState() => SheetNavigatorState(); +} + // SheetNavigator is a custom widget that allows multiple sheets to be displayed, one after another. // Useful if the user is navigating through many Sheets (e.g. BusSheet to StopSheet to BusSheet) -// SheetNavigator manages its back stack, so the Android back button (and whatever back buttons you add to the UI) go backwards through history with a nice card animation +// SheetNavigator manages its back stack, so the Android back button (and whatever back buttons +// you add to the UI) go backwards through history with a nice card animation class SheetNavigatorState extends State { List _stack = []; - int oldStackLength = 0; bool isGoingBackwards = false; int numSteps = 0; // Used for the Dismissable key bool shouldAnimate = true; // Used to prevent a "double animation" happening after the user swipes away a sheet - final ScrollController dummyScrollController = ScrollController(); + final ScrollController inactiveScrollController = ScrollController(); // Given to the sheet in the background so it doesn't fight when closing the modal void pushWidget(Widget sheet) { setState(() { @@ -48,80 +79,36 @@ class SheetNavigatorState extends State { _stack.add(sheet); isGoingBackwards = false; numSteps++; - updateScrollControllers(); }); } void popWidget() { setState(() { - oldStackLength = _stack.length; _stack.removeLast(); isGoingBackwards = true; numSteps++; - updateScrollControllers(); }); } - void updateScrollControllers() { - // debugPrint("******* UPDATING SCROLL CONTROLLERS"); - // _stack.last.setShouldUseScrollController(true); - // // _stack.last.setCustomScrollController(widget.scrollController); - // if (_stack.length > 1) { - // _stack[_stack.length - 2].setShouldUseScrollController(false); - // // _stack[_stack.length - 2].setCustomScrollController(ScrollController()); - // // Give the background element a dummy ScrollController so it doesn't fight the user when - // // trying to close the modal - // } - } - - void popWidgetNoAnimation() { - // shouldAnimate = false; - // popWidget(); // setState() called inside here - debugPrint("popWidgetNoAnimation call!"); - - setState(() {shouldAnimate = false;}); + void popWidgetNoAnimation() { setState(() { - oldStackLength = _stack.length; + shouldAnimate = false; _stack.removeLast(); isGoingBackwards = true; numSteps++; - updateScrollControllers(); }); - - // WidgetsBinding.instance.addPostFrameCallback((Duration d) { - // debugPrint("Post-frame callback reached!"); - // setState(() { - // oldStackLength = _stack.length; - // _stack.removeLast(); - // isGoingBackwards = true; - // numSteps++; - // }); - // }); - - - - // WidgetsBinding.instance.addPostFrameCallback((Duration d) { - // debugPrint("Post-frame callback reached!"); - // setState(() { - // oldStackLength = _stack.length; - // _stack.removeLast(); - // isGoingBackwards = true; - // numSteps++; - // }); - // }); - } @override void initState() { super.initState(); - _stack.add(widget.getInitialSheetFromOutside(widget.scrollController, pushWidget)); + _stack.add(widget.initialSheetBuilder(widget.scrollController, pushWidget)); } @override void dispose() { - dummyScrollController.dispose(); + inactiveScrollController.dispose(); super.dispose(); } @@ -132,107 +119,91 @@ class SheetNavigatorState extends State { return PopScope( canPop: _stack.length <= 1, onPopInvokedWithResult: (didPop, result) { - // When the Android back button is pressed (or the current widget gets closed programatically), it's routed here and SheetNavigator takes the current sheet off the back stack + // When the Android back button is pressed (or the current widget gets closed programatically), + // it's routed here and SheetNavigator takes the current sheet off the back stack if (!didPop) { shouldAnimate = true; // User clicked back button so we need a back animation popWidget(); } - }, child: AnimatedSwitcher( - duration: Duration(milliseconds: 400), - transitionBuilder: (Widget child, Animation animation) { - debugPrint("Building transition, shouldAnimate is ${shouldAnimate}"); - final isEntering = child.key == ValueKey(_stack.length); - - return SlideTransition( - // All this logic serves to make the forward and backward card animations look nice - position: Tween( - begin: - !shouldAnimate ? ( - Offset.zero - ) - : isGoingBackwards ? ( - isEntering ? - const Offset(-1, 0.0) : const Offset(1, 0.0) - ) : isEntering ? - const Offset(1, 0.0) - : const Offset(-1, 0.0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: (isGoingBackwards && !isEntering) ? Curves.easeInOut : Curves.ease - ) - ), - child: child, - ); - }, - layoutBuilder: (currentChild, previousChildren) { - if (isGoingBackwards) { - // If the user is going backwards through the back stack, display the stack in reverse order so it looks like the top card is lifting off the stack - return Stack( + }, child: Container( + decoration: BoxDecoration( + boxShadow: [SheetBoxShadow] // Shadow that sits behind all the sheets in the SheetNavigator + ), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + final isEntering = child.key == ValueKey(_stack.length); + + return SlideTransition( + // All this positioning logic serves to make the forward and backward card animations look nice + position: Tween( + begin: + !shouldAnimate ? ( + Offset.zero + ) + : isGoingBackwards ? ( + isEntering ? + const Offset(-1, 0.0) : const Offset(1, 0.0) + ) : isEntering ? + const Offset(1, 0.0) + : const Offset(0, 0.0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animation, + curve: (isGoingBackwards && !isEntering) ? Curves.easeInOut : Curves.ease + ) + ), + child: child, + ); + }, + layoutBuilder: (currentChild, previousChildren) { + if (isGoingBackwards) { + // If the user is going backwards through the back stack, display the stack + // in reverse order so it looks like the top card is lifting off the stack + return Stack( + children: [ + if (currentChild != null) currentChild, + ...previousChildren, + ] + ); + } + return Stack( // Forward order children: [ - if (currentChild != null) currentChild, ...previousChildren, + if (currentChild != null) currentChild ] ); - } - return Stack( // Forward order - children: [ - ...previousChildren, - if (currentChild != null) currentChild - ] - ); - }, - child: KeyedSubtree( - key: ValueKey(_stack.length), - child: _stack.length > 1 ? - Stack( - children: [ - // IgnorePointer( - // child: SizedBox.expand(child: _stack[_stack.length - 2]) - // ), - //SizedBox.expand(), - SizedBox.expand(child: (_stack.length - 2 >= 0) ? - SheetNavigationContext(child: _stack[_stack.length - 2], shouldShowShadow: false, scrollController: dummyScrollController) - : null), - Dismissible( - key: ValueKey(numSteps), - direction: DismissDirection.startToEnd, - onDismissed: (DismissDirection d) { - popWidgetNoAnimation(); - }, - child: SizedBox.expand(child: SheetNavigationContext(child: _stack.last, shouldShowShadow: true, scrollController: widget.scrollController)) - ) - ] - - // NEXT STEPS TODO: Figure out why the stacked sheets fight scrolling so much. Do they both hang on to the same ScrollController? - - ) - : SizedBox.expand(child: SheetNavigationContext(child: _stack.last, shouldShowShadow: true, scrollController: widget.scrollController,)) - ), + }, + child: KeyedSubtree( + key: ValueKey(_stack.length), + child: _stack.length > 1 ? + Stack( + children: [ + SizedBox.expand(child: (_stack.length - 2 >= 0) ? + SheetNavigationContext(scrollController: inactiveScrollController, child: _stack[_stack.length - 2]) + : null), + Dismissible( + key: ValueKey(numSteps), + direction: DismissDirection.startToEnd, + onDismissed: (DismissDirection d) { + popWidgetNoAnimation(); + }, + child: SizedBox.expand(child: SheetNavigationContext(scrollController: widget.scrollController, child: _stack.last)) + ) + ] + ) + : SizedBox.expand(child: SheetNavigationContext(scrollController: widget.scrollController,child: _stack.last,)) + ), + ) ) ); - //); } - -} - -class SheetNavigator extends StatefulWidget { - final Function(ScrollController, Function(Widget)) getInitialSheetFromOutside; - final ScrollController scrollController; - SheetNavigator({ - required this.scrollController, - required this.getInitialSheetFromOutside - }); - - @override - State createState() => SheetNavigatorState(); } // SheetNavigationManager is responsible for all the sheets that open up from the map screen. -// This code used to all live in map_screen.dart, but it was very unweildy, so I -// wrapped this into its own class whose job it is to manage which sheets are shown when. +// This code used to all live in map_screen.dart, so I wrapped this into a separate class. // All map_screen has to do is provide some callbacks (onSelectJourney, onSelectStop, etc) // and SheetNavigationManager figures out the history, back stack, DraggableScrollableSheet stuff, etc. class SheetNavigationManager { @@ -431,7 +402,7 @@ class SheetNavigationManager { ); } - BusSheet getBusSheet(String busID, ScrollController? scrollController, Function(Widget) pushNewSheet) { + BusSheet getBusSheet(String busID, ScrollController? scrollController, PushSheet pushNewSheet) { return BusSheet( busID: busID, scrollController: scrollController, @@ -439,7 +410,6 @@ class SheetNavigationManager { // When the user clicks on a specific bus stop to open the details (StopSheet) page LatLng? latLong = getLatLongFromStopID(id); if (latLong != null) { - // pushNewSheet(getStopSheet(id, name, latLong.latitude, latLong.longitude, pushNewSheet, scrollController)); pushNewSheet(getStopSheet(id, name, latLong.latitude, latLong.longitude, pushNewSheet, null)); } else { showMaizebusOKDialog( @@ -466,8 +436,7 @@ class SheetNavigationManager { return Container( child: SheetNavigator( scrollController: scrollController, - getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushNewSheet) => getBusSheet(busID, null, pushNewSheet) - // getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushNewSheet) => getBusSheet(busID, scrollControllerLocal, pushNewSheet) + initialSheetBuilder: (ScrollController scrollControllerLocal, PushSheet pushNewSheet) => getBusSheet(busID, null, pushNewSheet) ) ); }, @@ -490,7 +459,7 @@ class SheetNavigationManager { } - StopSheet getStopSheet(String stopID, String stopName, double lat, double long, Function(Widget) pushSheet, ScrollController? scrollControllerLocal) { + StopSheet getStopSheet(String stopID, String stopName, double lat, double long, PushSheet pushSheet, ScrollController? scrollControllerLocal) { final busProvider = Provider.of(context, listen: false); return StopSheet( @@ -501,7 +470,6 @@ class SheetNavigationManager { scrollController: scrollControllerLocal, showBusSheet: (busId) { // When someone clicks "See all stops for this bus" this callback runs - // pushSheet(getBusSheet(busId, scrollControllerLocal, pushSheet)); pushSheet(getBusSheet(busId, null, pushSheet)); }, busProvider: busProvider, @@ -539,8 +507,7 @@ class SheetNavigationManager { builder: (BuildContext context, ScrollController scrollController) { return SheetNavigator( scrollController: scrollController, - getInitialSheetFromOutside: (ScrollController scrollControllerLocal, Function(Widget) pushSheet) { - // return getStopSheet(stopID, stopName, lat, long, pushSheet, scrollControllerLocal); + initialSheetBuilder: (ScrollController scrollControllerLocal, PushSheet pushSheet) { return getStopSheet(stopID, stopName, lat, long, pushSheet, null); } ); @@ -548,7 +515,7 @@ class SheetNavigationManager { } ) - ).then((_) {}); + ); } } \ No newline at end of file diff --git a/lib/widgets/bus_sheet.dart b/lib/widgets/bus_sheet.dart index b58b40f..5346716 100644 --- a/lib/widgets/bus_sheet.dart +++ b/lib/widgets/bus_sheet.dart @@ -36,8 +36,9 @@ class BusSheet extends StatefulWidget { class _BusSheetState extends State { late Bus? currBus = BusRepository.getBus(widget.busID); - late Future> futureBusStops; - + late Future> futureBusStops; + + @override void initState() { super.initState(); @@ -50,10 +51,6 @@ class _BusSheetState extends State { // This accounts for that. if (currBus == null) return Text("Bus not found"); - // return Text("HI"); - - debugPrint("BusSheet: Building..."); - final bus = currBus!; Widget output = AnimatedContainer( @@ -64,7 +61,7 @@ class _BusSheetState extends State { topLeft: Radius.circular(30), topRight: Radius.circular(30), ), - boxShadow: (SheetNavigationContext.of(context) != null && !SheetNavigationContext.of(context)!.shouldShowShadow) ? null : [SheetBoxShadow] + boxShadow: [SheetBoxLeftShadow] ), child: Padding( @@ -75,8 +72,6 @@ class _BusSheetState extends State { bottom: 0, ), child: SingleChildScrollView( - // controller: widget.scrollController, - // controller: getScrollController(), controller: widget.scrollController ?? SheetNavigationContext.of(context)?.scrollController, child: Column( @@ -122,7 +117,6 @@ class _BusSheetState extends State { ), ), ); - debugPrint("BusSheet: Finished building"); return output; } } diff --git a/lib/widgets/stop_sheet.dart b/lib/widgets/stop_sheet.dart index e20a9aa..952e99e 100644 --- a/lib/widgets/stop_sheet.dart +++ b/lib/widgets/stop_sheet.dart @@ -305,8 +305,6 @@ class _StopSheetState extends State { @override Widget build(BuildContext context) { - debugPrint("StopSheet: Building..."); - // return Text("It÷ works!"); return Stack( children: [ // stacking the sheet on top of a gesture detector so you can close it by tapping out of it @@ -334,488 +332,458 @@ class _StopSheetState extends State { } } - double initialSize = 0.9; - - // if (snapshot.hasData) { - // final itemCount = arrivingBuses.length; - - // // edge case - // if (itemCount == 0) { - // if (imageBusStop) { - // initialSize = 0.8; - // } else { - // initialSize = 0.5; - // } - // } - // } else { - // // A fixed initial size for loading or error states. - // initialSize = 0.4; - // if (imageBusStop) { - // initialSize = 0.6; - // } - // } - // we know image dimensions, so we can use the width to find the height // with a lil simple math double heightOfImage = (imageBusStop)? ((MediaQuery.sizeOf(context).width) * 0.54345703125) : 0; double paddingBelowButtons = globalBottomPadding; // TODO: Figure out why these buttons are pushed so far down - // return DraggableScrollableSheet( - // initialChildSize: initialSize, - // minChildSize: 0.0, // leave at 0.0 to allow full dismissal - // maxChildSize: initialSize, - // snap: true, - // snapSizes: [initialSize], - // builder: (BuildContext context, ScrollController scrollController) { - return AnimatedContainer( - duration: Duration(milliseconds: 500), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: getColor(context, ColorType.background), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30), - ), - boxShadow: (SheetNavigationContext.of(context) != null && !SheetNavigationContext.of(context)!.shouldShowShadow) ? null : [SheetBoxShadow] - // boxShadow: [ - // SheetBoxShadow - // ] - ), + return AnimatedContainer( + duration: Duration(milliseconds: 500), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: getColor(context, ColorType.background), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + boxShadow: [SheetBoxLeftShadow] + // boxShadow: [ + // SheetBoxShadow + // ] + ), - // overflow box lets the stuff inside not shrink when page is being closed - child: OverflowBox( - alignment: Alignment.topCenter, - maxHeight: null, - // maxHeight: MediaQuery.of(context).size.height * initialSize, - // In this stack, - // First (bottom) layer is image, which sometimes doesn't exist - // Second layer is another stack. Inside that stack is: - // first layer: the the main body and content - // second layer is a box with a gradient - // third layer is the buttons themselves - child: Stack( + // overflow box lets the stuff inside not shrink when page is being closed + child: OverflowBox( + alignment: Alignment.topCenter, + maxHeight: null, + // maxHeight: MediaQuery.of(context).size.height * initialSize, + // In this stack, + // First (bottom) layer is image, which sometimes doesn't exist + // Second layer is another stack. Inside that stack is: + // first layer: the the main body and content + // second layer is a box with a gradient + // third layer is the buttons themselves + child: Stack( + children: [ + (imageBusStop)? + // Image of bus stop if it exists + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + child: Image.asset( + imagePath, + fit: BoxFit.cover, + ), + ) + // bus stop image does not exist, use empty widget + : SizedBox.shrink(), + + Stack( children: [ - (imageBusStop)? - // Image of bus stop if it exists - ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30), - ), - child: Image.asset( - imagePath, - fit: BoxFit.cover, - ), - ) - // bus stop image does not exist, use empty widget - : SizedBox.shrink(), - - Stack( + // yes this column only has one thing. yes this is + // the only way it works becuase the stack won't play + // nice without it. Thank you flutter + Column( + mainAxisSize: MainAxisSize.max, children: [ - // yes this column only has one thing. yes this is - // the only way it works becuase the stack won't play - // nice without it. Thank you flutter - Column( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - // controller: widget.scrollController, - // controller: getScrollController(), - controller: widget.scrollController ?? SheetNavigationContext.of(context)?.scrollController, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // spacer for image with gradient - Container( - height: heightOfImage, - decoration: BoxDecoration( - gradient: getStopHeroImageGradient(context) - ), - ), - - // wrapped in container to add background color - Container( - color: getColor(context, ColorType.background), - child: Column( - children: [ - // header - Padding( - padding: EdgeInsets.only( - top: (imageBusStop) ? 0 : 20, - left: 20, - right: 20, + Expanded( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + // controller: widget.scrollController, + // controller: getScrollController(), + controller: widget.scrollController ?? SheetNavigationContext.of(context)?.scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // spacer for image with gradient + Container( + height: heightOfImage, + decoration: BoxDecoration( + gradient: getStopHeroImageGradient(context) + ), + ), + + // wrapped in container to add background color + Container( + color: getColor(context, ColorType.background), + child: Column( + children: [ + // header + Padding( + padding: EdgeInsets.only( + top: (imageBusStop) ? 0 : 20, + left: 20, + right: 20, + ), + child: Row( + children: [ + Expanded( + child: Text( + widget.stopName, + style: TextStyle( + fontFamily: 'Urbanist', + fontWeight: FontWeight.w700, + fontSize: 30, + height: 1.1, + ), + ), ), - child: Row( - children: [ - Expanded( - child: Text( - widget.stopName, - style: TextStyle( - fontFamily: 'Urbanist', - fontWeight: FontWeight.w700, - fontSize: 30, - height: 1.1, + + SizedBox(width: 15), + + Column( + children: [ + IntrinsicWidth( + child: Container( + height: 25, + decoration: BoxDecoration( + color: Colors.amber, + borderRadius: + BorderRadius.circular(7), ), - ), - ), - - SizedBox(width: 15), - - Column( - children: [ - IntrinsicWidth( - child: Container( - height: 25, - decoration: BoxDecoration( - color: Colors.amber, - borderRadius: - BorderRadius.circular(7), - ), - child: Center( - child: Padding( - padding: - const EdgeInsets.symmetric( - horizontal: 5, - ), - child: MediaQuery( - data: MediaQuery.of(context) - .copyWith( - textScaler: - TextScaler.linear( - 1.0, - ), - ), - child: Text( - widget.stopID, - style: TextStyle( - color: Colors.black, - fontFamily: 'Urbanist', - fontWeight: - FontWeight.w700, - fontSize: 17, - ), + child: Center( + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 5, + ), + child: MediaQuery( + data: MediaQuery.of(context) + .copyWith( + textScaler: + TextScaler.linear( + 1.0, + ), ), + child: Text( + widget.stopID, + style: TextStyle( + color: Colors.black, + fontFamily: 'Urbanist', + fontWeight: + FontWeight.w700, + fontSize: 17, ), ), ), ), ), - ], + ), ), ], ), + ], + ), + ), + + SizedBox(height: 20), + + // loading text and button + Material( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, ), - - SizedBox(height: 20), - - // loading text and button - Material( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, + child: Row( + children: [ + SizedBox(width: 2), + Text( + "Next bus departures", + style: TextStyle( + fontFamily: 'Urbanist', + fontWeight: FontWeight.w400, + fontSize: 20, + ), ), - child: Row( - children: [ - SizedBox(width: 2), - Text( - "Next bus departures", - style: TextStyle( - fontFamily: 'Urbanist', - fontWeight: FontWeight.w400, - fontSize: 20, - ), - ), - SizedBox(width: 5), - RefreshButton( - loading: snapshot.connectionState == ConnectionState.waiting, - onTap: _refreshData - ), - ], + SizedBox(width: 5), + RefreshButton( + loading: snapshot.connectionState == ConnectionState.waiting, + onTap: _refreshData ), - ), + ], ), - - - // main page - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 0, - ), - child: - (snapshot.connectionState == - ConnectionState.waiting) - ? const SizedBox() - : (snapshot.hasData) - ? Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - (arrivingBuses.length == 0) - ? - Center( - child: Column( - children: [ - SizedBox(height: 50,), - Icon( - Icons.no_transfer, - size: 80, - color: Color.fromARGB(255, 150, 150, 150), - ), - Text( - "no buses arriving", - style: TextStyle( - color: Color.fromARGB(255, 150, 150, 150), - fontWeight: FontWeight.bold - ), - ), - ], - ), - ) - - : - SizedBox(height: 10), - - Column( - mainAxisSize: MainAxisSize.max, - children: [ - ListView.separated( - // controller: widget.scrollController,\ - // controller: getScrollController(), - controller: widget.scrollController ?? SheetNavigationContext.of(context)?.scrollController, - shrinkWrap: true, - physics: - NeverScrollableScrollPhysics(), - itemCount: arrivingBuses.length, - itemBuilder: (context, index) { - BusWithPrediction bus = - arrivingBuses[index]; - - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 575), - delay: const Duration(milliseconds: 100), - child: FadeInAnimation( - child: ExpandableStopWidget( - routeId: bus.id, - vehicleId: bus.vehicleId, - busId: bus.id, - busPrediction: - bus.prediction, - busDirection: bus.direction, - stopId: widget.stopID, - showBusSheet: - widget.showBusSheet, - busProvider: - widget.busProvider, - ) - ) - - - ); + ), + ), - }, - separatorBuilder: (context, index) { - return Divider( - height: 0, - indent: 20, - endIndent: 20, - thickness: 1, - ); - }, + // main page + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 0, + ), + child: + (snapshot.connectionState == + ConnectionState.waiting) + ? const SizedBox() + : (snapshot.hasData) + ? Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + (arrivingBuses.length == 0) + ? + Center( + child: Column( + children: [ + SizedBox(height: 50,), + Icon( + Icons.no_transfer, + size: 80, + color: Color.fromARGB(255, 150, 150, 150), + ), + Text( + "no buses arriving", + style: TextStyle( + color: Color.fromARGB(255, 150, 150, 150), + fontWeight: FontWeight.bold + ), + ), + ], ), - - SizedBox(height: paddingBelowButtons + 40,) // The +40 allows enough space for "See all stops for this bus" button to be clickable - ], + ) + + : + SizedBox(height: 10), + + Column( + mainAxisSize: MainAxisSize.max, + children: [ + ListView.separated( + controller: widget.scrollController ?? SheetNavigationContext.of(context)?.scrollController, + shrinkWrap: true, + physics: + NeverScrollableScrollPhysics(), + itemCount: arrivingBuses.length, + itemBuilder: (context, index) { + BusWithPrediction bus = + arrivingBuses[index]; + + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 575), + delay: const Duration(milliseconds: 100), + child: FadeInAnimation( + child: ExpandableStopWidget( + routeId: bus.id, + vehicleId: bus.vehicleId, + busId: bus.id, + busPrediction: + bus.prediction, + busDirection: bus.direction, + stopId: widget.stopID, + showBusSheet: + widget.showBusSheet, + busProvider: + widget.busProvider, + ) + ) + + + ); + + + }, + separatorBuilder: (context, index) { + return Divider( + height: 0, + indent: 20, + endIndent: 20, + thickness: 1, + ); + }, ), + + SizedBox(height: paddingBelowButtons + 40,) // The +40 allows enough space for "See all stops for this bus" button to be clickable ], - ) - : Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - child: Text( - "Can't load data. Check your internet connection and try refreshing", - style: TextStyle( - fontFamily: 'Urbanist', - fontWeight: FontWeight.w400, - fontSize: 20, - ), - ), ), + ], + ) + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Text( + "Can't load data. Check your internet connection and try refreshing", + style: TextStyle( + fontFamily: 'Urbanist', + fontWeight: FontWeight.w400, + fontSize: 20, + ), + ), ), - ], - ), - ) - ], + ), + ], + ), ) - ), - ), - ], + ], + ) + ), ), - - // white box with gradient that the buttons sit on - Column( - children: [ - Spacer(), // another spacer to stick this to the bottom - - Container( - height: paddingBelowButtons + 65, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - getColor(context, ColorType.backgroundGradientStart), // transparent - Color.lerp(getColor(context, ColorType.backgroundGradientStart), getColor(context, ColorType.background), 0.5)!, // half-way color - getColor(context, ColorType.background), // full color - ], - stops: [0, 0.4, 1] - ), - ), + ], + ), + + // white box with gradient that the buttons sit on + Column( + children: [ + Spacer(), // another spacer to stick this to the bottom + + Container( + height: paddingBelowButtons + 65, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + getColor(context, ColorType.backgroundGradientStart), // transparent + Color.lerp(getColor(context, ColorType.backgroundGradientStart), getColor(context, ColorType.background), 0.5)!, // half-way color + getColor(context, ColorType.background), // full color + ], + stops: [0, 0.4, 1] ), - ], + ), ), - - - // bottom buttons - Column( - children: [ - Spacer(), // sticks buttons to bottom - - Padding( - padding: EdgeInsets.symmetric(horizontal: globalLeftRightPadding), - child: Row( - children: [ - ElevatedButton.icon( - onPressed: () { - Navigator.pop(context); - widget.onGetDirections(); - }, - style: ElevatedButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - backgroundColor: getColor(context, ColorType.importantButtonBackground), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - elevation: 0 - ), - icon: Icon( - Icons.directions, - color: getColor(context, ColorType.importantButtonText), - size: 20, - ), - label: Text( - 'Get Directions', - style: TextStyle( - color: getColor(context, ColorType.importantButtonText), - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), + ], + ), + + + // bottom buttons + Column( + children: [ + Spacer(), // sticks buttons to bottom + + Padding( + padding: EdgeInsets.symmetric(horizontal: globalLeftRightPadding), + child: Row( + children: [ + ElevatedButton.icon( + onPressed: () { + Navigator.pop(context); + widget.onGetDirections(); + }, + style: ElevatedButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: getColor(context, ColorType.importantButtonBackground), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), ), - - Spacer(), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + elevation: 0 + ), + icon: Icon( + Icons.directions, + color: getColor(context, ColorType.importantButtonText), + size: 20, + ), + label: Text( + 'Get Directions', + style: TextStyle( + color: getColor(context, ColorType.importantButtonText), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + Spacer(), + + ElevatedButton( + onPressed: () { + // Read the current state + final bool currentStatus = _isFavorited ?? false; + + // Call the appropriate function + if (currentStatus){ + widget.onUnFavorite(widget.stopID, widget.stopName); + } else { + widget.onFavorite(widget.stopID, widget.stopName); + } + + // Update the UI immediately + setState(() { + _isFavorited = !currentStatus; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getColor(context, ColorType.secondaryButtonBackground), + shape: CircleBorder(), + shadowColor: Colors.black, + padding: EdgeInsets.zero, + minimumSize: Size(0,0), + fixedSize: Size(40,40), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + elevation: 0 + ), + child: Icon( + (_isFavorited ?? false)? Icons.favorite : Icons.favorite_border, + color: (_isFavorited ?? false)? Colors.red : getColor(context, ColorType.secondaryButtonText), + size: 20, + ), + ), + + SizedBox(width: 10,), + + ElevatedButton( + onPressed: () { + if (arrivingBuses.isEmpty) { + return; + } + + showDialog( + context: context, + builder: (context) { + return Dialog( - ElevatedButton( - onPressed: () { - // Read the current state - final bool currentStatus = _isFavorited ?? false; + backgroundColor: getColor(context, ColorType.background), - // Call the appropriate function - if (currentStatus){ - widget.onUnFavorite(widget.stopID, widget.stopName); - } else { - widget.onFavorite(widget.stopID, widget.stopName); - } - // Update the UI immediately - setState(() { - _isFavorited = !currentStatus; - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: getColor(context, ColorType.secondaryButtonBackground), - shape: CircleBorder(), - shadowColor: Colors.black, - padding: EdgeInsets.zero, - minimumSize: Size(0,0), - fixedSize: Size(40,40), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - elevation: 0 - ), - child: Icon( - (_isFavorited ?? false)? Icons.favorite : Icons.favorite_border, - color: (_isFavorited ?? false)? Colors.red : getColor(context, ColorType.secondaryButtonText), - size: 20, - ), - ), - - SizedBox(width: 10,), - - ElevatedButton( - onPressed: () { - if (arrivingBuses.isEmpty) { - return; - } - - showDialog( - context: context, - builder: (context) { - return Dialog( - - backgroundColor: getColor(context, ColorType.background), - - + + constraints: BoxConstraints( + minWidth: 0.0, + minHeight: 0.0, + ), + child: ReminderForm( + stpid: widget.stopID, - constraints: BoxConstraints( - minWidth: 0.0, - minHeight: 0.0, - ), - child: ReminderForm( - stpid: widget.stopID, - - activeRoutes: arrivingBuses - .fold([], (xs, x) => xs.contains(x.id) ? xs : xs + [x.id]), - ), - ); - } + activeRoutes: arrivingBuses + .fold([], (xs, x) => xs.contains(x.id) ? xs : xs + [x.id]), + ), ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: getColor(context, ColorType.secondaryButtonBackground), - shape: CircleBorder(), - shadowColor: Colors.black, - padding: EdgeInsets.zero, - minimumSize: Size(0,0), - fixedSize: Size(40,40), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - elevation: 0 - ), - child: Icon( - (arrivingBuses.isEmpty)? Icons.notifications_off_outlined : Icons.notifications_none, - color: getColor(context, ColorType.secondaryButtonText), - size: 20.0, - ) - ), - ], + } + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getColor(context, ColorType.secondaryButtonBackground), + shape: CircleBorder(), + shadowColor: Colors.black, + padding: EdgeInsets.zero, + minimumSize: Size(0,0), + fixedSize: Size(40,40), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + elevation: 0 + ), + child: Icon( + (arrivingBuses.isEmpty)? Icons.notifications_off_outlined : Icons.notifications_none, + color: getColor(context, ColorType.secondaryButtonText), + size: 20.0, + ) ), - ), - - SizedBox(height: paddingBelowButtons,) - ], + ], + ), ), + + SizedBox(height: paddingBelowButtons,) ], ), ], ), - ), - ); + ], + ), + ), + ); // }, // ); }, From bf215cca315dbe48094370d4bd1321c4c4d8ca19 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:10:09 -0400 Subject: [PATCH 10/15] Fixed dialogs so they accept strings again --- lib/screens/map_screen.dart | 4 ++-- lib/services/sheet_navigation_manager.dart | 4 ++-- lib/widgets/bus_sheet.dart | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 879e06e..53e1cce 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1746,8 +1746,8 @@ class _MaizeBusCoreState extends State { } else { showMaizebusOKDialog( contextIn: context, - title: const Text('Error'), - content: const Text('Couldn\'t load stop.'), + title: "Error", + content: 'Couldn\'t load stop.', ); } } diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index 2fec4a9..81b9aa1 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -414,8 +414,8 @@ class SheetNavigationManager { } else { showMaizebusOKDialog( contextIn: context, - title: const Text("Error"), - content: const Text("Couldn't load stop."), + title: "Error", + content: "Couldn't load stop.", ); } }, diff --git a/lib/widgets/bus_sheet.dart b/lib/widgets/bus_sheet.dart index 3adbd29..626022d 100644 --- a/lib/widgets/bus_sheet.dart +++ b/lib/widgets/bus_sheet.dart @@ -52,8 +52,8 @@ class _BusSheetState extends State { showMaizebusOKDialog( contextIn: context, - title: const Text("Uh Oh!"), - content: const Text("Unable to fetch bus data. Please check your internet connection and try again."), + title: "Uh Oh!", + content: "Unable to fetch bus data. Please check your internet connection and try again.", ); }); } else { From 26e8f818c6c3122c97abaca6b3629142d41ac0c7 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:15:24 -0400 Subject: [PATCH 11/15] Removed extra comment --- lib/main.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 4d3bb6c..4d99735 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,8 +9,6 @@ import 'services/bus_repository.dart'; import 'providers/bus_provider.dart'; import 'providers/theme_provider.dart'; -// TODO: Add descriptive comments to everything - // This function initializes the Flutter app and runs the MainApp widget void main() async { WidgetsFlutterBinding.ensureInitialized(); From a99be291cfb62ea9ba1f2eede21a5696145af322 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:25:40 -0400 Subject: [PATCH 12/15] Small cleanups --- lib/screens/map_screen.dart | 8 -------- lib/widgets/stop_sheet.dart | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 53e1cce..268361a 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1913,14 +1913,6 @@ class _MaizeBusCoreState extends State { // If showing a persistent bottom sheet, close it. // Fix android back button for buildings sheet and journey sheet (doesn't work without this) - // if (_bottomSheetController != null) { - // _bottomSheetController!.close(); - // _bottomSheetController = null; - // _removeSearchLocationMarker(); - // } - if (sheetNavigationManager!.isBottomSheetControllerAlive()) { - - } }, child: Stack( children: [ diff --git a/lib/widgets/stop_sheet.dart b/lib/widgets/stop_sheet.dart index 966c2b6..b6f64b5 100644 --- a/lib/widgets/stop_sheet.dart +++ b/lib/widgets/stop_sheet.dart @@ -25,7 +25,7 @@ class StopSheet extends StatefulWidget { final BusProvider busProvider; final ScrollController? scrollController; // Scroll controller of the DraggableScrollableSheet (the parent Widget) - const StopSheet({super.key, + const StopSheet({ required this.stopID, required this.stopName, required this.onFavorite, From 6fdab87dfe184ee5c7b3d91409cf529a58217452 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:08:42 -0400 Subject: [PATCH 13/15] Fixed animation for Android back button --- lib/services/sheet_navigation_manager.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart index 81b9aa1..a6f9f6b 100644 --- a/lib/services/sheet_navigation_manager.dart +++ b/lib/services/sheet_navigation_manager.dart @@ -131,7 +131,7 @@ class SheetNavigatorState extends State { boxShadow: [SheetBoxShadow] // Shadow that sits behind all the sheets in the SheetNavigator ), child: AnimatedSwitcher( - duration: Duration(milliseconds: 400), + duration: Duration(milliseconds: 300), transitionBuilder: (Widget child, Animation animation) { final isEntering = child.key == ValueKey(_stack.length); @@ -144,7 +144,7 @@ class SheetNavigatorState extends State { ) : isGoingBackwards ? ( isEntering ? - const Offset(-1, 0.0) : const Offset(1, 0.0) + const Offset(0, 0.0) : const Offset(1, 0.0) ) : isEntering ? const Offset(1, 0.0) : const Offset(0, 0.0), From 27f6dbc94529d2f740d48f44ca956ba761f1ad36 Mon Sep 17 00:00:00 2001 From: Static Date: Sat, 11 Apr 2026 15:05:00 -0400 Subject: [PATCH 14/15] fixed merge issue --- lib/services/bus_info_service.dart | 15 ++++++++------- lib/widgets/mini_stop_sheet.dart | 4 ++-- lib/widgets/stop_sheet.dart | 23 ++++++++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/services/bus_info_service.dart b/lib/services/bus_info_service.dart index bc53017..e6840f7 100644 --- a/lib/services/bus_info_service.dart +++ b/lib/services/bus_info_service.dart @@ -32,12 +32,7 @@ Future> fetchNextBusStops(String busID) async { } // for bus stops -Future<(List, bool)> fetchStopData(String stopID) async { - - final prefs = await SharedPreferences.getInstance(); - final list = prefs.getStringList('favorite_stops') ?? []; - bool toReturn = list.contains(stopID); - +Future> fetchStopData(String stopID) async { Uri url; if (int.tryParse(stopID) != null) { @@ -53,8 +48,14 @@ Future<(List, bool)> fetchStopData(String stopID) async { if (response.statusCode == 200) { final Map data = json.decode(response.body); final List predictions = data['bustime-response']['prd']; - return (predictions.map((json) => BusWithPrediction.fromJson(json)).toList(), toReturn); + return predictions.map((json) => BusWithPrediction.fromJson(json)).toList(); } else { throw Exception('Failed to load bus stops'); } } + +Future stopIsFavorited(String stopID) async { + final prefs = await SharedPreferences.getInstance(); + final list = prefs.getStringList('favorite_stops') ?? []; + return list.contains(stopID); +} \ No newline at end of file diff --git a/lib/widgets/mini_stop_sheet.dart b/lib/widgets/mini_stop_sheet.dart index 6bdb7b0..6a64204 100644 --- a/lib/widgets/mini_stop_sheet.dart +++ b/lib/widgets/mini_stop_sheet.dart @@ -40,7 +40,7 @@ class MiniStopSheet extends StatefulWidget { } class _MiniStopSheetState extends State { - late Future<(List, bool)> loadedStopData; + late Future> loadedStopData; @override void initState() { @@ -64,7 +64,7 @@ class _MiniStopSheetState extends State { List arrivingBuses = []; if (snapshot.hasData){ - arrivingBuses = snapshot.data!.$1; + arrivingBuses = snapshot.data!; } if (snapshot.hasData) { diff --git a/lib/widgets/stop_sheet.dart b/lib/widgets/stop_sheet.dart index b6f64b5..8c8927b 100644 --- a/lib/widgets/stop_sheet.dart +++ b/lib/widgets/stop_sheet.dart @@ -220,8 +220,8 @@ class ExpandableStopWidget extends StatefulWidget { } class _StopSheetState extends State with WidgetsBindingObserver { - late Future<(List, bool)> loadedStopData; - bool? _isFavorited; + late Future> loadedStopData; + bool _isFavorited = false; Timer? _refreshTimer; bool _isInBackground = false; @@ -266,6 +266,9 @@ class _StopSheetState extends State with WidgetsBindingObserver { // Start auto-refresh every 30 seconds _startRefreshTimer(); + + // check and update _isFavorited with status of isFavorited or not + _checkIsFavorited(); } void _startRefreshTimer() { @@ -281,6 +284,15 @@ class _StopSheetState extends State with WidgetsBindingObserver { _refreshTimer = null; } + void _checkIsFavorited() async { + // check if stop is favorited + if (await stopIsFavorited(widget.stopID)) { + setState(() { + _isFavorited = true; + }); + } + } + @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); @@ -340,13 +352,10 @@ class _StopSheetState extends State with WidgetsBindingObserver { List arrivingBuses = []; if (snapshot.hasData) { - arrivingBuses = snapshot.data!.$1; + arrivingBuses = snapshot.data!; arrivingBuses.sort( (lhs, rhs) => (int.tryParse(lhs.prediction) ?? 0).compareTo(int.tryParse(rhs.prediction) ?? 0) ); - if (_isFavorited == null) { - _isFavorited = snapshot.data!.$2; - } } // we know image dimensions, so we can use the width to find the height @@ -710,7 +719,7 @@ class _StopSheetState extends State with WidgetsBindingObserver { ElevatedButton( onPressed: () { // Read the current state - final bool currentStatus = _isFavorited ?? false; + final bool currentStatus = _isFavorited; // Call the appropriate function if (currentStatus){ From 91dfccd987f769c693353da53b682be8fed89808 Mon Sep 17 00:00:00 2001 From: iswheeler <22535264+iswheeler@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:13:26 -0400 Subject: [PATCH 15/15] Fixed merge with Matthew's code! Merge conflict fixed! --- lib/widgets/stop_sheet.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/widgets/stop_sheet.dart b/lib/widgets/stop_sheet.dart index ab71b28..95f56fa 100644 --- a/lib/widgets/stop_sheet.dart +++ b/lib/widgets/stop_sheet.dart @@ -18,7 +18,6 @@ import 'upcoming_stops_widget.dart'; class StopSheet extends StatefulWidget { final String stopID; final String stopName; - final bool isFavorite; final Future Function(String, String) onFavorite; final Future Function(String, String) onUnFavorite; final void Function() onGetDirections; @@ -29,7 +28,6 @@ class StopSheet extends StatefulWidget { const StopSheet({ required this.stopID, required this.stopName, - required this.isFavorite, required this.onFavorite, required this.onUnFavorite, required this.onGetDirections, @@ -236,7 +234,7 @@ class _StopSheetState extends State with WidgetsBindingObserver { super.initState(); WidgetsBinding.instance.addObserver(this); loadedStopData = fetchStopData(widget.stopID); - _isFavorite = widget.isFavorite; + _checkIsFavorited(); imageBusStop = (widget.stopID == "C250") || (widget.stopID == "N406") ||