933 lines
25 KiB
Dart
933 lines
25 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
import 'dart:math' as math;
|
||
import 'dart:typed_data';
|
||
import 'dart:ui';
|
||
|
||
import 'package:dio/dio.dart';
|
||
import 'package:floating_window_android/floating_window_android.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_background/flutter_background.dart';
|
||
import 'package:geolocator/geolocator.dart';
|
||
import 'package:libac_dart/nbt/NbtIo.dart';
|
||
import 'package:libac_dart/nbt/NbtUtils.dart';
|
||
import 'package:libac_dart/nbt/SnbtIo.dart';
|
||
import 'package:libac_dart/nbt/Stream.dart';
|
||
import 'package:libac_dart/nbt/impl/CompoundTag.dart';
|
||
import 'package:libac_dart/nbt/impl/DoubleTag.dart';
|
||
import 'package:libac_dart/nbt/impl/ListTag.dart';
|
||
import 'package:libac_dart/nbt/impl/StringTag.dart';
|
||
import 'package:libac_dart/utils/Converter.dart';
|
||
import 'package:libac_dart/utils/TimeUtils.dart';
|
||
import 'package:libacflutter/nbt/nbtHelpers.dart';
|
||
import 'package:timetrack/consts.dart';
|
||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||
|
||
class SessionData {
|
||
static DateTime StartTime = DateTime(0);
|
||
static DateTime EndTime = DateTime(0);
|
||
|
||
static bool IsOnTheClock = false;
|
||
|
||
static List<Trip> Trips = [];
|
||
|
||
static Delivery? currentDelivery;
|
||
static Trip? currentTrip;
|
||
static List<SmallPosition> positions = [];
|
||
static late StreamSubscription<Position> _listener;
|
||
static Callbacks Calls = Callbacks();
|
||
static String LastSessionID = "";
|
||
static String DisplayError = "";
|
||
static String DisplayMessage = "";
|
||
static bool DirtyState = false;
|
||
static double? TotalPay;
|
||
static bool ContainsTripTimes = true;
|
||
static bool IsSavedData = false;
|
||
static String SaveDataType = "";
|
||
|
||
/// This indicates whether the app is in a live session.
|
||
static bool Recording = false;
|
||
|
||
/// This is the version number of the recording as specified by the server.
|
||
static int RecordingVersion = 0;
|
||
|
||
/// This is used for the patch method detection. Do not edit this value, it will be overwritten.
|
||
static double LastTotalMiles = 0.0;
|
||
|
||
/// Is true if the try-catch is tripped or if not running on Android
|
||
static bool isWeb = false;
|
||
|
||
/// 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 GetTotalMiles() {
|
||
double total = 0;
|
||
total = _totalMilesTraveled(
|
||
positions,
|
||
minDistanceMeters: 5,
|
||
maxDistanceMeters: 512,
|
||
);
|
||
return total;
|
||
}
|
||
|
||
static int GetTotalDeliveries() {
|
||
int total = 0;
|
||
for (var trip in Trips) {
|
||
total += trip.deliveries.length;
|
||
}
|
||
|
||
return total;
|
||
}
|
||
|
||
static String GetPaidHours() {
|
||
return Duration2Notation(_GetPaidHours());
|
||
}
|
||
|
||
static Duration _GetPaidHours() {
|
||
Time stamp = Time(days: 0, hours: 0, minutes: 0, seconds: 0);
|
||
print("Total trips: ${Trips.length}");
|
||
for (var trip in Trips) {
|
||
var diff = trip.EndTime.difference(trip.StartTime);
|
||
var diffTime = Time(
|
||
days: 0,
|
||
hours: 0,
|
||
minutes: 0,
|
||
seconds: diff.inSeconds,
|
||
);
|
||
stamp.add(diffTime);
|
||
print("Add timestamp; $diff; $diffTime");
|
||
}
|
||
|
||
print("Final time: $stamp");
|
||
|
||
return stamp.toDuration();
|
||
}
|
||
|
||
static String GetUnpaidHours() {
|
||
// This is the inverted value of paid hours. We get total hours and subtract it from the paid hours. This gives us the unpaid hours.
|
||
Time totalHoursWorked = Time.fromNotation(GetPaidHours());
|
||
|
||
Duration totalDuration = EndTime.difference(StartTime);
|
||
Time totalTime = Time.fromDuration(totalDuration);
|
||
|
||
Time unpaid = totalTime.copy();
|
||
unpaid.subtract(totalHoursWorked);
|
||
|
||
return unpaid.toString();
|
||
}
|
||
|
||
static String GetTotalMilesAsString() {
|
||
double miles = GetTotalMiles();
|
||
if (miles == 0) return "0.0";
|
||
List<String> 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<SmallPosition> 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 */
|
||
|
||
/// A simple function that performs some immediate actions when a action has been performed. This does not replace the dirty state handler, which can perform additional tasks, such as upload of the patch data.
|
||
static Future<void> ActionPerformed() async {
|
||
await SaveCacheState();
|
||
DirtyState = true;
|
||
}
|
||
|
||
/// This replaces _upload. This method will serialize the current data as is with no alterations. It will then upload the data to the server in a patch.
|
||
static Future<void> _performPatch() async {
|
||
var data = await _serializeToNBT();
|
||
|
||
Dio dio = Dio();
|
||
// This is where we send the data to the server. In the server reply to a patch will be the new version number. This is not currently needed.
|
||
var packet = json.encode({
|
||
"cmd": "patch",
|
||
"id": LastSessionID,
|
||
"data": base64.encode(await NbtIo.writeToStream(data)),
|
||
});
|
||
var reply = await dio.post(TTConsts.SESSION_SERVER, data: packet);
|
||
var replyData = reply.data as String;
|
||
var replyJs = json.decode(replyData);
|
||
if (replyJs['status'] == "patched") {
|
||
RecordingVersion = replyJs['version'] as int;
|
||
return;
|
||
} else {
|
||
DisplayError = "Error: Patch upload failed. Retrying...";
|
||
Timer(Duration(seconds: 5), () async {
|
||
_performPatch();
|
||
});
|
||
}
|
||
}
|
||
|
||
static Future<bool> Login() async {
|
||
final androidConfig = FlutterBackgroundAndroidConfig(
|
||
notificationTitle: "Time Tracker",
|
||
notificationText:
|
||
"Background notification for keeping TimeTrack running in the background",
|
||
notificationImportance: AndroidNotificationImportance.normal,
|
||
);
|
||
|
||
bool success = await FlutterBackground.initialize(
|
||
androidConfig: androidConfig,
|
||
);
|
||
|
||
FlutterBackground.enableBackgroundExecution();
|
||
WakelockPlus.enable();
|
||
|
||
if (!success) {
|
||
return false;
|
||
}
|
||
|
||
bool granted = await FloatingWindowAndroid.isPermissionGranted();
|
||
if (!granted) {
|
||
success = await FloatingWindowAndroid.requestPermission();
|
||
|
||
if (!success) return false;
|
||
}
|
||
|
||
StartTime = DateTime.now();
|
||
IsOnTheClock = true;
|
||
|
||
bool hasGPS;
|
||
|
||
LocationPermission perm;
|
||
hasGPS = await Geolocator.isLocationServiceEnabled();
|
||
if (!hasGPS) {
|
||
IsOnTheClock = false;
|
||
return await Future.error("Location services are disabled");
|
||
}
|
||
|
||
perm = await Geolocator.checkPermission();
|
||
if (perm == LocationPermission.denied) {
|
||
perm = await Geolocator.requestPermission();
|
||
if (perm == LocationPermission.denied) {
|
||
IsOnTheClock = false;
|
||
return await Future.error("Location permissions are denied");
|
||
}
|
||
}
|
||
|
||
if (perm == LocationPermission.deniedForever) {
|
||
IsOnTheClock = false;
|
||
return await Future.error(
|
||
"Location permissions are denied permanently. Login cannot proceed.",
|
||
);
|
||
}
|
||
|
||
await _create();
|
||
|
||
_listener = Geolocator.getPositionStream(
|
||
locationSettings: TTConsts.LOCATION_SETTINGS,
|
||
).listen((pos) {
|
||
if (!IsOnTheClock) {
|
||
_listener.cancel();
|
||
return;
|
||
}
|
||
positions.add(SmallPosition.fromPosition(pos));
|
||
SessionData.SaveCacheState();
|
||
|
||
SessionData.Calls.dispatch();
|
||
|
||
var curMiles = GetTotalMiles();
|
||
if (LastTotalMiles + 0.25 < curMiles) {
|
||
_performPatch();
|
||
LastTotalMiles = curMiles;
|
||
}
|
||
});
|
||
|
||
return true;
|
||
}
|
||
|
||
static Future<void> Logout() async {
|
||
IsOnTheClock = false;
|
||
currentDelivery = null;
|
||
currentTrip = null;
|
||
_listener.cancel();
|
||
|
||
EndTime = DateTime.now();
|
||
|
||
FlutterBackground.disableBackgroundExecution();
|
||
WakelockPlus.disable();
|
||
|
||
var saveData = await _serializeToNBT();
|
||
ResetAppSession();
|
||
print(SnbtIo.writeToString(saveData));
|
||
Uint8List nbtData = await NbtIo.writeToStream(saveData);
|
||
|
||
Trips = [];
|
||
positions = [];
|
||
|
||
_upload(nbtData);
|
||
}
|
||
|
||
/// v2 Create function.
|
||
///
|
||
/// This function sets the Session ID globally. It will also set the Recording flag to true.
|
||
static Future<void> _create() async {
|
||
Dio dio = Dio();
|
||
Map<String, dynamic> payload = {"cmd": "createv2"};
|
||
|
||
try {
|
||
var reply = await dio.post(
|
||
TTConsts.SESSION_SERVER,
|
||
data: json.encode(payload),
|
||
);
|
||
|
||
if (reply.statusCode == null) {
|
||
throw Exception("Fatal error while creating");
|
||
}
|
||
if (reply.statusCode! != 200) {
|
||
throw Exception("Fatal error while creating");
|
||
}
|
||
|
||
Map<String, dynamic> replyJs = json.decode(reply.data as String);
|
||
if (replyJs["status"] == "created") {
|
||
print("Successful creation");
|
||
LastSessionID = replyJs['session'] as String;
|
||
Recording = true;
|
||
RecordingVersion = 0;
|
||
Calls.dispatch();
|
||
}
|
||
} catch (E) {
|
||
// Retry in 2 seconds
|
||
DisplayError =
|
||
"Error: Something went wrong during session creation. Retry in 5 seconds...";
|
||
Calls.dispatch();
|
||
|
||
Timer(Duration(seconds: 5), () {
|
||
_create();
|
||
});
|
||
}
|
||
}
|
||
|
||
/// Deprecated: This function uploads using the v1 API, and is intended to be called at the conclusion of data recording.
|
||
@Deprecated(
|
||
"This function utilizes the Protocol v1 Create method, which has been replaced by createv2 and patch. This function will be removed.",
|
||
)
|
||
static Future<void> _upload(List<int> nbtData) async {
|
||
Dio dio = Dio();
|
||
|
||
Map<String, dynamic> payload = {
|
||
"cmd": "create",
|
||
"type": "nbt",
|
||
"data": base64Encoder.encode(nbtData),
|
||
};
|
||
|
||
try {
|
||
var reply = await dio.post(
|
||
TTConsts.SESSION_SERVER,
|
||
data: json.encode(payload),
|
||
);
|
||
if (reply.statusCode == null) {
|
||
throw Exception("Fatal error while uploading");
|
||
}
|
||
if (reply.statusCode! != 200) {
|
||
throw Exception("Fatal error while uploading");
|
||
}
|
||
|
||
Map<String, dynamic> replyJs = json.decode(reply.data as String);
|
||
if (replyJs["status"] == "ok") {
|
||
print("Successful upload");
|
||
LastSessionID = replyJs['session'] as String;
|
||
Calls.dispatch();
|
||
}
|
||
} catch (E) {
|
||
// Retry in 2 seconds
|
||
DisplayError =
|
||
"Error: Something went wrong during upload. Retry in 5 seconds...";
|
||
Calls.dispatch();
|
||
|
||
Timer(Duration(seconds: 5), () {
|
||
_upload(nbtData);
|
||
});
|
||
}
|
||
}
|
||
|
||
static void ResetAppSession() {
|
||
IsOnTheClock = false;
|
||
StartTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||
Trips = [];
|
||
currentDelivery = null;
|
||
currentTrip = null;
|
||
positions = [];
|
||
EndTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||
LastSessionID = "";
|
||
DisplayError = "";
|
||
IsReadOnly = false;
|
||
ContainsTripTimes = true;
|
||
Recording = false;
|
||
if (FlutterBackground.isBackgroundExecutionEnabled) {
|
||
FlutterBackground.disableBackgroundExecution();
|
||
}
|
||
WakelockPlus.disable();
|
||
NBTHelper.CommitNBT(data: CompoundTag(), name: "appstate");
|
||
|
||
Calls.dispatch();
|
||
}
|
||
|
||
/// This function attempts to load the saved state from cache.
|
||
///
|
||
/// This will return true when a session exists; false when no session exists, or is empty.
|
||
static Future<bool> LoadSavedCacheState() async {
|
||
CompoundTag ct = await NBTHelper.GetNBT(name: "appstate");
|
||
// Restore various flags now.
|
||
if (ct.isEmpty) {
|
||
ResetAppSession();
|
||
return false;
|
||
}
|
||
|
||
await _deserialize(ct);
|
||
|
||
if (IsOnTheClock) ContainsTripTimes = true;
|
||
|
||
Calls.dispatch();
|
||
|
||
return true;
|
||
}
|
||
|
||
/// Saves the current session based on various factors.
|
||
static Future<void> SaveCacheState() async {
|
||
CompoundTag ct = IsOnTheClock ? await _serializeToNBT() : CompoundTag();
|
||
await NBTHelper.CommitNBT(data: ct, name: "appstate");
|
||
|
||
if (DirtyState) {
|
||
DirtyState = false;
|
||
DisplayMessage = "Saved cache state";
|
||
}
|
||
}
|
||
|
||
static Future<void> _deserialize(CompoundTag ct) async {
|
||
IsOnTheClock = NbtUtils.readBoolean(ct, "inprog");
|
||
if (ct.containsKey("record"))
|
||
Recording = NbtUtils.readBoolean(ct, "record");
|
||
else
|
||
Recording = false;
|
||
|
||
if (Recording && isWeb) {
|
||
IsOnTheClock = false;
|
||
IsReadOnly = true;
|
||
}
|
||
|
||
if (IsOnTheClock) {
|
||
await Login();
|
||
}
|
||
|
||
if (!ct.containsKey("start")) {
|
||
ResetAppSession();
|
||
return;
|
||
}
|
||
|
||
StartTime = DateTime.parse(ct.get("start")!.asString());
|
||
if (ct.containsKey("end")) {
|
||
EndTime = DateTime.parse(ct.get("end")!.asString());
|
||
} else {
|
||
EndTime = DateTime(0);
|
||
}
|
||
if (ct.containsKey("totalPay")) {
|
||
TotalPay = ct.get("totalPay")!.asDouble();
|
||
}
|
||
|
||
ListTag poses = ct.get("pos")! as ListTag;
|
||
positions.clear();
|
||
for (var pos in poses.value) {
|
||
positions.add(await SmallPosition.fromNBT(pos.asCompoundTag()));
|
||
}
|
||
|
||
ListTag trips = ct.get("trips") as ListTag;
|
||
for (var trip in trips.value) {
|
||
Trips.add(await Trip.fromNBT(trip.asCompoundTag()));
|
||
}
|
||
|
||
if (ct.containsKey("current_trip")) {
|
||
currentTrip = await Trip.fromNBT(ct.get("current_trip")!.asCompoundTag());
|
||
}
|
||
|
||
if (ct.containsKey("current_delivery")) {
|
||
currentDelivery = await Delivery.fromNBT(
|
||
ct.get("current_delivery")!.asCompoundTag(),
|
||
);
|
||
}
|
||
|
||
print("Deserialized data: ${SnbtIo.writeToString(ct)}");
|
||
}
|
||
|
||
/// This private function will turn all the data into NBT, for both the cache state, and newer usage, for storing it on the server in a more compact format.
|
||
static Future<CompoundTag> _serializeToNBT() async {
|
||
CompoundTag ct = CompoundTag();
|
||
|
||
NbtUtils.writeBoolean(ct, "inprog", IsOnTheClock);
|
||
NbtUtils.writeBoolean(ct, "record", Recording);
|
||
// No need to write the contains trip times flag, it is set during deserialization. For inprog sessions, it will be set to true by the system.
|
||
ct.put("start", StringTag.valueOf(StartTime.toIso8601String()));
|
||
if (EndTime.year > 2000) {
|
||
// We have a end time
|
||
ct.put("end", StringTag.valueOf(EndTime.toIso8601String()));
|
||
}
|
||
|
||
ListTag posX = ListTag();
|
||
int len = positions.length;
|
||
int i = 0;
|
||
for (i = 0; i < len; i++) {
|
||
var pos = positions[i];
|
||
posX.add(await pos.toNBT());
|
||
}
|
||
ct.put("pos", posX);
|
||
|
||
ListTag myTrips = ListTag();
|
||
len = Trips.length;
|
||
i = 0;
|
||
for (i = 0; i < len; i++) {
|
||
var trip = Trips[i];
|
||
myTrips.add(await trip.toNBT());
|
||
}
|
||
ct.put("trips", myTrips);
|
||
|
||
// This format supports saving current trip and current delivery.
|
||
if (currentDelivery != null) {
|
||
ct.put("current_delivery", await currentDelivery!.toNBT());
|
||
}
|
||
|
||
if (currentTrip != null) {
|
||
ct.put("current_trip", await currentTrip!.toNBT());
|
||
}
|
||
|
||
if (TotalPay != null) {
|
||
ct.put("totalPay", DoubleTag.valueOf(TotalPay!));
|
||
}
|
||
|
||
return ct;
|
||
}
|
||
|
||
static Map<String, dynamic> SaveData() {
|
||
Map<String, dynamic> saveData = {};
|
||
|
||
List<Map<String, dynamic>> trips = [];
|
||
for (var trip in Trips) {
|
||
trips.add(trip.toJsonMap());
|
||
}
|
||
|
||
List<Map<String, dynamic>> posx = [];
|
||
for (var pos in positions) {
|
||
posx.add(pos.toMap());
|
||
}
|
||
|
||
saveData["trips"] = trips;
|
||
saveData["positions"] = posx;
|
||
saveData["start"] = StartTime.toIso8601String();
|
||
saveData["end"] = EndTime.toIso8601String();
|
||
if (TotalPay != null) saveData["totalPay"] = TotalPay;
|
||
|
||
return saveData;
|
||
}
|
||
|
||
static Future<bool> DownloadData() async {
|
||
Dio dio = Dio();
|
||
IsSavedData = false;
|
||
Map<String, dynamic> payload = {"cmd": "get", "id": LastSessionID};
|
||
|
||
// Send the data, and get the response
|
||
try {
|
||
var reply = await dio.post(
|
||
TTConsts.SESSION_SERVER,
|
||
data: json.encode(payload),
|
||
);
|
||
|
||
String cType = reply.headers.value("Content-Type") ?? "application/json";
|
||
|
||
if (cType == "application/json") {
|
||
IsSavedData = true;
|
||
SaveDataType = "JSON";
|
||
return LoadData(reply.data as Map<String, dynamic>);
|
||
} else if (cType == "application/nbt") {
|
||
print("Data is in NBT Format");
|
||
Uint8List lst = base64Encoder.decode(reply.data as String);
|
||
// Convert this to a CompoundTag
|
||
CompoundTag ct = await NbtIo.readFromStream(lst) as CompoundTag;
|
||
_deserialize(ct);
|
||
|
||
IsReadOnly = true;
|
||
isWeb = true;
|
||
IsSavedData = true;
|
||
SaveDataType = "NBT";
|
||
return true;
|
||
} else
|
||
return false;
|
||
} catch (E, stack) {
|
||
print(E);
|
||
|
||
print(stack);
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static bool LoadData(Map<String, dynamic> jsMap) {
|
||
if (jsMap.containsKey("error")) {
|
||
LastSessionID = "";
|
||
return false;
|
||
}
|
||
List<dynamic> trips = jsMap['trips'] as List<dynamic>;
|
||
List<dynamic> pos = jsMap['positions'] as List<dynamic>;
|
||
|
||
for (var trip in trips) {
|
||
Trips.add(Trip.fromJsonMap(trip as Map<String, dynamic>));
|
||
}
|
||
|
||
for (var position in pos) {
|
||
positions.add(SmallPosition.fromMap(position as Map<String, dynamic>));
|
||
}
|
||
|
||
if (jsMap.containsKey("start")) {
|
||
StartTime = DateTime.parse(jsMap['start'] as String);
|
||
}
|
||
|
||
if (jsMap.containsKey("end")) {
|
||
EndTime = DateTime.parse(jsMap["end"] as String);
|
||
}
|
||
|
||
if (jsMap.containsKey("totalPay")) {
|
||
TotalPay = jsMap["totalPay"] as double;
|
||
} else {
|
||
TotalPay = null;
|
||
}
|
||
|
||
IsReadOnly = true;
|
||
|
||
return true;
|
||
}
|
||
|
||
static Future<Position> GetNewLocation() async {
|
||
Position pos = await Geolocator.getCurrentPosition(
|
||
locationSettings: TTConsts.LOCATION_SETTINGS,
|
||
);
|
||
|
||
return pos;
|
||
}
|
||
|
||
static Trip GetNewTrip() {
|
||
if (currentTrip != null) {
|
||
currentTrip!.EndTime = DateTime.now();
|
||
}
|
||
currentTrip = Trip();
|
||
Trips.add(currentTrip!);
|
||
return currentTrip!;
|
||
}
|
||
|
||
static Delivery GetNewDelivery() {
|
||
if (currentTrip != null) {
|
||
var dropOff = currentTrip!.startNewDelivery();
|
||
currentDelivery = dropOff;
|
||
return dropOff;
|
||
} else {
|
||
throw Exception("A delivery cannot exist without a trip");
|
||
}
|
||
}
|
||
|
||
static void EndTrip() {
|
||
if (currentTrip != null) {
|
||
currentTrip!.EndTime = DateTime.now();
|
||
}
|
||
currentDelivery = null;
|
||
currentTrip = null;
|
||
}
|
||
|
||
/// [a] should be the Start Time,
|
||
///
|
||
/// [b] is the end time
|
||
static String GetTotalTimeWorked(DateTime a, DateTime b) {
|
||
Duration diff = b.difference(a);
|
||
return Duration2Notation(diff);
|
||
}
|
||
|
||
static String Duration2Notation(Duration time) {
|
||
Time tm = Time.fromDuration(time);
|
||
|
||
return tm.toString();
|
||
}
|
||
|
||
static Future<int> FetchVersion() async {
|
||
Dio dio = Dio();
|
||
var packet = json.encode({"cmd": "get_version", "id": LastSessionID});
|
||
|
||
var response = await dio.post(TTConsts.SESSION_SERVER, data: packet);
|
||
if (response.statusCode == null) {
|
||
DisplayError = "Error: Version retrieval failed";
|
||
return -1;
|
||
}
|
||
if (response.statusCode! != 200) {
|
||
DisplayError = "Error: Session server HTTP Response code";
|
||
return -2;
|
||
}
|
||
|
||
var reply = json.decode(response.data as String);
|
||
if (reply["status"] == "version_back") {
|
||
return reply["version"] as int;
|
||
} else {
|
||
DisplayError = "Error: Unknown get_version reply";
|
||
return -3;
|
||
}
|
||
}
|
||
}
|
||
|
||
class Delivery {
|
||
SmallPosition? endLocation;
|
||
DateTime StartTime = DateTime.now();
|
||
|
||
Delivery() {
|
||
StartTime = DateTime.now();
|
||
}
|
||
|
||
Future<void> MarkEndLocation() async {
|
||
var pos = await SessionData.GetNewLocation();
|
||
endLocation = SmallPosition.fromPosition(pos);
|
||
}
|
||
|
||
Map<String, dynamic> toJsonMap() {
|
||
return {
|
||
"start": StartTime.toIso8601String(),
|
||
"endPos": endLocation?.toMap() ?? "incomplete",
|
||
};
|
||
}
|
||
|
||
static Delivery fromMap(Map<String, dynamic> jsx) {
|
||
Delivery delivery = Delivery();
|
||
delivery.StartTime = DateTime.parse(jsx['start'] as String);
|
||
if (jsx['endPos'] is String) {
|
||
delivery.endLocation = null;
|
||
} else {
|
||
delivery.endLocation = SmallPosition.fromMap(
|
||
jsx['endPos'] as Map<String, dynamic>,
|
||
);
|
||
}
|
||
|
||
return delivery;
|
||
}
|
||
|
||
Future<CompoundTag> toNBT() async {
|
||
CompoundTag ct = CompoundTag();
|
||
ct.put("start", StringTag.valueOf(StartTime.toIso8601String()));
|
||
if (endLocation != null) {
|
||
ct.put("endPos", await endLocation!.toNBT());
|
||
}
|
||
|
||
return ct;
|
||
}
|
||
|
||
static Future<Delivery> fromNBT(CompoundTag ct) async {
|
||
Delivery delivery = Delivery();
|
||
delivery.StartTime = DateTime.parse(ct.get("start")!.asString());
|
||
if (ct.containsKey("endPos")) {
|
||
delivery.endLocation = await SmallPosition.fromNBT(
|
||
ct.get("endPos") as CompoundTag,
|
||
);
|
||
}
|
||
|
||
return delivery;
|
||
}
|
||
}
|
||
|
||
class Trip {
|
||
List<Delivery> deliveries = [];
|
||
|
||
DateTime StartTime = DateTime(0);
|
||
DateTime EndTime = DateTime(0);
|
||
|
||
Trip() {
|
||
StartTime = DateTime.now();
|
||
}
|
||
|
||
Delivery startNewDelivery() {
|
||
var delivery = Delivery();
|
||
deliveries.add(delivery);
|
||
|
||
return delivery;
|
||
}
|
||
|
||
Map<String, dynamic> toJsonMap() {
|
||
Map<String, Object> trip = {
|
||
"start": StartTime.toIso8601String(),
|
||
"end": EndTime.toIso8601String(),
|
||
};
|
||
List<Map<String, dynamic>> dropOffs = [];
|
||
for (var delivery in deliveries) {
|
||
dropOffs.add(delivery.toJsonMap());
|
||
}
|
||
|
||
trip["deliveries"] = dropOffs;
|
||
|
||
return trip;
|
||
}
|
||
|
||
static Trip fromJsonMap(Map<String, dynamic> jsx) {
|
||
Trip trip = Trip();
|
||
trip.StartTime = DateTime.parse(jsx['start'] as String);
|
||
if (jsx.containsKey("end")) {
|
||
trip.EndTime = DateTime.parse(jsx['end'] as String);
|
||
} else {
|
||
SessionData.ContainsTripTimes = false;
|
||
}
|
||
trip.deliveries = [];
|
||
List<dynamic> dropOffs = jsx['deliveries'] as List<dynamic>;
|
||
|
||
for (var dropOff in dropOffs) {
|
||
trip.deliveries.add(Delivery.fromMap(dropOff as Map<String, dynamic>));
|
||
}
|
||
|
||
return trip;
|
||
}
|
||
|
||
Future<CompoundTag> toNBT() async {
|
||
CompoundTag ct = CompoundTag();
|
||
|
||
ct.put("start", StringTag.valueOf(StartTime.toIso8601String()));
|
||
if (EndTime.year > 2000) {
|
||
ct.put("end", StringTag.valueOf(EndTime.toIso8601String()));
|
||
}
|
||
|
||
ListTag drops = ListTag();
|
||
for (var drop in deliveries) {
|
||
drops.add(await drop.toNBT());
|
||
}
|
||
ct.put("deliveries", drops);
|
||
|
||
return ct;
|
||
}
|
||
|
||
static Future<Trip> fromNBT(CompoundTag tag) async {
|
||
Trip trip = Trip();
|
||
|
||
// Deserialize the Trip
|
||
trip.StartTime = DateTime.parse(tag.get("start")!.asString());
|
||
if (!tag.containsKey("end")) {
|
||
SessionData.ContainsTripTimes = false;
|
||
} else {
|
||
trip.EndTime = DateTime.parse(tag.get("end")!.asString());
|
||
}
|
||
|
||
ListTag drops = tag.get("deliveries")! as ListTag;
|
||
for (var drop in drops.value) {
|
||
Delivery del = await Delivery.fromNBT(drop.asCompoundTag());
|
||
trip.deliveries.add(del);
|
||
}
|
||
|
||
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!();
|
||
}
|
||
}
|
||
|
||
/// A simple wrapper for a position that strips away unnecessary information to create a more compact piece of save data
|
||
class SmallPosition {
|
||
double latitude;
|
||
double longitude;
|
||
|
||
SmallPosition({required this.latitude, required this.longitude});
|
||
|
||
static SmallPosition fromPosition(Position position) {
|
||
return SmallPosition(
|
||
latitude: position.latitude,
|
||
longitude: position.longitude,
|
||
);
|
||
}
|
||
|
||
Map<String, dynamic> toMap() {
|
||
return {"latitude": latitude, "longitude": longitude};
|
||
}
|
||
|
||
static SmallPosition fromMap(Map<String, dynamic> map) {
|
||
return SmallPosition(
|
||
latitude: map['latitude'] as double,
|
||
longitude: map['longitude'] as double,
|
||
);
|
||
}
|
||
|
||
Future<CompoundTag> toNBT() async {
|
||
CompoundTag ct = CompoundTag();
|
||
ct.put("latitude", DoubleTag.valueOf(latitude));
|
||
ct.put("longitude", DoubleTag.valueOf(longitude));
|
||
|
||
return ct;
|
||
}
|
||
|
||
static Future<SmallPosition> fromNBT(CompoundTag ct) async {
|
||
return SmallPosition(
|
||
latitude: ct.get("latitude")?.asDouble() ?? 0,
|
||
longitude: ct.get("longitude")?.asDouble() ?? 0,
|
||
);
|
||
}
|
||
}
|