Update 14 jan
This commit is contained in:
parent
21a18461ea
commit
c542883bec
|
@ -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
|
@ -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.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,19 +61,16 @@ 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(),
|
||||||
basicButtonOnProgress(),
|
Expanded(child: basicButtonText()),
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
}
|
||||||
|
File? files2;
|
||||||
|
Uint8List? blobBytes = await compressFile(file!);
|
||||||
var dir = await getTemporaryDirectory();
|
var dir = await getTemporaryDirectory();
|
||||||
String trimmed = dir.absolute.path;
|
String trimmed = dir.absolute.path;
|
||||||
String dateTimeString = DateTime.now().millisecondsSinceEpoch.toString();
|
String dateTimeString = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
String pathString = "$trimmed/$dateTimeString.jpg";
|
String pathString = "$trimmed/$dateTimeString.jpg";
|
||||||
File fileNew = File(pathString);
|
File fileNew = File(pathString);
|
||||||
fileNew.writeAsBytesSync(List.from(blobBytes!));
|
fileNew.writeAsBytesSync(List.from(blobBytes!));
|
||||||
files2.add(fileNew);
|
files2 = fileNew;
|
||||||
}
|
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pop(files2);
|
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 '../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 String get baseUrl => env[EnvironmentConstant.baseUrl] ?? '';
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, String> get env => dotenv.env;
|
||||||
|
|
||||||
|
static String get apiUrl => dotenv.env[EnvironmentConstant.baseUrl] ?? '';
|
||||||
|
static String get imageUrl => dotenv.env[EnvironmentConstant.imageUrl] ?? '';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: "")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) ?? "")),
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: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(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
Container(
|
||||||
width: double.infinity,
|
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(
|
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 '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() {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: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(() {
|
||||||
|
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: [
|
children: [
|
||||||
Expanded(
|
const Icon(Icons.camera_alt_outlined, size: 50),
|
||||||
child: _detection(context)),
|
const SizedBox(height: 16),
|
||||||
TextButton(
|
Text(
|
||||||
onPressed: controller.pickImage,
|
"Ambil gambar bahan makanan yang kamu miliki, aplikasi akan memberikan rekomendasi resep yang dapat kamu buat.",
|
||||||
child: const Text("ambil gmbr serah"),
|
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,
|
|
||||||
children: [
|
|
||||||
Obx(() {
|
|
||||||
if (controller.imageBytes.value != null) {
|
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 {
|
} else {
|
||||||
return const SizedBox();
|
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: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) {
|
|
||||||
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
|
@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 {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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";
|
|
||||||
}
|
}
|
|
@ -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(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
class AppAnimations {
|
||||||
|
static const scan = "assets/animations/scan_animation.json";
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
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:
|
||||||
|
|
12
pubspec.yaml
12
pubspec.yaml
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue