diff --git a/lib/api_service.dart b/lib/api_service.dart index 176c04ce..d80dd290 100644 --- a/lib/api_service.dart +++ b/lib/api_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; @@ -19,38 +20,56 @@ class Tasks { final String? end; final String entry; final String? modified; - - Tasks({ - required this.id, - required this.description, - required this.project, - required this.status, - required this.uuid, - required this.urgency, - required this.priority, - required this.due, - required this.end, - required this.entry, - required this.modified, - }); + final List? tags; + + Tasks( + {required this.id, + required this.description, + required this.project, + required this.status, + required this.uuid, + required this.urgency, + required this.priority, + required this.due, + required this.end, + required this.entry, + required this.modified, + required this.tags}); factory Tasks.fromJson(Map json) { return Tasks( - id: json['id'], - description: json['description'], - project: json['project'], - status: json['status'], - uuid: json['uuid'], - urgency: json['urgency'].toDouble(), - priority: json['priority'], - due: json['due'], - end: json['end'], - entry: json['entry'], - modified: json['modified'], - ); + id: json['id'], + description: json['description'], + project: json['project'], + status: json['status'], + uuid: json['uuid'], + urgency: json['urgency'].toDouble(), + priority: json['priority'], + due: json['due'], + end: json['end'], + entry: json['entry'], + modified: json['modified'], + tags: json['tags']); + } + factory Tasks.fromDbJson(Map json) { + debugPrint("FROM: $json"); + return Tasks( + id: json['id'], + description: json['description'], + project: json['project'], + status: json['status'], + uuid: json['uuid'], + urgency: json['urgency'].toDouble(), + priority: json['priority'], + due: json['due'], + end: json['end'], + entry: json['entry'], + modified: json['modified'], + tags: json['tags'].toString().split(' ')); } Map toJson() { + debugPrint("TAGS: $tags"); return { 'id': id, 'description': description, @@ -63,6 +82,24 @@ class Tasks { 'end': end, 'entry': entry, 'modified': modified, + 'tags': tags + }; + } + + Map toDbJson() { + return { + 'id': id, + 'description': description, + 'project': project, + 'status': status, + 'uuid': uuid, + 'urgency': urgency, + 'priority': priority, + 'due': due, + 'end': end, + 'entry': entry, + 'modified': modified, + 'tags': tags != null ? tags?.join(" ") : "" }; } } @@ -100,8 +137,8 @@ Future updateTasksInDatabase(List tasks) async { //add tasks without UUID to the server and delete them from database for (var task in tasksWithoutUUID) { try { - await addTaskAndDeleteFromDatabase( - task.description, task.project!, task.due!, task.priority!); + await addTaskAndDeleteFromDatabase(task.description, task.project!, + task.due!, task.priority!, task.tags != null ? task.tags! : []); } catch (e) { debugPrint('Failed to add task without UUID to server: $e'); } @@ -221,14 +258,15 @@ Future completeTask(String email, String taskUuid) async { } } -Future addTaskAndDeleteFromDatabase( - String description, String project, String due, String priority) async { +Future addTaskAndDeleteFromDatabase(String description, String project, + String due, String priority, List tags) async { String apiUrl = '$baseUrl/add-task'; var c = await CredentialsStorage.getClientId(); var e = await CredentialsStorage.getEncryptionSecret(); + debugPrint("Database Adding Tags $tags $description"); debugPrint(c); debugPrint(e); - await http.post( + var res = await http.post( Uri.parse(apiUrl), headers: { 'Content-Type': 'text/plain', @@ -241,9 +279,10 @@ Future addTaskAndDeleteFromDatabase( 'project': project, 'due': due, 'priority': priority, + 'tags': tags }), ); - + debugPrint('Database res ${res.body}'); var taskDatabase = TaskDatabase(); await taskDatabase.open(); await taskDatabase._database!.delete( @@ -302,9 +341,11 @@ class TaskDatabase { var databasesPath = await getDatabasesPath(); String path = join(databasesPath, 'tasks.db'); - _database = await openDatabase(path, version: 1, + _database = await openDatabase(path, + version: 1, + onOpen: (db) async => await addTagsColumnIfNeeded(db), onCreate: (Database db, version) async { - await db.execute(''' + await db.execute(''' CREATE TABLE Tasks ( uuid TEXT PRIMARY KEY, id INTEGER, @@ -319,7 +360,16 @@ class TaskDatabase { modified TEXT ) '''); - }); + }); + } + + Future addTagsColumnIfNeeded(Database db) async { + try { + await db.rawQuery("SELECT tags FROM Tasks LIMIT 0"); + } catch (e) { + await db.execute("ALTER TABLE Tasks ADD COLUMN tags TEXT"); + debugPrint("Added Column tags"); + } } Future ensureDatabaseIsOpen() async { @@ -332,20 +382,21 @@ class TaskDatabase { await ensureDatabaseIsOpen(); final List> maps = await _database!.query('Tasks'); + debugPrint("Database fetch ${maps.last}"); var a = List.generate(maps.length, (i) { return Tasks( - id: maps[i]['id'], - description: maps[i]['description'], - project: maps[i]['project'], - status: maps[i]['status'], - uuid: maps[i]['uuid'], - urgency: maps[i]['urgency'], - priority: maps[i]['priority'], - due: maps[i]['due'], - end: maps[i]['end'], - entry: maps[i]['entry'], - modified: maps[i]['modified'], - ); + id: maps[i]['id'], + description: maps[i]['description'], + project: maps[i]['project'], + status: maps[i]['status'], + uuid: maps[i]['uuid'], + urgency: maps[i]['urgency'], + priority: maps[i]['priority'], + due: maps[i]['due'], + end: maps[i]['end'], + entry: maps[i]['entry'], + modified: maps[i]['modified'], + tags: maps[i]['tags'] != null ? maps[i]['tags'].split(' ') : []); }); // debugPrint('Tasks from db'); // debugPrint(a.toString()); @@ -374,12 +425,13 @@ class TaskDatabase { Future insertTask(Tasks task) async { await ensureDatabaseIsOpen(); - - await _database!.insert( + debugPrint("Database Insert"); + var dbi = await _database!.insert( 'Tasks', - task.toJson(), + task.toDbJson(), conflictAlgorithm: ConflictAlgorithm.replace, ); + debugPrint("Database Insert ${task.toDbJson()} $dbi"); } Future updateTask(Tasks task) async { @@ -387,7 +439,7 @@ class TaskDatabase { await _database!.update( 'Tasks', - task.toJson(), + task.toDbJson(), where: 'uuid = ?', whereArgs: [task.uuid], ); @@ -403,7 +455,7 @@ class TaskDatabase { ); if (maps.isNotEmpty) { - return Tasks.fromJson(maps.first); + return Tasks.fromDbJson(maps.first); } else { return null; } @@ -470,7 +522,7 @@ class TaskDatabase { ); return List.generate(maps.length, (i) { - return Tasks.fromJson(maps[i]); + return Tasks.fromDbJson(maps[i]); }); } @@ -480,21 +532,21 @@ class TaskDatabase { where: 'project = ?', whereArgs: [project], ); - + debugPrint("DB Stored for $maps"); return List.generate(maps.length, (i) { return Tasks( - uuid: maps[i]['uuid'], - id: maps[i]['id'], - description: maps[i]['description'], - project: maps[i]['project'], - status: maps[i]['status'], - urgency: maps[i]['urgency'], - priority: maps[i]['priority'], - due: maps[i]['due'], - end: maps[i]['end'], - entry: maps[i]['entry'], - modified: maps[i]['modified'], - ); + uuid: maps[i]['uuid'], + id: maps[i]['id'], + description: maps[i]['description'], + project: maps[i]['project'], + status: maps[i]['status'], + urgency: maps[i]['urgency'], + priority: maps[i]['priority'], + due: maps[i]['due'], + end: maps[i]['end'], + entry: maps[i]['entry'], + modified: maps[i]['modified'], + tags: maps[i]['tags'].toString().split(' ')); }); } @@ -517,7 +569,7 @@ class TaskDatabase { whereArgs: ['%$query%', '%$query%'], ); return List.generate(maps.length, (i) { - return Tasks.fromJson(maps[i]); + return Tasks.fromDbJson(maps[i]); }); } diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 53074874..7db773ec 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -32,6 +32,7 @@ import 'package:taskwarrior/app/utils/taskfunctions/projects.dart'; import 'package:taskwarrior/app/utils/taskfunctions/query.dart'; import 'package:taskwarrior/app/utils/taskfunctions/tags.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; +import 'package:textfield_tags/textfield_tags.dart'; import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; class HomeController extends GetxController { @@ -45,11 +46,13 @@ class HomeController extends GetxController { final RxSet selectedTags = {}.obs; final RxList queriedTasks = [].obs; final RxList searchedTasks = [].obs; + final RxList selectedDates = List.filled(4, null).obs; final RxMap pendingTags = {}.obs; final RxMap projects = {}.obs; final RxBool sortHeaderVisible = false.obs; final RxBool searchVisible = false.obs; final TextEditingController searchController = TextEditingController(); + final StringTagController stringTagController = StringTagController(); late RxBool serverCertExists; final Rx selectedLanguage = SupportedLanguage.english.obs; final ScrollController scrollController = ScrollController(); @@ -78,7 +81,7 @@ class HomeController extends GetxController { handleHomeWidgetClicked(); } fetchTasksFromDB(); - everAll([ + everAll([ pendingFilter, waitingFilter, projectFilter, @@ -86,13 +89,12 @@ class HomeController extends GetxController { selectedSort, selectedTags, ], (_) { - if (Platform.isAndroid) { - WidgetController widgetController = - Get.put(WidgetController()); - widgetController.fetchAllData(); + if (Platform.isAndroid) { + WidgetController widgetController = Get.put(WidgetController()); + widgetController.fetchAllData(); - widgetController.update(); - } + widgetController.update(); + } }); } @@ -508,15 +510,12 @@ class HomeController extends GetxController { final projectcontroller = TextEditingController(); var due = Rxn(); RxString dueString = ''.obs; - final priorityList = ['L','X','M','H']; + final priorityList = ['L', 'X', 'M', 'H']; final priorityColors = [ TaskWarriorColors.green, TaskWarriorColors.grey, TaskWarriorColors.yellow, TaskWarriorColors.red, - - - ]; RxString priority = 'X'.obs; @@ -582,10 +581,9 @@ class HomeController extends GetxController { void initLanguageAndDarkMode() { isDarkModeOn.value = AppSettings.isDarkMode; selectedLanguage.value = AppSettings.selectedLanguage; - HomeWidget.saveWidgetData("themeMode", AppSettings.isDarkMode ? "dark" : "light"); - HomeWidget.updateWidget( - androidName: "TaskWarriorWidgetProvider" - ); + HomeWidget.saveWidgetData( + "themeMode", AppSettings.isDarkMode ? "dark" : "light"); + HomeWidget.updateWidget(androidName: "TaskWarriorWidgetProvider"); // print("called and value is${isDarkModeOn.value}"); } @@ -679,6 +677,7 @@ class HomeController extends GetxController { }, ); } + late RxString uuid = "".obs; late RxBool isHomeWidgetTaskTapped = false.obs; @@ -693,7 +692,7 @@ class HomeController extends GetxController { Get.toNamed(Routes.DETAIL_ROUTE, arguments: ["uuid", uuid.value]); }); } - }else if(uri.host == "addclicked"){ + } else if (uri.host == "addclicked") { showAddDialogAfterWidgetClick(); } } @@ -706,15 +705,17 @@ class HomeController extends GetxController { } debugPrint('uuid is $uuid'); Get.toNamed(Routes.DETAIL_ROUTE, arguments: ["uuid", uuid.value]); - }else if(uri.host == "addclicked"){ + } else if (uri.host == "addclicked") { showAddDialogAfterWidgetClick(); } } - }); } + void showAddDialogAfterWidgetClick() { - Widget showDialog = taskchampion.value ? AddTaskToTaskcBottomSheet(homeController: this) : AddTaskBottomSheet(homeController: this); + Widget showDialog = taskchampion.value + ? AddTaskToTaskcBottomSheet(homeController: this) + : AddTaskBottomSheet(homeController: this); Get.dialog(showDialog); } } diff --git a/lib/app/modules/home/views/add_task_bottom_sheet_new.dart b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart new file mode 100644 index 00000000..46258a59 --- /dev/null +++ b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart @@ -0,0 +1,391 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:taskwarrior/api_service.dart'; +import 'package:taskwarrior/app/models/json/task.dart'; +import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; +import 'package:taskwarrior/app/modules/home/controllers/widget.controller.dart'; +import 'package:taskwarrior/app/utils/add_task_dialogue/date_picker_input.dart'; +import 'package:taskwarrior/app/utils/add_task_dialogue/tags_input.dart'; +import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; +import 'package:taskwarrior/app/utils/constants/constants.dart'; +import 'package:taskwarrior/app/utils/language/sentence_manager.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/add_task_dialog_utils.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/tags.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/taskparser.dart'; + +class AddTaskBottomSheet extends StatelessWidget { + final HomeController homeController; + final bool forTaskC; + const AddTaskBottomSheet( + {required this.homeController, super.key, this.forTaskC = false}); + + @override + Widget build(BuildContext context) { + const padding = 12.0; + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Form( + key: homeController.formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(padding), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + Get.back(); + }, + child: const Text("Cancel"), + ), + Text( + SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .addTaskTitle, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + TextButton( + onPressed: () { + if (forTaskC) { + onSaveButtonClickedTaskC(context); + } else { + onSaveButtonClicked(context); + } + }, + child: const Text("Save"), + ), + ], + ), + ), + Flexible( + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(padding), + child: TextFormField( + controller: homeController.namecontroller, + validator: (value) => + value!.isEmpty ? "Description cannot be empty" : null, + decoration: const InputDecoration( + labelText: 'Enter Task Description', + border: OutlineInputBorder(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(padding), + child: buildProjectInput(context)), + Padding( + padding: const EdgeInsets.only( + left: padding, right: padding, top: padding), + child: buildDatePicker(context), + ), + Padding( + padding: const EdgeInsets.all(padding), + child: buildPriority(context), + ), + Padding( + padding: const EdgeInsets.all(padding), + child: buildTagsInput(context), + ), + const Padding(padding: EdgeInsets.all(20)), + ], + )), + ), + ], + ), + ), + ); + } + + Widget buildProjectInput(BuildContext context) => Autocomplete( + optionsBuilder: (textEditingValue) async { + Iterable projects = getProjects(); + debugPrint("projects found $projects"); + return projects.where( + (String project) => project.contains(textEditingValue.text)); + }, + optionsViewBuilder: (context, onAutoCompleteSelect, options) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + child: Container( + width: MediaQuery.of(context).size.width - 12 * 2, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.0), + ), + child: ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: options.length, + separatorBuilder: (context, i) => const Divider(height: 1), + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () { + onAutoCompleteSelect(options.elementAt(index)); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12.0), + child: Text( + options.elementAt(index), + style: const TextStyle(fontSize: 16), + ), + ), + ); + }, + ), + ), + ), + ); + }, + fieldViewBuilder: + (context, textEditingController, focusNode, onFieldSubmitted) => + TextFormField( + controller: textEditingController, + decoration: const InputDecoration( + labelText: 'Project', + border: OutlineInputBorder(), + ), + onChanged: (value) => homeController.projectcontroller.text = value, + focusNode: focusNode, + validator: (value) { + if (value != null && value.contains(" ")) { + return "Can not have Whitespace"; + } + return null; + }, + ), + ); + + Widget buildTagsInput(BuildContext context) => AddTaskTagsInput( + suggestions: tagSet(homeController.storage.data.allData()), + onTagsChanges: (p0) => homeController.tags.value = p0, + ); + + Widget buildDatePicker(BuildContext context) => AddTaskDatePickerInput( + onDateChanges: (List p0) { + homeController.selectedDates.value = p0; + }, + onlyDueDate: forTaskC, + ); + + Widget buildPriority(BuildContext context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx( + () => TextField( + readOnly: true, // Make the field read-only + controller: TextEditingController( + text: getPriorityText(homeController + .priority.value), // Display the selected priority + ), + decoration: InputDecoration( + labelText: 'Priority', + border: const OutlineInputBorder(), + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: GestureDetector( + onTap: () { + debugPrint("Open priority selection."); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; + i < homeController.priorityList.length; + i++) + GestureDetector( + onTap: () { + homeController.priority.value = + homeController.priorityList[i]; + debugPrint(homeController.priority.value); + }, + child: AnimatedContainer( + margin: const EdgeInsets.only(right: 5), + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: homeController.priority.value == + homeController.priorityList[i] + ? AppSettings.isDarkMode + ? TaskWarriorColors + .kLightPrimaryBackgroundColor + : TaskWarriorColors + .kprimaryBackgroundColor + : AppSettings.isDarkMode + ? TaskWarriorColors + .kprimaryBackgroundColor + : TaskWarriorColors + .kLightPrimaryBackgroundColor, + ), + ), + child: Center( + child: Text( + homeController.priorityList[i], + style: GoogleFonts.poppins( + fontWeight: FontWeight.bold, + fontSize: 16, + color: homeController.priorityColors[i], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ) + ], + ); + + Set getProjects() { + Iterable tasks = homeController.storage.data.allData(); + return tasks + .where((task) => task.project != null) + .fold({}, (aggregate, task) => aggregate..add(task.project!)); + } + + void onSaveButtonClickedTaskC(BuildContext context) async { + if (homeController.formKey.currentState!.validate()) { + debugPrint("tags ${homeController.tags}"); + var task = Tasks( + description: homeController.namecontroller.text, + status: 'pending', + priority: homeController.priority.value, + entry: DateTime.now().toIso8601String(), + id: 0, + project: homeController.projectcontroller.text != "" + ? homeController.projectcontroller.text + : null, + uuid: '', + urgency: 0, + due: getDueDate(homeController.selectedDates).toString(), + end: '', + modified: 'r', + tags: homeController.tags); + await homeController.taskdb.insertTask(task); + homeController.namecontroller.text = ''; + homeController.due.value = null; + homeController.priority.value = 'M'; + homeController.projectcontroller.text = ''; + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + 'Task Added Successfully!', + style: TextStyle( + color: AppSettings.isDarkMode + ? TaskWarriorColors.kprimaryTextColor + : TaskWarriorColors.kLightPrimaryTextColor, + ), + ), + backgroundColor: AppSettings.isDarkMode + ? TaskWarriorColors.ksecondaryBackgroundColor + : TaskWarriorColors.kLightSecondaryBackgroundColor, + duration: const Duration(seconds: 2))); + Navigator.of(context).pop(); + } + } + + void onSaveButtonClicked(BuildContext context) async { + // print(homeController.formKey.currentState); + if (homeController.formKey.currentState!.validate()) { + try { + var task = taskParser(homeController.namecontroller.text) + .rebuild((b) => + b..due = getDueDate(homeController.selectedDates)?.toUtc()) + .rebuild((p) => p..priority = homeController.priority.value) + .rebuild((t) => t..project = homeController.projectcontroller.text) + .rebuild((t) => + t..wait = getWaitDate(homeController.selectedDates)?.toUtc()) + .rebuild((t) => + t..until = getUntilDate(homeController.selectedDates)?.toUtc()) + .rebuild((t) => t + ..scheduled = + getSchedDate(homeController.selectedDates)?.toUtc()); + if (homeController.tags.isNotEmpty) { + task = task.rebuild((t) => t..tags.replace(homeController.tags)); + } + Get.find().mergeTask(task); + homeController.namecontroller.text = ''; + homeController.projectcontroller.text = ''; + homeController.dueString.value = ""; + homeController.priority.value = 'X'; + homeController.tagcontroller.text = ''; + homeController.tags.value = []; + homeController.update(); + Get.back(); + if (Platform.isAndroid) { + WidgetController widgetController = Get.put(WidgetController()); + widgetController.fetchAllData(); + widgetController.update(); + } + + homeController.update(); + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + SentenceManager( + currentLanguage: homeController.selectedLanguage.value) + .sentences + .addTaskTaskAddedSuccessfully, + style: TextStyle( + color: AppSettings.isDarkMode + ? TaskWarriorColors.kprimaryTextColor + : TaskWarriorColors.kLightPrimaryTextColor, + ), + ), + backgroundColor: AppSettings.isDarkMode + ? TaskWarriorColors.ksecondaryBackgroundColor + : TaskWarriorColors.kLightSecondaryBackgroundColor, + duration: const Duration(seconds: 2))); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + bool? value; + value = prefs.getBool('sync-OnTaskCreate') ?? false; + // late InheritedStorage storageWidget; + // storageWidget = StorageWidget.of(context); + var storageWidget = Get.find(); + if (value) { + storageWidget.synchronize(context, true); + } + } on FormatException catch (e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + e.message, + style: TextStyle( + color: AppSettings.isDarkMode + ? TaskWarriorColors.kprimaryTextColor + : TaskWarriorColors.kLightPrimaryTextColor, + ), + ), + backgroundColor: AppSettings.isDarkMode + ? TaskWarriorColors.ksecondaryBackgroundColor + : TaskWarriorColors.kLightSecondaryBackgroundColor, + duration: const Duration(seconds: 2))); + } + } + } +} diff --git a/lib/app/modules/home/views/add_task_to_taskc_bottom_sheet.dart b/lib/app/modules/home/views/add_task_to_taskc_bottom_sheet.dart index ae29a077..8ed3b055 100644 --- a/lib/app/modules/home/views/add_task_to_taskc_bottom_sheet.dart +++ b/lib/app/modules/home/views/add_task_to_taskc_bottom_sheet.dart @@ -350,7 +350,8 @@ class AddTaskToTaskcBottomSheet extends StatelessWidget { due: homeController.dueString.value, // dueString.toIso8601String(), end: '', - modified: 'r'); + modified: 'r', + tags: homeController.tags); await homeController.taskdb.insertTask(task); homeController.namecontroller.text = ''; homeController.due.value = null; diff --git a/lib/app/modules/home/views/home_page_floating_action_button.dart b/lib/app/modules/home/views/home_page_floating_action_button.dart index 8a0c32aa..6c99750c 100644 --- a/lib/app/modules/home/views/home_page_floating_action_button.dart +++ b/lib/app/modules/home/views/home_page_floating_action_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:taskwarrior/app/modules/home/views/add_task_bottom_sheet.dart'; -import 'package:taskwarrior/app/modules/home/views/add_task_to_taskc_bottom_sheet.dart'; +import 'package:taskwarrior/app/modules/home/views/add_task_bottom_sheet_new.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; @@ -30,18 +29,33 @@ class HomePageFloatingActionButton extends StatelessWidget { ), ), onPressed: () => (controller.taskchampion.value) - ? (showDialog( + ? (showModalBottomSheet( context: context, - builder: (context) => AddTaskToTaskcBottomSheet( + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(0), + ), + ), + builder: (context) => AddTaskBottomSheet( homeController: controller, + forTaskC: true, ), ).then((value) { if (controller.isSyncNeeded.value && value != "cancel") { controller.isNeededtoSyncOnStart(context); } })) - : (showDialog( + : (showModalBottomSheet( context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(0), + ), + ), builder: (context) => AddTaskBottomSheet( homeController: controller, ), diff --git a/lib/app/utils/add_task_dialogue/date_picker_input.dart b/lib/app/utils/add_task_dialogue/date_picker_input.dart new file mode 100644 index 00000000..53252739 --- /dev/null +++ b/lib/app/utils/add_task_dialogue/date_picker_input.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/add_task_dialog_utils.dart'; + +class AddTaskDatePickerInput extends StatefulWidget { + final Function(List)? onDateChanges; + final bool onlyDueDate; + const AddTaskDatePickerInput( + {super.key, this.onDateChanges, this.onlyDueDate = false}); + + @override + _AddTaskDatePickerInputState createState() => _AddTaskDatePickerInputState(); +} + +class _AddTaskDatePickerInputState extends State { + final List _selectedDates = List.filled(4, null); + final List dateLabels = ['Due', 'Wait', 'Sched', 'Until']; + final List _controllers = + List.generate(4, (index) => TextEditingController()); + final int length = 4; + int currentIndex = 0; + + int getNextIndex() => (currentIndex + 1) % length; + + int getPreviousIndex() => (currentIndex - 1) % length; + + void _showNextItem() { + setState(() { + currentIndex = getNextIndex(); + }); + } + + void _showPreviousItem() { + setState(() { + currentIndex = getPreviousIndex(); + }); + } + + @override + void dispose() { + for (var controller in _controllers) { + controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + bool isNextDateSelected = _selectedDates[getNextIndex()] != null; + bool isPreviousDateSelected = _selectedDates[getPreviousIndex()] != null; + String nextDateText = isNextDateSelected + ? "Change ${dateLabels[getNextIndex()]} Date" + : "Add ${dateLabels[getNextIndex()]} Date"; + + String prevDateText = isPreviousDateSelected + ? "Change ${dateLabels[getPreviousIndex()]} Date" + : "Add ${dateLabels[getPreviousIndex()]} Date"; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Display the current input field + Flexible( + child: buildDatePicker(context, currentIndex), + ), + // Navigation buttons + Visibility( + visible: !widget.onlyDueDate, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + onPressed: _showPreviousItem, + label: Text( + prevDateText, + style: TextStyle( + fontSize: 12, + decoration: isPreviousDateSelected + ? TextDecoration.none + : TextDecoration.underline, + decorationStyle: TextDecorationStyle.wavy, + ), + ), + icon: const Icon( + Icons.arrow_back_ios_rounded, + size: 12, + color: Colors.black, + ), + iconAlignment: IconAlignment.start, + ), + ), + const SizedBox(width: 8), // Space between buttons + Expanded( + child: TextButton.icon( + onPressed: _showNextItem, + label: Text( + nextDateText, + style: TextStyle( + fontSize: 12, + decoration: isNextDateSelected + ? TextDecoration.none + : TextDecoration.underline, + decorationStyle: TextDecorationStyle.wavy, + ), + ), + icon: const Icon( + Icons.arrow_forward_ios_rounded, + size: 12, + color: Colors.black, + ), + iconAlignment: IconAlignment.end, + ), + ), + ], + ), + ) + ], + ); + } + + Widget buildDatePicker(BuildContext context, int forIndex) { + _controllers[forIndex].text = _selectedDates[forIndex] == null + ? '' + : dateToStringForAddTask(_selectedDates[forIndex]!); + + return TextFormField( + controller: _controllers[forIndex], + decoration: InputDecoration( + labelText: '${dateLabels[forIndex]} Date', + hintText: 'Select a ${dateLabels[forIndex]}', + suffixIcon: const Icon(Icons.calendar_today), + border: const OutlineInputBorder(), + ), + validator: _validator, + readOnly: true, + onTap: () async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDates[forIndex] ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime(2101), + ); + final TimeOfDay? time = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (picked == null || time == null) return; + setState(() { + _selectedDates[forIndex] = + picked.add(Duration(hours: time.hour, minutes: time.minute)); + // Update the controller text + _controllers[forIndex].text = + dateToStringForAddTask(_selectedDates[forIndex]!); + }); + if (widget.onDateChanges != null) { + widget.onDateChanges!(_selectedDates); + } + }, + ); + } + + String? _validator(value) { + for (var i = 0; i < length; i++) { + DateTime? dt = _selectedDates[i]; + String? label = dateLabels[i]; + if (dt != null && dt.isBefore(DateTime.now())) { + return "$label date cannot be in the past"; + } + } + return null; + } +} diff --git a/lib/app/utils/add_task_dialogue/tags_input.dart b/lib/app/utils/add_task_dialogue/tags_input.dart new file mode 100644 index 00000000..6664845f --- /dev/null +++ b/lib/app/utils/add_task_dialogue/tags_input.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:textfield_tags/textfield_tags.dart'; + +class AddTaskTagsInput extends StatefulWidget { + final Iterable suggestions; + final Function(List)? onTagsChanges; + const AddTaskTagsInput( + {super.key, + this.suggestions = const Iterable.empty(), + this.onTagsChanges}); + + @override + _AddTaskTagsInputState createState() => _AddTaskTagsInputState(); +} + +class _AddTaskTagsInputState extends State { + late final StringTagController stringTagController; + + @override + void initState() { + super.initState(); + stringTagController = StringTagController(); + } + + @override + void dispose() { + stringTagController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + const paddingX = 12; + stringTagController.addListener(() { + if (widget.onTagsChanges != null) { + widget.onTagsChanges!(stringTagController.getTags!); + } + }); + return Autocomplete( + onSelected: (String value) { + stringTagController.onTagSubmitted(value); + }, + optionsViewBuilder: (context, onAutoCompleteSelect, options) { + return Align( + alignment: Alignment.topLeft, + child: Material( + child: SizedBox( + width: MediaQuery.of(context).size.width - paddingX * 2, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ...options.map((String tag) { + return Container( + margin: const EdgeInsets.only(left: 5), + child: InputChip( + label: Text(tag), + onPressed: () => + onAutoCompleteSelect(tag))); + }) + ], + ))))); + }, + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '') { + return widget.suggestions; + } + return widget.suggestions.where((option) => + option.toLowerCase().contains(textEditingValue.text.toLowerCase())); + }, + fieldViewBuilder: + (context, textEditingController, focusNode, onFieldSubmitted) { + return TextFieldTags( + textEditingController: textEditingController, + focusNode: focusNode, + textfieldTagsController: stringTagController, + textSeparators: const [' ', ','], + validator: (tag) { + Iterable tags = stringTagController.getTags ?? const []; + if (tags.contains(tag)) { + stringTagController.onTagRemoved(tag); + stringTagController.onTagSubmitted(tag); + return "Tag already exists"; + } + for (String tag in tags) { + if (tag.contains(" ")) return "Tag should not contain spaces"; + } + return null; + }, + inputFieldBuilder: (context, inputFieldValues) { + return TextFormField( + controller: inputFieldValues.textEditingController, + focusNode: inputFieldValues.focusNode, + decoration: InputDecoration( + labelText: "Enter tags", + border: const OutlineInputBorder(), + prefixIconConstraints: BoxConstraints( + maxWidth: + (MediaQuery.of(context).size.width - paddingX) * 0.7), + prefixIcon: inputFieldValues.tags.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: SingleChildScrollView( + controller: inputFieldValues.tagScrollController, + scrollDirection: Axis.horizontal, + child: Row( + children: inputFieldValues.tags.map((String tag) { + return Container( + margin: const EdgeInsets.only(left: 5), + child: InputChip( + label: Text(tag), + onDeleted: () { + inputFieldValues.onTagRemoved(tag); + }, + )); + }).toList()), + )) + : null, + ), + onChanged: inputFieldValues.onTagChanged, + onFieldSubmitted: inputFieldValues.onTagSubmitted, + ); + }, + ); + }, + ); + } +} diff --git a/lib/app/utils/taskfunctions/add_task_dialog_utils.dart b/lib/app/utils/taskfunctions/add_task_dialog_utils.dart new file mode 100644 index 00000000..a32bf0a7 --- /dev/null +++ b/lib/app/utils/taskfunctions/add_task_dialog_utils.dart @@ -0,0 +1,34 @@ +import 'package:intl/intl.dart'; + +String dateToStringForAddTask(DateTime dt) { + return 'On ${DateFormat('yyyy-MM-dd').format(dt)} at ${DateFormat('hh:mm:ss').format(dt)}'; +} + +String getPriorityText(String priority) { + switch (priority) { + case 'H': + return 'High'; + case 'M': + return 'Medium'; + case 'L': + return 'Low'; + default: + return 'None'; + } +} + +DateTime? getDueDate(List dates) { + return dates[0]; +} + +DateTime? getWaitDate(List dates) { + return dates[1]; +} + +DateTime? getSchedDate(List dates) { + return dates[2]; +} + +DateTime? getUntilDate(List dates) { + return dates[3]; +} diff --git a/pubspec.yaml b/pubspec.yaml index 19db109c..612e21fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: url_launcher: ^6.1.14 uuid: ^4.2.2 built_collection: ^5.1.1 + textfield_tags: ^3.0.1 dev_dependencies: build_runner: null