dzocesrce

Untitled

Apr 7th, 2026
84
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.07 KB | None | 0 0
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:google_maps_flutter/google_maps_flutter.dart';
  4. import 'package:provider/provider.dart';
  5.  
  6. import '../../app/theme/theme_provider.dart';
  7. import '../../core/utils/app_error_messages.dart';
  8. import '../../models/location_model.dart';
  9. import '../../providers/location_provider.dart';
  10. import '../../providers/map_provider.dart';
  11. import '../../widgets/common/app_error_state.dart';
  12. import 'widgets/location_permission_banner.dart';
  13. import 'widgets/map_filters_button.dart';
  14. import 'widgets/map_filters_sheet.dart';
  15. import 'widgets/map_legend_button.dart';
  16. import 'widgets/map_recenter_button.dart';
  17. import 'widgets/place_details_sheet.dart';
  18.  
  19. class MapScreen extends StatefulWidget {
  20. const MapScreen({super.key});
  21.  
  22. @override
  23. State<MapScreen> createState() => _MapScreenState();
  24. }
  25.  
  26. class _MapScreenState extends State<MapScreen> {
  27. GoogleMapController? _mapController;
  28.  
  29. String? _darkMapStyle;
  30. String? _lightMapStyle;
  31.  
  32. BitmapDescriptor? _busStopMarker;
  33. BitmapDescriptor? _ticketOfficeMarker;
  34. BitmapDescriptor? _attractionMarker;
  35. BitmapDescriptor? _cafeMarker;
  36.  
  37. bool _didLoadAssets = false;
  38. bool? _lastAppliedIsDarkMode;
  39.  
  40. static const CameraPosition _initialCameraPosition = CameraPosition(
  41. target: LatLng(41.3451, 21.5550),
  42. zoom: 13.8,
  43. );
  44.  
  45. @override
  46. void initState() {
  47. super.initState();
  48. _loadMapStyles();
  49. }
  50.  
  51. @override
  52. void didChangeDependencies() {
  53. super.didChangeDependencies();
  54.  
  55. if (!_didLoadAssets) {
  56. _didLoadAssets = true;
  57. _loadMarkerIcons();
  58. }
  59. }
  60.  
  61. Future<void> _loadMapStyles() async {
  62. final darkStyle = await rootBundle.loadString(
  63. 'assets/map_styles/dark_map_style.json',
  64. );
  65.  
  66. final lightStyle = await rootBundle.loadString(
  67. 'assets/map_styles/light_map_style.json',
  68. );
  69.  
  70. if (!mounted) return;
  71.  
  72. setState(() {
  73. _darkMapStyle = darkStyle;
  74. _lightMapStyle = lightStyle;
  75. });
  76. }
  77.  
  78. Future<void> _loadMarkerIcons() async {
  79. final imageConfiguration = createLocalImageConfiguration(context);
  80.  
  81. final busStop = await BitmapDescriptor.asset(
  82. imageConfiguration,
  83. 'assets/icons/monkey_stop.png',
  84. width: 28,
  85. height: 28,
  86. );
  87.  
  88. final ticketOffice = await BitmapDescriptor.asset(
  89. imageConfiguration,
  90. 'assets/icons/ticket_office.png',
  91. width: 24,
  92. height: 24,
  93. );
  94.  
  95. final attraction = await BitmapDescriptor.asset(
  96. imageConfiguration,
  97. 'assets/icons/attraction.png',
  98. width: 24,
  99. height: 24,
  100. );
  101.  
  102. final cafe = await BitmapDescriptor.asset(
  103. imageConfiguration,
  104. 'assets/icons/cafe.png',
  105. width: 24,
  106. height: 24,
  107. );
  108.  
  109. if (!mounted) return;
  110.  
  111. setState(() {
  112. _busStopMarker = busStop;
  113. _ticketOfficeMarker = ticketOffice;
  114. _attractionMarker = attraction;
  115. _cafeMarker = cafe;
  116. });
  117. }
  118.  
  119. Future<void> _applyMapStyle(bool isDarkMode) async {
  120. if (_mapController == null) return;
  121.  
  122. final styleToApply = isDarkMode ? _darkMapStyle : _lightMapStyle;
  123. if (styleToApply == null) return;
  124.  
  125. await _mapController!.setMapStyle(styleToApply);
  126. _lastAppliedIsDarkMode = isDarkMode;
  127. }
  128.  
  129. Future<void> _recenterToUser(LocationProvider locationProvider) async {
  130. if (!locationProvider.isGranted) {
  131. await locationProvider.requestPermission();
  132. return;
  133. }
  134.  
  135. if (locationProvider.currentPosition == null) {
  136. await locationProvider.getCurrentLocation();
  137. }
  138.  
  139. final position = locationProvider.currentPosition;
  140. if (position == null || _mapController == null) return;
  141.  
  142. await _mapController!.animateCamera(
  143. CameraUpdate.newCameraPosition(
  144. CameraPosition(
  145. target: LatLng(position.latitude, position.longitude),
  146. zoom: 15.5,
  147. ),
  148. ),
  149. );
  150. }
  151.  
  152. void _showLegendDialog() {
  153. showDialog(
  154. context: context,
  155. builder: (context) {
  156. return const AlertDialog(
  157. title: Text('Map Legend'),
  158. content: Column(
  159. mainAxisSize: MainAxisSize.min,
  160. crossAxisAlignment: CrossAxisAlignment.start,
  161. children: [
  162. _LegendImageRow(
  163. imagePath: 'assets/icons/monkey_stop.png',
  164. label: 'Bus stop',
  165. ),
  166. SizedBox(height: 12),
  167. _LegendImageRow(
  168. imagePath: 'assets/icons/ticket_office.png',
  169. label: 'Ticket office',
  170. ),
  171. SizedBox(height: 12),
  172. _LegendImageRow(
  173. imagePath: 'assets/icons/attraction.png',
  174. label: 'Attraction',
  175. ),
  176. SizedBox(height: 12),
  177. _LegendImageRow(
  178. imagePath: 'assets/icons/cafe.png',
  179. label: 'Cafe',
  180. ),
  181. ],
  182. ),
  183. );
  184. },
  185. );
  186. }
  187.  
  188. void _showFiltersSheet() {
  189. showModalBottomSheet(
  190. context: context,
  191. showDragHandle: false,
  192. isScrollControlled: true,
  193. builder: (_) => const MapFiltersSheet(),
  194. );
  195. }
  196.  
  197. void _showPlaceDetails(LocationModel location) {
  198. showModalBottomSheet(
  199. context: context,
  200. isScrollControlled: true,
  201. showDragHandle: false,
  202. builder: (_) => PlaceDetailsSheet(location: location),
  203. );
  204. }
  205.  
  206. Color _hexToColor(String hex) {
  207. final cleanedHex = hex.replaceAll('#', '');
  208. return Color(int.parse('FF$cleanedHex', radix: 16));
  209. }
  210.  
  211. Set<Polyline> _buildPolylines(MapProvider mapProvider) {
  212. return mapProvider.selectedRoutes.map((route) {
  213. final line = mapProvider.busLines.firstWhere(
  214. (line) => line.id == route.lineId,
  215. );
  216.  
  217. return Polyline(
  218. polylineId: PolylineId(route.id),
  219. width: 5,
  220. color: _hexToColor(line.colorHex),
  221. points: route.polylinePoints
  222. .where((point) {
  223. final lat = point['latitude'];
  224. final lng = point['longitude'];
  225.  
  226. if (lat is! num || lng is! num) return false;
  227.  
  228. return lat >= 41.0 &&
  229. lat <= 41.5 &&
  230. lng >= 21.2 &&
  231. lng <= 21.8;
  232. })
  233. .map(
  234. (point) => LatLng(
  235. (point['latitude'] as num).toDouble(),
  236. (point['longitude'] as num).toDouble(),
  237. ),
  238. )
  239. .toList(),
  240. );
  241. }).toSet();
  242. }
  243.  
  244. BitmapDescriptor _markerIconForType(String type) {
  245. switch (type) {
  246. case 'bus_stop':
  247. return _busStopMarker ?? BitmapDescriptor.defaultMarker;
  248. case 'ticket_office':
  249. return _ticketOfficeMarker ?? BitmapDescriptor.defaultMarker;
  250. case 'landmark':
  251. return _attractionMarker ?? BitmapDescriptor.defaultMarker;
  252. case 'cafe':
  253. return _cafeMarker ?? BitmapDescriptor.defaultMarker;
  254. default:
  255. return BitmapDescriptor.defaultMarker;
  256. }
  257. }
  258.  
  259. Set<Marker> _buildMarkers(MapProvider mapProvider) {
  260. final visibleBusStops = mapProvider.selectedRouteStops;
  261. final otherLocations = mapProvider.locations
  262. .where((location) => location.type != 'bus_stop')
  263. .toList();
  264.  
  265. final allVisibleLocations = [
  266. ...visibleBusStops,
  267. ...otherLocations,
  268. ];
  269.  
  270. return allVisibleLocations.map((location) {
  271. final lineNumbers = mapProvider.getLineNumbersForStop(location.id);
  272.  
  273. return Marker(
  274. markerId: MarkerId(location.id),
  275. position: LatLng(location.latitude, location.longitude),
  276. icon: _markerIconForType(location.type),
  277. infoWindow: InfoWindow(
  278. title: location.name,
  279. snippet: location.type == 'bus_stop'
  280. ? 'Lines: ${lineNumbers.join(', ')}'
  281. : mapProvider.getShortDescription(location.description),
  282. ),
  283. onTap: () {
  284. if (location.type == 'landmark' || location.type == 'cafe') {
  285. _showPlaceDetails(location);
  286. }
  287. },
  288. );
  289. }).toSet();
  290. }
  291.  
  292. @override
  293. Widget build(BuildContext context) {
  294. final locationProvider = context.watch<LocationProvider>();
  295. final mapProvider = context.watch<MapProvider>();
  296. final isDarkMode = context.watch<ThemeProvider>().isDarkMode;
  297.  
  298. if (_mapController != null && _lastAppliedIsDarkMode != isDarkMode) {
  299. WidgetsBinding.instance.addPostFrameCallback((_) {
  300. _applyMapStyle(isDarkMode);
  301. });
  302. }
  303.  
  304. if (mapProvider.isLoading && !mapProvider.hasData) {
  305. return const Center(
  306. child: CircularProgressIndicator(),
  307. );
  308. }
  309.  
  310. if (mapProvider.errorMessage != null && !mapProvider.hasData) {
  311. return AppErrorState(
  312. message: mapProvider.errorMessage!,
  313. imageAssetPath: AppErrorMessages.imageForType(mapProvider.errorType),
  314. onRetry: mapProvider.loadMapData,
  315. );
  316. }
  317.  
  318. return Stack(
  319. children: [
  320. GoogleMap(
  321. initialCameraPosition: _initialCameraPosition,
  322. myLocationEnabled: locationProvider.isGranted,
  323. myLocationButtonEnabled: false,
  324. zoomControlsEnabled: false,
  325. compassEnabled: true,
  326. mapToolbarEnabled: false,
  327. polylines: _buildPolylines(mapProvider),
  328. markers: _buildMarkers(mapProvider),
  329. onMapCreated: (controller) async {
  330. _mapController = controller;
  331. await _applyMapStyle(isDarkMode);
  332. },
  333. ),
  334. LocationPermissionBanner(
  335. permissionState: locationProvider.permissionState,
  336. onRequestPermission: locationProvider.requestPermission,
  337. onOpenSettings: locationProvider.openAppSettingsPage,
  338. onOpenLocationSettings: locationProvider.openLocationSettingsPage,
  339. ),
  340. Positioned(
  341. right: 16,
  342. top: MediaQuery.of(context).size.height * 0.34,
  343. child: Column(
  344. children: [
  345. MapRecenterButton(
  346. onPressed: () => _recenterToUser(locationProvider),
  347. ),
  348. const SizedBox(height: 12),
  349. MapLegendButton(
  350. onPressed: _showLegendDialog,
  351. ),
  352. ],
  353. ),
  354. ),
  355. Positioned(
  356. left: 0,
  357. right: 0,
  358. bottom: 36,
  359. child: MapFiltersButton(
  360. onPressed: _showFiltersSheet,
  361. ),
  362. ),
  363. ],
  364. );
  365. }
  366. }
  367.  
  368. class _LegendImageRow extends StatelessWidget {
  369. final String imagePath;
  370. final String label;
  371.  
  372. const _LegendImageRow({
  373. required this.imagePath,
  374. required this.label,
  375. });
  376.  
  377. @override
  378. Widget build(BuildContext context) {
  379. return Row(
  380. children: [
  381. Image.asset(
  382. imagePath,
  383. width: 22,
  384. height: 22,
  385. ),
  386. const SizedBox(width: 10),
  387. Text(label),
  388. ],
  389. );
  390. }
  391. }
  392.  
  393.  
  394. import 'package:flutter/material.dart';
  395. import 'package:provider/provider.dart';
  396.  
  397. import '../../core/utils/app_error_messages.dart';
  398. import '../../providers/notifications_provider.dart';
  399. import '../../widgets/common/app_error_state.dart';
  400. import 'widgets/notification_dialog.dart';
  401. import 'widgets/notification_row_card.dart';
  402.  
  403. class NotificationsScreen extends StatelessWidget {
  404. const NotificationsScreen({super.key});
  405.  
  406. @override
  407. Widget build(BuildContext context) {
  408. final provider = context.watch<NotificationsProvider>();
  409.  
  410. if (provider.isLoading && provider.notifications.isEmpty) {
  411. return const Center(
  412. child: CircularProgressIndicator(),
  413. );
  414. }
  415.  
  416. if (provider.errorMessage != null) {
  417. return AppErrorState(
  418. message: provider.errorMessage!,
  419. imageAssetPath: AppErrorMessages.imageForType(provider.errorType),
  420. onRetry: provider.loadNotifications,
  421. );
  422. }
  423.  
  424. if (provider.notifications.isEmpty) {
  425. return RefreshIndicator(
  426. onRefresh: provider.loadNotifications,
  427. child: ListView(
  428. physics: const AlwaysScrollableScrollPhysics(),
  429. children: const [
  430. SizedBox(height: 180),
  431. Center(
  432. child: Text('No notifications yet.'),
  433. ),
  434. ],
  435. ),
  436. );
  437. }
  438.  
  439. return RefreshIndicator(
  440. onRefresh: provider.loadNotifications,
  441. child: ListView.separated(
  442. physics: const AlwaysScrollableScrollPhysics(),
  443. padding: const EdgeInsets.all(16),
  444. itemCount: provider.notifications.length,
  445. separatorBuilder: (_, __) => const SizedBox(height: 10),
  446. itemBuilder: (context, index) {
  447. final notification = provider.notifications[index];
  448. final isRead = provider.isRead(notification.id);
  449. final preview = provider.getPreviewText(notification.message);
  450. final dateLabel = provider.formatNotificationDate(notification.publishedAt);
  451. final timeLabel = provider.formatNotificationTime(notification.publishedAt);
  452.  
  453. return NotificationRowCard(
  454. notification: notification,
  455. isRead: isRead,
  456. previewText: preview,
  457. dateLabel: dateLabel,
  458. timeLabel: timeLabel,
  459. onTap: () async {
  460. await provider.markAsRead(notification.id);
  461.  
  462. if (!context.mounted) return;
  463.  
  464. showDialog(
  465. context: context,
  466. builder: (_) => NotificationDialog(
  467. notification: notification,
  468. dateLabel: dateLabel,
  469. timeLabel: timeLabel,
  470. ),
  471. );
  472. },
  473. );
  474. },
  475. ),
  476. );
  477. }
  478. }
  479.  
  480.  
  481.  
  482. import 'package:flutter/material.dart';
  483. import 'package:provider/provider.dart';
  484.  
  485. import '../../core/utils/app_error_messages.dart';
  486. import '../../providers/info_provider.dart';
  487. import '../../widgets/common/app_error_state.dart';
  488. import 'widgets/info_page_indicator.dart';
  489. import 'widgets/info_slide_card.dart';
  490.  
  491. class InfoScreen extends StatefulWidget {
  492. const InfoScreen({super.key});
  493.  
  494. @override
  495. State<InfoScreen> createState() => _InfoScreenState();
  496. }
  497.  
  498. class _InfoScreenState extends State<InfoScreen> {
  499. final PageController _pageController = PageController();
  500. int _currentPage = 0;
  501.  
  502. @override
  503. void dispose() {
  504. _pageController.dispose();
  505. super.dispose();
  506. }
  507.  
  508. @override
  509. Widget build(BuildContext context) {
  510. final provider = context.watch<InfoProvider>();
  511. final slides = provider.slides;
  512.  
  513. if (provider.isLoading && slides.isEmpty) {
  514. return const Center(
  515. child: CircularProgressIndicator(),
  516. );
  517. }
  518.  
  519. if (provider.errorMessage != null) {
  520. return AppErrorState(
  521. message: provider.errorMessage!,
  522. imageAssetPath: AppErrorMessages.imageForType(provider.errorType),
  523. onRetry: provider.loadSlides,
  524. );
  525. }
  526.  
  527. if (slides.isEmpty) {
  528. return const Center(
  529. child: Text('No info slides available.'),
  530. );
  531. }
  532.  
  533. return Column(
  534. children: [
  535. Expanded(
  536. child: RefreshIndicator(
  537. onRefresh: provider.loadSlides,
  538. child: PageView.builder(
  539. controller: _pageController,
  540. itemCount: slides.length,
  541. onPageChanged: (index) {
  542. setState(() {
  543. _currentPage = index;
  544. });
  545. },
  546. itemBuilder: (context, index) {
  547. return ListView(
  548. physics: const AlwaysScrollableScrollPhysics(),
  549. children: [
  550. SizedBox(
  551. height: MediaQuery.of(context).size.height * 0.72,
  552. child: InfoSlideCard(
  553. slide: slides[index],
  554. ),
  555. ),
  556. ],
  557. );
  558. },
  559. ),
  560. ),
  561. ),
  562. Padding(
  563. padding: const EdgeInsets.only(bottom: 22),
  564. child: InfoPageIndicator(
  565. itemCount: slides.length,
  566. currentIndex: _currentPage,
  567. ),
  568. ),
  569. ],
  570. );
  571. }
  572. }
  573.  
  574.  
  575.  
  576. import 'package:flutter/material.dart';
  577. import 'package:geolocator/geolocator.dart';
  578. import 'package:provider/provider.dart';
  579.  
  580. import '../../core/utils/app_error_messages.dart';
  581. import '../../models/bus_line_model.dart';
  582. import '../../models/location_model.dart';
  583. import '../../providers/location_provider.dart';
  584. import '../../providers/map_provider.dart';
  585. import '../../providers/stops_provider.dart';
  586. import '../../widgets/common/app_error_state.dart';
  587. import '../../widgets/common/location_access_prompt.dart';
  588. import 'stop_arrivals_screen.dart';
  589. import 'widgets/stop_row_card.dart';
  590. import 'widgets/stops_direction_toggle.dart';
  591. import 'widgets/stops_search_bar.dart';
  592.  
  593. class StopsScreen extends StatefulWidget {
  594. const StopsScreen({super.key});
  595.  
  596. @override
  597. State<StopsScreen> createState() => _StopsScreenState();
  598. }
  599.  
  600. class _StopsScreenState extends State<StopsScreen> {
  601. late final TextEditingController _searchController;
  602.  
  603. @override
  604. void initState() {
  605. super.initState();
  606. _searchController = TextEditingController();
  607. }
  608.  
  609. @override
  610. void dispose() {
  611. _searchController.dispose();
  612. super.dispose();
  613. }
  614.  
  615. List<LocationModel> _stopsForDirection(
  616. MapProvider mapProvider,
  617. String direction,
  618. ) {
  619. final routeStopIds = mapProvider.lineRoutes
  620. .where((route) => route.direction == direction && route.isActive)
  621. .expand((route) => route.stopIdsOrdered)
  622. .toSet();
  623.  
  624. return mapProvider.locations.where((location) {
  625. return location.type == 'bus_stop' && routeStopIds.contains(location.id);
  626. }).toList();
  627. }
  628.  
  629. List<BusLineModel> _linesForStop(
  630. MapProvider mapProvider,
  631. String stopId,
  632. String direction,
  633. ) {
  634. final lineIds = mapProvider.lineRoutes
  635. .where(
  636. (route) =>
  637. route.direction == direction &&
  638. route.isActive &&
  639. route.stopIdsOrdered.contains(stopId),
  640. )
  641. .map((route) => route.lineId)
  642. .toSet();
  643.  
  644. final lines = mapProvider.busLines
  645. .where((line) => lineIds.contains(line.id))
  646. .toList()
  647. ..sort((a, b) => a.number.compareTo(b.number));
  648.  
  649. return lines;
  650. }
  651.  
  652. List<LocationModel> _nearestStops(
  653. List<LocationModel> stops,
  654. Position position,
  655. ) {
  656. final sorted = [...stops];
  657.  
  658. sorted.sort((a, b) {
  659. final distanceA = Geolocator.distanceBetween(
  660. position.latitude,
  661. position.longitude,
  662. a.latitude,
  663. a.longitude,
  664. );
  665. final distanceB = Geolocator.distanceBetween(
  666. position.latitude,
  667. position.longitude,
  668. b.latitude,
  669. b.longitude,
  670. );
  671. return distanceA.compareTo(distanceB);
  672. });
  673.  
  674. return sorted.take(5).toList();
  675. }
  676.  
  677. List<LocationModel> _searchStops(
  678. List<LocationModel> stops,
  679. String query,
  680. ) {
  681. final normalized = query.trim().toLowerCase();
  682.  
  683. final filtered = stops.where((stop) {
  684. return stop.name.toLowerCase().contains(normalized);
  685. }).toList()
  686. ..sort((a, b) => a.name.compareTo(b.name));
  687.  
  688. return filtered;
  689. }
  690.  
  691. @override
  692. Widget build(BuildContext context) {
  693. final mapProvider = context.watch<MapProvider>();
  694. final locationProvider = context.watch<LocationProvider>();
  695. final stopsProvider = context.watch<StopsProvider>();
  696.  
  697. if (_searchController.text != stopsProvider.searchQuery) {
  698. _searchController.value = TextEditingValue(
  699. text: stopsProvider.searchQuery,
  700. selection: TextSelection.collapsed(
  701. offset: stopsProvider.searchQuery.length,
  702. ),
  703. );
  704. }
  705.  
  706. final direction = stopsProvider.selectedDirection;
  707. final allStopsForDirection = _stopsForDirection(mapProvider, direction);
  708. final hasSearch = stopsProvider.searchQuery.trim().isNotEmpty;
  709.  
  710. List<LocationModel> visibleStops = [];
  711.  
  712. if (hasSearch) {
  713. visibleStops = _searchStops(
  714. allStopsForDirection,
  715. stopsProvider.searchQuery,
  716. );
  717. } else if (locationProvider.currentPosition != null &&
  718. locationProvider.isGranted) {
  719. visibleStops = _nearestStops(
  720. allStopsForDirection,
  721. locationProvider.currentPosition!,
  722. );
  723. }
  724.  
  725. return Column(
  726. children: [
  727. Expanded(
  728. child: Padding(
  729. padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
  730. child: Column(
  731. children: [
  732. StopsSearchBar(
  733. controller: _searchController,
  734. onChanged: stopsProvider.updateSearchQuery,
  735. ),
  736. const SizedBox(height: 16),
  737. Expanded(
  738. child: Builder(
  739. builder: (context) {
  740. if (mapProvider.errorMessage != null &&
  741. !mapProvider.hasData) {
  742. return AppErrorState(
  743. message: mapProvider.errorMessage!,
  744. imageAssetPath:
  745. AppErrorMessages.imageForType(mapProvider.errorType),
  746. onRetry: mapProvider.loadMapData,
  747. );
  748. }
  749.  
  750. if (!hasSearch && !locationProvider.isGranted) {
  751. return LocationAccessPrompt(
  752. permissionState: locationProvider.permissionState,
  753. onRequestPermission: locationProvider.requestPermission,
  754. onOpenAppSettings:
  755. locationProvider.openAppSettingsPage,
  756. onOpenLocationSettings:
  757. locationProvider.openLocationSettingsPage,
  758. variant: LocationAccessPromptVariant.fullPage,
  759. imageAssetPath:
  760. 'assets/images/error/mende_no_internet.png',
  761. );
  762. }
  763.  
  764. if (!hasSearch &&
  765. locationProvider.isGranted &&
  766. locationProvider.currentPosition == null &&
  767. locationProvider.isLoading) {
  768. return const Center(
  769. child: CircularProgressIndicator(),
  770. );
  771. }
  772.  
  773. return ListView.separated(
  774. itemCount: visibleStops.length,
  775. separatorBuilder: (_, __) => const Divider(height: 1),
  776. itemBuilder: (context, index) {
  777. final stop = visibleStops[index];
  778. final lines = _linesForStop(
  779. mapProvider,
  780. stop.id,
  781. direction,
  782. );
  783.  
  784. return StopRowCard(
  785. stop: stop,
  786. lines: lines,
  787. showNearby: !hasSearch,
  788. onTap: () {
  789. Navigator.of(context).push(
  790. MaterialPageRoute(
  791. builder: (_) => StopArrivalsScreen(
  792. stop: stop,
  793. direction: direction,
  794. ),
  795. ),
  796. );
  797. },
  798. );
  799. },
  800. );
  801. },
  802. ),
  803. ),
  804. ],
  805. ),
  806. ),
  807. ),
  808. StopsDirectionToggle(
  809. selectedDirection: stopsProvider.selectedDirection,
  810. onDirectionSelected: stopsProvider.selectDirection,
  811. ),
  812. ],
  813. );
  814. }
  815. }
Advertisement
Add Comment
Please, Sign In to add comment