Update 14 jan

This commit is contained in:
IbnuBatutah 2024-01-14 20:29:05 +07:00
parent 21a18461ea
commit c542883bec
57 changed files with 1834 additions and 271 deletions

3
.gitignore vendored
View File

@ -16,6 +16,9 @@ migrate_working_dir/
*.iws *.iws
.idea/ .idea/
production/.env
staging/.env
# The .vscode folder contains launch configuration and tasks you configure in # The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line # VS Code which you may wish to be included in version control, so this line
# is commented out by default. # is commented out by default.

File diff suppressed because one or more lines are too long

26
assets/labels_alpha.txt Normal file
View File

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

View File

@ -35,14 +35,14 @@ class BasicButton extends StatelessWidget {
this.isTransparent = false, this.isTransparent = false,
this.isBorder = false, this.isBorder = false,
this.isLeading = false, this.isLeading = false,
this.borderRadius = 0, this.borderRadius = 14,
this.bgColor = AppColors.heroBlack, this.bgColor = AppColors.primary,
this.textColor = AppColors.heroWhite, this.textColor = AppColors.heroWhite,
this.borderColor = AppColors.heroBlack, this.borderColor = AppColors.primary,
this.height = 60, this.height = 42,
this.sizeText = 18, this.sizeText = 16,
this.leadingIcon, this.leadingIcon,
this.fontWeight = FontWeight.w600, this.fontWeight = FontWeight.w500,
this.disableColor = AppColors.primaryGrey100, this.disableColor = AppColors.primaryGrey100,
this.loadingColor = AppColors.heroWhite, this.viewKey, this.loadingColor = AppColors.heroWhite, this.viewKey,
}) : super(key: key); }) : super(key: key);
@ -61,15 +61,12 @@ class BasicButton extends StatelessWidget {
style: buttonStyle(), style: buttonStyle(),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: IntrinsicHeight( child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ basicButtonLeading(),
basicButtonLeading(), Expanded(child: basicButtonText()),
basicButtonOnProgress(), ],
basicButtonText(),
],
),
), ),
), ),
), ),
@ -83,30 +80,6 @@ class BasicButton extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
leadingIcon ?? const SizedBox(), leadingIcon ?? const SizedBox(),
SizedBox(
width: 24.w,
),
],
);
}
return const SizedBox();
}
Widget basicButtonOnProgress() {
if (isLoading) {
return Row(
children: [
SizedBox(
width: 18.w,
height: 18.h,
child: CircularProgressIndicator(
color: loadingColor,
strokeWidth: 3.r,
),
),
SizedBox(
width: 12.w,
),
], ],
); );
} }

View File

@ -15,26 +15,20 @@ import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:photo_gallery/photo_gallery.dart'; import 'package:photo_gallery/photo_gallery.dart';
class CameraApp extends StatefulWidget { class CustomCameraWidget extends StatefulWidget {
final int? compressionQuality; final int? compressionQuality;
const CameraApp({Key? key, this.compressionQuality = 100}) : super(key: key); const CustomCameraWidget({Key? key, this.compressionQuality = 100}) : super(key: key);
@override @override
CameraAppState createState() => CameraAppState(); CustomCameraWidgetState createState() => CustomCameraWidgetState();
} }
class CameraAppState extends State<CameraApp> { class CustomCameraWidgetState extends State<CustomCameraWidget> {
CameraController? controller; CameraController? controller;
late List<CameraDescription> cameras; late List<CameraDescription> cameras;
List<Album> imageAlbums = [];
Set<Medium> imageMedium = {};
Uint8List? bytes;
List<File> results = [];
List<int> indexList = [];
bool flashOn = false; bool flashOn = false;
bool showPerformance = false; bool showPerformance = false;
late double width;
@override @override
void initState() { void initState() {
@ -124,7 +118,8 @@ class CameraAppState extends State<CameraApp> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_galleryButton(), _galleryButton(),
_cameraButton() _cameraButton(),
SizedBox(width: 25,)
], ],
), ),
), ),
@ -150,7 +145,7 @@ class CameraAppState extends State<CameraApp> {
size: 30, color: Colors.white)), size: 30, color: Colors.white)),
IconButton( IconButton(
onPressed: () { onPressed: () {
compress([]); compress(null);
}, },
icon: const Icon( icon: const Icon(
Icons.close_rounded, Icons.close_rounded,
@ -177,7 +172,7 @@ class CameraAppState extends State<CameraApp> {
return; return;
} }
File file = File(image.path); File file = File(image.path);
compress([file]); compress(file);
}, },
icon: const Icon(Icons.photo_library, icon: const Icon(Icons.photo_library,
size: 30, color: Colors.white), size: 30, color: Colors.white),
@ -189,7 +184,7 @@ class CameraAppState extends State<CameraApp> {
onTap: () async { onTap: () async {
XFile file2 = await controller!.takePicture(); XFile file2 = await controller!.takePicture();
File file = File(file2.path); File file = File(file2.path);
compress([file]); compress(file);
}, },
child: Container( child: Container(
width: 75, width: 75,
@ -212,18 +207,19 @@ class CameraAppState extends State<CameraApp> {
}); });
} }
void compress(List<File> files) async { void compress(File? file) async {
List<File> files2 = []; if (file == null) {
for (File file in files) { Navigator.of(context).pop(null);
Uint8List? blobBytes = await compressFile(file);
var dir = await getTemporaryDirectory();
String trimmed = dir.absolute.path;
String dateTimeString = DateTime.now().millisecondsSinceEpoch.toString();
String pathString = "$trimmed/$dateTimeString.jpg";
File fileNew = File(pathString);
fileNew.writeAsBytesSync(List.from(blobBytes!));
files2.add(fileNew);
} }
File? files2;
Uint8List? blobBytes = await compressFile(file!);
var dir = await getTemporaryDirectory();
String trimmed = dir.absolute.path;
String dateTimeString = DateTime.now().millisecondsSinceEpoch.toString();
String pathString = "$trimmed/$dateTimeString.jpg";
File fileNew = File(pathString);
fileNew.writeAsBytesSync(List.from(blobBytes!));
files2 = fileNew;
if (context.mounted) { if (context.mounted) {
Navigator.of(context).pop(files2); Navigator.of(context).pop(files2);

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../../styles/form/form_border_side.dart';
import '../../styles/form/form_input_borders.dart';
import '../../styles/radiuses.dart';
class SearchTextField extends StatelessWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final TextInputType inputType;
final int? maxLines, maxLength;
final String? hintText;
final Function(String)? onSubmitted;
final Function(String)? onChanged;
final Iterable<String>? autoFill;
final Icon? prefixIcon;
final Icon? suffixIcon;
final bool? isOptional;
final bool? isFieldDisable;
final Function? onFieldTap;
const SearchTextField({
Key? key,
required this.controller,
this.maxLines,
this.hintText,
required this.inputType,
this.onSubmitted,
this.maxLength,
this.prefixIcon,
this.isOptional,
this.suffixIcon,
this.onChanged,
this.autoFill,
this.focusNode,
this.isFieldDisable = false,
this.onFieldTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isFieldDisable == true) {
return GestureDetector(
onTap: () => onFieldTap?.call(),
child: AbsorbPointer(
child: IgnorePointer(
ignoring: true,
child: _searchBar(),
),
),
);
}
return _searchBar();
}
Widget _searchBar() => TextFormField(
showCursor: true,
focusNode: focusNode,
controller: controller,
keyboardType: inputType,
onChanged: onChanged,
maxLines: maxLines,
maxLength: maxLength,
onFieldSubmitted: onSubmitted,
cursorColor: Colors.black45,
autofillHints: autoFill,
validator: null,
decoration: InputDecoration(
suffixIcon: suffixIcon,
prefixIcon: prefixIcon,
fillColor: Colors.white,
filled: true,
hintText: hintText,
border: OutlineInputBorder(
borderRadius: AppBorderRadius.radius10,
borderSide: FormBorderSide.none,
),
hintStyle: const TextStyle(fontSize: 16, color: Colors.grey),
),
);
}

View File

@ -0,0 +1,73 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../styles/images.dart';
import '../asset_image_view.dart';
import '../skeleton/shimmering_box_skeleton.dart';
class BasicNetworkImage extends StatelessWidget {
final String imageUrl;
final double? height, width;
final ImageWidgetBuilder? imageBuilder;
final BoxFit? boxFit;
final Widget? placeHolder;
const BasicNetworkImage({
Key? key,
required this.imageUrl,
this.height,
this.width,
this.imageBuilder,
this.boxFit,
this.placeHolder,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return CachedNetworkImage(
imageUrl: imageUrl,
height: height,
width: width,
imageBuilder: imageBuilder,
fit: boxFit ?? BoxFit.cover,
progressIndicatorBuilder: (context, url, downloadProgress) =>
_imageShimmeringLoading(),
errorWidget: (context, url, error) => CachedNetworkImage(
imageUrl: imageUrl,
height: height,
width: width,
imageBuilder: imageBuilder,
fit: boxFit ?? BoxFit.cover,
progressIndicatorBuilder: (context, url, downloadProgress) =>
_imageShimmeringLoading(),
errorWidget: (context, url, error) => CachedNetworkImage(
imageUrl: imageUrl,
height: height,
width: width,
imageBuilder: imageBuilder,
fit: boxFit ?? BoxFit.cover,
progressIndicatorBuilder: (context, url, downloadProgress) =>
_imageShimmeringLoading(),
errorWidget: (context, url, error) => Image(
height: height,
width: width,
image: Image.network(imageUrl).image,
errorBuilder: (context, exception, stackTrace) =>
placeHolder ?? _imageErrorWidget()),
),
),
);
}
Widget _imageShimmeringLoading() => const ShimmeringBoxSkeleton();
Widget _imageErrorWidget() => const Center(
child: SizedBox(
height: 80,
width: 80,
child: AssetImageView(
fileName: AppImages.logoFull,
),
),
);
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/components/image/basic_network_image.dart';
import 'package:snap_and_cook_mobile/data/remote/models/recipe_model.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../domain/entities/recipe.dart';
class RecipeFullItem extends StatelessWidget {
final Recipe recipe;
final Function(String) onTap;
const RecipeFullItem({super.key, required this.recipe, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
onTap(recipe.uuid ?? '');
},
child: Card(
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16),
child: Text(
recipe.title ?? 'Masakan',
style: TTCommonsTextStyles.textLg.textSemiBold(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
BasicNetworkImage(
imageUrl: recipe.image ?? 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT80b6egSM9UngjcWwCu92vjmfRQux7WcZCMQ&usqp=CAU',
height: 120,
width: double.infinity,
boxFit: BoxFit.cover,
),
const SizedBox(
height: 12,
),
_cookingTimeWidget(),
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'${recipe.description}',
style: TTCommonsTextStyles.textSm.textRegular(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(
height: 18,
),
],
),
),
);
}
Widget _cookingTimeWidget(){
return Row(
children: [
const SizedBox(width: 8),
const Icon(Icons.timer, size: 16),
const SizedBox(width: 4),
Text(
'${recipe.cookTime ?? 0} menit',
style: TTCommonsTextStyles.textSm.textRegular(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 18),
const Icon(Icons.soup_kitchen, size: 16),
const SizedBox(width: 4),
Text(
'${recipe.cookTime ?? 0} menit',
style: TTCommonsTextStyles.textSm.textRegular(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 8),
],
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/components/image/basic_network_image.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../domain/entities/recipe.dart';
class RecipeItem extends StatelessWidget {
final Recipe recipe;
const RecipeItem({super.key, required this.recipe});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BasicNetworkImage(
imageUrl: recipe.image ?? 'https://img.freepik.com/free-photo/tasty-burger-isolated-white-background-fresh-hamburger-fastfood-with-beef-cheese_90220-1063.jpg',
height: 160,
width: double.infinity,
),
const SizedBox(
height: 8,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
recipe.title ?? 'Masakan',
style: TTCommonsTextStyles.textMd.textMedium(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'${recipe.cookTime ?? 0} menit',
style: TTCommonsTextStyles.textSm.textRegular(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
}

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../styles/colors.dart';
class ShimmeringBoxSkeleton extends StatelessWidget {
const ShimmeringBoxSkeleton({Key? key, this.height, this.width}) : super(key: key);
final double? height, width;
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: const Color(0xffcbcbcb),
highlightColor: const Color(0xffededed),
enabled: true,
child: Container(
height: height,
width: width,
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: AppColors.primaryLightGrey),
),
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import '../../styles/colors.dart';
class ShimmeringCircleSkeleton extends StatelessWidget {
const ShimmeringCircleSkeleton({Key? key, this.size = 24}) : super(key: key);
final double? size;
@override
Widget build(BuildContext context) {
return Container(
height: size,
width: size,
decoration: const BoxDecoration(
color: AppColors.primaryLightGrey,
shape: BoxShape.circle,
),
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../styles/colors.dart';
class ShimmeringIconSkeleton extends StatelessWidget {
const ShimmeringIconSkeleton({Key? key, this.size, required this.icon}) : super(key: key);
final double? size;
final IconData icon;
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: const Color(0xffcbcbcb),
highlightColor: const Color(0xffededed),
enabled: true,
child: Icon(icon, color: AppColors.primaryLightGrey, size: size ?? 24),
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../styles/colors.dart';
class ShimmeringRoundedSkeleton extends StatelessWidget {
const ShimmeringRoundedSkeleton({Key? key, this.height, this.width, this.radius}) : super(key: key);
final double? height, width, radius;
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: const Color(0xffcbcbcb),
highlightColor: const Color(0xffededed),
enabled: true,
child: Container(
height: height,
width: width,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primaryLightGrey,
borderRadius:
BorderRadius.all(Radius.circular(radius ?? 16))),
),
);
}
}

View File

@ -14,4 +14,4 @@ class AppBuildConfig {
instance._lock = true; instance._lock = true;
return instance; return instance;
} }
} }

View File

@ -1,11 +1,23 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../data/enums/environment_enum.dart';
import '../resources/constants/environtment_constant.dart'; import '../resources/constants/environtment_constant.dart';
import 'app_build_config.dart';
class AppEnvironment { class AppEnvironment {
static Map<String, String> get env => <String, String>{ static load() async {
EnvironmentConstant.baseUrl: const String.fromEnvironment( if (AppBuildConfig.instance.config == BuildConfigEnum.production) {
EnvironmentConstant.baseUrl, await dotenv.load(fileName: "production/.env");
defaultValue: ""), } else {
}; await dotenv.load(fileName: "staging/.env");
}
}
static Map<String, String> get env => dotenv.env;
static String get apiUrl => dotenv.env[EnvironmentConstant.baseUrl] ?? '';
static String get imageUrl => dotenv.env[EnvironmentConstant.imageUrl] ?? '';
static String get baseUrl => env[EnvironmentConstant.baseUrl] ?? '';
} }

View File

@ -1,11 +1,13 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import '../../../configuration/app_environtment.dart';
import '../../../domain/entities/recipe.dart';
import 'ingredient_model.dart'; import 'ingredient_model.dart';
part 'recipe_model.g.dart'; part 'recipe_model.g.dart';
@JsonSerializable() @JsonSerializable()
class Recipe { class RecipeModel {
String? uuid; String? uuid;
String? title; String? title;
String? description; String? description;
@ -17,7 +19,7 @@ class Recipe {
int? servings; int? servings;
List<String>? utensils; List<String>? utensils;
Recipe({ RecipeModel({
this.uuid, this.uuid,
this.title, this.title,
this.description, this.description,
@ -30,8 +32,23 @@ class Recipe {
this.utensils, this.utensils,
}); });
Map<String, dynamic> toJson() => _$RecipeToJson(this); Map<String, dynamic> toJson() => _$RecipeModelToJson(this);
factory Recipe.fromJson(Map<String, dynamic> json) => factory RecipeModel.fromJson(Map<String, dynamic> json) =>
_$RecipeFromJson(json); _$RecipeModelFromJson(json);
Recipe toEntity() {
return Recipe(
uuid: uuid,
title: title,
description: description,
image: AppEnvironment.apiUrl + (image ?? ''),
ingredients: ingredients,
instructions: instructions,
prepTime: prepTime,
cookTime: cookTime,
servings: servings,
utensils: utensils,
);
}
} }

View File

@ -6,7 +6,7 @@ part of 'recipe_model.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe( RecipeModel _$RecipeModelFromJson(Map<String, dynamic> json) => RecipeModel(
uuid: json['uuid'] as String?, uuid: json['uuid'] as String?,
title: json['title'] as String?, title: json['title'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
@ -25,7 +25,8 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
.toList(), .toList(),
); );
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{ Map<String, dynamic> _$RecipeModelToJson(RecipeModel instance) =>
<String, dynamic>{
'uuid': instance.uuid, 'uuid': instance.uuid,
'title': instance.title, 'title': instance.title,
'description': instance.description, 'description': instance.description,

View File

@ -12,10 +12,10 @@ abstract class RecipeServices {
factory RecipeServices(Dio dio) = _RecipeServices; factory RecipeServices(Dio dio) = _RecipeServices;
@GET(RecipeServiceConstants.listRecipe) @GET(RecipeServiceConstants.listRecipe)
Future<BaseResponse<List<Recipe>>> getAllRecipes( Future<BaseResponse<List<RecipeModel>>> getAllRecipes(
@CancelRequest() CancelToken cancelToken); @CancelRequest() CancelToken cancelToken);
@GET(RecipeServiceConstants.detailRecipe) @GET(RecipeServiceConstants.detailRecipe)
Future<BaseResponse<Recipe>> getDetailRecipe( Future<BaseResponse<RecipeModel>> getDetailRecipe(
@CancelRequest() CancelToken cancelToken, @Path("uuid") String uuid); @CancelRequest() CancelToken cancelToken, @Path("uuid") String uuid);
} }

View File

@ -19,13 +19,13 @@ class _RecipeServices implements RecipeServices {
String? baseUrl; String? baseUrl;
@override @override
Future<BaseResponse<List<Recipe>>> getAllRecipes(cancelToken) async { Future<BaseResponse<List<RecipeModel>>> getAllRecipes(cancelToken) async {
const _extra = <String, dynamic>{}; const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{}; final queryParameters = <String, dynamic>{};
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>>(
_setStreamType<BaseResponse<List<Recipe>>>(Options( _setStreamType<BaseResponse<List<RecipeModel>>>(Options(
method: 'GET', method: 'GET',
headers: _headers, headers: _headers,
extra: _extra, extra: _extra,
@ -38,17 +38,18 @@ class _RecipeServices implements RecipeServices {
cancelToken: cancelToken, cancelToken: cancelToken,
) )
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = BaseResponse<List<Recipe>>.fromJson( final value = BaseResponse<List<RecipeModel>>.fromJson(
_result.data!, _result.data!,
(json) => (json as List<dynamic>) (json) => (json as List<dynamic>)
.map<Recipe>((i) => Recipe.fromJson(i as Map<String, dynamic>)) .map<RecipeModel>(
(i) => RecipeModel.fromJson(i as Map<String, dynamic>))
.toList(), .toList(),
); );
return value; return value;
} }
@override @override
Future<BaseResponse<Recipe>> getDetailRecipe( Future<BaseResponse<RecipeModel>> getDetailRecipe(
cancelToken, cancelToken,
uuid, uuid,
) async { ) async {
@ -57,7 +58,7 @@ class _RecipeServices implements RecipeServices {
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>>(
_setStreamType<BaseResponse<Recipe>>(Options( _setStreamType<BaseResponse<RecipeModel>>(Options(
method: 'GET', method: 'GET',
headers: _headers, headers: _headers,
extra: _extra, extra: _extra,
@ -70,9 +71,9 @@ class _RecipeServices implements RecipeServices {
cancelToken: cancelToken, cancelToken: cancelToken,
) )
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = BaseResponse<Recipe>.fromJson( final value = BaseResponse<RecipeModel>.fromJson(
_result.data!, _result.data!,
(json) => Recipe.fromJson(json as Map<String, dynamic>), (json) => RecipeModel.fromJson(json as Map<String, dynamic>),
); );
return value; return value;
} }

View File

@ -0,0 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
import '../../data/remote/models/ingredient_model.dart';
@JsonSerializable()
class Recipe {
String? uuid;
String? title;
String? description;
String? image;
List<Ingredient>? ingredients;
List<String>? instructions;
int? prepTime;
int? cookTime;
int? servings;
List<String>? utensils;
Recipe({
this.uuid,
this.title,
this.description,
this.image,
this.ingredients,
this.instructions,
this.prepTime,
this.cookTime,
this.servings,
this.utensils,
});
}

View File

@ -1,7 +1,11 @@
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:snap_and_cook_mobile/data/remote/models/recipe_model.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);
Future<Either<DioError, Recipe>> fetchDetailRecipe(
CancelToken cancelToken, String uuid);
} }

View File

@ -2,21 +2,40 @@ 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 '../../../data/remote/models/recipe_model.dart';
import '../../../data/remote/services/recipe_service.dart'; import '../../../data/remote/services/recipe_service.dart';
import '../../entities/recipe.dart';
import 'recipe_interface.dart'; import 'recipe_interface.dart';
class RecipeUseCase implements RecipeInterface { class RecipeUseCase implements RecipeInterface {
final service = Get.find<RecipeServices>(); final service = Get.find<RecipeServices>();
@override @override
Future<Either<DioError, List<Recipe>>> Future<Either<DioError, List<Recipe>>> fetchRecipes(
fetchRecipes(CancelToken cancelToken) async { CancelToken cancelToken) async {
try { try {
final response = await service.getAllRecipes(cancelToken); final response = await service.getAllRecipes(cancelToken);
return Right(response.data ?? []); List<Recipe> recipes = [];
response.data?.forEach((element) {
recipes.add(element.toEntity());
});
return Right(recipes);
} on DioError catch (e) { } on DioError catch (e) {
return Left(e); return Left(e);
} catch (e) {
return Left(DioError(requestOptions: RequestOptions(path: "")));
}
}
@override
Future<Either<DioError, Recipe>> fetchDetailRecipe(
CancelToken cancelToken, String uuid) async {
try {
final response = await service.getDetailRecipe(cancelToken, uuid);
return Right(response.data?.toEntity() ?? Recipe());
} on DioError catch (e) {
return Left(e);
} catch (e) {
return Left(DioError(requestOptions: RequestOptions(path: "")));
} }
} }
} }

View File

@ -12,7 +12,7 @@ import 'utils/session/session.dart';
Future<void> init() async { Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Get.putAsync<Dio>( await Get.putAsync<Dio>(
() async => Dio().baseUrl(AppEnvironment.baseUrl).addInterceptor( () async => Dio().baseUrl(AppEnvironment.apiUrl).addInterceptor(
AuthorizationHeaderInterceptor( AuthorizationHeaderInterceptor(
onToken: () async => onToken: () async =>
await Session.get(SessionConstants.token) ?? "")), await Session.get(SessionConstants.token) ?? "")),

View File

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../components/dismissable_keyboard.dart'; import '../../components/dismissable_keyboard.dart';
import '../../components/progress_container.dart';
import '../../styles/colors.dart'; import '../../styles/colors.dart';
import 'base_view_model.dart'; import 'base_view_model.dart';
@ -39,13 +38,16 @@ abstract class BaseView<T extends BaseViewModel> extends GetView<T>{
floatingActionButton: floatingActionButton(), floatingActionButton: floatingActionButton(),
body: pageContent(context), body: pageContent(context),
bottomNavigationBar: bottomNavigationBar(), bottomNavigationBar: bottomNavigationBar(),
floatingActionButtonLocation: bottomSheet: bottomSheet(),
FloatingActionButtonLocation.centerFloat,
drawer: drawer(), drawer: drawer(),
), ),
); );
} }
Widget? bottomSheet() {
return null;
}
Color statusBarColor() { Color statusBarColor() {
return AppColors.heroWhite; return AppColors.heroWhite;
} }

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:snap_and_cook_mobile/components/recipe/recipe_item.dart';
import 'package:snap_and_cook_mobile/data/remote/models/recipe_model.dart';
import '../../../domain/entities/recipe.dart';
import '../view_model/home_view_model.dart';
class RecipeRecommendationWidget extends GetView<HomeViewModel> {
const RecipeRecommendationWidget({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 8,
primary: false,
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 8,
crossAxisCount: 2,
mainAxisSpacing: 0,
childAspectRatio: 0.75,
),
itemBuilder: (ctx, index) => GestureDetector(
onTap: () {
controller.navigateToRecipeDetail();
},
child: RecipeItem(
recipe: Recipe(),
),
),
),
);
}
double aspectRatio() {
int screenHeight = int.parse(Get.height.toStringAsFixed(0));
double aspectRatio;
if (screenHeight > 600 && screenHeight < 700) {
// 5.2 inch
aspectRatio = Get.width / (Get.height / 0.93);
} else if (screenHeight > 700 && screenHeight < 800) {
// 5.7 inch
aspectRatio = Get.width / (Get.height / 1.07);
} else if (screenHeight > 800 && screenHeight < 900) {
// 6.6 inch
aspectRatio = Get.width / (Get.height / 1.13);
} else if (screenHeight > 900) {
// > 7 inch
aspectRatio = Get.width / (Get.height / 1.09);
} else {
// < 5 inch
aspectRatio = Get.width / (Get.height / 0.85);
}
return aspectRatio;
}
}

View File

@ -1,8 +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: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 '../../base/base_view.dart';
import '../components/recipe_recommendation_widget.dart';
import '../view_model/home_view_model.dart'; import '../view_model/home_view_model.dart';
class HomeView extends BaseView<HomeViewModel> { class HomeView extends BaseView<HomeViewModel> {
@ -10,21 +12,132 @@ class HomeView extends BaseView<HomeViewModel> {
@override @override
PreferredSizeWidget? appBar(BuildContext context) { PreferredSizeWidget? appBar(BuildContext context) {
return BasicAppBar(appBarTitleText: "", centerTitle: false); return null;
}
@override
Widget? floatingActionButton() {
return FloatingActionButton(
onPressed: () {
controller.navigateToRecipeDetection();
},
backgroundColor: AppColors.copper,
child: const Icon(Icons.camera_alt),
);
}
@override
Color statusBarColor() {
return AppColors.primaryDarker;
} }
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return Column( return SingleChildScrollView(
children: [ child: Column(
const SizedBox( crossAxisAlignment: CrossAxisAlignment.start,
width: double.infinity, children: [
), Container(
Text(controller.version, style: TTCommonsTextStyles.textLg.textMedium()), width: double.infinity,
const SizedBox( color: AppColors.primary,
height: 32, padding: const EdgeInsets.symmetric(horizontal: 16.0),
), child: Column(
], children: [
SizedBox(
height: 16,
),
Row(
children: [
Text(
'Selamat Datang',
style: TTCommonsTextStyles.textLg.textRegular().copyWith(
color: AppColors.heroWhite,
),
),
const Spacer(),
IconButton(
onPressed: () {},
icon: const Icon(
Icons.settings,
color: Colors.white,
)),
],
),
SearchTextField(
controller: controller.searchController,
hintText: 'Cari Resep..',
inputType: TextInputType.text,
isOptional: true,
onSubmitted: controller.onSearchSubmitted,
),
const SizedBox(
height: 16,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12),
child: Text("Cari berdasarkan 1 bahan",
style: TTCommonsTextStyles.textLg.textMedium()),
),
_ingredientsWidget(),
const SizedBox(
height: 16,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12),
child: Row(
children: [
Text("Rekomendasi",
style: TTCommonsTextStyles.textLg.textMedium()),
const Spacer(),
Text("Lihat Semua",
style: TTCommonsTextStyles.textSm.textRegular()),
],
),
),
RecipeRecommendationWidget()
],
),
);
}
Widget _ingredientsWidget() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(
children: [
const SizedBox(width: 8),
_ingredientItem(),
_ingredientItem(),
_ingredientItem(),
const SizedBox(width: 24),
],
),
);
}
Widget _ingredientItem() {
return Padding(
padding: const EdgeInsets.only(left: 16),
child: Column(
children: [
Container(
height: 70,
width: 70,
decoration: BoxDecoration(
color: Colors.lightBlueAccent,
borderRadius: BorderRadius.circular(50),
),
),
const SizedBox(
height: 8,
),
Text('Data 1'),
],
),
); );
} }
} }

View File

@ -1,18 +1,31 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../resources/constants/session_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';
class HomeViewModel extends BaseViewModel { class HomeViewModel extends BaseViewModel {
String version = "Version 0.0.1-dev"; String version = "Version 0.0.1-dev";
TextEditingController searchController = TextEditingController();
void onSearchSubmitted(String value) {
print(value);
}
@override @override
void onReady() { void onReady() {
super.onReady(); super.onReady();
} }
void navigateToRecipeDetail() {
Get.toNamed(MainRoute.detail);
}
void navigateToRecipeDetection() {
Get.toNamed(MainRoute.detection);
}
@override @override
void onClose() {} void onClose() {}
} }

View File

@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../view_model/recipe_detail_view_model.dart';
class RecipeDetailBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RecipeDetailViewModel>(
() => RecipeDetailViewModel(),
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import '../../../styles/text_styles/tt_commons_text_styles.dart';
class FoodPrepWidget extends StatelessWidget {
const FoodPrepWidget({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_foodPrepItem('Porsi', '4'),
_foodPrepItem('Persiapan', '20 menit'),
_foodPrepItem('Memasak', '30 menit'),
],
);
}
Widget _foodPrepItem(String title, String description){
return Column(
children: [
Text(title, style: TTCommonsTextStyles.textLg.textMedium()),
const SizedBox(height: 8,),
Text(description, style: TTCommonsTextStyles.textSm.textRegular()),
],
);
}
}

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detail/components/ingredients_item.dart';
class IngredientListWidget extends StatelessWidget {
const IngredientListWidget({super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return IngredientItem(ingredient: Ingredient(
name: "Telur",
quantity: 1,
unit: "Butir"
));
},
itemCount: 5,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../../styles/colors.dart';
class IngredientItem extends StatelessWidget {
final Ingredient ingredient;
const IngredientItem({super.key, required this.ingredient});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
_circleWidget(),
const SizedBox(
width: 12,
),
Text(
'${ingredient.name} ${ingredient.quantity} ${ingredient.unit}',
style: TTCommonsTextStyles.textMd.textMedium(),
),
],
),
);
}
Widget _circleWidget() {
return Container(
width: 8.0,
height: 8.0,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.copper,
),
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../../styles/colors.dart';
class RecipeDetailDividerWidget extends StatelessWidget {
final String title;
const RecipeDetailDividerWidget({super.key, required this.title});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: AppColors.primary
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Text(title, style: TTCommonsTextStyles.textMd.textMedium().copyWith(
color: Colors.white
),),
],
),
);
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import '../../../styles/colors.dart';
import '../../../styles/text_styles/tt_commons_text_styles.dart';
class StepItem extends StatelessWidget {
final String step;
final int index;
const StepItem({super.key, required this.step, required this.index});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
_stepContainer(),
const SizedBox(
width: 12,
),
Text(
step,
style: TTCommonsTextStyles.textMd.textMedium(),
),
],
),
);
}
Widget _stepContainer() {
return Container(
width: 32.0,
height: 32.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppColors.copper,
),
child: Center(
child: Text(
'${index + 1}',
style: TTCommonsTextStyles.textMd
.textMedium()
.copyWith(color: Colors.white),
),
),
);
}
}

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detail/components/ingredients_item.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detail/components/step_item.dart';
class StepListWidget extends StatelessWidget {
const StepListWidget({super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return StepItem(step: "Panaskan minyak goreng", index: index);
},
itemCount: 5,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
);
}
}

View File

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/components/basic_button.dart';
import 'package:snap_and_cook_mobile/components/image/basic_network_image.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detail/components/ingredient_list_widget.dart';
import 'package:snap_and_cook_mobile/styles/colors.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../../components/appbar/basic_appbar.dart';
import '../../base/base_view.dart';
import '../components/food_prep_widget.dart';
import '../components/recipe_detail_divider_widget.dart';
import '../components/step_list_widget.dart';
import '../view_model/recipe_detail_view_model.dart';
class RecipeDetailView extends BaseView<RecipeDetailViewModel> {
const RecipeDetailView({super.key});
@override
PreferredSizeWidget? appBar(BuildContext context) {
return BasicAppBar(appBarTitleText: "", centerTitle: false, leadingIconData: Icons.arrow_back,);
}
@override
Widget body(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24,),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text('Nasi Goreng Dadakan', style: TTCommonsTextStyles.textXl.textMedium()),
),
SizedBox(
height: 200,
width: double.infinity,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(16)),
child: BasicNetworkImage(imageUrl: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT80b6egSM9UngjcWwCu92vjmfRQux7WcZCMQ&usqp=CAU')),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text('Orak arik telur buncis adalah hidangan lezat yang cocok sebagai lauk makan siang atau makan malam.', style: TTCommonsTextStyles.textMd.textRegular()),
),
Divider(
color: Colors.grey,
),
FoodPrepWidget(),
RecipeDetailDividerWidget(title: 'Bahan',),
IngredientListWidget(),
RecipeDetailDividerWidget(title: 'Alat Memasak',),
RecipeDetailDividerWidget(title: 'Langkah-langkah',),
StepListWidget(),
SizedBox(height: 24,),
BasicButton(onPress: (){},
bgColor: AppColors.copper,
text: 'Mulai Memasak'),
SizedBox(height: 24,),
],
),
),
);
}
}

View File

@ -0,0 +1,16 @@
import 'package:get/get.dart';
import '../../../routes/routes/main_route.dart';
import '../../base/base_view_model.dart';
class RecipeDetailViewModel extends BaseViewModel {
@override
void onReady() {
super.onReady();
}
@override
void onClose() {}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detection/view_model/recipe_detection_view_model.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
class DetectionItemWidget extends GetView<RecipeDetectionViewModel> {
final Ingredient ingredient;
final int index;
const DetectionItemWidget(
{super.key, required this.ingredient, required this.index});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey[200],
),
child: Row(
children: [
IconButton(onPressed: () {
controller.removeIngredient(index);
},
splashColor: Colors.transparent,
icon: const Icon(Icons.close)),
const SizedBox(
width: 4,
),
Text(
ingredient.name ?? '',
style: TTCommonsTextStyles.textMd.textMedium(),
),
const Spacer(),
Row(
children: [
IconButton(
onPressed: () {
controller.incrementIngredientQuantity(index);
},
splashColor: Colors.transparent,
icon: const Icon(Icons.add_circle_outline)),
Text(
'${ingredient.quantity}',
style: TTCommonsTextStyles.textMd.textMedium(),
),
IconButton(
onPressed: () {
controller.decrementIngredientQuantity(index);
},
splashColor: Colors.transparent,
icon: const Icon(Icons.remove_circle_outline)),
],
),
],
),
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:snap_and_cook_mobile/components/basic_button.dart';
import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detection/view_model/recipe_detection_view_model.dart';
import '../../../styles/text_styles/tt_commons_text_styles.dart';
import 'detection_item_widget.dart';
class DetectionResultWidget extends GetView<RecipeDetectionViewModel> {
const DetectionResultWidget({super.key});
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
expand: false,
controller: controller.draggableScrollableController,
minChildSize: 0.1,
maxChildSize: 0.85,
builder: (context, controller) {
return SizedBox(
width: Get.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Center(
child: Container(
width: 50,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[500],
borderRadius: BorderRadius.circular(10),
),
),
),
Expanded(
child: SingleChildScrollView(
controller: controller,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Bahan yang terdeteksi:', style: TTCommonsTextStyles.textMd.textBold(),),
),
Obx(() => ListView.builder(
itemCount: this.controller.detectedIngredients.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
Ingredient ingredient =
this.controller.detectedIngredients[index];
return DetectionItemWidget(
index: index, ingredient: ingredient);
},
)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24),
child: Row(
children: [
Expanded(
child: BasicButton(
onPress: this.controller.pickImage,
text: "Foto Lagi",
leadingIcon: Icon(Icons.camera_alt, color: Colors.white),
isLeading: true,
),
),
SizedBox(width: 8),
Expanded(
child: BasicButton(
onPress: this.controller.navigateToRecipeDetectionResult,
text: "Cari Resep",
leadingIcon: const Icon(Icons.search, color: Colors.white),
isLeading: true,
),
),
],
),
),
],
),
),
),
],
),
);
},
);
}
}

View File

@ -1,9 +1,12 @@
import 'package:flutter/cupertino.dart';
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/components/basic_button.dart';
import 'package:snap_and_cook_mobile/styles/colors.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../../components/appbar/basic_appbar.dart'; import '../../../components/appbar/basic_appbar.dart';
import '../../base/base_view.dart'; import '../../base/base_view.dart';
import '../components/detection_result_widget.dart';
import '../view_model/recipe_detection_view_model.dart'; import '../view_model/recipe_detection_view_model.dart';
class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> { class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> {
@ -11,73 +14,85 @@ class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> {
@override @override
PreferredSizeWidget? appBar(BuildContext context) { PreferredSizeWidget? appBar(BuildContext context) {
return BasicAppBar(appBarTitleText: "Detection", centerTitle: true); return BasicAppBar(
appBarTitleText: "Deteksi Bahan",
centerTitle: true,
leadingIconData: Icons.arrow_back,
);
}
@override
Color backgroundColor() {
return AppColors.canvas;
}
@override
Widget? bottomSheet() {
return _detectionResultWidget();
} }
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return Column( return Obx(() {
children: [ if (controller.isShowDetectionResult.value) {
Expanded( return _detection(context);
child: _detection(context)), }
TextButton(
onPressed: controller.pickImage, return _idleDetectionWidget();
child: const Text("ambil gmbr serah"), });
) }
],
); Widget _idleDetectionWidget() {
/// Rounded corner container
return Center(
child: Container(
width: Get.width * 0.8,
height: Get.height * 0.3,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.camera_alt_outlined, size: 50),
const SizedBox(height: 16),
Text(
"Ambil gambar bahan makanan yang kamu miliki, aplikasi akan memberikan rekomendasi resep yang dapat kamu buat.",
textAlign: TextAlign.center,
style: TTCommonsTextStyles.textSm.textRegular(),
),
const SizedBox(height: 16),
BasicButton(
onPress: controller.pickImage, height: 42, text: "Ambil Gambar")
],
),
));
}
Widget _detectionResultWidget() {
return Obx(() {
return Visibility(
visible: controller.isShowDetectionResult.value,
child: const DetectionResultWidget(),
);
});
} }
Widget _detection(BuildContext context) { Widget _detection(BuildContext context) {
return Stack( return Obx(() {
fit: StackFit.expand, if (controller.imageBytes.value != null) {
children: [ return SizedBox(
Obx(() { width: Get.width,
if (controller.imageBytes.value != null) { child: Image.memory(
return Image.memory(controller.imageBytes.value!, fit: BoxFit.contain,); controller.imageBytes.value!,
} else { fit: BoxFit.contain,
return const SizedBox();
}
}),
// ...displayBoxesAroundRecognizedObjects(MediaQuery.of(context).size),
],
);
}
List<Widget> displayBoxesAroundRecognizedObjects(Size screen) {
if (controller.modelResults.isEmpty) return [];
double factorX = screen.width / (controller.imageWidth.value);
double imgRatio =
controller.imageWidth.value / controller.imageHeight.value;
double newWidth = controller.imageWidth.value * factorX;
double newHeight = newWidth / imgRatio;
double factorY = newHeight / (controller.imageHeight.value);
double pady = (screen.height - newHeight) / 2;
Color colorPick = const Color.fromARGB(255, 50, 233, 30);
return controller.modelResults.map((result) {
return Positioned(
left: result["box"][0] * factorX,
top: result["box"][1] * factorY + pady,
width: (result["box"][2] - result["box"][0]) * factorX,
height: (result["box"][3] - result["box"][1]) * factorY,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
border: Border.all(color: Colors.pink, width: 2.0),
), ),
child: Text( );
"${result['tag']} ${(result['box'][4] * 100).toStringAsFixed(0)}%", } else {
style: TextStyle( return const SizedBox();
background: Paint()..color = colorPick, }
color: Colors.white, });
fontSize: 12.0,
),
),
),
);
}).toList();
} }
} }

View File

@ -1,13 +1,17 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_vision/flutter_vision.dart'; import 'package:flutter_vision/flutter_vision.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; 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 '../../../components/camera/custom_camera.dart';
import '../../../utils/helper/detection_helper.dart';
import '../../base/base_view_model.dart'; import '../../base/base_view_model.dart';
class RecipeDetectionViewModel extends BaseViewModel { class RecipeDetectionViewModel extends BaseViewModel {
@ -18,7 +22,14 @@ class RecipeDetectionViewModel extends BaseViewModel {
RxInt imageWidth = RxInt(1); RxInt imageWidth = RxInt(1);
RxBool isLoadingModel = RxBool(false); RxBool isLoadingModel = RxBool(false);
RxBool isProcessingModel = RxBool(false); RxBool isProcessingModel = RxBool(false);
RxBool isShowDetectionResult = RxBool(false);
RxList<Ingredient> detectedIngredients = RxList();
final Stopwatch _stopwatch = Stopwatch();
Timer? _timer;
Rxn<Uint8List> imageBytes = Rxn<Uint8List>(); Rxn<Uint8List> imageBytes = Rxn<Uint8List>();
DraggableScrollableController draggableScrollableController =
DraggableScrollableController();
@override @override
void onInit() { void onInit() {
@ -27,29 +38,45 @@ class RecipeDetectionViewModel extends BaseViewModel {
} }
Future<void> _loadMachineLearningModel() async { Future<void> _loadMachineLearningModel() async {
isLoadingModel.value = true; 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',
// modelPath: 'assets/wicaratanganV2.tflite',
modelVersion: "yolov8", modelVersion: "yolov8",
quantization: false, quantization: false,
numThreads: 2, numThreads: 2,
useGpu: true, useGpu: true,
); );
isLoadingModel.value = false; hideLoadingContainer();
} }
Future<void> pickImage() async { Future<void> pickImage() async {
final ImagePicker picker = ImagePicker(); File? data = await Navigator.of(Get.context!).push(
final XFile? photo = await picker.pickImage(source: ImageSource.gallery); MaterialPageRoute<File>(
if (photo != null) { builder: (BuildContext context) =>
imageFile.value = File(photo.path); const CustomCameraWidget(compressionQuality: 80),
),
);
if (data != null) {
imageFile.value = data;
_startTimer();
_detectIngredients(); _detectIngredients();
} }
} }
void _startTimer() {
_stopwatch.start();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
print("${_stopwatch.elapsed.inSeconds} seconds");
});
}
void _detectIngredients() async { void _detectIngredients() async {
modelResults.clear(); modelResults.clear();
Uint8List byte = await imageFile.value!.readAsBytes(); Uint8List byte = await imageFile.value!.readAsBytes();
final image = await decodeImageFromList(byte); final image = await decodeImageFromList(byte);
imageHeight.value = image.height; imageHeight.value = image.height;
@ -64,13 +91,51 @@ class RecipeDetectionViewModel extends BaseViewModel {
confThreshold: 0.2, confThreshold: 0.2,
classThreshold: 0.3, classThreshold: 0.3,
); );
closeLoadingDialog();
if (result.isNotEmpty) { if (result.isNotEmpty) {
modelResults.value = result; modelResults.value = result;
imageBytes.value = await drawOnImage( modelResults); imageBytes.value = await drawOnImage(modelResults);
closeLoadingDialog();
_showDraggableBottomSheet();
_timer?.cancel();
_stopwatch.stop();
_stopwatch.reset();
} else {
_timer?.cancel();
_stopwatch.stop();
_stopwatch.reset();
closeLoadingDialog();
showGeneralDialog(context: Get.context!, pageBuilder: (context, anim1, anim2) {
return AlertDialog(
title: const Text("Tidak ada bahan yang terdeteksi"),
content: const Text("Silahkan coba lagi"),
actions: [
TextButton(onPressed: () {
Get.back();
}, child: const Text("OK"))
],
);
});
} }
} }
void incrementIngredientQuantity(int index) {
detectedIngredients[index].quantity =
(detectedIngredients[index].quantity ?? 0) + 1;
detectedIngredients.refresh();
}
void decrementIngredientQuantity(int index) {
if (detectedIngredients[index].quantity == 1) return;
detectedIngredients[index].quantity =
(detectedIngredients[index].quantity ?? 0) - 1;
detectedIngredients.refresh();
}
void removeIngredient(int index) {
detectedIngredients.removeAt(index);
detectedIngredients.refresh();
}
Future<Uint8List> drawOnImage(List<Map<String, dynamic>> modelResults) async { Future<Uint8List> drawOnImage(List<Map<String, dynamic>> modelResults) async {
final image = imageFile.value; final image = imageFile.value;
if (image == null) { if (image == null) {
@ -83,70 +148,39 @@ class RecipeDetectionViewModel extends BaseViewModel {
final recorder = PictureRecorder(); final recorder = PictureRecorder();
final canvas = Canvas(recorder); final canvas = Canvas(recorder);
canvas.drawImage(img, Offset.zero, Paint()); canvas.drawImage(img, Offset.zero, Paint());
drawBoxesOnCanvas(canvas, Size(img.width.toDouble(), img.height.toDouble()), modelResults); List<Ingredient> detectedObject =
drawBoxesOnCanvasAndReturnDetectedIngredient(
canvas: canvas,
screen: Size(img.width.toDouble(), img.height.toDouble()),
modelResults: modelResults,
imageHeight: imageHeight.value,
imageWidth: imageWidth.value,
);
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);
final ByteData? byteData = await imgWithBoxes.toByteData(format: ImageByteFormat.png); final ByteData? byteData =
await imgWithBoxes.toByteData(format: ImageByteFormat.png);
return byteData!.buffer.asUint8List(); return byteData!.buffer.asUint8List();
} }
void drawBoxesOnCanvas(Canvas canvas, Size screen, List<Map<String, dynamic>> modelResults) { void _showDraggableBottomSheet() {
if (modelResults.isEmpty) return; isShowDetectionResult.value = true;
if (!draggableScrollableController.isAttached) return;
double factorX = screen.width / imageWidth.value; draggableScrollableController.animateTo(
double imgRatio = imageWidth.value / imageHeight.value; 0.5,
double newWidth = imageWidth.value * factorX; duration: const Duration(milliseconds: 200),
double newHeight = newWidth / imgRatio; curve: Curves.easeOut,
double factorY = newHeight / imageHeight.value;
double pady = (screen.height - newHeight) / 2;
Color colorPick = const Color.fromARGB(255, 50, 233, 30);
final Paint boxPaint = Paint()
..color = Colors.pink
..style = PaintingStyle.stroke
..strokeWidth = 4.0;
final Paint textBackgroundPaint = Paint()
..color = colorPick;
const TextStyle textStyle = TextStyle(
color: Colors.white,
fontSize: 28.0,
); );
}
for (Map<String, dynamic> result in modelResults) { void navigateToRecipeDetectionResult() {
double left = result["box"][0] * factorX; Get.toNamed(MainRoute.detectionResult, arguments: {
double top = result["box"][1] * factorY + pady; ArgumentConstants.ingredients: detectedIngredients.value,
double width = (result["box"][2] - result["box"][0]) * factorX; });
double height = (result["box"][3] - result["box"][1]) * factorY;
Rect rect = Rect.fromLTWH(left, top, width, height);
canvas.drawRect(rect, boxPaint);
TextSpan textSpan = TextSpan(
text: "${result['tag']} ${(result['box'][4] * 100).toStringAsFixed(0)}%",
style: textStyle,
);
TextPainter textPainter = TextPainter(
text: textSpan,
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textPainter.layout();
Offset textOffset = Offset(left, top - textPainter.height - 2.0);
Rect textBackgroundRect = Rect.fromPoints(
Offset(textOffset.dx, textOffset.dy),
Offset(textOffset.dx + textPainter.width, textOffset.dy + textPainter.height),
);
canvas.drawRect(textBackgroundRect, textBackgroundPaint);
textPainter.paint(canvas, textOffset);
}
} }
@override @override

View File

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

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/data/remote/models/ingredient_model.dart';
import 'package:snap_and_cook_mobile/styles/colors.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
class DetectedIngredientItem extends StatelessWidget {
final Ingredient ingredient;
const DetectedIngredientItem({super.key, required this.ingredient});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
width: 1.5,
color: AppColors.copper),
),
child: Text(
'${ingredient.name} ${ingredient.quantity} ${ingredient.unit}',
style: TTCommonsTextStyles.textMd.textMedium().copyWith(
color: AppColors.copper,
),
),
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detection_result/components/detected_ingredient_item.dart';
import '../../../data/remote/models/ingredient_model.dart';
class DetectedIngredientList extends StatelessWidget {
final List<Ingredient> ingredient;
const DetectedIngredientList({super.key, required this.ingredient});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: ListView.builder(
itemBuilder: (context, index) {
return DetectedIngredientItem(ingredient: ingredient[index]);
},
scrollDirection: Axis.horizontal,
itemCount: ingredient.length,
shrinkWrap: true,
physics: const BouncingScrollPhysics()),
);
}
}

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../components/recipe/recipe_full_item.dart';
import '../../../data/remote/models/recipe_model.dart';
import '../../../domain/entities/recipe.dart';
import '../view_model/recipe_detection_result_view_model.dart';
class RecipeResultList extends GetView<RecipeDetectionResultViewModel> {
const RecipeResultList({super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return RecipeFullItem(
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,
);
},
itemCount: 4,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics());
}
}

View File

@ -0,0 +1,48 @@
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 '../../base/base_view.dart';
import '../components/detected_ingredients_list.dart';
import '../components/recipe_result_list.dart';
import '../view_model/recipe_detection_result_view_model.dart';
class RecipeDetectionResultView
extends BaseView<RecipeDetectionResultViewModel> {
const RecipeDetectionResultView({super.key});
@override
PreferredSizeWidget? appBar(BuildContext context) {
return BasicAppBar(
appBarTitleText: "Hasil Deteksi",
centerTitle: false,
leadingIconData: Icons.arrow_back,
);
}
@override
Widget body(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 24),
Text('Bahan yang kamu punya',
style: TTCommonsTextStyles.textLg.textMedium()),
const SizedBox(height: 16),
DetectedIngredientList(
ingredient: controller.ingredients,
),
const SizedBox(height: 24),
Text('Rekomendasi Resep',
style: TTCommonsTextStyles.textLg.textMedium()),
const SizedBox(height: 16),
RecipeResultList(),
],
),
),
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:get/get.dart';
import '../../../data/remote/models/ingredient_model.dart';
import '../../../resources/arguments/argument_constants.dart';
import '../../../routes/routes/main_route.dart';
import '../../base/base_view_model.dart';
class RecipeDetectionResultViewModel extends BaseViewModel {
final _argument = Get.arguments as Map<String, dynamic>;
final List<Ingredient> ingredients = [];
@override
void onInit() {
super.onInit();
ingredients.addAll(_argument[ArgumentConstants.ingredients] as List<Ingredient>);
}
void navigateToRecipeDetail(String uuid){
Get.toNamed(MainRoute.detail, arguments: {
ArgumentConstants.recipeUuid: uuid
});
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {}
}

View File

@ -17,6 +17,6 @@ class SplashViewModel extends BaseViewModel {
Future<void> _startSplash() async { Future<void> _startSplash() async {
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));
Get.offNamed(MainRoute.detection); Get.offNamed(MainRoute.home);
} }
} }

View File

@ -1,11 +1,6 @@
class ArgumentConstants { class ArgumentConstants {
static const String phoneNumber = "phone_number_args"; static const String receivedFile = "received_file_args";
static const String forgotPasswordCode = "forgot_password_code_args"; static const String ingredients = "ingredients_args";
static const String recipeUuid = "recipe_uuid_args";
static const String isPhoneNumberNotRegistered = "is_phone_number_not_registered";
static const String otpCalledFrom = "otp_called_from_args";
//Success Screen
static const String successContentType = "success_screen_args";
} }

View File

@ -2,18 +2,5 @@
class EnvironmentConstant { class EnvironmentConstant {
static const baseUrl = "BASE_URL"; static const baseUrl = "BASE_URL";
static const imageUrl = "IMAGE_URL";
static const firebaseApiKeyAndroid = "FIREBASE_API_KEY_ANDROID";
static const firebaseAppIdAndroid = "FIREBASE_APP_ID_ANDROID";
static const firebaseMessagingSenderIdAndroid = "FIREBASE_MESSAGING_SENDER_ID_ANDROID";
static const firebaseProjectIdAndroid = "FIREBASE_PROJECT_ID_ANDROID";
static const firebaseStrokeBucketAndroid = "FIREBASE_STORE_BUCKET_ANDROID";
static const firebaseApiKeyIos = "FIREBASE_API_KEY_IOS";
static const firebaseAppIdIos = "FIREBASE_APP_ID_IOS";
static const firebaseMessagingSenderIdIos = "FIREBASE_MESSAGING_SENDER_ID_IOS";
static const firebaseProjectIdIos = "FIREBASE_PROJECT_ID_IOS";
static const firebaseStrokeBucketIos = "FIREBASE_STORE_BUCKET_IOS";
static const firebaseIosClientId = "FIREBASE_IOS_CLIENT_ID";
static const firebaseIosBundleId = "FIREBASE_IOS_BUNDLE_ID";
} }

View File

@ -3,6 +3,11 @@ 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 '../../presentation/recipe_detail/binding/recipe_detail_binding.dart';
import '../../presentation/recipe_detail/view/recipe_detail_view.dart';
import '../../presentation/recipe_detection_result/binding/recipe_detection_result_binding.dart';
import '../../presentation/recipe_detection_result/view/recipe_detection_result_view.dart';
import '../../presentation/splash/binding/splash_binding.dart'; import '../../presentation/splash/binding/splash_binding.dart';
import '../../presentation/splash/view/splash_view.dart'; import '../../presentation/splash/view/splash_view.dart';
@ -11,6 +16,8 @@ class MainRoute {
static const splash = "/"; static const splash = "/";
static const home = "/home-page"; static const home = "/home-page";
static const detection = "/recipe-detection-page"; static const detection = "/recipe-detection-page";
static const detectionResult = "/recipe-detection-result-page";
static const detail = "/recipe-detail-page";
static final routes = [ static final routes = [
GetPage( GetPage(
@ -28,5 +35,15 @@ class MainRoute {
page: () => const RecipeDetectionView(), page: () => const RecipeDetectionView(),
binding: RecipeDetectionBinding(), binding: RecipeDetectionBinding(),
), ),
GetPage(
name: detectionResult,
page: () => const RecipeDetectionResultView(),
binding: RecipeDetectionResultBinding(),
),
GetPage(
name: detail,
page: () => const RecipeDetailView(),
binding: RecipeDetailBinding(),
),
]; ];
} }

View File

@ -0,0 +1,3 @@
class AppAnimations {
static const scan = "assets/animations/scan_animation.json";
}

View File

@ -3,9 +3,16 @@
import 'dart:ui'; import 'dart:ui';
class AppColors { class AppColors {
static const Color transparent = Color(0x00f5f5f5);
static const Color copper = Color(0xFFE48364);
static const Color canvas = Color(0xFFF5F5F5);
static const Color heroWhite = Color(0xFFFFFFFF); static const Color heroWhite = Color(0xFFFFFFFF);
static const Color heroBlack = Color(0xFF14130E); static const Color heroBlack = Color(0xFF14130E);
static const Color primary = Color(0xFFAED6d0);
static const Color primaryDarker = Color(0xFF6C9790);
static const Color primaryLightGrey = Color(0xFFCCD2D9); static const Color primaryLightGrey = Color(0xFFCCD2D9);
static const Color primaryLightGrey100 = Color(0xFFCCD2D9); static const Color primaryLightGrey100 = Color(0xFFCCD2D9);
static const Color primaryLightGrey200 = Color(0xFFEBEDF0); static const Color primaryLightGrey200 = Color(0xFFEBEDF0);

View File

@ -5,6 +5,9 @@ import '../colors.dart';
class FormBorderSide { class FormBorderSide {
// Border Side // Border Side
static BorderSide get none =>
const BorderSide(width: 0, color: AppColors.transparent);
static BorderSide get sideNormal => static BorderSide get sideNormal =>
const BorderSide(width: 1, color: AppColors.primaryGrey300); const BorderSide(width: 1, color: AppColors.primaryGrey300);
static BorderSide get sideInactive => static BorderSide get sideInactive =>
@ -18,4 +21,5 @@ class FormBorderSide {
static BorderSide get sideSuccess => static BorderSide get sideSuccess =>
const BorderSide(width: 1, color: AppColors.semanticGreen500); const BorderSide(width: 1, color: AppColors.semanticGreen500);
} }

View File

@ -21,6 +21,14 @@ class AppTheme {
textTheme: TextTheme( textTheme: TextTheme(
displaySmall: TTCommonsTextStyles.displayXs.textBold() displaySmall: TTCommonsTextStyles.displayXs.textBold()
), ),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: AppColors.heroWhite,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.0),
),
),
),
// text button theme // text button theme
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import '../../data/remote/models/ingredient_model.dart';
List<Ingredient> drawBoxesOnCanvasAndReturnDetectedIngredient(
{required Canvas canvas,
required Size screen,
required List<Map<String, dynamic>> modelResults,
required int imageWidth,
required int imageHeight}) {
if (modelResults.isEmpty) return [];
double factorX = screen.width / imageWidth;
double imgRatio = imageWidth / imageHeight;
double newWidth = imageWidth * factorX;
double newHeight = newWidth / imgRatio;
double factorY = newHeight / imageHeight;
double pady = (screen.height - newHeight) / 2;
Color colorPick = const Color.fromARGB(255, 50, 233, 30);
final Paint boxPaint = Paint()
..color = Colors.pink
..style = PaintingStyle.stroke
..strokeWidth = imageWidth * 0.006;
final Paint textBackgroundPaint = Paint()..color = colorPick;
double fontSizePercentage = imageHeight * 0.025;
TextStyle textStyle = TextStyle(
color: Colors.white,
fontSize: fontSizePercentage,
);
List<Ingredient> detectedObjects = [];
for (Map<String, dynamic> result in modelResults) {
double left = result["box"][0] * factorX;
double top = result["box"][1] * factorY + pady;
double width = (result["box"][2] - result["box"][0]) * factorX;
double height = (result["box"][3] - result["box"][1]) * factorY;
Rect rect = Rect.fromLTWH(left, top, width, height);
canvas.drawRect(rect, boxPaint);
/// Add detected object to list and check if the item is exist then increase the quantity
if (detectedObjects.isEmpty){
detectedObjects.add(Ingredient(
name: "${result['tag']}",
quantity: 1,
));
} else {
bool isExist = false;
for (int i = 0; i < detectedObjects.length; i++) {
if (detectedObjects[i].name == result['tag']) {
detectedObjects[i].quantity = detectedObjects[i].quantity! + 1;
isExist = true;
break;
}
}
if (!isExist) {
detectedObjects.add(Ingredient(
name: "${result['tag']}",
quantity: 1,
));
}
}
TextSpan textSpan = TextSpan(
text: "${result['tag']} ${(result['box'][4] * 100).toStringAsFixed(0)}%",
style: textStyle,
);
TextPainter textPainter = TextPainter(
text: textSpan,
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textPainter.layout();
Offset textOffset = Offset(left, top - textPainter.height - 2.0);
Rect textBackgroundRect = Rect.fromPoints(
Offset(textOffset.dx, textOffset.dy),
Offset(textOffset.dx + textPainter.width,
textOffset.dy + textPainter.height),
);
canvas.drawRect(textBackgroundRect, textBackgroundPaint);
textPainter.paint(canvas, textOffset);
}
return detectedObjects;
}

View File

@ -17,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "5.13.0"
archive:
dependency: transitive
description:
name: archive
sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
url: "https://pub.dev"
source: hosted
version: "3.4.9"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -105,6 +113,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.8.0" version: "8.8.0"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
url: "https://pub.dev"
source: hosted
version: "3.3.0"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
camera: camera:
dependency: "direct main" dependency: "direct main"
description: description:
@ -310,6 +342,22 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
flutter_image_compress: flutter_image_compress:
dependency: "direct main" dependency: "direct main"
description: description:
@ -432,14 +480,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image_gallery_saver:
dependency: "direct main"
description:
name: image_gallery_saver
sha256: be812580c7a320d3bf583af89cac6b376f170d48000aca75215a73285a3223a0
url: "https://pub.dev"
source: hosted
version: "1.7.1"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -560,6 +600,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
lottie:
dependency: "direct main"
description:
name: lottie
sha256: "14b489bf93af84727a07c87c73580a9413f9cbffa4ea2342f3f71447a674a3fb"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -592,6 +640,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@ -736,6 +792,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.7" version: "2.1.7"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
url: "https://pub.dev"
source: hosted
version: "3.7.3"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -784,6 +848,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.0" version: "4.2.0"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
url: "https://pub.dev"
source: hosted
version: "0.27.7"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -856,6 +928,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
shimmer:
dependency: "direct main"
description:
name: shimmer
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -885,6 +965,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6
url: "https://pub.dev"
source: hosted
version: "2.5.0+2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -917,6 +1021,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -957,6 +1069,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
uuid:
dependency: transitive
description:
name: uuid
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
vector_graphics: vector_graphics:
dependency: transitive dependency: transitive
description: description:

View File

@ -46,11 +46,15 @@ dependencies:
flutter_screenutil: ^5.6.1 flutter_screenutil: ^5.6.1
get: ^4.6.5 get: ^4.6.5
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
image_gallery_saver: ^1.7.1
photo_gallery: ^1.1.1 photo_gallery: ^1.1.1
flutter_image_compress: ^1.1.0 flutter_image_compress: ^1.1.0
camera: ^0.10.3+1 camera: ^0.10.3+1
permission_handler: ^10.2.0 permission_handler: ^10.2.0
lottie: 2.3.1
cached_network_image: ^3.2.3
shimmer:
flutter_dotenv: ^5.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -84,10 +88,12 @@ flutter:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
assets: assets:
- assets/yolov8m_float16.tflite - production/
- assets/labels.txt - staging/
- assets/
- assets/images/ - assets/images/
- assets/fonts/ - assets/fonts/
- assets/animations/
fonts: fonts:
- family: TTCommons - family: TTCommons