Skip to content
Snippets Groups Projects
editevent.dart 19.1 KiB
Newer Older
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 OverlayEntry _overlayEntry;  //For event creation success notifications
  late Timer _overlayCloseTimer;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() async {
    try {
Maximilian Betz's avatar
Maximilian Betz committed
      //_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(
Maximilian Betz's avatar
Maximilian Betz committed
      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:
                        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!