Menambahkan utensil page, dan utensil contract

This commit is contained in:
IbnuBatutah 2024-01-24 21:23:15 +07:00
parent 279a03be5d
commit 2bdf328d20
33 changed files with 988 additions and 117 deletions

View File

@ -6,43 +6,58 @@ import '../../domain/entities/recipe.dart';
class RecipeItem extends StatelessWidget {
final Recipe recipe;
const RecipeItem({super.key, required this.recipe});
final Function(String) onTap;
const RecipeItem({super.key, required this.recipe, required this.onTap});
@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,
),
),
print("IMAGE IS ${recipe.image}");
return GestureDetector(
onTap: () {
onTap(recipe.uuid ?? '');
},
child: Card(
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
child: BasicNetworkImage(
imageUrl: recipe.image ?? '',
height: 160,
width: double.infinity,
),
),
const SizedBox(
height: 8,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
recipe.title ?? 'Masakan',
style: TTCommonsTextStyles.textMd.textMedium(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'${recipe.cookTime ?? 0} menit',
style: TTCommonsTextStyles.textSm.textRegular(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'app_build_config.dart';
class AppEnvironment {
static load() async {
if (AppBuildConfig.instance.config == BuildConfigEnum.production) {
await dotenv.load(fileName: "production/.env");
} else {

View File

@ -0,0 +1,40 @@
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import '../../resources/constants/database_constant.dart';
class DatabaseHelper {
static DatabaseHelper? _databaseHelper;
static late Database _database;
DatabaseHelper._internal() {
_databaseHelper = this;
}
factory DatabaseHelper() => _databaseHelper ?? DatabaseHelper._internal();
Future<Database> get database async {
_database = await _initializeDb();
return _database;
}
Future<Database> _initializeDb() async {
var path = await getDatabasesPath();
var db = openDatabase(
join(path, DatabaseConstant.dbName),
onCreate: (db, version) async {
await db.execute(
'''CREATE TABLE ${DatabaseConstant.utensilsTable} (
id INTEGER PRIMARY KEY,
name TEXT, is_selected INTEGER
)''',
);
},
version: 1,
);
return db;
}
}

View File

@ -0,0 +1,38 @@
import 'package:snap_and_cook_mobile/data/remote/models/utensil_model.dart';
import 'package:snap_and_cook_mobile/utils/extension/database_extension.dart';
import 'package:sqflite/sqflite.dart';
import '../../resources/constants/database_constant.dart';
import 'database_helper.dart';
class UtensilContract {
final DatabaseHelper _databaseHelper = DatabaseHelper();
Future<void> insertUtensil(Utensil utensil) async {
final Database db = await _databaseHelper.database;
await db.insert(DatabaseConstant.utensilsTable, utensil.toJson());
print('Data saved!');
}
Future<void> insertAllUtensil(List<Utensil> utensil) async {
final Database db = await _databaseHelper.database;
await db.insertMultiple(
DatabaseConstant.utensilsTable, utensil.map((e) => e.toJson()),
conflictAlgorithm: ConflictAlgorithm.ignore);
print('All Data saved!');
}
Future<void> updateUtensil(Utensil utensil) async {
final Database db = await _databaseHelper.database;
await db.update(DatabaseConstant.utensilsTable, utensil.toJson(),
where: 'id = ?', whereArgs: [utensil.id]);
print('Data changed!');
}
Future<List<Utensil>> getUtensils() async {
final Database db = await _databaseHelper.database;
List<Map<String, dynamic>> results =
await db.query(DatabaseConstant.utensilsTable);
return results.map((res) => Utensil.fromJson(res)).toList();
}
}

View File

@ -5,7 +5,7 @@ part 'ingredient_model.g.dart';
@JsonSerializable()
class Ingredient {
String? name;
int? quantity;
double? quantity;
String? unit;
Ingredient({

View File

@ -8,7 +8,7 @@ part of 'ingredient_model.dart';
Ingredient _$IngredientFromJson(Map<String, dynamic> json) => Ingredient(
name: json['name'] as String?,
quantity: json['quantity'] as int?,
quantity: (json['quantity'] as num?)?.toDouble(),
unit: json['unit'] as String?,
);

View File

@ -42,7 +42,7 @@ class RecipeModel {
uuid: uuid,
title: title,
description: description,
image: AppEnvironment.apiUrl + (image ?? ''),
image: AppEnvironment.imageUrl + (image ?? ''),
ingredients: ingredients,
instructions: instructions,
prepTime: prepTime,

View File

@ -0,0 +1,21 @@
import 'package:json_annotation/json_annotation.dart';
part 'utensil_model.g.dart';
@JsonSerializable()
class Utensil {
int? id;
String? name;
int? isSelected;
Utensil({
this.id,
this.name,
this.isSelected,
});
Map<String, dynamic> toJson() => _$UtensilToJson(this);
factory Utensil.fromJson(Map<String, dynamic> json) =>
_$UtensilFromJson(json);
}

View File

@ -21,8 +21,6 @@ class RecipeUseCase implements RecipeInterface {
return Right(recipes);
} on DioError catch (e) {
return Left(e);
} catch (e) {
return Left(DioError(requestOptions: RequestOptions(path: "")));
}
}

View File

@ -3,19 +3,20 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:snap_and_cook_mobile/data/remote/services/recipe_service.dart';
import 'package:snap_and_cook_mobile/utils/extension/dio_extension.dart';
import 'package:snap_and_cook_mobile/utils/interceptor/platform_header_interceptor.dart';
import 'components/app/app.dart';
import 'configuration/app_environtment.dart';
import 'resources/constants/session_constants.dart';
import 'utils/interceptor/authorization_header_interceptor.dart';
import 'utils/session/session.dart';
Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.load();
await Get.putAsync<Dio>(
() async => Dio().baseUrl(AppEnvironment.apiUrl).addInterceptor(
AuthorizationHeaderInterceptor(
onToken: () async =>
await Session.get(SessionConstants.token) ?? "")),
() async => Dio()
.baseUrl(AppEnvironment.apiUrl)
.addInterceptor(PlatformHeaderInterceptor()),
);
await Get.putAsync(() async => Session());

View File

@ -3,10 +3,11 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../components/dismissable_keyboard.dart';
import '../../components/progress_container.dart';
import '../../styles/colors.dart';
import 'base_view_model.dart';
abstract class BaseView<T extends BaseViewModel> extends GetView<T>{
abstract class BaseView<T extends BaseViewModel> extends GetView<T> {
const BaseView({super.key});
Widget body(BuildContext context);
@ -62,9 +63,24 @@ abstract class BaseView<T extends BaseViewModel> extends GetView<T>{
Widget pageContent(BuildContext context) {
return SafeArea(
child: body(context),
child: Stack(
children: [
Obx(
() => AnimatedOpacity(
opacity: controller.isLoadingContainer ? 0 : 1,
duration: const Duration(milliseconds: 750),
child: body(context),
),
),
Obx(
() => ProgressContainer(
isShow: controller.isLoadingContainer, onDismiss: null),
)
],
),
);
}
Widget? drawer() {
return null;
}
@ -72,4 +88,4 @@ abstract class BaseView<T extends BaseViewModel> extends GetView<T>{
Widget? bottomNavigationBar() {
return null;
}
}
}

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:snap_and_cook_mobile/components/recipe/recipe_item.dart';
import '../../../domain/entities/recipe.dart';
import '../view_model/home_view_model.dart';
class RecipeRecommendationWidget extends GetView<HomeViewModel> {
@ -12,26 +11,22 @@ class RecipeRecommendationWidget extends GetView<HomeViewModel> {
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(),
),
),
),
child: Obx(() => GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.recipes.length,
primary: false,
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 8,
crossAxisCount: 2,
mainAxisSpacing: 16,
childAspectRatio: 0.675,
),
itemBuilder: (ctx, index) => RecipeItem(
recipe: controller.recipes[index],
onTap: controller.navigateToRecipeDetail,
),
)),
);
}

View File

@ -97,7 +97,8 @@ class HomeView extends BaseView<HomeViewModel> {
],
),
),
RecipeRecommendationWidget()
RecipeRecommendationWidget(),
SizedBox(height: 64,)
],
),
);

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:snap_and_cook_mobile/domain/use_case/general/recipe_use_case.dart';
import '../../../domain/entities/recipe.dart';
import '../../../resources/arguments/argument_constants.dart';
import '../../../routes/routes/main_route.dart';
import '../../base/base_view_model.dart';
@ -8,18 +11,35 @@ class HomeViewModel extends BaseViewModel {
String version = "Version 0.0.1-dev";
TextEditingController searchController = TextEditingController();
final RecipeUseCase _recipeUseCase = RecipeUseCase();
final RxList<Recipe> recipes = RxList<Recipe>();
void onSearchSubmitted(String value) {
print(value);
}
@override
void onReady() {
super.onReady();
void onInit() {
super.onInit();
_fetchAllRecipes();
}
void navigateToRecipeDetail() {
Get.toNamed(MainRoute.detail);
Future<void> _fetchAllRecipes() async {
showLoadingContainer();
var data = await _recipeUseCase.fetchRecipes(cancelToken);
data.fold((l){
}, (result){
hideLoadingContainer();
recipes.clear();
recipes.addAll(result);
});
}
void navigateToRecipeDetail(String uuid) {
Get.toNamed(MainRoute.detail, arguments: {
ArgumentConstants.recipeUuid: uuid,
});
}
void navigateToRecipeDetection() {

View File

@ -3,16 +3,17 @@ import 'package:flutter/material.dart';
import '../../../styles/text_styles/tt_commons_text_styles.dart';
class FoodPrepWidget extends StatelessWidget {
const FoodPrepWidget({super.key});
final int? serving, prepTime, cookTime;
const FoodPrepWidget({super.key, this.serving, this.prepTime, this.cookTime});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_foodPrepItem('Porsi', '4'),
_foodPrepItem('Persiapan', '20 menit'),
_foodPrepItem('Memasak', '30 menit'),
_foodPrepItem('Porsi', '${serving ?? 0}'),
_foodPrepItem('Persiapan', '${prepTime ?? 0} menit'),
_foodPrepItem('Memasak', '${cookTime ?? 0} menit'),
],
);
}

View File

@ -3,19 +3,17 @@ 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});
final List<Ingredient> ingredients;
const IngredientListWidget({super.key, required this.ingredients});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return IngredientItem(ingredient: Ingredient(
name: "Telur",
quantity: 1,
unit: "Butir"
));
return IngredientItem(ingredient: ingredients[index]);
},
itemCount: 5,
itemCount: ingredients.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
);

View File

@ -1,6 +1,7 @@
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 'package:snap_and_cook_mobile/utils/extension/double_extension.dart';
import '../../../styles/colors.dart';
@ -19,15 +20,43 @@ class IngredientItem extends StatelessWidget {
const SizedBox(
width: 12,
),
Text(
'${ingredient.name} ${ingredient.quantity} ${ingredient.unit}',
style: TTCommonsTextStyles.textMd.textMedium(),
Expanded(
child: Text(
'${ingredient.name} ${_getQuantity()} ${ingredient.unit}',
style: TTCommonsTextStyles.textMd.textMedium(),
),
),
],
),
);
}
String _getQuantity() {
if (ingredient.quantity == null || ingredient.quantity == 0) {
return '';
}
if (ingredient.quantity! % 1 == 0) {
return '${ingredient.quantity?.toInt()}';
} else {
int wholeNumber = ingredient.quantity!.toInt();
double decimal = ingredient.quantity! - wholeNumber;
String fraction = decimal.decimalToFraction();
if (wholeNumber == 0) return fraction.trim();
return '$wholeNumber $fraction';
}
}
// String _getQuantity(){
// if(ingredient.quantity == null || ingredient.quantity == 0){
// return '';
// }
//
// return '${ingredient.quantity?.toInt()}';
// }
Widget _circleWidget() {
return Container(
width: 8.0,

View File

@ -14,14 +14,17 @@ class StepItem extends StatelessWidget {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_stepContainer(),
const SizedBox(
width: 12,
),
Text(
step,
style: TTCommonsTextStyles.textMd.textMedium(),
Expanded(
child: Text(
step,
style: TTCommonsTextStyles.textMd.textMedium(),
),
),
],
),

View File

@ -2,15 +2,16 @@ import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/presentation/recipe_detail/components/step_item.dart';
class StepListWidget extends StatelessWidget {
const StepListWidget({super.key});
final List<String> steps;
const StepListWidget({super.key, required this.steps});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return StepItem(step: "Panaskan minyak goreng", index: index);
return StepItem(step: steps[index], index: index);
},
itemCount: 5,
itemCount: steps.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
);

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get_state_manager/get_state_manager.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';
@ -17,13 +18,19 @@ class RecipeDetailView extends BaseView<RecipeDetailViewModel> {
@override
PreferredSizeWidget? appBar(BuildContext context) {
return BasicAppBar(appBarTitleText: "", centerTitle: false, leadingIconData: Icons.arrow_back,);
return BasicAppBar(
appBarTitleText: "",
centerTitle: false,
leadingIconData: Icons.arrow_back,
);
}
@override
Widget body(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24,),
padding: const EdgeInsets.symmetric(
horizontal: 24,
),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
@ -31,33 +38,62 @@ class RecipeDetailView extends BaseView<RecipeDetailViewModel> {
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text('Nasi Goreng Dadakan', style: TTCommonsTextStyles.textXl.textMedium()),
child: Obx(() => Text(controller.recipe.value?.title ?? '',
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')),
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Obx(
() => BasicNetworkImage(
imageUrl: controller.recipe.value?.image ?? ''),
),
),
),
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()),
child: Obx(
() => Text(controller.recipe.value?.description ?? '',
style: TTCommonsTextStyles.textMd.textRegular()),
),
),
Divider(
const Divider(
color: Colors.grey,
),
FoodPrepWidget(),
RecipeDetailDividerWidget(title: 'Bahan',),
IngredientListWidget(),
RecipeDetailDividerWidget(title: 'Alat Memasak',),
RecipeDetailDividerWidget(title: 'Langkah-langkah',),
StepListWidget(),
SizedBox(height: 24,),
BasicButton(onPress: (){},
Obx(
() => FoodPrepWidget(
cookTime: controller.recipe.value?.cookTime,
prepTime: controller.recipe.value?.prepTime,
serving: controller.recipe.value?.servings,
),
),
const RecipeDetailDividerWidget(
title: 'Bahan',
),
Obx(
() => IngredientListWidget(
ingredients: controller.recipe.value?.ingredients ?? [],
),
),
const RecipeDetailDividerWidget(
title: 'Alat Memasak',
),
const RecipeDetailDividerWidget(
title: 'Langkah-langkah',
),
StepListWidget(steps:controller.recipe.value?.instructions ?? [],),
const SizedBox(
height: 24,
),
BasicButton(
onPress: () {},
bgColor: AppColors.copper,
text: 'Mulai Memasak'),
SizedBox(height: 24,),
const SizedBox(
height: 24,
),
],
),
),

View File

@ -1,10 +1,31 @@
import 'package:get/get.dart';
import '../../../domain/entities/recipe.dart';
import '../../../domain/use_case/general/recipe_use_case.dart';
import '../../../resources/arguments/argument_constants.dart';
import '../../base/base_view_model.dart';
class RecipeDetailViewModel extends BaseViewModel {
final _arguments = Get.arguments;
String get recipeUuid => _arguments[ArgumentConstants.recipeUuid];
final RecipeUseCase _recipeUseCase = RecipeUseCase();
final Rxn<Recipe> recipe = Rxn<Recipe>();
@override
void onReady() {
super.onReady();
void onInit() {
super.onInit();
_fetchRecipeDetail();
}
Future<void> _fetchRecipeDetail() async {
showLoadingContainer();
var data = await _recipeUseCase.fetchDetailRecipe(cancelToken, recipeUuid);
data.fold((l){
}, (result){
hideLoadingContainer();
recipe.value = result;
});
}
@override

View File

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

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:snap_and_cook_mobile/styles/text_styles/tt_commons_text_styles.dart';
import '../../../components/asset_image_view.dart';
import '../../../styles/values.dart';
import '../../base/base_view.dart';
import '../../../components/appbar/basic_appbar.dart';
import '../../../styles/images.dart';
import '../view_model/utensil_view_model.dart';
class UtensilView extends BaseView<UtensilViewModel> {
const UtensilView({super.key});
@override
PreferredSizeWidget? appBar(BuildContext context) {
return BasicAppBar(appBarTitleText: "", centerTitle: false);
}
@override
Widget body(BuildContext context) {
return Column(
children: [
const SizedBox(
width: double.infinity,
),
const Expanded(
child: AssetImageView(
fileName: AppImages.logoFull,
width: AppValues.logoWidth,
height: AppValues.logoHeight,
)),
Text(controller.version, style: TTCommonsTextStyles.textLg.textMedium()),
const SizedBox(
height: 32,
),
],
);
}
}

View File

@ -0,0 +1,22 @@
import 'package:get/get.dart';
import '../../../routes/routes/main_route.dart';
import '../../base/base_view_model.dart';
class UtensilViewModel extends BaseViewModel {
String version = "Version 0.0.1-dev";
@override
void onReady() {
super.onReady();
_startSplash();
}
@override
void onClose() {}
Future<void> _startSplash() async {
await Future.delayed(const Duration(seconds: 2));
Get.offNamed(MainRoute.home);
}
}

View File

@ -0,0 +1,5 @@
class DatabaseConstant {
static const String dbName = 'snapandcook_db.db';
static const String utensilsTable = 'utensil_tb';
}

View File

@ -0,0 +1,56 @@
import 'package:snap_and_cook_mobile/utils/extension/list_extension.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/sql.dart';
Iterable<Iterable<T>> chunk<T>(Iterable<T> iterable, int chunkSize) sync* {
var start = 0;
while (start < iterable.length) {
yield iterable.skip(start).take(chunkSize);
start += chunkSize;
}
}
const _conflictValues = {
ConflictAlgorithm.rollback: 'OR ROLLBACK',
ConflictAlgorithm.abort: 'OR ABORT',
ConflictAlgorithm.fail: 'OR FAIL',
ConflictAlgorithm.ignore: 'OR IGNORE',
ConflictAlgorithm.replace: 'OR REPLACE'
};
extension MultipleInsert on Database {
Future<int?> insertMultiple(
String table,
Iterable<Map<String, Object?>> data, {
ConflictAlgorithm? conflictAlgorithm,
int blockSize = 100,
}) async {
final conflictStr = conflictAlgorithm == null
? ''
: '${_conflictValues[conflictAlgorithm]}';
final cols = data.first.keys.toList();
final colsString = cols.map((e) => '"$e"').join(',\n\t');
final command =
'INSERT $conflictStr INTO "$table" (\n\t$colsString\n\t)\nVALUES\n\t';
final argsString = '(${cols.map((e) => '?').join(', ')})';
int? result;
for (var chunk in chunk(data, blockSize)) {
final sql = StringBuffer(command);
final params = <Object?>[];
chunk.forEachIndexed((i, row) {
sql.write(argsString);
if (i == chunk.length - 1) {
sql.write(';');
} else {
sql.write(',\n\t');
}
for (var col in cols) {
params.add(row[col]);
}
});
result = await rawInsert(sql.toString(), params);
}
return result;
}
}

View File

@ -1,5 +1,8 @@
import 'package:chucker_flutter/chucker_flutter.dart';
import 'package:dio/dio.dart';
import '../interceptor/pretty_dio_logger_interceptor.dart';
extension DioExtention on Dio {
Dio addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
@ -14,8 +17,19 @@ extension DioExtention on Dio {
);
return this;
}
Dio modify(Function (Dio) modify) {
modify.call(this);
return this;
}
Dio usePrettyLogger() {
addInterceptor(PrettyDioLoggerInterceptor());
return this;
}
Dio useChucker() {
addInterceptor(ChuckerDioInterceptor());
return this;
}
}

View File

@ -0,0 +1,23 @@
extension DoubleExtension on double {
String decimalToFraction() {
const EPSILON = 1.0E-15;
double h1 = 1;
double h2 = 0;
double k1 = 0;
double k2 = 1;
double b = this;
do {
double a = b.floorToDouble();
double aux = h1;
h1 = a * h1 + h2;
h2 = aux;
aux = k1;
k1 = a * k1 + k2;
k2 = aux;
b = 1 / (b - a);
} while ((this - h1 / k1).abs() > this * EPSILON);
String fraction = "${h1.toInt()}/${k1.toInt()}";
return fraction;
}
}

View File

@ -0,0 +1,10 @@
extension ListKt<T> on Iterable<T> {
///forEach with index
void forEachIndexed(void Function(int index, T element) action) {
int index = 0;
for (T element in this) {
action(index++, element);
}
}
}

View File

@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
class PlatformHeaderInterceptor extends Interceptor {
PlatformHeaderInterceptor();
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
options.headers.addAll({
'content-type': 'application/json',
'x-platform': 'apps',
'Accept': 'application/json',
});
super.onRequest(options, handler);
}
}

View File

@ -0,0 +1,307 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:dio/dio.dart';
/// code copied from https://pub.dev/packages/pretty_dio_logger
class PrettyDioLoggerInterceptor extends Interceptor {
/// Print request [Options]
final bool request;
/// Print request header [Options.headers]
final bool requestHeader;
/// Print request data [Options.tribeCollectionData]
final bool requestBody;
/// Print [Response.data]
final bool responseBody;
/// Print [Response.headers]
final bool responseHeader;
/// Print error message
final bool error;
/// InitialTab count to logPrint json response
static const int initialTab = 1;
/// 1 tab length
static const String tabStep = ' ';
/// Print compact json response
final bool compact;
/// Width size per logPrint
final int maxWidth;
/// Log printer; defaults logPrint log to console.
/// In flutter, you'd better use debugPrint.
/// you can also write log in a file.
void Function(Object object) logPrint;
static const int defaultMaxWidth = 160;
PrettyDioLoggerInterceptor({
this.request = true,
this.requestHeader = true,
this.requestBody = true,
this.responseHeader = false,
this.responseBody = true,
this.error = true,
this.maxWidth = defaultMaxWidth,
this.compact = true,
this.logPrint = print,
});
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (request) {
_printRequestHeader(options);
}
if (requestHeader) {
_printMapAsTable(options.queryParameters, header: 'Query Parameters');
final requestHeaders = <String, dynamic>{};
requestHeaders.addAll(options.headers);
requestHeaders['contentType'] = options.contentType?.toString();
requestHeaders['responseType'] = options.responseType.toString();
requestHeaders['followRedirects'] = options.followRedirects;
requestHeaders['connectTimeout'] = options.connectTimeout;
requestHeaders['receiveTimeout'] = options.receiveTimeout;
_printMapAsTable(requestHeaders, header: 'Headers');
_printMapAsTable(options.extra, header: 'Extras');
}
if (requestBody && options.method != 'GET') {
final dynamic data = options.data;
if (data != null) {
if (data is Map) _printMapAsTable(options.data as Map?, header: 'Body');
if (data is FormData) {
final formDataMap = <String, dynamic>{}
..addEntries(data.fields)
..addEntries(data.files);
_printMapAsTable(formDataMap, header: 'Form data | ${data.boundary}');
} else {
try {
var encoder = const JsonEncoder.withIndent(" ");
// _printBoxed(header: "Body (Request)", text: encoder.convert(data));
logPrint('╔ Body (Request)');
logPrint('');
// logPrint(encoder.convert(data));
_printData(encoder.convert(data));
logPrint('');
_printLine('');
} catch (e) {}
}
}
}
super.onRequest(options, handler);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
if (error) {
if (err.type == DioErrorType.response) {
final uri = err.response?.requestOptions.uri;
_printBoxed(
header:
'DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage}',
text: uri.toString());
if (err.response != null && err.response?.data != null) {
logPrint('${err.type.toString()}');
_printResponse(err.response!);
}
_printLine('');
logPrint('');
} else {
_printBoxed(header: 'DioError ║ ${err.type}', text: err.message);
}
}
super.onError(err, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
_printResponseHeader(response);
if (responseHeader) {
final responseHeaders = <String, String>{};
response.headers
.forEach((k, list) => responseHeaders[k] = list.toString());
_printMapAsTable(responseHeaders, header: 'Headers');
}
if (responseBody) {
logPrint('╔ Body (Response)');
logPrint('');
_printResponse(response);
logPrint('');
_printLine('');
}
super.onResponse(response, handler);
}
void _printBoxed({String? header, String? text}) {
logPrint('');
logPrint('╔╣ $header');
logPrint('$text');
_printLine('');
}
void _printResponse(Response response) {
if (response.data != null) {
if (response.data is Map) {
_printPrettyMap(response.data as Map);
} else if (response.data is List) {
logPrint('${_indent()}[');
_printList(response.data as List);
logPrint('${_indent()}[');
} else {
_printBlock(response.data.toString());
}
}
}
void _printData(dynamic data) {
if (data != null) {
if (data is Map) {
_printPrettyMap(data as Map);
} else if (data is List) {
logPrint('${_indent()}[');
_printList(data as List);
logPrint('${_indent()}[');
} else {
_printBlock(data.toString());
}
}
}
void _printResponseHeader(Response response) {
final uri = response.requestOptions.uri;
final method = response.requestOptions.method;
_printBoxed(
header:
'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}',
text: uri.toString());
}
void _printRequestHeader(RequestOptions options) {
final uri = options.uri;
final method = options.method;
_printBoxed(header: 'Request ║ $method ', text: uri.toString());
}
void _printLine([String pre = '', String suf = '']) =>
logPrint('$pre${'' * maxWidth}$suf');
void _printKV(String? key, Object? v) {
final pre = '╟ "$key": ';
final msg = v.toString();
if (pre.length + msg.length > maxWidth) {
logPrint(pre);
_printBlock(msg);
} else {
logPrint('$pre$msg');
}
}
void _printBlock(String msg) {
final lines = (msg.length / maxWidth).ceil();
for (var i = 0; i < lines; ++i) {
logPrint((i >= 0 ? '' : '') +
msg.substring(i * maxWidth,
math.min<int>(i * maxWidth + maxWidth, msg.length)));
}
}
String _indent([int tabCount = initialTab]) => tabStep * tabCount;
void _printPrettyMap(
Map data, {
int tabs = initialTab,
bool isListItem = false,
bool isLast = false,
}) {
var _tabs = tabs;
final isRoot = _tabs == initialTab;
final initialIndent = _indent(_tabs);
_tabs++;
if (isRoot || isListItem) logPrint('$initialIndent{');
data.keys.toList().asMap().forEach((index, dynamic key) {
final isLast = index == data.length - 1;
dynamic value = data[key];
if (value is String) {
value = '"${value.toString().replaceAll(RegExp(r'(\r|\n)+'), " ")}"';
}
if (value is Map) {
if (compact && _canFlattenMap(value)) {
logPrint('${_indent(_tabs)} "$key": $value${!isLast ? ',' : ''}');
} else {
logPrint('${_indent(_tabs)} "$key": {');
_printPrettyMap(value, tabs: _tabs);
}
} else if (value is List) {
if (compact && _canFlattenList(value)) {
logPrint('${_indent(_tabs)} "$key": ${value.toString()}');
} else {
logPrint('${_indent(_tabs)} "$key": [');
_printList(value, tabs: _tabs);
logPrint('${_indent(_tabs)} ]${isLast ? '' : ','}');
}
} else {
final msg = value.toString().replaceAll('\n', '');
final indent = _indent(_tabs);
final linWidth = maxWidth - indent.length;
if (msg.length + indent.length > linWidth) {
final lines = (msg.length / linWidth).ceil();
for (var i = 0; i < lines; ++i) {
logPrint(
'${_indent(_tabs)} ${msg.substring(i * linWidth, math.min<int>(i * linWidth + linWidth, msg.length))}');
}
} else {
logPrint('${_indent(_tabs)} "$key": $msg${!isLast ? ',' : ''}');
}
}
});
logPrint('$initialIndent}${isListItem && !isLast ? ',' : ''}');
}
void _printList(List list, {int tabs = initialTab}) {
int tabsCount = 2;
list.asMap().forEach((i, dynamic e) {
final isLast = i == list.length - 1;
if (e is Map) {
if (compact && _canFlattenMap(e)) {
logPrint('${_indent(tabs)} $e${!isLast ? ',' : ''}');
} else {
_printPrettyMap(e, tabs: tabs + 1, isListItem: true, isLast: isLast);
}
} else {
logPrint('${_indent(tabs + tabsCount)} $e${isLast ? '' : ','}');
}
});
}
bool _canFlattenMap(Map map) {
return map.values
.where((dynamic val) => val is Map || val is List)
.isEmpty &&
map.toString().length < maxWidth;
}
bool _canFlattenList(List list) {
int maxListLength = 10;
return list.length < maxListLength && list.toString().length < maxWidth;
}
void _printMapAsTable(Map? map, {String? header}) {
if (map == null || map.isEmpty) return;
logPrint('$header ');
map.forEach(
(dynamic key, dynamic value) => _printKV(key.toString(), value));
_printLine('');
}
}

View File

@ -193,6 +193,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
chucker_flutter:
dependency: "direct main"
description:
name: chucker_flutter
sha256: e8df01ec830e1364dee83a66e304df490b9b416008faf559bfc3a6e341608513
url: "https://pub.dev"
source: hosted
version: "1.0.0+1"
clock:
dependency: transitive
description:
@ -293,10 +301,10 @@ packages:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "6.1.4"
file_selector_linux:
dependency: transitive
description:
@ -366,6 +374,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.3"
flutter_json_viewer:
dependency: transitive
description:
name: flutter_json_viewer
sha256: "3acc20693c3e6465ff2e2a7884269e362cb8acc2b11e7caa9b283872f854e7bf"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
flutter_lints:
dependency: "direct dev"
description:
@ -374,6 +390,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -548,10 +569,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
url: "https://pub.dev"
source: hosted
version: "0.17.0"
version: "0.18.0"
io:
dependency: transitive
description:
@ -856,6 +877,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.27.7"
share_plus:
dependency: transitive
description:
name: share_plus
sha256: f582d5741930f3ad1bf0211d358eddc0508cc346e5b4b248bd1e569c995ebb7a
url: "https://pub.dev"
source: hosted
version: "4.5.3"
share_plus_linux:
dependency: transitive
description:
name: share_plus_linux
sha256: dc32bf9f1151b9864bb86a997c61a487967a08f2e0b4feaa9a10538712224da4
url: "https://pub.dev"
source: hosted
version: "3.0.1"
share_plus_macos:
dependency: transitive
description:
name: share_plus_macos
sha256: "44daa946f2845045ecd7abb3569b61cd9a55ae9cc4cbec9895b2067b270697ae"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
url: "https://pub.dev"
source: hosted
version: "3.3.1"
share_plus_web:
dependency: transitive
description:
name: share_plus_web
sha256: eaef05fa8548b372253e772837dd1fbe4ce3aca30ea330765c945d7d4f7c9935
url: "https://pub.dev"
source: hosted
version: "3.1.0"
share_plus_windows:
dependency: transitive
description:
name: share_plus_windows
sha256: "3a21515ae7d46988d42130cd53294849e280a5de6ace24bae6912a1bffd757d4"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
shared_preferences:
dependency: "direct main"
description:
@ -974,7 +1043,7 @@ packages:
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
dependency: "direct main"
description:
name: sqflite
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
@ -1069,6 +1138,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
url: "https://pub.dev"
source: hosted
version: "6.1.14"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f"
url: "https://pub.dev"
source: hosted
version: "6.2.2"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
url: "https://pub.dev"
source: hosted
version: "6.2.4"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
url: "https://pub.dev"
source: hosted
version: "3.1.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f
url: "https://pub.dev"
source: hosted
version: "2.3.1"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2
url: "https://pub.dev"
source: hosted
version: "2.0.19"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
version: "3.1.1"
uuid:
dependency: transitive
description:

View File

@ -42,7 +42,7 @@ dependencies:
fluttertoast: ^8.2.1
retrofit: ^3.0.1+1
pull_to_refresh: ^2.0.0
intl: ^0.17.0
intl:
flutter_screenutil: ^5.6.1
get: ^4.6.5
shared_preferences: ^2.2.2
@ -54,7 +54,8 @@ dependencies:
cached_network_image: ^3.2.3
shimmer:
flutter_dotenv: ^5.0.2
chucker_flutter:
sqflite: ^2.2.8+4
dev_dependencies:
flutter_test: