import 'package:flutter/material.dart'; import '../app_theme.dart'; import '../model/alarm/alarm.dart'; import '../model/alarm/fixed_time_alarm.dart'; import '../model/alarm/location_alarm.dart'; import '../model/alarm/relative_time_alarm.dart'; import '../model/requests/create_alarm_request.dart'; import '../model/requests/create_fixed_time_alarm_request.dart'; import '../model/requests/create_location_alarm_request.dart'; import '../model/requests/create_relative_time_alarm_request.dart'; import '../model/requests/create_task_request.dart'; import '../model/extensions/controller_context.dart'; import '../model/task.dart'; import '../service/controllers/task_controller.dart'; import '../service/tools.dart'; import '../widgets/alarm_widgets/alarm_form_widget.dart'; import '../widgets/time_selector.dart'; class TaskEditPage extends StatefulWidget { static const routeName = '/edit'; const TaskEditPage({super.key}); @override State createState() => _TaskEditPageState(); } class _TaskEditPageState extends State { Task? task; bool isInitialized = false; final titleController = TextEditingController(); final descriptionController = TextEditingController(); final categoryController = TextEditingController(); final urlController = TextEditingController(); final dueDateController = TextEditingController(); final dueTimeController = TextEditingController(); String pageTitle = 'Create Task'; final dueDateFocusNode = FocusNode(); final dueTimeFocusNode = FocusNode(); final categoryFocusNode = FocusNode(); final formKey = GlobalKey(debugLabel: 'taskEditFormKey'); bool didFormChange = false; bool isDueTimeEnabled = false; final List alarmRequests = []; @override void didChangeDependencies() { super.didChangeDependencies(); task = ModalRoute.of(context)!.settings.arguments as Task?; if (task != null && !isInitialized) { titleController.text = task!.title; descriptionController.text = task!.description; categoryController.text = task!.category; urlController.text = task!.url; if (task!.due != null) { dueDateController.text = getIsoDateString(task!.due!); dueTimeController.text = TimeOfDay.fromDateTime( task!.due!, ).format(context); isDueTimeEnabled = true; } isInitialized = true; alarmRequests.addAll( task!.alarms.map((alarm) { if (alarm is FixedTimeAlarm) { return CreateFixedTimeAlarmRequest.fromAlarm(alarm); } else if (alarm is RelativeTimeAlarm) { return CreateRelativeTimeAlarmRequest.fromAlarm(alarm); } else if (alarm is LocationAlarm) { return CreateLocationAlarmRequest.fromAlarm(alarm); } throw TypeError(); }), ); pageTitle = task!.title; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(pageTitle), actions: [ if (task != null) IconButton( onPressed: () { context.controller().deleteTask(task!); Navigator.of(context).pop(); }, icon: Icon(Icons.delete), ), ], ), body: Form( canPop: !didFormChange, onChanged: () => didFormChange = true, onPopInvokedWithResult: onPopInvoked, autovalidateMode: AutovalidateMode.onUnfocus, key: formKey, child: Padding( padding: EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.05, ), child: SingleChildScrollView( child: Column( spacing: AppTheme.formColumnSpacing, children: [ SizedBox(height: 6), TextFormField( autofocus: true, controller: titleController, decoration: InputDecoration(label: Text('Title')), keyboardType: TextInputType.text, textInputAction: TextInputAction.next, ), TextFormField( controller: descriptionController, decoration: InputDecoration(label: Text('Description')), keyboardType: TextInputType.multiline, textInputAction: TextInputAction.newline, minLines: 3, maxLines: 10, ), TimeSelector( initialDueDateTime: task?.due, nextFocusNode: categoryFocusNode, dueDateController: dueDateController, dueTimeController: dueTimeController, formKey: formKey, ), TextFormField( focusNode: categoryFocusNode, controller: categoryController, decoration: InputDecoration(label: Text('Category')), keyboardType: TextInputType.text, textInputAction: TextInputAction.next, ), TextFormField( controller: urlController, decoration: InputDecoration(label: Text('Url')), keyboardType: TextInputType.text, textInputAction: TextInputAction.next, ), Text( 'Alarms', style: Theme.of(context).textTheme.headlineLarge, ), Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, spacing: AppTheme.formColumnSpacing, children: [ ElevatedButton.icon( onPressed: () => onAddAlarmPressed(AlarmType.timeFixed), label: Text('Fixed Time'), icon: Icon(Icons.alarm_add), ), ElevatedButton.icon( onPressed: () => onAddAlarmPressed(AlarmType.timeRelative), label: Text('Relative Time'), icon: Icon(Icons.timer_outlined), ), ElevatedButton.icon( onPressed: () => onAddAlarmPressed(AlarmType.location), label: Text('Location'), icon: Icon(Icons.location_on), ), for (final (index, alarmRequest) in alarmRequests.indexed) Row( children: [ Expanded( child: AlarmForm( request: alarmRequest, onChanged: (updated) { setState(() { alarmRequests[index] = updated; }); }, ), ), IconButton( onPressed: () => setState(() { alarmRequests.removeAt(index); }), icon: Icon(Icons.close), ), ], ), SizedBox(height: 120), ], ), ], ), ), ), ), floatingActionButton: FloatingActionButton( onPressed: onSavePressed, child: Icon(Icons.save), ), ); } void onSavePressed() { if (formKey.currentState!.validate()) { Navigator.of(context).pop( CreateTaskRequest( title: titleController.text, description: descriptionController.text, start: null, due: DateTime.tryParse( '${dueDateController.text} ${dueTimeController.text}'.trim(), ), isCompleted: false, category: categoryController.text, subtasks: [], url: urlController.text, alarms: alarmRequests.map((e) { if (e is CreateFixedTimeAlarmRequest) { return e.toAlarm(generateId()); } if (e is CreateRelativeTimeAlarmRequest) { return e.toAlarm(generateId()); } if (e is LocationAlarm) { return e.toAlarm(generateId()); } throw TypeError(); }).toList(), ), ); } } void onPopInvoked(bool didPop, Object? result) { if (!didPop) { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Unsaved Changes'), content: Text( 'Are you sure you want to cancel editing? Unsaved changes will be lost.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text('No'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: Text('Yes'), ), ], ), ).then((result) { if (result != null && result && mounted) { Navigator.of(context).pop(); } }); } } void onAddAlarmPressed(AlarmType alarmType) { switch (alarmType) { case AlarmType.location: alarmRequests.add(CreateLocationAlarmRequest()); case AlarmType.timeFixed: alarmRequests.add(CreateFixedTimeAlarmRequest()); case AlarmType.timeRelative: alarmRequests.add(CreateFixedTimeAlarmRequest()); } setState(() {}); } }