diff --git a/Jenkinsfile b/Jenkinsfile index 38ee14a..799743d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -44,5 +44,33 @@ pipeline { } } } + + stage("Build Web App") { + agent { + label 'linux' + } + + steps { + script { + sh ''' + #!/bin/bash + + flutter build web + + cd build/web + tar -cvf ../../web.tgz . + cd ../.. + ''' + } + } + + post { + always { + archiveArtifacts artifacts: "web.tgz" + + cleanWs() + } + } + } } } \ No newline at end of file diff --git a/latest-releases.json b/latest-releases.json index d84fd53..786bc62 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,4 @@ { - "alpha": "1.0.0-dev.10" + "alpha": "1.0.0-dev.10", + "beta": "1.0.0-beta.31" } diff --git a/lib/consts.dart b/lib/consts.dart index 3ee858c..85c2227 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:geolocator/geolocator.dart'; @@ -7,10 +6,13 @@ 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.10"; + static get SESSION_SERVER => + "https://api.zontreck.com/timetrack/${UPDATE_CHANNEL}/timetrack.php"; + + static const VERSION = "1.0.0-beta.3"; static bool UPDATE_AVAILABLE = false; - static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; + static UpdateChannel UPDATE_CHANNEL = UpdateChannel.beta; static final LocationSettings LOCATION_SETTINGS = LocationSettings( accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 15, diff --git a/lib/data.dart b/lib/data.dart index fdce05f..460585f 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'dart:math' as math; import 'dart:ui'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:libac_dart/nbt/Stream.dart'; import 'package:timetrack/consts.dart'; @@ -19,6 +21,8 @@ class SessionData { static List positions = []; static late StreamSubscription _listener; static Callbacks Calls = Callbacks(); + static String LastSessionID = ""; + static String DisplayError = ""; /// 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; @@ -185,10 +189,22 @@ class SessionData { Trips = []; positions = []; - // TODO: Upload to the server. + Dio dio = Dio(); + Map payload = {"cmd": "create", "data": saveData}; + + var reply = await dio.post( + TTConsts.SESSION_SERVER, + data: json.encode(payload), + ); + Map replyJs = json.decode(reply.data as String); + if (replyJs["status"] == "ok") { + print("Successful upload"); + LastSessionID = replyJs['session'] as String; + Calls.dispatch(); + } } - static String SaveData() { + static Map SaveData() { Map saveData = {}; List> _trips = []; @@ -204,11 +220,28 @@ class SessionData { saveData["trips"] = _trips; saveData["positions"] = _pos; - return json.encode(saveData); + return saveData; } - void LoadData(String js) { + static Future DownloadData() async { + Dio dio = Dio(); + Map payload = {"cmd": "get", "id": LastSessionID}; + + // Send the data, and get the response + var reply = await dio.post( + TTConsts.SESSION_SERVER, + data: json.encode(payload), + ); + + LoadData(reply.data as String); + } + + static void LoadData(String js) { Map _js = json.decode(js); + if (_js.containsKey("error")) { + LastSessionID = ""; + return; + } List> _trips = _js['trips'] as List>; List> _pos = diff --git a/lib/main.dart b/lib/main.dart index 3c1c510..fef79bc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,19 @@ import 'package:flutter/material.dart'; import 'package:timetrack/consts.dart'; +import 'package:timetrack/data.dart'; import 'package:timetrack/pages/MainApp.dart'; Future main() async { await TTConsts.checkUpdate(); + var sess = Uri.base.queryParameters["code"] ?? ""; + SessionData.LastSessionID = sess; + if (SessionData.LastSessionID.isNotEmpty) { + await SessionData.DownloadData(); + if (SessionData.LastSessionID.isEmpty) { + // Invalid session token + SessionData.DisplayError = "The URL and or session token is invalid"; + } + } runApp(MainApp()); } diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index c63d778..9d9b7b9 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:libacflutter/Constants.dart'; import 'package:libacflutter/Prompt.dart'; import 'package:timetrack/consts.dart'; @@ -112,10 +113,22 @@ class _HomePageState extends State { body: SingleChildScrollView( child: Column( children: [ - Text( - "Hit engage when you are ready to go online and start tracking location data, and trips.", - style: TextStyle(fontSize: 18), - ), + if (!SessionData.IsOnTheClock) + Text( + "Hit engage when you are ready to go online and start tracking location data, and trips.", + style: TextStyle(fontSize: 18), + ), + + if (SessionData.LastSessionID.isNotEmpty) + ListTile( + title: Text("Session ID"), + subtitle: Text("${SessionData.LastSessionID} - Tap to copy"), + onTap: () { + Clipboard.setData( + ClipboardData(text: SessionData.LastSessionID), + ); + }, + ), if (!SessionData.IsOnTheClock) Center( child: ElevatedButton( diff --git a/lib/pages/MainApp.dart b/lib/pages/MainApp.dart index 4a01ae0..087fd6f 100644 --- a/lib/pages/MainApp.dart +++ b/lib/pages/MainApp.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + 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/WebMainPage.dart'; import 'package:timetrack/pages/WorkData.dart'; class MainApp extends StatefulWidget { @@ -24,7 +27,7 @@ class MainAppState extends State { return MaterialApp( title: "Time Tracker", routes: { - "/": (ctx) => HomePage(), + "/": (ctx) => Platform.isAndroid ? HomePage() : WebMain(), "/upd": (ctx) => UpdateSettingsPage(), "/map": (ctx) => MapPage(), "/work": (ctx) => WorkDataPage(), diff --git a/lib/pages/WebMainPage.dart b/lib/pages/WebMainPage.dart new file mode 100644 index 0000000..41db49f --- /dev/null +++ b/lib/pages/WebMainPage.dart @@ -0,0 +1,128 @@ +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 WebMain extends StatefulWidget { + @override + State createState() { + return _WebMain(); + } +} + +class _WebMain extends State { + TextEditingController sessionIDController = TextEditingController(); + + @override + void didChangeDependencies() { + sessionIDController.text = SessionData.LastSessionID; + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Time Tracker"), + backgroundColor: LibACFlutterConstants.TITLEBAR_COLOR, + ), + drawer: Drawer( + elevation: 8, + child: SingleChildScrollView( + child: Column( + children: [ + DrawerHeader( + child: Column( + children: [ + Text("Time Tracker"), + Text("Created by Tara Piccari"), + Text("Copyright 2025 - Present"), + Text("Version: ${TTConsts.VERSION}"), + ], + ), + ), + if (SessionData.IsReadOnly) + ListTile( + title: Text("Trip Map"), + leading: Icon(Icons.map), + subtitle: Text( + "View a map of the route\n(NOTE: This is not live, and reflects the current state as of the time the map is opened.)", + ), + onTap: () async { + await Navigator.pushNamed(context, "/map"); + }, + ), + if (SessionData.IsReadOnly) + ListTile( + title: Text("Work Data"), + subtitle: Text("View 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(() {}); + }, + ), + ], + ), + ), + ), + body: Padding( + padding: EdgeInsets.all(8), + child: SingleChildScrollView( + child: Column( + children: [ + // Start doing magic! + if (SessionData.DisplayError.isNotEmpty) + Text(SessionData.DisplayError, style: TextStyle(fontSize: 18)), + // Check what widgets need to be displayed. + if (SessionData.IsReadOnly) GetReadOnlyWidgets(), + if (!SessionData.IsReadOnly) GetLoadWidgets(), + ], + ), + ), + ), + ); + } + + Widget GetReadOnlyWidgets() { + return Column( + children: [ + Text( + "Use the top left menu to show the various pages for the data viewer.", + ), + ElevatedButton( + onPressed: () async { + SessionData.IsReadOnly = false; + SessionData.Trips = []; + SessionData.positions = []; + SessionData.DisplayError = ""; + }, + child: Text("Close Session"), + ), + ], + ); + } + + Widget GetLoadWidgets() { + return Column( + children: [ + // Present a text box for the session ID, and a button for loading. + ListTile(title: Text("Session ID")), + TextField( + controller: sessionIDController, + decoration: InputDecoration(border: OutlineInputBorder()), + ), + ElevatedButton( + onPressed: () async { + await SessionData.DownloadData(); + setState(() {}); + }, + child: Text("Load Session", style: TextStyle(fontSize: 18)), + ), + ], + ); + } +} diff --git a/lib/pages/WorkData.dart b/lib/pages/WorkData.dart index eeac3fe..f99e551 100644 --- a/lib/pages/WorkData.dart +++ b/lib/pages/WorkData.dart @@ -1,7 +1,6 @@ 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 { @@ -70,7 +69,7 @@ class _WorkData extends State { ), SizedBox(height: 20), Text( - "Total Miles: ${SessionData.GetTotalMilesAsString()}", + "Total Estimated Miles: ${SessionData.GetTotalMilesAsString()}\n(Note: The miles displayed above may not be 100% accurate)", style: TextStyle(fontSize: 24), ), ], diff --git a/pubspec.yaml b/pubspec.yaml index 1eb0ca2..dd244a5 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.10 +version: 1.0.0-beta.3 environment: sdk: ^3.7.2 diff --git a/server/php/timetrack.php b/server/php/timetrack.php index 56cde70..f29d28c 100644 --- a/server/php/timetrack.php +++ b/server/php/timetrack.php @@ -10,6 +10,7 @@ $DB = get_DB("timetrack"); $jsx = json_decode(file_get_contents("php://input"), true); // Get operation information +// DISCLAIMER: All php code below this point is AI Generated switch($jsx['cmd']) { case "create": { // Get UUID from MySQL and insert into sessions table