diff --git a/lib/constants.dart b/lib/constants.dart index ba5bbf8..103cebe 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -422,6 +422,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( @@ -431,3 +432,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/screens/map_screen.dart b/lib/screens/map_screen.dart index 7104b98..ab9d79d 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'; @@ -158,7 +159,8 @@ class _MaizeBusCoreState extends State { int _routesFingerprint = 0; // store persistent bottom sheet controller - PersistentBottomSheetController? _bottomSheetController; + // PersistentBottomSheetController? _bottomSheetController; + SheetNavigationManager? sheetNavigationManager; // GoogleMaps styles String _darkMapStyle = "{}"; @@ -176,6 +178,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); @@ -816,7 +831,7 @@ class _MaizeBusCoreState extends State { Haptics.vibrate(HapticsType.light); } catch (e) {} - _showStopSheet( + sheetNavigationManager?.showStopSheetFromMap( stop.id, stop.name, stop.location.latitude, @@ -1012,7 +1027,7 @@ class _MaizeBusCoreState extends State { try { Haptics.vibrate(HapticsType.light); } catch (e) {} - _showBusSheet(bus.id); + sheetNavigationManager?.showBusSheetFromMap(bus.id); }, ); }) @@ -1046,7 +1061,7 @@ 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) ), ); } @@ -1175,242 +1190,13 @@ 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 - } - }, - ); - }, - ); - } - - 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, - ); - }, - ); - }, + onTap: () => sheetNavigationManager?.showBusSheetFromMap(bus.id) ); } - _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; @@ -1846,111 +1632,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: "Error", - content: "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: 'Error', - content: '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, - isFavorite: _favoriteStops.contains(stopID), - 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 @@ -2065,6 +1747,144 @@ 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?.showStopSheetFromMap( + stopID, + location.name, + searchCoordinates.latitude, + searchCoordinates.longitude, + ); + } else { + _centerOnLocation( + false, + searchCoordinates.latitude, + searchCoordinates.longitude, + ); + _showSearchLocationMarker( + location.latlng!.latitude, + location.latlng!.longitude + ); + sheetNavigationManager?.showBuildingSheet(location); + } + } else { + // Location has no coordinates + } + } + + void onSelectStop(name, id) { + LatLng? latLong = getLatLongFromStopID(id); + if (latLong != null) { + sheetNavigationManager?.showStopSheetFromMap(id, name, latLong.latitude, latLong.longitude); + } else { + showMaizebusOKDialog( + contextIn: context, + title: "Error", + content: '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 @@ -2138,11 +1958,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(); - } }, child: Stack( children: [ @@ -2635,7 +2450,9 @@ class _MaizeBusCoreState extends State { ), ), child: ElevatedButton.icon( - onPressed: _showJourneySheetOnReopen, + onPressed: () { + sheetNavigationManager?.showJourneySheetOnReopen(currDisplayed); + }, style: ElevatedButton.styleFrom( backgroundColor: getColor( context, @@ -2756,8 +2573,11 @@ class _MaizeBusCoreState extends State { HapticsType.light, ); } - _showBusRoutesModal( + sheetNavigationManager?.showBusRoutesModal( busProvider.routes, + _availableRoutes, + _selectedRoutes, + canVibrate ); }, heroTag: 'routes_fab', @@ -2806,7 +2626,7 @@ class _MaizeBusCoreState extends State { HapticsType.light, ); } - _showFavoritesSheet(); + sheetNavigationManager?.showFavoritesSheet(); }, heroTag: 'favorites_fab', elevation: 0, @@ -2853,7 +2673,7 @@ class _MaizeBusCoreState extends State { HapticsType.light, ); } - _showSearchSheet(); + sheetNavigationManager?.showSearchSheet(); }, style: ElevatedButton.styleFrom( alignment: Alignment.centerLeft, diff --git a/lib/services/bus_info_service.dart b/lib/services/bus_info_service.dart index 30c1d87..e6840f7 100644 --- a/lib/services/bus_info_service.dart +++ b/lib/services/bus_info_service.dart @@ -52,4 +52,10 @@ Future> fetchStopData(String stopID) async { } 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/services/sheet_navigation_manager.dart b/lib/services/sheet_navigation_manager.dart new file mode 100644 index 0000000..a6f9f6b --- /dev/null +++ b/lib/services/sheet_navigation_manager.dart @@ -0,0 +1,521 @@ +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'; + +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; + + const SheetNavigationContext({required this.scrollController, 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; +} + +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 +class SheetNavigatorState extends State { + List _stack = []; + 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 inactiveScrollController = ScrollController(); // Given to the sheet in the background so it doesn't fight when closing the modal + + void pushWidget(Widget sheet) { + setState(() { + shouldAnimate = true; + _stack.add(sheet); + isGoingBackwards = false; + numSteps++; + }); + } + + void popWidget() { + setState(() { + _stack.removeLast(); + isGoingBackwards = true; + numSteps++; + }); + } + + void popWidgetNoAnimation() { + setState(() { + shouldAnimate = false; + _stack.removeLast(); + isGoingBackwards = true; + numSteps++; + }); + + } + + @override + void initState() { + super.initState(); + _stack.add(widget.initialSheetBuilder(widget.scrollController, pushWidget)); + } + + @override + void dispose() { + inactiveScrollController.dispose(); + super.dispose(); + } + + @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) { + shouldAnimate = true; // User clicked back button so we need a back animation + popWidget(); + } + + }, child: Container( + decoration: BoxDecoration( + boxShadow: [SheetBoxShadow] // Shadow that sits behind all the sheets in the SheetNavigator + ), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 300), + 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(0, 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: [ + ...previousChildren, + if (currentChild != null) currentChild + ] + ); + }, + 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,)) + ), + ) + ) + ); + } +} + +// SheetNavigationManager is responsible for all the sheets that open up from the map screen. +// 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 { + 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, + 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) { + + _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, + ); + }, + ); + } + + BusSheet getBusSheet(String busID, ScrollController? scrollController, PushSheet pushNewSheet) { + return BusSheet( + busID: busID, + scrollController: scrollController, + onSelectStop: (name, id) { + // 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, null)); + } else { + showMaizebusOKDialog( + contextIn: context, + title: "Error", + content: "Couldn't load stop.", + ); + } + }, + ); + } + + void showBusSheetFromMap(String busID) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + backgroundColor: Colors.transparent, + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.85, + maxChildSize: 0.85, + snap: true, + builder: (BuildContext context, ScrollController scrollController) { + return Container( + child: SheetNavigator( + scrollController: scrollController, + initialSheetBuilder: (ScrollController scrollControllerLocal, PushSheet pushNewSheet) => getBusSheet(busID, null, pushNewSheet) + ) + ); + }, + ) + ); + } + + void showFavoritesSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return FavoritesSheet( + onSelectStop: onSelectStop, + onUnfavorite: onUnfavorite, + ); + }, + ); + } + + + StopSheet getStopSheet(String stopID, String stopName, double lat, double long, PushSheet pushSheet, ScrollController? scrollControllerLocal) { + final busProvider = Provider.of(context, listen: false); + + return StopSheet( + stopID: stopID, + stopName: stopName, + onFavorite: addFavoriteStop, + onUnFavorite: removeFavoriteStop, + scrollController: scrollControllerLocal, + showBusSheet: (busId) { + // When someone clicks "See all stops for this bus" this callback runs + pushSheet(getBusSheet(busId, null, pushSheet)); + }, + busProvider: busProvider, + onGetDirections: () { + Map? start; + Map? end = {'lat': lat, 'lon': long}; + + showDirectionsSheet( + start, + end, + "Current Location", + stopName, + false, + ); + }, + ); + } + + // 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) => DraggableScrollableSheet( + initialChildSize: 0.85, + maxChildSize: 0.85, + snap: true, + builder: (BuildContext context, ScrollController scrollController) { + return SheetNavigator( + scrollController: scrollController, + initialSheetBuilder: (ScrollController scrollControllerLocal, PushSheet pushSheet) { + return getStopSheet(stopID, stopName, lat, long, pushSheet, null); + } + ); + + } + + ) + ); + } + +} \ No newline at end of file diff --git a/lib/widgets/bus_sheet.dart b/lib/widgets/bus_sheet.dart index 1e664d8..626022d 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:bluebus/widgets/route_icon.dart'; import 'package:flutter/material.dart'; import 'package:bluebus/widgets/dialog.dart'; @@ -19,26 +20,27 @@ bool isNumber(String? s) { class BusSheet extends StatefulWidget { final String busID; - final ScrollController scrollController; + ScrollController? scrollController; final void Function(String name, String id) onSelectStop; - const BusSheet({ - Key? key, + BusSheet({ required this.busID, required this.onSelectStop, - required this.scrollController, - }) : super(key: key); + 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(); } + } class _BusSheetState extends State { late Bus? currBus = BusRepository.getBus(widget.busID); - late Future> futureBusStops; - + late Future> futureBusStops; + + @override void initState() { super.initState(); @@ -69,16 +71,16 @@ class _BusSheetState extends State { final bus = currBus!; - return Container( + return 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: [SheetBoxLeftShadow] + ), child: Padding( padding: const EdgeInsets.only( @@ -88,7 +90,7 @@ class _BusSheetState extends State { bottom: 0, ), child: SingleChildScrollView( - controller: widget.scrollController, + 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 5e331cd..95f56fa 100644 --- a/lib/widgets/stop_sheet.dart +++ b/lib/widgets/stop_sheet.dart @@ -3,6 +3,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:bluebus/widgets/route_icon.dart'; @@ -17,24 +18,24 @@ 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; final void Function(String) showBusSheet; final BusProvider busProvider; + final ScrollController? scrollController; // Scroll controller of the DraggableScrollableSheet (the parent Widget) - StopSheet({ - Key? key, + const StopSheet({ required this.stopID, required this.stopName, - required this.isFavorite, required this.onFavorite, required this.onUnFavorite, required this.onGetDirections, required this.showBusSheet, required this.busProvider, - }) : super(key: key); + 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() => _StopSheetState(); @@ -220,7 +221,7 @@ class ExpandableStopWidget extends StatefulWidget { class _StopSheetState extends State with WidgetsBindingObserver { late Future> loadedStopData; - late bool _isFavorite; + bool _isFavorited = false; Timer? _refreshTimer; bool _isInBackground = false; @@ -233,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") || @@ -266,6 +267,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 +285,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); @@ -323,505 +336,487 @@ class _StopSheetState extends State with WidgetsBindingObserver { @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 = []; - - if (snapshot.hasData) { - arrivingBuses = snapshot.data!; - arrivingBuses.sort( - (lhs, rhs) => (int.tryParse(lhs.prediction) ?? 0).compareTo(int.tryParse(rhs.prediction) ?? 0) - ); - } - - double initialSize = 0.9; - - if (snapshot.hasData) { - final itemCount = arrivingBuses.length; + FutureBuilder( + future: loadedStopData, + builder: (context, snapshot) { + List arrivingBuses = []; - // 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; + if (snapshot.hasData) { + arrivingBuses = snapshot.data!; + arrivingBuses.sort( + (lhs, rhs) => (int.tryParse(lhs.prediction) ?? 0).compareTo(int.tryParse(rhs.prediction) ?? 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; + // 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; // 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 Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: getColor(context, ColorType.background), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30), - ), - 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: 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), + // 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, ), - 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( - 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) - ), + ) + // 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, + // 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, - ), + ), + + // 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, - ), - ), - child: Text( - widget.stopID, - style: TextStyle( - color: Colors.black, - fontFamily: 'Urbanist', - fontWeight: - FontWeight.w700, - fontSize: 17, + ), + + 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, ), ), ), ), ), ), - ], - ), - ], - ), - ), - - 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 ), ], ), - ), + ], ), - - - // main page - Padding( + ), + + SizedBox(height: 20), + + // loading text and button + Material( + color: Colors.transparent, + child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 0, + horizontal: 20, ), - 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, + 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) + ? 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 ), - 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: 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, - ) - ) + ), + ], + ), + ) + + : + 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 + 20,) - ], - ), - ], - ) - : Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, + ); + + + }, + 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 + ], ), - 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 + ), + ], + ), + + + // 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), ), - 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, - ), - ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + elevation: 0 ), - - Spacer(), - - ElevatedButton( - onPressed: () { - // Call the appropriate function - if (_isFavorite){ - widget.onUnFavorite(widget.stopID, widget.stopName); - } else { - widget.onFavorite(widget.stopID, widget.stopName); - } - - // Update the UI immediately - setState(() { - _isFavorite = !_isFavorite; - }); - }, - 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 + 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, ), - child: Icon( - (_isFavorite ?? false)? Icons.favorite : Icons.favorite_border, - color: (_isFavorite ?? false)? Colors.red : getColor(context, ColorType.secondaryButtonText), - size: 20, - ), + ), + ), + + Spacer(), + + ElevatedButton( + onPressed: () { + // Read the current state + final bool currentStatus = _isFavorited; + + // 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 ), - - SizedBox(width: 10,), - - ElevatedButton( - onPressed: () { - if (arrivingBuses.isEmpty) { - return; - } + 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), - - + showDialog( + context: context, + builder: (context) { + return Dialog( - 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, - ) + 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, + ) + ), + ], ), - - SizedBox(height: paddingBelowButtons,) - ], - ), - ], - ), - ], - ), + ), + + SizedBox(height: paddingBelowButtons,) + ], + ), + ], + ), + ], ), - ); - }, - ); - }, - ), - ], + ), + ); + // }, + // ); + }, + ), + ], + ); } } diff --git a/lib/widgets/upcoming_stops_widget.dart b/lib/widgets/upcoming_stops_widget.dart index 55bbd47..d2061ac 100644 --- a/lib/widgets/upcoming_stops_widget.dart +++ b/lib/widgets/upcoming_stops_widget.dart @@ -436,9 +436,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; @@ -487,9 +485,6 @@ class _UpcomingStopsWidgetState extends State { } if (filter_check_passed) { - debugPrint( - " ${result[i].name} found after both conditions met, adding...", - ); results_filtered.add(result[i]); } } @@ -506,7 +501,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, @@ -537,50 +532,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), - - (stop.prediction != null) ? Text( - predictionText, - style: TextStyle( - fontSize: 15.0, - ) - ) : - SizedBox.shrink(), + SizedBox(width: 15), - (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(), + ], + ), + ) ); }