Files
tripz-llc/apps/rider/lib/features/home/view/home_screen.dart
T

529 lines
20 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../core/l10n/app_localizations.dart';
import '../../../core/design/tokens.dart';
import '../../auth/cubit/auth_cubit.dart';
import '../../auth/cubit/auth_state.dart';
import '../../trip/bloc/trip_bloc.dart';
import '../../trip/bloc/trip_state.dart';
import '../../trip/bloc/trip_event.dart';
import '../cubit/home_cubit.dart';
import '../cubit/saved_places_cubit.dart';
import '../data/models/place_model.dart';
import '../data/maps_repository.dart';
import '../../../core/storage/saved_places_store.dart';
import '../../../core/di.dart';
/// زر عائم دائري صغير — للقائمة وإعادة التوسيط فوق الخريطة.
class _MapCircleButton extends StatelessWidget {
const _MapCircleButton({required this.icon, required this.onPressed});
final IconData icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: cs.surface,
shape: BoxShape.circle,
boxShadow: const [T.shadowSm],
),
child: IconButton(
icon: Icon(icon, color: cs.onSurface),
onPressed: onPressed,
),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
IntaleqMapController? _mapController;
bool _isSelectingOnMap = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<TripBloc>().add(const TripCheckActive());
context.read<SavedPlacesCubit>().load();
});
}
String _timeBasedGreeting(String? name, AppLocalizations l10n) {
final hour = DateTime.now().hour;
final isMorning = hour >= 5 && hour < 12;
final isAr = Localizations.localeOf(context).languageCode == 'ar';
final prefix = isMorning
? (isAr ? 'صباح الخير' : 'Good morning')
: (isAr ? 'مساء الخير' : 'Good evening');
final firstName = (name != null && name.trim().isNotEmpty)
? name.trim().split(' ').first
: '';
return firstName.isNotEmpty ? '$prefix، $firstName' : prefix;
}
String _whereToPrompt(String? name, AppLocalizations l10n) {
final firstName = (name != null && name.trim().isNotEmpty)
? name.trim().split(' ').first
: '';
if (firstName.isNotEmpty) {
final isAr = Localizations.localeOf(context).languageCode == 'ar';
return isAr ? 'إلى أين يا $firstName؟' : 'Where to, $firstName?';
}
return l10n.whereTo;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final cs = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return BlocListener<TripBloc, TripState>(
listenWhen: (prev, cur) => prev.trip?.status != cur.trip?.status,
listener: (context, state) {
if (state.trip != null && !state.isCompleted) {
context.push('/trip-tracking');
}
},
child: Scaffold(
backgroundColor: cs.surface,
// الخريطة تملأ الشاشة حتى الحافة العلوية (docs/29 §3.6) — كل العناصر
// العائمة فوقها تعيش داخل SafeArea الخاصة بها لا الشاشة كلها.
extendBodyBehindAppBar: true,
body: Stack(
children: [
// ── الخريطة (75% من الشاشة) ──
Positioned(
top: 0,
left: 0,
right: 0,
height: MediaQuery.of(context).size.height * 0.75,
child: BlocConsumer<HomeCubit, HomeState>(
listenWhen: (prev, cur) =>
prev.currentLocation == null && cur.currentLocation != null,
buildWhen: (prev, cur) =>
prev.currentLocation != cur.currentLocation ||
prev.pickup != cur.pickup ||
prev.destination != cur.destination,
listener: (context, state) {
if (state.currentLocation != null && _mapController != null) {
_mapController!.animateCamera(
CameraUpdate.newLatLngZoom(state.currentLocation!, 16),
);
}
},
builder: (context, state) {
final center =
state.currentLocation ?? const LatLng(31.9539, 35.9106);
return IntaleqMap(
apiKey: '',
initialCameraPosition:
CameraPosition(target: center, zoom: 15),
mapType:
isDark ? IntaleqMapType.normal : IntaleqMapType.light,
myLocationEnabled: true,
myLocationButtonEnabled: false,
zoomControlsEnabled: false,
compassEnabled: false,
onMapCreated: (controller) {
_mapController = controller;
if (state.currentLocation != null) {
Future.delayed(const Duration(milliseconds: 300), () {
controller.animateCamera(
CameraUpdate.newLatLngZoom(
state.currentLocation!, 16),
);
});
}
},
markers: _buildMarkers(state),
onCameraMove: (position) {
context
.read<HomeCubit>()
.updateCameraTarget(position.target);
},
);
},
),
),
// زر القائمة أعلى-بداية
PositionedDirectional(
top: MediaQuery.of(context).padding.top + T.s8,
start: T.pagePadding,
child: _MapCircleButton(
icon: Icons.menu_rounded,
onPressed: () => _showLogoutDialog(context, l10n),
),
),
// زر إعادة التوسيط عائم أعلى البطاقة السفلية
PositionedDirectional(
end: T.pagePadding,
bottom: (_isSelectingOnMap ? 100 : MediaQuery.of(context).size.height * 0.28) + T.s16,
child: BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
return _MapCircleButton(
icon: Icons.my_location_rounded,
onPressed: state.currentLocation == null
? () {}
: () => _mapController?.animateCamera(
CameraUpdate.newLatLngZoom(
state.currentLocation!, 16),
),
);
},
),
),
if (_isSelectingOnMap)
// علامة المركز (دبوس) لاختيار الموقع
Positioned(
top: (MediaQuery.of(context).size.height * 0.75) / 2 - 20,
left: MediaQuery.of(context).size.width / 2 - 20,
child: const Icon(Icons.location_on, size: 40, color: Colors.black),
),
// ── البطاقة السفلية: «إلى أين؟» والأماكن المحفوظة ──
if (!_isSelectingOnMap)
Positioned(
left: 0,
right: 0,
bottom: 0,
height: MediaQuery.of(context).size.height * 0.30,
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(
T.pagePadding, T.s24, T.pagePadding, T.s16),
decoration: BoxDecoration(
color: cs.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(T.r24),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
offset: const Offset(0, -5),
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// التحية الزمنية مع اسم الراكب بتصميم أنيق
BlocBuilder<AuthCubit, AuthState>(
builder: (context, authState) {
final greeting = _timeBasedGreeting(authState.user?.name, l10n);
return Text(
greeting,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
fontWeight: FontWeight.w800,
color: cs.onSurface,
letterSpacing: -0.5,
),
);
},
),
const SizedBox(height: T.s24),
// حقل «إلى أين؟»
BlocBuilder<AuthCubit, AuthState>(
builder: (context, authState) {
final searchPrompt = _whereToPrompt(authState.user?.name, l10n);
return Material(
color: cs.surfaceContainerLowest,
elevation: 2,
shadowColor: cs.shadow.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(T.r16),
child: InkWell(
borderRadius: BorderRadius.circular(T.r16),
onTap: () => _openDestinationSearch(context),
child: Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: T.s16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(T.r16),
border: Border.all(
color: cs.outlineVariant.withValues(alpha: 0.5),
width: 1,
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: cs.primaryContainer.withValues(alpha: 0.5),
shape: BoxShape.circle,
),
child: Icon(
Icons.search_rounded,
color: cs.primary,
size: 22,
),
),
const SizedBox(width: T.s16),
Expanded(
child: Text(
searchPrompt,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
color: cs.onSurface,
fontWeight: FontWeight.w700,
fontSize: 18,
),
),
),
],
),
),
),
);
},
),
const Spacer(),
Divider(color: cs.outlineVariant.withValues(alpha: 0.3), height: 1),
const SizedBox(height: T.s12),
// الأماكن المحفوظة (سطر أفقي أنيق)
BlocBuilder<SavedPlacesCubit, SavedPlacesState>(
builder: (context, savedState) {
final items = savedState.places.take(3).toList();
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildQuickAction(context, Icons.home_rounded, l10n.homePlace, items.isNotEmpty ? items.first : null),
Container(width: 1, height: 32, color: cs.outlineVariant.withValues(alpha: 0.3)),
_buildQuickAction(context, Icons.work_rounded, l10n.workPlace, items.length > 1 ? items[1] : null),
Container(width: 1, height: 32, color: cs.outlineVariant.withValues(alpha: 0.3)),
_buildQuickAction(context, Icons.star_rounded, l10n.favorites, items.length > 2 ? items[2] : null),
],
);
},
),
const SizedBox(height: T.s8),
],
),
),
)
else
// زر تأكيد الوجهة في وضع اختيار الخريطة
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.fromLTRB(
T.pagePadding, T.s16, T.pagePadding, T.s32),
decoration: BoxDecoration(
color: cs.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(T.r24),
),
boxShadow: const [T.shadowLg],
),
child: FilledButton(
onPressed: _confirmMapDestination,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(T.r12),
),
),
child: Text('تأكيد الوجهة', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: cs.onPrimary)),
),
),
),
],
),
),
);
}
void _onPlaceTap(BuildContext context, SavedPlace place) {
context.read<HomeCubit>().setDestination(PlaceModel(
name: place.name,
address: place.address,
lat: place.lat,
lng: place.lng,
));
final state = context.read<HomeCubit>().state;
if (state.pickup != null) {
_openRoutePreview(context, state);
}
}
Set<Marker> _buildMarkers(HomeState state) {
final markers = <Marker>{};
if (state.pickup != null) {
markers.add(Marker(
markerId: const MarkerId('pickup'),
position: LatLng(state.pickup!.lat, state.pickup!.lng),
icon: InlqBitmap.defaultMarkerWithHue(210),
anchor: const Offset(0.5, 1.0),
zIndex: 2,
));
}
if (state.destination != null) {
markers.add(Marker(
markerId: const MarkerId('destination'),
position: LatLng(state.destination!.lat, state.destination!.lng),
icon: InlqBitmap.defaultMarkerWithHue(0),
anchor: const Offset(0.5, 1.0),
zIndex: 1,
));
}
return markers;
}
void _openDestinationSearch(BuildContext context) {
final homeCubit = context.read<HomeCubit>();
final pickup = homeCubit.state.pickup;
final currentLocation = homeCubit.state.currentLocation;
context.push<dynamic>('/destination', extra: {
'currentLocation': currentLocation,
}).then((result) {
if (!mounted) return;
if (result is Map && result['pickFromMap'] == true) {
setState(() {
_isSelectingOnMap = true;
});
return;
}
final place = result as PlaceModel?;
if (place != null && pickup != null) {
homeCubit.setDestination(place);
// ignore: use_build_context_synchronously
context.push('/route-preview', extra: {
'pickup': pickup,
'destination': place,
});
}
});
}
Future<void> _confirmMapDestination() async {
final homeCubit = context.read<HomeCubit>();
final target = homeCubit.state.cameraTarget;
if (target == null) return;
// We reverse geocode the target to get the address name
final repo = getIt<MapsRepository>();
try {
final place = await repo.reverseGeocode(target.latitude, target.longitude);
setState(() {
_isSelectingOnMap = false;
});
homeCubit.setDestination(place);
if (homeCubit.state.pickup != null) {
_openRoutePreview(context, homeCubit.state);
}
} catch (_) {
// In case of error, still set it with empty name
setState(() {
_isSelectingOnMap = false;
});
homeCubit.setDestination(PlaceModel(
name: 'موقع محدد على الخريطة',
lat: target.latitude,
lng: target.longitude,
));
if (homeCubit.state.pickup != null) {
_openRoutePreview(context, homeCubit.state);
}
}
}
void _openRoutePreview(BuildContext context, HomeState state) {
if (state.pickup == null || state.destination == null) return;
context.push('/route-preview', extra: {
'pickup': state.pickup,
'destination': state.destination,
});
}
void _showLogoutDialog(BuildContext context, AppLocalizations l10n) {
final cs = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(l10n.logout),
content: Text(l10n.logout),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () async {
Navigator.pop(ctx);
await context.read<AuthCubit>().logout();
if (context.mounted) context.go('/phone');
},
style: FilledButton.styleFrom(backgroundColor: cs.error),
child: Text(l10n.logout),
),
],
),
);
}
Widget _buildQuickAction(
BuildContext context, IconData icon, String label, SavedPlace? place) {
final cs = Theme.of(context).colorScheme;
return InkWell(
onTap: place != null ? () => _onPlaceTap(context, place) : null,
borderRadius: BorderRadius.circular(T.r12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: T.s12, vertical: T.s8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon,
color: place != null
? cs.primary
: cs.onSurfaceVariant.withValues(alpha: 0.5),
size: 28),
const SizedBox(height: T.s4),
Text(
label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: place != null
? cs.onSurface
: cs.onSurfaceVariant.withValues(alpha: 0.5),
fontWeight:
place != null ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
);
}
}