first commit
This commit is contained in:
361
lib/main.dart
Normal file
361
lib/main.dart
Normal file
@@ -0,0 +1,361 @@
|
||||
import 'dart:io';
|
||||
import 'package:miler/helpers/http_overrides.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:miler/helpers/app_bootstrap.dart';
|
||||
import 'package:miler/views/offline/offline_banner.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/controllers/profile_controller.dart';
|
||||
import 'package:miler/controllers/riderlog.dart';
|
||||
import 'package:miler/controllers/pickup.dart';
|
||||
import 'package:miler/controllers/logcontroller.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:miler/providers/notifications/notificationservce.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:new_version_plus/new_version_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:miler/background/backgroundservice.dart';
|
||||
import 'package:miler/helpers/shift_end_alarm.dart';
|
||||
import 'package:miler/views/helpers/constants/app_theme.dart';
|
||||
import 'package:miler/views/helpers/widgets/page_transitions.dart';
|
||||
|
||||
import 'views/offline/offline_page.dart';
|
||||
|
||||
// Background message handler
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
await Firebase.initializeApp();
|
||||
await NotificationServce.display(message);
|
||||
}
|
||||
|
||||
String currentVersion = '';
|
||||
String storeVersion = '';
|
||||
bool updateRequired = false;
|
||||
|
||||
Future<void> getAppVersion() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
String version = packageInfo.version;
|
||||
currentVersion = version;
|
||||
prefs.setString('CurrentVersion', currentVersion);
|
||||
debugPrint('Current version from main: $currentVersion');
|
||||
}
|
||||
|
||||
Future<void> checkForUpdate(BuildContext context) async {
|
||||
// Version check is handled in splash screen
|
||||
}
|
||||
|
||||
Future<bool> recheckVersion() async {
|
||||
final newVersion = NewVersionPlus(
|
||||
iOSId: '284882215',
|
||||
androidId: "com.doormile.partner",
|
||||
);
|
||||
|
||||
try {
|
||||
final status = await newVersion.getVersionStatus();
|
||||
if (status != null) {
|
||||
debugPrint(
|
||||
"Recheck - Current: ${status.localVersion}, Store: ${status.storeVersion}",
|
||||
);
|
||||
return status.canUpdate;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error rechecking version: $e");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
HttpOverrides.global = MyHttpOverrides();
|
||||
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
||||
} catch (e) {
|
||||
debugPrint('[FIREBASE] Initialization failed: $e');
|
||||
// Firebase may not be configured for this platform (e.g. iOS simulator without GoogleService-Info.plist)
|
||||
}
|
||||
|
||||
// Loaded at boot, not on first visit to Account: the Home app bar draws the
|
||||
// rider's photo, so the controller has to be populated before the first
|
||||
// frame or the avatar flashes its letter fallback on every launch.
|
||||
await Get.put(ProfileController(), permanent: true).loadFromPrefs();
|
||||
Get.put(RiderLogController(), permanent: true);
|
||||
Get.put(PickupController(), permanent: true);
|
||||
final logController = Get.put(LogController(), permanent: true);
|
||||
|
||||
await logController.startLogging();
|
||||
|
||||
_applySystemChrome();
|
||||
|
||||
runApp(const _RootApp());
|
||||
}
|
||||
|
||||
/// Paints the status and navigation bars to match the page behind them.
|
||||
void _applySystemChrome() {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: ColorConstants.surface,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: ColorConstants.surface,
|
||||
systemNavigationBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _RootApp extends StatefulWidget {
|
||||
const _RootApp();
|
||||
|
||||
@override
|
||||
State<_RootApp> createState() => _RootAppState();
|
||||
}
|
||||
|
||||
class _RootAppState extends State<_RootApp> with WidgetsBindingObserver {
|
||||
List<ConnectivityResult> _connectionStatus = [ConnectivityResult.none];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
Connectivity().onConnectivityChanged.listen((result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_connectionStatus = result;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Connectivity().checkConnectivity().then((result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_connectionStatus = result;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Check if shift ended while app was killed OR if shift end time has already passed
|
||||
_checkShiftEndOnStartup();
|
||||
|
||||
// Decides banner-vs-blocker only. See [_hasSession].
|
||||
_readSessionPresence();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Check if shift ended while app was killed OR if shift end time has already passed
|
||||
/// This will trigger break log immediately if rider is on duty and shift end time passed
|
||||
Future<void> _checkShiftEndOnStartup() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final int onduty = prefs.getInt('onduty') ?? 0;
|
||||
|
||||
// Only check if rider was on duty
|
||||
if (onduty != 1) {
|
||||
debugPrint('[APP_STARTUP] Rider not on duty, skipping shift end check');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if shift has ended
|
||||
final String endTimeStr = prefs.getString('endtime') ?? '';
|
||||
if (endTimeStr.isEmpty) {
|
||||
debugPrint('[APP_STARTUP] No endtime found, skipping shift end check');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[APP_STARTUP] Checking shift end - Current time: ${DateTime.now()}, End time: $endTimeStr',
|
||||
);
|
||||
|
||||
// Use the same logic as BackgroundCollectionLog.checkShiftEnd()
|
||||
final now = DateTime.now();
|
||||
final endParts = endTimeStr.split(':');
|
||||
if (endParts.length < 2) return;
|
||||
|
||||
final int endHour = int.tryParse(endParts[0]) ?? 0;
|
||||
final int endMinute = int.tryParse(endParts[1]) ?? 0;
|
||||
final int endSecond = endParts.length > 2
|
||||
? (int.tryParse(endParts[2]) ?? 0)
|
||||
: 0;
|
||||
|
||||
final DateTime endToday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
endHour,
|
||||
endMinute,
|
||||
endSecond,
|
||||
);
|
||||
|
||||
bool isShiftOver = false;
|
||||
final String startTimeStr = prefs.getString('starttime') ?? '';
|
||||
|
||||
if (startTimeStr.isNotEmpty) {
|
||||
final startParts = startTimeStr.split(':');
|
||||
if (startParts.length >= 2) {
|
||||
final int startHour = int.tryParse(startParts[0]) ?? 0;
|
||||
final int startMinute = int.tryParse(startParts[1]) ?? 0;
|
||||
|
||||
final double startVal = startHour + (startMinute / 60.0);
|
||||
final double endVal = endHour + (endMinute / 60.0);
|
||||
|
||||
if (startVal > endVal) {
|
||||
// Overnight shift
|
||||
final DateTime startToday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
startHour,
|
||||
startMinute,
|
||||
);
|
||||
|
||||
if (now.isAfter(endToday) && now.isBefore(startToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
} else {
|
||||
// Normal day shift
|
||||
if (now.isAfter(endToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (now.isAfter(endToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (now.isAfter(endToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isShiftOver) {
|
||||
debugPrint(
|
||||
'[APP_STARTUP] ⏰ Shift end time ($endTimeStr) has already passed - creating break log IMMEDIATELY...',
|
||||
);
|
||||
// Call the background service to create break log
|
||||
await BackgroundCollectionLog.checkShiftEnd();
|
||||
debugPrint('[APP_STARTUP] ✅ Break log created (if needed)');
|
||||
} else {
|
||||
debugPrint(
|
||||
'[APP_STARTUP] Shift end time ($endTimeStr) has not passed yet - scheduling alarm',
|
||||
);
|
||||
// Also reschedule the alarm in case it was cancelled
|
||||
try {
|
||||
final String startTime = prefs.getString('starttime') ?? '';
|
||||
await ShiftEndAlarm.scheduleAlarm(
|
||||
endTime: endTimeStr,
|
||||
startTime: startTime,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[APP_STARTUP] Error rescheduling alarm: $e');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[APP_STARTUP] Error checking shift end: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
// When app comes back to foreground, ensure rider logs resume
|
||||
try {
|
||||
if (Get.isRegistered<RiderLogController>()) {
|
||||
final ctl = Get.find<RiderLogController>();
|
||||
// Fire an immediate rider log (best-effort)
|
||||
ctl.createLoginNowV2();
|
||||
// Ensure the periodic background/foreground loop is running
|
||||
ctl.startAutoCreateLoginLoop();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore lifecycle errors; app should not crash because of logging
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isOffline {
|
||||
return _connectionStatus.isEmpty ||
|
||||
_connectionStatus.every((status) => status == ConnectivityResult.none);
|
||||
}
|
||||
|
||||
/// Whether there is a signed-in rider whose work is already on the device.
|
||||
///
|
||||
/// Read once, purely to choose between two *presentations* of being offline —
|
||||
/// it changes no auth behaviour and no routing. [AppBootstrap] still decides
|
||||
/// where the app actually goes.
|
||||
bool _hasSession = false;
|
||||
|
||||
Future<void> _readSessionPresence() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final loggedOut = prefs.getBool('logged_out') ?? false;
|
||||
final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
if (mounted) setState(() => _hasSession = !loggedOut && userId != 0);
|
||||
} catch (_) {
|
||||
// Unknown → treat as no session, which is the more cautious of the two
|
||||
// presentations. It only affects which offline UI is shown.
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ⭐ RESPONSIVE WRAPPER ADDED
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(390, 844), // YOUR BASE SIZE
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder: (_, __) {
|
||||
return GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Miler partner',
|
||||
// Smooth, consistent page motion across the whole app (Uber-style
|
||||
// horizontal slide with a gentle ease) instead of abrupt cuts.
|
||||
// Screens navigated to by hand use `openScreen`/`openSheet` from
|
||||
// views/helpers/widgets/page_transitions.dart; this covers the
|
||||
// whole-app swaps (`Get.offAll`) that don't go through them.
|
||||
defaultTransition: Transition.cupertino,
|
||||
transitionDuration: kMilerPushIn,
|
||||
theme: AppTheme.light,
|
||||
home: const AppBootstrap(),
|
||||
|
||||
// ── Offline is a strip, not a wall ──
|
||||
//
|
||||
// This used to stack an opaque [OfflinePage] over the whole app the
|
||||
// moment connectivity dropped — so a rider at a doorstep in a
|
||||
// basement lost the address, the customer, the parcel count and the
|
||||
// OTP field while holding the parcel. Everything he needs at the door
|
||||
// was already fetched; the network is required to *report* the stop,
|
||||
// not to work it.
|
||||
//
|
||||
// So a signed-in rider keeps his screen and gets a line of text above
|
||||
// it. The full-screen page survives only for a cold start with no
|
||||
// session, where there genuinely is nothing to show him.
|
||||
builder: (context, child) {
|
||||
final baseChild = child ?? const SizedBox.shrink();
|
||||
|
||||
if (_isOffline && !_hasSession) {
|
||||
return Stack(children: [baseChild, const OfflinePage()]);
|
||||
}
|
||||
|
||||
return OfflineScope(offline: _isOffline, child: baseChild);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user