import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'datamodel.dart'; import 'sensorconnector.dart'; import 'databaseconnector.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; class Configuration extends StatefulWidget { const Configuration({Key? key}) : super(key: key); @override State<Configuration> createState() => _MyHomePageState(); } class _MyHomePageState extends State<Configuration> { late Future<Collection> futureCollection; late Future<List<Collection>> futureCollections; late Future<List<EventType>> futureEventTypes; late Future<List<Device>> futureDevices; late Future<String> futureAuthToken; bool onlineConnectivity = false; bool changedCollection = false; //Flag to indicate that the chosen collection has changed and device need to be updated. var database = DatabaseInstance(); SensorConnector connector = SensorConnector(); final ConfigurationStoreInstance configuration = ConfigurationStoreInstance(); late OverlayEntry _overlayEntry; late Timer _overlayCloseTimer; bool _initError = false; String _status = ''; Future<void> dumpToFile(BuildContext context) async{ var date = DateTime.now().toUtc(); var isoDate = date.toIso8601String(); String filename = date.year.toString().padLeft(4, '0') + date.month.toString().padLeft(2, '0') + date.day.toString().padLeft(2, '0') + date.hour.toString().padLeft(2, '0') + date.minute.toString().padLeft(2, '0') + date.second.toString().padLeft(2, '0') + '_events.json'; String eventsJson = await database.eventDump(); final Directory? directory = await getExternalStorageDirectory(); if(directory != null) { String filepath = join(directory.path, filename); final File file = File(filepath); String dumpStr = "{\"events\":" + eventsJson + "}"; await file.writeAsString(dumpStr); debugPrint('Stored file at: ' + filepath); setState(() {}); HapticFeedback.vibrate(); _showResultPopup(context, 'Event database dump created: ' + filepath, false ); } } Future<void> _showDelayed(BuildContext context, String text, bool error) async { await Future.delayed(const Duration(milliseconds: 250)); _showResultPopup(context, text, error); } Future<void> _showResultPopup(BuildContext context, String text, bool error) async { OverlayState? overlayState = Overlay.of(context); try { _overlayEntry.remove(); // Allow only one Overlay Popup at a time }catch(e){ debugPrint('Overlay already removed, during dispose: ' + e.toString()); } _overlayEntry = OverlayEntry(builder: (context) { Color backGroundColor; Color textColor; if (error == true){ backGroundColor = Colors.redAccent; //Style for error message textColor = Colors.black; } else { backGroundColor = Colors.greenAccent; //Style for notification textColor = Colors.black; } return Stack( alignment: Alignment.center, children: [ Positioned( // Position at 10% of height from bottom bottom: MediaQuery.of(context).size.height * 0.1, child: Material( borderRadius: BorderRadius.circular(8.0), color: backGroundColor, //Some transparency remains child: Container( padding: const EdgeInsets.all(5.0), // Space between Text and Bubble width: MediaQuery.of(context).size.width * 0.95, child: TextFormField( minLines: 1, maxLines: 5, readOnly: true, autofocus: false, enabled: false, style: TextStyle(color: textColor), controller: TextEditingController( text: text, ), ), ), ), ), ], ); }); overlayState?.insert(_overlayEntry); try { _overlayCloseTimer.cancel(); // Kill old timers }catch(e){ debugPrint('Timer cancel error: ' + e.toString()); } _overlayCloseTimer = Timer( const Duration(seconds: 5), () { try { _overlayEntry.remove(); // Allow only one Overlay Popup. NOTE: Is this a quick an dirty or a proper solution? }catch(e){ debugPrint('Overlay already removed, during dispose: ' + e.toString()); } }, ); } Future<void> updateConfiguration(BuildContext context) async { final EventStoreInstance event = EventStoreInstance(); String token = ''; String statusPopUpMessageText = ''; bool popUpError = false; try { if (changedCollection == true) { token = await connector.getAuthToken( configuration.loginInformation.mail, configuration.loginInformation.password); configuration.devices = await futureDevices; //Http get already initiated configuration.eventTypes = await connector.fetchEventTypes(); if (configuration.devices.isEmpty) { throw Exception('Collection with 0 devices.'); } event.currentEvent.id = 0; event.currentEvent.urnId = configuration.devices[0] .id; //TODO: fix if devices are an empty list. event.currentEvent.urn = configuration.devices[0].urn; event.currentEvent.description = ''; event.currentEvent.label = ''; event.currentEvent.type = configuration.eventTypes[0].name; var date = DateTime.now().toUtc(); var isoDate = date.toIso8601String(); event.currentEvent.startDate = isoDate; event.currentEvent.endDate = isoDate; configuration.initialized = true; HapticFeedback.vibrate(); await event.storeToSharedPrefs(); await configuration.storeToSharedPrefs(); debugPrint('Configuration stored!'); statusPopUpMessageText = 'Login success, got all EventTypes, loaded Collection Devices & stored configuration for offline usage.'; popUpError = false; changedCollection = false; //Reset flag }else{ await configuration.storeToSharedPrefs(); // collection has not been changed but store the other parameters statusPopUpMessageText = 'Stored changed configuration.'; popUpError = false; } } catch (e) { String errorText = e.toString(); statusPopUpMessageText = errorText.substring(10, errorText.length); //Remove 'Exception' from string. popUpError = true; } setState(() {_showResultPopup(context, statusPopUpMessageText, popUpError);}); } Future<void> fetchInitData() async { try { final ConfigurationStoreInstance configuration = ConfigurationStoreInstance(); List<Collection> localCollections; futureCollections = connector.fetchCollections(); localCollections = await futureCollections; // Check if selected collection is still available bool stillExists = false; for (var collection in localCollections) { if (collection.id == configuration.currentCollection.id){ stillExists = true; // Collection still exists } } configuration.collections = localCollections; if (stillExists == false){ // Set chosen collection to first in list if it not exists there configuration.currentCollection = configuration.collections[0]; } onlineConnectivity = true; setState(() {}); }catch(e){ String errorText = e.toString(); errorText = errorText.substring(10, errorText.length); _status = errorText; //store errorText globally for pop up message during Widget build _initError = true; //Trigger popup error display! setState(() {}); } } @override void initState() { super.initState(); fetchInitData(); } @override void dispose() { try { _overlayEntry.remove(); }catch(e){ debugPrint('Dispose error: $e'); } super.dispose(); } @override Widget build(BuildContext context) { final ConfigurationStoreInstance configuration = ConfigurationStoreInstance(); final EventStoreInstance events = EventStoreInstance(); if(_initError == true){ _initError = false; _showDelayed(context, _status, true); //NOTE: Dirty hack with delay. Stack error without delay. Why? } return Scaffold( appBar: AppBar( title: const Text('Configuration'), ), body: Container( //constraints: const BoxConstraints.expand(), margin: const EdgeInsets.symmetric(horizontal: 5.0), child: SingleChildScrollView( scrollDirection: Axis.vertical, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: <Widget>[ const SizedBox(height: 50), const Text( 'Online access required for initial configuration!', style: TextStyle(fontSize: 14) ), const SizedBox(height: 50), TextFormField( keyboardType: TextInputType.emailAddress, autofocus: false, initialValue: configuration.loginInformation.mail, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'E-Mail', hintText: '', ), onChanged: (value) { configuration.loginInformation.mail = value; }, ), const SizedBox(height: 15.0), TextFormField( obscureText: true, autofocus: false, initialValue: configuration.loginInformation.password, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Password', hintText: '', ), onChanged: (value){ configuration.loginInformation.password = value; }, ), const SizedBox(height: 15.0), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded(child: TextFormField( keyboardType: TextInputType.text, autofocus: false, initialValue: configuration.labelConfig.prefix, inputFormatters:[FilteringTextInputFormatter.allow(RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$'))], decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Label Prefix', hintText: '', ), onChanged: (value) { configuration.labelConfig.prefix = value; }, ) ), const SizedBox(width: 5.0), Expanded(child: TextFormField( keyboardType: TextInputType.number, autofocus: false, initialValue: configuration.labelConfig.cnt.toString(), inputFormatters:[FilteringTextInputFormatter.allow(RegExp("[0-9]"))], decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Index count', hintText: '', ), onChanged: (value) { configuration.labelConfig.cnt = int.parse(value); setState(() {}); }, ) ), const SizedBox(width: 5.0), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value primary: Colors.blue, ), onPressed: () { configuration.labelConfig.cntUp = !configuration.labelConfig.cntUp ; setState(() {}); }, child: configuration.labelConfig.cntUp ? const Text('+') : const Text('-'), ), ], ), const SizedBox(height: 15.0), TextFormField( keyboardType: TextInputType.url, autofocus: false, initialValue: configuration.restRequestUrl, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Rest URL', hintText: '', ), onChanged: (value) { configuration.restRequestUrl = value; }, ), const SizedBox(height: 50), DropdownButtonFormField( value: configuration.currentCollection.collectionName, decoration: const InputDecoration( labelText: 'Chose Collection', border: OutlineInputBorder(), ), items: configuration.collections.map((Collection collection) { return DropdownMenuItem( value: collection.collectionName, child: Text(collection.collectionName), ); }).toList(), onChanged: onlineConnectivity ? ((value) { configuration.currentCollection = configuration.getCollectionFromName(value.toString()); // Start query to fetch the collection devices futureDevices = connector.fetchCollectionDevices(configuration.currentCollection.id); changedCollection = true; }) : null, ), const SizedBox(height: 70.0), ], ), ), ), bottomNavigationBar: Container( margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0), child:Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: [ FloatingActionButton.extended( heroTag: null, tooltip: 'Export events to local .json file', icon: const Icon(Icons.download), label: const Text('Dump Events'), onPressed: () { dumpToFile(context); //Reset for debug testing only. //Remove in RELEASE Version: //database.delete(); //events.reset(); //configuration.reset(); }, ), FloatingActionButton.extended( heroTag: null, tooltip: 'Download corresponding devices and store locally', icon: const Icon(Icons.save), label: const Text('Set'), onPressed: () { updateConfiguration(context); }, ), ], ) ) ); } } //TODO: write configuration on app dispose! //TODO: grey out update button if nothing has changed. ?