Newer
Older
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'datamodel.dart';
import 'databaseconnector.dart';
import 'dart:async';
import 'package:geolocator/geolocator.dart';
class AddEventKottasPegel extends StatefulWidget {
const AddEventKottasPegel({Key? key}) : super(key: key);
@override
State<AddEventKottasPegel> createState() => _AddEventKottasPegelPageState();
}
class _AddEventKottasPegelPageState extends State<AddEventKottasPegel> {
List<String> angleStatusItems = ['90° (vertical)', '80°', '70°', '60°', '50°', '40°', '30°', ];
Maximilian Betz
committed
List<int> measurementOffsets = [0, 1000, 2000, 3000]; //Colors no, green, red, blue
//Parameters which shall be added to the measurement event as json string data
List<String> measurementStatusItems = ['ok', 'broken', 'missing', 'tilted'];
Maximilian Betz
committed
int measurementStatusId = -1;
Maximilian Betz
committed
late String angleStatus;
Maximilian Betz
committed
int lengthOld = 0;
int lengthNew = 0;
bool syncGNSSData = true;
bool _addButtonEnabled = true;
bool _eventAdded = false;
bool displayAngle = true;
bool displayOldLength = true;
bool displayNewLength = true;
late StreamSubscription<Position> streamHandler;
late Timer restQueryTimer;
final prefs = SharedPreferences.getInstance(); // Is async
var database = DatabaseInstance();
late OverlayEntry _overlayEntry; //For event creation success notifications
late Timer _overlayCloseTimer;
Maximilian Betz
committed
TextEditingController labelEditingController = TextEditingController();
Future startRest() async {
final ConfigurationStoreInstance configuration = ConfigurationStoreInstance();
final EventStoreInstance event = EventStoreInstance();
/* Function which starts querying the URL configuration.restRequestUrl */
restQueryTimer = Timer.periodic(const Duration(seconds: 1), (timer) async {
// Query data from raspberry pi here
try{
Map data = await restConnector.fetchData(configuration.restRequestUrl);
debugPrint("Rest Query succeeded: $data");
event.currentEvent.latitude = data['lat'];
event.currentEvent.longitude = data['lon'];
event.currentEvent.elevation = data['elevation'].toString();
var date = DateTime.parse(data['timestamp']);
var isoDate = date.toIso8601String();
event.currentEvent.startDate = isoDate; //TODO: test and validate
event.currentEvent.endDate = isoDate;
rawMeasurementValue = data['length']; //in mm //accept only int values
accuracy = double.parse(data['precision'].toString()); //accept int and double and string values
//Set some useful error values. TODO: Ensure that the timeout is not to short otherwise this makes things worse:
var date = DateTime.now().toUtc();
var isoDate = date.toIso8601String();
event.currentEvent.startDate = isoDate;
event.currentEvent.endDate = isoDate;
event.currentEvent.latitude = "";
event.currentEvent.longitude = "";
event.currentEvent.elevation = "";
rawMeasurementValue = 0;
if(mounted){
setState(() {
//refresh UI on update
});}
}
final EventStoreInstance event = EventStoreInstance();
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
debugPrint("Check Location Permission");
bool serviceStatus = false;
bool hasPermission = false;
serviceStatus = await Geolocator.isLocationServiceEnabled();
if(serviceStatus){
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
debugPrint('Location permissions are denied');
}else if(permission == LocationPermission.deniedForever){
debugPrint('Location permissions are permanently denied');
}else{
hasPermission = true;
}
}else{
hasPermission = true;
}
if(hasPermission){
debugPrint('Location permissions granted');
if(mounted){
setState(() {
//refresh the UI
});}
debugPrint('Starting location stream');
streamHandler = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 0,
)).listen((Position position) {
debugPrint('Get Location: Lat:${position.latitude} Long:${position.longitude} Alt:${position.altitude}');
event.currentEvent.latitude = position.latitude.toString();
event.currentEvent.longitude = position.longitude.toString();
event.currentEvent.elevation = position.altitude.toString();
var date = DateTime.now().toUtc();
var isoDate = date.toIso8601String();
event.currentEvent.startDate = isoDate;
event.currentEvent.endDate = isoDate;
if(mounted){
setState(() {
//refresh UI on update
});}
});
}
}else{
debugPrint("GPS Service is not enabled, turn on GPS location");
}
if(mounted){
setState(() {
//refresh the UI
});}
}
@override
void initState() {
final EventStoreInstance event = EventStoreInstance();
final ConfigurationStoreInstance configuration = ConfigurationStoreInstance();
// Init displayed parameters with some values to prevent GUI exceptions before the async REST/GNSS data is ready
event.currentEvent.latitude = '0.0';
event.currentEvent.longitude = '0.0';
event.currentEvent.elevation = '0.0';
var date = DateTime.now().toUtc();
var isoDate = date.toIso8601String();
event.currentEvent.startDate = isoDate; //TODO: test and validate
event.currentEvent.endDate = isoDate;
if(event.gnssSync){
// Get data from REST / Raspberry PI
startRest();
}else{
// Get Lat / Long ... from internal GNSS
startGNSS();
}
//Update current event with prefix and cnt
event.currentEvent.label = configuration.labelConfig.prefix + configuration.labelConfig.cnt.toString().padLeft(3, '0');
Maximilian Betz
committed
labelEditingController.text = event.currentEvent.label;
Maximilian Betz
committed
angleStatus = angleStatusItems[0];
super.initState();
}
@override
void dispose() async {
try {
streamHandler.cancel();
debugPrint('Cancel location stream');
}catch(e){
debugPrint('Canceling location stream failed');
}
try {
_overlayEntry.remove();
}catch(e){
debugPrint('Dispose error on overlay remove: $e');
try{
restQueryTimer.cancel();
}catch(e){
debugPrint('Timer cancel error on dispose: $e');
}
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/*Async update current event configuration to shared preferences*/
final EventStoreInstance event = EventStoreInstance();
event.storeToSharedPrefs();
super.dispose();
}
bool _validateLatitude(value){
if (value == ""){
return true; //Empty string is valid
}
var number = num.tryParse(value);
if(number != null){
if (number >= -90.0 && number <= 90.0){
return true; // Latitude valid
}
}
return false;
}
bool _validateLongitude(value){
if (value == ""){
return true; //Empty string is valid
}
var number = num.tryParse(value);
if(number != null){
if (number >= -180.0 && number <= 180.0){
return true; // Longitude valid
}
}
return false;
}
bool _validateElevation(value){
if (value == ""){
return true; //Empty string is valid
}
var number = num.tryParse(value);
if(number != null){
return true; // Any numerical value is valid for elevation
}
return false;
}
bool _validateInput(){
final EventStoreInstance event = EventStoreInstance();
if (RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch(
event.currentEvent.label)) {
if (RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch(
event.currentEvent.description) || event.currentEvent.description == '') {
if(_validateLatitude(event.currentEvent.latitude)){
if(_validateLongitude(event.currentEvent.longitude)){
if(_validateElevation(event.currentEvent.elevation)){
if(measurementStatusId >= 0 && measurementStatusId < measurementStatusItems.length) {
return true;
}
}
}
}
}
}
return false;
}
bool _addButtonStatus(){
if((_validateInput() == true) && (_addButtonEnabled == true)){
return true;
}
return false;
}
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');
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
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
}
_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');
}
_overlayCloseTimer = Timer(
const Duration(seconds: 3),
() {
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');
Future<void> _storeCurrentMeasurementEvent(BuildContext context, bool addAngle, bool addOldLength, bool addNewLength) async {
final EventStoreInstance event = EventStoreInstance();
final ConfigurationStoreInstance configuration = ConfigurationStoreInstance();
//TODO: this is a prototype. URN and ID are hardcoded. Make configurable
Maximilian Betz
committed
event.currentEvent.urn = 'station:neumayer_iii:snow_level_measure';
event.currentEvent.urnId = 9094;
Maximilian Betz
committed
event.currentEvent.type = 'Deployment';
event.currentEvent.typeId = 187;
Maximilian Betz
committed
//Generate Payload Measurement Data
Map<String, dynamic> data =
{
'status' : measurementStatusItems[measurementStatusId],
Maximilian Betz
committed
};
if(true == addAngle){
data['angle_old'] = angleStatus;
}
if(true == addOldLength) {
data['length_old'] = lengthOld;
}
if (true == addNewLength) {
data['length_new'] = lengthNew;
}
Maximilian Betz
committed
//Add payload as json to data field in db
//event.currentEvent.data = data.toString();
event.currentEvent.data = jsonEncode(data);
Maximilian Betz
committed
await database.addEvent(event.currentEvent);
HapticFeedback.vibrate(); //Feedback that adding event succeeded
//Get next Label prefix, id and event timestamp
if (configuration.labelConfig.cntUp == true){
configuration.labelConfig.cnt++;
}else{
configuration.labelConfig.cnt--;
}
event.currentEvent.label = configuration.labelConfig.prefix + configuration.labelConfig.cnt.toString().padLeft(3, '0');
Maximilian Betz
committed
labelEditingController.text = event.currentEvent.label; //Trigger label update
event.currentEvent.description = '';
var date = DateTime.now().toUtc();
var isoDate = date.toIso8601String();
event.currentEvent.startDate = isoDate;
event.currentEvent.endDate = isoDate;
// Reset fields for next event
measurementStatusId = -1;
angleStatus = angleStatusItems[0];
lengthOld = 0;
lengthNew = 0;
await event.storeToSharedPrefs();
await configuration.storeToSharedPrefs();
_addButtonEnabled = true; //Activate button to add more events
_eventAdded = true; // Trigger popup in build method
setState(() {});
}
@override
Widget build(BuildContext context) {
/* Get singletons to access relevant data here.*/
final EventStoreInstance event = EventStoreInstance();
final ConfigurationStoreInstance configuration = ConfigurationStoreInstance();
String switchStatusText = "";
String switchStatusTextLine2 = "";
if (true == event.gnssSync){
switchStatusText = "External REST Data";
switchStatusText = "Internal GNSS"; // Use internal GNSS source
switchStatusTextLine2 = 'No-Fix';
switchStatusTextLine2 = "Precision ${accuracy.toStringAsFixed(1)}m";
// Set visibility parameters
displayAngle = false;
displayOldLength = false;
displayNewLength = false;
if(measurementStatusId == 0){
//ok
displayAngle = true;
displayOldLength = true;
}else if (measurementStatusId == 1){
//broken
displayNewLength = true;
}else if (measurementStatusId == 2){
//missing
displayNewLength = true;
}else {
//tilted / to short
displayAngle = true;
displayOldLength = true;
displayNewLength = true;
}
if (true == _eventAdded){
_eventAdded = false;
Future.delayed(Duration.zero,() {
_showResultPopup(context, "Successfully created Event !", false);
});
}
//if (configuration.initialized == true) {
if (true) {
return Scaffold(
appBar: AppBar(
title: const Text("Kottaspegel"),
actions: <Widget>[
Column(
children: [
Text(switchStatusText, style: const TextStyle(fontStyle: FontStyle.italic)),
Text(switchStatusTextLine2, style: const TextStyle(fontStyle: FontStyle.italic)),
],
),
Switch( //Enable showing all or only pending events. Default is to show only pending events
value: event.gnssSync,
event.gnssSync = value;
debugPrint('Switched to:${event.gnssSync}');
// Get data from rest / raspberry pi
streamHandler.cancel();
startRest();
}else{
// Get data from internal GNSS
restQueryTimer.cancel();
startGNSS();
rawMeasurementValue = 0; //Clear Raw Measurement value
body: SingleChildScrollView(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 5.0),
child:
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
const SizedBox(height: 10.0),
TextFormField(
readOnly: false,
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: TextEditingController(
"${event.currentEvent.startDate.substring(11,19)} / "
"${event.currentEvent.latitude} / "
"${event.currentEvent.longitude} / "
"${event.currentEvent.elevation}"
labelText: 'UTC / Lat / Long / Elv ', //Example
border: OutlineInputBorder(),
),
),
const SizedBox(height: 15.0),
TextFormField(
readOnly: false,
enabled: false,
style: const TextStyle(fontSize: 30.0),
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: TextEditingController(
text: "Length: ${rawMeasurementValue.toString()} [mm]"
),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 15.0),
TextFormField(
Maximilian Betz
committed
controller: labelEditingController,
autovalidateMode: AutovalidateMode.onUserInteraction,
event.currentEvent.label = value;
Maximilian Betz
committed
validator: (value) {
if (!RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch(
value!)) {
return "Only: a-z , A-Z , _ , 0-9 , ,(Comma) , ( , ) , + , - , . , :";
} else {
return null; // Entered Text is valid
}
},
),
const SizedBox(height: 15.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: measurementStatusId == 0 ? Colors.blue : Colors.grey,
measurementStatusId = 0;
setState(() {});
},
child: const Text('Ok'),
),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: measurementStatusId == 1 ? Colors.blue : Colors.grey,
measurementStatusId = 1;
setState(() {});
},
child: const Text('broken'),
),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: measurementStatusId == 2 ? Colors.blue : Colors.grey,
measurementStatusId = 2;
setState(() {});
},
child: const Text('?'),
),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: measurementStatusId == 3 ? Colors.blue : Colors.grey,
measurementStatusId = 3;
setState(() {});
),
]
),
const SizedBox(height: 15.0),
Visibility(
visible: displayAngle,
child:
DropdownButtonFormField(
iconDisabledColor: Colors.green,
Maximilian Betz
committed
value: angleStatus,
isExpanded: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Angle',
),
items:
angleStatusItems.map((String angle) {
return DropdownMenuItem(
value: angle,
//enabled: displayAngle,
child: Text(angle),
);
}).toList(),
onChanged: (value) {
Maximilian Betz
committed
angleStatus = value.toString();
}
),
),
const SizedBox(height: 15.0),
Visibility(
visible: displayOldLength,
child: event.gnssSync ?
TextFormField(
readOnly: false,
enabled: !event.gnssSync,
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
inputFormatters:[FilteringTextInputFormatter.allow(RegExp("[0-9]"))],
controller: TextEditingController(
text: lengthOld.toString(),
),
labelText: 'old length', //Example
validator: (value) {
if (value == "") {
return null; // Empty value is allowed
}
final number = num.tryParse(value!);
if (number != null){
return null; // Elevation valid
}
return "Only numerical values [mm]";
},
) :
TextFormField(
readOnly: false,
enabled: !event.gnssSync,
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
inputFormatters:[FilteringTextInputFormatter.allow(RegExp("[0-9]"))],
initialValue: lengthOld.toString(),
decoration: const InputDecoration(
labelText: 'old length', //Example
border: OutlineInputBorder(),
),
Maximilian Betz
committed
lengthOld = int.parse(value);
},
onFieldSubmitted: (value){
if (!event.gnssSync) {
setState(() {});
}
},
validator: (value) {
if (value == "") {
return null; // Empty value is allowed
}
final number = num.tryParse(value!);
if (number != null){
return null; // Elevation valid
}
return "Only numerical values [mm]";
},
Visibility(
visible: displayOldLength,
child:
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: event.gnssSync == true ? Colors.grey : Colors.grey,
),
onPressed: () {
lengthOld = rawMeasurementValue + measurementOffsets[0];
setState(() {});
},
child: const Text('+0m'),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: event.gnssSync == true ? Colors.green : Colors.grey,
),
onPressed: () {
lengthOld = rawMeasurementValue + measurementOffsets[1];
setState(() {});
},
child: const Text('+1m'),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: event.gnssSync == true ? Colors.red : Colors.grey,
),
onPressed: () {
lengthOld = rawMeasurementValue + measurementOffsets[2];
setState(() {});
},
child: const Text('+2m'),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: event.gnssSync == true ? Colors.blue : Colors.grey,
),
onPressed: () {
lengthOld = rawMeasurementValue + measurementOffsets[3];
setState(() {});
},
child: const Text('+3m'),
Visibility(
visible: displayNewLength,
child: event.gnssSync ?
TextFormField(
readOnly: false,
enabled: !event.gnssSync,
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
inputFormatters:[FilteringTextInputFormatter.allow(RegExp("[0-9]"))],
controller: TextEditingController(
text: lengthNew.toString()
),
decoration: const InputDecoration(
labelText: 'new length', //Example
border: OutlineInputBorder(),
),
onChanged: (value) {
Maximilian Betz
committed
lengthNew = int.parse(value);
},
onFieldSubmitted: (value){
if (!event.gnssSync) {
setState(() {});
}
},
validator: (value) {
if (value == "") {
return null; // Empty value is allowed
}
final number = num.tryParse(value!);
if (number != null){
return null; // Elevation valid
}
return "Only numerical values for elevation in [m]";
},
) :
TextFormField(
readOnly: false,
enabled: !event.gnssSync,
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
inputFormatters:[FilteringTextInputFormatter.allow(RegExp("[0-9]"))],
initialValue: lengthNew.toString(),
decoration: const InputDecoration(
labelText: 'new length', //Example
border: OutlineInputBorder(),
),
validator: (value) {
if (value == "") {
return null; // Empty value is allowed
}
final number = num.tryParse(value!);
if (number != null){
return null; // Elevation valid
}
return "Only numerical values for elevation in [m]";
},
),
),
const SizedBox(height: 5.0),
Visibility(
visible: displayNewLength,
child:
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: event.gnssSync == true ? Colors.grey : Colors.grey,
),
onPressed: () {
lengthNew = rawMeasurementValue + measurementOffsets[0];
setState(() {});
},
child: const Text('+0m'),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: event.gnssSync == true ? Colors.green : Colors.grey,
),
onPressed: () {
lengthNew = rawMeasurementValue + measurementOffsets[1];
setState(() {});
},
child: const Text('+1m'),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: event.gnssSync == true ? Colors.red : Colors.grey,
),
onPressed: () {
lengthNew = rawMeasurementValue + measurementOffsets[2];
setState(() {});
},
child: const Text('+2m'),
const SizedBox(width: 5),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value
primary: event.gnssSync == true ? Colors.blue : Colors.grey,
),
onPressed: () {
lengthNew = rawMeasurementValue + measurementOffsets[3];
setState(() {});
},
child: const Text('+3m'),
initialValue: event.currentEvent.description,
autovalidateMode: AutovalidateMode.always,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Description'
),
onChanged: (value){
event.currentEvent.description = value;
setState(() {});
},
validator: (value) {
if (!RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch(
value!)) {
if(value == ''){
return null; //An empty description is also allowed.
}
return "Only: a-z , A-Z , _ , 0-9 , ,(Comma) , ( , ) , + , - , . , :";
} else {
return null; // Entered Text is valid
}
},
),
]
),
),
),
bottomNavigationBar:
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: _addButtonStatus() ?
FloatingActionButton.extended(
heroTag: null,
tooltip: 'Create new event in local database',
icon: null,
Maximilian Betz
committed
label: const Text('Save'),
onPressed: () {
if (_validateInput()) {
_addButtonEnabled = false; //Disable button until event is stored
_storeCurrentMeasurementEvent(context, displayAngle, displayOldLength, displayNewLength);
}
setState(() {});
},
):
FloatingActionButton.extended(
heroTag: null,
tooltip: 'Input invalid',
icon: null,
backgroundColor: Colors.grey,
Maximilian Betz
committed
label: const Text('Save'),
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
onPressed: () {
},
),
),
const SizedBox(width: 5.0),
],
),
);
}else {
return Scaffold(
appBar: AppBar(title: const Text("Add Event")),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
margin: const EdgeInsets.all(10.0),
child:const Text(
'Check Configuration Page for initial setup!',
style: TextStyle(fontSize: 20)
),
),
],
),
);
}
}
}
//TODO: The app shall prefill the label with a configurable prefix and optionally a running number. E.g. prefix: (PS122.3-4_) running number: 1 label = PS122.3-4_1