From 4ccd64a82310b4064085b7107c1acf9e5b985899 Mon Sep 17 00:00:00 2001 From: Shubham Ingale Date: Sun, 24 Aug 2025 14:35:12 +0530 Subject: [PATCH] add route to getx --- lib/app/modules/home/views/show_tasks.dart | 10 +- .../bindings/taskc_details_binding.dart | 12 + .../controllers/taskc_details_controller.dart | 282 ++++++++++++++++++ .../views/taskc_details_view.dart | 231 ++++++++++++++ lib/app/routes/app_pages.dart | 7 + lib/app/routes/app_routes.dart | 2 + lib/app/v3/db/update.dart | 9 +- lib/app/v3/net/modify.dart | 32 +- 8 files changed, 556 insertions(+), 29 deletions(-) create mode 100644 lib/app/modules/taskc_details/bindings/taskc_details_binding.dart create mode 100644 lib/app/modules/taskc_details/controllers/taskc_details_controller.dart create mode 100644 lib/app/modules/taskc_details/views/taskc_details_view.dart diff --git a/lib/app/modules/home/views/show_tasks.dart b/lib/app/modules/home/views/show_tasks.dart index f0cb1640..7440043c 100644 --- a/lib/app/modules/home/views/show_tasks.dart +++ b/lib/app/modules/home/views/show_tasks.dart @@ -3,7 +3,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; -import 'package:taskwarrior/app/modules/home/views/show_details.dart'; +import 'package:taskwarrior/app/routes/app_pages.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_fonts.dart'; @@ -178,12 +178,8 @@ class TaskViewBuilder extends StatelessWidget { color: tColors.secondaryBackgroundColor, child: InkWell( splashColor: tColors.primaryBackgroundColor, - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TaskDetails(task: task), - ), - ), + onTap: () => + Get.toNamed(Routes.TASKC_DETAILS, arguments: task), child: Container( decoration: BoxDecoration( border: Border.all( diff --git a/lib/app/modules/taskc_details/bindings/taskc_details_binding.dart b/lib/app/modules/taskc_details/bindings/taskc_details_binding.dart new file mode 100644 index 00000000..f0825aef --- /dev/null +++ b/lib/app/modules/taskc_details/bindings/taskc_details_binding.dart @@ -0,0 +1,12 @@ +import 'package:get/get.dart'; + +import '../controllers/taskc_details_controller.dart'; + +class TaskcDetailsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => TaskcDetailsController(), + ); + } +} diff --git a/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart b/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart new file mode 100644 index 00000000..7dd803b2 --- /dev/null +++ b/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart @@ -0,0 +1,282 @@ +// ignore_for_file: deprecated_member_use, use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; +import 'package:taskwarrior/app/utils/constants/utilites.dart'; +import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; +import 'package:taskwarrior/app/utils/language/sentence_manager.dart'; +import 'package:taskwarrior/app/v3/db/task_database.dart'; +import 'package:taskwarrior/app/v3/models/annotation.dart'; +import 'package:taskwarrior/app/v3/models/task.dart'; +import 'package:taskwarrior/app/v3/net/modify.dart'; + +enum UnsavedChangesAction { save, discard, cancel } + +class TaskcDetailsController extends GetxController { + late final TaskForC initialTask; + late TaskDatabase taskDatabase; + + final hasChanges = false.obs; + + late RxString description; + late RxString project; + late RxString status; + late RxString priority; + late RxString due; + late RxString start; + late RxString wait; + late RxList tags; + late RxList depends; + late RxString rtype; + late RxString recur; + late RxList annotations; + + @override + void onInit() { + super.onInit(); + initialTask = Get.arguments as TaskForC; + _initializeState(initialTask); + taskDatabase = TaskDatabase(); + taskDatabase.open(); + } + + void _initializeState(TaskForC task) { + description = task.description.obs; + project = (task.project ?? '-').obs; + status = task.status.obs; + priority = (task.priority ?? '-').obs; + due = formatDate(task.due).obs; + start = formatDate(task.start).obs; + wait = formatDate(task.wait).obs; + tags = (task.tags ?? []).obs; + depends = (task.depends ?? []).obs; + rtype = (task.rtype ?? '-').obs; + recur = (task.recur ?? '-').obs; + annotations = (task.annotations ?? []).obs; + } + + String formatDate(String? dateString) { + if (dateString == null || dateString.isEmpty || dateString == '-') { + return '-'; + } + try { + DateTime parsedDate = DateTime.parse(dateString); + return DateFormat('yyyy-MM-dd HH:mm:ss').format(parsedDate); + } catch (e) { + debugPrint('Error parsing date: $dateString'); + return '-'; + } + } + + void updateField(Rx field, T value) { + if (field.value != value) { + field.value = value; + hasChanges.value = true; + } + } + + void updateListField(RxList field, String value) { + final newList = value.split(',').map((e) => e.trim()).toList(); + if (field.toList().toString() != newList.toString()) { + field.assignAll(newList); + hasChanges.value = true; + } + } + + Future saveTask() async { + final updatedTask = TaskForC( + id: initialTask.id, + description: description.value, + project: project.value == '-' ? null : project.value, + status: status.value, + uuid: initialTask.uuid, + urgency: initialTask.urgency, // Urgency is typically calculated + priority: priority.value == '-' ? null : priority.value, + due: due.value == '-' ? null : due.value, + start: start.value == '-' ? null : start.value, + end: initialTask.end, // 'end' is usually set when completed + entry: initialTask.entry, // 'entry' is static + wait: wait.value == '-' ? null : wait.value, + modified: DateFormat('yyyy-MM-dd HH:mm:ss') + .format(DateTime.now()), // Update modified time + tags: tags.isEmpty ? null : tags.toList(), + depends: depends.isEmpty ? null : depends.toList(), + rtype: rtype.value == '-' ? null : rtype.value, + recur: recur.value == '-' ? null : recur.value, + annotations: annotations.isEmpty ? null : annotations.toList(), + ); + await TaskDatabase().updateTask(updatedTask); + hasChanges.value = false; + await modifyTaskOnTaskwarrior(updatedTask); + } + + Future handleWillPop() async { + if (hasChanges.value) { + final action = await _showUnsavedChangesDialog(); + if (action == UnsavedChangesAction.save) { + await saveTask(); + return true; + } else if (action == UnsavedChangesAction.discard) { + return true; + } else { + return false; + } + } + return true; + } + + Future pickDateTime(RxString field) async { + final BuildContext context = Get.context!; + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: field.value != '-' + ? DateTime.tryParse(field.value) ?? DateTime.now() + : DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2101), + ); + + if (pickedDate != null) { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(field.value != '-' + ? DateTime.tryParse(field.value) ?? DateTime.now() + : DateTime.now()), + ); + + DateTime fullDateTime; + if (pickedTime != null) { + fullDateTime = DateTime(pickedDate.year, pickedDate.month, + pickedDate.day, pickedTime.hour, pickedTime.minute); + } else { + fullDateTime = pickedDate; + } + updateField( + field, DateFormat('yyyy-MM-dd HH:mm:ss').format(fullDateTime)); + } + } + + Future showEditDialog(String label, String initialValue) async { + final BuildContext context = Get.context!; + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; + final TextEditingController textController = + TextEditingController(text: initialValue); + + return await Get.dialog( + Utils.showAlertDialog( + title: Text( + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.edit} $label', + style: TextStyle(color: tColors.primaryTextColor), + ), + content: TextField( + style: TextStyle(color: tColors.primaryTextColor), + controller: textController, + decoration: InputDecoration( + hintText: + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.enterNew} $label', + hintStyle: TextStyle(color: tColors.primaryTextColor), + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + SentenceManager(currentLanguage: AppSettings.selectedLanguage) + .sentences + .cancel, + style: TextStyle(color: tColors.primaryTextColor), + ), + ), + TextButton( + onPressed: () => Get.back(result: textController.text), + child: Text( + SentenceManager(currentLanguage: AppSettings.selectedLanguage) + .sentences + .save, + style: TextStyle(color: tColors.primaryTextColor), + ), + ), + ], + ), + ); + } + + Future showSelectDialog( + String label, String initialValue, List options) async { + final BuildContext context = Get.context!; + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; + + return await Get.dialog( + Utils.showAlertDialog( + title: Text( + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.select} $label', + style: TextStyle(color: tColors.primaryTextColor), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: options.map((option) { + return RadioListTile( + title: Text( + option, + style: TextStyle(color: tColors.primaryTextColor), + ), + value: option, + groupValue: initialValue, + onChanged: (value) => Get.back(result: value), + ); + }).toList(), + ), + ), + ); + } + + Future _showUnsavedChangesDialog() async { + final BuildContext context = Get.context!; + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; + return Get.dialog( + barrierDismissible: false, + Utils.showAlertDialog( + title: Text( + SentenceManager(currentLanguage: AppSettings.selectedLanguage) + .sentences + .unsavedChanges, + style: TextStyle(color: tColors.primaryTextColor), + ), + content: Text( + SentenceManager(currentLanguage: AppSettings.selectedLanguage) + .sentences + .unsavedChangesWarning, + style: TextStyle(color: tColors.primaryTextColor), + ), + actions: [ + TextButton( + onPressed: () => Get.back(result: UnsavedChangesAction.cancel), + child: Text( + SentenceManager(currentLanguage: AppSettings.selectedLanguage) + .sentences + .cancel), + ), + TextButton( + onPressed: () => Get.back(result: UnsavedChangesAction.discard), + child: Text( + SentenceManager(currentLanguage: AppSettings.selectedLanguage) + .sentences + .dontSave), + ), + TextButton( + onPressed: () => Get.back(result: UnsavedChangesAction.save), + child: Text( + SentenceManager(currentLanguage: AppSettings.selectedLanguage) + .sentences + .save), + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/taskc_details/views/taskc_details_view.dart b/lib/app/modules/taskc_details/views/taskc_details_view.dart new file mode 100644 index 00000000..7c65e854 --- /dev/null +++ b/lib/app/modules/taskc_details/views/taskc_details_view.dart @@ -0,0 +1,231 @@ +// ignore_for_file: deprecated_member_use, use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; +import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; +import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; +import 'package:taskwarrior/app/utils/language/sentence_manager.dart'; +import '../controllers/taskc_details_controller.dart'; + +class TaskcDetailsView extends GetView { + const TaskcDetailsView({super.key}); + + @override + Widget build(BuildContext context) { + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; + return WillPopScope( + onWillPop: controller.handleWillPop, + child: Scaffold( + backgroundColor: tColors.primaryBackgroundColor, + appBar: AppBar( + foregroundColor: TaskWarriorColors.lightGrey, + backgroundColor: TaskWarriorColors.kprimaryBackgroundColor, + title: Text( + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.task}: ${controller.initialTask.description}', + style: GoogleFonts.poppins(color: TaskWarriorColors.white), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Obx( + () => ListView( + children: [ + _buildEditableDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageDescription}:', + controller.description.value, + (value) => + controller.updateField(controller.description, value), + ), + _buildEditableDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.project}:', + controller.project.value, + (value) => controller.updateField(controller.project, value), + ), + _buildSelectableDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageStatus}:', + controller.status.value, + ['pending', 'completed'], + (value) => controller.updateField(controller.status, value), + ), + _buildSelectableDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPagePriority}:', + controller.priority.value, + ['H', 'M', 'L', '-'], + (value) => controller.updateField(controller.priority, value), + ), + _buildDatePickerDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.homePageDue}:', + controller.due.value, + () => controller.pickDateTime(controller.due), + ), + _buildDatePickerDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageStart}:', + controller.start.value, + () => controller.pickDateTime(controller.start), + ), + _buildDatePickerDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageWait}:', + controller.wait.value, + () => controller.pickDateTime(controller.wait), + ), + _buildEditableDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageTags}:', + controller.tags.join(', '), + (value) => controller.updateListField(controller.tags, value), + ), + _buildEditableDetail( + context, + 'Depends:', + controller.depends.join(', '), + (value) => + controller.updateListField(controller.depends, value), + ), + _buildEditableDetail( + context, + 'Rtype:', + controller.rtype.value, + (value) => controller.updateField(controller.rtype, value), + ), + _buildEditableDetail( + context, + 'Recur:', + controller.recur.value, + (value) => controller.updateField(controller.recur, value), + ), + _buildDetail( + context, 'UUID:', controller.initialTask.uuid ?? '-'), + _buildDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageUrgency}:', + controller.initialTask.urgency?.toStringAsFixed(2) ?? '-', + ), + _buildDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageEnd}:', + controller.formatDate(controller.initialTask.end), + ), + _buildDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageEntry}:', + controller.formatDate(controller.initialTask.entry), + ), + _buildDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageModified}:', + controller.formatDate(controller.initialTask.modified), + ), + _buildDetail( + context, + '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences}:', + controller.annotations.isNotEmpty + ? controller.annotations + .map((e) => e.description) + .join('\n') + : '-', + ), + ], + ), + ), + ), + floatingActionButton: Obx( + () => controller.hasChanges.value + ? FloatingActionButton( + onPressed: controller.saveTask, + child: const Icon(Icons.save), + ) + : const SizedBox.shrink(), + ), + ), + ); + } + + Widget _buildEditableDetail(BuildContext context, String label, String value, + Function(String) onChanged) { + return InkWell( + onTap: () async { + final result = await controller.showEditDialog(label, value); + if (result != null) { + onChanged(result); + } + }, + child: _buildDetail(context, label, value), + ); + } + + Widget _buildSelectableDetail(BuildContext context, String label, + String value, List options, Function(String) onChanged) { + return InkWell( + onTap: () async { + final result = await controller.showSelectDialog(label, value, options); + if (result != null) { + onChanged(result); + } + }, + child: _buildDetail(context, label, value), + ); + } + + Widget _buildDatePickerDetail( + BuildContext context, String label, String value, VoidCallback onTap) { + return InkWell( + onTap: onTap, + child: _buildDetail(context, label, value), + ); + } + + Widget _buildDetail(BuildContext context, String label, String value) { + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: tColors.secondaryBackgroundColor, + borderRadius: BorderRadius.circular(8.0), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 4.0, + offset: Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: tColors.primaryTextColor, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: TextStyle( + fontSize: 18, + color: tColors.primaryTextColor, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index c866376b..90ace627 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -24,6 +24,8 @@ import '../modules/settings/bindings/settings_binding.dart'; import '../modules/settings/views/settings_view.dart'; import '../modules/splash/bindings/splash_binding.dart'; import '../modules/splash/views/splash_view.dart'; +import '../modules/taskc_details/bindings/taskc_details_binding.dart'; +import '../modules/taskc_details/views/taskc_details_view.dart'; // ignore_for_file: constant_identifier_names @@ -95,5 +97,10 @@ class AppPages { page: () => const LogsView(), binding: LogsBinding(), ), + GetPage( + name: _Paths.TASKC_DETAILS, + page: () => const TaskcDetailsView(), + binding: TaskcDetailsBinding(), + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index b93194a2..04490f3f 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -17,6 +17,7 @@ abstract class Routes { static const PERMISSION = _Paths.PERMISSION; static const MANAGE_TASK_CHAMPION_CREDS = _Paths.MANAGE_TASK_CHAMPION_CREDS; static const LOGS = _Paths.LOGS; + static const TASKC_DETAILS = _Paths.TASKC_DETAILS; } abstract class _Paths { @@ -33,4 +34,5 @@ abstract class _Paths { static const PERMISSION = '/permission'; static const MANAGE_TASK_CHAMPION_CREDS = '/manage-task-champion-creds'; static const LOGS = '/logs'; + static const TASKC_DETAILS = '/taskc-details'; } diff --git a/lib/app/v3/db/update.dart b/lib/app/v3/db/update.dart index c8675632..cc0f1008 100644 --- a/lib/app/v3/db/update.dart +++ b/lib/app/v3/db/update.dart @@ -53,14 +53,7 @@ Future updateTasksInDatabase(List tasks) async { await taskDatabase.updateTask(serverTask); } else if (serverTaskModifiedDate.isBefore(localTaskModifiedDate)) { // local task is newer, update server - await modifyTaskOnTaskwarrior( - localTask.description, - localTask.project!, - localTask.due!, - localTask.priority!, - localTask.status, - localTask.uuid!, - ); + await modifyTaskOnTaskwarrior(localTask); if (localTask.status == 'completed') { completeTask('email', localTask.uuid!); } else if (localTask.status == 'deleted') { diff --git a/lib/app/v3/net/modify.dart b/lib/app/v3/net/modify.dart index 928f531f..1f435963 100644 --- a/lib/app/v3/net/modify.dart +++ b/lib/app/v3/net/modify.dart @@ -1,12 +1,13 @@ import 'dart:convert'; +import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart'; import 'package:flutter/material.dart'; import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; import 'package:taskwarrior/app/v3/db/task_database.dart'; +import 'package:taskwarrior/app/v3/models/task.dart'; -Future modifyTaskOnTaskwarrior(String description, String project, - String due, String priority, String status, String taskuuid) async { +Future modifyTaskOnTaskwarrior(TaskForC tskc) async { var baseUrl = await CredentialsStorage.getApiUrl(); var c = await CredentialsStorage.getClientId(); var e = await CredentialsStorage.getEncryptionSecret(); @@ -22,25 +23,28 @@ Future modifyTaskOnTaskwarrior(String description, String project, "email": "e", "encryptionSecret": e, "UUID": c, - "description": description, - "priority": priority, - "project": project, - "due": due, - "status": status, - "taskuuid": taskuuid, + "description": tskc.description, + "priority": tskc.priority, + "project": tskc.project, + "due": tskc.due, + "status": tskc.status, + "taskuuid": tskc.uuid, }), ); if (response.statusCode != 200) { - ScaffoldMessenger.of(context as BuildContext).showSnackBar(const SnackBar( - content: Text( - "Failed to update task!", - style: TextStyle(color: Colors.red), - ))); + Get.snackbar( + 'Error', + 'Failed to modify task: ${response.reasonPhrase}', + snackPosition: SnackPosition.BOTTOM, + ); } var taskDatabase = TaskDatabase(); await taskDatabase.open(); await taskDatabase.deleteTask( - description: description, due: due, project: project, priority: priority); + description: tskc.description, + due: tskc.due, + project: tskc.project, + priority: tskc.priority); }