MEnambahkan fitur tutorial

This commit is contained in:
ibnubatutah 2024-06-11 21:27:26 +07:00
parent 1a5cc31678
commit e81d566892
23 changed files with 543 additions and 54 deletions

View File

@ -67,7 +67,7 @@ class RecipeFullItem extends StatelessWidget {
const Icon(Icons.timer, size: 16), const Icon(Icons.timer, size: 16),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${recipe.cookTime ?? 0} menit', '${recipe.prepTime ?? 0} menit',
style: TTCommonsTextStyles.textSm.textRegular(), style: TTCommonsTextStyles.textSm.textRegular(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@ -12,6 +12,8 @@ class RecipeModel {
String? title; String? title;
String? description; String? description;
String? image; String? image;
String? difficulity;
double? confidence;
List<Ingredient>? ingredients; List<Ingredient>? ingredients;
List<String>? instructions; List<String>? instructions;
int? prepTime; int? prepTime;
@ -49,6 +51,8 @@ class RecipeModel {
cookTime: cookTime, cookTime: cookTime,
servings: servings, servings: servings,
utensils: utensils, utensils: utensils,
difficulity: difficulity,
confidence: confidence
); );
} }
} }

View File

@ -23,7 +23,9 @@ RecipeModel _$RecipeModelFromJson(Map<String, dynamic> json) => RecipeModel(
utensils: (json['utensils'] as List<dynamic>?) utensils: (json['utensils'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList(), .toList(),
); )
..difficulity = json['difficulity'] as String?
..confidence = (json['confidence'] as num?)?.toDouble();
Map<String, dynamic> _$RecipeModelToJson(RecipeModel instance) => Map<String, dynamic> _$RecipeModelToJson(RecipeModel instance) =>
<String, dynamic>{ <String, dynamic>{
@ -31,6 +33,8 @@ Map<String, dynamic> _$RecipeModelToJson(RecipeModel instance) =>
'title': instance.title, 'title': instance.title,
'description': instance.description, 'description': instance.description,
'image': instance.image, 'image': instance.image,
'difficulity': instance.difficulity,
'confidence': instance.confidence,
'ingredients': instance.ingredients, 'ingredients': instance.ingredients,
'instructions': instance.instructions, 'instructions': instance.instructions,
'prepTime': instance.prepTime, 'prepTime': instance.prepTime,

View File

@ -8,6 +8,8 @@ class Recipe {
String? title; String? title;
String? description; String? description;
String? image; String? image;
String? difficulity;
double? confidence;
List<Ingredient>? ingredients; List<Ingredient>? ingredients;
List<String>? instructions; List<String>? instructions;
int? prepTime; int? prepTime;
@ -26,5 +28,7 @@ class Recipe {
this.cookTime, this.cookTime,
this.servings, this.servings,
this.utensils, this.utensils,
this.difficulity,
this.confidence
}); });
} }

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';
List<TargetFocus> createTargets({
required GlobalKey keyBottomNavigation1,
required GlobalKey keyBottomNavigation2,
required GlobalKey keyBottomNavigation3,
}) {
List<TargetFocus> 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;
}

View File

@ -18,6 +18,7 @@ class HomeView extends BaseView<HomeViewModel> {
@override @override
Widget? floatingActionButton() { Widget? floatingActionButton() {
return FloatingActionButton( return FloatingActionButton(
key: controller.cameraKey,
onPressed: () { onPressed: () {
controller.navigateToRecipeDetection(); controller.navigateToRecipeDetection();
}, },
@ -33,6 +34,11 @@ class HomeView extends BaseView<HomeViewModel> {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
controller.getPageContext(context);
return _buildBody();
}
Widget _buildBody(){
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -43,7 +49,7 @@ class HomeView extends BaseView<HomeViewModel> {
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
children: [ children: [
SizedBox( const SizedBox(
height: 16, height: 16,
), ),
Row( Row(
@ -51,11 +57,12 @@ class HomeView extends BaseView<HomeViewModel> {
Text( Text(
'Selamat Datang', 'Selamat Datang',
style: TTCommonsTextStyles.textLg.textRegular().copyWith( style: TTCommonsTextStyles.textLg.textRegular().copyWith(
color: AppColors.heroWhite, color: AppColors.heroWhite,
), ),
), ),
const Spacer(), const Spacer(),
IconButton( IconButton(
key: controller.settingKey,
onPressed: controller.navigateToUtensilPage, onPressed: controller.navigateToUtensilPage,
icon: const Icon( icon: const Icon(
Icons.settings, Icons.settings,
@ -64,6 +71,7 @@ class HomeView extends BaseView<HomeViewModel> {
], ],
), ),
SearchTextField( SearchTextField(
key: controller.searchKey,
controller: controller.searchController, controller: controller.searchController,
hintText: 'Cari Resep..', hintText: 'Cari Resep..',
inputType: TextInputType.text, inputType: TextInputType.text,
@ -89,8 +97,8 @@ class HomeView extends BaseView<HomeViewModel> {
], ],
), ),
), ),
RecipeRecommendationWidget(), const RecipeRecommendationWidget(),
SizedBox(height: 64,) const SizedBox(height: 64,)
], ],
), ),
); );
@ -128,7 +136,7 @@ class HomeView extends BaseView<HomeViewModel> {
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
Text('Data 1'), const Text('Data 1'),
], ],
), ),
); );

View File

@ -1,17 +1,30 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/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 '../../../domain/entities/recipe.dart';
import '../../../resources/arguments/argument_constants.dart'; import '../../../resources/arguments/argument_constants.dart';
import '../../../routes/routes/main_route.dart'; import '../../../routes/routes/main_route.dart';
import '../../../utils/session/session.dart';
import '../../base/base_view_model.dart'; import '../../base/base_view_model.dart';
import '../components/tutorial_home_items.dart';
class HomeViewModel extends BaseViewModel { class HomeViewModel extends BaseViewModel {
TextEditingController searchController = TextEditingController(); TextEditingController searchController = TextEditingController();
final RecipeUseCase _recipeUseCase = RecipeUseCase(); final RecipeUseCase _recipeUseCase = RecipeUseCase();
final RxList<Recipe> recipes = RxList<Recipe>(); final RxList<Recipe> recipes = RxList<Recipe>();
late TutorialCoachMark tutorialCoachMark;
BuildContext? pageContext;
final cameraKey = GlobalKey();
final searchKey = GlobalKey();
final settingKey = GlobalKey();
void onSearchSubmitted(String value) { void onSearchSubmitted(String value) {
if (value.isEmpty) { if (value.isEmpty) {
@ -25,6 +38,51 @@ class HomeViewModel extends BaseViewModel {
void onInit() { void onInit() {
super.onInit(); super.onInit();
_fetchAllRecipes(); _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<void> 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<void> _fetchAllRecipes() async { Future<void> _fetchAllRecipes() async {

View File

@ -4,18 +4,37 @@ import 'package:snap_and_cook_mobile/presentation/recipe_detail/components/ingre
class IngredientListWidget extends StatelessWidget { class IngredientListWidget extends StatelessWidget {
final List<Ingredient> ingredients; final List<Ingredient> ingredients;
final List<String> selectedIngredient;
const IngredientListWidget({super.key, required this.ingredients}); const IngredientListWidget(
{super.key, required this.ingredients, required this.selectedIngredient});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.builder( return ListView.builder(
itemBuilder: (context, index) { itemBuilder: (context, index) {
return IngredientItem(ingredient: ingredients[index]); return IngredientItem(
ingredient: ingredients[index],
isSelected: isSelectedIngredient(index),
);
}, },
itemCount: ingredients.length, itemCount: ingredients.length,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), 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() ?? ''));
}
} }

View File

@ -7,8 +7,9 @@ import '../../../styles/colors.dart';
class IngredientItem extends StatelessWidget { class IngredientItem extends StatelessWidget {
final Ingredient ingredient; final Ingredient ingredient;
final bool? isSelected;
const IngredientItem({super.key, required this.ingredient}); const IngredientItem({super.key, required this.ingredient, this.isSelected});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -58,12 +59,14 @@ class IngredientItem extends StatelessWidget {
// } // }
Widget _circleWidget() { Widget _circleWidget() {
print("IS SLECTED = $isSelected");
return Container( return Container(
width: 8.0, width: 8.0,
height: 8.0, height: 8.0,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: AppColors.copper, color: (isSelected ?? false) ? Colors.green : AppColors.copper,
), ),
); );
} }

View File

@ -1,10 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/components/recipe/utensils.dart'; import 'package:snap_and_cook_mobile/components/recipe/utensils.dart';
import '../../../data/remote/models/utensil_model.dart';
class UtensilsListWidget extends StatelessWidget { class UtensilsListWidget extends StatelessWidget {
final List<String> utensils; final List<String> utensils;
final List<String> selectedUtensil;
const UtensilsListWidget({super.key, required this.utensils}); const UtensilsListWidget(
{super.key, required this.utensils, required this.selectedUtensil});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -13,7 +17,10 @@ class UtensilsListWidget extends StatelessWidget {
child: ListView.builder( child: ListView.builder(
clipBehavior: Clip.none, clipBehavior: Clip.none,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return UtensilItem(name: utensils[index], isSelected: false,); return UtensilItem(
name: utensils[index],
isSelected: selectedUtensil.contains(utensils[index]),
);
}, },
itemCount: utensils.length, itemCount: utensils.length,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,

View File

@ -75,13 +75,16 @@ class RecipeDetailView extends BaseView<RecipeDetailViewModel> {
), ),
Obx( Obx(
() => IngredientListWidget( () => IngredientListWidget(
selectedIngredient: controller.selectedIngredientName,
ingredients: controller.recipe.value?.ingredients ?? [], ingredients: controller.recipe.value?.ingredients ?? [],
), ),
), ),
const RecipeDetailDividerWidget( const RecipeDetailDividerWidget(
title: 'Alat Memasak', title: 'Alat Memasak',
), ),
UtensilsListWidget(utensils: controller.recipe.value?.utensils ?? [],), UtensilsListWidget(
selectedUtensil: controller.selectedUtensil,
utensils: controller.recipe.value?.utensils ?? [],),
const RecipeDetailDividerWidget( const RecipeDetailDividerWidget(
title: 'Langkah-langkah', title: 'Langkah-langkah',
), ),
@ -89,13 +92,6 @@ class RecipeDetailView extends BaseView<RecipeDetailViewModel> {
const SizedBox( const SizedBox(
height: 24, height: 24,
), ),
BasicButton(
onPress: () {},
bgColor: AppColors.copper,
text: 'Mulai Memasak'),
const SizedBox(
height: 24,
),
], ],
), ),
), ),

View File

@ -1,5 +1,6 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../data/remote/models/ingredient_model.dart';
import '../../../domain/entities/recipe.dart'; import '../../../domain/entities/recipe.dart';
import '../../../domain/use_case/general/recipe_use_case.dart'; import '../../../domain/use_case/general/recipe_use_case.dart';
import '../../../resources/arguments/argument_constants.dart'; import '../../../resources/arguments/argument_constants.dart';
@ -8,7 +9,10 @@ import '../../base/base_view_model.dart';
class RecipeDetailViewModel extends BaseViewModel { class RecipeDetailViewModel extends BaseViewModel {
final _arguments = Get.arguments; final _arguments = Get.arguments;
String get recipeUuid => _arguments[ArgumentConstants.recipeUuid]; String get recipeUuid => _arguments[ArgumentConstants.recipeUuid];
List<String> get selectedUtensil => _arguments[ArgumentConstants.selectedUtensil];
List<Ingredient> get selectedIngredient => _arguments[ArgumentConstants.selectedIngredient];
List<String> selectedIngredientName = [];
final RecipeUseCase _recipeUseCase = RecipeUseCase(); final RecipeUseCase _recipeUseCase = RecipeUseCase();
final Rxn<Recipe> recipe = Rxn<Recipe>(); final Rxn<Recipe> recipe = Rxn<Recipe>();
@ -16,6 +20,9 @@ class RecipeDetailViewModel extends BaseViewModel {
void onInit() { void onInit() {
super.onInit(); super.onInit();
_fetchRecipeDetail(); _fetchRecipeDetail();
selectedIngredient.forEach((element) {
selectedIngredientName.add((element.name ?? '').toLowerCase());
});
} }
Future<void> _fetchRecipeDetail() async { Future<void> _fetchRecipeDetail() async {

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';
List<TargetFocus> createDetectionIdleTutorialTargets({
required GlobalKey keyBottomNavigation1,
}) {
List<TargetFocus> 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;
}

View File

@ -37,6 +37,7 @@ class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
controller.getPageContext(context);
return Obx(() { return Obx(() {
if (controller.isShowDetectionResult.value) { if (controller.isShowDetectionResult.value) {
return _detection(context); return _detection(context);
@ -69,7 +70,10 @@ class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
BasicButton( 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<RecipeDetectionViewModel> {
Widget _detection(BuildContext context) { Widget _detection(BuildContext context) {
return Obx(() { return Obx(() {
final bboxesColors = List<Color>.generate( final bboxesColors = [
6, Colors.green,
(_) => Colors.blue,
Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0), Colors.redAccent,
); Colors.purpleAccent,
Colors.amberAccent,
Colors.tealAccent
];
final double displayWidth = MediaQuery.of(context).size.width; final double displayWidth = MediaQuery.of(context).size.width;
@ -108,7 +115,6 @@ class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> {
for (int i = 0; i < controller.bboxes.length; i++) { for (int i = 0; i < controller.bboxes.length; i++) {
final box = controller.bboxes[i]; final box = controller.bboxes[i];
final boxClass = controller.classes[i]; final boxClass = controller.classes[i];
print("boxClass ${boxClass}");
bboxesWidgets.add( bboxesWidgets.add(
Bbox( Bbox(
box[0] * resizeFactor, box[0] * resizeFactor,
@ -127,7 +133,8 @@ class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> {
child: Center( child: Center(
child: Stack( child: Stack(
children: [ children: [
if (controller.imageFile.value != null) Image.file(controller.imageFile.value!), if (controller.imageFile.value != null)
Image.file(controller.imageFile.value!),
...bboxesWidgets, ...bboxesWidgets,
], ],
), ),

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/resources/arguments/argument_constants.dart';
import 'package:snap_and_cook_mobile/routes/routes/main_route.dart'; import 'package:snap_and_cook_mobile/routes/routes/main_route.dart';
import 'package:snap_and_cook_mobile/utils/detection/labels.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 '../../../components/camera/custom_camera.dart';
import '../../../domain/use_case/utensils/utensil_use_case.dart'; import '../../../domain/use_case/utensils/utensil_use_case.dart';
import '../../../resources/constants/session_constants.dart';
import '../../../utils/detection/yolo.dart'; import '../../../utils/detection/yolo.dart';
import '../../../utils/session/session.dart';
import '../../base/base_view_model.dart'; import '../../base/base_view_model.dart';
import '../components/tutorial_detection_idle_items.dart';
class RecipeDetectionViewModel extends BaseViewModel { class RecipeDetectionViewModel extends BaseViewModel {
RxList<Map<String, dynamic>> modelResults = RxList(); RxList<Map<String, dynamic>> modelResults = RxList();
final _utensilUseCase = UtensilUseCase(); final _utensilUseCase = UtensilUseCase();
final buttonKey = GlobalKey();
Rxn<File> imageFile = Rxn<File>(); Rxn<File> imageFile = Rxn<File>();
@ -35,8 +41,8 @@ class RecipeDetectionViewModel extends BaseViewModel {
double maxImageWidgetHeight = 400; double maxImageWidgetHeight = 400;
double confidenceThreshold = 0.25; double confidenceThreshold = 0.3;
double iouThreshold = 0.40; double iouThreshold = 0.4;
RxList<List<double>> inferenceOutput = RxList(); RxList<List<double>> inferenceOutput = RxList();
RxList<int> classes = RxList(); RxList<int> classes = RxList();
@ -49,16 +55,62 @@ class RecipeDetectionViewModel extends BaseViewModel {
DraggableScrollableController(); DraggableScrollableController();
final YoloModel model = YoloModel( final YoloModel model = YoloModel(
'assets/yolov8s_snapcook.tflite', 'assets/yolov8m_snapcook.tflite',
inModelWidth, inModelWidth,
inModelHeight, inModelHeight,
numClasses, numClasses,
); );
late TutorialCoachMark tutorialCoachMark;
BuildContext? pageContext;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_loadMachineLearningModel(); _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<void> 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<void> _loadMachineLearningModel() async { Future<void> _loadMachineLearningModel() async {

View File

@ -48,7 +48,11 @@ class RecipeDetectionResultViewModel extends BaseViewModel {
void navigateToRecipeDetail(String uuid) { void navigateToRecipeDetail(String uuid) {
Get.toNamed(MainRoute.detail, Get.toNamed(MainRoute.detail,
arguments: {ArgumentConstants.recipeUuid: uuid}); arguments: {
ArgumentConstants.recipeUuid: uuid,
ArgumentConstants.selectedIngredient: ingredients,
ArgumentConstants.selectedUtensil: selectedUtensil.value
});
} }
@override @override

View File

@ -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<TargetFocus> createUtensilTutorialTargets({
required GlobalKey keyBottomNavigation1,
}) {
List<TargetFocus> 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;
}

View File

@ -21,18 +21,31 @@ class UtensilView extends BaseView<UtensilViewModel> {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
controller.getPageContext(context);
return Obx( return Obx(
() => Wrap( () => Stack(
children: [ children: [
for (int i = 0; i < controller.utensils.length; i++) Wrap(
GestureDetector( children: [
onTap: () { for (int i = 0; i < controller.utensils.length; i++)
controller.onSelectUtensil(controller.utensils[i], i); GestureDetector(
}, onTap: () {
child: UtensilItem( controller.onSelectUtensil(controller.utensils[i], i);
name: controller.utensils[i].name ?? '', },
isSelected: controller.utensils[i].isSelected == 1), 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,
),
)
], ],
), ),
); );

View File

@ -1,20 +1,75 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:snap_and_cook_mobile/domain/use_case/utensils/utensil_use_case.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 '../../../data/remote/models/utensil_model.dart';
import '../../../resources/constants/session_constants.dart';
import '../../../utils/session/session.dart';
import '../../base/base_view_model.dart'; import '../../base/base_view_model.dart';
import '../components/tutorial_utensils_items.dart';
class UtensilViewModel extends BaseViewModel { class UtensilViewModel extends BaseViewModel {
final _useCase = UtensilUseCase(); final _useCase = UtensilUseCase();
RxList<Utensil> utensils = RxList(); RxList<Utensil> utensils = RxList();
final buttonKey = GlobalKey();
late TutorialCoachMark tutorialCoachMark;
BuildContext? pageContext;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_fetchUtensils(); _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<void> 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<void> _fetchUtensils() async { Future<void> _fetchUtensils() async {
showLoadingContainer(); showLoadingContainer();
utensils.value = await _useCase.fetchUtensils(); utensils.value = await _useCase.fetchUtensils();

View File

@ -2,6 +2,8 @@ class ArgumentConstants {
static const String receivedFile = "received_file_args"; static const String receivedFile = "received_file_args";
static const String ingredients = "ingredients_args"; static const String ingredients = "ingredients_args";
static const String recipeUuid = "recipe_uuid_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"; static const String search = "search_args";
} }

View File

@ -1,4 +1,8 @@
class SessionConstants { class SessionConstants {
static const String token = "session_token"; static const String isAlreadyOnBoardingHome =
static const String isAlreadyOnBoarding = "session_is_already_on_boarding"; "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";
} }

View File

@ -21,10 +21,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.5.1" version: "3.6.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -415,10 +415,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_screenutil name: flutter_screenutil
sha256: "8cf100b8e4973dc570b6415a2090b0bfaa8756ad333db46939efc3e774ee100d" sha256: b372c35a772a1dc84142a3b9c5ee89a390834bd258e5e6a450d9b975b985d1c9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.9.0" version: "5.9.1"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -505,10 +505,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.7" version: "4.2.0"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1146,6 +1146,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" 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: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@ -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 # 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 # 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. # 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: environment:
sdk: '>=3.0.2 <4.0.0' sdk: '>=3.0.2 <4.0.0'
@ -55,6 +55,7 @@ dependencies:
chucker_flutter: chucker_flutter:
sqflite: ^2.2.8+4 sqflite: ^2.2.8+4
tflite_flutter: ^0.10.4 tflite_flutter: ^0.10.4
tutorial_coach_mark: ^1.2.11
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: