diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 181abe96..ae7602e5 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -23,6 +23,7 @@ import 'package:taskwarrior/app/routes/app_pages.dart'; import 'package:taskwarrior/app/services/tag_filter.dart'; import 'package:taskwarrior/app/tour/filter_drawer_tour.dart'; import 'package:taskwarrior/app/tour/home_page_tour.dart'; +import 'package:taskwarrior/app/tour/task_swipe_tour.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; import 'package:taskwarrior/app/utils/language/supported_language.dart'; import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; @@ -673,6 +674,34 @@ class HomeController extends GetxController { ); } + final taskItemKey = GlobalKey(); + + void initTaskSwipeTutorial() { + tutorialCoachMark = TutorialCoachMark( + targets: addTaskSwipeTutorialTargets(taskItemKey: taskItemKey), + colorShadow: TaskWarriorColors.black, + paddingFocus: 10, + opacityShadow: 1.00, + hideSkip: true, + onFinish: () { + SaveTourStatus.saveTaskSwipeTutorialStatus(true); + }, + ); + } + + void showTaskSwipeTutorial(BuildContext context) { + SaveTourStatus.getTaskSwipeTutorialStatus().then((value) { + print("value is $value"); + print("tasks is ${tasks.isNotEmpty}"); + if (value == false) { + initTaskSwipeTutorial(); + tutorialCoachMark.show(context: context); + } else { + debugPrint('User has already seen the task swipe tutorial'); + } + }); + } + late RxString uuid = "".obs; late RxBool isHomeWidgetTaskTapped = false.obs; diff --git a/lib/app/modules/home/views/tasks_builder.dart b/lib/app/modules/home/views/tasks_builder.dart index 5b165118..39aef14e 100644 --- a/lib/app/modules/home/views/tasks_builder.dart +++ b/lib/app/modules/home/views/tasks_builder.dart @@ -70,9 +70,7 @@ class TasksBuilder extends StatelessWidget { action: SnackBarAction( label: 'Undo', onPressed: () { - undoChanges( - context, id, 'pending'); - + undoChanges(context, id, 'pending'); }, ), )); @@ -170,9 +168,13 @@ class TasksBuilder extends StatelessWidget { primary: false, itemBuilder: (context, index) { var task = taskData[index]; + final itemKey = index == 0 + ? storageWidget.taskItemKey + : ValueKey(task.uuid); + return pendingFilter ? Slidable( - key: ValueKey(task.uuid), + key: itemKey, startActionPane: ActionPane( motion: const BehindMotion(), children: [ diff --git a/lib/app/tour/task_swipe_tour.dart b/lib/app/tour/task_swipe_tour.dart new file mode 100644 index 00000000..0cd16a86 --- /dev/null +++ b/lib/app/tour/task_swipe_tour.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +List addTaskSwipeTutorialTargets({ + required GlobalKey taskItemKey, +}) { + List targets = []; + + targets.add( + TargetFocus( + identify: "taskSwipeTutorial", + keyTarget: taskItemKey, + alignSkip: Alignment.bottomRight, + radius: 10, + shape: ShapeLightFocus.RRect, + contents: [ + TargetContent( + align: ContentAlign.bottom, + builder: (context, controller) { + return Container( + alignment: Alignment.center, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Task Swipe Actions", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 22.0, + ), + ), + const SizedBox(height: 8), + Text( + "This is how you manage your tasks quickly : ", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + color: Colors.white, + fontStyle: FontStyle.italic, + fontSize: 16.0, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.arrow_right_alt, + color: Colors.green, size: 28), + const SizedBox(width: 8), + Flexible( + child: Text( + "Swipe RIGHT to COMPLETE", + textAlign: TextAlign.left, + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.arrow_right_alt, + textDirection: TextDirection.rtl, + color: Colors.red, + size: 28), + const SizedBox(width: 8), + Flexible( + child: Text( + "Swipe LEFT to DELETE", + textAlign: TextAlign.left, + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + + return targets; +} diff --git a/lib/app/utils/app_settings/save_tour_status.dart b/lib/app/utils/app_settings/save_tour_status.dart index 59306bc6..11b23689 100644 --- a/lib/app/utils/app_settings/save_tour_status.dart +++ b/lib/app/utils/app_settings/save_tour_status.dart @@ -54,4 +54,12 @@ class SaveTourStatus { static Future getManageTaskServerTourStatus() async { return _preferences?.getBool('manage_task_server_tour') ?? false; } + + static Future saveTaskSwipeTutorialStatus(bool status) async { + await _preferences?.setBool('task_swipe_tutorial_completed', status); + } + + static Future getTaskSwipeTutorialStatus() async { + return _preferences?.getBool('task_swipe_tutorial_completed') ?? false; + } } diff --git a/pubspec.yaml b/pubspec.yaml index 69486e78..6ab9fbf1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: package_info_plus: ^8.3.0 pem: ^2.0.1 permission_handler: ^11.4.0 - shared_preferences: ^2.2.2 + shared_preferences: ^2.5.2 shared_preferences_web: ^2.0.3 sizer: ^3.0.5 sqflite: ^2.3.3+1 @@ -61,7 +61,7 @@ dependencies: timezone: ^0.10.0 tuple: ^2.0.0 tutorial_coach_mark: ^1.2.11 - url_launcher: ^6.1.14 + url_launcher: ^6.3.1 uuid: ^4.2.2 built_collection: ^5.1.1 textfield_tags: ^3.0.1 diff --git a/test/tour/task_swipe_tour_test.dart b/test/tour/task_swipe_tour_test.dart new file mode 100644 index 00000000..6d2a75ec --- /dev/null +++ b/test/tour/task_swipe_tour_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:taskwarrior/app/tour/task_swipe_tour.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +class MockTutorialCoachMarkController extends Mock + implements TutorialCoachMarkController {} + +void main() { + group('Task Swipe Tour', () { + late GlobalKey taskItemKey; + final controller = MockTutorialCoachMarkController(); + + setUp(() { + taskItemKey = GlobalKey(); + }); + + test('should return a list of TargetFocus with correct properties', () { + final targets = addTaskSwipeTutorialTargets( + taskItemKey: taskItemKey, + ); + + expect(targets.length, 1); + + expect(targets[0].keyTarget, taskItemKey); + expect(targets[0].identify, "taskSwipeTutorial"); + expect(targets[0].alignSkip, Alignment.bottomRight); + expect(targets[0].shape, ShapeLightFocus.RRect); + expect(targets[0].radius, 10); + }); + + testWidgets('should render correct text for task swipe TargetContent', + (WidgetTester tester) async { + final targets = addTaskSwipeTutorialTargets( + taskItemKey: taskItemKey, + ); + + final content = targets[0].contents!.first; + + await tester.pumpWidget(MaterialApp( + home: Builder( + builder: (context) => content.builder!(context, controller), + ), + )); + + expect(find.text("Task Swipe Actions"), findsOneWidget); + expect(find.text("This is how you manage your tasks quickly : "), + findsOneWidget); + expect(find.text("Swipe RIGHT to COMPLETE"), findsOneWidget); + expect(find.text("Swipe LEFT to DELETE"), findsOneWidget); + }); + }); +}