Menambahkan utensil page, dan utensil contract

This commit is contained in:
IbnuBatutah 2024-03-07 14:05:40 +07:00
parent 2bdf328d20
commit 6c46c04008
48 changed files with 673 additions and 359 deletions

View File

@ -1,26 +1,4 @@
A Telur
B Ayam
C Kentang
D Wortel
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z

BIN
assets/snapcook_v7.tflite Normal file

Binary file not shown.

View File

@ -165,7 +165,9 @@ class CustomCameraWidgetState extends State<CustomCameraWidget> {
ImagePicker(); ImagePicker();
final XFile? image = final XFile? image =
await picker0.pickImage( await picker0.pickImage(
source: ImageSource.gallery); source: ImageSource.gallery,
);
if (image == null) { if (image == null) {
return; return;
} }

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../styles/colors.dart';
class UtensilItem extends StatelessWidget {
final String name;
final bool isSelected;
const UtensilItem({super.key, required this.name, required this.isSelected});
@override
Widget build(BuildContext context) {
return Container(
height: 32,
width: 108,
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(26),
border: Border.all(
color: isSelected ? Colors.white : AppColors.copper,
width: 1.5
),
color: isSelected ? AppColors.copper : Colors.white
),
child: Center(
child: Text(
name,
style: TTCommonsTextStyles.textMd
.textRegular()
.copyWith(color: isSelected ? Colors.white : AppColors.copper),
),
),
);
}
}

View File

@ -26,7 +26,7 @@ class DatabaseHelper {
await db.execute( await db.execute(
'''CREATE TABLE ${DatabaseConstant.utensilsTable} ( '''CREATE TABLE ${DatabaseConstant.utensilsTable} (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT, is_selected INTEGER name TEXT, isSelected INTEGER
)''', )''',
); );
}, },

View File

@ -35,4 +35,11 @@ class UtensilContract {
await db.query(DatabaseConstant.utensilsTable); await db.query(DatabaseConstant.utensilsTable);
return results.map((res) => Utensil.fromJson(res)).toList(); return results.map((res) => Utensil.fromJson(res)).toList();
} }
Future<List<String>> getSelectedUtensils() async {
final Database db = await _databaseHelper.database;
List<Map<String, dynamic>> results =
await db.query(DatabaseConstant.utensilsTable,where: 'isSelected = 1');
return results.map((res) => Utensil.fromJson(res).name ?? '').toList();
}
} }

View File

@ -6,6 +6,7 @@ part 'ingredient_model.g.dart';
class Ingredient { class Ingredient {
String? name; String? name;
double? quantity; double? quantity;
@JsonKey(name: 'unit', includeIfNull: false)
String? unit; String? unit;
Ingredient({ Ingredient({

View File

@ -12,9 +12,18 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) => Ingredient(
unit: json['unit'] as String?, unit: json['unit'] as String?,
); );
Map<String, dynamic> _$IngredientToJson(Ingredient instance) => Map<String, dynamic> _$IngredientToJson(Ingredient instance) {
<String, dynamic>{ final val = <String, dynamic>{
'name': instance.name, 'name': instance.name,
'quantity': instance.quantity, 'quantity': instance.quantity,
'unit': instance.unit, };
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('unit', instance.unit);
return val;
}

View File

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'utensil_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Utensil _$UtensilFromJson(Map<String, dynamic> json) => Utensil(
id: json['id'] as int?,
name: json['name'] as String?,
isSelected: json['isSelected'] as int?,
);
Map<String, dynamic> _$UtensilToJson(Utensil instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'isSelected': instance.isSelected,
};

View File

@ -0,0 +1,21 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart';
part 'detect_ingredient_request.g.dart';
@JsonSerializable()
class DetectIngredientRequest {
List<Ingredient>? ingredients;
List<String>? utensils;
DetectIngredientRequest({
this.ingredients,
this.utensils,
});
Map<String, dynamic> toJson() => _$DetectIngredientRequestToJson(this);
factory DetectIngredientRequest.fromJson(Map<String, dynamic> json) =>
_$DetectIngredientRequestFromJson(json);
}

View File

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'detect_ingredient_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DetectIngredientRequest _$DetectIngredientRequestFromJson(
Map<String, dynamic> json) =>
DetectIngredientRequest(
ingredients: (json['ingredients'] as List<dynamic>?)
?.map((e) => Ingredient.fromJson(e as Map<String, dynamic>))
.toList(),
utensils: (json['utensils'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$DetectIngredientRequestToJson(
DetectIngredientRequest instance) =>
<String, dynamic>{
'ingredients': instance.ingredients,
'utensils': instance.utensils,
};

View File

@ -1,20 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'forgot_password_request.g.dart';
@JsonSerializable()
class ForgotPasswordRequest {
String phone;
String key;
ForgotPasswordRequest({
required this.phone,
required this.key,
});
Map<String, dynamic> toJson() => _$ForgotPasswordRequestToJson(this);
factory ForgotPasswordRequest.fromJson(Map<String, dynamic> json) =>
_$ForgotPasswordRequestFromJson(json);
}

View File

@ -1,21 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'forgot_password_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ForgotPasswordRequest _$ForgotPasswordRequestFromJson(
Map<String, dynamic> json) =>
ForgotPasswordRequest(
phone: json['phone'] as String,
key: json['key'] as String,
);
Map<String, dynamic> _$ForgotPasswordRequestToJson(
ForgotPasswordRequest instance) =>
<String, dynamic>{
'phone': instance.phone,
'key': instance.key,
};

View File

@ -1,20 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'login_request.g.dart';
@JsonSerializable()
class LoginRequest {
String phone;
String password;
LoginRequest({
required this.phone,
required this.password,
});
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
factory LoginRequest.fromJson(Map<String, dynamic> json) =>
_$LoginRequestFromJson(json);
}

View File

@ -1,18 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'login_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
LoginRequest _$LoginRequestFromJson(Map<String, dynamic> json) => LoginRequest(
phone: json['phone'] as String,
password: json['password'] as String,
);
Map<String, dynamic> _$LoginRequestToJson(LoginRequest instance) =>
<String, dynamic>{
'phone': instance.phone,
'password': instance.password,
};

View File

@ -1,16 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'phone_number_check_request.g.dart';
@JsonSerializable()
class PhoneNumberCheckRequest {
String phone;
PhoneNumberCheckRequest({
required this.phone,
});
Map<String, dynamic> toJson() => _$PhoneNumberCheckRequestToJson(this);
factory PhoneNumberCheckRequest.fromJson(Map<String, dynamic> json) =>
_$PhoneNumberCheckRequestFromJson(json);
}

View File

@ -1,19 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'phone_number_check_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PhoneNumberCheckRequest _$PhoneNumberCheckRequestFromJson(
Map<String, dynamic> json) =>
PhoneNumberCheckRequest(
phone: json['phone'] as String,
);
Map<String, dynamic> _$PhoneNumberCheckRequestToJson(
PhoneNumberCheckRequest instance) =>
<String, dynamic>{
'phone': instance.phone,
};

View File

@ -1,23 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'register_request.g.dart';
@JsonSerializable()
class RegisterRequest {
String name;
String phone;
String email;
String password;
RegisterRequest({
required this.name,
required this.phone,
required this.email,
required this.password,
});
Map<String, dynamic> toJson() => _$RegisterRequestToJson(this);
factory RegisterRequest.fromJson(Map<String, dynamic> json) =>
_$RegisterRequestFromJson(json);
}

View File

@ -1,23 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'register_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
RegisterRequest _$RegisterRequestFromJson(Map<String, dynamic> json) =>
RegisterRequest(
name: json['name'] as String,
phone: json['phone'] as String,
email: json['email'] as String,
password: json['password'] as String,
);
Map<String, dynamic> _$RegisterRequestToJson(RegisterRequest instance) =>
<String, dynamic>{
'name': instance.name,
'phone': instance.phone,
'email': instance.email,
'password': instance.password,
};

View File

@ -1,24 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'reset_password_request.g.dart';
@JsonSerializable()
class ResetPasswordRequest {
String code;
String phone;
String password;
String passwordConfirmation;
ResetPasswordRequest({
required this.code,
required this.phone,
required this.password,
required this.passwordConfirmation,
});
Map<String, dynamic> toJson() => _$ResetPasswordRequestToJson(this);
factory ResetPasswordRequest.fromJson(Map<String, dynamic> json) =>
_$ResetPasswordRequestFromJson(json);
}

View File

@ -1,25 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'reset_password_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ResetPasswordRequest _$ResetPasswordRequestFromJson(
Map<String, dynamic> json) =>
ResetPasswordRequest(
code: json['code'] as String,
phone: json['phone'] as String,
password: json['password'] as String,
passwordConfirmation: json['passwordConfirmation'] as String,
);
Map<String, dynamic> _$ResetPasswordRequestToJson(
ResetPasswordRequest instance) =>
<String, dynamic>{
'code': instance.code,
'phone': instance.phone,
'password': instance.password,
'passwordConfirmation': instance.passwordConfirmation,
};

View File

@ -1,19 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'verify_otp_request.g.dart';
@JsonSerializable()
class VerifyOtpRequest {
String phone;
String otpCode;
VerifyOtpRequest({
required this.phone,
required this.otpCode,
});
Map<String, dynamic> toJson() => _$VerifyOtpRequestToJson(this);
factory VerifyOtpRequest.fromJson(Map<String, dynamic> json) =>
_$VerifyOtpRequestFromJson(json);
}

View File

@ -1,19 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'verify_otp_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
VerifyOtpRequest _$VerifyOtpRequestFromJson(Map<String, dynamic> json) =>
VerifyOtpRequest(
phone: json['phone'] as String,
otpCode: json['otpCode'] as String,
);
Map<String, dynamic> _$VerifyOtpRequestToJson(VerifyOtpRequest instance) =>
<String, dynamic>{
'phone': instance.phone,
'otpCode': instance.otpCode,
};

View File

@ -4,6 +4,7 @@ import 'package:snap_and_cook_mobile/data/remote/models/recipe_model.dart';
import 'package:snap_and_cook_mobile/data/remote/responses/base_response.dart'; import 'package:snap_and_cook_mobile/data/remote/responses/base_response.dart';
import '../../../resources/services/recipe_service_constant.dart'; import '../../../resources/services/recipe_service_constant.dart';
import '../requests/detect_ingredient_request.dart';
part 'recipe_service.g.dart'; part 'recipe_service.g.dart';
@ -13,9 +14,18 @@ abstract class RecipeServices {
@GET(RecipeServiceConstants.listRecipe) @GET(RecipeServiceConstants.listRecipe)
Future<BaseResponse<List<RecipeModel>>> getAllRecipes( Future<BaseResponse<List<RecipeModel>>> getAllRecipes(
@CancelRequest() CancelToken cancelToken); @CancelRequest() CancelToken cancelToken,
@Query("page[size]") int size,
@Query("page[current]") int currentPage,
@Query("filter[search]") String? search,
);
@GET(RecipeServiceConstants.detailRecipe) @GET(RecipeServiceConstants.detailRecipe)
Future<BaseResponse<RecipeModel>> getDetailRecipe( Future<BaseResponse<RecipeModel>> getDetailRecipe(
@CancelRequest() CancelToken cancelToken, @Path("uuid") String uuid); @CancelRequest() CancelToken cancelToken, @Path("uuid") String uuid);
@POST(RecipeServiceConstants.recipeRecommendation)
Future<BaseResponse<List<RecipeModel>>> getRecipeRecommendation(
@CancelRequest() CancelToken cancelToken,
@Body() DetectIngredientRequest request);
} }

View File

@ -19,9 +19,19 @@ class _RecipeServices implements RecipeServices {
String? baseUrl; String? baseUrl;
@override @override
Future<BaseResponse<List<RecipeModel>>> getAllRecipes(cancelToken) async { Future<BaseResponse<List<RecipeModel>>> getAllRecipes(
cancelToken,
size,
currentPage,
search,
) async {
const _extra = <String, dynamic>{}; const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{}; final queryParameters = <String, dynamic>{
r'page[size]': size,
r'page[current]': currentPage,
r'filter[search]': search,
};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{}; final _headers = <String, dynamic>{};
final _data = <String, dynamic>{}; final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>( final _result = await _dio.fetch<Map<String, dynamic>>(
@ -78,6 +88,40 @@ class _RecipeServices implements RecipeServices {
return value; return value;
} }
@override
Future<BaseResponse<List<RecipeModel>>> getRecipeRecommendation(
cancelToken,
request,
) async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(request.toJson());
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<BaseResponse<List<RecipeModel>>>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'recipe/recommendation',
queryParameters: queryParameters,
data: _data,
cancelToken: cancelToken,
)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = BaseResponse<List<RecipeModel>>.fromJson(
_result.data!,
(json) => (json as List<dynamic>)
.map<RecipeModel>(
(i) => RecipeModel.fromJson(i as Map<String, dynamic>))
.toList(),
);
return value;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) { RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic && if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes || !(requestOptions.responseType == ResponseType.bytes ||

View File

@ -1,10 +1,20 @@
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../../data/remote/models/ingredient_model.dart';
import '../../entities/recipe.dart'; import '../../entities/recipe.dart';
abstract class RecipeInterface { abstract class RecipeInterface {
Future<Either<DioError, List<Recipe>>> fetchRecipes(CancelToken cancelToken); Future<Either<DioError, List<Recipe>>> fetchRecipes(CancelToken cancelToken, {
required int size,
required int currentPage,
String? search,
});
Future<Either<DioError, List<Recipe>>> fetchRecipeRecommendations(
CancelToken cancelToken,
List<Ingredient> ingredients,
List<String> utensils);
Future<Either<DioError, Recipe>> fetchDetailRecipe( Future<Either<DioError, Recipe>> fetchDetailRecipe(
CancelToken cancelToken, String uuid); CancelToken cancelToken, String uuid);

View File

@ -1,6 +1,8 @@
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart';
import 'package:snap_and_cook_mobile/data/remote/requests/detect_ingredient_request.dart';
import '../../../data/remote/services/recipe_service.dart'; import '../../../data/remote/services/recipe_service.dart';
import '../../entities/recipe.dart'; import '../../entities/recipe.dart';
@ -11,9 +13,13 @@ class RecipeUseCase implements RecipeInterface {
@override @override
Future<Either<DioError, List<Recipe>>> fetchRecipes( Future<Either<DioError, List<Recipe>>> fetchRecipes(
CancelToken cancelToken) async { CancelToken cancelToken, {
required int size,
required int currentPage,
String? search,
}) async {
try { try {
final response = await service.getAllRecipes(cancelToken); final response = await service.getAllRecipes(cancelToken, size, currentPage, search);
List<Recipe> recipes = []; List<Recipe> recipes = [];
response.data?.forEach((element) { response.data?.forEach((element) {
recipes.add(element.toEntity()); recipes.add(element.toEntity());
@ -36,4 +42,28 @@ class RecipeUseCase implements RecipeInterface {
return Left(DioError(requestOptions: RequestOptions(path: ""))); return Left(DioError(requestOptions: RequestOptions(path: "")));
} }
} }
@override
Future<Either<DioError, List<Recipe>>> fetchRecipeRecommendations(
CancelToken cancelToken,
List<Ingredient> ingredients,
List<String> utensils) async {
try {
final response = await service.getRecipeRecommendation(
cancelToken,
DetectIngredientRequest(
ingredients: ingredients, utensils: utensils));
List<Recipe> recipes = [];
response.data?.forEach((element) {
recipes.add(element.toEntity());
});
return Right(recipes);
} on DioError catch (e) {
print("DioError is ${e}");
return Left(e);
} catch (e) {
print("Error is ${e}");
return Left(DioError(requestOptions: RequestOptions(path: "")));
}
}
} }

View File

@ -0,0 +1,10 @@
import 'package:snap_and_cook_mobile/data/remote/models/utensil_model.dart';
abstract class UtensilInterface {
Future<List<Utensil>> fetchUtensils();
Future<List<String>> fetchSelectedUtensils();
Future<void> updateUtensil(Utensil utensil);
}

View File

@ -0,0 +1,34 @@
import '../../../data/remote/models/utensil_model.dart';
List<Utensil> utensilResource = [
Utensil(
id: 1,
name: "Wajan",
isSelected: 0
),
Utensil(
id: 2,
name: "Pisau",
isSelected: 0
),
Utensil(
id: 3,
name: "Spatula",
isSelected: 0
),
Utensil(
id: 4,
name: "Kompor",
isSelected: 0
),
Utensil(
id: 5,
name: "Mangkuk",
isSelected: 0
),
Utensil(
id: 6,
name: "Pemanggang",
isSelected: 0
),
];

View File

@ -0,0 +1,32 @@
import 'package:snap_and_cook_mobile/data/local/utensils_contract.dart';
import 'package:snap_and_cook_mobile/domain/use_case/utensils/utensil_resource.dart';
import '../../../data/remote/models/utensil_model.dart';
import 'utensil_interface.dart';
class UtensilUseCase implements UtensilInterface {
final _dbContract = UtensilContract();
@override
Future<List<Utensil>> fetchUtensils() async {
List<Utensil> utensils = await _dbContract.getUtensils();
if (utensils.isEmpty){
_dbContract.insertAllUtensil(utensilResource);
return utensilResource;
}
return utensils;
}
@override
Future<void> updateUtensil(Utensil utensil) async {
await _dbContract.updateUtensil(utensil);
}
@override
Future<List<String>> fetchSelectedUtensils() async {
List<String> utensils = await _dbContract.getSelectedUtensils();
return utensils;
}
}

View File

@ -6,7 +6,9 @@ import 'package:snap_and_cook_mobile/utils/extension/dio_extension.dart';
import 'package:snap_and_cook_mobile/utils/interceptor/platform_header_interceptor.dart'; import 'package:snap_and_cook_mobile/utils/interceptor/platform_header_interceptor.dart';
import 'components/app/app.dart'; import 'components/app/app.dart';
import 'configuration/app_build_config.dart';
import 'configuration/app_environtment.dart'; import 'configuration/app_environtment.dart';
import 'data/enums/environment_enum.dart';
import 'utils/session/session.dart'; import 'utils/session/session.dart';
Future<void> init() async { Future<void> init() async {
@ -16,7 +18,13 @@ Future<void> init() async {
await Get.putAsync<Dio>( await Get.putAsync<Dio>(
() async => Dio() () async => Dio()
.baseUrl(AppEnvironment.apiUrl) .baseUrl(AppEnvironment.apiUrl)
.addInterceptor(PlatformHeaderInterceptor()), .addInterceptor(PlatformHeaderInterceptor())
.modify((dio) {
if (AppBuildConfig.instance.config == BuildConfigEnum.staging) {
dio.usePrettyLogger();
}
return dio;
}),
); );
await Get.putAsync(() async => Session()); await Get.putAsync(() async => Session());

View File

@ -2,7 +2,6 @@ import 'configuration/app_build_config.dart';
import 'data/enums/environment_enum.dart'; import 'data/enums/environment_enum.dart';
import 'init.dart'; import 'init.dart';
void main() async{ void main() async{
AppBuildConfig.instantiate(config: BuildConfigEnum.staging); AppBuildConfig.instantiate(config: BuildConfigEnum.staging);
await init(); await init();

View File

@ -56,7 +56,7 @@ class HomeView extends BaseView<HomeViewModel> {
), ),
const Spacer(), const Spacer(),
IconButton( IconButton(
onPressed: () {}, onPressed: controller.navigateToUtensilPage,
icon: const Icon( icon: const Icon(
Icons.settings, Icons.settings,
color: Colors.white, color: Colors.white,
@ -76,12 +76,6 @@ class HomeView extends BaseView<HomeViewModel> {
], ],
), ),
), ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12),
child: Text("Cari berdasarkan 1 bahan",
style: TTCommonsTextStyles.textLg.textMedium()),
),
_ingredientsWidget(),
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
@ -89,11 +83,9 @@ class HomeView extends BaseView<HomeViewModel> {
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12),
child: Row( child: Row(
children: [ children: [
Text("Rekomendasi", Text("Rekomendasi Resep :",
style: TTCommonsTextStyles.textLg.textMedium()), style: TTCommonsTextStyles.textLg.textMedium()),
const Spacer(), const Spacer(),
Text("Lihat Semua",
style: TTCommonsTextStyles.textSm.textRegular()),
], ],
), ),
), ),

View File

@ -8,15 +8,17 @@ import '../../../routes/routes/main_route.dart';
import '../../base/base_view_model.dart'; import '../../base/base_view_model.dart';
class HomeViewModel extends BaseViewModel { class HomeViewModel extends BaseViewModel {
String version = "Version 0.0.1-dev";
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>();
void onSearchSubmitted(String value) { void onSearchSubmitted(String value) {
print(value); if (value.isEmpty) {
return;
}
Get.toNamed(MainRoute.searchResult,
arguments: {ArgumentConstants.search: value});
} }
@override @override
@ -27,9 +29,9 @@ class HomeViewModel extends BaseViewModel {
Future<void> _fetchAllRecipes() async { Future<void> _fetchAllRecipes() async {
showLoadingContainer(); showLoadingContainer();
var data = await _recipeUseCase.fetchRecipes(cancelToken); var data = await _recipeUseCase.fetchRecipes(cancelToken,
data.fold((l){ size: 20, currentPage: 1);
}, (result){ data.fold((l) {}, (result) {
hideLoadingContainer(); hideLoadingContainer();
recipes.clear(); recipes.clear();
recipes.addAll(result); recipes.addAll(result);
@ -46,6 +48,10 @@ class HomeViewModel extends BaseViewModel {
Get.toNamed(MainRoute.detection); Get.toNamed(MainRoute.detection);
} }
void navigateToUtensilPage() {
Get.toNamed(MainRoute.utensil);
}
@override @override
void onClose() {} void onClose() {}
} }

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/components/recipe/utensils.dart';
class UtensilsListWidget extends StatelessWidget {
final List<String> utensils;
const UtensilsListWidget({super.key, required this.utensils});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 50,
child: ListView.builder(
itemBuilder: (context, index) {
return UtensilItem(name: utensils[index], isSelected: false,);
},
itemCount: utensils.length,
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
),
);
}
}

View File

@ -11,6 +11,7 @@ import '../../base/base_view.dart';
import '../components/food_prep_widget.dart'; import '../components/food_prep_widget.dart';
import '../components/recipe_detail_divider_widget.dart'; import '../components/recipe_detail_divider_widget.dart';
import '../components/step_list_widget.dart'; import '../components/step_list_widget.dart';
import '../components/utensils_list_widget.dart';
import '../view_model/recipe_detail_view_model.dart'; import '../view_model/recipe_detail_view_model.dart';
class RecipeDetailView extends BaseView<RecipeDetailViewModel> { class RecipeDetailView extends BaseView<RecipeDetailViewModel> {
@ -80,6 +81,7 @@ class RecipeDetailView extends BaseView<RecipeDetailViewModel> {
const RecipeDetailDividerWidget( const RecipeDetailDividerWidget(
title: 'Alat Memasak', title: 'Alat Memasak',
), ),
UtensilsListWidget(utensils: controller.recipe.value?.utensils ?? [],),
const RecipeDetailDividerWidget( const RecipeDetailDividerWidget(
title: 'Langkah-langkah', title: 'Langkah-langkah',
), ),

View File

@ -40,13 +40,12 @@ class RecipeDetectionViewModel extends BaseViewModel {
Future<void> _loadMachineLearningModel() async { Future<void> _loadMachineLearningModel() async {
showLoadingContainer(); showLoadingContainer();
await _vision.loadYoloModel( await _vision.loadYoloModel(
labels: 'assets/labels.txt', // labels: 'assets/labels.txt',
modelPath: 'assets/yolov8m_float16.tflite', modelPath: 'assets/yolov8m_float16.tflite',
// labels: 'assets/labels_alpha.txt', labels: 'assets/labels.txt',
// modelPath: 'assets/wicaratanganV2.tflite',
modelVersion: "yolov8", modelVersion: "yolov8",
quantization: false, quantization: false,
numThreads: 2, numThreads: 3,
useGpu: true, useGpu: true,
); );
hideLoadingContainer(); hideLoadingContainer();
@ -87,10 +86,12 @@ class RecipeDetectionViewModel extends BaseViewModel {
bytesList: byte, bytesList: byte,
imageHeight: image.height, imageHeight: image.height,
imageWidth: image.width, imageWidth: image.width,
iouThreshold: 0.8, iouThreshold: 0.2,
confThreshold: 0.2, confThreshold: 0.2,
classThreshold: 0.3, classThreshold: 0.2,
); );
print("DATA IS ${result.length}");
if (result.isNotEmpty) { if (result.isNotEmpty) {
modelResults.value = result; modelResults.value = result;
imageBytes.value = await drawOnImage(modelResults); imageBytes.value = await drawOnImage(modelResults);
@ -118,6 +119,27 @@ class RecipeDetectionViewModel extends BaseViewModel {
} }
} }
final translationDict = {
'carrot': 'Wortel',
};
Ingredient translateIngredient(Ingredient ingredient, Map<String, String> translationDict) {
final translatedName = translationDict[ingredient.name] ?? ingredient.name;
return Ingredient(
name: translatedName,
quantity: ingredient.quantity,
unit: ingredient.unit,
);
}
List<Ingredient> translateIngredients(List<Ingredient> ingredients, Map<String, String> translationDict) {
return ingredients.map((ingredient) => translateIngredient(ingredient, translationDict)).toList();
}
// List<String> translateIngredients(List<Ingredient> originalList) {
// return originalList.map((Ingredient item) => item.name?.replaceAll('carrot', 'wortel')).toList();
// }
void incrementIngredientQuantity(int index) { void incrementIngredientQuantity(int index) {
detectedIngredients[index].quantity = detectedIngredients[index].quantity =
(detectedIngredients[index].quantity ?? 0) + 1; (detectedIngredients[index].quantity ?? 0) + 1;
@ -157,7 +179,9 @@ class RecipeDetectionViewModel extends BaseViewModel {
imageWidth: imageWidth.value, imageWidth: imageWidth.value,
); );
detectedIngredients.value = detectedObject; detectedIngredients.value = translateIngredients(detectedObject, translationDict);
// detectedIngredients.value = detectedObject;
final picture = recorder.endRecording(); final picture = recorder.endRecording();
final imgWithBoxes = await picture.toImage(img.width, img.height); final imgWithBoxes = await picture.toImage(img.width, img.height);

View File

@ -19,7 +19,7 @@ class DetectedIngredientItem extends StatelessWidget {
color: AppColors.copper), color: AppColors.copper),
), ),
child: Text( child: Text(
'${ingredient.name} ${ingredient.quantity} ${ingredient.unit}', '${ingredient.name} ${ingredient.quantity}',
style: TTCommonsTextStyles.textMd.textMedium().copyWith( style: TTCommonsTextStyles.textMd.textMedium().copyWith(
color: AppColors.copper, color: AppColors.copper,
), ),

View File

@ -10,20 +10,19 @@ class RecipeResultList extends GetView<RecipeDetectionResultViewModel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.builder( return Obx(
() => ListView.builder(
itemBuilder: (context, index) { itemBuilder: (context, index) {
Recipe recipe = controller.recipes[index];
return RecipeFullItem( return RecipeFullItem(
recipe: Recipe( recipe: recipe,
title: 'Masakan',
cookTime: 0,
image:
'https://img.freepik.com/free-photo/tasty-burger-isolated-white-background-fresh-hamburger-fastfood-with-beef-cheese_90220-1063.jpg',
),
onTap: controller.navigateToRecipeDetail, onTap: controller.navigateToRecipeDetail,
); );
}, },
itemCount: 4, itemCount: controller.recipes.length,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics()); physics: const NeverScrollableScrollPhysics(),
),
);
} }
} }

View File

@ -1,27 +1,56 @@
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 '../../../data/remote/models/ingredient_model.dart'; import '../../../data/remote/models/ingredient_model.dart';
import '../../../domain/entities/recipe.dart';
import '../../../domain/use_case/utensils/utensil_use_case.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 '../../base/base_view_model.dart'; import '../../base/base_view_model.dart';
class RecipeDetectionResultViewModel extends BaseViewModel { class RecipeDetectionResultViewModel extends BaseViewModel {
final _utensilUseCase = UtensilUseCase();
final _recipeUseCase = RecipeUseCase();
final _argument = Get.arguments as Map<String, dynamic>; final _argument = Get.arguments as Map<String, dynamic>;
final List<Ingredient> ingredients = []; final List<Ingredient> ingredients = [];
final RxList<String> selectedUtensil = RxList();
final RxList<Recipe> recipes = RxList<Recipe>();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
ingredients.addAll(_argument[ArgumentConstants.ingredients] as List<Ingredient>); ingredients
.addAll(_argument[ArgumentConstants.ingredients] as List<Ingredient>);
_fetchRecipeRecommendation();
} }
void navigateToRecipeDetail(String uuid){ Future<void> _fetchSelectedUtensil() async {
Get.toNamed(MainRoute.detail, arguments: { selectedUtensil.value = await _utensilUseCase.fetchSelectedUtensils();
ArgumentConstants.recipeUuid: uuid }
Future<void> _fetchRecipeRecommendation() async {
showLoadingContainer();
await _fetchSelectedUtensil();
var data = await _recipeUseCase.fetchRecipeRecommendations(
cancelToken, ingredients, selectedUtensil);
data.fold((l) {
hideLoadingContainer();
}, (result) {
hideLoadingContainer();
recipes.clear();
recipes.addAll(result);
}); });
} }
void navigateToRecipeDetail(String uuid) {
Get.toNamed(MainRoute.detail,
arguments: {ArgumentConstants.recipeUuid: uuid});
}
@override @override
void onReady() { void onReady() {
super.onReady(); super.onReady();
@ -29,5 +58,4 @@ class RecipeDetectionResultViewModel extends BaseViewModel {
@override @override
void onClose() {} void onClose() {}
} }

View File

@ -0,0 +1,13 @@
import 'package:get/get.dart';
import '../view_model/recipe_search_result_view_model.dart';
class RecipeSearchResultBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RecipeSearchResultViewModel>(
() => RecipeSearchResultViewModel(),
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../components/recipe/recipe_full_item.dart';
import '../../../domain/entities/recipe.dart';
import '../view_model/recipe_search_result_view_model.dart';
class RecipeResultList extends GetView<RecipeSearchResultViewModel> {
const RecipeResultList({super.key});
@override
Widget build(BuildContext context) {
return Obx(
() => ListView.builder(
itemBuilder: (context, index) {
Recipe recipe = controller.recipes[index];
return RecipeFullItem(
recipe: recipe,
onTap: controller.navigateToRecipeDetail,
);
},
itemCount: controller.recipes.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
),
);
}
}

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../../components/appbar/basic_appbar.dart';
import '../../../components/form/search_text_field.dart';
import '../../../styles/colors.dart';
import '../../base/base_view.dart';
import '../components/recipe_result_list.dart';
import '../view_model/recipe_search_result_view_model.dart';
class RecipeSearchResultView
extends BaseView<RecipeSearchResultViewModel> {
const RecipeSearchResultView({super.key});
@override
PreferredSizeWidget? appBar(BuildContext context) {
return BasicAppBar(
appBarTitleText: "",
centerTitle: false,
leadingIconData: Icons.arrow_back,
);
}
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_recipeResultSearch(),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Text('Hasil Pencarian:',
style: TTCommonsTextStyles.textLg.textMedium()),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: RecipeResultList(),
),
],
),
);
}
Widget _recipeResultSearch(){
return Container(
width: double.infinity,
color: AppColors.primary,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
SizedBox(
height: 16,
),
SearchTextField(
controller: controller.searchController,
hintText: 'Cari Resep..',
inputType: TextInputType.text,
isOptional: true,
onSubmitted: controller.onSearchSubmitted,
),
const SizedBox(
height: 16,
),
],
),
);
}
}

View File

@ -0,0 +1,60 @@
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 '../../../data/remote/models/ingredient_model.dart';
import '../../../domain/entities/recipe.dart';
import '../../../domain/use_case/utensils/utensil_use_case.dart';
import '../../../resources/arguments/argument_constants.dart';
import '../../../routes/routes/main_route.dart';
import '../../base/base_view_model.dart';
class RecipeSearchResultViewModel extends BaseViewModel {
final _recipeUseCase = RecipeUseCase();
TextEditingController searchController = TextEditingController();
final _argument = Get.arguments as Map<String, dynamic>;
String search = "";
final RxList<String> selectedUtensil = RxList();
final RxList<Recipe> recipes = RxList<Recipe>();
@override
void onInit() {
super.onInit();
search = _argument[ArgumentConstants.search] as String;
searchController.text = search;
_fetchRecipes();
}
Future<void> _fetchRecipes() async {
showLoadingContainer();
var data = await _recipeUseCase.fetchRecipes(cancelToken, size: 20, currentPage: 1, search: search);
data.fold((l) {
hideLoadingContainer();
}, (result) {
hideLoadingContainer();
recipes.clear();
recipes.addAll(result);
});
}
void onSearchSubmitted(String value) {
search = value;
_fetchRecipes();
}
void navigateToRecipeDetail(String uuid) {
Get.toNamed(MainRoute.detail,
arguments: {ArgumentConstants.recipeUuid: uuid});
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {}
}

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart'; import 'package:get/get.dart';
import '../../../components/asset_image_view.dart'; import 'package:get/get_state_manager/get_state_manager.dart';
import '../../../styles/values.dart'; import 'package:snap_and_cook_mobile/components/recipe/utensils.dart';
import '../../base/base_view.dart';
import '../../../components/appbar/basic_appbar.dart'; import '../../../components/appbar/basic_appbar.dart';
import '../../../styles/images.dart'; import '../../base/base_view.dart';
import '../view_model/utensil_view_model.dart'; import '../view_model/utensil_view_model.dart';
class UtensilView extends BaseView<UtensilViewModel> { class UtensilView extends BaseView<UtensilViewModel> {
@ -12,26 +12,39 @@ class UtensilView extends BaseView<UtensilViewModel> {
@override @override
PreferredSizeWidget? appBar(BuildContext context) { PreferredSizeWidget? appBar(BuildContext context) {
return BasicAppBar(appBarTitleText: "", centerTitle: false); return BasicAppBar(
appBarTitleText: "Alat-Alat Memasak",
onTapBack: () => Get.back(),
leadingIconData: Icons.arrow_back,
centerTitle: false);
} }
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return Column( return Obx(
() => 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),
)
],
),
);
}
Widget _utensilsWidget() {
return Wrap(
children: [ children: [
const SizedBox( for (int i = 0; i < controller.utensils.length; i++)
width: double.infinity, UtensilItem(
), name: controller.utensils[i].name ?? '',
const Expanded( isSelected: controller.utensils[i].isSelected == 1)
child: AssetImageView(
fileName: AppImages.logoFull,
width: AppValues.logoWidth,
height: AppValues.logoHeight,
)),
Text(controller.version, style: TTCommonsTextStyles.textLg.textMedium()),
const SizedBox(
height: 32,
),
], ],
); );
} }

View File

@ -1,22 +1,34 @@
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 '../../../routes/routes/main_route.dart'; import '../../../data/remote/models/utensil_model.dart';
import '../../base/base_view_model.dart'; import '../../base/base_view_model.dart';
class UtensilViewModel extends BaseViewModel { class UtensilViewModel extends BaseViewModel {
String version = "Version 0.0.1-dev"; final _useCase = UtensilUseCase();
RxList<Utensil> utensils = RxList();
@override @override
void onReady() { void onInit() {
super.onReady(); super.onInit();
_startSplash(); _fetchUtensils();
} }
@override Future<void> _fetchUtensils() async {
void onClose() {} showLoadingContainer();
utensils.value = await _useCase.fetchUtensils();
Future<void> _startSplash() async { hideLoadingContainer();
await Future.delayed(const Duration(seconds: 2));
Get.offNamed(MainRoute.home);
} }
void onSelectUtensil(Utensil utensil, int index){
if (utensil.isSelected == 0){
utensil.isSelected = 1;
} else{
utensil.isSelected = 0;
}
utensils[index] = utensil;
_useCase.updateUtensil(utensil);
}
} }

View File

@ -2,5 +2,6 @@ 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 search = "search_args";
} }

View File

@ -3,6 +3,10 @@ import 'package:snap_and_cook_mobile/presentation/home/binding/home_binding.dart
import 'package:snap_and_cook_mobile/presentation/home/view/home_view.dart'; import 'package:snap_and_cook_mobile/presentation/home/view/home_view.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detection/binding/recipe_detection_binding.dart'; import 'package:snap_and_cook_mobile/presentation/recipe_detection/binding/recipe_detection_binding.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detection/view/recipe_detection_view.dart'; import 'package:snap_and_cook_mobile/presentation/recipe_detection/view/recipe_detection_view.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_search_result/binding/recipe_search_result_binding.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_search_result/view/recipe_search_result_view.dart';
import 'package:snap_and_cook_mobile/presentation/utensils/binding/utensil_binding.dart';
import 'package:snap_and_cook_mobile/presentation/utensils/view/utensil_view.dart';
import '../../presentation/recipe_detail/binding/recipe_detail_binding.dart'; import '../../presentation/recipe_detail/binding/recipe_detail_binding.dart';
import '../../presentation/recipe_detail/view/recipe_detail_view.dart'; import '../../presentation/recipe_detail/view/recipe_detail_view.dart';
@ -18,6 +22,8 @@ class MainRoute {
static const detection = "/recipe-detection-page"; static const detection = "/recipe-detection-page";
static const detectionResult = "/recipe-detection-result-page"; static const detectionResult = "/recipe-detection-result-page";
static const detail = "/recipe-detail-page"; static const detail = "/recipe-detail-page";
static const utensil = "/utensil-page";
static const searchResult = "/recipe-search-result-page";
static final routes = [ static final routes = [
GetPage( GetPage(
@ -45,5 +51,15 @@ class MainRoute {
page: () => const RecipeDetailView(), page: () => const RecipeDetailView(),
binding: RecipeDetailBinding(), binding: RecipeDetailBinding(),
), ),
GetPage(
name: utensil,
page: () => const UtensilView(),
binding: UtensilBinding(),
),
GetPage(
name: searchResult,
page: () => const RecipeSearchResultView(),
binding: RecipeSearchResultBinding(),
),
]; ];
} }