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'; class EditEvent extends StatefulWidget { const EditEvent({Key? key}) : super(key: key); @override State<EditEvent> createState() => _EditEventPageState(); } class _EditEventPageState extends State<EditEvent> { bool _initialized = false; late String long = ""; late String lat = ""; late String alt = ""; late double accuracy = 0.0; final prefs = SharedPreferences.getInstance(); // Is async var database = DatabaseInstance(); late Event editEvent; late List<Device> localDevices; late OverlayEntry _overlayEntry; //For event creation success notifications late Timer _overlayCloseTimer; @override void initState() { super.initState(); } @override void dispose() async { try { //_overlayEntry.remove(); }catch(e){ debugPrint('Dispose error on overlay remove: ' + e.toString()); } 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(Event event){ if (RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch( event.label)) { if (RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch( event.description) || event.description == '') { if(_validateLatitude(event.latitude)){ if(_validateLongitude(event.longitude)){ if(_validateElevation(event.elevation)){ 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.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: 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.toString()); } }, ); } Future<void> _updateOpenEvent(BuildContext context) async { await database.updateEvent(editEvent); HapticFeedback.vibrate(); //Feedback that adding event succeeded } @override Widget build(BuildContext context) { /* Get singletons to access relevant data here.*/ final ConfigurationStoreInstance configuration = ConfigurationStoreInstance(); if(false == _initialized) { localDevices = List.from(configuration.devices); //Create a deep copy _initialized = true; final arguments = (ModalRoute .of(context) ?.settings .arguments ?? <String, dynamic>{}) as Map; editEvent = Event.fromEvent(arguments['event']); if (configuration.devices.any((item) => item.urn == editEvent.urn)){ }else{ debugPrint("URN does not exists, adding locally here"); // Device does not exist in configuration as this event has been // created with another selection collection. Add this event temporarily // to the device list! localDevices.add(Device(editEvent.urnId, editEvent.urn)); localDevices.sort((a, b) => a.urn.compareTo(b.urn)); // Sort alphanumeric } } if (configuration.initialized == true) { return Scaffold( appBar: AppBar( title: const Text("Edit Event"), actions: <Widget>[ Column( ), ], ), body: SingleChildScrollView( child: Container( margin: const EdgeInsets.symmetric(horizontal: 5.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ const SizedBox(height: 10.0), TextFormField( initialValue: editEvent.label, autovalidateMode: AutovalidateMode.always, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Label', ), onChanged: (value){ editEvent.label = value; setState(() {}); }, 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), DropdownButtonFormField( value: editEvent.type, isExpanded: true, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Event Type', ), items: configuration.eventTypes.map((EventType event) { return DropdownMenuItem( value: event.name, child: Text(event.name), ); }).toList(), onChanged: (value) { editEvent.type = value.toString(); // TODO: selection shall be based on an object. // Selection of the type id shall not be necessary editEvent.typeId = configuration.getEventIdFromName(editEvent.type); } ), const SizedBox(height: 15.0), DropdownButtonFormField( value: editEvent.urn, isDense: false, isExpanded: true, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'URN', ), items: localDevices.map((Device device) { return DropdownMenuItem( value: device.urn, child: Text(device.urn), ); }).toList(), onChanged: (value) { editEvent.urn = value.toString(); //TODO: introduce more object orientation and be able to // use configuration.getDeviceIdFromUrn(value.toString()); bool found = false; for (var device in localDevices) { if (device.urn == value.toString()) { editEvent.urnId = device.id; found = true; } } if (found == false) { throw Exception( 'Device with urn:' + value.toString() + ' was not found.'); } } ), const SizedBox(height: 15.0), TextFormField( initialValue: editEvent.description, autovalidateMode: AutovalidateMode.always, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Description' ), onChanged: (value){ editEvent.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 } }, ), const SizedBox(height: 15.0), TextFormField( controller: TextEditingController( text: editEvent.startDate.substring(0, 19) + 'Z' //Do not show microseconds ), readOnly: true, decoration: const InputDecoration( labelText: 'Start Timestamp', border: OutlineInputBorder(), ), onTap: () { DatePicker.showDateTimePicker(context, showTitleActions: true, onConfirm: (date) { //Only one field for start and end date. var isoDate = date.toIso8601String(); editEvent.startDate = isoDate; debugPrint('Start Date set to : $isoDate'); setState(() {}); }, currentTime: DateTime.now().toUtc(), locale: LocaleType.en); }, ), const SizedBox(height: 15.0), TextFormField( controller: TextEditingController( text: editEvent.endDate.substring(0, 19) + 'Z' //Do not show microseconds ), readOnly: true, decoration: const InputDecoration( labelText: 'End Timestamp', border: OutlineInputBorder(), ), onTap: () { DatePicker.showDateTimePicker(context, showTitleActions: true, onConfirm: (date) { //Only one field for start and end date. var isoDate = date.toIso8601String(); editEvent.endDate = isoDate; debugPrint('End Date set to : $isoDate'); setState(() {}); }, currentTime: DateTime.now().toUtc(), locale: LocaleType.en); }, ), const SizedBox(height: 15.0), TextFormField( readOnly: false, enabled: true, keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.onUserInteraction, controller: TextEditingController( text: editEvent.latitude.toString()), decoration: const InputDecoration( labelText: 'Latitude', border: OutlineInputBorder(), ), onChanged: (value) { editEvent.latitude = value; }, onFieldSubmitted: (value){ setState(() {}); }, validator: (value) { if (value == "") { return null; // Empty value is allowed } final number = num.tryParse(value!); if (number != null){ if (number >= -90.0 && number <= 90.0){ return null; // Latitude valid } } return "-90 => Latitude <= +90"; }, ), const SizedBox(height: 15.0), TextFormField( readOnly: false, enabled: true, keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.onUserInteraction, controller: TextEditingController( text: editEvent.longitude.toString()), decoration: const InputDecoration( labelText: 'Longitude', border: OutlineInputBorder(), ), onChanged: (value) { editEvent.longitude = value; }, onFieldSubmitted: (value){ setState(() {}); }, validator: (value) { if (value == "") { return null; // Empty value is allowed } final number = num.tryParse(value!); if (number != null){ if (number >= -180.0 && number <= 180.0){ return null; // Longitude valid } } return "-180 => Longitude <= +180"; }, ), const SizedBox(height: 15.0), TextFormField( readOnly: false, enabled: true, keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.onUserInteraction, controller: TextEditingController( text: editEvent.elevation.toString()), decoration: const InputDecoration( labelText: 'Elevation', border: OutlineInputBorder(), ), onChanged: (value) { editEvent.elevation = value; }, onFieldSubmitted: (value){ 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]"; }, ), ] ), ), ), bottomNavigationBar:Container( margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton.extended( heroTag: null, tooltip: 'Cancel', icon: null, label: const Text('Cancel'), onPressed: () { Navigator.pop(context); }, ), const SizedBox(width: 5.0), _validateInput(editEvent) ? FloatingActionButton.extended( heroTag: null, tooltip: 'Update selected event', icon: null, label: const Text('Update'), onPressed: () { if (_validateInput(editEvent)) { _updateOpenEvent(context); _showResultPopup(context, "Updated Event !", false ); Navigator.pop(context); } }, ) : FloatingActionButton.extended( heroTag: null, tooltip: 'Update selected event', icon: null, backgroundColor: Colors.grey, label: const Text('Update'), onPressed: () { }, ), ], ), ), ); }else { return Scaffold( appBar: AppBar(title: const Text("Edit 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: introduce more object orientation and reuse code between edit and add event!