672 lines
22 KiB
Dart
Executable File
672 lines
22 KiB
Dart
Executable File
import 'dart:io';
|
|
|
|
import 'package:bubble_head/bubble.dart';
|
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
|
import 'package:sefer_driver/views/home/Captin/home_captain/drawer_captain.dart';
|
|
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
|
|
|
import '../../../../constant/box_name.dart';
|
|
import '../../../../constant/colors.dart';
|
|
import '../../../../constant/info.dart';
|
|
import '../../../../controller/functions/location_controller.dart';
|
|
import '../../../../controller/functions/overlay_permisssion.dart';
|
|
import '../../../../controller/functions/package_info.dart';
|
|
import '../../../../controller/home/captin/home_captain_controller.dart';
|
|
import '../../../../controller/home/captin/map_driver_controller.dart';
|
|
import '../../../../controller/home/navigation/navigation_view.dart';
|
|
import '../../../../controller/profile/setting_controller.dart';
|
|
import '../../../../env/env.dart';
|
|
import '../../../../main.dart';
|
|
import '../../../notification/available_rides_page.dart';
|
|
import '../driver_map_page.dart';
|
|
import 'widget/connect.dart';
|
|
import 'widget/left_menu_map_captain.dart';
|
|
|
|
// ─────────────────────────────────────────────
|
|
// Design Tokens (Responsive to Theme)
|
|
// ─────────────────────────────────────────────
|
|
class _Token {
|
|
static Color surface(BuildContext context) =>
|
|
Theme.of(context).brightness == Brightness.dark
|
|
? const Color(0xFF1A1A2E)
|
|
: Colors.white;
|
|
|
|
static Color surfaceCard(BuildContext context) =>
|
|
Theme.of(context).brightness == Brightness.dark
|
|
? const Color(0xFF16213E)
|
|
: const Color(0xFFF8F9FA);
|
|
|
|
static const Color accent = Color(0xFFF0A500);
|
|
static Color accentSoft(BuildContext context) =>
|
|
const Color(0xFFF0A500).withOpacity(0.12);
|
|
static const Color danger = Color(0xFFE53935);
|
|
static const Color success = Color(0xFF2ECC71);
|
|
static const Color info = Color(0xFF3498DB);
|
|
|
|
static Color border(BuildContext context) =>
|
|
Theme.of(context).brightness == Brightness.dark
|
|
? const Color(0xFF2A2A4A)
|
|
: Colors.grey.withOpacity(0.2);
|
|
|
|
static Color text(BuildContext context) =>
|
|
Theme.of(context).brightness == Brightness.dark
|
|
? Colors.white
|
|
: const Color(0xFF2D3436);
|
|
|
|
static Color textDim(BuildContext context) =>
|
|
Theme.of(context).brightness == Brightness.dark
|
|
? Colors.white38
|
|
: Colors.black45;
|
|
|
|
static const double radius = 16.0;
|
|
static const double radiusSm = 10.0;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// Root Widget
|
|
// ─────────────────────────────────────────────
|
|
class HomeCaptain extends StatelessWidget {
|
|
HomeCaptain({super.key});
|
|
|
|
final LocationController locationController =
|
|
Get.put(LocationController(), permanent: true);
|
|
final HomeCaptainController homeCaptainController =
|
|
Get.put(HomeCaptainController());
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
await closeOverlayIfFound();
|
|
if (!context.mounted) return;
|
|
await checkForUpdate(context);
|
|
if (!context.mounted) return;
|
|
await getPermissionOverlay();
|
|
if (!context.mounted) return;
|
|
await showDriverGiftClaim(context);
|
|
if (!context.mounted) return;
|
|
await checkForAppliedRide(context);
|
|
});
|
|
|
|
return Scaffold(
|
|
extendBodyBehindAppBar: true,
|
|
backgroundColor: _Token.surface(context),
|
|
appBar: const _HomeAppBar(),
|
|
drawer: AppDrawer(),
|
|
body: SafeArea(
|
|
top: false,
|
|
child: Stack(
|
|
children: [
|
|
const _MapView(),
|
|
leftMainMenuCaptainIcons(),
|
|
const _BottomStatusBar(),
|
|
const _FloatingControls(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// AppBar — no blur, solid gradient
|
|
// ─────────────────────────────────────────────
|
|
class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|
const _HomeAppBar();
|
|
|
|
@override
|
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final ctrl = Get.find<HomeCaptainController>();
|
|
|
|
return AppBar(
|
|
backgroundColor: _Token.surface(context),
|
|
elevation: 0,
|
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
|
titleSpacing: 0,
|
|
// ── Logo + App Name ──────────────────────
|
|
title: Padding(
|
|
padding: const EdgeInsets.only(left: 4),
|
|
child: Row(
|
|
children: [
|
|
_LogoBadge(),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
AppInformation.appName.split(' ')[0].tr,
|
|
style: const TextStyle(
|
|
color: _Token.accent,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
// ── Refuse Counter ───────────────────
|
|
GetBuilder<HomeCaptainController>(
|
|
builder: (c) => _PillBadge(
|
|
icon: Icons.block_rounded,
|
|
label: c.countRefuse.toString(),
|
|
color: _Token.danger,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
// ── Map Controls Row ─────────────────
|
|
_AppBarControls(ctrl: ctrl),
|
|
const SizedBox(width: 8),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LogoBadge extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: _Token.surfaceCard(context),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: _Token.accent, width: 1.5),
|
|
),
|
|
child: ClipOval(
|
|
child: Image.asset('assets/images/logo.gif', fit: BoxFit.cover),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _PillBadge extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
final Color color;
|
|
const _PillBadge(
|
|
{required this.icon, required this.label, required this.color});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.12),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: color.withOpacity(0.35)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, color: color, size: 13),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: color, fontWeight: FontWeight.bold, fontSize: 13),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
class _AppBarControls extends StatelessWidget {
|
|
final HomeCaptainController ctrl;
|
|
const _AppBarControls({required this.ctrl});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: _Token.surfaceCard(context),
|
|
borderRadius: BorderRadius.circular(_Token.radiusSm),
|
|
border: Border.all(color: _Token.border(context)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Navigation
|
|
_IconBtn(
|
|
icon: Icons.map_rounded,
|
|
color: _Token.success,
|
|
tooltip: 'Navigation'.tr,
|
|
onTap: () => Get.to(() => const NavigationView()),
|
|
),
|
|
// Heatmap
|
|
GetBuilder<HomeCaptainController>(
|
|
builder: (c) => _IconBtn(
|
|
icon: Icons.local_fire_department_rounded,
|
|
color:
|
|
c.isHeatmapVisible ? Colors.orange : Colors.grey.shade600,
|
|
tooltip: 'Heatmap'.tr,
|
|
onTap: c.toggleHeatmap,
|
|
),
|
|
),
|
|
// Center on me
|
|
_IconBtn(
|
|
icon: Icons.my_location_rounded,
|
|
color: _Token.accent,
|
|
tooltip: 'My Location'.tr,
|
|
onTap: () {
|
|
ctrl.mapHomeCaptainController?.animateCamera(
|
|
CameraUpdate.newLatLngZoom(
|
|
Get.find<LocationController>().myLocation,
|
|
17.5,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
class _IconBtn extends StatelessWidget {
|
|
final IconData icon;
|
|
final Color color;
|
|
final String tooltip;
|
|
final VoidCallback onTap;
|
|
const _IconBtn(
|
|
{required this.icon,
|
|
required this.color,
|
|
required this.tooltip,
|
|
required this.onTap});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Tooltip(
|
|
message: tooltip,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
|
|
child: Icon(icon, color: color, size: 20),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// Map View
|
|
// ─────────────────────────────────────────────
|
|
class _MapView extends StatelessWidget {
|
|
const _MapView();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GetBuilder<HomeCaptainController>(
|
|
builder: (ctrl) {
|
|
if (ctrl.isLoading) return const MyCircularProgressIndicator();
|
|
final s = Get.find<SettingController>();
|
|
|
|
return GetBuilder<LocationController>(
|
|
builder: (loc) => IntaleqMap(
|
|
apiKey: Env.mapSaasKey,
|
|
onMapCreated: ctrl.onMapCreated,
|
|
minMaxZoomPreference: const MinMaxZoomPreference(6, 18),
|
|
initialCameraPosition: CameraPosition(
|
|
target: (loc.myLocation.latitude == 0 ||
|
|
loc.myLocation.latitude.isNaN)
|
|
? ctrl.myLocation
|
|
: loc.myLocation,
|
|
zoom: 15,
|
|
),
|
|
mapType:
|
|
s.isMapDarkMode ? IntaleqMapType.normal : IntaleqMapType.light,
|
|
polygons: ctrl.heatmapPolygons,
|
|
markers: {
|
|
Marker(
|
|
markerId: MarkerId('MyLocation'.tr),
|
|
position: loc.myLocation,
|
|
rotation: loc.heading,
|
|
flat: true,
|
|
anchor: const Offset(0.5, 0.5),
|
|
icon: ctrl.carIcon,
|
|
)
|
|
},
|
|
myLocationButtonEnabled: false,
|
|
myLocationEnabled: false,
|
|
compassEnabled: false,
|
|
zoomControlsEnabled: false,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// Bottom Status Bar — no blur, solid card
|
|
// ─────────────────────────────────────────────
|
|
class _BottomStatusBar extends StatelessWidget {
|
|
const _BottomStatusBar();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Positioned(
|
|
bottom: 12,
|
|
left: 12,
|
|
right: 12,
|
|
child: GestureDetector(
|
|
onTap: () => _showDetailsSheet(context),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: _Token.surfaceCard(context),
|
|
borderRadius: BorderRadius.circular(_Token.radius),
|
|
border: Border.all(color: _Token.border(context)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.35),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 4),
|
|
)
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Online toggle
|
|
const ConnectWidget(),
|
|
const Spacer(),
|
|
// Ride count
|
|
GetBuilder<HomeCaptainController>(
|
|
builder: (c) => _StatChip(
|
|
icon: Icons.directions_car_rounded,
|
|
value: c.countRideToday,
|
|
label: 'Rides'.tr,
|
|
color: _Token.info,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
// Today earnings
|
|
GetBuilder<HomeCaptainController>(
|
|
builder: (c) => _StatChip(
|
|
icon: Entypo.wallet,
|
|
value: c.totalMoneyToday.toString(),
|
|
label: 'Today'.tr,
|
|
color: _Token.success,
|
|
),
|
|
),
|
|
// Chevron hint
|
|
const SizedBox(width: 8),
|
|
const Icon(Icons.keyboard_arrow_up_rounded,
|
|
color: Colors.grey, size: 18),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showDetailsSheet(BuildContext context) {
|
|
final ctrl = Get.find<HomeCaptainController>();
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (_) => _DetailsSheet(ctrl),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _StatChip extends StatelessWidget {
|
|
final IconData icon;
|
|
final String value;
|
|
final String label;
|
|
final Color color;
|
|
const _StatChip(
|
|
{required this.icon,
|
|
required this.value,
|
|
required this.label,
|
|
required this.color});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(icon, color: color, size: 16),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 15),
|
|
),
|
|
],
|
|
),
|
|
Text(label,
|
|
style: const TextStyle(color: Colors.white38, fontSize: 11)),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// Details Bottom Sheet — replaces AlertDialog
|
|
// ─────────────────────────────────────────────
|
|
class _DetailsSheet extends StatelessWidget {
|
|
final HomeCaptainController ctrl;
|
|
const _DetailsSheet(this.ctrl);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: _Token.surfaceCard(context),
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Drag handle
|
|
Container(
|
|
width: 36,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: _Token.border(context),
|
|
borderRadius: BorderRadius.circular(2)),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Your Activity'.tr,
|
|
style: TextStyle(
|
|
color: _Token.text(context),
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Divider(color: _Token.border(context), height: 1),
|
|
const SizedBox(height: 16),
|
|
_SheetRow(
|
|
icon: Entypo.wallet,
|
|
color: _Token.success,
|
|
label: 'Today'.tr,
|
|
value: ctrl.totalMoneyToday.toString(),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_SheetRow(
|
|
icon: Entypo.wallet,
|
|
color: _Token.accent,
|
|
label: AppInformation.appName,
|
|
value: ctrl.totalMoneyInSEFER.toString(),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Divider(color: _Token.border(context), height: 1),
|
|
const SizedBox(height: 12),
|
|
_SheetRow(
|
|
icon: Icons.timer_outlined,
|
|
color: _Token.success,
|
|
label: 'Active Duration'.tr,
|
|
value: ctrl.stringActiveDuration,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_SheetRow(
|
|
icon: Icons.access_time_rounded,
|
|
color: _Token.info,
|
|
label: 'Total Connection'.tr,
|
|
value: ctrl.totalDurationToday,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Divider(color: _Token.border(context), height: 1),
|
|
const SizedBox(height: 12),
|
|
_SheetRow(
|
|
icon: Icons.star_rounded,
|
|
color: _Token.accent,
|
|
label: 'Total Points'.tr,
|
|
value: ctrl.totalPoints.toString(),
|
|
),
|
|
const SizedBox(height: 20),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: TextButton(
|
|
onPressed: () => Get.back(),
|
|
style: TextButton.styleFrom(
|
|
backgroundColor: _Token.accentSoft(context),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(_Token.radiusSm)),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
child: Text('Close'.tr,
|
|
style: const TextStyle(
|
|
color: _Token.accent, fontWeight: FontWeight.bold)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SheetRow extends StatelessWidget {
|
|
final IconData icon;
|
|
final Color color;
|
|
final String label;
|
|
final String value;
|
|
const _SheetRow(
|
|
{required this.icon,
|
|
required this.color,
|
|
required this.label,
|
|
required this.value});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Row(
|
|
children: [
|
|
Icon(icon, color: color, size: 20),
|
|
const SizedBox(width: 12),
|
|
Text(label,
|
|
style: TextStyle(color: _Token.textDim(context), fontSize: 14)),
|
|
const Spacer(),
|
|
Text(value,
|
|
style: TextStyle(
|
|
color: color, fontSize: 16, fontWeight: FontWeight.bold)),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// Floating Action Buttons (right side)
|
|
// ─────────────────────────────────────────────
|
|
class _FloatingControls extends StatelessWidget {
|
|
const _FloatingControls({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Positioned(
|
|
bottom: 88,
|
|
right: 12,
|
|
child: GetBuilder<HomeCaptainController>(
|
|
builder: (ctrl) => Column(
|
|
children: [
|
|
// Bubble/overlay mode (Android only)
|
|
if (Platform.isAndroid) ...[
|
|
_Fab(
|
|
onTap: () =>
|
|
Bubble().startBubbleHead(sendAppToBackground: true),
|
|
tooltip: 'Overlay'.tr,
|
|
child: Image.asset('assets/images/logo1.png',
|
|
width: 26, height: 26),
|
|
),
|
|
const SizedBox(height: 10),
|
|
],
|
|
// Available rides
|
|
_Fab(
|
|
onTap: () => Get.to(() => const AvailableRidesPage()),
|
|
icon: Icons.list_alt_rounded,
|
|
color: AppColor.primaryColor,
|
|
tooltip: 'Available Rides'.tr,
|
|
),
|
|
// Continue active ride
|
|
if (box.read(BoxName.rideStatus) == 'Applied' ||
|
|
box.read(BoxName.rideStatus) == 'Begin') ...[
|
|
const SizedBox(height: 10),
|
|
_Fab(
|
|
onTap: () {
|
|
if (box.read(BoxName.rideStatus) == 'Applied') {
|
|
Get.to(() => PassengerLocationMapPage(),
|
|
arguments: box.read(BoxName.rideArguments));
|
|
Get.put(MapDriverController())
|
|
.changeRideToBeginToPassenger();
|
|
} else {
|
|
Get.to(() => PassengerLocationMapPage(),
|
|
arguments: box.read(BoxName.rideArguments));
|
|
Get.put(MapDriverController()).startRideFromStartApp();
|
|
}
|
|
},
|
|
icon: Icons.navigation_rounded,
|
|
color: _Token.info,
|
|
tooltip: 'Continue Ride'.tr,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Fab extends StatelessWidget {
|
|
final VoidCallback onTap;
|
|
final IconData? icon;
|
|
final Widget? child;
|
|
final Color? color;
|
|
final String? tooltip;
|
|
const _Fab(
|
|
{required this.onTap, this.icon, this.child, this.color, this.tooltip});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Tooltip(
|
|
message: tooltip ?? '',
|
|
child: Material(
|
|
color: color ?? _Token.surfaceCard(context),
|
|
shape: const CircleBorder(),
|
|
elevation: 6,
|
|
shadowColor: (color ?? Colors.black).withOpacity(0.4),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
customBorder: const CircleBorder(),
|
|
child: SizedBox(
|
|
width: 52,
|
|
height: 52,
|
|
child: Center(
|
|
child: child ??
|
|
Icon(icon,
|
|
color: color != null ? Colors.white : _Token.accent,
|
|
size: 24),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// Helper
|
|
// ─────────────────────────────────────────────
|
|
Future<void> checkForAppliedRide(BuildContext context) async {
|
|
checkForPendingOrderFromServer();
|
|
}
|