Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:google_maps_flutter/google_maps_flutter.dart';
- import 'package:provider/provider.dart';
- import '../../app/theme/theme_provider.dart';
- import '../../core/utils/app_error_messages.dart';
- import '../../models/location_model.dart';
- import '../../providers/location_provider.dart';
- import '../../providers/map_provider.dart';
- import '../../widgets/common/app_error_state.dart';
- import 'widgets/location_permission_banner.dart';
- import 'widgets/map_filters_button.dart';
- import 'widgets/map_filters_sheet.dart';
- import 'widgets/map_legend_button.dart';
- import 'widgets/map_recenter_button.dart';
- import 'widgets/place_details_sheet.dart';
- class MapScreen extends StatefulWidget {
- const MapScreen({super.key});
- @override
- State<MapScreen> createState() => _MapScreenState();
- }
- class _MapScreenState extends State<MapScreen> {
- GoogleMapController? _mapController;
- String? _darkMapStyle;
- String? _lightMapStyle;
- BitmapDescriptor? _busStopMarker;
- BitmapDescriptor? _ticketOfficeMarker;
- BitmapDescriptor? _attractionMarker;
- BitmapDescriptor? _cafeMarker;
- bool _didLoadAssets = false;
- bool? _lastAppliedIsDarkMode;
- static const CameraPosition _initialCameraPosition = CameraPosition(
- target: LatLng(41.3451, 21.5550),
- zoom: 13.8,
- );
- @override
- void initState() {
- super.initState();
- _loadMapStyles();
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- if (!_didLoadAssets) {
- _didLoadAssets = true;
- _loadMarkerIcons();
- }
- }
- Future<void> _loadMapStyles() async {
- final darkStyle = await rootBundle.loadString(
- 'assets/map_styles/dark_map_style.json',
- );
- final lightStyle = await rootBundle.loadString(
- 'assets/map_styles/light_map_style.json',
- );
- if (!mounted) return;
- setState(() {
- _darkMapStyle = darkStyle;
- _lightMapStyle = lightStyle;
- });
- }
- Future<void> _loadMarkerIcons() async {
- final imageConfiguration = createLocalImageConfiguration(context);
- final busStop = await BitmapDescriptor.asset(
- imageConfiguration,
- 'assets/icons/monkey_stop.png',
- width: 28,
- height: 28,
- );
- final ticketOffice = await BitmapDescriptor.asset(
- imageConfiguration,
- 'assets/icons/ticket_office.png',
- width: 24,
- height: 24,
- );
- final attraction = await BitmapDescriptor.asset(
- imageConfiguration,
- 'assets/icons/attraction.png',
- width: 24,
- height: 24,
- );
- final cafe = await BitmapDescriptor.asset(
- imageConfiguration,
- 'assets/icons/cafe.png',
- width: 24,
- height: 24,
- );
- if (!mounted) return;
- setState(() {
- _busStopMarker = busStop;
- _ticketOfficeMarker = ticketOffice;
- _attractionMarker = attraction;
- _cafeMarker = cafe;
- });
- }
- Future<void> _applyMapStyle(bool isDarkMode) async {
- if (_mapController == null) return;
- final styleToApply = isDarkMode ? _darkMapStyle : _lightMapStyle;
- if (styleToApply == null) return;
- await _mapController!.setMapStyle(styleToApply);
- _lastAppliedIsDarkMode = isDarkMode;
- }
- Future<void> _recenterToUser(LocationProvider locationProvider) async {
- if (!locationProvider.isGranted) {
- await locationProvider.requestPermission();
- return;
- }
- if (locationProvider.currentPosition == null) {
- await locationProvider.getCurrentLocation();
- }
- final position = locationProvider.currentPosition;
- if (position == null || _mapController == null) return;
- await _mapController!.animateCamera(
- CameraUpdate.newCameraPosition(
- CameraPosition(
- target: LatLng(position.latitude, position.longitude),
- zoom: 15.5,
- ),
- ),
- );
- }
- void _showLegendDialog() {
- showDialog(
- context: context,
- builder: (context) {
- return const AlertDialog(
- title: Text('Map Legend'),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _LegendImageRow(
- imagePath: 'assets/icons/monkey_stop.png',
- label: 'Bus stop',
- ),
- SizedBox(height: 12),
- _LegendImageRow(
- imagePath: 'assets/icons/ticket_office.png',
- label: 'Ticket office',
- ),
- SizedBox(height: 12),
- _LegendImageRow(
- imagePath: 'assets/icons/attraction.png',
- label: 'Attraction',
- ),
- SizedBox(height: 12),
- _LegendImageRow(
- imagePath: 'assets/icons/cafe.png',
- label: 'Cafe',
- ),
- ],
- ),
- );
- },
- );
- }
- void _showFiltersSheet() {
- showModalBottomSheet(
- context: context,
- showDragHandle: false,
- isScrollControlled: true,
- builder: (_) => const MapFiltersSheet(),
- );
- }
- void _showPlaceDetails(LocationModel location) {
- showModalBottomSheet(
- context: context,
- isScrollControlled: true,
- showDragHandle: false,
- builder: (_) => PlaceDetailsSheet(location: location),
- );
- }
- Color _hexToColor(String hex) {
- final cleanedHex = hex.replaceAll('#', '');
- return Color(int.parse('FF$cleanedHex', radix: 16));
- }
- Set<Polyline> _buildPolylines(MapProvider mapProvider) {
- return mapProvider.selectedRoutes.map((route) {
- final line = mapProvider.busLines.firstWhere(
- (line) => line.id == route.lineId,
- );
- return Polyline(
- polylineId: PolylineId(route.id),
- width: 5,
- color: _hexToColor(line.colorHex),
- points: route.polylinePoints
- .where((point) {
- final lat = point['latitude'];
- final lng = point['longitude'];
- if (lat is! num || lng is! num) return false;
- return lat >= 41.0 &&
- lat <= 41.5 &&
- lng >= 21.2 &&
- lng <= 21.8;
- })
- .map(
- (point) => LatLng(
- (point['latitude'] as num).toDouble(),
- (point['longitude'] as num).toDouble(),
- ),
- )
- .toList(),
- );
- }).toSet();
- }
- BitmapDescriptor _markerIconForType(String type) {
- switch (type) {
- case 'bus_stop':
- return _busStopMarker ?? BitmapDescriptor.defaultMarker;
- case 'ticket_office':
- return _ticketOfficeMarker ?? BitmapDescriptor.defaultMarker;
- case 'landmark':
- return _attractionMarker ?? BitmapDescriptor.defaultMarker;
- case 'cafe':
- return _cafeMarker ?? BitmapDescriptor.defaultMarker;
- default:
- return BitmapDescriptor.defaultMarker;
- }
- }
- Set<Marker> _buildMarkers(MapProvider mapProvider) {
- final visibleBusStops = mapProvider.selectedRouteStops;
- final otherLocations = mapProvider.locations
- .where((location) => location.type != 'bus_stop')
- .toList();
- final allVisibleLocations = [
- ...visibleBusStops,
- ...otherLocations,
- ];
- return allVisibleLocations.map((location) {
- final lineNumbers = mapProvider.getLineNumbersForStop(location.id);
- return Marker(
- markerId: MarkerId(location.id),
- position: LatLng(location.latitude, location.longitude),
- icon: _markerIconForType(location.type),
- infoWindow: InfoWindow(
- title: location.name,
- snippet: location.type == 'bus_stop'
- ? 'Lines: ${lineNumbers.join(', ')}'
- : mapProvider.getShortDescription(location.description),
- ),
- onTap: () {
- if (location.type == 'landmark' || location.type == 'cafe') {
- _showPlaceDetails(location);
- }
- },
- );
- }).toSet();
- }
- @override
- Widget build(BuildContext context) {
- final locationProvider = context.watch<LocationProvider>();
- final mapProvider = context.watch<MapProvider>();
- final isDarkMode = context.watch<ThemeProvider>().isDarkMode;
- if (_mapController != null && _lastAppliedIsDarkMode != isDarkMode) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- _applyMapStyle(isDarkMode);
- });
- }
- if (mapProvider.isLoading && !mapProvider.hasData) {
- return const Center(
- child: CircularProgressIndicator(),
- );
- }
- if (mapProvider.errorMessage != null && !mapProvider.hasData) {
- return AppErrorState(
- message: mapProvider.errorMessage!,
- imageAssetPath: AppErrorMessages.imageForType(mapProvider.errorType),
- onRetry: mapProvider.loadMapData,
- );
- }
- return Stack(
- children: [
- GoogleMap(
- initialCameraPosition: _initialCameraPosition,
- myLocationEnabled: locationProvider.isGranted,
- myLocationButtonEnabled: false,
- zoomControlsEnabled: false,
- compassEnabled: true,
- mapToolbarEnabled: false,
- polylines: _buildPolylines(mapProvider),
- markers: _buildMarkers(mapProvider),
- onMapCreated: (controller) async {
- _mapController = controller;
- await _applyMapStyle(isDarkMode);
- },
- ),
- LocationPermissionBanner(
- permissionState: locationProvider.permissionState,
- onRequestPermission: locationProvider.requestPermission,
- onOpenSettings: locationProvider.openAppSettingsPage,
- onOpenLocationSettings: locationProvider.openLocationSettingsPage,
- ),
- Positioned(
- right: 16,
- top: MediaQuery.of(context).size.height * 0.34,
- child: Column(
- children: [
- MapRecenterButton(
- onPressed: () => _recenterToUser(locationProvider),
- ),
- const SizedBox(height: 12),
- MapLegendButton(
- onPressed: _showLegendDialog,
- ),
- ],
- ),
- ),
- Positioned(
- left: 0,
- right: 0,
- bottom: 36,
- child: MapFiltersButton(
- onPressed: _showFiltersSheet,
- ),
- ),
- ],
- );
- }
- }
- class _LegendImageRow extends StatelessWidget {
- final String imagePath;
- final String label;
- const _LegendImageRow({
- required this.imagePath,
- required this.label,
- });
- @override
- Widget build(BuildContext context) {
- return Row(
- children: [
- Image.asset(
- imagePath,
- width: 22,
- height: 22,
- ),
- const SizedBox(width: 10),
- Text(label),
- ],
- );
- }
- }
- import 'package:flutter/material.dart';
- import 'package:provider/provider.dart';
- import '../../core/utils/app_error_messages.dart';
- import '../../providers/notifications_provider.dart';
- import '../../widgets/common/app_error_state.dart';
- import 'widgets/notification_dialog.dart';
- import 'widgets/notification_row_card.dart';
- class NotificationsScreen extends StatelessWidget {
- const NotificationsScreen({super.key});
- @override
- Widget build(BuildContext context) {
- final provider = context.watch<NotificationsProvider>();
- if (provider.isLoading && provider.notifications.isEmpty) {
- return const Center(
- child: CircularProgressIndicator(),
- );
- }
- if (provider.errorMessage != null) {
- return AppErrorState(
- message: provider.errorMessage!,
- imageAssetPath: AppErrorMessages.imageForType(provider.errorType),
- onRetry: provider.loadNotifications,
- );
- }
- if (provider.notifications.isEmpty) {
- return RefreshIndicator(
- onRefresh: provider.loadNotifications,
- child: ListView(
- physics: const AlwaysScrollableScrollPhysics(),
- children: const [
- SizedBox(height: 180),
- Center(
- child: Text('No notifications yet.'),
- ),
- ],
- ),
- );
- }
- return RefreshIndicator(
- onRefresh: provider.loadNotifications,
- child: ListView.separated(
- physics: const AlwaysScrollableScrollPhysics(),
- padding: const EdgeInsets.all(16),
- itemCount: provider.notifications.length,
- separatorBuilder: (_, __) => const SizedBox(height: 10),
- itemBuilder: (context, index) {
- final notification = provider.notifications[index];
- final isRead = provider.isRead(notification.id);
- final preview = provider.getPreviewText(notification.message);
- final dateLabel = provider.formatNotificationDate(notification.publishedAt);
- final timeLabel = provider.formatNotificationTime(notification.publishedAt);
- return NotificationRowCard(
- notification: notification,
- isRead: isRead,
- previewText: preview,
- dateLabel: dateLabel,
- timeLabel: timeLabel,
- onTap: () async {
- await provider.markAsRead(notification.id);
- if (!context.mounted) return;
- showDialog(
- context: context,
- builder: (_) => NotificationDialog(
- notification: notification,
- dateLabel: dateLabel,
- timeLabel: timeLabel,
- ),
- );
- },
- );
- },
- ),
- );
- }
- }
- import 'package:flutter/material.dart';
- import 'package:provider/provider.dart';
- import '../../core/utils/app_error_messages.dart';
- import '../../providers/info_provider.dart';
- import '../../widgets/common/app_error_state.dart';
- import 'widgets/info_page_indicator.dart';
- import 'widgets/info_slide_card.dart';
- class InfoScreen extends StatefulWidget {
- const InfoScreen({super.key});
- @override
- State<InfoScreen> createState() => _InfoScreenState();
- }
- class _InfoScreenState extends State<InfoScreen> {
- final PageController _pageController = PageController();
- int _currentPage = 0;
- @override
- void dispose() {
- _pageController.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- final provider = context.watch<InfoProvider>();
- final slides = provider.slides;
- if (provider.isLoading && slides.isEmpty) {
- return const Center(
- child: CircularProgressIndicator(),
- );
- }
- if (provider.errorMessage != null) {
- return AppErrorState(
- message: provider.errorMessage!,
- imageAssetPath: AppErrorMessages.imageForType(provider.errorType),
- onRetry: provider.loadSlides,
- );
- }
- if (slides.isEmpty) {
- return const Center(
- child: Text('No info slides available.'),
- );
- }
- return Column(
- children: [
- Expanded(
- child: RefreshIndicator(
- onRefresh: provider.loadSlides,
- child: PageView.builder(
- controller: _pageController,
- itemCount: slides.length,
- onPageChanged: (index) {
- setState(() {
- _currentPage = index;
- });
- },
- itemBuilder: (context, index) {
- return ListView(
- physics: const AlwaysScrollableScrollPhysics(),
- children: [
- SizedBox(
- height: MediaQuery.of(context).size.height * 0.72,
- child: InfoSlideCard(
- slide: slides[index],
- ),
- ),
- ],
- );
- },
- ),
- ),
- ),
- Padding(
- padding: const EdgeInsets.only(bottom: 22),
- child: InfoPageIndicator(
- itemCount: slides.length,
- currentIndex: _currentPage,
- ),
- ),
- ],
- );
- }
- }
- import 'package:flutter/material.dart';
- import 'package:geolocator/geolocator.dart';
- import 'package:provider/provider.dart';
- import '../../core/utils/app_error_messages.dart';
- import '../../models/bus_line_model.dart';
- import '../../models/location_model.dart';
- import '../../providers/location_provider.dart';
- import '../../providers/map_provider.dart';
- import '../../providers/stops_provider.dart';
- import '../../widgets/common/app_error_state.dart';
- import '../../widgets/common/location_access_prompt.dart';
- import 'stop_arrivals_screen.dart';
- import 'widgets/stop_row_card.dart';
- import 'widgets/stops_direction_toggle.dart';
- import 'widgets/stops_search_bar.dart';
- class StopsScreen extends StatefulWidget {
- const StopsScreen({super.key});
- @override
- State<StopsScreen> createState() => _StopsScreenState();
- }
- class _StopsScreenState extends State<StopsScreen> {
- late final TextEditingController _searchController;
- @override
- void initState() {
- super.initState();
- _searchController = TextEditingController();
- }
- @override
- void dispose() {
- _searchController.dispose();
- super.dispose();
- }
- List<LocationModel> _stopsForDirection(
- MapProvider mapProvider,
- String direction,
- ) {
- final routeStopIds = mapProvider.lineRoutes
- .where((route) => route.direction == direction && route.isActive)
- .expand((route) => route.stopIdsOrdered)
- .toSet();
- return mapProvider.locations.where((location) {
- return location.type == 'bus_stop' && routeStopIds.contains(location.id);
- }).toList();
- }
- List<BusLineModel> _linesForStop(
- MapProvider mapProvider,
- String stopId,
- String direction,
- ) {
- final lineIds = mapProvider.lineRoutes
- .where(
- (route) =>
- route.direction == direction &&
- route.isActive &&
- route.stopIdsOrdered.contains(stopId),
- )
- .map((route) => route.lineId)
- .toSet();
- final lines = mapProvider.busLines
- .where((line) => lineIds.contains(line.id))
- .toList()
- ..sort((a, b) => a.number.compareTo(b.number));
- return lines;
- }
- List<LocationModel> _nearestStops(
- List<LocationModel> stops,
- Position position,
- ) {
- final sorted = [...stops];
- sorted.sort((a, b) {
- final distanceA = Geolocator.distanceBetween(
- position.latitude,
- position.longitude,
- a.latitude,
- a.longitude,
- );
- final distanceB = Geolocator.distanceBetween(
- position.latitude,
- position.longitude,
- b.latitude,
- b.longitude,
- );
- return distanceA.compareTo(distanceB);
- });
- return sorted.take(5).toList();
- }
- List<LocationModel> _searchStops(
- List<LocationModel> stops,
- String query,
- ) {
- final normalized = query.trim().toLowerCase();
- final filtered = stops.where((stop) {
- return stop.name.toLowerCase().contains(normalized);
- }).toList()
- ..sort((a, b) => a.name.compareTo(b.name));
- return filtered;
- }
- @override
- Widget build(BuildContext context) {
- final mapProvider = context.watch<MapProvider>();
- final locationProvider = context.watch<LocationProvider>();
- final stopsProvider = context.watch<StopsProvider>();
- if (_searchController.text != stopsProvider.searchQuery) {
- _searchController.value = TextEditingValue(
- text: stopsProvider.searchQuery,
- selection: TextSelection.collapsed(
- offset: stopsProvider.searchQuery.length,
- ),
- );
- }
- final direction = stopsProvider.selectedDirection;
- final allStopsForDirection = _stopsForDirection(mapProvider, direction);
- final hasSearch = stopsProvider.searchQuery.trim().isNotEmpty;
- List<LocationModel> visibleStops = [];
- if (hasSearch) {
- visibleStops = _searchStops(
- allStopsForDirection,
- stopsProvider.searchQuery,
- );
- } else if (locationProvider.currentPosition != null &&
- locationProvider.isGranted) {
- visibleStops = _nearestStops(
- allStopsForDirection,
- locationProvider.currentPosition!,
- );
- }
- return Column(
- children: [
- Expanded(
- child: Padding(
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
- child: Column(
- children: [
- StopsSearchBar(
- controller: _searchController,
- onChanged: stopsProvider.updateSearchQuery,
- ),
- const SizedBox(height: 16),
- Expanded(
- child: Builder(
- builder: (context) {
- if (mapProvider.errorMessage != null &&
- !mapProvider.hasData) {
- return AppErrorState(
- message: mapProvider.errorMessage!,
- imageAssetPath:
- AppErrorMessages.imageForType(mapProvider.errorType),
- onRetry: mapProvider.loadMapData,
- );
- }
- if (!hasSearch && !locationProvider.isGranted) {
- return LocationAccessPrompt(
- permissionState: locationProvider.permissionState,
- onRequestPermission: locationProvider.requestPermission,
- onOpenAppSettings:
- locationProvider.openAppSettingsPage,
- onOpenLocationSettings:
- locationProvider.openLocationSettingsPage,
- variant: LocationAccessPromptVariant.fullPage,
- imageAssetPath:
- 'assets/images/error/mende_no_internet.png',
- );
- }
- if (!hasSearch &&
- locationProvider.isGranted &&
- locationProvider.currentPosition == null &&
- locationProvider.isLoading) {
- return const Center(
- child: CircularProgressIndicator(),
- );
- }
- return ListView.separated(
- itemCount: visibleStops.length,
- separatorBuilder: (_, __) => const Divider(height: 1),
- itemBuilder: (context, index) {
- final stop = visibleStops[index];
- final lines = _linesForStop(
- mapProvider,
- stop.id,
- direction,
- );
- return StopRowCard(
- stop: stop,
- lines: lines,
- showNearby: !hasSearch,
- onTap: () {
- Navigator.of(context).push(
- MaterialPageRoute(
- builder: (_) => StopArrivalsScreen(
- stop: stop,
- direction: direction,
- ),
- ),
- );
- },
- );
- },
- );
- },
- ),
- ),
- ],
- ),
- ),
- ),
- StopsDirectionToggle(
- selectedDirection: stopsProvider.selectedDirection,
- onDirectionSelected: stopsProvider.selectDirection,
- ),
- ],
- );
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment