Update 14 jan
This commit is contained in:
parent
21a18461ea
commit
c542883bec
|
@ -16,6 +16,9 @@ migrate_working_dir/
|
|||
*.iws
|
||||
.idea/
|
||||
|
||||
production/.env
|
||||
staging/.env
|
||||
|
||||
# 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
|
||||
# is commented out by default.
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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
|
|
@ -35,14 +35,14 @@ class BasicButton extends StatelessWidget {
|
|||
this.isTransparent = false,
|
||||
this.isBorder = false,
|
||||
this.isLeading = false,
|
||||
this.borderRadius = 0,
|
||||
this.bgColor = AppColors.heroBlack,
|
||||
this.borderRadius = 14,
|
||||
this.bgColor = AppColors.primary,
|
||||
this.textColor = AppColors.heroWhite,
|
||||
this.borderColor = AppColors.heroBlack,
|
||||
this.height = 60,
|
||||
this.sizeText = 18,
|
||||
this.borderColor = AppColors.primary,
|
||||
this.height = 42,
|
||||
this.sizeText = 16,
|
||||
this.leadingIcon,
|
||||
this.fontWeight = FontWeight.w600,
|
||||
this.fontWeight = FontWeight.w500,
|
||||
this.disableColor = AppColors.primaryGrey100,
|
||||
this.loadingColor = AppColors.heroWhite, this.viewKey,
|
||||
}) : super(key: key);
|
||||
|
@ -61,19 +61,16 @@ class BasicButton extends StatelessWidget {
|
|||
style: buttonStyle(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
basicButtonLeading(),
|
||||
basicButtonOnProgress(),
|
||||
basicButtonText(),
|
||||
Expanded(child: basicButtonText()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -83,30 +80,6 @@ class BasicButton extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,26 +15,20 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:photo_gallery/photo_gallery.dart';
|
||||
|
||||
class CameraApp extends StatefulWidget {
|
||||
class CustomCameraWidget extends StatefulWidget {
|
||||
final int? compressionQuality;
|
||||
|
||||
const CameraApp({Key? key, this.compressionQuality = 100}) : super(key: key);
|
||||
const CustomCameraWidget({Key? key, this.compressionQuality = 100}) : super(key: key);
|
||||
|
||||
@override
|
||||
CameraAppState createState() => CameraAppState();
|
||||
CustomCameraWidgetState createState() => CustomCameraWidgetState();
|
||||
}
|
||||
|
||||
class CameraAppState extends State<CameraApp> {
|
||||
class CustomCameraWidgetState extends State<CustomCameraWidget> {
|
||||
CameraController? controller;
|
||||
late List<CameraDescription> cameras;
|
||||
List<Album> imageAlbums = [];
|
||||
Set<Medium> imageMedium = {};
|
||||
Uint8List? bytes;
|
||||
List<File> results = [];
|
||||
List<int> indexList = [];
|
||||
bool flashOn = false;
|
||||
bool showPerformance = false;
|
||||
late double width;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -124,7 +118,8 @@ class CameraAppState extends State<CameraApp> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_galleryButton(),
|
||||
_cameraButton()
|
||||
_cameraButton(),
|
||||
SizedBox(width: 25,)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -150,7 +145,7 @@ class CameraAppState extends State<CameraApp> {
|
|||
size: 30, color: Colors.white)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
compress([]);
|
||||
compress(null);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.close_rounded,
|
||||
|
@ -177,7 +172,7 @@ class CameraAppState extends State<CameraApp> {
|
|||
return;
|
||||
}
|
||||
File file = File(image.path);
|
||||
compress([file]);
|
||||
compress(file);
|
||||
},
|
||||
icon: const Icon(Icons.photo_library,
|
||||
size: 30, color: Colors.white),
|
||||
|
@ -189,7 +184,7 @@ class CameraAppState extends State<CameraApp> {
|
|||
onTap: () async {
|
||||
XFile file2 = await controller!.takePicture();
|
||||
File file = File(file2.path);
|
||||
compress([file]);
|
||||
compress(file);
|
||||
},
|
||||
child: Container(
|
||||
width: 75,
|
||||
|
@ -212,18 +207,19 @@ class CameraAppState extends State<CameraApp> {
|
|||
});
|
||||
}
|
||||
|
||||
void compress(List<File> files) async {
|
||||
List<File> files2 = [];
|
||||
for (File file in files) {
|
||||
Uint8List? blobBytes = await compressFile(file);
|
||||
void compress(File? file) async {
|
||||
if (file == null) {
|
||||
Navigator.of(context).pop(null);
|
||||
}
|
||||
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.add(fileNew);
|
||||
}
|
||||
files2 = fileNew;
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop(files2);
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,23 @@
|
|||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
import '../data/enums/environment_enum.dart';
|
||||
import '../resources/constants/environtment_constant.dart';
|
||||
import 'app_build_config.dart';
|
||||
|
||||
class AppEnvironment {
|
||||
static Map<String, String> get env => <String, String>{
|
||||
EnvironmentConstant.baseUrl: const String.fromEnvironment(
|
||||
EnvironmentConstant.baseUrl,
|
||||
defaultValue: ""),
|
||||
};
|
||||
static load() async {
|
||||
if (AppBuildConfig.instance.config == BuildConfigEnum.production) {
|
||||
await dotenv.load(fileName: "production/.env");
|
||||
} 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] ?? '';
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../configuration/app_environtment.dart';
|
||||
import '../../../domain/entities/recipe.dart';
|
||||
import 'ingredient_model.dart';
|
||||
|
||||
part 'recipe_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Recipe {
|
||||
class RecipeModel {
|
||||
String? uuid;
|
||||
String? title;
|
||||
String? description;
|
||||
|
@ -17,7 +19,7 @@ class Recipe {
|
|||
int? servings;
|
||||
List<String>? utensils;
|
||||
|
||||
Recipe({
|
||||
RecipeModel({
|
||||
this.uuid,
|
||||
this.title,
|
||||
this.description,
|
||||
|
@ -30,8 +32,23 @@ class Recipe {
|
|||
this.utensils,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => _$RecipeToJson(this);
|
||||
Map<String, dynamic> toJson() => _$RecipeModelToJson(this);
|
||||
|
||||
factory Recipe.fromJson(Map<String, dynamic> json) =>
|
||||
_$RecipeFromJson(json);
|
||||
factory RecipeModel.fromJson(Map<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'recipe_model.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
||||
RecipeModel _$RecipeModelFromJson(Map<String, dynamic> json) => RecipeModel(
|
||||
uuid: json['uuid'] as String?,
|
||||
title: json['title'] as String?,
|
||||
description: json['description'] as String?,
|
||||
|
@ -25,7 +25,8 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
|||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
||||
Map<String, dynamic> _$RecipeModelToJson(RecipeModel instance) =>
|
||||
<String, dynamic>{
|
||||
'uuid': instance.uuid,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
|
|
|
@ -12,10 +12,10 @@ abstract class RecipeServices {
|
|||
factory RecipeServices(Dio dio) = _RecipeServices;
|
||||
|
||||
@GET(RecipeServiceConstants.listRecipe)
|
||||
Future<BaseResponse<List<Recipe>>> getAllRecipes(
|
||||
Future<BaseResponse<List<RecipeModel>>> getAllRecipes(
|
||||
@CancelRequest() CancelToken cancelToken);
|
||||
|
||||
@GET(RecipeServiceConstants.detailRecipe)
|
||||
Future<BaseResponse<Recipe>> getDetailRecipe(
|
||||
Future<BaseResponse<RecipeModel>> getDetailRecipe(
|
||||
@CancelRequest() CancelToken cancelToken, @Path("uuid") String uuid);
|
||||
}
|
||||
|
|
|
@ -19,13 +19,13 @@ class _RecipeServices implements RecipeServices {
|
|||
String? baseUrl;
|
||||
|
||||
@override
|
||||
Future<BaseResponse<List<Recipe>>> getAllRecipes(cancelToken) async {
|
||||
Future<BaseResponse<List<RecipeModel>>> getAllRecipes(cancelToken) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
final _result = await _dio.fetch<Map<String, dynamic>>(
|
||||
_setStreamType<BaseResponse<List<Recipe>>>(Options(
|
||||
_setStreamType<BaseResponse<List<RecipeModel>>>(Options(
|
||||
method: 'GET',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
|
@ -38,17 +38,18 @@ class _RecipeServices implements RecipeServices {
|
|||
cancelToken: cancelToken,
|
||||
)
|
||||
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
|
||||
final value = BaseResponse<List<Recipe>>.fromJson(
|
||||
final value = BaseResponse<List<RecipeModel>>.fromJson(
|
||||
_result.data!,
|
||||
(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(),
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BaseResponse<Recipe>> getDetailRecipe(
|
||||
Future<BaseResponse<RecipeModel>> getDetailRecipe(
|
||||
cancelToken,
|
||||
uuid,
|
||||
) async {
|
||||
|
@ -57,7 +58,7 @@ class _RecipeServices implements RecipeServices {
|
|||
final _headers = <String, dynamic>{};
|
||||
final _data = <String, dynamic>{};
|
||||
final _result = await _dio.fetch<Map<String, dynamic>>(
|
||||
_setStreamType<BaseResponse<Recipe>>(Options(
|
||||
_setStreamType<BaseResponse<RecipeModel>>(Options(
|
||||
method: 'GET',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
|
@ -70,9 +71,9 @@ class _RecipeServices implements RecipeServices {
|
|||
cancelToken: cancelToken,
|
||||
)
|
||||
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
|
||||
final value = BaseResponse<Recipe>.fromJson(
|
||||
final value = BaseResponse<RecipeModel>.fromJson(
|
||||
_result.data!,
|
||||
(json) => Recipe.fromJson(json as Map<String, dynamic>),
|
||||
(json) => RecipeModel.fromJson(json as Map<String, dynamic>),
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
import 'package:dartz/dartz.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 {
|
||||
Future<Either<DioError, List<Recipe>>> fetchRecipes(CancelToken cancelToken);
|
||||
|
||||
Future<Either<DioError, Recipe>> fetchDetailRecipe(
|
||||
CancelToken cancelToken, String uuid);
|
||||
}
|
||||
|
|
|
@ -2,21 +2,40 @@ import 'package:dartz/dartz.dart';
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../data/remote/models/recipe_model.dart';
|
||||
import '../../../data/remote/services/recipe_service.dart';
|
||||
import '../../entities/recipe.dart';
|
||||
import 'recipe_interface.dart';
|
||||
|
||||
class RecipeUseCase implements RecipeInterface {
|
||||
final service = Get.find<RecipeServices>();
|
||||
|
||||
@override
|
||||
Future<Either<DioError, List<Recipe>>>
|
||||
fetchRecipes(CancelToken cancelToken) async {
|
||||
Future<Either<DioError, List<Recipe>>> fetchRecipes(
|
||||
CancelToken cancelToken) async {
|
||||
try {
|
||||
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) {
|
||||
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: "")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import 'utils/session/session.dart';
|
|||
Future<void> init() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Get.putAsync<Dio>(
|
||||
() async => Dio().baseUrl(AppEnvironment.baseUrl).addInterceptor(
|
||||
() async => Dio().baseUrl(AppEnvironment.apiUrl).addInterceptor(
|
||||
AuthorizationHeaderInterceptor(
|
||||
onToken: () async =>
|
||||
await Session.get(SessionConstants.token) ?? "")),
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
|
|||
import 'package:get/get.dart';
|
||||
|
||||
import '../../components/dismissable_keyboard.dart';
|
||||
import '../../components/progress_container.dart';
|
||||
import '../../styles/colors.dart';
|
||||
import 'base_view_model.dart';
|
||||
|
||||
|
@ -39,13 +38,16 @@ abstract class BaseView<T extends BaseViewModel> extends GetView<T>{
|
|||
floatingActionButton: floatingActionButton(),
|
||||
body: pageContent(context),
|
||||
bottomNavigationBar: bottomNavigationBar(),
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.centerFloat,
|
||||
bottomSheet: bottomSheet(),
|
||||
drawer: drawer(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? bottomSheet() {
|
||||
return null;
|
||||
}
|
||||
|
||||
Color statusBarColor() {
|
||||
return AppColors.heroWhite;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
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_recommendation_widget.dart';
|
||||
import '../view_model/home_view_model.dart';
|
||||
|
||||
class HomeView extends BaseView<HomeViewModel> {
|
||||
|
@ -10,21 +12,132 @@ class HomeView extends BaseView<HomeViewModel> {
|
|||
|
||||
@override
|
||||
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
|
||||
Widget body(BuildContext context) {
|
||||
return Column(
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: AppColors.primary,
|
||||
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,
|
||||
),
|
||||
Text(controller.version, style: TTCommonsTextStyles.textLg.textMedium()),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
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'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../resources/constants/session_constants.dart';
|
||||
import '../../../routes/routes/main_route.dart';
|
||||
import '../../../utils/session/session.dart';
|
||||
import '../../base/base_view_model.dart';
|
||||
|
||||
class HomeViewModel extends BaseViewModel {
|
||||
String version = "Version 0.0.1-dev";
|
||||
|
||||
TextEditingController searchController = TextEditingController();
|
||||
|
||||
void onSearchSubmitted(String value) {
|
||||
print(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
void navigateToRecipeDetail() {
|
||||
Get.toNamed(MainRoute.detail);
|
||||
}
|
||||
|
||||
void navigateToRecipeDetection() {
|
||||
Get.toNamed(MainRoute.detection);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
),),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
|
||||
}
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
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/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/detection_result_widget.dart';
|
||||
import '../view_model/recipe_detection_view_model.dart';
|
||||
|
||||
class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> {
|
||||
|
@ -11,73 +14,85 @@ class RecipeDetectionView extends BaseView<RecipeDetectionViewModel> {
|
|||
|
||||
@override
|
||||
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
|
||||
Widget body(BuildContext context) {
|
||||
return Column(
|
||||
return Obx(() {
|
||||
if (controller.isShowDetectionResult.value) {
|
||||
return _detection(context);
|
||||
}
|
||||
|
||||
return _idleDetectionWidget();
|
||||
});
|
||||
}
|
||||
|
||||
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: [
|
||||
Expanded(
|
||||
child: _detection(context)),
|
||||
TextButton(
|
||||
onPressed: controller.pickImage,
|
||||
child: const Text("ambil gmbr serah"),
|
||||
)
|
||||
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) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Obx(() {
|
||||
return Obx(() {
|
||||
if (controller.imageBytes.value != null) {
|
||||
return Image.memory(controller.imageBytes.value!, fit: BoxFit.contain,);
|
||||
return SizedBox(
|
||||
width: Get.width,
|
||||
child: Image.memory(
|
||||
controller.imageBytes.value!,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
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)}%",
|
||||
style: TextStyle(
|
||||
background: Paint()..color = colorPick,
|
||||
color: Colors.white,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_vision/flutter_vision.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';
|
||||
|
||||
class RecipeDetectionViewModel extends BaseViewModel {
|
||||
|
@ -18,7 +22,14 @@ class RecipeDetectionViewModel extends BaseViewModel {
|
|||
RxInt imageWidth = RxInt(1);
|
||||
RxBool isLoadingModel = 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>();
|
||||
DraggableScrollableController draggableScrollableController =
|
||||
DraggableScrollableController();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
|
@ -27,29 +38,45 @@ class RecipeDetectionViewModel extends BaseViewModel {
|
|||
}
|
||||
|
||||
Future<void> _loadMachineLearningModel() async {
|
||||
isLoadingModel.value = true;
|
||||
showLoadingContainer();
|
||||
await _vision.loadYoloModel(
|
||||
labels: 'assets/labels.txt',
|
||||
modelPath: 'assets/yolov8m_float16.tflite',
|
||||
// labels: 'assets/labels_alpha.txt',
|
||||
// modelPath: 'assets/wicaratanganV2.tflite',
|
||||
modelVersion: "yolov8",
|
||||
quantization: false,
|
||||
numThreads: 2,
|
||||
useGpu: true,
|
||||
);
|
||||
isLoadingModel.value = false;
|
||||
hideLoadingContainer();
|
||||
}
|
||||
|
||||
Future<void> pickImage() async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? photo = await picker.pickImage(source: ImageSource.gallery);
|
||||
if (photo != null) {
|
||||
imageFile.value = File(photo.path);
|
||||
File? data = await Navigator.of(Get.context!).push(
|
||||
MaterialPageRoute<File>(
|
||||
builder: (BuildContext context) =>
|
||||
const CustomCameraWidget(compressionQuality: 80),
|
||||
),
|
||||
);
|
||||
|
||||
if (data != null) {
|
||||
imageFile.value = data;
|
||||
_startTimer();
|
||||
_detectIngredients();
|
||||
}
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_stopwatch.start();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
print("${_stopwatch.elapsed.inSeconds} seconds");
|
||||
});
|
||||
}
|
||||
|
||||
void _detectIngredients() async {
|
||||
modelResults.clear();
|
||||
|
||||
Uint8List byte = await imageFile.value!.readAsBytes();
|
||||
final image = await decodeImageFromList(byte);
|
||||
imageHeight.value = image.height;
|
||||
|
@ -64,13 +91,51 @@ class RecipeDetectionViewModel extends BaseViewModel {
|
|||
confThreshold: 0.2,
|
||||
classThreshold: 0.3,
|
||||
);
|
||||
closeLoadingDialog();
|
||||
if (result.isNotEmpty) {
|
||||
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 {
|
||||
final image = imageFile.value;
|
||||
if (image == null) {
|
||||
|
@ -83,70 +148,39 @@ class RecipeDetectionViewModel extends BaseViewModel {
|
|||
final recorder = PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
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 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();
|
||||
}
|
||||
|
||||
void drawBoxesOnCanvas(Canvas canvas, Size screen, List<Map<String, dynamic>> modelResults) {
|
||||
if (modelResults.isEmpty) return;
|
||||
|
||||
double factorX = screen.width / imageWidth.value;
|
||||
double imgRatio = imageWidth.value / imageHeight.value;
|
||||
double newWidth = imageWidth.value * factorX;
|
||||
double newHeight = newWidth / imgRatio;
|
||||
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,
|
||||
void _showDraggableBottomSheet() {
|
||||
isShowDetectionResult.value = true;
|
||||
if (!draggableScrollableController.isAttached) return;
|
||||
draggableScrollableController.animateTo(
|
||||
0.5,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void navigateToRecipeDetectionResult() {
|
||||
Get.toNamed(MainRoute.detectionResult, arguments: {
|
||||
ArgumentConstants.ingredients: detectedIngredients.value,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
|
||||
}
|
|
@ -17,6 +17,6 @@ class SplashViewModel extends BaseViewModel {
|
|||
|
||||
Future<void> _startSplash() async {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
Get.offNamed(MainRoute.detection);
|
||||
Get.offNamed(MainRoute.home);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
class ArgumentConstants {
|
||||
static const String phoneNumber = "phone_number_args";
|
||||
static const String forgotPasswordCode = "forgot_password_code_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";
|
||||
static const String receivedFile = "received_file_args";
|
||||
static const String ingredients = "ingredients_args";
|
||||
static const String recipeUuid = "recipe_uuid_args";
|
||||
}
|
||||
|
||||
|
|
|
@ -2,18 +2,5 @@
|
|||
|
||||
class EnvironmentConstant {
|
||||
static const baseUrl = "BASE_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";
|
||||
static const imageUrl = "IMAGE_URL";
|
||||
}
|
|
@ -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/recipe_detection/binding/recipe_detection_binding.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/view/splash_view.dart';
|
||||
|
||||
|
@ -11,6 +16,8 @@ class MainRoute {
|
|||
static const splash = "/";
|
||||
static const home = "/home-page";
|
||||
static const detection = "/recipe-detection-page";
|
||||
static const detectionResult = "/recipe-detection-result-page";
|
||||
static const detail = "/recipe-detail-page";
|
||||
|
||||
static final routes = [
|
||||
GetPage(
|
||||
|
@ -28,5 +35,15 @@ class MainRoute {
|
|||
page: () => const RecipeDetectionView(),
|
||||
binding: RecipeDetectionBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: detectionResult,
|
||||
page: () => const RecipeDetectionResultView(),
|
||||
binding: RecipeDetectionResultBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: detail,
|
||||
page: () => const RecipeDetailView(),
|
||||
binding: RecipeDetailBinding(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
class AppAnimations {
|
||||
static const scan = "assets/animations/scan_animation.json";
|
||||
}
|
|
@ -3,9 +3,16 @@
|
|||
import 'dart:ui';
|
||||
|
||||
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 heroBlack = Color(0xFF14130E);
|
||||
|
||||
static const Color primary = Color(0xFFAED6d0);
|
||||
static const Color primaryDarker = Color(0xFF6C9790);
|
||||
|
||||
static const Color primaryLightGrey = Color(0xFFCCD2D9);
|
||||
static const Color primaryLightGrey100 = Color(0xFFCCD2D9);
|
||||
static const Color primaryLightGrey200 = Color(0xFFEBEDF0);
|
||||
|
|
|
@ -5,6 +5,9 @@ import '../colors.dart';
|
|||
class FormBorderSide {
|
||||
|
||||
// Border Side
|
||||
static BorderSide get none =>
|
||||
const BorderSide(width: 0, color: AppColors.transparent);
|
||||
|
||||
static BorderSide get sideNormal =>
|
||||
const BorderSide(width: 1, color: AppColors.primaryGrey300);
|
||||
static BorderSide get sideInactive =>
|
||||
|
@ -18,4 +21,5 @@ class FormBorderSide {
|
|||
static BorderSide get sideSuccess =>
|
||||
const BorderSide(width: 1, color: AppColors.semanticGreen500);
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,14 @@ class AppTheme {
|
|||
textTheme: TextTheme(
|
||||
displaySmall: TTCommonsTextStyles.displayXs.textBold()
|
||||
),
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
backgroundColor: AppColors.heroWhite,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// text button theme
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
|
|
|
@ -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;
|
||||
}
|
136
pubspec.lock
136
pubspec.lock
|
@ -17,6 +17,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -105,6 +113,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -310,6 +342,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -432,14 +480,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -560,6 +600,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
lottie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: lottie
|
||||
sha256: "14b489bf93af84727a07c87c73580a9413f9cbffa4ea2342f3f71447a674a3fb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -592,6 +640,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -736,6 +792,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.3"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -784,6 +848,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.27.7"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -856,6 +928,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -885,6 +965,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -917,6 +1021,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -957,6 +1069,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.2"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
12
pubspec.yaml
12
pubspec.yaml
|
@ -46,11 +46,15 @@ dependencies:
|
|||
flutter_screenutil: ^5.6.1
|
||||
get: ^4.6.5
|
||||
shared_preferences: ^2.2.2
|
||||
image_gallery_saver: ^1.7.1
|
||||
photo_gallery: ^1.1.1
|
||||
flutter_image_compress: ^1.1.0
|
||||
camera: ^0.10.3+1
|
||||
permission_handler: ^10.2.0
|
||||
lottie: 2.3.1
|
||||
cached_network_image: ^3.2.3
|
||||
shimmer:
|
||||
flutter_dotenv: ^5.0.2
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -84,10 +88,12 @@ flutter:
|
|||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
assets:
|
||||
- assets/yolov8m_float16.tflite
|
||||
- assets/labels.txt
|
||||
- production/
|
||||
- staging/
|
||||
- assets/
|
||||
- assets/images/
|
||||
- assets/fonts/
|
||||
- assets/animations/
|
||||
|
||||
fonts:
|
||||
- family: TTCommons
|
||||
|
|
Loading…
Reference in New Issue