Newer
Older
import 'package:flutter/material.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 {
@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;

Maximilian Betz
committed
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);
_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 {

Maximilian Betz
committed
OverlayState? overlayState = Overlay.of(context);
_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

Maximilian Betz
committed
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

Maximilian Betz
committed
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),

Maximilian Betz
committed
controller: TextEditingController(

Maximilian Betz
committed
),
),
),
),
),
],
);
});
overlayState?.insert(_overlayEntry);

Maximilian Betz
committed
_overlayCloseTimer.cancel(); // Kill old timers
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());
}
},

Maximilian Betz
committed
}
Future<void> updateConfiguration(BuildContext context) async {
final EventStoreInstance event = EventStoreInstance();
String statusPopUpMessageText = '';
bool popUpError = false;
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
try {
if (changedCollection == true) {
token = await connector.getAuthToken(
configuration.loginInformation.mail,
configuration.loginInformation.password);
configuration.devices = await connector.fetchCollectionDevices(
configuration.currentCollection
.id); //await futureDevices; //already fetched just use the data.
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
await configuration.storeToSharedPrefs();
// collection has not been changed but store the other parameters
statusPopUpMessageText = 'Stored changed configuration.';
popUpError = false;
} catch (e) {
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;
errorText = errorText.substring(10, errorText.length);
_status = errorText; //store errorText globally for pop up message during Widget build
_initError = true; //Trigger popup error display!
}
@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?
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;
},
),
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
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: 'Label 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('-'),
),
],
),
Maximilian Betz
committed
const SizedBox(height: 15.0),
TextFormField(
keyboardType: TextInputType.url,
Maximilian Betz
committed
autofocus: false,
initialValue: configuration.restRequestUrl,
decoration: const InputDecoration(
border: OutlineInputBorder(),
Maximilian Betz
committed
hintText: '',
),
onChanged: (value) {
configuration.restRequestUrl = value;
},
),
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()); //TODO: do we need another local flag and shall change only only if device query possible?
//Fetch new selected collection devices
futureDevices = connector.fetchCollectionDevices(configuration.currentCollection.id); //TODO: do we need to do this here?
changedCollection = true;
}) : null,
const SizedBox(height: 70.0),
),
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
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. ?