From 8adaf6169a13d1612097bb6c2872477464296650 Mon Sep 17 00:00:00 2001 From: zontreck Date: Sun, 15 Jun 2025 02:05:52 -0700 Subject: [PATCH] Do some refactoring for protocol v2 --- lib/consts.dart | 2 +- lib/data.dart | 87 ++++++++++++++++++++++++++++++++++++++ lib/pages/HomePage.dart | 2 +- lib/pages/WebMainPage.dart | 32 ++++++++++++++ pubspec.yaml | 2 +- 5 files changed, 122 insertions(+), 3 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index 9aea815..6ddee55 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -9,7 +9,7 @@ class TTConsts { static get SESSION_SERVER => "https://api.zontreck.com/timetrack/$UPDATE_CHANNEL/timetrack.php"; - static const VERSION = "1.0.0-beta.31"; + static const VERSION = "1.0.0-beta.32"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.beta; diff --git a/lib/data.dart b/lib/data.dart index 9097219..cdc8bc4 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui'; @@ -45,6 +46,12 @@ class SessionData { 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; + /// Is true if the try-catch is tripped or if not running on Android static bool isWeb = false; @@ -236,6 +243,8 @@ class SessionData { ); } + await _create(); + _listener = Geolocator.getPositionStream( locationSettings: TTConsts.LOCATION_SETTINGS, ).listen((pos) { @@ -274,6 +283,50 @@ class SessionData { _upload(nbtData); } + /// v2 Create function. + /// + /// This function sets the Session ID globally. It will also set the Recording flag to true. + static Future _create() async { + Dio dio = Dio(); + Map 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 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 _upload(List nbtData) async { Dio dio = Dio(); @@ -325,6 +378,7 @@ class SessionData { DisplayError = ""; IsReadOnly = false; ContainsTripTimes = true; + Recording = false; if (FlutterBackground.isBackgroundExecutionEnabled) { FlutterBackground.disableBackgroundExecution(); } @@ -367,6 +421,15 @@ class SessionData { static Future _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(); @@ -416,6 +479,7 @@ class SessionData { 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) { @@ -603,6 +667,29 @@ class SessionData { return tm.toString(); } + + static Future 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 { diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index 80e185d..8edbac1 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -411,7 +411,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 ${SessionData.GetTotalTimeWorked(SessionData.StartTime, DateTime.now())} ago\n\n", + "Your location is being tracked for record keeping purposes.\n\nYou started ${SessionData.GetTotalTimeWorked(SessionData.StartTime, DateTime.now())} ago\n\n", style: TextStyle(fontSize: 18), ), if (SessionData.currentTrip != null) GetTripWidgets(), diff --git a/lib/pages/WebMainPage.dart b/lib/pages/WebMainPage.dart index 96914e2..8b61805 100644 --- a/lib/pages/WebMainPage.dart +++ b/lib/pages/WebMainPage.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:libacflutter/Constants.dart'; import 'package:timetrack/consts.dart'; @@ -19,9 +21,31 @@ class _WebMain extends State { @override void didChangeDependencies() { sessionIDController.text = SessionData.LastSessionID; + + // Check if FirstRun + if (SessionData.Calls.HomeCallback == null) { + SessionData.Calls.HomeCallback = _callback; + // After doing this, we also want to schedule the timer + Timer.periodic(Duration(seconds: 5), (timer) async { + if (!SessionData.Recording) { + timer.cancel(); + return; + } + + // Fetch the latest version number, compare, then redownload the data. + int ver = await SessionData.FetchVersion(); + if (ver != SessionData.RecordingVersion) { + await SessionData.DownloadData(); + } + }); + } super.didChangeDependencies(); } + Future _callback() async { + setState(() {}); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -132,6 +156,14 @@ class _WebMain extends State { ), tileColor: const Color.fromARGB(255, 7, 123, 255), ), + if (SessionData.Recording) + ListTile( + title: Text("LIVE SESSION"), + subtitle: Text( + "This session is live! Recording is still in progress. Over time this live view will automatically refresh until the recording is ended.\n\nSession Version: ${SessionData.RecordingVersion}", + ), + tileColor: LibACFlutterConstants.TITLEBAR_COLOR, + ), ElevatedButton( onPressed: () async { await showDialog( diff --git a/pubspec.yaml b/pubspec.yaml index bd8696c..6891933 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-beta.31 +version: 1.0.0-beta.32 environment: sdk: ^3.7.2