diff --git a/lib/api_service.dart b/lib/api_service.dart index 384a5726..62f5ba94 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,73 @@ 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, + 'project': project, + 'status': status, + 'uuid': uuid, + 'urgency': urgency, + 'priority': priority, + 'due': due, + 'end': end, + 'entry': entry, + 'modified': modified, + 'tags': tags + }; + } + + Map toDbJson() { return { 'id': id, 'description': description, @@ -63,9 +99,11 @@ class Tasks { 'end': end, 'entry': entry, 'modified': modified, + 'tags': tags != null ? tags?.join(" ") : "" }; } } + String origin = 'http://localhost:8080'; Future> fetchTasks(String uuid, String encryptionSecret) async { @@ -99,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'); } @@ -222,15 +260,16 @@ 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 { var baseUrl = await CredentialsStorage.getApiUrl(); 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', @@ -243,9 +282,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( @@ -305,9 +345,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, @@ -322,7 +364,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 { @@ -335,20 +386,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()); @@ -377,12 +429,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 { @@ -390,7 +443,7 @@ class TaskDatabase { await _database!.update( 'Tasks', - task.toJson(), + task.toDbJson(), where: 'uuid = ?', whereArgs: [task.uuid], ); @@ -406,7 +459,7 @@ class TaskDatabase { ); if (maps.isNotEmpty) { - return Tasks.fromJson(maps.first); + return Tasks.fromDbJson(maps.first); } else { return null; } @@ -473,7 +526,7 @@ class TaskDatabase { ); return List.generate(maps.length, (i) { - return Tasks.fromJson(maps[i]); + return Tasks.fromDbJson(maps[i]); }); } @@ -483,21 +536,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(' ')); }); } @@ -520,7 +573,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 9bf0beae..181abe96 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -17,8 +17,7 @@ import 'package:taskwarrior/app/models/storage.dart'; import 'package:taskwarrior/app/models/storage/client.dart'; import 'package:taskwarrior/app/models/tag_meta_data.dart'; import 'package:taskwarrior/app/modules/home/controllers/widget.controller.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/modules/splash/controllers/splash_controller.dart'; import 'package:taskwarrior/app/routes/app_pages.dart'; import 'package:taskwarrior/app/services/tag_filter.dart'; @@ -32,6 +31,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:taskwarrior/app/utils/themes/theme_extension.dart'; import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; @@ -46,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(); @@ -323,7 +325,8 @@ class HomeController extends GetxController { Future synchronize(BuildContext context, bool isDialogNeeded) async { try { final connectivityResult = await Connectivity().checkConnectivity(); - TaskwarriorColorTheme tColors = Theme.of(context).extension()!; + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; if (connectivityResult == ConnectivityResult.none) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( @@ -705,9 +708,9 @@ class HomeController extends GetxController { } void showAddDialogAfterWidgetClick() { - Widget showDialog = taskchampion.value - ? AddTaskToTaskcBottomSheet(homeController: this) - : AddTaskBottomSheet(homeController: this); + Widget showDialog = Material( + child: AddTaskBottomSheet( + homeController: this, forTaskC: taskchampion.value)); Get.dialog(showDialog); } } diff --git a/lib/app/modules/home/views/add_task_bottom_sheet.dart b/lib/app/modules/home/views/add_task_bottom_sheet.dart deleted file mode 100644 index 11c79114..00000000 --- a/lib/app/modules/home/views/add_task_bottom_sheet.dart +++ /dev/null @@ -1,477 +0,0 @@ -// ignore_for_file: use_build_context_synchronously -import 'dart:developer'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:intl/intl.dart'; -import 'package:shared_preferences/shared_preferences.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/constants/taskwarrior_colors.dart'; -import 'package:taskwarrior/app/utils/constants/taskwarrior_fonts.dart'; -import 'package:taskwarrior/app/utils/language/sentence_manager.dart'; -import 'package:taskwarrior/app/utils/taskfunctions/taskparser.dart'; -import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; - -class AddTaskBottomSheet extends StatelessWidget { - final HomeController homeController; - const AddTaskBottomSheet({required this.homeController, super.key}); - @override - Widget build(BuildContext context) { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; - return Scaffold( - backgroundColor: Colors.transparent, - body: Center( - child: SingleChildScrollView( - child: AlertDialog( - surfaceTintColor: tColors.dialogBackgroundColor, - shadowColor: tColors.dialogBackgroundColor, - backgroundColor: tColors.dialogBackgroundColor, - title: Center( - child: Text( - SentenceManager( - currentLanguage: homeController.selectedLanguage.value) - .sentences - .addTaskTitle, - style: TextStyle(color: tColors.primaryTextColor), - ), - ), - content: Form( - key: homeController.formKey, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - buildName(tColors), - const SizedBox(height: 12), - buildDueDate(context, tColors), - const SizedBox(height: 8), - buildPriority(tColors), - buildTags(tColors), - ], - ), - ), - ), - actions: [ - buildCancelButton(context, homeController, tColors), - buildAddButton(context), - ], - ), - ), - ), - ); - } - - Widget buildTags(TaskwarriorColorTheme tColors) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx( - () => Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: buildTagChips(), - ), - ), - Row( - children: [ - Expanded( - child: TextFormField( - controller: homeController.tagcontroller, - style: TextStyle(color: tColors.primaryTextColor), - decoration: InputDecoration( - hintText: SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .addTaskAddTags, - hintStyle: TextStyle(color: tColors.primaryTextColor), - ), - onFieldSubmitted: (tag) { - addTag(tag.trim()); - }, - onChanged: (value) { - String trimmedString = value.trim(); - if (value.endsWith(" ") && - trimmedString.split(' ').length == 1) { - addTag(trimmedString); - } - }, - ), - ), - IconButton( - onPressed: () { - addTag(homeController.tagcontroller.text.trim()); - }, - icon: const Icon(Icons.add), - ), - ], - ), - ], - ); - } - - List buildTagChips() { - return homeController.tags.map((tag) { - return InputChip( - label: Text(tag), - onDeleted: () { - removeTag(tag); - }, - ); - }).toList(); - } - - Widget buildName(TaskwarriorColorTheme tColors) => TextFormField( - autofocus: true, - controller: homeController.namecontroller, - style: TextStyle( - color: tColors.primaryTextColor, - ), - decoration: InputDecoration( - hintText: SentenceManager( - currentLanguage: homeController.selectedLanguage.value) - .sentences - .addTaskEnterTask, - hintStyle: TextStyle( - color: tColors.primaryTextColor, - ), - ), - validator: (name) => name != null && name.isEmpty - ? SentenceManager( - currentLanguage: homeController.selectedLanguage.value) - .sentences - .addTaskFieldCannotBeEmpty - : null, - ); - - Widget buildDueDate(BuildContext context, TaskwarriorColorTheme tColors) => - Row( - children: [ - Text( - SentenceManager( - currentLanguage: homeController.selectedLanguage.value) - .sentences - .addTaskDue, - style: GoogleFonts.poppins( - color: tColors.primaryTextColor, - fontWeight: TaskWarriorFonts.bold, - height: 3.3, - ), - ), - Expanded( - child: GestureDetector( - child: Obx( - () => TextFormField( - style: homeController.inThePast.value - ? TextStyle(color: TaskWarriorColors.red) - : TextStyle(color: tColors.primaryTextColor), - readOnly: true, - controller: - TextEditingController(text: homeController.dueString.value), - decoration: InputDecoration( - hintText: SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .addTaskTitle, - hintStyle: homeController.inThePast.value - ? TextStyle(color: TaskWarriorColors.red) - : Theme.of(context).textTheme.bodyLarge, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12.0, vertical: 16.0), - ), - onTap: () async { - var date = await showDatePicker( - builder: (BuildContext context, Widget? child) { - return Theme( - data: Theme.of(context), - child: child!, - ); - }, - fieldHintText: "Month/Date/Year", - context: context, - initialDate: homeController.due.value ?? DateTime.now(), - firstDate: DateTime.now(), - lastDate: DateTime(2037, 12, 31), - ); - if (date != null) { - var time = await showTimePicker( - builder: (BuildContext context, Widget? child) { - return Theme( - data: Theme.of(context).copyWith( - textTheme: const TextTheme(), - colorScheme: Theme.of(context).colorScheme), - child: Obx(() => MediaQuery( - data: MediaQuery.of(context).copyWith( - alwaysUse24HourFormat: - homeController.change24hr.value, - ), - child: child!)), - ); - }, - context: context, - initialTime: TimeOfDay.fromDateTime( - homeController.due.value ?? DateTime.now()), - ); - // print("date$date Time : $time"); - if (time != null) { - var dateTime = date.add( - Duration( - hours: time.hour, - minutes: time.minute, - ), - ); - // print(dateTime); - homeController.due.value = dateTime; - - // print("due value ${homeController.due}"); - homeController.dueString.value = - DateFormat("dd-MM-yyyy HH:mm").format(dateTime); - // print(homeController.dueString.value); - if (dateTime.isBefore(DateTime.now())) { - //Try changing the color. in the settings and Due display. - - homeController.inThePast.value = true; - - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .addTaskTimeInPast, - style: TextStyle(color: tColors.primaryTextColor), - ), - backgroundColor: tColors.secondaryBackgroundColor, - duration: const Duration(seconds: 2))); - } else { - homeController.inThePast.value = false; - } - - // setState(() {}); - } - } - }, - ), - )), - ), - ], - ); - - Widget buildPriority(TaskwarriorColorTheme tColors) => Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "${SentenceManager(currentLanguage: homeController.selectedLanguage.value).sentences.addTaskPriority} :", - style: GoogleFonts.poppins( - fontWeight: TaskWarriorFonts.bold, - color: tColors.primaryTextColor, - ), - textAlign: TextAlign.left, - ), - const SizedBox( - width: 2, - ), - Obx( - () => Row( - children: [ - for (int i = 0; i < homeController.priorityList.length; i++) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 2.5), - child: GestureDetector( - onTap: () { - homeController.priority.value = - homeController.priorityList[i]; - debugPrint(homeController.priority.value); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 100), - height: 30, - width: 37, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: homeController.priority.value == - homeController.priorityList[i] - ? tColors.primaryTextColor! - : tColors.primaryBackgroundColor!, - )), - child: Center( - child: Text( - homeController.priorityList[i], - textAlign: TextAlign.center, - style: GoogleFonts.poppins( - fontWeight: FontWeight.bold, - fontSize: 17, - color: homeController.priorityColors[i]), - ), - ), - ), - ), - ) - ], - ), - ) - ], - ), - ], - ); - - Widget buildCancelButton(BuildContext context, HomeController homeController, - TaskwarriorColorTheme tColors) => - TextButton( - child: Text( - SentenceManager( - currentLanguage: homeController.selectedLanguage.value) - .sentences - .addTaskCancel, - style: TextStyle( - color: tColors.primaryTextColor, - ), - ), - onPressed: () { - Navigator.of(context).pop("cancel"); - homeController.namecontroller.text = ''; - homeController.dueString.value = ""; - homeController.priority.value = 'M'; - homeController.tagcontroller.text = ''; - homeController.tags.value = []; - homeController.update(); - }, - ); - - Widget buildAddButton(BuildContext context) { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; - return TextButton( - child: Text( - SentenceManager(currentLanguage: homeController.selectedLanguage.value) - .sentences - .addTaskAdd, - style: TextStyle( - color: tColors.primaryTextColor, - ), - ), - onPressed: () async { - // print(homeController.formKey.currentState); - if (homeController.due.value != null && - DateTime.now().isAfter(homeController.due.value!)) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - SentenceManager( - currentLanguage: homeController.selectedLanguage.value) - .sentences - .addTaskTimeInPast, - style: TextStyle( - color: tColors.primaryTextColor, - ), - ), - backgroundColor: tColors.secondaryBackgroundColor, - duration: const Duration(seconds: 2))); - return; - } - if (homeController.formKey.currentState!.validate()) { - try { - var task = taskParser(homeController.namecontroller.text) - .rebuild((b) => b..due = homeController.due.value?.toUtc()) - .rebuild((p) => p..priority = homeController.priority.value); - if (homeController.tagcontroller.text != "") { - homeController.tags.add(homeController.tagcontroller.text.trim()); - } - if (homeController.tags.isNotEmpty) { - task = task.rebuild((t) => t..tags.replace(homeController.tags)); - } - Get.find().mergeTask(task); - // print(task); - - // StorageWidget.of(context).mergeTask(task); - homeController.namecontroller.text = ''; - homeController.dueString.value = ""; - homeController.priority.value = 'M'; - homeController.tagcontroller.text = ''; - homeController.tags.value = []; - homeController.due.value = null; - homeController.update(); - // Navigator.of(context).pop(); - 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: tColors.primaryTextColor, - ), - ), - backgroundColor: tColors.secondaryBackgroundColor, - 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: tColors.primaryTextColor, - ), - ), - backgroundColor: tColors.secondaryBackgroundColor, - duration: const Duration(seconds: 2))); - log(e.toString()); - } - } - }, - ); - } - - void addTag(String tag) { - if (tag.isNotEmpty) { - String trimmedString = tag.trim(); - List tags = trimmedString.split(" "); - for (tag in tags) { - if (checkTagIfExists(tag)) { - removeTag(tag); - } - homeController.tags.add(tag); - } - homeController.tagcontroller.text = ''; - } - } - - bool checkTagIfExists(String tag) { - return homeController.tags.contains(tag); - } - - void removeTag(String tag) { - homeController.tags.remove(tag); - } -} 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..1fcc2687 --- /dev/null +++ b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart @@ -0,0 +1,401 @@ +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'; +import 'package:taskwarrior/app/utils/themes/theme_extension.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) { + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; + return Autocomplete( + optionsBuilder: (textEditingValue) async { + Iterable projects = getProjects(); + return projects.where((String project) => + project + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()) && + project != ''); + }, + 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: tColors.primaryBackgroundColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: options.length, + separatorBuilder: (context, i) => + Divider(height: 1, color: tColors.secondaryBackgroundColor), + 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 deleted file mode 100644 index 47071cf5..00000000 --- a/lib/app/modules/home/views/add_task_to_taskc_bottom_sheet.dart +++ /dev/null @@ -1,288 +0,0 @@ -// ignore_for_file: use_build_context_synchronously, file_names -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:intl/intl.dart'; -import 'package:taskwarrior/api_service.dart'; -import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; -import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; -import 'package:taskwarrior/app/utils/constants/taskwarrior_fonts.dart'; -import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; - -class AddTaskToTaskcBottomSheet extends StatelessWidget { - final HomeController homeController; - const AddTaskToTaskcBottomSheet({super.key, required this.homeController}); - - @override - Widget build(BuildContext context) { - const title = 'Add Task'; - TaskwarriorColorTheme tColors = Theme.of(context).extension()!; - return Scaffold( - backgroundColor: Colors.transparent, - body: Center( - child: SingleChildScrollView( - child: AlertDialog( - surfaceTintColor: tColors.dialogBackgroundColor, - shadowColor: tColors.dialogBackgroundColor, - backgroundColor: tColors.dialogBackgroundColor, - title: Center( - child: Text( - title, - style: TextStyle( - color: tColors.primaryTextColor, - ), - ), - ), - content: Form( - key: homeController.formKey, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - buildName(context, tColors), - const SizedBox(height: 8), - buildProject(context, tColors), - const SizedBox(height: 12), - buildDueDate(context, tColors), - const SizedBox(height: 8), - buildPriority(context, tColors), - ], - ), - ), - ), - actions: [ - buildCancelButton(context, tColors), - buildAddButton(context, tColors), - ], - ), - ), - ), - ); - } - - Widget buildName(BuildContext context,TaskwarriorColorTheme tColors) => TextFormField( - autofocus: true, - controller: homeController.namecontroller, - style: TextStyle( - color: tColors.primaryTextColor, - ), - decoration: InputDecoration( - hintText: 'Enter Task', - hintStyle: TextStyle( - color: tColors.primaryTextColor, - ), - ), - validator: (name) => name != null && name.isEmpty - ? 'You cannot leave this field empty!' - : null, - ); - - Widget buildProject(BuildContext context, TaskwarriorColorTheme tColors) => TextFormField( - autofocus: true, - controller: homeController.projectcontroller, - style: TextStyle( - color: tColors.primaryTextColor, - ), - decoration: InputDecoration( - hintText: 'Enter Project', - hintStyle: TextStyle( - color: tColors.primaryTextColor, - ), - ), - ); - - Widget buildDueDate(BuildContext context, TaskwarriorColorTheme tColors) => Row( - children: [ - Text( - "Due : ", - style: GoogleFonts.poppins( - color: tColors.primaryTextColor, - fontWeight: TaskWarriorFonts.bold, - height: 3.3, - ), - ), - Expanded( - child: GestureDetector( - child: TextFormField( - style: homeController.inThePast.value - ? TextStyle(color: TaskWarriorColors.red) - : TextStyle( - color: tColors.primaryTextColor, - ), - readOnly: true, - controller: TextEditingController( - text: (homeController.due.value != null) - ? homeController.dueString.value - : null, - ), - decoration: InputDecoration( - hintText: 'Select due date', - hintStyle: homeController.inThePast.value - ? TextStyle(color: TaskWarriorColors.red) - : TextStyle( - color: tColors.primaryTextColor, - ), - ), - onTap: () async { - var date = await showDatePicker( - builder: (BuildContext context, Widget? child) { - return Theme( - data: Theme.of(context), - child: child!, - ); - }, - fieldHintText: "Month/Date/Year", - context: context, - initialDate: homeController.due.value ?? DateTime.now(), - firstDate: DateTime.now(), - lastDate: DateTime(2037, 12, 31), - ); - if (date != null) { - var time = await showTimePicker( - builder: (BuildContext context, Widget? child) { - return Theme( - data: Theme.of(context).copyWith( - textTheme: const TextTheme(), - colorScheme: Theme.of(context).colorScheme - ), - child: MediaQuery( - data: MediaQuery.of(context).copyWith( - alwaysUse24HourFormat: - homeController.change24hr.value, - ), - child: child!), - ); - }, - context: context, - initialTime: TimeOfDay.fromDateTime( - homeController.due.value ?? DateTime.now()), - ); - if (time != null) { - var dateTime = date.add( - Duration( - hours: time.hour, - minutes: time.minute, - ), - ); - homeController.due.value = dateTime.toUtc(); - homeController.dueString.value = - DateFormat("yyyy-MM-dd").format(dateTime); - if (dateTime.isBefore(DateTime.now())) { - homeController.inThePast.value = true; - - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - "The selected time is in the past.", - style: TextStyle( - color: tColors.primaryTextColor, - ), - ), - backgroundColor: tColors.secondaryBackgroundColor, - duration: const Duration(seconds: 2))); - } else { - homeController.inThePast.value = false; - } - } - } - }, - ), - ), - ), - ], - ); - - Widget buildPriority(BuildContext context, TaskwarriorColorTheme tColors) => Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'Priority : ', - style: GoogleFonts.poppins( - fontWeight: TaskWarriorFonts.bold, - color: tColors.primaryTextColor, - ), - textAlign: TextAlign.left, - ), - DropdownButton( - dropdownColor: tColors.dialogBackgroundColor, - value: homeController.priority.value, - elevation: 16, - style: GoogleFonts.poppins( - color: tColors.primaryTextColor, - ), - underline: Container( - height: 1.5, - color: tColors.dialogBackgroundColor, - ), - onChanged: (String? newValue) { - homeController.priority.value = newValue!; - }, - items: ['H', 'M', 'L', 'None'] - .map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(' $value'), - ); - }).toList(), - ) - ], - ), - ], - ); - - Widget buildCancelButton(BuildContext context, TaskwarriorColorTheme tColors) => TextButton( - child: Text( - 'Cancel', - style: TextStyle( - color: tColors.primaryTextColor, - ), - ), - onPressed: () => Navigator.of(context).pop("cancel"), - ); - - Widget buildAddButton(BuildContext context, TaskwarriorColorTheme tColors) { - return TextButton( - child: Text( - "Add", - style: TextStyle( - color: tColors.primaryTextColor, - ), - ), - onPressed: () async { - if (homeController.formKey.currentState!.validate()) { - var task = Tasks( - description: homeController.namecontroller.text, - status: 'pending', - priority: homeController.priority.value, - entry: DateTime.now().toIso8601String(), - id: 0, - project: homeController.projectcontroller.text, - uuid: '', - urgency: 0, - due: homeController.dueString.value, - // dueString.toIso8601String(), - end: '', - modified: 'r'); - 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: tColors.primaryTextColor, - ), - ), - backgroundColor: tColors.primaryBackgroundColor, - duration: const Duration(seconds: 2))); - Navigator.of(context).pop(); - } - }, - ); - } -} 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 bc76d36f..44764117 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,6 +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/themes/theme_extension.dart'; import '../controllers/home_controller.dart'; @@ -10,7 +10,8 @@ class HomePageFloatingActionButton extends StatelessWidget { @override Widget build(BuildContext context) { - TaskwarriorColorTheme tColors = Theme.of(context).extension()!; + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; return FloatingActionButton( key: controller.addKey, heroTag: "btn3", @@ -23,18 +24,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..5a94a1e9 --- /dev/null +++ b/lib/app/utils/add_task_dialogue/date_picker_input.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/add_task_dialog_utils.dart'; +import 'package:taskwarrior/app/utils/themes/theme_extension.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) { + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; + 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: Icon( + Icons.arrow_back_ios_rounded, + size: 12, + color: tColors.primaryTextColor, + ), + 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: Icon( + Icons.arrow_forward_ios_rounded, + size: 12, + color: tColors.primaryTextColor, + ), + 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 0ba71a2f..69486e78 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 path_provider: ^2.1.5 dev_dependencies: diff --git a/test/api_service_test.dart b/test/api_service_test.dart index 02ac4508..ae9973f6 100644 --- a/test/api_service_test.dart +++ b/test/api_service_test.dart @@ -58,18 +58,18 @@ void main() { test('toJson converts Tasks object to JSON', () { final task = Tasks( - id: 1, - description: 'Task 1', - project: 'Project 1', - status: 'pending', - uuid: '123', - urgency: 5.0, - priority: 'H', - due: '2024-12-31', - end: null, - entry: '2024-01-01', - modified: '2024-11-01', - ); + id: 1, + description: 'Task 1', + project: 'Project 1', + status: 'pending', + uuid: '123', + urgency: 5.0, + priority: 'H', + due: '2024-12-31', + end: null, + entry: '2024-01-01', + modified: '2024-11-01', + tags: ['t1']); final json = task.toJson(); @@ -118,18 +118,18 @@ void main() { test('insertTask adds a task to the database', () async { final task = Tasks( - id: 1, - description: 'Task 1', - project: 'Project 1', - status: 'pending', - uuid: '123', - urgency: 5.0, - priority: 'H', - due: '2024-12-31', - end: null, - entry: '2024-01-01', - modified: '2024-11-01', - ); + id: 1, + description: 'Task 1', + project: 'Project 1', + status: 'pending', + uuid: '123', + urgency: 5.0, + priority: 'H', + due: '2024-12-31', + end: null, + entry: '2024-01-01', + modified: '2024-11-01', + tags: ['t1']); await taskDatabase.insertTask(task); @@ -141,18 +141,18 @@ void main() { test('deleteAllTasksInDB removes all tasks', () async { final task = Tasks( - id: 1, - description: 'Task 1', - project: 'Project 1', - status: 'pending', - uuid: '123', - urgency: 5.0, - priority: 'H', - due: '2024-12-31', - end: null, - entry: '2024-01-01', - modified: '2024-11-01', - ); + id: 1, + description: 'Task 1', + project: 'Project 1', + status: 'pending', + uuid: '123', + urgency: 5.0, + priority: 'H', + due: '2024-12-31', + end: null, + entry: '2024-01-01', + modified: '2024-11-01', + tags: ['t1']); await taskDatabase.insertTask(task); await taskDatabase.deleteAllTasksInDB();