From e81d566892b7349b35091baa6787a0ed3f0df854 Mon Sep 17 00:00:00 2001 From: ibnubatutah Date: Tue, 11 Jun 2024 21:27:26 +0700 Subject: [PATCH] MEnambahkan fitur tutorial --- lib/components/recipe/recipe_full_item.dart | 2 +- lib/data/remote/models/recipe_model.dart | 4 + lib/data/remote/models/recipe_model.g.dart | 6 +- lib/domain/entities/recipe.dart | 4 + .../home/components/tutorial_home_items.dart | 87 +++++++++++++++++ lib/presentation/home/view/home_view.dart | 20 ++-- .../home/view_model/home_view_model.dart | 58 ++++++++++++ .../components/ingredient_list_widget.dart | 23 ++++- .../components/ingredients_item.dart | 9 +- .../components/utensils_list_widget.dart | 11 ++- .../view/recipe_detail_view.dart | 12 +-- .../view_model/recipe_detail_view_model.dart | 7 ++ .../tutorial_detection_idle_items.dart | 52 ++++++++++ .../view/recipe_detection_view.dart | 23 +++-- .../recipe_detection_view_model.dart | 58 +++++++++++- .../recipe_detection_result_view_model.dart | 6 +- .../components/tutorial_utensils_items.dart | 94 +++++++++++++++++++ .../utensils/view/utensil_view.dart | 33 +++++-- .../view_model/utensil_view_model.dart | 55 +++++++++++ .../arguments/argument_constants.dart | 2 + .../constants/session_constants.dart | 8 +- pubspec.lock | 20 ++-- pubspec.yaml | 3 +- 23 files changed, 543 insertions(+), 54 deletions(-) create mode 100644 lib/presentation/home/components/tutorial_home_items.dart create mode 100644 lib/presentation/recipe_detection/components/tutorial_detection_idle_items.dart create mode 100644 lib/presentation/utensils/components/tutorial_utensils_items.dart diff --git a/lib/components/recipe/recipe_full_item.dart b/lib/components/recipe/recipe_full_item.dart index a7e79b0..54c2d37 100644 --- a/lib/components/recipe/recipe_full_item.dart +++ b/lib/components/recipe/recipe_full_item.dart @@ -67,7 +67,7 @@ class RecipeFullItem extends StatelessWidget { const Icon(Icons.timer, size: 16), const SizedBox(width: 4), Text( - '${recipe.cookTime ?? 0} menit', + '${recipe.prepTime ?? 0} menit', style: TTCommonsTextStyles.textSm.textRegular(), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/data/remote/models/recipe_model.dart b/lib/data/remote/models/recipe_model.dart index 135c819..234133b 100644 --- a/lib/data/remote/models/recipe_model.dart +++ b/lib/data/remote/models/recipe_model.dart @@ -12,6 +12,8 @@ class RecipeModel { String? title; String? description; String? image; + String? difficulity; + double? confidence; List? ingredients; List? instructions; int? prepTime; @@ -49,6 +51,8 @@ class RecipeModel { cookTime: cookTime, servings: servings, utensils: utensils, + difficulity: difficulity, + confidence: confidence ); } } diff --git a/lib/data/remote/models/recipe_model.g.dart b/lib/data/remote/models/recipe_model.g.dart index b863f0c..1f6b84f 100644 --- a/lib/data/remote/models/recipe_model.g.dart +++ b/lib/data/remote/models/recipe_model.g.dart @@ -23,7 +23,9 @@ RecipeModel _$RecipeModelFromJson(Map json) => RecipeModel( utensils: (json['utensils'] as List?) ?.map((e) => e as String) .toList(), - ); + ) + ..difficulity = json['difficulity'] as String? + ..confidence = (json['confidence'] as num?)?.toDouble(); Map _$RecipeModelToJson(RecipeModel instance) => { @@ -31,6 +33,8 @@ Map _$RecipeModelToJson(RecipeModel instance) => 'title': instance.title, 'description': instance.description, 'image': instance.image, + 'difficulity': instance.difficulity, + 'confidence': instance.confidence, 'ingredients': instance.ingredients, 'instructions': instance.instructions, 'prepTime': instance.prepTime, diff --git a/lib/domain/entities/recipe.dart b/lib/domain/entities/recipe.dart index 423161e..c376784 100644 --- a/lib/domain/entities/recipe.dart +++ b/lib/domain/entities/recipe.dart @@ -8,6 +8,8 @@ class Recipe { String? title; String? description; String? image; + String? difficulity; + double? confidence; List? ingredients; List? instructions; int? prepTime; @@ -26,5 +28,7 @@ class Recipe { this.cookTime, this.servings, this.utensils, + this.difficulity, + this.confidence }); } diff --git a/lib/presentation/home/components/tutorial_home_items.dart b/lib/presentation/home/components/tutorial_home_items.dart new file mode 100644 index 0000000..a5d2123 --- /dev/null +++ b/lib/presentation/home/components/tutorial_home_items.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +List createTargets({ + required GlobalKey keyBottomNavigation1, + required GlobalKey keyBottomNavigation2, + required GlobalKey keyBottomNavigation3, +}) { + List targets = []; + targets.add( + TargetFocus( + identify: "keyBottomNavigation1", + keyTarget: keyBottomNavigation1, + color: Colors.black38, +alignSkip: Alignment.topRight, + contents: [ + TargetContent( + align: ContentAlign.top, + builder: (context, controller) { + return const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Deteksi Resep", + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold + ), + ), + SizedBox(height: 8,), + Text( + "Tekan tombol berikut untuk mengambil foto bahan makanan dari kamera ataupun galeri", + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ], + ); + }, + ), + ], + ), + ); + + targets.add( + TargetFocus( + identify: "keyBottomNavigation3", + keyTarget: keyBottomNavigation3, + color: Colors.black38, + alignSkip: Alignment.topLeft, + contents: [ + TargetContent( + align: ContentAlign.bottom, + builder: (context, controller) { + return const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Pengaturan Alat Memasak Yang Dimiliki", + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold + ), + ), + SizedBox(height: 8,), + Text( + "Tekan tombol berikut untuk mengatur dan menambahkan alat-alat memasak yang kamu miliki di dapurmu", + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ], + ); + }, + ), + ], + ), + ); + + return targets; +} \ No newline at end of file diff --git a/lib/presentation/home/view/home_view.dart b/lib/presentation/home/view/home_view.dart index a59ce60..41dcac4 100644 --- a/lib/presentation/home/view/home_view.dart +++ b/lib/presentation/home/view/home_view.dart @@ -18,6 +18,7 @@ class HomeView extends BaseView { @override Widget? floatingActionButton() { return FloatingActionButton( + key: controller.cameraKey, onPressed: () { controller.navigateToRecipeDetection(); }, @@ -33,6 +34,11 @@ class HomeView extends BaseView { @override Widget body(BuildContext context) { + controller.getPageContext(context); + return _buildBody(); + } + + Widget _buildBody(){ return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -43,7 +49,7 @@ class HomeView extends BaseView { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ - SizedBox( + const SizedBox( height: 16, ), Row( @@ -51,11 +57,12 @@ class HomeView extends BaseView { Text( 'Selamat Datang', style: TTCommonsTextStyles.textLg.textRegular().copyWith( - color: AppColors.heroWhite, - ), + color: AppColors.heroWhite, + ), ), const Spacer(), IconButton( + key: controller.settingKey, onPressed: controller.navigateToUtensilPage, icon: const Icon( Icons.settings, @@ -64,6 +71,7 @@ class HomeView extends BaseView { ], ), SearchTextField( + key: controller.searchKey, controller: controller.searchController, hintText: 'Cari Resep..', inputType: TextInputType.text, @@ -89,8 +97,8 @@ class HomeView extends BaseView { ], ), ), - RecipeRecommendationWidget(), - SizedBox(height: 64,) + const RecipeRecommendationWidget(), + const SizedBox(height: 64,) ], ), ); @@ -128,7 +136,7 @@ class HomeView extends BaseView { const SizedBox( height: 8, ), - Text('Data 1'), + const Text('Data 1'), ], ), ); diff --git a/lib/presentation/home/view_model/home_view_model.dart b/lib/presentation/home/view_model/home_view_model.dart index eb7ba8b..e58be75 100644 --- a/lib/presentation/home/view_model/home_view_model.dart +++ b/lib/presentation/home/view_model/home_view_model.dart @@ -1,17 +1,30 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:snap_and_cook_mobile/domain/use_case/general/recipe_use_case.dart'; +import 'package:snap_and_cook_mobile/resources/constants/session_constants.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; import '../../../domain/entities/recipe.dart'; import '../../../resources/arguments/argument_constants.dart'; import '../../../routes/routes/main_route.dart'; +import '../../../utils/session/session.dart'; import '../../base/base_view_model.dart'; +import '../components/tutorial_home_items.dart'; class HomeViewModel extends BaseViewModel { TextEditingController searchController = TextEditingController(); final RecipeUseCase _recipeUseCase = RecipeUseCase(); final RxList recipes = RxList(); + late TutorialCoachMark tutorialCoachMark; + + BuildContext? pageContext; + + final cameraKey = GlobalKey(); + final searchKey = GlobalKey(); + final settingKey = GlobalKey(); void onSearchSubmitted(String value) { if (value.isEmpty) { @@ -25,6 +38,51 @@ class HomeViewModel extends BaseViewModel { void onInit() { super.onInit(); _fetchAllRecipes(); + createTutorial(); + showTutorial(); + } + + + void getPageContext(BuildContext context){ + pageContext = context; + } + + void createTutorial() { + tutorialCoachMark = TutorialCoachMark( + targets: createTargets( + keyBottomNavigation1: cameraKey, + keyBottomNavigation2: searchKey, + keyBottomNavigation3: settingKey), + colorShadow: Colors.black38, + textSkip: "SKIP", + paddingFocus: 10, + opacityShadow: 0.5, + imageFilter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), + onFinish: () { + print("finish"); + Session.set(SessionConstants.isAlreadyOnBoardingHome, "yes"); + }, + onClickTarget: (target) { + }, + onClickTargetWithTapPosition: (target, tapDetails) {}, + onClickOverlay: (target) {}, + onSkip: () { + Session.set(SessionConstants.isAlreadyOnBoardingHome, "yes"); + return true; + }, + ); + } + + Future showTutorial() async { + Future.delayed(const Duration(seconds: 1)); + String? isOnBoarded = await Session.get(SessionConstants.isAlreadyOnBoardingHome); + if (isOnBoarded != null) { + return; + } + + if (pageContext?.mounted ?? false){ + tutorialCoachMark.show(context: pageContext!); + } } Future _fetchAllRecipes() async { diff --git a/lib/presentation/recipe_detail/components/ingredient_list_widget.dart b/lib/presentation/recipe_detail/components/ingredient_list_widget.dart index f2bd78d..6379b67 100644 --- a/lib/presentation/recipe_detail/components/ingredient_list_widget.dart +++ b/lib/presentation/recipe_detail/components/ingredient_list_widget.dart @@ -4,18 +4,37 @@ import 'package:snap_and_cook_mobile/presentation/recipe_detail/components/ingre class IngredientListWidget extends StatelessWidget { final List ingredients; + final List selectedIngredient; - const IngredientListWidget({super.key, required this.ingredients}); + const IngredientListWidget( + {super.key, required this.ingredients, required this.selectedIngredient}); @override Widget build(BuildContext context) { return ListView.builder( itemBuilder: (context, index) { - return IngredientItem(ingredient: ingredients[index]); + return IngredientItem( + ingredient: ingredients[index], + isSelected: isSelectedIngredient(index), + ); }, itemCount: ingredients.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), ); } + + String _toChecker(String data){ + if (data == "daging ayam") return 'ayam'; + if (data == "sayap ayam") return 'ayam'; + if (data == "paha ayam") return 'ayam'; + if (data == "dada ayam") return 'ayam'; + + return data; + } + + bool isSelectedIngredient(int index){ + return selectedIngredient.contains(_toChecker(ingredients[index].name?.toLowerCase() ?? '')); + } + } diff --git a/lib/presentation/recipe_detail/components/ingredients_item.dart b/lib/presentation/recipe_detail/components/ingredients_item.dart index 9cf0ac8..3304139 100644 --- a/lib/presentation/recipe_detail/components/ingredients_item.dart +++ b/lib/presentation/recipe_detail/components/ingredients_item.dart @@ -7,8 +7,9 @@ import '../../../styles/colors.dart'; class IngredientItem extends StatelessWidget { final Ingredient ingredient; + final bool? isSelected; - const IngredientItem({super.key, required this.ingredient}); + const IngredientItem({super.key, required this.ingredient, this.isSelected}); @override Widget build(BuildContext context) { @@ -58,12 +59,14 @@ class IngredientItem extends StatelessWidget { // } Widget _circleWidget() { + print("IS SLECTED = $isSelected"); + return Container( width: 8.0, height: 8.0, - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, - color: AppColors.copper, + color: (isSelected ?? false) ? Colors.green : AppColors.copper, ), ); } diff --git a/lib/presentation/recipe_detail/components/utensils_list_widget.dart b/lib/presentation/recipe_detail/components/utensils_list_widget.dart index 48a5fa8..36fcafd 100644 --- a/lib/presentation/recipe_detail/components/utensils_list_widget.dart +++ b/lib/presentation/recipe_detail/components/utensils_list_widget.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; import 'package:snap_and_cook_mobile/components/recipe/utensils.dart'; +import '../../../data/remote/models/utensil_model.dart'; + class UtensilsListWidget extends StatelessWidget { final List utensils; + final List selectedUtensil; - const UtensilsListWidget({super.key, required this.utensils}); + const UtensilsListWidget( + {super.key, required this.utensils, required this.selectedUtensil}); @override Widget build(BuildContext context) { @@ -13,7 +17,10 @@ class UtensilsListWidget extends StatelessWidget { child: ListView.builder( clipBehavior: Clip.none, itemBuilder: (context, index) { - return UtensilItem(name: utensils[index], isSelected: false,); + return UtensilItem( + name: utensils[index], + isSelected: selectedUtensil.contains(utensils[index]), + ); }, itemCount: utensils.length, scrollDirection: Axis.horizontal, diff --git a/lib/presentation/recipe_detail/view/recipe_detail_view.dart b/lib/presentation/recipe_detail/view/recipe_detail_view.dart index e923c05..ebf863d 100644 --- a/lib/presentation/recipe_detail/view/recipe_detail_view.dart +++ b/lib/presentation/recipe_detail/view/recipe_detail_view.dart @@ -75,13 +75,16 @@ class RecipeDetailView extends BaseView { ), Obx( () => IngredientListWidget( + selectedIngredient: controller.selectedIngredientName, ingredients: controller.recipe.value?.ingredients ?? [], ), ), const RecipeDetailDividerWidget( title: 'Alat Memasak', ), - UtensilsListWidget(utensils: controller.recipe.value?.utensils ?? [],), + UtensilsListWidget( + selectedUtensil: controller.selectedUtensil, + utensils: controller.recipe.value?.utensils ?? [],), const RecipeDetailDividerWidget( title: 'Langkah-langkah', ), @@ -89,13 +92,6 @@ class RecipeDetailView extends BaseView { const SizedBox( height: 24, ), - BasicButton( - onPress: () {}, - bgColor: AppColors.copper, - text: 'Mulai Memasak'), - const SizedBox( - height: 24, - ), ], ), ), diff --git a/lib/presentation/recipe_detail/view_model/recipe_detail_view_model.dart b/lib/presentation/recipe_detail/view_model/recipe_detail_view_model.dart index 2eef467..9ba07bf 100644 --- a/lib/presentation/recipe_detail/view_model/recipe_detail_view_model.dart +++ b/lib/presentation/recipe_detail/view_model/recipe_detail_view_model.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; +import '../../../data/remote/models/ingredient_model.dart'; import '../../../domain/entities/recipe.dart'; import '../../../domain/use_case/general/recipe_use_case.dart'; import '../../../resources/arguments/argument_constants.dart'; @@ -8,7 +9,10 @@ import '../../base/base_view_model.dart'; class RecipeDetailViewModel extends BaseViewModel { final _arguments = Get.arguments; String get recipeUuid => _arguments[ArgumentConstants.recipeUuid]; + List get selectedUtensil => _arguments[ArgumentConstants.selectedUtensil]; + List get selectedIngredient => _arguments[ArgumentConstants.selectedIngredient]; + List selectedIngredientName = []; final RecipeUseCase _recipeUseCase = RecipeUseCase(); final Rxn recipe = Rxn(); @@ -16,6 +20,9 @@ class RecipeDetailViewModel extends BaseViewModel { void onInit() { super.onInit(); _fetchRecipeDetail(); + selectedIngredient.forEach((element) { + selectedIngredientName.add((element.name ?? '').toLowerCase()); + }); } Future _fetchRecipeDetail() async { diff --git a/lib/presentation/recipe_detection/components/tutorial_detection_idle_items.dart b/lib/presentation/recipe_detection/components/tutorial_detection_idle_items.dart new file mode 100644 index 0000000..878f1f9 --- /dev/null +++ b/lib/presentation/recipe_detection/components/tutorial_detection_idle_items.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +List createDetectionIdleTutorialTargets({ + required GlobalKey keyBottomNavigation1, +}) { + List targets = []; + targets.add( + TargetFocus( + identify: "keyBottomNavigation1", + keyTarget: keyBottomNavigation1, + color: Colors.black38, +alignSkip: Alignment.topRight, + contents: [ + TargetContent( + align: ContentAlign.custom, + customPosition: CustomTargetContentPosition( + top: 56, + left: 0, + right: 0, + ), + builder: (context, controller) { + return const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Ambil Gambar", + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold + ), + ), + SizedBox(height: 8,), + Text( + "Tekan tombol berikut untuk mengambil foto bahan makanan dari kamera ataupun galeri", + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ], + ); + }, + ), + ], + ), + ); + + return targets; +} \ No newline at end of file diff --git a/lib/presentation/recipe_detection/view/recipe_detection_view.dart b/lib/presentation/recipe_detection/view/recipe_detection_view.dart index 05a07a8..3ef9c39 100644 --- a/lib/presentation/recipe_detection/view/recipe_detection_view.dart +++ b/lib/presentation/recipe_detection/view/recipe_detection_view.dart @@ -37,6 +37,7 @@ class RecipeDetectionView extends BaseView { @override Widget body(BuildContext context) { + controller.getPageContext(context); return Obx(() { if (controller.isShowDetectionResult.value) { return _detection(context); @@ -69,7 +70,10 @@ class RecipeDetectionView extends BaseView { ), const SizedBox(height: 16), BasicButton( - onPress: controller.pickImage, height: 42, text: "Ambil Gambar") + key: controller.buttonKey, + onPress: controller.pickImage, + height: 42, + text: "Ambil Gambar") ], ), )); @@ -86,11 +90,14 @@ class RecipeDetectionView extends BaseView { Widget _detection(BuildContext context) { return Obx(() { - final bboxesColors = List.generate( - 6, - (_) => - Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0), - ); + final bboxesColors = [ + Colors.green, + Colors.blue, + Colors.redAccent, + Colors.purpleAccent, + Colors.amberAccent, + Colors.tealAccent + ]; final double displayWidth = MediaQuery.of(context).size.width; @@ -108,7 +115,6 @@ class RecipeDetectionView extends BaseView { for (int i = 0; i < controller.bboxes.length; i++) { final box = controller.bboxes[i]; final boxClass = controller.classes[i]; - print("boxClass ${boxClass}"); bboxesWidgets.add( Bbox( box[0] * resizeFactor, @@ -127,7 +133,8 @@ class RecipeDetectionView extends BaseView { child: Center( child: Stack( children: [ - if (controller.imageFile.value != null) Image.file(controller.imageFile.value!), + if (controller.imageFile.value != null) + Image.file(controller.imageFile.value!), ...bboxesWidgets, ], ), diff --git a/lib/presentation/recipe_detection/view_model/recipe_detection_view_model.dart b/lib/presentation/recipe_detection/view_model/recipe_detection_view_model.dart index 50b6c74..29be1f8 100644 --- a/lib/presentation/recipe_detection/view_model/recipe_detection_view_model.dart +++ b/lib/presentation/recipe_detection/view_model/recipe_detection_view_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -9,15 +10,20 @@ import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart'; import 'package:snap_and_cook_mobile/resources/arguments/argument_constants.dart'; import 'package:snap_and_cook_mobile/routes/routes/main_route.dart'; import 'package:snap_and_cook_mobile/utils/detection/labels.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; import '../../../components/camera/custom_camera.dart'; import '../../../domain/use_case/utensils/utensil_use_case.dart'; +import '../../../resources/constants/session_constants.dart'; import '../../../utils/detection/yolo.dart'; +import '../../../utils/session/session.dart'; import '../../base/base_view_model.dart'; +import '../components/tutorial_detection_idle_items.dart'; class RecipeDetectionViewModel extends BaseViewModel { RxList> modelResults = RxList(); final _utensilUseCase = UtensilUseCase(); + final buttonKey = GlobalKey(); Rxn imageFile = Rxn(); @@ -35,8 +41,8 @@ class RecipeDetectionViewModel extends BaseViewModel { double maxImageWidgetHeight = 400; - double confidenceThreshold = 0.25; - double iouThreshold = 0.40; + double confidenceThreshold = 0.3; + double iouThreshold = 0.4; RxList> inferenceOutput = RxList(); RxList classes = RxList(); @@ -49,16 +55,62 @@ class RecipeDetectionViewModel extends BaseViewModel { DraggableScrollableController(); final YoloModel model = YoloModel( - 'assets/yolov8s_snapcook.tflite', + 'assets/yolov8m_snapcook.tflite', inModelWidth, inModelHeight, numClasses, ); + late TutorialCoachMark tutorialCoachMark; + + BuildContext? pageContext; + @override void onInit() { super.onInit(); _loadMachineLearningModel(); + createTutorial(); + showTutorial(); + } + + void getPageContext(BuildContext context){ + pageContext = context; + } + + void createTutorial() { + tutorialCoachMark = TutorialCoachMark( + targets: createDetectionIdleTutorialTargets( + keyBottomNavigation1: buttonKey,), + colorShadow: Colors.black38, + textSkip: "SKIP", + paddingFocus: 10, + opacityShadow: 0.5, + imageFilter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), + onFinish: () { + print("finish"); + Session.set(SessionConstants.isAlreadyOnBoardingDetectIngredient, "yes"); + }, + onClickTarget: (target) { + }, + onClickTargetWithTapPosition: (target, tapDetails) {}, + onClickOverlay: (target) {}, + onSkip: () { + Session.set(SessionConstants.isAlreadyOnBoardingDetectIngredient, "yes"); + return true; + }, + ); + } + + Future showTutorial() async { + Future.delayed(const Duration(seconds: 1)); + String? isOnBoarded = await Session.get(SessionConstants.isAlreadyOnBoardingDetectIngredient); + if (isOnBoarded != null) { + return; + } + + if (pageContext?.mounted ?? false){ + tutorialCoachMark.show(context: pageContext!); + } } Future _loadMachineLearningModel() async { diff --git a/lib/presentation/recipe_detection_result/view_model/recipe_detection_result_view_model.dart b/lib/presentation/recipe_detection_result/view_model/recipe_detection_result_view_model.dart index da98675..b96d008 100644 --- a/lib/presentation/recipe_detection_result/view_model/recipe_detection_result_view_model.dart +++ b/lib/presentation/recipe_detection_result/view_model/recipe_detection_result_view_model.dart @@ -48,7 +48,11 @@ class RecipeDetectionResultViewModel extends BaseViewModel { void navigateToRecipeDetail(String uuid) { Get.toNamed(MainRoute.detail, - arguments: {ArgumentConstants.recipeUuid: uuid}); + arguments: { + ArgumentConstants.recipeUuid: uuid, + ArgumentConstants.selectedIngredient: ingredients, + ArgumentConstants.selectedUtensil: selectedUtensil.value + }); } @override diff --git a/lib/presentation/utensils/components/tutorial_utensils_items.dart b/lib/presentation/utensils/components/tutorial_utensils_items.dart new file mode 100644 index 0000000..09e0fca --- /dev/null +++ b/lib/presentation/utensils/components/tutorial_utensils_items.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:snap_and_cook_mobile/styles/colors.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +List createUtensilTutorialTargets({ + required GlobalKey keyBottomNavigation1, +}) { + List targets = []; + targets.add( + TargetFocus( + identify: "keyBottomNavigation1", + keyTarget: keyBottomNavigation1, + color: Colors.black38, +alignSkip: Alignment.topRight, + contents: [ + TargetContent( + align: ContentAlign.custom, + customPosition: CustomTargetContentPosition( + top: Get.height * 0.25, + left: 0, + right: 0, + ), + builder: (context, controller) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Pilih Alat Memasak", + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold + ), + ), + const SizedBox(height: 8,), + const Text( + "Tekan tombol alat memasak yang kamu miliki, dan tekan lagi untuk membatalkan pilihan", + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + const SizedBox(height: 16,), + Row( + children: [ + Container( + color: AppColors.copper, + height: 22, + width: 22, + ), + const SizedBox(width: 8,), + const Expanded( + child: Text( + "Warna oranye berarti kamu telah memilih dan miliki", + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ), + ], + ), + const SizedBox(height: 8,), + Row( + children: [ + Container( + height: 22, + width: 22, + color: AppColors.heroWhite, + ), + const SizedBox(width: 8,), + const Expanded( + child: Text( + "Warna putih berarti tidak kamu pilih", + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ), + ], + ), + ], + ); + }, + ), + ], + ), + ); + + return targets; +} \ No newline at end of file diff --git a/lib/presentation/utensils/view/utensil_view.dart b/lib/presentation/utensils/view/utensil_view.dart index 9a3428b..8da3050 100644 --- a/lib/presentation/utensils/view/utensil_view.dart +++ b/lib/presentation/utensils/view/utensil_view.dart @@ -21,18 +21,31 @@ class UtensilView extends BaseView { @override Widget body(BuildContext context) { + controller.getPageContext(context); return Obx( - () => Wrap( + () => Stack( children: [ - for (int i = 0; i < controller.utensils.length; i++) - GestureDetector( - onTap: () { - controller.onSelectUtensil(controller.utensils[i], i); - }, - child: UtensilItem( - name: controller.utensils[i].name ?? '', - isSelected: controller.utensils[i].isSelected == 1), - ) + Wrap( + children: [ + for (int i = 0; i < controller.utensils.length; i++) + GestureDetector( + onTap: () { + controller.onSelectUtensil(controller.utensils[i], i); + }, + child: UtensilItem( + name: controller.utensils[i].name ?? '', + isSelected: controller.utensils[i].isSelected == 1), + ) + ], + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + key: controller.buttonKey, + height: 20, + width: 60, + ), + ) ], ), ); diff --git a/lib/presentation/utensils/view_model/utensil_view_model.dart b/lib/presentation/utensils/view_model/utensil_view_model.dart index 9abb46e..7456482 100644 --- a/lib/presentation/utensils/view_model/utensil_view_model.dart +++ b/lib/presentation/utensils/view_model/utensil_view_model.dart @@ -1,20 +1,75 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:snap_and_cook_mobile/domain/use_case/utensils/utensil_use_case.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; import '../../../data/remote/models/utensil_model.dart'; +import '../../../resources/constants/session_constants.dart'; +import '../../../utils/session/session.dart'; import '../../base/base_view_model.dart'; +import '../components/tutorial_utensils_items.dart'; class UtensilViewModel extends BaseViewModel { final _useCase = UtensilUseCase(); RxList utensils = RxList(); + final buttonKey = GlobalKey(); + + + late TutorialCoachMark tutorialCoachMark; + BuildContext? pageContext; @override void onInit() { super.onInit(); _fetchUtensils(); + createTutorial(); + showTutorial(); } + void getPageContext(BuildContext context){ + pageContext = context; + } + + void createTutorial() { + tutorialCoachMark = TutorialCoachMark( + targets: createUtensilTutorialTargets( + keyBottomNavigation1: buttonKey,), + colorShadow: Colors.black38, + textSkip: "SKIP", + paddingFocus: 10, + opacityShadow: 0.5, + imageFilter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), + onFinish: () { + print("finish"); + Session.set(SessionConstants.isAlreadyOnBoardingUtensil, "yes"); + }, + onClickTarget: (target) { + }, + onClickTargetWithTapPosition: (target, tapDetails) {}, + onClickOverlay: (target) {}, + onSkip: () { + Session.set(SessionConstants.isAlreadyOnBoardingUtensil, "yes"); + return true; + }, + ); + } + + Future showTutorial() async { + Future.delayed(const Duration(seconds: 1)); + String? isOnBoarded = await Session.get(SessionConstants.isAlreadyOnBoardingUtensil); + if (isOnBoarded != null) { + return; + } + + if (pageContext?.mounted ?? false){ + tutorialCoachMark.show(context: pageContext!); + } + } + + Future _fetchUtensils() async { showLoadingContainer(); utensils.value = await _useCase.fetchUtensils(); diff --git a/lib/resources/arguments/argument_constants.dart b/lib/resources/arguments/argument_constants.dart index 303335c..22d24e8 100644 --- a/lib/resources/arguments/argument_constants.dart +++ b/lib/resources/arguments/argument_constants.dart @@ -2,6 +2,8 @@ class ArgumentConstants { static const String receivedFile = "received_file_args"; static const String ingredients = "ingredients_args"; static const String recipeUuid = "recipe_uuid_args"; + static const String selectedUtensil = "selected_utensil_args"; + static const String selectedIngredient = "selected_ingredient_args"; static const String search = "search_args"; } diff --git a/lib/resources/constants/session_constants.dart b/lib/resources/constants/session_constants.dart index 061508d..73924f7 100644 --- a/lib/resources/constants/session_constants.dart +++ b/lib/resources/constants/session_constants.dart @@ -1,4 +1,8 @@ class SessionConstants { - static const String token = "session_token"; - static const String isAlreadyOnBoarding = "session_is_already_on_boarding"; + static const String isAlreadyOnBoardingHome = + "session_is_already_on_boarding_home"; + static const String isAlreadyOnBoardingDetectIngredient = + "session_is_already_on_boarding_detect_ingredient"; + static const String isAlreadyOnBoardingUtensil = + "session_is_already_on_boarding_utensil"; } diff --git a/pubspec.lock b/pubspec.lock index ffec89d..14f1723 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: archive - sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.6.0" args: dependency: transitive description: @@ -415,10 +415,10 @@ packages: dependency: "direct main" description: name: flutter_screenutil - sha256: "8cf100b8e4973dc570b6415a2090b0bfaa8756ad333db46939efc3e774ee100d" + sha256: b372c35a772a1dc84142a3b9c5ee89a390834bd258e5e6a450d9b975b985d1c9 url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.9.1" flutter_svg: dependency: "direct main" description: @@ -505,10 +505,10 @@ packages: dependency: transitive description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" image_picker: dependency: "direct main" description: @@ -1146,6 +1146,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + tutorial_coach_mark: + dependency: "direct main" + description: + name: tutorial_coach_mark + sha256: "1f1fd234790afb929dec7391a4d90aa54ffe8c8e4d278d9283df8e3f5ac5d63e" + url: "https://pub.dev" + source: hosted + version: "1.2.11" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 31c6563..7fd3ba0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+5 +version: 1.0.0+6 environment: sdk: '>=3.0.2 <4.0.0' @@ -55,6 +55,7 @@ dependencies: chucker_flutter: sqflite: ^2.2.8+4 tflite_flutter: ^0.10.4 + tutorial_coach_mark: ^1.2.11 dev_dependencies: flutter_test: