diff --git a/latest-releases.json b/latest-releases.json index f7cc517..d84fd53 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,3 @@ { - "alpha": "1.0.0-dev.9" + "alpha": "1.0.0-dev.10" } diff --git a/lib/consts.dart b/lib/consts.dart index 0a57556..3ee858c 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:geolocator/geolocator.dart'; @@ -6,7 +7,8 @@ import 'package:geolocator/geolocator.dart'; class TTConsts { static get UPDATE_URL => "https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json"; - static const VERSION = "1.0.0-dev.9"; + static const VERSION = "1.0.0-dev.10"; + static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; static final LocationSettings LOCATION_SETTINGS = LocationSettings( diff --git a/lib/data.dart b/lib/data.dart index af2d0b2..fdce05f 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -1,15 +1,14 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math' as math; +import 'dart:ui'; import 'package:geolocator/geolocator.dart'; -import 'package:libac_dart/utils/TimeUtils.dart'; +import 'package:libac_dart/nbt/Stream.dart'; import 'package:timetrack/consts.dart'; class SessionData { - static int StartTime = 0; - - static get StartTimeAsDateTime => - DateTime.fromMillisecondsSinceEpoch(StartTime * 1000); + static DateTime StartTime = DateTime(0); static bool IsOnTheClock = false; @@ -19,12 +18,121 @@ class SessionData { static Trip? currentTrip; static List positions = []; static late StreamSubscription _listener; + static Callbacks Calls = Callbacks(); /// This flag is usually set when data is loaded from a saved state. Or when accessed using the Web version of the app. static bool IsReadOnly = false; + static double GetTotalBasePay() { + double total = 0; + for (var trip in Trips) { + total += trip.BasePay; + } + + return total; + } + + static double GetTotalTips() { + double total = 0; + for (var trip in Trips) { + for (var drop in trip.deliveries) { + total += drop.TipAmount; + } + } + + return total; + } + + static double GetTotalPay() { + return GetTotalBasePay() + GetTotalTips(); + } + + static double GetTotalMiles() { + double total = 0; + total = _totalMilesTraveled( + positions, + minDistanceMeters: 5, + maxDistanceMeters: 512, + ); + return total; + } + + static String GetTotalMilesAsString() { + double miles = GetTotalMiles(); + if (miles == 0) return "0.0"; + List split = miles.toString().split("."); + StringBuilder out = StringBuilder(); + + out.append(split[0]); + out.append("."); + out.append(split[1].substring(0, 4)); + + return out.toString(); + } + + //** Begin AI Generated code */ + /// Converts meters → miles. + static const _metersPerMile = 1609.344; + + /// Radius of the earth in meters (WGS‑84 mean radius). + static const _earthRadiusM = 6371000.0; + static double _haversineMeters( + double lat1, + double lon1, + double lat2, + double lon2, + ) { + final dLat = _deg2rad(lat2 - lat1); + final dLon = _deg2rad(lon2 - lon1); + + final a = + math.sin(dLat / 2) * math.sin(dLat / 2) + + math.cos(_deg2rad(lat1)) * + math.cos(_deg2rad(lat2)) * + math.sin(dLon / 2) * + math.sin(dLon / 2); + + return _earthRadiusM * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + } + + static double _deg2rad(double deg) => deg * math.pi / 180.0; + + /// Returns total miles traveled after basic GPS‑noise cleanup. + /// + /// * [minDistanceMeters] – drop segments shorter than this (jitter). + /// * [maxDistanceMeters] – drop segments longer than this (impossible jump). + static double _totalMilesTraveled( + List positions, { + double minDistanceMeters = 5, + double? maxDistanceMeters, + }) { + if (positions.length < 2) return 0; + + var meters = 0.0; + + for (var i = 1; i < positions.length; i++) { + final p1 = positions[i - 1]; + final p2 = positions[i]; + + final d = _haversineMeters( + p1.latitude, + p1.longitude, + p2.latitude, + p2.longitude, + ); + + if (d < minDistanceMeters) continue; // too small → jitter + if (maxDistanceMeters != null && d > maxDistanceMeters) + continue; // glitch + + meters += d; + } + return meters / _metersPerMile; + } + //** End AI Generated code */ + static Future Login() async { - StartTime = TimeUtils.getUnixTimestamp(); + StartTime = DateTime.now(); IsOnTheClock = true; bool hasGPS; @@ -60,6 +168,8 @@ class SessionData { return; } positions.add(pos); + + SessionData.Calls.dispatch(); }); } @@ -226,3 +336,17 @@ class Trip { return trip; } } + +class Callbacks { + VoidCallback? HomeCallback; + VoidCallback? MapCallback; + VoidCallback? UpdateSettingsCallback; + VoidCallback? WorkDataCallback; + + void dispatch() { + if (HomeCallback != null) HomeCallback!(); + if (MapCallback != null) MapCallback!(); + if (UpdateSettingsCallback != null) UpdateSettingsCallback!(); + if (WorkDataCallback != null) WorkDataCallback!(); + } +} diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index 217fa73..c63d778 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -20,6 +20,22 @@ class _HomePageState extends State { super.didChangeDependencies(); } + @override + void initState() { + SessionData.Calls.HomeCallback = call; + super.initState(); + } + + void call() { + setState(() {}); + } + + @override + void dispose() { + SessionData.Calls.HomeCallback = null; + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -63,15 +79,29 @@ class _HomePageState extends State { await Navigator.pushNamed(context, "/map"); }, ), + ListTile( + title: Text("Work Data"), + subtitle: Text("View and edit work data"), + leading: Icon(Icons.work_history), + onTap: () async { + // Open up the work data viewer and editor. + // Edit will be disabled for web or read only mode. + await Navigator.pushNamed(context, "/work"); + setState(() {}); + }, + ), ListTile( title: Text("RESET APP SESSION"), onTap: () async { setState(() { SessionData.IsOnTheClock = false; - SessionData.StartTime = 0; + SessionData.StartTime = DateTime.fromMillisecondsSinceEpoch( + 0, + ); SessionData.Trips = []; SessionData.currentDelivery = null; SessionData.currentTrip = null; + SessionData.positions = []; }); }, ), @@ -233,7 +263,7 @@ class _HomePageState extends State { return Column( children: [ Text( - "You are now on the clock\nYour location is being tracked for record keeping purposes.\n\nYou started at ${SessionData.StartTimeAsDateTime}\n\n", + "You are now on the clock\nYour location is being tracked for record keeping purposes.\n\nYou started at ${SessionData.StartTime.toLocal()}\n\n", style: TextStyle(fontSize: 18), ), if (SessionData.currentTrip != null) GetTripWidgets(), diff --git a/lib/pages/MainApp.dart b/lib/pages/MainApp.dart index 1da68e6..4a01ae0 100644 --- a/lib/pages/MainApp.dart +++ b/lib/pages/MainApp.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:timetrack/pages/HomePage.dart'; import 'package:timetrack/pages/MapPage.dart'; import 'package:timetrack/pages/UpdateSettings.dart'; +import 'package:timetrack/pages/WorkData.dart'; class MainApp extends StatefulWidget { const MainApp({super.key}); @@ -26,6 +27,7 @@ class MainAppState extends State { "/": (ctx) => HomePage(), "/upd": (ctx) => UpdateSettingsPage(), "/map": (ctx) => MapPage(), + "/work": (ctx) => WorkDataPage(), }, theme: ThemeData.dark(), ); diff --git a/lib/pages/MapPage.dart b/lib/pages/MapPage.dart index 16d55fb..fc05285 100644 --- a/lib/pages/MapPage.dart +++ b/lib/pages/MapPage.dart @@ -19,6 +19,20 @@ class _MapPage extends State { LatLng initialPosition = LatLng(0, 0); List PointMap = []; List Markers = []; + bool autorefresh = true; + + @override + void initState() { + SessionData.Calls.MapCallback = call; + super.initState(); + } + + void call() { + if (autorefresh) { + didChangeDependencies(); + } + setState(() {}); + } @override void didChangeDependencies() { @@ -80,7 +94,8 @@ class _MapPage extends State { @override void dispose() { - print("Map page disposed"); + SessionData.Calls.MapCallback = null; + controller.dispose(); super.dispose(); } @@ -98,6 +113,15 @@ class _MapPage extends State { }, icon: Icon(Icons.refresh), ), + IconButton( + onPressed: () { + autorefresh = !autorefresh; + }, + icon: + autorefresh + ? Icon(Icons.play_disabled) + : Icon(Icons.play_circle), + ), ], ), body: GestureDetector( @@ -122,7 +146,7 @@ class _MapPage extends State { Polyline( points: PointMap, color: Colors.blue, - borderStrokeWidth: 3, + borderStrokeWidth: 8, borderColor: Colors.blue, ), ], diff --git a/lib/pages/UpdateSettings.dart b/lib/pages/UpdateSettings.dart index 7bb0b25..2922068 100644 --- a/lib/pages/UpdateSettings.dart +++ b/lib/pages/UpdateSettings.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; import 'package:libacflutter/Constants.dart'; import 'package:ota_update/ota_update.dart'; import 'package:timetrack/consts.dart'; +import 'package:timetrack/data.dart'; class UpdateSettingsPage extends StatefulWidget { const UpdateSettingsPage({super.key}); @@ -14,6 +15,22 @@ class UpdateSettingsPage extends StatefulWidget { } class _UpdSet extends State { + @override + void initState() { + SessionData.Calls.UpdateSettingsCallback = call; + super.initState(); + } + + @override + void dispose() { + SessionData.Calls.UpdateSettingsCallback = null; + super.dispose(); + } + + void call() { + setState(() {}); + } + @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/pages/WorkData.dart b/lib/pages/WorkData.dart new file mode 100644 index 0000000..eeac3fe --- /dev/null +++ b/lib/pages/WorkData.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:libacflutter/Constants.dart'; +import 'package:timetrack/consts.dart'; +import 'package:timetrack/data.dart'; + +class WorkDataPage extends StatefulWidget { + WorkDataPage({super.key}); + + @override + State createState() { + return _WorkData(); + } +} + +class _WorkData extends State { + void call() { + setState(() {}); + } + + @override + void initState() { + SessionData.Calls.WorkDataCallback = call; + super.initState(); + } + + @override + void dispose() { + SessionData.Calls.WorkDataCallback = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Time Tracker - Work Data"), + backgroundColor: LibACFlutterConstants.TITLEBAR_COLOR, + ), + body: Padding( + padding: EdgeInsets.all(8), + child: SingleChildScrollView( + child: Column( + children: [ + // This is where we'll display all the work data, like total earnings, and present a editor + Text( + "Total saved GPS Positions: ${SessionData.positions.length}", + style: TextStyle(fontSize: 18), + ), + Text( + "Start Date & Time: ${SessionData.StartTime.toString()}", + style: TextStyle(fontSize: 18), + ), + SizedBox(height: 20), + Text( + "Total Trips: ${SessionData.Trips.length}", + style: TextStyle(fontSize: 18), + ), + Text( + "Total Base Pay: \$${SessionData.GetTotalBasePay()}", + style: TextStyle(fontSize: 18), + ), + Text( + "Total Tips: \$${SessionData.GetTotalTips()}", + style: TextStyle(fontSize: 18), + ), + Text( + "Total Earnings: \$${SessionData.GetTotalPay()}", + style: TextStyle(fontSize: 18), + ), + SizedBox(height: 20), + Text( + "Total Miles: ${SessionData.GetTotalMilesAsString()}", + style: TextStyle(fontSize: 24), + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 8dbead9..1eb0ca2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-dev.9 +version: 1.0.0-dev.10 environment: sdk: ^3.7.2