diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..284494c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: CI +on: + # Run the workflow when code is pushed to master branch + pull_request: + branches: + - master + +jobs: + flutter_test: + name: Run flutter test and analyze + runs-on: ubuntu-latest + steps: + # Setup Java environment in order to build the Android app + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: "12.x" + + # Setup the flutter environment + - uses: subosito/flutter-action@v1 + with: + channel: "stable" + + # Get flutter dependencies + - run: flutter pub get + + build_ios: + name: Build Flutter (iOS) + needs: [flutter_test] + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: "12.x" + - uses: subosito/flutter-action@v1 + with: + channel: "stable" + - run: flutter pub get + - run: flutter clean + + # Build iOS app + - run: flutter build ios --release --no-codesign + + # Upload the iOS build to GitHub as artifact + - uses: actions/upload-artifact@v1 + with: + name: ios-app + path: /Users/runner/work/enviroCar-Cross-Platform-App/enviroCar-Cross-Platform-App/build/ios/iphoneos + + build_appbundle: + name: Build Flutter (Android) + + # Runs only when Flutter test executes successfully + needs: [flutter_test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: "12.x" + - uses: subosito/flutter-action@v1 + with: + channel: "stable" + - run: flutter pub get + + # Clean any previous builds + - run: flutter clean + + # Build apk + - run: flutter build apk + + # Upload the build as artifact + - uses: actions/upload-artifact@v1 + with: + name: release-apk + path: build/app/outputs/apk/release/app-release.apk diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..fc932fe --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,26 @@ +include: package:lint/analysis_options.yaml + +# Linting rules for the project + +linter: + rules: + # Blindly follow the Flutter code style, which prefers types everywhere + prefer_relative_imports: true + + # Ignoring naming convention of using underscores instead of camelCase + file_names: false + + # Ignoring having to make getters for setters + avoid_setters_without_getters: false + + # Ignoring avoiding using true and false in conditions + avoid_bool_literals_in_conditional_expressions: false + + # Ignoring ordering directives in alphabetical order + directives_ordering: false + + # Ignoring converting outter single to doube quotes + avoid_escaping_inner_quotes: false + + # ignoring naming convention for constant identifiers + constant_identifier_names: false \ No newline at end of file diff --git a/assets/images/img_logbook.png b/assets/images/img_logbook.png new file mode 100644 index 0000000..b5807e5 Binary files /dev/null and b/assets/images/img_logbook.png differ diff --git a/lib/constants.dart b/lib/constants.dart index 09bbf65..ab408a5 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -3,27 +3,27 @@ import 'package:flutter/material.dart'; const InputDecoration inputDecoration = InputDecoration( alignLabelWithHint: true, labelStyle: TextStyle( - color: const Color(0xff7e7e7e), + color: Color(0xff7e7e7e), ), - contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), + contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), border: OutlineInputBorder( - borderRadius: const BorderRadius.all( - const Radius.circular(5.0), + borderRadius: BorderRadius.all( + Radius.circular(5.0), ), ), enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: kSecondaryColor, width: 1.5), - borderRadius: const BorderRadius.all( - const Radius.circular(5.0), + borderSide: BorderSide(color: kSecondaryColor, width: 1.5), + borderRadius: BorderRadius.all( + Radius.circular(5.0), ), ), focusedBorder: OutlineInputBorder( - borderSide: const BorderSide( + borderSide: BorderSide( color: kGreyColor, width: 1.5, ), - borderRadius: const BorderRadius.all( - const Radius.circular(5.0), + borderRadius: BorderRadius.all( + Radius.circular(5.0), ), ), ); @@ -34,3 +34,8 @@ const Color kPrimaryColor = Color(0xff000000); const Color kSecondaryColor = Color(0xffD8D8D8); const Color kErrorColor = Color.fromARGB(250, 255, 50, 64); const Color kWhiteColor = Color(0xffffffff); +const Color kTertiaryColor = Color.fromARGB(200, 0, 0, 0); + +// PlayStore URL for 'Rate Us' feature +const String playstoreUrl = + 'https://play.google.com/store/apps/details?id=org.envirocar.app'; diff --git a/lib/database/carsTable.dart b/lib/database/carsTable.dart new file mode 100644 index 0000000..6dc29f0 --- /dev/null +++ b/lib/database/carsTable.dart @@ -0,0 +1,24 @@ +class CarsTable { + // name of the table + static const String tableName = 'cars'; + + // columns of the table + static const String idColumn = 'id'; + static const String manufacturerColumn = 'manufacturer'; + static const String modelColumn = 'model'; + static const String constructionYearColumn = 'constructionYear'; + static const String fuelTypeColumn = 'fuelType'; + static const String engineDisplacementColumn = 'engineDisplacement'; + + // query to create the table + static const String carsTableQuery = ''' + CREATE TABLE ${CarsTable.tableName} ( + ${CarsTable.idColumn} INTEGER PRIMARY KEY AUTOINCREMENT, + ${CarsTable.manufacturerColumn} TEXT NOT NULL, + ${CarsTable.modelColumn} TEXT NOT NULL, + ${CarsTable.constructionYearColumn} INTEGER NOT NULL, + ${CarsTable.fuelTypeColumn} TEXT NOT NULL, + ${CarsTable.engineDisplacementColumn} INTEGER NOT NULL + ) + '''; +} diff --git a/lib/database/databaseHelper.dart b/lib/database/databaseHelper.dart new file mode 100644 index 0000000..956f113 --- /dev/null +++ b/lib/database/databaseHelper.dart @@ -0,0 +1,75 @@ +import 'package:flutter/foundation.dart'; + +import 'package:sqflite/sqflite.dart'; +import 'package:path/path.dart'; + +import './carsTable.dart'; + +class DatabaseHelper { + static Database _database; + + // name and version of the database, to be changed later + static const _dbName = 'testDB1.db'; + static const _dbVersion = 1; + + // private construction to create a singleton of the database instance + DatabaseHelper._privateConstructor(); + static final DatabaseHelper instance = DatabaseHelper._privateConstructor(); + + // fetching the database + Future get getDatabase async { + // return database if it is already open + if (_database != null) { + return _database; + } else { + return _database = await initDatabase(); + } + } + + // openining the database + Future initDatabase() async { + final String dbPath = await getDatabasesPath(); + return openDatabase( + join(dbPath, _dbName), + version: _dbVersion, + onCreate: createDatabase, + ); + } + + // creating the database and the tables if it is the first time + Future createDatabase(Database db, int version) async { + await db.execute(CarsTable.carsTableQuery); + } + + // method to insert values in the database + // return the primary key of the inserted value + Future insertValue( + {@required String tableName, @required Map data}) async { + final Database db = await instance.getDatabase; + final int primaryKey = await db.insert( + tableName, + data, + ); + + return primaryKey; + } + + // reads all values in the table + Future>> readAllValues( + {@required String tableName}) async { + final Database db = await instance.getDatabase; + + return db.query(tableName); + } + + // deletes the value + Future deleteValue( + {@required String tableName, @required Map data}) async { + final Database db = await instance.getDatabase; + db.delete( + tableName, + where: 'id = ?', + whereArgs: [data['id']], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 18a1168..f73dd16 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import './providers/authProvider.dart'; import './providers/tracksProvider.dart'; +import 'models/track.dart'; import './screens/splashScreen.dart'; import './screens/bluetoothDevicesScreen.dart'; import './screens/index.dart'; @@ -22,10 +23,18 @@ import './screens/createCarScreen.dart'; import './screens/trackDetailsScreen.dart'; import './providers/bluetoothStatusProvider.dart'; import './providers/locationStatusProvider.dart'; +import './providers/bluetoothProvider.dart'; +import './providers/fuelingsProvider.dart'; +import './screens/createFuelingScreen.dart'; +import './screens/logBookScreen.dart'; +import './screens/reportIssueScreen.dart'; +import './screens/helpScreen.dart'; -void main() async { +Future main() async { + // Ensures all the future functions of main() finish before launching the app WidgetsFlutterBinding.ensureInitialized(); + // Instance of shared prefs preferences = await SharedPreferences.getInstance(); // Restricts rotation of screen @@ -42,6 +51,7 @@ void main() async { } class MyApp extends StatelessWidget { + @override Widget build(BuildContext context) { return MultiProvider( providers: [ @@ -73,7 +83,16 @@ class MyApp extends StatelessWidget { // Provides location status update to the different widgets on the tree ChangeNotifierProvider( create: (context) => LocationStatusProvider(), - ) + ), + + // Provides bluetooth functions to the different widgets on the tree + ChangeNotifierProvider( + create: (context) => BluetoothProvider(), + ), + // Provides Fueling data to different widgets + ChangeNotifierProvider( + create: (context) => FuelingsProvider(), + ), ], child: MaterialApp( locale: DevicePreview.locale(context), @@ -90,7 +109,8 @@ class MyApp extends StatelessWidget { case TrackDetailsScreen.routeName: return MaterialPageRoute( builder: (_) { - return TrackDetailsScreen(track: settings.arguments); + final Track track = settings.arguments as Track; + return TrackDetailsScreen(track: track); }, ); @@ -110,6 +130,10 @@ class MyApp extends StatelessWidget { MapScreen.routeName: (context) => MapScreen(), CarScreen.routeName: (context) => CarScreen(), CreateCarScreen.routeName: (context) => CreateCarScreen(), + CreateFuelingScreen.routeName: (context) => CreateFuelingScreen(), + LogBookScreen.routeName: (context) => LogBookScreen(), + ReportIssueScreen.routeName: (context) => ReportIssueScreen(), + HelpScreen.routeName: (context) => HelpScreen(), }, ), ); diff --git a/lib/models/car.dart b/lib/models/car.dart index 82c521b..c46b90a 100644 --- a/lib/models/car.dart +++ b/lib/models/car.dart @@ -1,5 +1,5 @@ class Car { - String id; + int id; String manufacturer; String model; int constructionYear; @@ -14,4 +14,25 @@ class Car { this.fuelType, this.engineDisplacement, }); + + Car.fromJson(Map json) { + id = json['id'] as int; + manufacturer = json['manufacturer'] as String; + model = json['model'] as String; + constructionYear = json['constructionYear'] as int; + fuelType = json['fuelType'] as String; + engineDisplacement = json['engineDisplacement'] as int; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['manufacturer'] = manufacturer; + data['model'] = model; + data['constructionYear'] = constructionYear; + data['fuelType'] = fuelType; + data['engineDisplacement'] = engineDisplacement; + + return data; + } } diff --git a/lib/models/fueling.dart b/lib/models/fueling.dart new file mode 100644 index 0000000..e90da20 --- /dev/null +++ b/lib/models/fueling.dart @@ -0,0 +1,30 @@ +import 'package:flutter/foundation.dart'; + +import './car.dart'; + +// Model for Fueling data in Log Book +class Fueling { + String id; + Car car; + String mileage; + String fueledVolume; + String pricePerLitre; + String totalPrice; + bool partialFueling; + bool missedPreviousFueling; + String comment; + + // TODO: Add datetime attribute + + Fueling({ + @required this.id, + @required this.car, + @required this.mileage, + @required this.fueledVolume, + @required this.totalPrice, + @required this.pricePerLitre, + @required this.partialFueling, + @required this.missedPreviousFueling, + @required this.comment, + }); +} diff --git a/lib/models/properties.dart b/lib/models/properties.dart index 4bb60a1..02857d0 100644 --- a/lib/models/properties.dart +++ b/lib/models/properties.dart @@ -16,22 +16,22 @@ class Properties { }); Properties.fromJson(Map json) { - engineDisplacement = json['engineDisplacement']; - model = json['model']; - id = json['id']; - fuelType = json['fuelType']; - constructionYear = json['constructionYear']; - manufacturer = json['manufacturer']; + engineDisplacement = json['engineDisplacement'] as int; + model = json['model'] as String; + id = json['id'] as String; + fuelType = json['fuelType'] as String; + constructionYear = json['constructionYear'] as int; + manufacturer = json['manufacturer'] as String; } Map toJson() { - final Map data = new Map(); - data['engineDisplacement'] = this.engineDisplacement; - data['model'] = this.model; - data['id'] = this.id; - data['fuelType'] = this.fuelType; - data['constructionYear'] = this.constructionYear; - data['manufacturer'] = this.manufacturer; + final Map data = {}; + data['engineDisplacement'] = engineDisplacement; + data['model'] = model; + data['id'] = id; + data['fuelType'] = fuelType; + data['constructionYear'] = constructionYear; + data['manufacturer'] = manufacturer; return data; } } diff --git a/lib/models/report.dart b/lib/models/report.dart new file mode 100644 index 0000000..fcd1f7e --- /dev/null +++ b/lib/models/report.dart @@ -0,0 +1,38 @@ +import 'package:flutter/foundation.dart'; + +// Model for report created in Report Issue Screen + +class Report { + String id; + String estimatedTime; + String problem; + bool forceCrash; + bool suddenLags; + bool appWasUnresponsive; + bool componentDoesNotWorkAsExpected; + bool requestForAFeature; + + Report({ + @required this.id, + @required this.estimatedTime, + @required this.problem, + @required this.forceCrash, + @required this.suddenLags, + @required this.appWasUnresponsive, + @required this.componentDoesNotWorkAsExpected, + @required this.requestForAFeature, + }); + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['estimatedTime'] = estimatedTime; + data['problem'] = problem; + data['forceCrash'] = forceCrash; + data['suddenLags'] = suddenLags; + data['appWasUnresponsive'] = appWasUnresponsive; + data['componentDoesNotWorkAsExpected'] = componentDoesNotWorkAsExpected; + data['requestForAFeature'] = requestForAFeature; + return data; + } +} diff --git a/lib/models/sensor.dart b/lib/models/sensor.dart index e5cf7d6..3d9a4f2 100644 --- a/lib/models/sensor.dart +++ b/lib/models/sensor.dart @@ -10,17 +10,17 @@ class Sensor { }); Sensor.fromJson(Map json) { - type = json['type']; + type = json['type'] as String; properties = json['properties'] != null - ? new Properties.fromJson(json['properties']) + ? Properties.fromJson(json['properties'] as Map) : null; } Map toJson() { - final Map data = new Map(); - data['type'] = this.type; - if (this.properties != null) { - data['properties'] = this.properties.toJson(); + final Map data = {}; + data['type'] = type; + if (properties != null) { + data['properties'] = properties.toJson(); } return data; } diff --git a/lib/models/settingsTileModel.dart b/lib/models/settingsTileModel.dart new file mode 100644 index 0000000..4a88559 --- /dev/null +++ b/lib/models/settingsTileModel.dart @@ -0,0 +1,14 @@ +import 'package:flutter/foundation.dart'; + +// Model for the checkbox tile on settings screen +class SettingsTileModel { + String title; + String subtitle; + bool isChecked; + + SettingsTileModel({ + @required this.title, + @required this.subtitle, + @required this.isChecked, + }); +} diff --git a/lib/models/track.dart b/lib/models/track.dart index 2be1ab3..859c8fc 100644 --- a/lib/models/track.dart +++ b/lib/models/track.dart @@ -16,22 +16,23 @@ class Track { }); Track.fromJson(Map json) { - id = json['id']; - length = json['length']; - begin = DateTime.parse(json['begin']); - end = DateTime.parse(json['end']); - sensor = - json['sensor'] != null ? new Sensor.fromJson(json['sensor']) : null; + id = json['id'] as String; + length = json['length'] as double; + begin = DateTime.parse(json['begin'] as String); + end = DateTime.parse(json['end'] as String); + sensor = json['sensor'] != null + ? Sensor.fromJson(json['sensor'] as Map) + : null; } Map toJson() { - final Map data = new Map(); - data['id'] = this.id; - data['length'] = this.length; - data['begin'] = this.begin; - data['end'] = this.end; - if (this.sensor != null) { - data['sensor'] = this.sensor.toJson(); + final Map data = {}; + data['id'] = id; + data['length'] = length; + data['begin'] = begin; + data['end'] = end; + if (sensor != null) { + data['sensor'] = sensor.toJson(); } return data; } diff --git a/lib/models/user.dart b/lib/models/user.dart index cad4a94..a88dfb9 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -12,11 +12,11 @@ class User { String password, bool acceptedTerms, bool acceptedPrivacy}) { - this.setUsername = username; - this.setEmail = email; - this.setPassword = password; - this.setAcceptedTerms = acceptedTerms; - this.setAcceptedPrivacy = acceptedPrivacy; + setUsername = username; + setEmail = email; + setPassword = password; + setAcceptedTerms = acceptedTerms; + setAcceptedPrivacy = acceptedPrivacy; } set setUsername(String username) { @@ -61,11 +61,11 @@ class User { Map toMap() { return { - 'name': this.getUsername, - 'mail': this.getEmail, - 'token': this.getPassword, - 'acceptedTerms': this.getAccpetedTerms, - 'acceptedPrivacy': this.getAccpetedPrivacy, + 'name': getUsername, + 'mail': getEmail, + 'token': getPassword, + 'acceptedTerms': getAccpetedTerms, + 'acceptedPrivacy': getAccpetedPrivacy, }; } } diff --git a/lib/models/userStats.dart b/lib/models/userStats.dart index fcfb0c0..7eb047b 100644 --- a/lib/models/userStats.dart +++ b/lib/models/userStats.dart @@ -8,22 +8,22 @@ class UserStats { {this.distance, this.duration, this.userstatistic, this.trackCount}); UserStats.fromJson(Map json) { - distance = json['distance']; - duration = json['duration']; + distance = json['distance'] as double; + duration = json['duration'] as double; userstatistic = json['userstatistic'] != null - ? new Userstatistic.fromJson(json['userstatistic']) + ? Userstatistic.fromJson(json['userstatistic'] as Map) : null; - trackCount = json['trackCount']; + trackCount = json['trackCount'] as int; } Map toJson() { - final Map data = new Map(); - data['distance'] = this.distance; - data['duration'] = this.duration; - if (this.userstatistic != null) { - data['userstatistic'] = this.userstatistic.toJson(); + final Map data = {}; + data['distance'] = distance; + data['duration'] = duration; + if (userstatistic != null) { + data['userstatistic'] = userstatistic.toJson(); } - data['trackCount'] = this.trackCount; + data['trackCount'] = trackCount; return data; } } @@ -37,24 +37,26 @@ class Userstatistic { Userstatistic.fromJson(Map json) { below60kmh = json['below60kmh'] != null - ? new Below60kmh.fromJson(json['below60kmh']) + ? Below60kmh.fromJson(json['below60kmh'] as Map) : null; above130kmh = json['above130kmh'] != null - ? new Below60kmh.fromJson(json['above130kmh']) + ? Below60kmh.fromJson(json['above130kmh'] as Map) + : null; + naN = json['NaN'] != null + ? Below60kmh.fromJson(json['NaN'] as Map) : null; - naN = json['NaN'] != null ? new Below60kmh.fromJson(json['NaN']) : null; } Map toJson() { - final Map data = new Map(); - if (this.below60kmh != null) { - data['below60kmh'] = this.below60kmh.toJson(); + final Map data = {}; + if (below60kmh != null) { + data['below60kmh'] = below60kmh.toJson(); } - if (this.above130kmh != null) { - data['above130kmh'] = this.above130kmh.toJson(); + if (above130kmh != null) { + data['above130kmh'] = above130kmh.toJson(); } - if (this.naN != null) { - data['NaN'] = this.naN.toJson(); + if (naN != null) { + data['NaN'] = naN.toJson(); } return data; } @@ -67,14 +69,14 @@ class Below60kmh { Below60kmh({this.distance, this.duration}); Below60kmh.fromJson(Map json) { - distance = json['distance']; - duration = json['duration']; + distance = json['distance'] as double; + duration = json['duration'] as double; } Map toJson() { - final Map data = new Map(); - data['distance'] = this.distance; - data['duration'] = this.duration; + final Map data = {}; + data['distance'] = distance; + data['duration'] = duration; return data; } } diff --git a/lib/providers/authProvider.dart b/lib/providers/authProvider.dart index 8acf101..a32cc9b 100644 --- a/lib/providers/authProvider.dart +++ b/lib/providers/authProvider.dart @@ -25,7 +25,7 @@ class AuthProvider with ChangeNotifier { } void removeUser() { - this.setUser = null; - this.setAuthStatus = false; + setUser = null; + setAuthStatus = false; } } diff --git a/lib/providers/bluetoothProvider.dart b/lib/providers/bluetoothProvider.dart new file mode 100644 index 0000000..df91bc4 --- /dev/null +++ b/lib/providers/bluetoothProvider.dart @@ -0,0 +1,201 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:toast/toast.dart'; + +import '../constants.dart'; + +class BluetoothProvider extends ChangeNotifier { + FlutterReactiveBle _flutterReactiveBle; + DiscoveredDevice connectedBluetoothDevice; + BuildContext context; + + List _detectedBluetoothDevices; + + // list of services advertised by the connected bluetooth device + List _services; + + Map> _servicesCharacteristics; + + /// creating the [BluetoothProvider] a singleton class + factory BluetoothProvider() => _bluetoothProvider; + + BluetoothProvider._() { + _flutterReactiveBle = FlutterReactiveBle(); + _detectedBluetoothDevices = []; + _services = []; + _servicesCharacteristics = {}; + } + + static final BluetoothProvider _bluetoothProvider = BluetoothProvider._(); + + Future bluetoothState() async { + return _flutterReactiveBle.status; + } + + /// function to scan the devices and populate the [_detectedBluetoothDevices] list + Future startScan() async { + // withServices specifies the advertised services IDs to look for + // but if an empty list is passed as a parameter, all advertising devices are listed + _flutterReactiveBle = FlutterReactiveBle(); + final BleStatus bleStatus = await bluetoothState(); + if (bleStatus == BleStatus.ready) { + _flutterReactiveBle.scanForDevices(withServices: [], scanMode: ScanMode.balanced).listen((device) { + populateList(device); + }).onError(handleError); + } + } + + /// function to add the [discoveredDevice] to [_detectedBluetoothDevices] list + void populateList(DiscoveredDevice discoveredDevice) { + if (_detectedBluetoothDevices.isEmpty) { + debugPrint(discoveredDevice.toString()); + _detectedBluetoothDevices.add(discoveredDevice); + notifyListeners(); + return; + } + + bool addDeviceToList = true; + for (final DiscoveredDevice device in _detectedBluetoothDevices) { + if (device.id == discoveredDevice.id) { + addDeviceToList = false; + break; + } + } + + if (addDeviceToList) { + debugPrint(discoveredDevice.toString()); + _detectedBluetoothDevices.add(discoveredDevice); + notifyListeners(); + } + } + + /// function to handle error while scanning + void handleError(Object e, StackTrace stackTrace) { + debugPrint(e.toString()); + debugPrint(stackTrace.toString()); + } + + /// function to stop scanning by deinitializing [_flutterReactiveBle] + void stopScan() { + _flutterReactiveBle.deinitialize(); + } + + /// function to connect to selected device + Future connectToDevice(DiscoveredDevice selectedDevice, BuildContext ctx, int index) async { + context = ctx; + _flutterReactiveBle.connectToDevice( + id: selectedDevice.id, + connectionTimeout: const Duration(seconds: 10), + // servicesWithCharacteristicsToDiscover: // TODO: specify characteristics for OBD-II for faster connection + ).listen(sendConnectionStatusUpdates).onError(handleError); + connectedBluetoothDevice = selectedDevice; + notifyListeners(); + return connectedBluetoothDevice == null ? false : true; + } + + /// callback function to send connection status updates + void sendConnectionStatusUpdates(ConnectionStateUpdate connectionStateUpdate) { + debugPrint('connected to ${connectedBluetoothDevice.id}'); + debugPrint('connection state ${connectionStateUpdate.connectionState.toString()}'); + debugPrint('device id ${connectionStateUpdate.deviceId}'); + debugPrint('failure ${connectionStateUpdate.failure.toString()}'); + + if (connectionStateUpdate.failure != null) { + debugPrint('connection unsuccessful'); + Toast.show( + 'Cannot connect to ${connectedBluetoothDevice.name.isNotEmpty ? connectedBluetoothDevice.name : connectedBluetoothDevice.id}', + context, + duration: Toast.LENGTH_LONG, + gravity: Toast.BOTTOM, + backgroundColor: kTertiaryColor + ); + connectedBluetoothDevice = null; + notifyListeners(); + } + + if (connectionStateUpdate.connectionState == DeviceConnectionState.connected) { + Toast.show( + 'Connected to ${connectedBluetoothDevice.name.isNotEmpty ? connectedBluetoothDevice.name : connectedBluetoothDevice.id}', + context, + duration: Toast.LENGTH_LONG, + gravity: Toast.BOTTOM, + backgroundColor: kTertiaryColor + ); + discoverServices(); + } + } + + /// function to discover services + Future discoverServices() async { + _flutterReactiveBle = FlutterReactiveBle(); + _services = await _flutterReactiveBle.discoverServices(connectedBluetoothDevice.id); + debugPrint(_services.toString()); + // logCharacteristicsForServices(); + } + + /// function to add the [serviceId] and [characteristicIds] to [_servicesCharacteristics] + Future logCharacteristicsForServices() async { + for (final DiscoveredService service in _services) { + _servicesCharacteristics[service.serviceId] = service.characteristicIds; + // debugPrint('service id ${service.serviceId.toString()} characteristics Id ${service.characteristicIds.toString()}'); + for (final Uuid characteristicId in service.characteristicIds) { + readCharacteristic(service.serviceId, characteristicId); + // writeCharacteristic(service.serviceId, characteristicId, [0x00]); + // subscribeCharacteristic(service.serviceId, characteristicId); + } + } + } + + /// function to read characteristic + Future readCharacteristic(Uuid serviceId, Uuid characteristicId) async { + _flutterReactiveBle = FlutterReactiveBle(); + final QualifiedCharacteristic qualifiedCharacteristic = QualifiedCharacteristic(characteristicId: characteristicId, serviceId: serviceId, deviceId: connectedBluetoothDevice.id); + + final List response = await _flutterReactiveBle.readCharacteristic(qualifiedCharacteristic) + .catchError((e) { + debugPrint('error is ${e.toString()}'); + }); + + debugPrint('read characteristics response ${response.toString()}'); + } + + /// function to write characteristic value + Future writeCharacteristic(Uuid serviceId, Uuid characteristicId, List byteArray) async { + _flutterReactiveBle = FlutterReactiveBle(); + final QualifiedCharacteristic qualifiedCharacteristic = QualifiedCharacteristic(characteristicId: characteristicId, serviceId: serviceId, deviceId: connectedBluetoothDevice.id); + + await _flutterReactiveBle.writeCharacteristicWithResponse(qualifiedCharacteristic, value: byteArray); + } + + /// function to subscribe to a characteristic and listen to the updates in the value of [characteristic] + Future subscribeCharacteristic(Uuid serviceId, Uuid characteristicId) async { + _flutterReactiveBle = FlutterReactiveBle(); + final QualifiedCharacteristic qualifiedCharacteristic = QualifiedCharacteristic(characteristicId: characteristicId, serviceId: serviceId, deviceId: connectedBluetoothDevice.id); + + try { + _flutterReactiveBle.subscribeToCharacteristic(qualifiedCharacteristic).listen((value) { + debugPrint('the value of characteristic is ${value.toString()}'); + }).onError((e) { + debugPrint('error is ${e.toString()}'); + }); + } catch (e) { + debugPrint(e.toString()); + rethrow; + } + } + + /// function to return [_detectedBluetoothDevices] + List get bluetoothDevices { + return [..._detectedBluetoothDevices]; + } + + /// function to return [connectedBluetoothDevice] + DiscoveredDevice get getConnectedDevice => connectedBluetoothDevice; + + /// function to check whether the device is connected + bool isConnected() { + return connectedBluetoothDevice == null ? false : true; + } + +} \ No newline at end of file diff --git a/lib/providers/carsProvider.dart b/lib/providers/carsProvider.dart index e89bb46..b63ace4 100644 --- a/lib/providers/carsProvider.dart +++ b/lib/providers/carsProvider.dart @@ -4,71 +4,18 @@ import '../models/car.dart'; class CarsProvider with ChangeNotifier { Car _selectedCar; - List _carsList = [ - Car( - id: '1', - manufacturer: 'Volkswagen', - model: 'S309', - constructionYear: 1930, - fuelType: 'Diesel', - engineDisplacement: 1600, - // isSelected: true, - ), - Car( - id: '2', - manufacturer: 'BMW', - model: 'M3-GT', - constructionYear: 1930, - fuelType: 'Diesel', - engineDisplacement: 1600, - // isSelected: false, - ), - Car( - id: '3', - manufacturer: 'Ferrari', - model: 'Ultra', - constructionYear: 1930, - fuelType: 'Diesel', - engineDisplacement: 1600, - // isSelected: false, - ), - Car( - id: '4', - manufacturer: 'Hector', - model: 'SH43', - constructionYear: 1930, - fuelType: 'Diesel', - engineDisplacement: 1600, - // isSelected: false, - ), - Car( - id: '5', - manufacturer: 'Audi', - model: 'R8', - constructionYear: 1930, - fuelType: 'Diesel', - engineDisplacement: 1600, - // isSelected: false, - ), - Car( - id: '6', - manufacturer: 'Mercedez', - model: 'Benz', - constructionYear: 1930, - fuelType: 'Diesel', - engineDisplacement: 1600, - // isSelected: false, - ), - Car( - id: '7', - manufacturer: 'Rolls Royce', - model: 'Antique', - constructionYear: 1930, - fuelType: 'Diesel', - engineDisplacement: 1600, - // isSelected: false, - ), - ]; + List _carsList; + + set setCarsList(List> carsList) { + final List list = []; + for (final Map carMap in carsList) { + list.add(Car.fromJson(carMap)); + + notifyListeners(); + } + + _carsList = list; + } Car get getSelectedCar { return _selectedCar; @@ -93,7 +40,7 @@ class CarsProvider with ChangeNotifier { void deleteCar(Car car) { _carsList.removeWhere( (Car carItem) { - return (carItem.id == car.id); + return carItem.id == car.id; }, ); diff --git a/lib/providers/fuelingsProvider.dart b/lib/providers/fuelingsProvider.dart new file mode 100644 index 0000000..8e7a09d --- /dev/null +++ b/lib/providers/fuelingsProvider.dart @@ -0,0 +1,82 @@ +import 'package:flutter/foundation.dart'; + +import '../models/fueling.dart'; + +// Provides data of the Log Book +class FuelingsProvider with ChangeNotifier { + // Dummy list for fueling data + final List _fuelingsList = [ + // Fueling( + // id: '1', + // car: Car( + // id: '1', + // manufacturer: 'Volkswagen', + // model: 'S309', + // constructionYear: 1930, + // fuelType: 'Diesel', + // engineDisplacement: 1600, + // // isSelected: true, + // ), + // mileage: '25', + // fueledVolume: '25', + // totalPrice: '25', + // pricePerLitre: '25', + // partialFueling: true, + // missedPreviousFueling: true, + // comment: 'Hello There', + // ), + // Fueling( + // id: '2', + // car: Car( + // id: '2', + // manufacturer: 'BMW', + // model: 'M3-GT', + // constructionYear: 1930, + // fuelType: 'Diesel', + // engineDisplacement: 1600, + // // isSelected: false, + // ), + // mileage: '25', + // fueledVolume: '25', + // totalPrice: '25', + // pricePerLitre: '25', + // partialFueling: false, + // missedPreviousFueling: true, + // comment: 'Hello There', + // ), + // Fueling( + // id: '3', + // car: Car( + // id: '3', + // manufacturer: 'Ferrari', + // model: 'Ultra', + // constructionYear: 1930, + // fuelType: 'Diesel', + // engineDisplacement: 1600, + // // isSelected: false, + // ), + // mileage: '25', + // fueledVolume: '25', + // totalPrice: '25', + // pricePerLitre: '25', + // partialFueling: false, + // missedPreviousFueling: true, + // comment: 'Hello There', + // ), + ]; + + // Adds newly created fueling data and notifies log book screen + void addFueling(Fueling fueling) { + _fuelingsList.add(fueling); + + notifyListeners(); + } + + // TODO: Write method to delete using ID + void deleteFueling() {} + + // get method to get the list data + List get getFuelingsList { + return _fuelingsList; + } +} diff --git a/lib/providers/locationStatusProvider.dart b/lib/providers/locationStatusProvider.dart index 1a78557..1906692 100644 --- a/lib/providers/locationStatusProvider.dart +++ b/lib/providers/locationStatusProvider.dart @@ -12,7 +12,7 @@ class LocationStatusProvider extends ChangeNotifier { } /// function to update location stream upon listening to status updates - void sendStatusUpdates() async { + Future sendStatusUpdates() async { LocationStatusChecker().onStatusChange.listen((status) { locationStatus = status; notifyListeners(); diff --git a/lib/providers/tracksProvider.dart b/lib/providers/tracksProvider.dart index 017238d..9c581ba 100644 --- a/lib/providers/tracksProvider.dart +++ b/lib/providers/tracksProvider.dart @@ -9,9 +9,9 @@ class TracksProvider with ChangeNotifier { // and notifies widgets once done void setTracks(Map json) { if (json['tracks'] != null) { - List tracks = []; + final List tracks = []; json['tracks'].forEach((v) { - tracks.add(new Track.fromJson(v)); + tracks.add(Track.fromJson(v as Map)); }); _tracks = tracks; diff --git a/lib/providers/userStatsProvider.dart b/lib/providers/userStatsProvider.dart index 9441322..13e2798 100644 --- a/lib/providers/userStatsProvider.dart +++ b/lib/providers/userStatsProvider.dart @@ -15,6 +15,6 @@ class UserStatsProvider with ChangeNotifier { } void removeStats() { - this._userStats = null; + _userStats = null; } } diff --git a/lib/screens/bluetoothDevicesScreen.dart b/lib/screens/bluetoothDevicesScreen.dart index 07c5dab..c5c7afd 100644 --- a/lib/screens/bluetoothDevicesScreen.dart +++ b/lib/screens/bluetoothDevicesScreen.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; - +import 'package:provider/provider.dart'; import 'package:system_shortcuts/system_shortcuts.dart'; -import 'package:toast/toast.dart'; -import '../widgets/bleDialog.dart'; import '../constants.dart'; +import '../widgets/bleDialog.dart'; +import '../providers/bluetoothProvider.dart'; class BluetoothDevicesScreen extends StatefulWidget { static const routeName = '/bluetoothDeviceScreen'; @@ -18,11 +18,8 @@ class _BluetoothDevicesScreenState extends State { BleStatus _state; bool _isScanning = false; FlutterReactiveBle flutterReactiveBlue = FlutterReactiveBle(); - final List detectedBluetoothDevices = []; int selected = -1; - String selectedDeviceId = ''; - List _services; - Map> _servicesCharacteristics; + BluetoothProvider bluetoothProvider; /// Toggles bluetooth on and off // Works only on Android @@ -34,155 +31,27 @@ class _BluetoothDevicesScreenState extends State { @override void initState() { determineBluetoothStatus(); - _services = []; - _servicesCharacteristics = {}; super.initState(); } - /// function to scan the devices and populate the [detectedBluetoothDevices] list - void startScan() { - // withServices specifies the advertised services IDs to look for - // but if an empty list is passed as a parameter, all advertising devices are listed - flutterReactiveBlue = FlutterReactiveBle(); - determineBluetoothStatus(); - if (_state == BleStatus.ready) { - flutterReactiveBlue.scanForDevices(withServices: [], scanMode: ScanMode.balanced).listen((device) { - populateList(device); - }).onError(handleError); - } - } - - /// function to add the [discoveredDevice] to [detectedBluetoothDevices] list - void populateList(DiscoveredDevice discoveredDevice) { - if (detectedBluetoothDevices.isEmpty) { - print(discoveredDevice.toString()); - setState(() { - detectedBluetoothDevices.add(discoveredDevice); - }); - return; - } - - bool addDeviceToList = true; - for (DiscoveredDevice device in detectedBluetoothDevices) { - if (device.id == discoveredDevice.id) { - addDeviceToList = false; - break; - } - } - - if (addDeviceToList) { - print(discoveredDevice.toString()); - setState(() { - detectedBluetoothDevices.add(discoveredDevice); - }); - } - } - /// function to determine [_state] the status of Bluetooth - void determineBluetoothStatus() { - flutterReactiveBlue.statusStream.listen((status) { - // code for handling status updates - setState(() { - _state = status; - }); - }); - } - - /// function to handle error while scanning - void handleError(e) { - print(e.toString()); - } - - /// function to stop scanning by deinitializing [flutterReactiveBlue] - void stopScan() { - flutterReactiveBlue.deinitialize(); - } - - /// function to connect to selected device - void connectToDevice() { - flutterReactiveBlue.connectToDevice( - id: selectedDeviceId, - connectionTimeout: Duration(seconds: 10), - // servicesWithCharacteristicsToDiscover: // TODO: specify characteristics for OBD-II for faster connection - ).listen(sendConnectionStatusUpdates).onError(handleError); - } - - /// callback function to send connection status updates - void sendConnectionStatusUpdates(ConnectionStateUpdate connectionStateUpdate) { - print('connected to ${detectedBluetoothDevices[selected].name}'); - print('connection state ${connectionStateUpdate.connectionState.toString()}'); - print('device id ${connectionStateUpdate.deviceId}'); - print('failure ${connectionStateUpdate.failure.toString()}'); - - if (connectionStateUpdate.failure != null) { - debugPrint('connection unsuccessful'); - Toast.show( - 'Cannot connect to ${detectedBluetoothDevices[selected].name.isNotEmpty ? detectedBluetoothDevices[selected].name : selectedDeviceId}', - context, - duration: Toast.LENGTH_LONG, - gravity: Toast.BOTTOM, - backgroundColor: Colors.black87 - ); - } - - if (connectionStateUpdate.connectionState == DeviceConnectionState.connected) { - Toast.show( - 'Connected to ${detectedBluetoothDevices[selected].name.isNotEmpty ? detectedBluetoothDevices[selected].name : selectedDeviceId}', - context, - duration: Toast.LENGTH_LONG, - gravity: Toast.BOTTOM, - backgroundColor: Colors.black87 - ); - discoverServices(); - } - } - - /// function to discover services - void discoverServices() async { - _services = await flutterReactiveBlue.discoverServices(selectedDeviceId); - debugPrint('${_services.toString()}'); - } - - /// function to add the [serviceId] and [characteristicIds] to [_servicesCharacteristics] - void logCharacteristicsForServices() async { - for (DiscoveredService service in _services) { - _servicesCharacteristics[service.serviceId] = service.characteristicIds; - debugPrint('service id ${service.serviceId.toString()} characteristics Id ${service.characteristicIds.toString()}'); - } - } - - /// function to read characteristic - void readCharacteristic(Uuid serviceId, Uuid characteristicId) async { - QualifiedCharacteristic qualifiedCharacteristic = QualifiedCharacteristic(characteristicId: characteristicId, serviceId: serviceId, deviceId: selectedDeviceId); - - final List response = await flutterReactiveBlue.readCharacteristic(qualifiedCharacteristic) - .catchError((e) { - debugPrint('error is ${e.toString()}'); + Future determineBluetoothStatus() async { + final BleStatus status = await Provider.of(context, listen: false).bluetoothState(); + setState(() { + _state = status; }); - - debugPrint('read characteristics response ${response.toString()}'); - } - - /// function to write characteristic value - void writeCharacteristic(Uuid serviceId, Uuid characteristicId, List byteArray) async { - QualifiedCharacteristic qualifiedCharacteristic = QualifiedCharacteristic(characteristicId: characteristicId, serviceId: serviceId, deviceId: selectedDeviceId); - - await flutterReactiveBlue.writeCharacteristicWithResponse(qualifiedCharacteristic, value: byteArray); } - /// function to subscribe to a characteristic and listen to the updates in the value of [characteristic] - void subscribeCharacteristic(Uuid serviceId, Uuid characteristicId) async { - QualifiedCharacteristic qualifiedCharacteristic = QualifiedCharacteristic(characteristicId: characteristicId, serviceId: serviceId, deviceId: selectedDeviceId); - - flutterReactiveBlue.subscribeToCharacteristic(qualifiedCharacteristic).listen((value) { - debugPrint('the value of characteristic is ${value.toString()}'); - }); + @override + void didChangeDependencies() { + bluetoothProvider = Provider.of(context, listen: false); + super.didChangeDependencies(); } @override void dispose() { super.dispose(); - stopScan(); + flutterReactiveBlue.deinitialize(); } @override @@ -196,13 +65,13 @@ class _BluetoothDevicesScreenState extends State { StreamBuilder( stream: flutterReactiveBlue.statusStream, initialData: BleStatus.unknown, - builder: (context, snapshot) { + builder: (context, AsyncSnapshot snapshot) { _state = snapshot.data; return Switch( value: _state == BleStatus.ready ? true : false, onChanged: (value) async { if (_isScanning) { - stopScan(); + bluetoothProvider.stopScan(); setState(() { _isScanning = false; }); @@ -218,22 +87,13 @@ class _BluetoothDevicesScreenState extends State { // Button to start and stop scanning floatingActionButton: FloatingActionButton( backgroundColor: _isScanning ? Colors.red : kSpringColor, - child: _isScanning - ? Icon( - Icons.stop, - color: Colors.white, - ) - : Icon( - Icons.search_rounded, - color: Colors.white, - ), onPressed: () { if (!_isScanning) { if (_state == BleStatus.ready) { setState(() { _isScanning = true; }); - startScan(); + bluetoothProvider.startScan(); } else { showDialog( context: context, @@ -245,42 +105,57 @@ class _BluetoothDevicesScreenState extends State { ); } } else { - stopScan(); + bluetoothProvider.stopScan(); setState(() { _isScanning = false; }); } }, + child: _isScanning + ? const Icon( + Icons.stop, + color: Colors.white, + ) + : const Icon( + Icons.search_rounded, + color: Colors.white, + ), ), - body: Container( - child: ListView.builder( - itemBuilder: (BuildContext context, index) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 5), - child: ListTile( - title: Text( - (detectedBluetoothDevices[index].name == null || detectedBluetoothDevices[index].name.trim().length == 0) ? 'Unknown device' : detectedBluetoothDevices[index].name, + body: Consumer( + builder: (context, bluetoothProvider, child) { + final List detectedBluetoothDevices = bluetoothProvider.bluetoothDevices; + + return ListView.builder( + itemBuilder: (BuildContext context, index) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: ListTile( + title: Text( + (detectedBluetoothDevices[index].name == null || detectedBluetoothDevices[index].name.trim().isEmpty) ? 'Unknown device' : detectedBluetoothDevices[index].name, + ), + subtitle: Text( + detectedBluetoothDevices[index].id, + ), + trailing: Radio( + activeColor: kSpringColor, + value: index, + groupValue: selected, + onChanged: (int value) async { + setState(() { + selected = value; + }); + final bool connectedStatus = await bluetoothProvider.connectToDevice(detectedBluetoothDevices[value], context, value); + if (connectedStatus) { + bluetoothProvider.discoverServices(); + } + }, + ), ), - subtitle: Text( - detectedBluetoothDevices[index].id, - ), - trailing: Radio( - activeColor: kSpringColor, - value: index, - groupValue: selected, - onChanged: (value) { - setState(() { - selected = value; - selectedDeviceId = detectedBluetoothDevices[value].id; - }); - connectToDevice(); - }, - ), - ), - ); - }, - itemCount: detectedBluetoothDevices.length, - ), + ); + }, + itemCount: detectedBluetoothDevices.length, + ); + } ), ); } diff --git a/lib/screens/carScreen.dart b/lib/screens/carScreen.dart index 94aebff..a22a296 100644 --- a/lib/screens/carScreen.dart +++ b/lib/screens/carScreen.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../providers/carsProvider.dart'; -import './createCarScreen.dart'; -import '../models/car.dart'; +import 'createCarScreen.dart'; import '../constants.dart'; +import '../widgets/carScreenWidgets/carsListWidget.dart'; class CarScreen extends StatefulWidget { static const routeName = '/carScreen'; @@ -23,13 +20,13 @@ class _CarScreenState extends State { elevation: 0, actions: [ GestureDetector( - child: Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Icon(Icons.add), - ), onTap: () { Navigator.of(context).pushNamed(CreateCarScreen.routeName); }, + child: const Padding( + padding: EdgeInsets.only(right: 10.0), + child: Icon(Icons.add), + ), ), ], @@ -41,51 +38,8 @@ class _CarScreenState extends State { centerTitle: true, ), body: Container( - child: Consumer( - builder: (_, carsProvider, child) { - List carsList = carsProvider.getCarsList; - Car selectedCar = carsProvider.getSelectedCar; - if (carsList.isNotEmpty) { - return ListView.builder( - padding: EdgeInsets.only(bottom: 20), - itemCount: carsList.length, - itemBuilder: (_, index) { - return GestureDetector( - onTap: () { - carsProvider.setSelectedCar = carsList[index]; - }, - child: ListTile( - leading: Icon(Icons.drive_eta_sharp), - title: Text(carsList[index].manufacturer + - ' - ' + - carsList[index].model), - subtitle: Text( - carsList[index].constructionYear.toString() + - ', ' + - carsList[index].engineDisplacement.toString() + - ', ' + - carsList[index].fuelType), - trailing: Radio( - onChanged: (bool value) {}, - groupValue: true, - value: selectedCar == null - ? false - : (carsList[index].id == selectedCar.id - ? true - : false), - ), - ), - ); - }, - ); - } - return Center( - child: Text( - 'There are no cars here', - ), - ); - }, - ), + padding: const EdgeInsets.all(15), + child: CarsListWidget(), ), ); } diff --git a/lib/screens/createCarScreen.dart b/lib/screens/createCarScreen.dart index 3f9674e..efdeb17 100644 --- a/lib/screens/createCarScreen.dart +++ b/lib/screens/createCarScreen.dart @@ -1,31 +1,25 @@ -import 'package:flutter/material.dart'; +import 'Package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:uuid/uuid.dart'; +import '../widgets/createCarWidgets/attributesOption.dart'; +import '../widgets/createCarWidgets/tsnHsnOption.dart'; -import '../constants.dart' as constants; -import '../models/car.dart'; -import '../providers/carsProvider.dart'; +import '../widgets/tabButtonsPage.dart'; +import '../utils/enums.dart'; class CreateCarScreen extends StatefulWidget { - static const routeName = '/createCarScreen'; + static const String routeName = '/createCarScreen'; @override _CreateCarScreenState createState() => _CreateCarScreenState(); } class _CreateCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - Car newCar = Car(); + CreateCarTab selectedTab = CreateCarTab.hsnTsn; @override Widget build(BuildContext context) { - MediaQueryData _mediaQuery = MediaQuery.of(context); - double height = _mediaQuery.size.height; - double width = _mediaQuery.size.width; - return Scaffold( appBar: AppBar( - backgroundColor: Color.fromARGB(255, 23, 33, 43), + backgroundColor: const Color.fromARGB(255, 23, 33, 43), elevation: 0, // enviroCar logo @@ -35,174 +29,18 @@ class _CreateCarScreenState extends State { ), centerTitle: true, ), - body: SafeArea( - child: Container( - height: height, - width: width, - padding: EdgeInsets.fromLTRB(width * 0.05, 0, width * 0.05, 0), - child: SingleChildScrollView( - padding: EdgeInsets.only(top: height * 0.03), - child: Form( - key: _formKey, - child: Column( - children: [ - Text( - 'Please enter the details of your vehicle. Be precise. Parts of this information are especially important for calculating estimated values such as consumption.', - style: TextStyle( - color: Colors.grey, - fontSize: 15, - ), - ), - - SizedBox( - height: height * 0.03, - ), - - // Manufacturer - TextFormField( - decoration: constants.inputDecoration.copyWith( - labelText: 'Manufacturer', - ), - onChanged: (value) { - newCar.manufacturer = value; - }, - validator: (value) { - if (value.isEmpty || value == null) { - return 'Required'; - } - return null; - }, - ), - - SizedBox( - height: height * 0.03, - ), - - // Model - TextFormField( - decoration: constants.inputDecoration.copyWith( - labelText: 'Model', - ), - onChanged: (value) { - newCar.model = value; - }, - validator: (value) { - if (value.isEmpty || value == null) { - return 'Required'; - } - return null; - }, - ), - - SizedBox( - height: height * 0.03, - ), - - // Construction year - TextFormField( - keyboardType: TextInputType.number, - decoration: constants.inputDecoration.copyWith( - labelText: 'Construction year', - ), - onChanged: (value) { - newCar.constructionYear = int.parse(value); - }, - validator: (value) { - if (value.isEmpty || value == null) { - return 'Required'; - } - return null; - }, - ), - - SizedBox( - height: height * 0.03, - ), - - // Fuel Type - TextFormField( - decoration: constants.inputDecoration.copyWith( - labelText: 'Fuel Type', - ), - onChanged: (value) { - newCar.fuelType = value; - }, - validator: (value) { - if (value.isEmpty || value == null) { - return 'Required'; - } - return null; - }, - ), - - SizedBox( - height: height * 0.03, - ), - - // Engine Displacement - TextFormField( - keyboardType: TextInputType.number, - decoration: constants.inputDecoration.copyWith( - labelText: 'Engine Displacement', - ), - onChanged: (value) { - newCar.engineDisplacement = int.parse(value); - }, - validator: (value) { - if (value.isEmpty || value == null) { - return 'Required'; - } - return null; - }, - ), - - SizedBox( - height: height * 0.03, - ), - - // Login Button - GestureDetector( - child: Container( - width: double.infinity, - height: 50, - decoration: BoxDecoration( - color: constants.kSpringColor, - borderRadius: BorderRadius.all(Radius.circular(5)), - ), - child: Center( - child: Text( - 'Create Car', - style: TextStyle( - color: Colors.white, - fontSize: 17, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - onTap: () { - if (_formKey.currentState.validate()) { - Uuid uuid = Uuid(); - newCar.id = uuid.v1(); - - print(newCar.id); - - CarsProvider carsProvider = - Provider.of(context, listen: false); - - carsProvider.addCar(newCar); - Navigator.pop(context); - } - }, - ), - - SizedBox( - height: height * 0.03, - ), - ], - ), + body: SingleChildScrollView( + child: Column( + children: [ + TabButtonsPage( + button1Title: 'HSN/TSN', + button2Title: 'Attributes', + tab1: CreateCarTab.hsnTsn, + tab2: CreateCarTab.attributes, + page1: TsnHsnOption(), + page2: AttributesOption(), ), - ), + ], ), ), ); diff --git a/lib/screens/createFuelingScreen.dart b/lib/screens/createFuelingScreen.dart new file mode 100644 index 0000000..5870b47 --- /dev/null +++ b/lib/screens/createFuelingScreen.dart @@ -0,0 +1,252 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import '../models/car.dart'; +import '../models/fueling.dart'; +import '../providers/carsProvider.dart'; +import '../providers/fuelingsProvider.dart'; +import '../widgets/singleRowForm.dart'; +import '../widgets/button.dart'; +import '../widgets/dividerLine.dart'; +import '../widgets/titleWidget.dart'; +import '../constants.dart'; + +// Screen to create fueling logs +class CreateFuelingScreen extends StatefulWidget { + static const String routeName = '/createFuelingScreen'; + + @override + _CreateFuelingScreenState createState() => _CreateFuelingScreenState(); +} + +class _CreateFuelingScreenState extends State { + // Form key to validate the fueling form + static final GlobalKey _formKey = GlobalKey(); + + bool partialFueling; + bool missedPreviousFueling; + + Car selectedCar; + + // Controllers for textfields to fetch the input data + TextEditingController mileageController; + TextEditingController fueledVolumeController; + TextEditingController pricePerLitreController; + TextEditingController totalPriceController; + TextEditingController commentController; + + // Triggered when 'Add' button is pressed after filling the form + // Creates the fueling object and stores it in Provider + void addFueling() { + if (_formKey.currentState.validate()) { + final Fueling fueling = Fueling( + // TODO: generate uuid for id + id: '32', + car: selectedCar, + mileage: mileageController.text, + fueledVolume: fueledVolumeController.text, + totalPrice: totalPriceController.text, + pricePerLitre: pricePerLitreController.text, + partialFueling: partialFueling, + missedPreviousFueling: missedPreviousFueling, + comment: commentController.text, + ); + + // Provider instance to add the new fueling log to the list + final FuelingsProvider fuelingsProvider = + Provider.of(context, listen: false); + + fuelingsProvider.addFueling(fueling); + + // Navigates user to the Log book screen after fueling log is added + Navigator.of(context).pop(); + } + } + + // Navigates user to the Log book screen when 'Cancel' button is pressed + void cancelFueling() { + Navigator.of(context).pop(); + } + + @override + void initState() { + super.initState(); + + partialFueling = false; + missedPreviousFueling = false; + + mileageController = TextEditingController(); + fueledVolumeController = TextEditingController(); + pricePerLitreController = TextEditingController(); + totalPriceController = TextEditingController(); + commentController = TextEditingController(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: kGreyColor, + elevation: 0, + // enviroCar logo + title: const Text('LogBook'), + centerTitle: true, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all( + 15, + ), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const TitleWidget(title: 'Fueling Detals'), + const SizedBox( + height: 20, + ), + + // Consumer provides the Cars data in the dropdown button + Consumer( + builder: (_, carsProvider, child) { + final List carsList = carsProvider.getCarsList; + + // Drop Down button to select Car + return DropdownButtonFormField( + decoration: inputDecoration.copyWith( + labelText: 'Car', + ), + validator: (value) { + if (value == null) { + return 'Required'; + } + return null; + }, + elevation: 24, + icon: const Icon(Icons.arrow_drop_down_rounded), + value: selectedCar, + onChanged: (Car newValue) { + FocusScope.of(context).requestFocus(FocusNode()); + setState(() { + selectedCar = newValue; + }); + }, + items: List.generate( + carsList.length, + (index) { + return DropdownMenuItem( + value: carsList[index], + child: Text( + '${carsList[index].manufacturer} ${carsList[index].model}'), + ); + }, + ), + ); + }, + ), + DividerLine(), + + // Mileage textfield + SingleRowForm( + title: 'Mileage', + hint: '0.00 km', + textEditingController: mileageController, + ), + + // Fueled volume textfield + SingleRowForm( + title: 'Fueled Volume', + hint: '0.00 I', + textEditingController: fueledVolumeController, + ), + + // Price per litre textfield + SingleRowForm( + title: 'Price per litre', + hint: '0.00 \$/L', + textEditingController: pricePerLitreController, + ), + + // Total price textfield + SingleRowForm( + title: 'Total Price', + hint: '0.00 \$', + textEditingController: totalPriceController, + ), + DividerLine(), + + // Checkbox for partial fueling + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + title: const Text('Partial Fueling?'), + value: partialFueling, + onChanged: (bool val) { + setState(() { + partialFueling = val; + }); + }, + ), + + // Checkbox for missed previous fueling + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + title: const Text('Missed Previous Fueling?'), + value: missedPreviousFueling, + onChanged: (bool val) { + setState(() { + missedPreviousFueling = val; + }); + }, + ), + DividerLine(), + + // Textfield for comments + TextFormField( + decoration: inputDecoration.copyWith( + labelText: 'Comments', + ), + maxLines: 5, + controller: commentController, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + // Cancel button + Expanded( + child: Button( + title: 'Cancel', + onTap: cancelFueling, + color: Colors.red, + ), + ), + + const SizedBox( + width: 10, + ), + + // Add button + Expanded( + child: Button( + title: 'Add', + onTap: addFueling, + color: kSpringColor, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/dashboardScreen.dart b/lib/screens/dashboardScreen.dart index 629774a..7556917 100644 --- a/lib/screens/dashboardScreen.dart +++ b/lib/screens/dashboardScreen.dart @@ -1,4 +1,6 @@ +import '../providers/bluetoothProvider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:provider/provider.dart'; import '../widgets/dashboardWidgets/statsWidget.dart'; @@ -15,6 +17,7 @@ import './carScreen.dart'; import '../providers/carsProvider.dart'; import '../models/car.dart'; import '../utils/enums.dart'; +import '../widgets/button.dart'; class DashboardScreen extends StatefulWidget { @override @@ -24,139 +27,145 @@ class DashboardScreen extends StatefulWidget { class _DashboardScreenState extends State { @override Widget build(BuildContext context) { - return Container( - child: SingleChildScrollView( - child: Column( - children: [ - // Widget that shows the stats of user - StatsWidget(), + return SingleChildScrollView( + child: Column( + children: [ + // Widget that shows the stats of user + StatsWidget(), - // Record Settings Button - Padding( - padding: const EdgeInsets.only( - top: 15.0, - bottom: 15.0, - ), - child: GestureDetector( - child: Container( - child: Center( - child: Text( - 'Recording Settings', - style: TextStyle( - color: Colors.white, - fontSize: deviceHeight * 0.02, - ), - ), - ), - height: deviceHeight * 0.05, - width: deviceWidth * 0.5, - decoration: BoxDecoration( - color: kSpringColor, - borderRadius: BorderRadius.circular(20), - ), - ), - ), - ), - - // Bluetooth, OBD, GPS and Car buttons - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Consumer( - builder: (context, provider, child) { - return DashboardIconButton( - routeName: BluetoothDevicesScreen.routeName, - assetName: 'assets/icons/bluetooth.svg', - buttonColor: provider.bluetoothState == BluetoothConnectionStatus.ON ? kSpringColor : kErrorColor, - ); - }, - ), - DashboardIconButton( - routeName: BluetoothDevicesScreen.routeName, - assetName: 'assets/icons/smartphone.svg', - ), - Consumer( - builder: (context, provider, child) { - return DashboardIconButton( - routeName: BluetoothDevicesScreen.routeName, - assetName: 'assets/icons/car.svg', - buttonColor: provider.getSelectedCar != null ? kSpringColor : kErrorColor, - ); - }, - ), - Consumer( - builder: (context, provider, child) { - return DashboardIconButton( - routeName: MapScreen.routeName, - assetName: 'assets/icons/gps.svg', - buttonColor: provider.locationState == LocationStatus.enabled ? kSpringColor : kErrorColor, - ); - }, - ), - ], - ), - SizedBox( - height: deviceHeight * 0.02, - ), - - // Bluetooth Card - DashboardCard( - assetName: 'assets/icons/bluetooth.svg', - title: 'OBD-II V9', - subtitle: 'ELM327', - routeName: BluetoothDevicesScreen.routeName, - ), - SizedBox( - height: deviceHeight * 0.02, - ), - - // Car Card - Consumer( - builder: (_, carsProvider, child) { - Car selectedCar = carsProvider.getSelectedCar; - return DashboardCard( - assetName: 'assets/icons/car.svg', - title: selectedCar == null - ? 'No car selected' - : '${selectedCar.manufacturer}, ${selectedCar.model}', - subtitle: selectedCar == null - ? 'Select a car' - : '${selectedCar.constructionYear}, ${selectedCar.engineDisplacement}, ${selectedCar.fuelType}', - routeName: CarScreen.routeName, - iconBackgroundColor: carsProvider.getSelectedCar != null ? kSpringColor : kErrorColor, - ); - }, - ), - SizedBox( - height: deviceHeight * 0.02, + // Record Settings Button + Padding( + padding: const EdgeInsets.only( + top: 15.0, + bottom: 15.0, ), - - // Start Tracks Button - GestureDetector( + child: GestureDetector( child: Container( + height: deviceHeight * 0.05, + width: deviceWidth * 0.5, + decoration: BoxDecoration( + color: kSpringColor, + borderRadius: BorderRadius.circular(20), + ), child: Center( child: Text( - 'Start Track', + 'Recording Settings', style: TextStyle( color: Colors.white, - fontWeight: FontWeight.bold, fontSize: deviceHeight * 0.02, ), ), ), - height: deviceHeight * 0.065, - width: deviceWidth * 0.4, - decoration: BoxDecoration( - color: kSpringColor, - borderRadius: BorderRadius.circular(10), - ), ), ), - SizedBox( - height: deviceHeight * 0.02, + ), + + // Bluetooth, OBD, GPS and Car buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Consumer( + builder: (context, provider, child) { + return DashboardIconButton( + routeName: BluetoothDevicesScreen.routeName, + assetName: 'assets/icons/bluetooth.svg', + buttonColor: provider.bluetoothState == BluetoothConnectionStatus.ON ? kSpringColor : kErrorColor, + ); + }, + ), + + Consumer( + builder: (context, bluetoothProvider, child) { + final bool isConnected = bluetoothProvider.isConnected(); + return DashboardIconButton( + routeName: BluetoothDevicesScreen.routeName, + assetName: 'assets/icons/smartphone.svg', + buttonColor: isConnected ? kSpringColor : kErrorColor, + ); + } + ), + + Consumer( + builder: (context, provider, child) { + return DashboardIconButton( + routeName: BluetoothDevicesScreen.routeName, + assetName: 'assets/icons/car.svg', + buttonColor: provider.getSelectedCar != null ? kSpringColor : kErrorColor, + ); + }, + ), + Consumer( + builder: (context, provider, child) { + return DashboardIconButton( + routeName: MapScreen.routeName, + assetName: 'assets/icons/gps.svg', + buttonColor: provider.locationState == LocationStatus.enabled ? kSpringColor : kErrorColor, + ); + }, + ), + ], + ), + SizedBox( + height: deviceHeight * 0.02, + ), + + // Bluetooth Card + Consumer( + builder: (context, bluetoothProvider, child) { + final bool isConnected = bluetoothProvider.isConnected(); + DiscoveredDevice connectedDevice; + if (isConnected) { + connectedDevice = bluetoothProvider.getConnectedDevice; + } + + return DashboardCard( + assetName: 'assets/icons/bluetooth.svg', + title: isConnected ? (connectedDevice.name.trim().isNotEmpty ? connectedDevice.name : 'Unknown Device') : 'No OBD-II adapter selected', + subtitle: isConnected ? connectedDevice.id : 'Click here to select one', + routeName: BluetoothDevicesScreen.routeName, + iconBackgroundColor: isConnected ? kSpringColor : kErrorColor, + ); + } + ), + + SizedBox( + height: deviceHeight * 0.02, + ), + + // Car Card + Consumer( + builder: (_, carsProvider, child) { + final Car selectedCar = carsProvider.getSelectedCar; + return DashboardCard( + assetName: 'assets/icons/car.svg', + title: selectedCar == null + ? 'No car selected' + : '${selectedCar.manufacturer}, ${selectedCar.model}', + subtitle: selectedCar == null + ? 'Select a car' + : '${selectedCar.constructionYear}, ${selectedCar.engineDisplacement}, ${selectedCar.fuelType}', + routeName: CarScreen.routeName, + iconBackgroundColor: carsProvider.getSelectedCar != null ? kSpringColor : kErrorColor, + ); + }, + ), + SizedBox( + height: deviceHeight * 0.02, + ), + + // Start Tracks Button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Button( + title: 'Start Track', + color: kSpringColor, + onTap: () {}, ), - ], - ), + ), + SizedBox( + height: deviceHeight * 0.02, + ), + ], ), ); } diff --git a/lib/screens/helpScreen.dart b/lib/screens/helpScreen.dart new file mode 100644 index 0000000..077b8eb --- /dev/null +++ b/lib/screens/helpScreen.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; + +import '../constants.dart'; +import '../globals.dart'; +import '../values/helpScreenValues.dart'; +import '../widgets/dividerLine.dart'; + +class HelpScreen extends StatelessWidget { + static const String routeName = '/helpScreen'; + + // For heading 1 + Widget buildH1({@required String headline1}) { + return Text( + headline1, + style: const TextStyle( + fontSize: 22, + ), + ); + } + + // For heading 2 + Widget buildH2({@required String headline2}) { + return Text( + headline2, + style: const TextStyle( + fontSize: 17, + ), + ); + } + + // For paragraphs + Widget buildParagraph({@required String paragraph}) { + return Text( + paragraph, + style: const TextStyle( + color: Colors.black45, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: kGreyColor, + elevation: 0, + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: deviceWidth, + color: kGreyColor, + child: Column( + children: [ + Image.asset( + 'assets/images/img_envirocar_logo_white.png', + scale: 4.5, + ), + const SizedBox( + height: 20, + ), + const Text( + 'Help', + style: TextStyle( + color: Colors.white, + fontSize: 30, + ), + ), + const SizedBox( + height: 20, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Headline 1 + buildH1(headline1: help_headline_1), + buildParagraph(paragraph: help_content_1), + + DividerLine(), + + // Headline 2 + buildH1(headline1: help_headline_2), + const SizedBox( + height: 5, + ), + buildH2(headline2: help_headline_2_1), + buildParagraph(paragraph: help_content_2_1), + buildParagraph(paragraph: help_content_2_1_enum_1), + buildParagraph(paragraph: help_content_2_1_enum_2), + buildParagraph(paragraph: help_content_2_1_enum_3), + + buildH2(headline2: help_headline_2_2), + buildParagraph(paragraph: help_content_2_2), + + buildH2(headline2: help_headline_2_3), + buildParagraph(paragraph: help_content_2_3), + + DividerLine(), + + // Headline 3 + buildH1(headline1: help_headline_3), + buildParagraph(paragraph: help_content_3_1), + buildParagraph(paragraph: help_content_3_bullet_1), + buildParagraph(paragraph: help_content_3_bullet_2), + buildParagraph(paragraph: help_content_3_bullet_3), + buildParagraph(paragraph: help_content_3_bullet_4), + buildParagraph(paragraph: help_content_3_2), + + DividerLine(), + + // Headline 4 + buildH1(headline1: help_headline_4), + + buildParagraph(paragraph: help_content_4_0), + + buildH2(headline2: help_headline_4_1), + buildParagraph(paragraph: help_content_4_1), + + buildH2(headline2: help_headline_4_2), + buildParagraph(paragraph: help_content_4_2), + + DividerLine(), + + // Headline 5 + buildH1(headline1: help_headline_5), + buildParagraph(paragraph: help_content_5), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/index.dart b/lib/screens/index.dart index 4e98e2b..631f793 100644 --- a/lib/screens/index.dart +++ b/lib/screens/index.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; - import 'package:flutter_svg/flutter_svg.dart'; import './dashboardScreen.dart'; -import './tracksScreen.dart'; -import './settingsScreen.dart'; import './profileScreen.dart'; +import './settingsScreen.dart'; +import './tracksScreen.dart'; import '../constants.dart'; import '../widgets/OBDhelpDialog.dart'; @@ -54,7 +53,7 @@ class _IndexState extends State { backgroundColor: kGreyColor, elevation: 0, leading: IconButton( - icon: Icon( + icon: const Icon( Icons.help, color: Colors.white, ), diff --git a/lib/screens/logBookScreen.dart b/lib/screens/logBookScreen.dart new file mode 100644 index 0000000..c74f997 --- /dev/null +++ b/lib/screens/logBookScreen.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import '../constants.dart'; +import '../globals.dart'; +import '../models/fueling.dart'; +import '../providers/fuelingsProvider.dart'; +import './createFuelingScreen.dart'; +import '../widgets/logbookWidgets/fuelingCard.dart'; +import '../widgets/titleWidget.dart'; + +// Screen that displays all the fueling logs +class LogBookScreen extends StatelessWidget { + static const routeName = '/logBookScreen'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: kGreyColor, + elevation: 0, + actions: [ + // Button to navigate to CreateFuelingScreen + GestureDetector( + onTap: () { + Navigator.of(context).pushNamed(CreateFuelingScreen.routeName); + }, + child: const Padding( + padding: EdgeInsets.only(right: 10.0), + child: Icon(Icons.add), + ), + ), + ], + + // enviroCar logo + title: const Text('LogBook'), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0), + + // Consumer provides all the fueling logs + child: Consumer( + builder: (_, fuelingsProvider, child) { + final List fuelingsList = fuelingsProvider.getFuelingsList; + + // If there are no logs then show 'No logs' image + if (fuelingsList.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const TitleWidget(title: 'My Fuelings'), + Expanded( + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Image of no logs + Image.asset( + 'assets/images/img_logbook.png', + height: deviceHeight * 0.3, + ), + const SizedBox( + height: 20, + ), + const Text( + 'NO FUELINGS', + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + const Text( + 'There are no fuelings assigned\nto the current user', + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ); + } + + // else show the fueling logs in a list + return SingleChildScrollView( + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + const TitleWidget(title: 'Your Fuelings'), + + // Creates fueling cards by taking data from fuelings provider + for (Fueling fuelings in fuelingsList) + FuelingCard(fueling: fuelings) + ], + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/screens/loginScreen.dart b/lib/screens/loginScreen.dart index 0b292e8..dc12e91 100644 --- a/lib/screens/loginScreen.dart +++ b/lib/screens/loginScreen.dart @@ -21,13 +21,13 @@ class LoginScreen extends StatefulWidget { } class _LoginScreenState extends State { - static final GlobalKey _formKey = new GlobalKey(); + static final GlobalKey _formKey = GlobalKey(); String _username; String _password; bool _wrongCredentials = false; - _showDialogbox(String message) async { + Future _showDialogbox(String message) async { await showDialog( context: context, builder: (context) => AlertDialog( @@ -61,7 +61,6 @@ class _LoginScreenState extends State { child: SingleChildScrollView( padding: EdgeInsets.only(top: deviceHeight * 0.03), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( height: deviceHeight * 0.1, @@ -97,7 +96,6 @@ class _LoginScreenState extends State { // Password TextFormField( obscureText: true, - autofocus: false, decoration: inputDecoration.copyWith( labelText: 'Password', ), @@ -114,14 +112,15 @@ class _LoginScreenState extends State { ), // Error for wrong credentials - _wrongCredentials - ? Text( - 'Wrong Credentials', - style: TextStyle( - color: Colors.red, - ), - ) - : Container(), + if (_wrongCredentials) + const Text( + 'Wrong Credentials', + style: TextStyle( + color: Colors.red, + ), + ) + else + Container(), SizedBox( height: deviceHeight * 0.03, @@ -129,24 +128,6 @@ class _LoginScreenState extends State { // Login Button GestureDetector( - child: Container( - width: double.infinity, - height: 50, - decoration: BoxDecoration( - color: kSpringColor, - borderRadius: BorderRadius.all(Radius.circular(5)), - ), - child: Center( - child: Text( - 'Login', - style: TextStyle( - color: Colors.white, - fontSize: 17, - fontWeight: FontWeight.w500, - ), - ), - ), - ), onTap: () async { setState( () { @@ -155,12 +136,12 @@ class _LoginScreenState extends State { ); // if (_formKey.currentState.validate()) { - User _user = new User( + final User _user = User( username: _username, password: _password, ); - String _status = + final String _status = await AuthenticationServices().loginUser( authProvider: _authProvider, user: _user, @@ -182,6 +163,24 @@ class _LoginScreenState extends State { } // } }, + child: Container( + width: double.infinity, + height: 50, + decoration: const BoxDecoration( + color: kSpringColor, + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: const Center( + child: Text( + 'Login', + style: TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w500, + ), + ), + ), + ), ), SizedBox( @@ -195,7 +194,7 @@ class _LoginScreenState extends State { RegisterScreen.routeName, ); }, - child: Text( + child: const Text( 'New to enviroCar?\nRegister here', textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/screens/mapScreen.dart b/lib/screens/mapScreen.dart index 7ef38e0..81db628 100644 --- a/lib/screens/mapScreen.dart +++ b/lib/screens/mapScreen.dart @@ -18,7 +18,8 @@ class _MapScreenState extends State { // checks if location service in enabled // promts permission dialogbox if service is disabled Future initializeLocation() async { - bool permissionStatus = await MapServices().initializeLocationService(); + final bool permissionStatus = + await MapServices().initializeLocationService(); setState(() { _locationServiceEnabled = permissionStatus; @@ -38,7 +39,7 @@ class _MapScreenState extends State { backgroundColor: kGreyColor, elevation: 0, centerTitle: true, - title: Text('Location'), + title: const Text('Location'), ), body: FutureBuilder( future: _startLocationService, @@ -46,13 +47,12 @@ class _MapScreenState extends State { if (snapshot.connectionState == ConnectionState.done) { // when location service is disabled by clicking 'No Thanks' on dialogbox if (!_locationServiceEnabled) { - return Container( + return SizedBox( width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( + const Text( 'Location Service is OFF', style: TextStyle( fontSize: 15, @@ -63,14 +63,14 @@ class _MapScreenState extends State { await initializeLocation(); }, child: Container( - margin: EdgeInsets.all(5), + margin: const EdgeInsets.all(5), height: 40, width: 100, - decoration: BoxDecoration( + decoration: const BoxDecoration( color: kSpringColor, borderRadius: BorderRadius.all(Radius.circular(5)), ), - child: Center( + child: const Center( child: Text( 'Turn on', style: TextStyle( @@ -91,7 +91,7 @@ class _MapScreenState extends State { ); } } else { - return Center( + return const Center( child: CircularProgressIndicator(), ); } diff --git a/lib/screens/onboardingScreen.dart b/lib/screens/onboardingScreen.dart index cd04b63..73fa5c2 100644 --- a/lib/screens/onboardingScreen.dart +++ b/lib/screens/onboardingScreen.dart @@ -13,7 +13,7 @@ class OnboardingScreen extends StatelessWidget { key: GlobalKey(), pages: [ PageViewModel( - decoration: PageDecoration( + decoration: const PageDecoration( descriptionPadding: EdgeInsets.all(10), titlePadding: EdgeInsets.all(10), ), @@ -26,7 +26,7 @@ class OnboardingScreen extends StatelessWidget { ), PageViewModel( titleWidget: Column( - children: [ + children: const [ Text( 'enviroCar', style: TextStyle( @@ -46,46 +46,46 @@ class OnboardingScreen extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { - EnvirocarStats envirocarStats = snapshot.data; + final EnvirocarStats envirocarStats = snapshot.data; return Column( children: [ Text( envirocarStats.users.toString(), - style: TextStyle( + style: const TextStyle( fontSize: 30, ), ), - Text( + const Text( 'Users', style: TextStyle( fontSize: 25, ), ), - SizedBox( + const SizedBox( height: 20, ), Text( envirocarStats.tracks.toString(), - style: TextStyle( + style: const TextStyle( fontSize: 30, ), ), - Text( + const Text( 'Tracks', style: TextStyle( fontSize: 25, ), ), - SizedBox( + const SizedBox( height: 20, ), Text( envirocarStats.measurements.toString(), - style: TextStyle( + style: const TextStyle( fontSize: 30, ), ), - Text( + const Text( 'Measurements', style: TextStyle( fontSize: 25, diff --git a/lib/screens/profileScreen.dart b/lib/screens/profileScreen.dart index f178268..7953e15 100644 --- a/lib/screens/profileScreen.dart +++ b/lib/screens/profileScreen.dart @@ -1,13 +1,19 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../providers/authProvider.dart'; import './loginScreen.dart'; import '../services/authenticationServices.dart'; -import '../constants.dart'; +import './reportIssueScreen.dart'; +import '../widgets/dividerLine.dart'; import '../providers/userStatsProvider.dart'; import '../providers/tracksProvider.dart'; +import './logBookScreen.dart'; +import './helpScreen.dart'; +import '../constants.dart'; class ProfileScreen extends StatefulWidget { @override @@ -15,6 +21,41 @@ class ProfileScreen extends StatefulWidget { } class _ProfileScreenState extends State { + Widget buildIconButton( + {@required String title, + @required IconData iconData, + @required void Function() onTap, + @required Color color}) { + return Column( + children: [ + GestureDetector( + onTap: onTap, + child: Row( + children: [ + Icon( + iconData, + color: color, + size: 35, + ), + const SizedBox( + width: 10, + ), + Text( + title, + style: TextStyle( + color: color, + fontSize: 18, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + DividerLine(), + ], + ); + } + @override Widget build(BuildContext context) { // Provides user data @@ -29,172 +70,84 @@ class _ProfileScreenState extends State { final TracksProvider _tracksProvider = Provider.of(context, listen: false); - return Container( - padding: EdgeInsets.symmetric( - vertical: 20, - ), + return SizedBox( width: double.infinity, child: SingleChildScrollView( + padding: const EdgeInsets.all(15), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: 20, - ), - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextButton( - onPressed: () {}, - child: Row( - children: [ - Icon( - Icons.menu_book_rounded, - color: kGreyColor, - size: 35, - ), - SizedBox( - width: 10, - ), - Text( - 'Log Book', - style: TextStyle( - color: kGreyColor, - fontSize: 18, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - TextButton( - onPressed: () {}, - child: Row( - children: [ - Icon( - Icons.help_outline, - color: kGreyColor, - size: 35, - ), - SizedBox( - width: 10, - ), - Text( - 'Help', - style: TextStyle( - color: kGreyColor, - fontSize: 18, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - TextButton( - onPressed: () {}, - child: Row( - children: [ - Icon( - Icons.bug_report_rounded, - color: kGreyColor, - size: 35, - ), - SizedBox( - width: 10, - ), - Text( - 'Bug Report', - style: TextStyle( - color: kGreyColor, - fontSize: 18, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - TextButton( - onPressed: () {}, - child: Row( - children: [ - Icon( - Icons.star, - color: kGreyColor, - size: 35, - ), - SizedBox( - width: 10, - ), - Text( - 'Rate Us', - style: TextStyle( - color: kGreyColor, - fontSize: 18, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - TextButton( - onPressed: () { - AuthenticationServices().logoutUser( - authProvider: _authProvider, - userStatsProvider: _userStatsProvider, - tracksProvider: _tracksProvider, - ); - Navigator.of(context).pushReplacementNamed( - LoginScreen.routeName, - ); - }, - child: Row( - children: [ - Icon( - Icons.logout, - color: kGreyColor, - size: 35, - ), - SizedBox( - width: 10, - ), - Text( - 'Logout', - style: TextStyle( - color: kGreyColor, - fontSize: 18, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - TextButton( - onPressed: () {}, - child: Row( - children: [ - Icon( - Icons.highlight_off_rounded, - color: Colors.red, - size: 35, - ), - SizedBox( - width: 10, - ), - Text( - 'Close enviroCar', - style: TextStyle( - color: Colors.red, - fontSize: 18, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - ], - ), + // Log book button + buildIconButton( + title: 'Log Book', + iconData: Icons.menu_book, + color: Colors.black, + onTap: () { + Navigator.of(context).pushNamed(LogBookScreen.routeName); + }, + ), + + // Help button + buildIconButton( + title: 'Help', + iconData: Icons.help_outline, + color: Colors.black, + onTap: () { + Navigator.of(context).pushNamed(HelpScreen.routeName); + }, + ), + + // Report Issue button + buildIconButton( + title: 'Report Issue', + iconData: Icons.bug_report, + color: Colors.black, + onTap: () { + Navigator.of(context).pushNamed(ReportIssueScreen.routeName); + }, + ), + + // Rate us button + buildIconButton( + title: 'Rate Us', + iconData: Icons.star, + color: Colors.black, + onTap: () async { + if (await canLaunch(playstoreUrl)) { + launch(playstoreUrl); + return; + } else { + throw 'Cannot Launch'; + } + }, + ), + + // Logout button + buildIconButton( + title: 'Logout', + iconData: Icons.logout, + color: Colors.black, + onTap: () { + AuthenticationServices().logoutUser( + authProvider: _authProvider, + userStatsProvider: _userStatsProvider, + tracksProvider: _tracksProvider, + ); + Navigator.of(context).pushReplacementNamed( + LoginScreen.routeName, + ); + }, + ), + + // Button to navigate to close the app + buildIconButton( + title: 'Close enviroCar', + iconData: Icons.highlight_off_rounded, + color: Colors.red, + onTap: () { + // Closes the app programatically + // Apple may SUSPEND THE APP as it is again Apple's Human Interface Guidelines + exit(0); + }, ), ], ), diff --git a/lib/screens/registerScreen.dart b/lib/screens/registerScreen.dart index 3f4c63a..a2f6fae 100644 --- a/lib/screens/registerScreen.dart +++ b/lib/screens/registerScreen.dart @@ -15,7 +15,7 @@ class RegisterScreen extends StatefulWidget { } class _RegisterScreenState extends State { - final GlobalKey _formKey = new GlobalKey(); + final GlobalKey _formKey = GlobalKey(); String _username; String _password; @@ -25,7 +25,7 @@ class _RegisterScreenState extends State { bool _acceptedPrivacy = false; bool _showError = false; - void _showDialogbox(String message) async { + Future _showDialogbox(String message) async { await showDialog( context: context, builder: (context) => AlertDialog( @@ -58,7 +58,6 @@ class _RegisterScreenState extends State { child: SingleChildScrollView( padding: EdgeInsets.only(top: deviceHeight * 0.03), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ // enviroCar logo Image.asset( @@ -107,7 +106,6 @@ class _RegisterScreenState extends State { // Password TextFormField( obscureText: true, - autofocus: false, decoration: inputDecoration.copyWith( labelText: 'Password', ), @@ -126,7 +124,6 @@ class _RegisterScreenState extends State { // Confirm Password TextFormField( obscureText: true, - autofocus: false, decoration: inputDecoration.copyWith( labelText: 'Password', ), @@ -158,7 +155,7 @@ class _RegisterScreenState extends State { ); }, ), - Flexible( + const Flexible( child: Text( 'I acknowledge I have read and agree to enviroCar\'s Terms and Conditions'), ), @@ -182,7 +179,7 @@ class _RegisterScreenState extends State { ); }, ), - Flexible( + const Flexible( child: Text( 'I have taken note of the Privacy Statement'), ), @@ -190,14 +187,15 @@ class _RegisterScreenState extends State { ), // Error if both boxes aren't checked - _showError - ? Text( - 'Please check both boxes', - style: TextStyle( - color: Colors.red, - ), - ) - : Container(), + if (_showError) + const Text( + 'Please check both boxes', + style: TextStyle( + color: Colors.red, + ), + ) + else + Container(), SizedBox( height: deviceHeight * 0.03, @@ -205,24 +203,6 @@ class _RegisterScreenState extends State { // Register button GestureDetector( - child: Container( - width: double.infinity, - height: 50, - decoration: BoxDecoration( - color: kSpringColor, - borderRadius: BorderRadius.all(Radius.circular(5)), - ), - child: Center( - child: Text( - 'Register', - style: TextStyle( - color: Colors.white, - fontSize: 17, - fontWeight: FontWeight.w500, - ), - ), - ), - ), onTap: () async { if (_acceptedTerms && _acceptedPrivacy) { setState(() { @@ -235,7 +215,7 @@ class _RegisterScreenState extends State { } if (_formKey.currentState.validate() && !_showError) { - User _newUser = new User( + final User _newUser = User( username: _username, email: _email, password: _password, @@ -243,12 +223,11 @@ class _RegisterScreenState extends State { acceptedPrivacy: _acceptedPrivacy, ); - String _status = + final String _status = await AuthenticationServices().registerUser( user: _newUser, ); - print(_status); if (_status == 'Mail Sent') { _showDialogbox('Mail Sent'); } else if (_status == 'name already exists') { @@ -258,6 +237,24 @@ class _RegisterScreenState extends State { } } }, + child: Container( + width: double.infinity, + height: 50, + decoration: const BoxDecoration( + color: kSpringColor, + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: const Center( + child: Text( + 'Register', + style: TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w500, + ), + ), + ), + ), ), SizedBox( @@ -269,7 +266,7 @@ class _RegisterScreenState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: Text( + child: const Text( 'Already have an account?\nLogin here', textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/screens/reportIssueScreen.dart b/lib/screens/reportIssueScreen.dart new file mode 100644 index 0000000..7f886f5 --- /dev/null +++ b/lib/screens/reportIssueScreen.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; + +import '../constants.dart'; +import '../globals.dart'; +import '../models/report.dart'; +import '../widgets/singleRowForm.dart'; +import '../widgets/button.dart'; +import '../widgets/titleWidget.dart'; +import '../values/reportIssueValues.dart'; + +class ReportIssueScreen extends StatefulWidget { + static const String routeName = '/ReportIssueScreen'; + + @override + _ReportIssueScreenState createState() => _ReportIssueScreenState(); +} + +class _ReportIssueScreenState extends State { + // Key to validate the form + GlobalKey _formKey; + + // Controller to extract the data from textfields + TextEditingController estimatedTimeController; + TextEditingController problemController; + + // bool variables for checkboxes + bool forceCrashBool; + bool suddenLagsBool; + bool unresponsiveAppBool; + bool componentNotWorkingBool; + bool featureRequestBool; + + // triggered when 'Create Report' button is pressed + void createReport() { + final Report report = Report( + // TODO: Add id from uuid package + id: '32', + estimatedTime: estimatedTimeController.text, + problem: problemController.text, + forceCrash: forceCrashBool, + suddenLags: suddenLagsBool, + appWasUnresponsive: unresponsiveAppBool, + componentDoesNotWorkAsExpected: componentNotWorkingBool, + requestForAFeature: featureRequestBool, + ); + } + + @override + void initState() { + super.initState(); + + _formKey = GlobalKey(); + + estimatedTimeController = TextEditingController(); + problemController = TextEditingController(); + + forceCrashBool = false; + suddenLagsBool = false; + unresponsiveAppBool = false; + componentNotWorkingBool = false; + featureRequestBool = false; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: kGreyColor, + elevation: 0, + // enviroCar logo + title: const Text('Report Issue'), + centerTitle: true, + ), + body: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(15), + width: deviceWidth, + child: Form( + key: _formKey, + child: Column( + children: [ + // Heading text + const Text(heading), + const SizedBox( + height: 10, + ), + + // Estimated time textfield + SingleRowForm( + title: estimatedTime, + hint: '10.00', + textEditingController: estimatedTimeController, + ), + const SizedBox( + height: 10, + ), + + // Textfield for problem description + TextFormField( + controller: problemController, + maxLines: 5, + decoration: inputDecoration.copyWith( + labelText: describeYourProblem, + ), + ), + const SizedBox( + height: 10, + ), + + // Categories heading + const TitleWidget( + title: categories, + ), + + // Force Crash Checkbox + CheckboxListTile( + title: const Text(forceCrash), + value: forceCrashBool, + onChanged: (bool val) { + setState(() { + forceCrashBool = val; + }); + }, + ), + + // Sudden Lags Checkbox + CheckboxListTile( + title: const Text(suddenLags), + value: suddenLagsBool, + onChanged: (bool val) { + setState(() { + suddenLagsBool = val; + }); + }, + ), + + // App was unresponsive Checkbox + CheckboxListTile( + title: const Text(appWasUnresponsive), + value: unresponsiveAppBool, + onChanged: (bool val) { + setState(() { + unresponsiveAppBool = val; + }); + }, + ), + + // Component Does not work Checkbox + CheckboxListTile( + title: const Text(componentDoesNotWorkAsExpected), + value: componentNotWorkingBool, + onChanged: (bool val) { + setState(() { + componentNotWorkingBool = val; + }); + }, + ), + + // Feature request Checkbox + CheckboxListTile( + title: const Text(requestForAFeature), + value: featureRequestBool, + onChanged: (bool val) { + setState(() { + featureRequestBool = val; + }); + }, + ), + const SizedBox( + height: 10, + ), + + // Create Report button + Button( + title: sendReport, + color: kSpringColor, + onTap: () { + createReport(); + }, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/settingsScreen.dart b/lib/screens/settingsScreen.dart index 2ae9b9e..82fad88 100644 --- a/lib/screens/settingsScreen.dart +++ b/lib/screens/settingsScreen.dart @@ -1,5 +1,10 @@ import 'Package:flutter/material.dart'; +import '../widgets/dividerLine.dart'; +import '../widgets/titleWidget.dart'; +import '../values/settingsValues.dart'; +import '../widgets/settingsScreenWidgets/settingsListWidget.dart'; + class SettingsScreen extends StatefulWidget { @override _SettingsScreenState createState() => _SettingsScreenState(); @@ -8,10 +13,31 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { @override Widget build(BuildContext context) { - return Container( - child: Center( - child: Text( - 'Settings Screen', + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // General Settings + const TitleWidget(title: 'General Settings'), + SettingsListWidget(settings: generalSettings), + DividerLine(), + + // OBD Mode Settings + const TitleWidget(title: 'OBD Mode'), + SettingsListWidget(settings: OBDModeSettings), + DividerLine(), + + // GPS Mode Settings + const TitleWidget(title: 'GPS Mode'), + SettingsListWidget(settings: GPSModeSettings), + DividerLine(), + + // Debugging Settings + const TitleWidget(title: 'Debugging'), + SettingsListWidget(settings: debuggingSettings), + ], ), ), ); diff --git a/lib/screens/splashScreen.dart b/lib/screens/splashScreen.dart index 221374e..1feb248 100644 --- a/lib/screens/splashScreen.dart +++ b/lib/screens/splashScreen.dart @@ -1,4 +1,3 @@ -import 'package:envirocar_app_main/providers/tracksProvider.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -10,6 +9,7 @@ import '../services/authenticationServices.dart'; import '../providers/userStatsProvider.dart'; import '../globals.dart'; import './onboardingScreen.dart'; +import '../providers/tracksProvider.dart'; class SplashScreen extends StatefulWidget { static const routeName = '/splashScreen'; @@ -61,7 +61,7 @@ class _SplashScreenState extends State { future: buildScreen, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { - return snapshot.data ? Index() : navigate(); + return snapshot.data != null ? Index() : navigate(); } else { return Scaffold( body: Center( diff --git a/lib/screens/trackDetailsScreen.dart b/lib/screens/trackDetailsScreen.dart index 7002863..28b5b87 100644 --- a/lib/screens/trackDetailsScreen.dart +++ b/lib/screens/trackDetailsScreen.dart @@ -2,16 +2,15 @@ import 'package:flutter/material.dart'; import '../constants.dart'; import '../models/track.dart'; -import '../widgets/trackDetailsWidgets/carDetailsCard.dart'; import '../widgets/trackDetailsWidgets/stackedMapButton.dart'; -import '../widgets/tracksScrrenWidgets/trackCard.dart'; +import '../widgets/tracksScrrenWidgets/trackDetailsCard.dart'; class TrackDetailsScreen extends StatelessWidget { static const routeName = '/trackDetailsScreen'; final Track track; - TrackDetailsScreen({ + const TrackDetailsScreen({ @required this.track, }); @@ -32,16 +31,14 @@ class TrackDetailsScreen extends StatelessWidget { child: Column( children: [ StackedMapButton(), - SizedBox( + const SizedBox( height: 20, ), - TrackCard(track: track), - SizedBox( - height: 20, + TrackDetailsCard( + track: track, ), - CarDetailsCard(), - SizedBox( - height: 40, + const SizedBox( + height: 100, ), ], ), diff --git a/lib/screens/tracksScreen.dart b/lib/screens/tracksScreen.dart index 574ac7b..fb960c4 100644 --- a/lib/screens/tracksScreen.dart +++ b/lib/screens/tracksScreen.dart @@ -1,8 +1,8 @@ -import 'Package:flutter/material.dart'; +import 'package:flutter/material.dart'; import '../widgets/tracksScrrenWidgets/localTracksList.dart'; import '../widgets/tracksScrrenWidgets/uploadedTracksList.dart'; -import '../widgets/tracksScrrenWidgets/tabButtons.dart'; +import '../widgets/tabButtonsPage.dart'; import '../utils/enums.dart'; class TracksScreen extends StatefulWidget { @@ -11,14 +11,14 @@ class TracksScreen extends StatefulWidget { } class _TracksScreenState extends State { - SelectedTab selectedTab = SelectedTab.Local; + TracksTab selectedTab = TracksTab.local; void changeTab() { setState(() { - if (selectedTab == SelectedTab.Local) { - selectedTab = SelectedTab.Uploaded; + if (selectedTab == TracksTab.local) { + selectedTab = TracksTab.uploaded; } else { - selectedTab = SelectedTab.Local; + selectedTab = TracksTab.local; } }); } @@ -28,13 +28,14 @@ class _TracksScreenState extends State { return SingleChildScrollView( child: Column( children: [ - TabButtons( - changeTab: changeTab, - selectedTab: selectedTab, + TabButtonsPage( + button1Title: 'Local', + button2Title: 'Uploaded', + tab1: TracksTab.local, + tab2: TracksTab.uploaded, + page1: LocalTracksList(), + page2: UploadedTracksList(), ), - selectedTab == SelectedTab.Uploaded - ? UploadedTracksList() - : LocalTracksList(), ], ), ); diff --git a/lib/services/authenticationServices.dart b/lib/services/authenticationServices.dart index b126c8c..e87448a 100644 --- a/lib/services/authenticationServices.dart +++ b/lib/services/authenticationServices.dart @@ -1,4 +1,3 @@ -import 'package:envirocar_app_main/providers/tracksProvider.dart'; import 'package:flutter/foundation.dart'; import 'dart:convert'; @@ -9,6 +8,7 @@ import '../models/user.dart'; import './secureStorageServices.dart'; import './statsServices.dart'; import '../providers/userStatsProvider.dart'; +import '../providers/tracksProvider.dart'; class AuthenticationServices { final baseUri = 'https://envirocar.org/api/stable/users'; @@ -19,9 +19,9 @@ class AuthenticationServices { }) async { final Uri uri = Uri.parse(baseUri); - String jsonPayload = jsonEncode(user.toMap()); + final String jsonPayload = jsonEncode(user.toMap()); - http.Response response = await http.post( + final http.Response response = await http.post( uri, headers: {'Content-Type': 'application/json'}, body: jsonPayload, @@ -30,8 +30,8 @@ class AuthenticationServices { if (response.statusCode == 201) { return 'Mail Sent'; } else { - var decodedBody = jsonDecode(response.body); - return decodedBody['message']; + final decodedBody = jsonDecode(response.body); + return decodedBody['message'] as String; } } @@ -42,9 +42,9 @@ class AuthenticationServices { @required UserStatsProvider userStatsProvider, }) async { if (user.getUsername != null && user.getPassword != null) { - final Uri uri = Uri.parse(baseUri + '/' + user.getUsername); + final Uri uri = Uri.parse('$baseUri/${user.getUsername}'); - http.Response response = await http.get( + final http.Response response = await http.get( uri, headers: { 'X-User': user.getUsername, @@ -57,9 +57,9 @@ class AuthenticationServices { SecureStorageServices().setUserInSecureStorage( username: user.getUsername, password: user.getPassword); - var decodedBody = jsonDecode(response.body); + final decodedBody = jsonDecode(response.body); - user.setEmail = decodedBody['mail']; + user.setEmail = decodedBody['mail'] as String; user.setAcceptedTerms = true; user.setAcceptedPrivacy = true; @@ -75,8 +75,8 @@ class AuthenticationServices { } else { authProvider.setAuthStatus = false; - var decodedBody = jsonDecode(response.body); - return decodedBody['message']; + final decodedBody = jsonDecode(response.body); + return decodedBody['message'] as String; } } else { authProvider.setAuthStatus = false; @@ -92,7 +92,7 @@ class AuthenticationServices { }) async { final User _user = await SecureStorageServices().getUserFromSecureStorage(); - String message = await this.loginUser( + final String message = await loginUser( authProvider: authProvider, user: _user, userStatsProvider: userStatsProvider, @@ -101,7 +101,7 @@ class AuthenticationServices { // if user deletes account from website and opens app again // then send to login screen and remove data from secure storage if (message == 'invalid username or password') { - this.logoutUser( + logoutUser( authProvider: authProvider, userStatsProvider: userStatsProvider, tracksProvider: tracksProvider, diff --git a/lib/services/bluetoothStatusChecker.dart b/lib/services/bluetoothStatusChecker.dart index 59973fb..74e3307 100644 --- a/lib/services/bluetoothStatusChecker.dart +++ b/lib/services/bluetoothStatusChecker.dart @@ -22,19 +22,19 @@ class BluetoothStatusChecker { /// function to get [BluetoothConnectionStatus] Future get connectionStatus async { - FlutterReactiveBle flutterReactiveBle = FlutterReactiveBle(); - BleStatus hasConnection = flutterReactiveBle.status; + final FlutterReactiveBle flutterReactiveBle = FlutterReactiveBle(); + final BleStatus hasConnection = flutterReactiveBle.status; return hasConnection == BleStatus.ready ? BluetoothConnectionStatus.ON : BluetoothConnectionStatus.OFF; } Duration checkInterval = DEFAULT_INTERVAL; /// function to send status updates - void _sendStatusUpdate([Timer timer]) async { + Future _sendStatusUpdate([Timer timer]) async { _timer?.cancel(); timer?.cancel(); - var _currentStatus = await connectionStatus; + final _currentStatus = await connectionStatus; // if the current status is different from the last known status and stream has listeners then adding the new value to the stream if (_currentStatus != _lastStatus && _statusController.hasListener) { @@ -42,8 +42,9 @@ class BluetoothStatusChecker { } // if the stream does not have any listeners returning from the function - if (!_statusController.hasListener) + if (!_statusController.hasListener) { return; + } // calling the function after an interval of time _timer = Timer(checkInterval, _sendStatusUpdate); @@ -55,7 +56,7 @@ class BluetoothStatusChecker { BluetoothConnectionStatus _lastStatus; Timer _timer; - StreamController _statusController = StreamController.broadcast(); + final StreamController _statusController = StreamController.broadcast(); /// function to return stream of [BluetoothConnectionStatus] Stream get onStatusChange => _statusController.stream; diff --git a/lib/services/locationStatusChecker.dart b/lib/services/locationStatusChecker.dart index 8e6653b..7707f36 100644 --- a/lib/services/locationStatusChecker.dart +++ b/lib/services/locationStatusChecker.dart @@ -22,18 +22,18 @@ class LocationStatusChecker { /// function to get [LocationStatus] Future get isLocationEnabled async { - bool serviceIsEnabled = await Geolocator.isLocationServiceEnabled(); + final bool serviceIsEnabled = await Geolocator.isLocationServiceEnabled(); return serviceIsEnabled ? LocationStatus.enabled : LocationStatus.disabled; } Duration checkInterval = DEFAULT_INTERVAL; /// function to send status updates - void _sendStatusUpdates([Timer timer]) async { + Future _sendStatusUpdates([Timer timer]) async { _timer?.cancel(); timer?.cancel(); - var _currentStatus = await isLocationEnabled; + final _currentStatus = await isLocationEnabled; // if the current status is different than the last known status and stream has listeners then adding the new value to the stream if (_currentStatus != _lastStatus && _statusController.hasListener) { @@ -41,8 +41,9 @@ class LocationStatusChecker { } // if the stream does not have any listeners returning from the function - if (!_statusController.hasListener) + if (!_statusController.hasListener) { return; + } // calling the function after an interval of time _timer = Timer(checkInterval, _sendStatusUpdates); @@ -55,7 +56,7 @@ class LocationStatusChecker { Timer _timer; /// function to send status updates - StreamController _statusController = StreamController.broadcast(); + final StreamController _statusController = StreamController.broadcast(); /// function to return stream of [LocationStatus] Stream get onStatusChange => _statusController.stream; diff --git a/lib/services/mapServices.dart b/lib/services/mapServices.dart index f88121c..61195c0 100644 --- a/lib/services/mapServices.dart +++ b/lib/services/mapServices.dart @@ -5,7 +5,7 @@ class MapServices { // Checks if location service is enabled // Prompts a dialogbox to ask to turn it on if disabled Future initializeLocationService() async { - bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + final bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool permissionStatus = true; if (!serviceEnabled) { diff --git a/lib/services/secureStorageServices.dart b/lib/services/secureStorageServices.dart index 04e2ad6..fe18637 100644 --- a/lib/services/secureStorageServices.dart +++ b/lib/services/secureStorageServices.dart @@ -5,14 +5,14 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../models/user.dart'; class SecureStorageServices { - final FlutterSecureStorage _secureStorage = FlutterSecureStorage(); + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); // Fetches data stored locally on a secure storage Future getUserFromSecureStorage() async { - String username = await _secureStorage.read(key: 'username'); - String password = await _secureStorage.read(key: 'password'); + final String username = await _secureStorage.read(key: 'username'); + final String password = await _secureStorage.read(key: 'password'); - User user = new User( + final User user = User( username: username, password: password, ); diff --git a/lib/services/statsServices.dart b/lib/services/statsServices.dart index 69b8150..98963d3 100644 --- a/lib/services/statsServices.dart +++ b/lib/services/statsServices.dart @@ -16,13 +16,10 @@ class StatsServices { @required AuthProvider authProvider, @required UserStatsProvider userStatsProvider, }) async { - final Uri uri = Uri.parse(baseUri + - '/users' + - '/' + - authProvider.getUser.getUsername + - '/userStatistic'); + final Uri uri = Uri.parse( + '$baseUri/users/${authProvider.getUser.getUsername}/userStatistic'); - http.Response response = await http.get( + final http.Response response = await http.get( uri, headers: { 'X-User': authProvider.getUser.getUsername, @@ -31,22 +28,22 @@ class StatsServices { ); userStatsProvider.setUserStats = - UserStats.fromJson(jsonDecode(response.body)); + UserStats.fromJson(jsonDecode(response.body) as Map); } // Fetches general enviroCar stats Future getEnvirocarStats() async { final Uri uri = Uri.parse(baseUri); - http.Response response = await http.get( + final http.Response response = await http.get( uri, ); - dynamic responseBody = jsonDecode(response.body)['counts']; + final dynamic responseBody = jsonDecode(response.body)['counts']; - int users = responseBody['users']; - int tracks = responseBody['tracks']; - int measurements = responseBody['measurements']; + final int users = responseBody['users'] as int; + final int tracks = responseBody['tracks'] as int; + final int measurements = responseBody['measurements'] as int; return EnvirocarStats( users: users, diff --git a/lib/services/tracksServices.dart b/lib/services/tracksServices.dart index 140e2df..855e4fb 100644 --- a/lib/services/tracksServices.dart +++ b/lib/services/tracksServices.dart @@ -10,9 +10,9 @@ class TracksServices { @required AuthProvider authProvider, @required TracksProvider tracksProvider, }) async { - final String uri = 'https://envirocar.org/api/stable/tracks'; + const String uri = 'https://envirocar.org/api/stable/tracks'; - http.Response response = await http.get( + final http.Response response = await http.get( Uri.parse(uri), headers: { 'X-User': authProvider.getUser.getUsername, @@ -20,6 +20,6 @@ class TracksServices { }, ); - tracksProvider.setTracks(jsonDecode(response.body)); + tracksProvider.setTracks(jsonDecode(response.body) as Map); } } diff --git a/lib/utils/enums.dart b/lib/utils/enums.dart index 93371e3..3d28686 100644 --- a/lib/utils/enums.dart +++ b/lib/utils/enums.dart @@ -1,6 +1,11 @@ -enum SelectedTab { - Local, - Uploaded, +enum TracksTab { + local, + uploaded, +} + +enum CreateCarTab { + hsnTsn, + attributes, } enum BluetoothConnectionStatus { diff --git a/lib/values/dummyCarsList.dart b/lib/values/dummyCarsList.dart new file mode 100644 index 0000000..3fe2e6f --- /dev/null +++ b/lib/values/dummyCarsList.dart @@ -0,0 +1,60 @@ +import '../models/car.dart'; + +final List _carsList = [ + Car( + manufacturer: 'Volkswagen', + model: 'S309', + constructionYear: 1930, + fuelType: 'Diesel', + engineDisplacement: 1600, + // isSelected: true, + ), + Car( + manufacturer: 'BMW', + model: 'M3-GT', + constructionYear: 1930, + fuelType: 'Diesel', + engineDisplacement: 1600, + // isSelected: false, + ), + Car( + manufacturer: 'Ferrari', + model: 'Ultra', + constructionYear: 1930, + fuelType: 'Diesel', + engineDisplacement: 1600, + // isSelected: false, + ), + Car( + manufacturer: 'Hector', + model: 'SH43', + constructionYear: 1930, + fuelType: 'Diesel', + engineDisplacement: 1600, + // isSelected: false, + ), + Car( + manufacturer: 'Audi', + model: 'R8', + constructionYear: 1930, + fuelType: 'Diesel', + engineDisplacement: 1600, + // isSelected: false, + ), + Car( + manufacturer: 'Mercedez', + model: 'Benz', + constructionYear: 1930, + fuelType: 'Diesel', + engineDisplacement: 1600, + // isSelected: false, + ), + Car( + manufacturer: 'Rolls Royce', + model: 'Antique', + constructionYear: 1930, + fuelType: 'Diesel', + engineDisplacement: 1600, + // isSelected: false, + ), +]; diff --git a/lib/values/helpScreenValues.dart b/lib/values/helpScreenValues.dart new file mode 100644 index 0000000..260fed2 --- /dev/null +++ b/lib/values/helpScreenValues.dart @@ -0,0 +1,45 @@ +const String help_headline_1 = '1. About enviroCar'; +const String help_content_1 = + 'This app is part of the Citizen Science platform enviroCar. You can use it to collect and analyze test drives and provide the measured data as Open Data.\n\nenviroCar is currently in a beta phase. As a result, the software’s stability is not yet comparable to standards associated with mature software.\n\nWe welcome your feedback (enviroCar@52north.org)! Your advice and suggestions help us to improve the software.'; + +const String help_headline_2 = '2. Getting started'; +const String help_headline_2_1 = '2.1. Login/Register'; +const String help_content_2_1 = + 'In order to take full advantage of the enviroCar app\'s functional range, you must have a user account for the enviroCar platform. This enables you to upload your tracks as Open Data and evaluate them using the website\'s various analysis tools.\n'; +const String help_content_2_1_enum_1 = + '1. Click "Login/Register" in the navigation menu.'; +const String help_content_2_1_enum_2 = + '2. Fill in your user name and password in the following window.'; +const String help_content_2_1_enum_3 = '3. Then click "Login"\n'; + +const String help_headline_2_2 = '2.2. Select car type'; +const String help_content_2_2 = + 'Certain information is needed to calculate data from the car diagnostics. Click "car settings" on the dashboard to access the car manager. This is where you manage your car types.\n'; + +const String help_headline_2_3 = '2.3. Connect OBD-II adapter'; +const String help_content_2_3 = + 'In order to record a track, you must connect the app to an ODB-II adapter. First pair the app with the appropriate adapter. Plug the ODB-II adapter in your car’s ODB-II port. Follow the Bluetooth adapter instructions for connecting your smartphone and the adapter. If Bluetooth has not been activated, you can turn it on here.'; + +const String help_headline_3 = '3. Record track'; +const String help_content_3_1 = + 'The following four requirements must be fulfilled before you can start recording a track:\n'; +const String help_content_3_bullet_1 = '> Activate Bluetooth'; +const String help_content_3_bullet_2 = '> Activate GPS'; +const String help_content_3_bullet_3 = '> Select car type'; +const String help_content_3_bullet_4 = '> Select OBD-II adapter\n'; +const String help_content_3_2 = + 'Once these parameters have been set, click the "START TRACK" button on the dashboard to begin recording your track. Once the motor has been started, the ODB-II adapter placed within communication range and a connection to the car established, the app will display the speed control panel.\n\nPlease note: the time display will begin once a stable GPS connection has been established. Measurements are recorded and saved only when the app receives valid GPS positioning information.'; + +const String help_headline_4 = '4. Observe tracks'; +const String help_content_4_0 = + 'Click "My Tracks" in the bottom bar to get a list of all your tracks. This list is divided into local and uploaded tracks. Local tracks are those that have been saved locally, but not yet uploaded to the enviroCar server. Uploaded tracks are those tracks that have been uploaded to the enviroCar server and are available as Open Data. These tracks have been explicitly assigned to your account. Subsequently, you can download them to another device and query them.\n'; +const String help_headline_4_1 = '4.1. Track details'; +const String help_content_4_1 = + 'In order to view the track details, click on a track\'s map or select "Show Details" in the track\'s menu. This opens a track view containing the track’s most important attributes and statistics. You can view statistics and/or visualize an attribute on a map. Click on the blue graph symbol to view "Track Statistics". Select/change the attribute via the menu. Click on the map to visualize the attribute, CO2 for example. Change the attribute (phenomenon) by clicking on the white graph symbol in the lower left corner.\n'; +const String help_headline_4_2 = '4.2. Upload track'; +const String help_content_4_2 = + 'You must be a registered user and logged in to the app to upload your track data to the enviroCar server. Click on the menu in the upper right corner of the track to be uploaded in the "My Tracks" view and select "Upload as Open Data". Wait until the app has successfully completed the upload. Use the upload symbol in the lower right corner to upload all tracks at once. Carry out various analysis on all uploaded tracks and compare your data with other user groups’ data on the enviroCar website.\n\nImportant note: When you upload your enviroCar data, you agree to publish the data as anonymized, Open Data, under an open data license. User data and information, such as which tracks belong to which user, is hidden from other users.'; + +const String help_headline_5 = '5. Problems and feedback'; +const String help_content_5 = + 'We welcome your feedback (enviroCar@52north.org)! Your advice and suggestions help us to improve the software.\n\nIf you discover any problems, please send us an issue report. Click on "Others" in the bottom bar and select "Report Issue". Fill out the form and click "SEND REPORT" to the enviroCar Team. Please be as detailed as possible.'; diff --git a/lib/values/reportIssueValues.dart b/lib/values/reportIssueValues.dart new file mode 100644 index 0000000..c0ed521 --- /dev/null +++ b/lib/values/reportIssueValues.dart @@ -0,0 +1,23 @@ +// String values for Report Issue Screen + +const String heading = + 'Timestamps are automatically included in the logs. Sending us reports helps us reproduce unexpected behavior or crashes. This makes enviroCar better and more stable. Click the send button below to email this report to us.'; + +const String estimatedTime = 'Estimated Time'; + +const String describeYourProblem = 'Describe your problem...'; + +const String categories = 'Categories'; + +const String forceCrash = 'Force Crash'; + +const String suddenLags = 'Sudden Lags'; + +const String appWasUnresponsive = 'App was unrsponsive'; + +const String componentDoesNotWorkAsExpected = + 'Component does not work as expected'; + +const String requestForAFeature = 'Request for a feature'; + +const String sendReport = 'Send Report'; diff --git a/lib/values/settingsValues.dart b/lib/values/settingsValues.dart new file mode 100644 index 0000000..7e00a97 --- /dev/null +++ b/lib/values/settingsValues.dart @@ -0,0 +1,173 @@ +import '../models/settingsTileModel.dart'; + +/// Contains values of settings on Settings Screen + +// General Settings List +List generalSettings = [ + SettingsTileModel( + title: 'Automatic Upload', + subtitle: + 'Enables the automatic upload of recorded tracks after the recording has been finished. It is only tried to upload the track directly afterwards. If the upload fails (e.g. no internet connection), the track has to be uploaded manually.', + isChecked: false, + ), + SettingsTileModel( + title: 'Keep the screen on', + subtitle: + 'When checked, this setting keeps the screen on while the application is running.', + isChecked: false, + ), + SettingsTileModel( + title: 'Verbal Announcements', + subtitle: + 'Enabling the verbal announcements of specific events, e.g. OBD-II connection established/lost, track finished.', + isChecked: false, + ), + SettingsTileModel( + title: 'Anonymize Start and Destination', + subtitle: + 'Only upload measurements which are taken 250 meter (820 feet) and one minute after start and before end of each track.', + isChecked: false, + ), +]; + +// OBD Mode Settings List +List OBDModeSettings = [ + SettingsTileModel( + title: 'Auto Connect', + subtitle: + 'In case of OBD track, it automatically connects to the OBD-II adapter and starts the recording of a track. This feature periodically checks whether the selected OBD-II adapter is within range. In case of GPS track, it uses activity recognition features and automatically starts the GPS based track once it detects that the user is IN_VEHICLE. Therefore, this setting requires additional battery power.', + isChecked: false, + ), + SettingsTileModel( + title: 'Discovery Interval', + subtitle: + 'The search interval at which the final String device searches for the selected OBD-II final String device. Note: the higher the value, the more the battery gets drained.', + isChecked: false, + ), + SettingsTileModel( + title: 'Diesel Consumption Estimation', + subtitle: + 'Enables the estimation of consumption values for diesel. NOTE: This feature is just a beta feature.', + isChecked: false, + ), +]; + +// GPS Mode Settings List +List GPSModeSettings = [ + SettingsTileModel( + title: 'Enable GPS based track recording', + subtitle: + "Activates an additional recording mode that enables the recording of plain GPS based tracks that does not require an OBD-II adapter.\n\nNOTE: This feature is just a beta feature.", + isChecked: false, + ), + SettingsTileModel( + title: 'Automatic Recording (GPS)', + subtitle: + 'Activates automatic recording of GPS-based trips based on activity detection mechanisms.\n\nNote: This Android function does not work reliably on some smartphone models.', + isChecked: false, + ), +]; + +List debuggingSettings = [ + SettingsTileModel( + title: 'Enable Debug Logging', + subtitle: 'Increase the log level (used in issue/problem report)', + isChecked: false, + ), +]; + + +/// Values copied from Android enviroCar +/// May need in the future + +// final String pref_obd_recording = "OBD Mode"; +// final String pref_gps_recording = "GPS Mode"; + +// // Settings Activity ; +// final String pref_settings_general = "General Settings"; +// final String pref_settings_general_summary = +// "Display, Verbal Announcements, Automatic Upload ..."; +// final String pref_settings_autoconnect = "Connection Settings"; +// final String pref_settings_autoconnect_summary = +// "Settings for the automatic connection with the OBD-II adapter"; +// final String pref_settings_optional = "Advanced Settings"; +// final String pref_settings_optional_summary = +// "Optional settings and beta functionalities"; +// final String pref_settings_other = "App Infos"; +// final String pref_settings_other_summary = "Version, Licenses, etc."; + +// // General Settings; +// final String pref_automatic_upload = "Automatic Upload"; +// final String pref_automatic_upload_sub = +// "Enables the automatic upload of recorded tracks after the recording has been finished. It is only tried to upload the track directly afterwards. If the upload fails (e.g. no internet connection), the track has to be uploaded manually."; +// final String pref_display_always_activ = "Keep the Screen On"; +// final String pref_display_always_activ_sub = +// "When checked, this setting keeps the screen on while the application is running."; +// final String pref_imperial_unit_summary = +// "Use imperial units (miles) for displaying values."; +// final String pref_obfuscate_track = "Anonymize Start and Destination"; +// final String pref_obfuscate_track_summary = +// "Only upload measurements which are taken 250 meter (820 feet) and one minute after start and before end of each track."; +// final String pref_text_to_speech = "Verbal Announcements"; +// final String pref_text_to_speech_summary = +// "Enabling the verbal announcements of specific events, e.g. OBD-II connection established/lost, track finished."; + +// // OBD-Settings ; +// final String pref_obd_settings = "OBD-II Adapter Settings"; +// final String pref_bluetooth_switch_isenabled = "Bluetooth is On"; +// final String pref_bluetooth_switch_isdisabled = "Bluetooth is Off"; +// final String pref_bluetooth_pairing = "Bluetooth Pairing"; +// final String pref_bluetooth_pairing_summary = +// "Manage the pairings to Bluetooth final String devices."; +// final String pref_bluetooth_select_adapter_title = "Select OBD-II adapter"; +// final String pref_bluetooth_auto_connect = "Auto Connect"; +// final String pref_bluetooth_auto_connect_summary = +// "In case of OBD track, it automatically connects to the OBD-II adapter and starts the recording of a track. This feature periodically checks whether the selected OBD-II adapter is within range. In case of GPS track, it uses activity recognition features and automatically starts the GPS based track once it detects that the user is IN_VEHICLE. Therefore, this setting requires additional battery power."; +// final String pref_bluetooth_start_background = "Background Service"; +// final String pref_bluetooth_start_background_summary = +// "Automatically starts the background service whenever Bluetooth/GPS gets activated, depending on the track recording type selected."; +// final String pref_bluetooth_discovery_interval = "Discovery Interval"; +// final String pref_bluetooth_discovery_interval_summary = +// "The search interval at which the final String device searches for the selected OBD-II final String device. Note: the higher the value, the more the battery gets drained."; + +// // Auto-connect Settings ; +// final String settings_autoconnect_title = "Auto-connect Settings"; +// final String settings_autoconnect_subtitle = +// "OBD Auto-connect, GPS Auto-connect etc."; + +// // Optional Settings ; +// final String sampling_rate_title = "Sampling Rate"; +// final String sampling_rate_summary = +// "The time delta between two measurements in seconds. The lower the value, the bigger the data volume. Only consider changing if you are aware of the consequences."; +// final String enable_debug_logging = "Enable Debug Logging"; +// final String enable_debug_logging_summary = +// "Increase the log level (used in issue/problem reports)"; +// final String pref_track_cut_duration = "Track Trim Duration"; +// final String pref_track_cut_duration_summary = +// "GPS based tracks will be stopped automatically on detecting that the user is NOT DRIVING. However it has some latency in detecting. So we cut the track for that duration. Change this, if you know that latency."; + +// final String preference_track_trim_duration_title = "Set Track Trim Duration"; +// final String preference_beta_diesel_consumption = +// "Diesel Consumption Estimation"; +// final String preference_beta_diesel_consumption_sum = +// "Enables the estimation of consumption values for diesel. NOTE: This feature is just a beta feature."; + +// final String preference_beta_enable_gps_based_track_recording = +// "Enable GPS based track recording"; +// final String preference_beta_enable_gps_based_track_recording_sum = +// "Activates an additional recording mode that enables the recording of plain GPS based tracks that does not require an OBD-II adapter.\n\nNOTE: This feature is just a beta feature."; + +// // Other Settings ; +// final String pref_licenses = "Licenses"; + +// final String pref_car_settings = "Vehicle Settings"; +// final String pref_bluetooth_disabled = "Please enable Bluetooth"; +// final String pref_optional_features = "Optional Features"; +// final String pref_beta_features = "Beta Features"; +// final String pref_version = "Version"; +// final String pref_other = "Other"; +// final String pref_settings_title = "Settings"; + +// final String pref_gps_mode_ar_title = "Automatic Recording (GPS)"; +// final String pref_gps_mode_ar_summary = +// "Activates automatic recording of GPS-based trips based on activity detection mechanisms.\n\nNote: This Android function does not work reliably on some smartphone models."; diff --git a/lib/widgets/OBDhelpDialog.dart b/lib/widgets/OBDhelpDialog.dart index c7b2103..2b15dbc 100644 --- a/lib/widgets/OBDhelpDialog.dart +++ b/lib/widgets/OBDhelpDialog.dart @@ -17,7 +17,7 @@ class _OBDHelpDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + const Text( 'Locate DLC (Diagnostic Link Connector)', style: TextStyle( fontSize: 20, @@ -26,17 +26,10 @@ class _OBDHelpDialogState extends State { ), ), Image.asset('assets/images/findobddesgin.png'), - SizedBox(height: 10), - Text( + const SizedBox(height: 10), + const Text( 'This is somewhat triangulalr shaped 16-pin connector that is commonly located underneath the left hand side of the dash near the steering column. If you have trouble locating DLC, refer owner manual.'), GestureDetector( - child: Text( - 'www.wikiobd.co.uk', - style: TextStyle( - color: kSpringColor, - fontWeight: FontWeight.bold, - ), - ), onTap: () async { if (await canLaunch('https://wikiobd.co.uk')) { launch('https://wikiobd.co.uk'); @@ -44,6 +37,13 @@ class _OBDHelpDialogState extends State { throw 'Cannot Launch'; } }, + child: const Text( + 'www.wikiobd.co.uk', + style: TextStyle( + color: kSpringColor, + fontWeight: FontWeight.bold, + ), + ), ), ], ), @@ -52,8 +52,8 @@ class _OBDHelpDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Plug OBD into car\s OBD Port', + const Text( + 'Plug OBD into cars OBD Port', style: TextStyle( color: kSpringColor, fontWeight: FontWeight.bold, @@ -61,7 +61,7 @@ class _OBDHelpDialogState extends State { ), ), Image.asset('assets/images/obdport.png'), - Text( + const Text( 'Plug OBD Bluetooth into car\'s OBD port and turn the ignition on'), ], ), @@ -70,7 +70,7 @@ class _OBDHelpDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + const Text( 'Connect enviroCar app to OBD Bluetooth', style: TextStyle( color: kSpringColor, @@ -78,9 +78,9 @@ class _OBDHelpDialogState extends State { fontSize: 20, ), ), - Text('1. Select'), + const Text('1. Select'), Image.asset('assets/images/noobdselected.jpeg'), - Text( + const Text( '2. Turn on Bluetooth\n3. Connect to Bluetooth signals with names similar to: OBDII, VLink, ELM327 etc.\n4. All set! Start recording track'), ], ), @@ -95,7 +95,7 @@ class _OBDHelpDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text( + title: const Text( 'OBD Help', style: TextStyle( fontWeight: FontWeight.bold, @@ -110,24 +110,25 @@ class _OBDHelpDialogState extends State { onPressed: () { Navigator.pop(context); }, - child: Text( + child: const Text( 'Close', - style: TextStyle(color: const Color(0xFFd71f1f)), + style: TextStyle(color: Color(0xFFd71f1f)), ), ), //delete - index < 2 - ? TextButton( - onPressed: () { - nextDialog(); - }, - child: Text( - 'Next', - style: TextStyle(color: const Color(0xFFd71f1f)), - ), - ) - : Container(), + if (index < 2) + TextButton( + onPressed: () { + nextDialog(); + }, + child: const Text( + 'Next', + style: TextStyle(color: Color(0xFFd71f1f)), + ), + ) + else + Container(), ], ); } diff --git a/lib/widgets/bleDialog.dart b/lib/widgets/bleDialog.dart index 5caa01b..5649074 100644 --- a/lib/widgets/bleDialog.dart +++ b/lib/widgets/bleDialog.dart @@ -5,19 +5,19 @@ import '../constants.dart'; class BLEDialog extends StatelessWidget { final Function toggleBluetooth; - BLEDialog({@required this.toggleBluetooth}); + const BLEDialog({@required this.toggleBluetooth}); @override Widget build(BuildContext context) { return AlertDialog( - title: Text('Turn on Bluetooth?'), + title: const Text('Turn on Bluetooth?'), actions: [ //cancel button TextButton( onPressed: () { Navigator.pop(context); }, - child: Text( + child: const Text( 'Cancel', style: TextStyle( color: kSpringColor, @@ -31,7 +31,7 @@ class BLEDialog extends StatelessWidget { toggleBluetooth(); Navigator.pop(context); }, - child: Text( + child: const Text( 'Yes', style: TextStyle( color: kSpringColor, diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart new file mode 100644 index 0000000..579b555 --- /dev/null +++ b/lib/widgets/button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +/// Used in +/// 1. Create Fueling Screen +/// 2. Create Car Screen +/// 3. Dashboard Screen +/// 4. Report Issue Screen + +// General button used throughout the app +class Button extends StatelessWidget { + final String title; + final Color color; + final void Function() onTap; + + const Button({ + @required this.title, + @required this.color, + @required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 45, + decoration: BoxDecoration( + color: color, + borderRadius: const BorderRadius.all(Radius.circular(5)), + ), + child: Center( + child: Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/carScreenWidgets/carsListWidget.dart b/lib/widgets/carScreenWidgets/carsListWidget.dart new file mode 100644 index 0000000..ef883b2 --- /dev/null +++ b/lib/widgets/carScreenWidgets/carsListWidget.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import '../../models/car.dart'; +import '../../providers/carsProvider.dart'; +import '../../database/carsTable.dart'; +import '../../database/databaseHelper.dart'; + +class CarsListWidget extends StatefulWidget { + @override + _CarsListWidgetState createState() => _CarsListWidgetState(); +} + +class _CarsListWidgetState extends State { + // Provider to set cars list in the data store and notify widgets + CarsProvider carsProvider; + + // method to fetch cars from database and store them in provider + Future getCarsFromDatabase() async { + final List> carsList = await DatabaseHelper.instance + .readAllValues(tableName: CarsTable.tableName); + carsProvider.setCarsList = carsList; + } + + @override + void initState() { + super.initState(); + + carsProvider = Provider.of(context, listen: false); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (_, carsProvider, child) { + final List carsList = carsProvider.getCarsList; + final Car selectedCar = carsProvider.getSelectedCar; + // Cars haven't been fetched from DB yet + // Fetch them + if (carsList == null) { + getCarsFromDatabase(); + return const Center( + child: Text( + 'There are no cars here', + ), + ); + } + + // Cars have been fetched from DB but it's empty + else if (carsList.isEmpty) { + return const Center( + child: Text( + 'There are no cars here', + ), + ); + } + + // Cars have been fetched from DB and it's not empty + else { + return ListView.builder( + padding: const EdgeInsets.all(0), + itemCount: carsList.length, + itemBuilder: (_, index) { + return GestureDetector( + onTap: () { + carsProvider.setSelectedCar = carsList[index]; + }, + child: ListTile( + contentPadding: const EdgeInsets.all(0), + leading: const Icon(Icons.drive_eta_sharp), + title: Text( + '${carsList[index].manufacturer} - ${carsList[index].model}'), + subtitle: Text( + '${carsList[index].constructionYear}, ${carsList[index].engineDisplacement}, ${carsList[index].fuelType}'), + trailing: Radio( + onChanged: (bool value) {}, + groupValue: true, + value: selectedCar == null + ? false + : (carsList[index].id == selectedCar.id ? true : false), + ), + ), + ); + }, + ); + } + }, + ); + } +} diff --git a/lib/widgets/createCarWidgets/attributesOption.dart b/lib/widgets/createCarWidgets/attributesOption.dart new file mode 100644 index 0000000..2d5272b --- /dev/null +++ b/lib/widgets/createCarWidgets/attributesOption.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import '../../constants.dart'; +import '../../models/car.dart'; +import '../../providers/carsProvider.dart'; +import '../button.dart'; +import '../../database/carsTable.dart'; +import '../../database/databaseHelper.dart'; + +class AttributesOption extends StatefulWidget { + static const routeName = '/createCarScreen'; + @override + _AttributesOptionState createState() => _AttributesOptionState(); +} + +class _AttributesOptionState extends State { + final GlobalKey _formKey = GlobalKey(); + Car newCar = Car(); + + Future createCar() async { + if (_formKey.currentState.validate()) { + final CarsProvider carsProvider = + Provider.of(context, listen: false); + + carsProvider.addCar(newCar); + await DatabaseHelper.instance.insertValue( + tableName: CarsTable.tableName, + data: newCar.toJson(), + ); + + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, + ), + child: SingleChildScrollView( + padding: const EdgeInsets.only(top: 20), + child: Form( + key: _formKey, + child: Column( + children: [ + const Text( + 'Please enter the details of your vehicle by attributes. Be precise. Parts of this information are especially important for calculating estimated values such as consumption.', + style: TextStyle( + color: Colors.grey, + fontSize: 15, + ), + ), + + const SizedBox( + height: 20, + ), + + // Manufacturer + TextFormField( + decoration: inputDecoration.copyWith( + labelText: 'Manufacturer', + ), + onChanged: (value) { + newCar.manufacturer = value; + }, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + + const SizedBox( + height: 20, + ), + + // Model + TextFormField( + decoration: inputDecoration.copyWith( + labelText: 'Model', + ), + onChanged: (value) { + newCar.model = value; + }, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + + const SizedBox( + height: 20, + ), + + // Construction year + TextFormField( + keyboardType: TextInputType.number, + decoration: inputDecoration.copyWith( + labelText: 'Construction year', + ), + onChanged: (value) { + newCar.constructionYear = int.parse(value); + }, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + + const SizedBox( + height: 20, + ), + + // Fuel Type + TextFormField( + decoration: inputDecoration.copyWith( + labelText: 'Fuel Type', + ), + onChanged: (value) { + newCar.fuelType = value; + }, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + + const SizedBox( + height: 20, + ), + + // Engine Displacement + TextFormField( + keyboardType: TextInputType.number, + decoration: inputDecoration.copyWith( + labelText: 'Engine Displacement', + ), + onChanged: (value) { + newCar.engineDisplacement = int.parse(value); + }, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + + const SizedBox( + height: 20, + ), + + // Login Button + Button( + title: 'Create Car', + color: kSpringColor, + onTap: createCar, + ), + + const SizedBox( + height: 20, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/createCarWidgets/tsnHsnOption.dart b/lib/widgets/createCarWidgets/tsnHsnOption.dart new file mode 100644 index 0000000..1b0d80e --- /dev/null +++ b/lib/widgets/createCarWidgets/tsnHsnOption.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import '../../constants.dart'; + +class TsnHsnOption extends StatefulWidget { + @override + _TsnHsnOptionState createState() => _TsnHsnOptionState(); +} + +class _TsnHsnOptionState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, + ), + child: SingleChildScrollView( + padding: const EdgeInsets.only(top: 20), + child: Column( + children: [ + const Text( + 'Please enter the details of you vehicle by HSN and TSN', + style: TextStyle( + color: Colors.grey, + fontSize: 15, + ), + ), + + const SizedBox( + height: 20, + ), + TextFormField( + decoration: inputDecoration.copyWith( + labelText: 'HSN', + ), + onChanged: (value) {}, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + + const SizedBox( + height: 20, + ), + + // Construction year + TextFormField( + keyboardType: TextInputType.number, + decoration: inputDecoration.copyWith( + labelText: 'TSN', + ), + onChanged: (value) {}, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/dashboardWidgets/dashboardCard.dart b/lib/widgets/dashboardWidgets/dashboardCard.dart index 0651df9..4a42bdf 100644 --- a/lib/widgets/dashboardWidgets/dashboardCard.dart +++ b/lib/widgets/dashboardWidgets/dashboardCard.dart @@ -17,7 +17,7 @@ class DashboardCard extends StatelessWidget { @required this.title, @required this.subtitle, @required this.routeName, - this.iconBackgroundColor: kSpringColor + this.iconBackgroundColor = kSpringColor }); @override @@ -27,11 +27,22 @@ class DashboardCard extends StatelessWidget { Navigator.of(context).pushNamed(routeName); }, child: Container( + margin: const EdgeInsets.symmetric(horizontal: 15), height: deviceHeight * 0.12, - width: deviceWidth * 0.9, - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( horizontal: 15, ), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey[300], + blurRadius: 8.0, + offset: const Offset(3, 4), + ), + ], + borderRadius: BorderRadius.circular(10), + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -54,7 +65,7 @@ class DashboardCard extends StatelessWidget { ), ), ), - SizedBox( + const SizedBox( width: 10, ), Column( @@ -65,13 +76,16 @@ class DashboardCard extends StatelessWidget { title, style: TextStyle( fontWeight: FontWeight.bold, - fontSize: deviceHeight * 0.022, + fontSize: deviceHeight * 0.02, ), ), + const SizedBox( + height: 4, + ), Text( subtitle, style: TextStyle( - fontSize: deviceHeight * 0.02, + fontSize: deviceHeight * 0.018, ), ), ], @@ -86,17 +100,6 @@ class DashboardCard extends StatelessWidget { ), ], ), - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.grey[300], - blurRadius: 8.0, - offset: Offset(3, 4), - ), - ], - borderRadius: BorderRadius.circular(10), - ), ), ); } diff --git a/lib/widgets/dashboardWidgets/statsWidget.dart b/lib/widgets/dashboardWidgets/statsWidget.dart index 246c594..c1fbc7f 100644 --- a/lib/widgets/dashboardWidgets/statsWidget.dart +++ b/lib/widgets/dashboardWidgets/statsWidget.dart @@ -21,12 +21,16 @@ class StatsWidget extends StatelessWidget { ), child: Consumer( builder: (context, userStatsProvider, child) { - UserStats userStats = userStatsProvider.getUserStats; + final UserStats userStats = userStatsProvider.getUserStats; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( + width: deviceWidth * 0.3, + decoration: const BoxDecoration( + color: Color.fromARGB(255, 65, 80, 100), + borderRadius: BorderRadius.all(Radius.circular(5)), + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -53,13 +57,13 @@ class StatsWidget extends StatelessWidget { ), ], ), + ), + Container( width: deviceWidth * 0.3, - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Color.fromARGB(255, 65, 80, 100), borderRadius: BorderRadius.all(Radius.circular(5)), ), - ), - Container( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -86,13 +90,13 @@ class StatsWidget extends StatelessWidget { ), ], ), + ), + Container( width: deviceWidth * 0.3, - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Color.fromARGB(255, 65, 80, 100), borderRadius: BorderRadius.all(Radius.circular(5)), ), - ), - Container( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -119,11 +123,6 @@ class StatsWidget extends StatelessWidget { ), ], ), - width: deviceWidth * 0.3, - decoration: BoxDecoration( - color: Color.fromARGB(255, 65, 80, 100), - borderRadius: BorderRadius.all(Radius.circular(5)), - ), ), ], ); diff --git a/lib/widgets/dividerLine.dart b/lib/widgets/dividerLine.dart new file mode 100644 index 0000000..43541df --- /dev/null +++ b/lib/widgets/dividerLine.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +/// Used in +/// 1. Create Fueling Screen +/// 2. Profile Screen +/// 3. Settings Screen +/// 4. Fueling Card Widget + +// Creates a divider line +class DividerLine extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric( + vertical: 8, + ), + child: Divider( + thickness: 2, + ), + ); + } +} diff --git a/lib/widgets/logbookWidgets/fuelingCard.dart b/lib/widgets/logbookWidgets/fuelingCard.dart new file mode 100644 index 0000000..b52b9f1 --- /dev/null +++ b/lib/widgets/logbookWidgets/fuelingCard.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; + +import '../../constants.dart'; +import '../../models/fueling.dart'; +import '../dividerLine.dart'; + +class FuelingCard extends StatelessWidget { + final Fueling fueling; + + const FuelingCard({@required this.fueling}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric( + vertical: 10, + ), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 15, + ), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey[300], + blurRadius: 8.0, + offset: const Offset(3, 4), + ), + ], + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Gas station icon and date + // TODO: Add real date + Row( + children: const [ + Icon( + Icons.ev_station_rounded, + size: 30, + ), + SizedBox( + width: 10, + ), + Text( + 'Today', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + + // Total Price + Text( + '\$${fueling.totalPrice}', + style: const TextStyle( + color: kSpringColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + DividerLine(), + + // Mileage and Fueled Volume + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${fueling.mileage} km - ${fueling.fueledVolume} L'), + Text('${fueling.pricePerLitre} \$/L'), + ], + ), + const SizedBox( + height: 20, + ), + + // Car information + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.local_taxi), + const SizedBox( + width: 10, + ), + Flexible( + child: Text( + '${fueling.car.manufacturer} ${fueling.car.model} (${fueling.car.constructionYear} / ${fueling.car.engineDisplacement}ccm)'), + ), + ], + ), + const SizedBox( + height: 10, + ), + + // Comment + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.comment), + const SizedBox( + width: 10, + ), + Flexible( + child: Text(fueling.comment), + ), + ], + ), + DividerLine(), + + // Partial Fill-Up and Missed Previous Fill-Up + if (fueling.partialFueling) + Row( + children: const [ + Icon(Icons.check), + SizedBox( + width: 10, + ), + Text('Partial Fill-up'), + ], + ) + else + Container(), + if (fueling.missedPreviousFueling) + Row( + children: const [ + Icon(Icons.check), + SizedBox( + width: 10, + ), + Text('Missed Previous Fill-up'), + ], + ) + else + Container(), + ], + ), + ); + } +} diff --git a/lib/widgets/mapWidget.dart b/lib/widgets/mapWidget.dart index a59c243..4e25964 100644 --- a/lib/widgets/mapWidget.dart +++ b/lib/widgets/mapWidget.dart @@ -7,7 +7,7 @@ import 'package:latlong/latlong.dart'; class MapWidget extends StatefulWidget { final Function initializeLocation; - MapWidget({@required this.initializeLocation}); + const MapWidget({@required this.initializeLocation}); @override _MapWidgetState createState() => _MapWidgetState(); @@ -20,7 +20,7 @@ class _MapWidgetState extends State { stream: Geolocator.getPositionStream(), builder: (_, snapshot) { if (snapshot.hasData) { - Position position = snapshot.data; + final Position position = Position.fromMap(snapshot.data); return Center( child: FlutterMap( options: MapOptions( @@ -43,11 +43,9 @@ class _MapWidgetState extends State { width: 50.0, height: 50.0, point: LatLng(position.latitude, position.longitude), - builder: (ctx) => Container( - child: Icon( - Icons.location_on, - color: Colors.red, - ), + builder: (ctx) => const Icon( + Icons.location_on, + color: Colors.red, ), ) ], @@ -62,7 +60,7 @@ class _MapWidgetState extends State { widget.initializeLocation(); return Container(); } else { - return Center( + return const Center( child: CircularProgressIndicator(), ); } diff --git a/lib/widgets/settingsScreenWidgets/settingsListWidget.dart b/lib/widgets/settingsScreenWidgets/settingsListWidget.dart new file mode 100644 index 0000000..071a306 --- /dev/null +++ b/lib/widgets/settingsScreenWidgets/settingsListWidget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import '../../models/settingsTileModel.dart'; + +// List of checkbox settings tile on settings screen +class SettingsListWidget extends StatefulWidget { + final List settings; + + const SettingsListWidget({ + @required this.settings, + }); + @override + _SettingsListWidgetState createState() => _SettingsListWidgetState(); +} + +class _SettingsListWidgetState extends State { + // Toggles the checkbox + void toggleCheckBox({@required int index}) { + setState(() { + final bool currentVal = widget.settings[index].isChecked; + widget.settings[index].isChecked = !currentVal; + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: List.generate( + widget.settings.length, + (index) { + return CheckboxListTile( + contentPadding: const EdgeInsets.only( + bottom: 10, + ), + title: Text(widget.settings[index].title), + subtitle: Text(widget.settings[index].subtitle), + value: widget.settings[index].isChecked, + onChanged: (bool val) { + toggleCheckBox(index: index); + }, + ); + }, + ), + ); + } +} diff --git a/lib/widgets/singleRowForm.dart b/lib/widgets/singleRowForm.dart new file mode 100644 index 0000000..7fac8ce --- /dev/null +++ b/lib/widgets/singleRowForm.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import '../constants.dart'; + +/// Used in +/// 1. Create Fueling Screen +/// 2. Report Issue Screen + +// Title and Textfield in a Row Widget of CreateFuelingScreen +class SingleRowForm extends StatelessWidget { + final String title; + final String hint; + final TextEditingController textEditingController; + + const SingleRowForm({ + @required this.title, + @required this.hint, + @required this.textEditingController, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + // Title of the textfield + Expanded(child: Text(title)), + + // Textfield + Expanded( + child: TextFormField( + controller: textEditingController, + decoration: inputDecoration.copyWith( + hintText: hint, + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value.isEmpty || value == null) { + return 'Required'; + } + return null; + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/tabButtonsPage.dart b/lib/widgets/tabButtonsPage.dart new file mode 100644 index 0000000..4756e85 --- /dev/null +++ b/lib/widgets/tabButtonsPage.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; + +import '../constants.dart'; +import '../globals.dart'; + +class TabButtonsPage extends StatefulWidget { + final String button1Title; + final String button2Title; + final dynamic tab1; + final dynamic tab2; + final Widget page1; + final Widget page2; + + const TabButtonsPage({ + @required this.button1Title, + @required this.button2Title, + @required this.tab1, + @required this.tab2, + @required this.page1, + @required this.page2, + }); + + @override + _TabButtonsPageState createState() => _TabButtonsPageState(); +} + +class _TabButtonsPageState extends State { + dynamic selectedTab; + + void changeTab() { + setState(() { + if (selectedTab == widget.tab1) { + selectedTab = widget.tab2; + } else { + selectedTab = widget.tab1; + } + }); + } + + @override + void initState() { + super.initState(); + + selectedTab = widget.tab1; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + changeTab(); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 2, + color: kSpringColor, + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topLeft: Radius.circular(10), + ), + color: selectedTab == widget.tab1 + ? kSpringColor + : Colors.white, + ), + height: 40, + width: deviceWidth * 0.3, + child: Center( + child: Text( + widget.button1Title, + style: TextStyle( + color: selectedTab == widget.tab1 + ? Colors.white + : kSpringColor, + ), + ), + ), + ), + ), + GestureDetector( + onTap: () { + changeTab(); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 2, + color: kSpringColor, + ), + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(10), + topRight: Radius.circular(10), + ), + color: selectedTab == widget.tab2 + ? kSpringColor + : Colors.white, + ), + height: 40, + width: deviceWidth * 0.3, + child: Center( + child: Text( + widget.button2Title, + style: TextStyle( + color: selectedTab == widget.tab2 + ? Colors.white + : kSpringColor, + ), + ), + ), + ), + ), + ], + ), + ), + if (selectedTab == widget.tab1) widget.page1 else widget.page2, + ], + ); + } +} diff --git a/lib/widgets/titleWidget.dart b/lib/widgets/titleWidget.dart new file mode 100644 index 0000000..0125db6 --- /dev/null +++ b/lib/widgets/titleWidget.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../constants.dart'; + +/// Used in +/// 1. Create Fueling Screen +/// 2. Log Book Screen +/// 3. Report Issue Screen +/// 4. Settings Screen + +class TitleWidget extends StatelessWidget { + final String title; + + const TitleWidget({@required this.title}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 5.0), + child: Text( + title, + style: const TextStyle( + fontSize: 15, + color: kSpringColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/trackDetailsWidgets/carDetailsCard.dart b/lib/widgets/trackDetailsWidgets/carDetailsCard.dart index e37563f..ee9e5ea 100644 --- a/lib/widgets/trackDetailsWidgets/carDetailsCard.dart +++ b/lib/widgets/trackDetailsWidgets/carDetailsCard.dart @@ -3,73 +3,85 @@ import 'package:flutter/material.dart'; import '../../globals.dart'; class CarDetailsCard extends StatelessWidget { + Widget buildWidget({ + @required String title, + @required IconData iconData, + @required String information, + }) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + iconData, + size: 30, + ), + const SizedBox( + width: 10, + ), + Text(title), + ], + ), + Flexible( + child: Text(information), + ), + ], + ); + } + @override Widget build(BuildContext context) { return Container( width: deviceWidth * 0.9, - padding: EdgeInsets.all(20), - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - Icons.drive_eta, - size: 30, - ), - Text('Car'), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('ALPHA ROMEO 209'), - Text('2009, Diesel'), - ], - ), - ], - ), - SizedBox( - height: 20, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - Icons.drive_eta, - size: 30, - ), - Text('Car'), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('ALPHA ROMEO 209'), - Text('2009, Diesel'), - ], - ), - ], - ), - ], - ), + padding: const EdgeInsets.all(20), decoration: BoxDecoration( boxShadow: [ BoxShadow( color: Colors.grey[350], blurRadius: 3.0, spreadRadius: 1.0, - offset: Offset(-2, 2), + offset: const Offset(-2, 2), ), ], color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5)), + borderRadius: const BorderRadius.all(Radius.circular(5)), + ), + child: Column( + children: [ + buildWidget( + title: 'Car', + iconData: Icons.drive_eta, + information: 'ALPHA ROMEO 209\n2009, Diesel', + ), + const SizedBox( + height: 20, + ), + buildWidget( + title: 'Start', + iconData: Icons.timer, + information: 'Jun 28, 2021 1:10:31 PM', + ), + const SizedBox( + height: 20, + ), + buildWidget( + title: 'End', + iconData: Icons.lock_clock, + information: 'Jun 28, 2021 1:10:31 PM', + ), + const SizedBox( + height: 20, + ), + const Align( + child: Text( + 'Bei diesen Werten handelt es sich um geschätzte Werte, die sich deutlich von den realen Werten unterscheiden können. Wir können daher keine Gewährleistungfür diese Angaben geben.', + style: TextStyle( + fontSize: 10, + ), + ), + ), + ], ), ); } diff --git a/lib/widgets/trackDetailsWidgets/stackedMapButton.dart b/lib/widgets/trackDetailsWidgets/stackedMapButton.dart index 58d1baf..521c26c 100644 --- a/lib/widgets/trackDetailsWidgets/stackedMapButton.dart +++ b/lib/widgets/trackDetailsWidgets/stackedMapButton.dart @@ -4,34 +4,34 @@ import '../../constants.dart'; import '../../globals.dart'; class StackedMapButton extends StatelessWidget { + final double buttonDiameter = 55; + @override Widget build(BuildContext context) { - return Container( - height: (deviceHeight * 0.3) + (deviceHeight * 0.06) / 2, + return SizedBox( + height: (deviceHeight * 0.3) + buttonDiameter / 2, width: double.infinity, child: Stack( children: [ - Container( - child: Image.asset( - 'assets/images/map_placeholder.png', - fit: BoxFit.cover, - height: deviceHeight * 0.3, - width: double.infinity, - ), + Image.asset( + 'assets/images/map_placeholder.png', + fit: BoxFit.cover, + height: deviceHeight * 0.3, + width: double.infinity, ), Positioned( - top: (deviceHeight * 0.3) - (deviceHeight * 0.06) / 2, - right: (deviceHeight * 0.06) / 2, + top: (deviceHeight * 0.3) - buttonDiameter / 2, + right: (30) / 2, child: GestureDetector( onTap: () {}, child: Container( - decoration: BoxDecoration( + decoration: const BoxDecoration( color: kSpringColor, shape: BoxShape.circle, ), - height: deviceHeight * 0.06, - width: deviceHeight * 0.06, - child: Icon( + height: buttonDiameter, + width: buttonDiameter, + child: const Icon( Icons.map, color: Colors.white, ), diff --git a/lib/widgets/trackDetailsWidgets/trackDetailsTile.dart b/lib/widgets/trackDetailsWidgets/trackDetailsTile.dart new file mode 100644 index 0000000..60dc9ac --- /dev/null +++ b/lib/widgets/trackDetailsWidgets/trackDetailsTile.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import '../../constants.dart'; + +class TrackDetailsTile extends StatelessWidget { + final String title; + final String details; + final IconData iconData; + + const TrackDetailsTile({@required this.title, @required this.details, @required this.iconData}); + + @override + Widget build(BuildContext context) { + return ListTile( + dense: true, + title: Text( + title, + style: TextStyle( + color: kGreyColor.withOpacity(0.8), + fontWeight: FontWeight.w600 + ), + ), + leading: Icon( + iconData, + color: kSpringColor, + ), + trailing: Text( + details, + textAlign: TextAlign.end, + style: TextStyle( + color: kGreyColor.withOpacity(0.7), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tracksScrrenWidgets/localTracksList.dart b/lib/widgets/tracksScrrenWidgets/localTracksList.dart index cf7968a..21497ef 100644 --- a/lib/widgets/tracksScrrenWidgets/localTracksList.dart +++ b/lib/widgets/tracksScrrenWidgets/localTracksList.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class LocalTracksList extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( + return const Center( child: Text('No Tracks Here'), ); } diff --git a/lib/widgets/tracksScrrenWidgets/tabButtons.dart b/lib/widgets/tracksScrrenWidgets/tabButtons.dart deleted file mode 100644 index 58c5b1b..0000000 --- a/lib/widgets/tracksScrrenWidgets/tabButtons.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../constants.dart'; -import '../../globals.dart'; -import '../../utils/enums.dart'; - -class TabButtons extends StatelessWidget { - final Function changeTab; - final SelectedTab selectedTab; - - TabButtons({@required this.changeTab, @required this.selectedTab}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - changeTab(); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - width: 2, - color: kSpringColor, - ), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(10), - topLeft: Radius.circular(10), - ), - color: selectedTab == SelectedTab.Local - ? kSpringColor - : Colors.white, - ), - height: 40, - width: deviceWidth * 0.3, - child: Center( - child: Text( - 'Local', - style: TextStyle( - color: selectedTab == SelectedTab.Local - ? Colors.white - : kSpringColor), - ), - ), - ), - ), - GestureDetector( - onTap: () { - changeTab(); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - width: 2, - color: kSpringColor, - ), - borderRadius: BorderRadius.only( - bottomRight: Radius.circular(10), - topRight: Radius.circular(10), - ), - color: selectedTab == SelectedTab.Uploaded - ? kSpringColor - : Colors.white, - ), - height: 40, - width: deviceWidth * 0.3, - child: Center( - child: Text( - 'Uploaded', - style: TextStyle( - color: selectedTab == SelectedTab.Uploaded - ? Colors.white - : kSpringColor), - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/widgets/tracksScrrenWidgets/trackCard.dart b/lib/widgets/tracksScrrenWidgets/trackCard.dart index c6ec46c..5cd9680 100644 --- a/lib/widgets/tracksScrrenWidgets/trackCard.dart +++ b/lib/widgets/tracksScrrenWidgets/trackCard.dart @@ -8,75 +8,140 @@ import '../../screens/trackDetailsScreen.dart'; class TrackCard extends StatelessWidget { final Track track; - TrackCard({@required this.track}); + const TrackCard({@required this.track}); @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - TrackDetailsScreen.routeName, - arguments: track, - ); - }, - child: Container( - width: deviceWidth * 0.9, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - color: kSpringColor, - borderRadius: BorderRadius.only(topLeft: Radius.circular(deviceWidth * 0.018), topRight: Radius.circular(deviceWidth * 0.018)), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: EdgeInsets.symmetric( - vertical: 20, - horizontal: 20, - ), - child: RichText( - text: TextSpan( - text: 'Track ', - style: TextStyle( - fontWeight: FontWeight.w800, - color: kWhiteColor, - ), - children: [ - TextSpan( - text: track.begin.toUtc().toString().replaceFirst('.000Z', ''), - ), - ], + return Container( + width: deviceWidth * 0.9, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.grey[350], + blurRadius: 3.0, + spreadRadius: 1.0, + offset: const Offset(-2, 2), + ), + ], + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(5)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: kSpringColor, + borderRadius: BorderRadius.only(topLeft: Radius.circular(deviceWidth * 0.018), topRight: Radius.circular(deviceWidth * 0.018)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 20, + ), + child: RichText( + text: TextSpan( + text: 'Track ', + style: const TextStyle( + fontWeight: FontWeight.w800, + color: kWhiteColor, ), - textAlign: TextAlign.center, + children: [ + TextSpan( + text: track.begin.toUtc().toString().replaceFirst('.000Z', ''), + ), + ], ), + textAlign: TextAlign.center, ), - Container( - padding: EdgeInsets.all(15), - child: Icon( + ), + Container( + padding: const EdgeInsets.all(15), + child: PopupMenuButton( + enabled: true, + onSelected: (int index) { + if (index == 0) { + Navigator.of(context).pushNamed( + TrackDetailsScreen.routeName, + arguments: track, + ); + } + else if (index == 1) { + // TODO: function to delete track + debugPrint('delete track tapped'); + } + else if (index == 2) { + // TODO: function to upload track as open data + debugPrint('upload as open data tapped'); + } + else if (index == 3) { + // TODO: function to export track + debugPrint('export track tapped'); + } + }, + itemBuilder: (_) => [ + const PopupMenuItem( + value: 0, + child: Text( + 'Show Details', + ), + ), + const PopupMenuItem( + value: 1, + child: Text( + 'Delete Track', + ), + ), + const PopupMenuItem( + value: 2, + child: Text( + 'Upload Track as Open Data', + ), + ), + const PopupMenuItem( + value: 3, + child: Text( + 'Export Track', + ), + ), + ], + child: const Icon( Icons.more_vert_outlined, color: kWhiteColor, - // TODO: add onTap functionality to the more_vertical icon ), ), - ], - ), + ), + ], ), + ), + GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + TrackDetailsScreen.routeName, + arguments: track, + ); + }, // TODO: replace the placeholder map image with map widget - Container( - child: Image.asset( - 'assets/images/map_placeholder.png', - fit: BoxFit.cover, - height: deviceHeight * 0.2, - width: double.infinity, - ), + child: Image.asset( + 'assets/images/map_placeholder.png', + fit: BoxFit.cover, + height: deviceHeight * 0.2, + width: double.infinity, ), - Container( - padding: EdgeInsets.symmetric( + ), + GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + TrackDetailsScreen.routeName, + arguments: track, + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( vertical: 20, horizontal: 20, ), @@ -91,7 +156,7 @@ class TrackCard extends StatelessWidget { .difference(track.begin) .toString() .replaceFirst('.000000', ''), - style: TextStyle( + style: const TextStyle( color: kSpringColor, fontSize: 25, fontWeight: FontWeight.w800, @@ -100,7 +165,7 @@ class TrackCard extends StatelessWidget { // SizedBox( // height: deviceHeight * 0.02, // ), - Text( + const Text( 'Duration', style: TextStyle( fontSize: 15, @@ -113,8 +178,8 @@ class TrackCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - track.length.toStringAsFixed(2) + 'km', - style: TextStyle( + '${track.length.toStringAsFixed(2)}km', + style: const TextStyle( color: kSpringColor, fontSize: 25, fontWeight: FontWeight.w800, @@ -123,7 +188,7 @@ class TrackCard extends StatelessWidget { // SizedBox( // height: deviceHeight * 0.02, // ), - Text( + const Text( 'Distance', style: TextStyle( fontSize: 15, @@ -135,20 +200,8 @@ class TrackCard extends StatelessWidget { ], ), ), - ], - ), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey[350], - blurRadius: 3.0, - spreadRadius: 1.0, - offset: Offset(-2, 2), - ), - ], - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5)), - ), + ), + ], ), ); } diff --git a/lib/widgets/tracksScrrenWidgets/trackDetailsCard.dart b/lib/widgets/tracksScrrenWidgets/trackDetailsCard.dart new file mode 100644 index 0000000..1271d0c --- /dev/null +++ b/lib/widgets/tracksScrrenWidgets/trackDetailsCard.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import '../../models/track.dart'; + +import '../../globals.dart'; +import '../../constants.dart'; +import '../../widgets/trackDetailsWidgets/trackDetailsTile.dart'; + +class TrackDetailsCard extends StatelessWidget { + final Track track; + + const TrackDetailsCard({@required this.track}); + + @override + Widget build(BuildContext context) { + return Container( + width: deviceWidth * 0.9, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.grey[350], + blurRadius: 3.0, + spreadRadius: 1.0, + offset: const Offset(-2, 2), + ), + ], + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(5)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 15), + alignment: Alignment.center, + decoration: const BoxDecoration( + color: kSpringColor, + borderRadius: BorderRadius.only(topRight: Radius.circular(5), topLeft: Radius.circular(5)) + ), + child: RichText( + text: TextSpan( + text: 'Track ', + style: const TextStyle( + color: kWhiteColor, + fontWeight: FontWeight.w600, + fontSize: 22, + ), + children: [ + TextSpan( + text: track.begin.toUtc().toString().replaceFirst('.000Z', '') + ) + ] + ), + ), + ), + const SizedBox(height: 5), + Container( + padding: const EdgeInsets.all(10), + child: Text( + 'Your track with ${track.sensor.properties.manufacturer} ${track.sensor.properties.model} on ${trackDay(track.begin.weekday)}', + style: TextStyle( + color: kGreyColor.withOpacity(0.8) + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + track.end + .difference(track.begin) + .toString() + .replaceFirst('.000000', ''), + style: const TextStyle( + color: kSpringColor, + fontWeight: FontWeight.w500, + fontSize: 22, + ), + ), + Text( + 'Duration', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: kGreyColor.withOpacity(0.8) + ), + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${track.length.toStringAsFixed(2)} km', + style: const TextStyle( + color: kSpringColor, + fontWeight: FontWeight.w500, + fontSize: 22, + ), + ), + Text( + 'Distance', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: kGreyColor.withOpacity(0.8) + ), + ), + ], + ), + ], + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + color: kGreyColor.withOpacity(0.3), + thickness: 1.2, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: TrackDetailsTile( + title: 'Car', + details: '${track.sensor.properties.manufacturer} - ${track.sensor.properties.model} ${track.sensor.properties.constructionYear}, ${track.sensor.properties.engineDisplacement} cm, ${track.sensor.properties.fuelType}', + iconData: Icons.directions_car + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: TrackDetailsTile( + title: 'Start', + details: track.begin.toUtc().toString().replaceFirst('.000Z', ''), + iconData: Icons.timer_outlined, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: TrackDetailsTile( + title: 'End', + details: track.end.toUtc().toString().replaceFirst('.000Z', ''), + iconData: Icons.timelapse, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: TrackDetailsTile( + title: 'Speed', + details: '${determineSpeed(track.length, track.begin, track.end).toStringAsFixed(2)} km/hr', + iconData: Icons.speed_rounded, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: const TrackDetailsTile( + title: 'Number of stops', + details: '1 stops', + iconData: Icons.bus_alert, + ), + ), + const SizedBox( + height: 10, + ), + ], + ), + ); + } + + String trackDay(int weekday) { + String day = 'Sunday'; + if (weekday == 1) { + day = 'Monday'; + } else if (weekday == 2) { + day = 'Tuesday'; + } else if (weekday == 3) { + day = 'Wednesday'; + } else if (weekday == 4) { + day = 'Thursday'; + } else if (weekday == 5) { + day = 'Friday'; + } else if (weekday == 6) { + day = 'Saturday'; + } + return day; + } + + double determineSpeed(double distance, DateTime start, DateTime end) { + final int duration = end.difference(start).inMinutes; + final double timeInHours = duration / 60; + final double speed = distance / timeInHours; + return speed; + } +} \ No newline at end of file diff --git a/lib/widgets/tracksScrrenWidgets/uploadedTracksList.dart b/lib/widgets/tracksScrrenWidgets/uploadedTracksList.dart index 22fdd92..92da7ed 100644 --- a/lib/widgets/tracksScrrenWidgets/uploadedTracksList.dart +++ b/lib/widgets/tracksScrrenWidgets/uploadedTracksList.dart @@ -34,16 +34,16 @@ class _UploadedTracksListState extends State { _tracksProvider.getTracks().isNotEmpty) { return Consumer( builder: (_, tracksProvider, child) { - List tracksList = tracksProvider.getTracks(); + final List tracksList = tracksProvider.getTracks(); if (tracksList.isEmpty) { - return Center( + return const Center( child: Text('No Tracks'), ); } return ListView.builder( shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), + physics: const NeverScrollableScrollPhysics(), itemCount: tracksList.length, itemBuilder: (_, i) { return Padding( @@ -57,7 +57,7 @@ class _UploadedTracksListState extends State { }, ); } else { - return Center( + return const Center( child: CircularProgressIndicator(), ); } diff --git a/pubspec.lock b/pubspec.lock index 859d2af..67e64f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.6.1" boolean_selector: dependency: transitive description: @@ -273,6 +273,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.1" + lint: + dependency: "direct main" + description: + name: lint + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.3" lists: dependency: transitive description: @@ -515,7 +522,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sqflite: dependency: "direct main" description: @@ -578,7 +585,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" + version: "0.3.0" toast: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d0bf34b..42071c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,24 +24,56 @@ dependencies: flutter: sdk: flutter + # for state management provider: ^5.0.0 + + # for making http calls http: ^0.13.1 + + # to securely store user info for silent signin flutter_secure_storage: ^4.1.0 + + # for communicating with BLE devices on Android and iOS flutter_reactive_ble: ^3.1.1+1 + system_shortcuts: ^1.0.0 + + # for getting user location geolocator: ^7.0.1 latlong: ^0.6.1 flutter_map: ^0.12.0 location: ^4.1.1 + + # to launch App store and Play store to rate the app url_launcher: ^6.0.6 + + # to preview the app on different screens sizes device_preview: ^0.7.3 + + # for onboarding screens introduction_screen: ^2.1.0 + + # to store small persistent data shared_preferences: ^2.0.6 + + # to show SVGs flutter_svg: ^0.22.0 + + # to use custom fonts google_fonts: ^2.1.0 + toast: ^0.1.5 + + # to generate unique IDs uuid: ^3.0.4 + + # for storing data locally sqflite: ^2.0.0+3 + + # to follow good code guidelines by google + lint: ^1.5.3 + + # to get path of the app on the device path: ^1.8.0