Update project

This commit is contained in:
2026-07-23 15:36:11 +05:30
parent 1197cfc161
commit 14fffa40c6
84 changed files with 20918 additions and 2761 deletions

View File

@@ -0,0 +1,744 @@
// delivery_tracking_page.dart
//
// SETUP:
// 1. pubspec.yaml -> add:
// google_maps_flutter: ^2.9.0
// url_launcher: ^6.3.0 (for calling the delivery partner)
//
// 2. Android -> android/app/src/main/AndroidManifest.xml, inside <application>:
// <meta-data
// android:name="com.google.android.geo.API_KEY"
// android:value="xxx" />
//
// 3. iOS -> ios/Runner/AppDelegate.swift, inside application(didFinishLaunchingWithOptions):
// GMSServices.provideAPIKey("xxx")
//
// Replace "xxx" above with your real Google Maps API key in BOTH places.
import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import '../../../widgets/text_widget.dart';
/// Draws a small circular badge marker at runtime (no external icon assets
/// needed) — a colored circle with an icon inside, similar to Swiggy's
/// bike / restaurant / home pins.
Future<BitmapDescriptor> _drawBadgeMarker({
required IconData icon,
required Color background,
Color iconColor = Colors.white,
double size = 60,
}) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final paint = Paint()..color = background;
final radius = size / 2;
canvas.drawCircle(Offset(radius, radius), radius, Paint()..color = Colors.white);
canvas.drawCircle(Offset(radius, radius), radius - 6, paint);
final textPainter = TextPainter(textDirection: TextDirection.ltr);
textPainter.text = TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
fontSize: size * 0.5,
fontFamily: icon.fontFamily,
package: icon.fontPackage,
color: iconColor,
),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(radius - textPainter.width / 2, radius - textPainter.height / 2),
);
final picture = recorder.endRecording();
final img = await picture.toImage(size.toInt(), size.toInt());
final bytes = await img.toByteData(format: ui.ImageByteFormat.png);
return BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List());
}
/// Your Google Maps API key (also enable it for "Directions API").
const String kGoogleApiKey = "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q";
/// Custom map style JSON — a clean, low-saturation grey theme.
const String kSwiggyLikeMapStyle = '''
[
{"elementType": "geometry", "stylers": [{"color": "#f5f5f3"}]},
{"elementType": "labels.icon", "stylers": [{"visibility": "off"}]},
{"elementType": "labels.text.fill", "stylers": [{"color": "#8a8a8a"}]},
{"elementType": "labels.text.stroke", "stylers": [{"color": "#f5f5f3"}]},
{"featureType": "administrative", "elementType": "geometry", "stylers": [{"visibility": "off"}]},
{"featureType": "administrative.land_parcel", "stylers": [{"visibility": "off"}]},
{"featureType": "administrative.neighborhood", "stylers": [{"visibility": "off"}]},
{"featureType": "poi", "stylers": [{"visibility": "off"}]},
{"featureType": "poi.park", "elementType": "geometry", "stylers": [{"color": "#e5e8e0"}]},
{"featureType": "road", "elementType": "geometry", "stylers": [{"color": "#ffffff"}]},
{"featureType": "road", "elementType": "geometry.stroke", "stylers": [{"color": "#e3e3e3"}]},
{"featureType": "road.arterial", "elementType": "labels", "stylers": [{"visibility": "off"}]},
{"featureType": "road.highway", "elementType": "geometry", "stylers": [{"color": "#f2c9a0"}]},
{"featureType": "road.local", "elementType": "labels", "stylers": [{"visibility": "off"}]},
{"featureType": "transit", "stylers": [{"visibility": "off"}]},
{"featureType": "water", "elementType": "geometry", "stylers": [{"color": "#c9d6df"}]}
]
''';
class DeliveryTrackingPage extends StatefulWidget {
const DeliveryTrackingPage({super.key});
@override
State<DeliveryTrackingPage> createState() => _DeliveryTrackingPageState();
}
class _DeliveryTrackingPageState extends State<DeliveryTrackingPage> {
GoogleMapController? _mapController;
String _partnerName = 'Majid Khan';
double _partnerRating = 4.8;
int _partnerOrders = 1240;
// Sample coordinates - replace with your real pickup / drop / rider coords
final LatLng _pickup = const LatLng(28.6450, 77.3550); // restaurant
final LatLng _drop = const LatLng(28.6395, 77.3480); // customer
LatLng _riderPosition = const LatLng(28.6440, 77.3520);
final Set<Marker> _markers = {};
final Set<Polyline> _polylines = {};
List<LatLng> _routePoints = [];
Timer? _riderTimer;
int _routeIndex = 0;
int _etaMinutes = 2;
BitmapDescriptor? _pickupIcon;
BitmapDescriptor? _dropIcon;
BitmapDescriptor? _riderIcon;
@override
void initState() {
super.initState();
_loadIcons();
}
Future<void> _loadIcons() async {
_pickupIcon = await _drawBadgeMarker(
icon: Icons.storefront,
background: const Color(0xFFFF6A00),
);
_dropIcon = await _drawBadgeMarker(
icon: Icons.home,
background: const Color(0xFF7A1F5C),
);
_riderIcon = await _drawBadgeMarker(
icon: Icons.two_wheeler,
background: const Color(0xFF662582),
size: 60,
);
_setupMarkers();
_fetchRoute();
}
@override
void dispose() {
_riderTimer?.cancel();
super.dispose();
}
void _setupMarkers() {
_markers
..clear()
..addAll([
Marker(
markerId: const MarkerId('pickup'),
position: _pickup,
icon: _pickupIcon ??
BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueOrange),
infoWindow: const InfoWindow(title: 'Wrapperz'),
),
Marker(
markerId: const MarkerId('drop'),
position: _drop,
icon: _dropIcon ??
BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueViolet),
infoWindow: const InfoWindow(title: 'Friends and Family'),
),
Marker(
markerId: const MarkerId('rider'),
position: _riderPosition,
icon: _riderIcon ??
BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
infoWindow: const InfoWindow(title: 'MAJID - delivery partner'),
),
]);
}
/// Fetches a real road-following route from Google Directions API and
/// draws it as a polyline.
Future<void> _fetchRoute() async {
try {
final url = Uri.parse(
'https://maps.googleapis.com/maps/api/directions/json'
'?origin=${_pickup.latitude},${_pickup.longitude}'
'&destination=${_drop.latitude},${_drop.longitude}'
'&mode=driving'
'&key=$kGoogleApiKey',
);
final response = await http.get(url);
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final points = data['routes'][0]['overview_polyline']['points'];
_routePoints = _decodePolyline(points);
} else {
_routePoints = _fallbackRoute();
}
} else {
_routePoints = _fallbackRoute();
}
} catch (_) {
_routePoints = _fallbackRoute();
}
setState(() {
_polylines
..clear()
..add(
Polyline(
polylineId: const PolylineId('route'),
points: _routePoints,
color: const Color(0xFF662582),
width: 5,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
jointType: JointType.round,
),
);
});
_startRiderAnimation();
}
/// Simple zig-zag fallback so the UI still shows a route without a live key.
List<LatLng> _fallbackRoute() {
return [
_pickup,
LatLng(_pickup.latitude - 0.0025, _pickup.longitude - 0.0015),
LatLng(_pickup.latitude - 0.0035, _pickup.longitude - 0.0060),
LatLng(_pickup.latitude - 0.0060, _pickup.longitude - 0.0075),
_drop,
];
}
List<LatLng> _decodePolyline(String encoded) {
List<LatLng> points = [];
int index = 0, len = encoded.length;
int lat = 0, lng = 0;
while (index < len) {
int b, shift = 0, result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlat = (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlng = (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
lng += dlng;
points.add(LatLng(lat / 1E5, lng / 1E5));
}
return points;
}
/// Moves the rider marker along the route to simulate live tracking.
void _startRiderAnimation() {
if (_routePoints.isEmpty) return;
_riderTimer?.cancel();
_riderTimer = Timer.periodic(const Duration(seconds: 2), (timer) {
if (_routeIndex >= _routePoints.length - 1) {
timer.cancel();
return;
}
_routeIndex++;
_riderPosition = _routePoints[_routeIndex];
_setupMarkers();
setState(() {});
_mapController?.animateCamera(CameraUpdate.newLatLng(_riderPosition));
});
}
Future<void> _callPartner() async {
final uri = Uri(scheme: 'tel', path: '+910000000000');
if (await canLaunchUrl(uri)) await launchUrl(uri);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: SizedBox(),
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ReusableTextWidget(
text: 'Out for delivery',
fontSize: 17,
fontWeight: FontWeight.w700,
color: Colors.white,
),
const SizedBox(height: 2),
ReusableTextWidget(
text: '11:38 AM • 1 items',
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white
),
],
),
centerTitle: true,
backgroundColor: const Color(0xFF662582),
),
backgroundColor: Colors.white,
body: Stack(
children: [
// ---------------- MAP ----------------
SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: _pickup,
zoom: 14.5,
),
markers: _markers,
polylines: _polylines,
myLocationButtonEnabled: false,
zoomControlsEnabled: false,
compassEnabled: false,
mapToolbarEnabled: false,
buildingsEnabled: false,
indoorViewEnabled: false,
trafficEnabled: false,
style: kSwiggyLikeMapStyle,
onMapCreated: (controller) => _mapController = controller,
),
),
// -------------- TOP BAR --------------
// SafeArea(
// child: Padding(
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// _circleIconButton(Icons.arrow_back, () {
// Navigator.maybePop(context);
// }),
// Column(
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// ReusableTextWidget(
// text: 'Out for delivery',
// fontSize: 17,
// fontWeight: FontWeight.w700,
// color: Colors.grey.shade900,
// ),
// const SizedBox(height: 2),
// ReusableTextWidget(
// text: '11:38 AM • 1 items',
// fontSize: 12,
// fontWeight: FontWeight.w500,
// color: Colors.grey.shade600,
// ),
// ],
// ),
// const SizedBox(width: 40),
// ],
// ),
// ),
// ),
// -------------- BOTTOM SHEET --------------
DraggableScrollableSheet(
initialChildSize: 0.42,
minChildSize: 0.42,
maxChildSize: 0.9,
builder: (context, scrollController) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 12,
offset: Offset(0, -2),
),
],
),
child: ListView(
controller: scrollController,
padding: EdgeInsets.zero,
children: [
// Out for delivery row
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: 'Out for delivery',
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.grey.shade800,
),
const SizedBox(height: 4),
ReusableTextWidget(
text: 'MAJID is on the way to deliver your order',
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF7A1F5C),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
ReusableTextWidget(
text: '$_etaMinutes',
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white,
),
ReusableTextWidget(
text: 'mins',
fontSize: 11,
fontWeight: FontWeight.w500,
color: Colors.white,
),
],
),
),
],
),
),
// Delivery partner info card
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade200),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Stack(
children: [
CircleAvatar(
radius: 28,
backgroundColor: Colors.grey.shade100,
backgroundImage: const NetworkImage(
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSmbomkQ5Aqtd848F_UVR86eY1wrq5tuNJRjmRXAGgXsqJtlL5H6EF8Efws&s=10'),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: Colors.green.shade500,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
],
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: _partnerName,
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.grey.shade800,
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.star_rounded,
size: 16, color: Colors.amber.shade700),
const SizedBox(width: 4),
ReusableTextWidget(
text: '$_partnerRating',
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
const SizedBox(width: 10),
ReusableTextWidget(
text: '$_partnerOrders orders',
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
],
),
],
),
),
InkWell(
onTap: _callPartner,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFFCE4EC),
),
child: const Icon(Icons.call,
color: Colors.redAccent, size: 18),
),
),
],
),
const SizedBox(height: 14),
// Action buttons row
],
),
),
),
Container(height: 8, color: const Color(0xFFF5F5F5)),
// Product / order items card
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade200),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Icon(Icons.receipt_long, size: 20, color: Colors.grey.shade700),
const SizedBox(width: 8),
ReusableTextWidget(
text: 'Order ID: # 123456789',
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
],
),
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 12),
// Items list
_buildBillItem('Chicken Biryani', 1, 250),
const SizedBox(height: 10),
_buildBillItem('Mutton Biryani', 1, 350),
const SizedBox(height: 10),
const SizedBox(height: 4),
const Divider(height: 1),
const SizedBox(height: 12),
// Price breakdown
_buildPriceRow('Subtotal', 600),
const SizedBox(height: 6),
_buildPriceRow('Delivery Fee', 40),
const SizedBox(height: 6),
_buildPriceRow('Tax (5%)', 30),
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 12),
// Total
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ReusableTextWidget(
text: 'Total',
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.grey.shade900,
),
ReusableTextWidget(
text: '₹670',
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.grey.shade900,
),
],
),
],
),
),
),
],
),
);
},
),
],
),
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 6),
ReusableTextWidget(
text: label,
fontSize: 13,
fontWeight: FontWeight.w600,
color: color,
),
],
),
),
);
}
Widget _buildBillItem(String name, int qty, double price) {
final total = qty * price;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ReusableTextWidget(
text: '${qty}x $name',
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
ReusableTextWidget(
text: '${price.toStringAsFixed(0)}',
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
const SizedBox(width: 12),
ReusableTextWidget(
text: '${total.toStringAsFixed(0)}',
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
],
);
}
Widget _buildPriceRow(String label, double amount) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ReusableTextWidget(
text: label,
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
ReusableTextWidget(
text: '${amount.toStringAsFixed(0)}',
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
],
);
}
Widget _circleIconButton(IconData icon, VoidCallback onTap) {
return InkWell(
onTap: onTap,
customBorder: const CircleBorder(),
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(color: Colors.black26, blurRadius: 6),
],
),
child: Icon(icon, color: Colors.black87, size: 20),
),
);
}
}