import 'package:flutter/material.dart'; import '../model/callback_models/create_task_request.dart'; import '../model/extensions/controller_context.dart'; import '../model/task.dart'; import '../service/task_controller.dart'; import '../service/tools.dart'; import '../service/validators.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; @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; 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: Column( children: [ 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, ), TextFormField( focusNode: dueDateFocusNode, controller: dueDateController, onChanged: maybeEnableDueTime, onFieldSubmitted: (_) { isDueTimeEnabled ? dueDateFocusNode.nextFocus() : categoryFocusNode.requestFocus(); }, decoration: InputDecoration( label: Text('Due Date'), suffix: IconButton( onPressed: () async { final result = await onOpenCalendarPickerPressed(); if (result != null) { final dateString = getIsoDateString(result); dueDateController.text = dateString; maybeEnableDueTime(dateString); formKey.currentState?.validate(); dueTimeFocusNode.requestFocus(); } }, icon: Icon(Icons.calendar_month), ), ), validator: dateTimeValidator, keyboardType: TextInputType.datetime, textInputAction: TextInputAction.next, ), TextFormField( focusNode: dueTimeFocusNode, onFieldSubmitted: (_) => dueTimeFocusNode.nextFocus(), controller: dueTimeController, enabled: isDueTimeEnabled, decoration: InputDecoration( label: Text('Due Time'), suffix: IconButton( onPressed: () async { final result = await onOpenTimePickerPressed(); if (result != null && context.mounted) { dueTimeController.text = result.format(context); categoryFocusNode.requestFocus(); } }, icon: Icon(Icons.schedule), ), ), validator: timeValidator, keyboardType: TextInputType.text, textInputAction: TextInputAction.next, ), 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, ), ], ), ), ), floatingActionButton: FloatingActionButton( onPressed: onSavePressed, child: Icon(Icons.save), ), ); } Future onOpenCalendarPickerPressed() { return showDialog( context: context, builder: (context) => DatePickerDialog( firstDate: DateTime(DateTime.now().year - 100), lastDate: DateTime(DateTime.now().year + 100), initialDate: DateTime.tryParse(dueDateController.text) ?? DateTime.now(), ), ); } Future onOpenTimePickerPressed() { return showDialog( context: context, builder: (context) => TimePickerDialog( initialTime: TimeOfDay.fromDateTime(task?.due ?? DateTime.now()), ), ); } void onSavePressed() { Navigator.of(context).pop( CreateTaskRequest( title: titleController.text, description: descriptionController.text, start: null, due: null, isCompleted: false, category: categoryController.text, subtasks: [], alarms: [], location: null, url: urlController.text, ), ); } 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 && context.mounted) { Navigator.of(context).pop(); } }); } } void maybeEnableDueTime(String value) { if (value.isNotEmpty && dateTimeValidator(value) == null && !isDueTimeEnabled) { setState(() { isDueTimeEnabled = true; }); } else if (isDueTimeEnabled) { setState(() { isDueTimeEnabled = false; }); } } }