dzocesrce

Untitled

Apr 8th, 2026
61
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 7.81 KB | None | 0 0
  1. import 'package:flutter/material.dart';
  2. import 'package:geolocator/geolocator.dart';
  3. import 'package:provider/provider.dart';
  4.  
  5. import '../../core/utils/app_error_messages.dart';
  6. import '../../models/bus_line_model.dart';
  7. import '../../models/location_model.dart';
  8. import '../../providers/location_provider.dart';
  9. import '../../providers/map_provider.dart';
  10. import '../../providers/stops_provider.dart';
  11. import '../../widgets/common/app_error_state.dart';
  12. import '../../widgets/common/location_access_prompt.dart';
  13. import 'stop_arrivals_screen.dart';
  14. import 'widgets/stop_row_card.dart';
  15. import 'widgets/stops_direction_toggle.dart';
  16. import 'widgets/stops_search_bar.dart';
  17.  
  18. class StopsScreen extends StatefulWidget {
  19. const StopsScreen({super.key});
  20.  
  21. @override
  22. State<StopsScreen> createState() => _StopsScreenState();
  23. }
  24.  
  25. class _StopsScreenState extends State<StopsScreen> {
  26. late final TextEditingController _searchController;
  27.  
  28. @override
  29. void initState() {
  30. super.initState();
  31. _searchController = TextEditingController();
  32. }
  33.  
  34. @override
  35. void dispose() {
  36. _searchController.dispose();
  37. super.dispose();
  38. }
  39.  
  40. List<LocationModel> _stopsForDirection(
  41. MapProvider mapProvider,
  42. String direction,
  43. ) {
  44. final routeStopIds = mapProvider.lineRoutes
  45. .where((route) => route.direction == direction && route.isActive)
  46. .expand((route) => route.stopIdsOrdered)
  47. .toSet();
  48.  
  49. return mapProvider.locations.where((location) {
  50. return location.type == 'bus_stop' && routeStopIds.contains(location.id);
  51. }).toList();
  52. }
  53.  
  54. List<BusLineModel> _linesForStop(
  55. MapProvider mapProvider,
  56. String stopId,
  57. String direction,
  58. ) {
  59. final lineIds = mapProvider.lineRoutes
  60. .where(
  61. (route) =>
  62. route.direction == direction &&
  63. route.isActive &&
  64. route.stopIdsOrdered.contains(stopId),
  65. )
  66. .map((route) => route.lineId)
  67. .toSet();
  68.  
  69. final lines = mapProvider.busLines
  70. .where((line) => lineIds.contains(line.id))
  71. .toList()
  72. ..sort((a, b) => a.number.compareTo(b.number));
  73.  
  74. return lines;
  75. }
  76.  
  77. List<LocationModel> _nearestStops(
  78. List<LocationModel> stops,
  79. Position position,
  80. ) {
  81. final sorted = [...stops];
  82.  
  83. sorted.sort((a, b) {
  84. final distanceA = Geolocator.distanceBetween(
  85. position.latitude,
  86. position.longitude,
  87. a.latitude,
  88. a.longitude,
  89. );
  90. final distanceB = Geolocator.distanceBetween(
  91. position.latitude,
  92. position.longitude,
  93. b.latitude,
  94. b.longitude,
  95. );
  96. return distanceA.compareTo(distanceB);
  97. });
  98.  
  99. return sorted.take(5).toList();
  100. }
  101.  
  102. List<LocationModel> _searchStops(
  103. List<LocationModel> stops,
  104. String query,
  105. ) {
  106. final normalized = query.trim().toLowerCase();
  107.  
  108. final filtered = stops.where((stop) {
  109. return stop.name.toLowerCase().contains(normalized);
  110. }).toList()
  111. ..sort((a, b) => a.name.compareTo(b.name));
  112.  
  113. return filtered;
  114. }
  115.  
  116. @override
  117. Widget build(BuildContext context) {
  118. final mapProvider = context.watch<MapProvider>();
  119. final locationProvider = context.watch<LocationProvider>();
  120. final stopsProvider = context.watch<StopsProvider>();
  121.  
  122. if (mapProvider.isLoading && !mapProvider.hasData) {
  123. return const Center(
  124. child: CircularProgressIndicator(),
  125. );
  126. }
  127.  
  128. if (mapProvider.errorMessage != null && !mapProvider.hasData) {
  129. return AppErrorState(
  130. message: mapProvider.errorMessage!,
  131. imageAssetPath: AppErrorMessages.imageForType(mapProvider.errorType),
  132. onRetry: mapProvider.loadMapData,
  133. );
  134. }
  135.  
  136. if (_searchController.text != stopsProvider.searchQuery) {
  137. _searchController.value = TextEditingValue(
  138. text: stopsProvider.searchQuery,
  139. selection: TextSelection.collapsed(
  140. offset: stopsProvider.searchQuery.length,
  141. ),
  142. );
  143. }
  144.  
  145. final direction = stopsProvider.selectedDirection;
  146. final allStopsForDirection = _stopsForDirection(mapProvider, direction);
  147. final hasSearch = stopsProvider.searchQuery.trim().isNotEmpty;
  148.  
  149. List<LocationModel> visibleStops = [];
  150.  
  151. if (hasSearch) {
  152. visibleStops = _searchStops(
  153. allStopsForDirection,
  154. stopsProvider.searchQuery,
  155. );
  156. } else if (locationProvider.currentPosition != null &&
  157. locationProvider.isGranted) {
  158. visibleStops = _nearestStops(
  159. allStopsForDirection,
  160. locationProvider.currentPosition!,
  161. );
  162. }
  163.  
  164. return Column(
  165. children: [
  166. Expanded(
  167. child: Padding(
  168. padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
  169. child: Column(
  170. children: [
  171. StopsSearchBar(
  172. controller: _searchController,
  173. onChanged: stopsProvider.updateSearchQuery,
  174. ),
  175. const SizedBox(height: 16),
  176. Expanded(
  177. child: Builder(
  178. builder: (context) {
  179. if (!hasSearch && !locationProvider.isGranted) {
  180. return LocationAccessPrompt(
  181. permissionState: locationProvider.permissionState,
  182. onRequestPermission: locationProvider.requestPermission,
  183. onOpenAppSettings:
  184. locationProvider.openAppSettingsPage,
  185. onOpenLocationSettings:
  186. locationProvider.openLocationSettingsPage,
  187. variant: LocationAccessPromptVariant.fullPage,
  188. imageAssetPath:
  189. 'assets/images/error/mende_no_internet.png',
  190. );
  191. }
  192.  
  193. if (!hasSearch &&
  194. locationProvider.isGranted &&
  195. locationProvider.currentPosition == null &&
  196. locationProvider.isLoading) {
  197. return const Center(
  198. child: CircularProgressIndicator(),
  199. );
  200. }
  201.  
  202. return ListView.separated(
  203. itemCount: visibleStops.length,
  204. separatorBuilder: (_, __) => const Divider(height: 1),
  205. itemBuilder: (context, index) {
  206. final stop = visibleStops[index];
  207. final lines = _linesForStop(
  208. mapProvider,
  209. stop.id,
  210. direction,
  211. );
  212.  
  213. return StopRowCard(
  214. stop: stop,
  215. lines: lines,
  216. showNearby: !hasSearch,
  217. onTap: () {
  218. Navigator.of(context).push(
  219. MaterialPageRoute(
  220. builder: (_) => StopArrivalsScreen(
  221. stop: stop,
  222. direction: direction,
  223. ),
  224. ),
  225. );
  226. },
  227. );
  228. },
  229. );
  230. },
  231. ),
  232. ),
  233. ],
  234. ),
  235. ),
  236. ),
  237. StopsDirectionToggle(
  238. selectedDirection: stopsProvider.selectedDirection,
  239. onDirectionSelected: stopsProvider.selectDirection,
  240. ),
  241. ],
  242. );
  243. }
  244. }
Advertisement
Add Comment
Please, Sign In to add comment