diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
index 3ab485dc..196d2aa1 100644
--- a/.gitea/workflows/test.yml
+++ b/.gitea/workflows/test.yml
@@ -3,8 +3,8 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
- flutter_version: "3.24.2"
- java_version: "17.x"
+ flutter_version: "3.24.4"
+ java_version: "21.x"
flutter_channel: "stable"
jobs:
@@ -24,8 +24,7 @@ jobs:
- name: Install Android SDK
run: |
apt-get update
- apt-get install -y openjdk-11-jdk wget
- wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip
+ https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip
unzip commandlinetools.zip
mkdir -p $HOME/Android/Sdk/cmdline-tools/latest
mv cmdline-tools/* $HOME/Android/Sdk/cmdline-tools/latest/
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 8a009c79..e5218dce 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -7,15 +7,15 @@ plugins {
android {
namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion
- ndkVersion = "25.1.8937393"
+ ndkVersion = "28.0.12433566"
compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
+ sourceCompatibility JavaVersion.VERSION_21
+ targetCompatibility JavaVersion.VERSION_21
}
kotlinOptions {
- jvmTarget = 17
+ jvmTarget = 21
}
defaultConfig {
diff --git a/assets/no_one_image_was_added.svg b/assets/no_one_image_was_added.svg
new file mode 100644
index 00000000..dc20c61c
--- /dev/null
+++ b/assets/no_one_image_was_added.svg
@@ -0,0 +1,8 @@
+
diff --git a/lib/features/diary/models/diary_day_model.dart b/lib/features/diary/models/diary_day_model.dart
index 773920dc..e4d8a26f 100644
--- a/lib/features/diary/models/diary_day_model.dart
+++ b/lib/features/diary/models/diary_day_model.dart
@@ -47,10 +47,10 @@ class MealModel with _$MealModel {
@freezed
class DiaryPhotoModel with _$DiaryPhotoModel {
const factory DiaryPhotoModel({
- @JsonKey(name: "id") required String id,
+ @JsonKey(name: "id") required int id,
@JsonKey(name: "original_name") required String originalName,
@JsonKey(name: "path") required String path,
- @JsonKey(name: "size") required String size,
+ @JsonKey(name: "size") required int size,
}) = _DiaryPhotoModel;
factory DiaryPhotoModel.fromJson(Map json) => _$DiaryPhotoModelFromJson(json);
@@ -78,3 +78,14 @@ class TypeOfDiaryModel with _$TypeOfDiaryModel {
factory TypeOfDiaryModel.fromJson(Map json) => _$TypeOfDiaryModelFromJson(json);
}
+
+@freezed
+class ProductInMeal with _$ProductInMeal {
+ const factory ProductInMeal({
+ @JsonKey(name: 'product_name') required String productName,
+ @JsonKey(name: 'portion') required String portion,
+ @JsonKey(name: 'quantity') required int quantity,
+ }) = _ProductInMeal;
+
+ factory ProductInMeal.fromJson(Map json) => _$ProductInMealFromJson(json);
+}
diff --git a/lib/features/diary/models/diary_day_model.freezed.dart b/lib/features/diary/models/diary_day_model.freezed.dart
index dc780145..72b0d70b 100644
--- a/lib/features/diary/models/diary_day_model.freezed.dart
+++ b/lib/features/diary/models/diary_day_model.freezed.dart
@@ -850,13 +850,13 @@ DiaryPhotoModel _$DiaryPhotoModelFromJson(Map json) {
/// @nodoc
mixin _$DiaryPhotoModel {
@JsonKey(name: "id")
- String get id => throw _privateConstructorUsedError;
+ int get id => throw _privateConstructorUsedError;
@JsonKey(name: "original_name")
String get originalName => throw _privateConstructorUsedError;
@JsonKey(name: "path")
String get path => throw _privateConstructorUsedError;
@JsonKey(name: "size")
- String get size => throw _privateConstructorUsedError;
+ int get size => throw _privateConstructorUsedError;
/// Serializes this DiaryPhotoModel to a JSON map.
Map toJson() => throw _privateConstructorUsedError;
@@ -875,10 +875,10 @@ abstract class $DiaryPhotoModelCopyWith<$Res> {
_$DiaryPhotoModelCopyWithImpl<$Res, DiaryPhotoModel>;
@useResult
$Res call(
- {@JsonKey(name: "id") String id,
+ {@JsonKey(name: "id") int id,
@JsonKey(name: "original_name") String originalName,
@JsonKey(name: "path") String path,
- @JsonKey(name: "size") String size});
+ @JsonKey(name: "size") int size});
}
/// @nodoc
@@ -905,7 +905,7 @@ class _$DiaryPhotoModelCopyWithImpl<$Res, $Val extends DiaryPhotoModel>
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
- as String,
+ as int,
originalName: null == originalName
? _value.originalName
: originalName // ignore: cast_nullable_to_non_nullable
@@ -917,7 +917,7 @@ class _$DiaryPhotoModelCopyWithImpl<$Res, $Val extends DiaryPhotoModel>
size: null == size
? _value.size
: size // ignore: cast_nullable_to_non_nullable
- as String,
+ as int,
) as $Val);
}
}
@@ -931,10 +931,10 @@ abstract class _$$DiaryPhotoModelImplCopyWith<$Res>
@override
@useResult
$Res call(
- {@JsonKey(name: "id") String id,
+ {@JsonKey(name: "id") int id,
@JsonKey(name: "original_name") String originalName,
@JsonKey(name: "path") String path,
- @JsonKey(name: "size") String size});
+ @JsonKey(name: "size") int size});
}
/// @nodoc
@@ -959,7 +959,7 @@ class __$$DiaryPhotoModelImplCopyWithImpl<$Res>
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
- as String,
+ as int,
originalName: null == originalName
? _value.originalName
: originalName // ignore: cast_nullable_to_non_nullable
@@ -971,7 +971,7 @@ class __$$DiaryPhotoModelImplCopyWithImpl<$Res>
size: null == size
? _value.size
: size // ignore: cast_nullable_to_non_nullable
- as String,
+ as int,
));
}
}
@@ -990,7 +990,7 @@ class _$DiaryPhotoModelImpl implements _DiaryPhotoModel {
@override
@JsonKey(name: "id")
- final String id;
+ final int id;
@override
@JsonKey(name: "original_name")
final String originalName;
@@ -999,7 +999,7 @@ class _$DiaryPhotoModelImpl implements _DiaryPhotoModel {
final String path;
@override
@JsonKey(name: "size")
- final String size;
+ final int size;
@override
String toString() {
@@ -1041,18 +1041,17 @@ class _$DiaryPhotoModelImpl implements _DiaryPhotoModel {
abstract class _DiaryPhotoModel implements DiaryPhotoModel {
const factory _DiaryPhotoModel(
- {@JsonKey(name: "id") required final String id,
- @JsonKey(name: "original_name") required final String originalName,
- @JsonKey(name: "path") required final String path,
- @JsonKey(name: "size") required final String size}) =
- _$DiaryPhotoModelImpl;
+ {@JsonKey(name: "id") required final int id,
+ @JsonKey(name: "original_name") required final String originalName,
+ @JsonKey(name: "path") required final String path,
+ @JsonKey(name: "size") required final int size}) = _$DiaryPhotoModelImpl;
factory _DiaryPhotoModel.fromJson(Map json) =
_$DiaryPhotoModelImpl.fromJson;
@override
@JsonKey(name: "id")
- String get id;
+ int get id;
@override
@JsonKey(name: "original_name")
String get originalName;
@@ -1061,7 +1060,7 @@ abstract class _DiaryPhotoModel implements DiaryPhotoModel {
String get path;
@override
@JsonKey(name: "size")
- String get size;
+ int get size;
/// Create a copy of DiaryPhotoModel
/// with the given fields replaced by the non-null parameter values.
@@ -1502,3 +1501,208 @@ abstract class _TypeOfDiaryModel implements TypeOfDiaryModel {
_$$TypeOfDiaryModelImplCopyWith<_$TypeOfDiaryModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}
+
+ProductInMeal _$ProductInMealFromJson(Map json) {
+ return _ProductInMeal.fromJson(json);
+}
+
+/// @nodoc
+mixin _$ProductInMeal {
+ @JsonKey(name: 'product_name')
+ String get productName => throw _privateConstructorUsedError;
+ @JsonKey(name: 'portion')
+ String get portion => throw _privateConstructorUsedError;
+ @JsonKey(name: 'quantity')
+ int get quantity => throw _privateConstructorUsedError;
+
+ /// Serializes this ProductInMeal to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of ProductInMeal
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $ProductInMealCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ProductInMealCopyWith<$Res> {
+ factory $ProductInMealCopyWith(
+ ProductInMeal value, $Res Function(ProductInMeal) then) =
+ _$ProductInMealCopyWithImpl<$Res, ProductInMeal>;
+ @useResult
+ $Res call(
+ {@JsonKey(name: 'product_name') String productName,
+ @JsonKey(name: 'portion') String portion,
+ @JsonKey(name: 'quantity') int quantity});
+}
+
+/// @nodoc
+class _$ProductInMealCopyWithImpl<$Res, $Val extends ProductInMeal>
+ implements $ProductInMealCopyWith<$Res> {
+ _$ProductInMealCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of ProductInMeal
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? productName = null,
+ Object? portion = null,
+ Object? quantity = null,
+ }) {
+ return _then(_value.copyWith(
+ productName: null == productName
+ ? _value.productName
+ : productName // ignore: cast_nullable_to_non_nullable
+ as String,
+ portion: null == portion
+ ? _value.portion
+ : portion // ignore: cast_nullable_to_non_nullable
+ as String,
+ quantity: null == quantity
+ ? _value.quantity
+ : quantity // ignore: cast_nullable_to_non_nullable
+ as int,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$ProductInMealImplCopyWith<$Res>
+ implements $ProductInMealCopyWith<$Res> {
+ factory _$$ProductInMealImplCopyWith(
+ _$ProductInMealImpl value, $Res Function(_$ProductInMealImpl) then) =
+ __$$ProductInMealImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {@JsonKey(name: 'product_name') String productName,
+ @JsonKey(name: 'portion') String portion,
+ @JsonKey(name: 'quantity') int quantity});
+}
+
+/// @nodoc
+class __$$ProductInMealImplCopyWithImpl<$Res>
+ extends _$ProductInMealCopyWithImpl<$Res, _$ProductInMealImpl>
+ implements _$$ProductInMealImplCopyWith<$Res> {
+ __$$ProductInMealImplCopyWithImpl(
+ _$ProductInMealImpl _value, $Res Function(_$ProductInMealImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of ProductInMeal
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? productName = null,
+ Object? portion = null,
+ Object? quantity = null,
+ }) {
+ return _then(_$ProductInMealImpl(
+ productName: null == productName
+ ? _value.productName
+ : productName // ignore: cast_nullable_to_non_nullable
+ as String,
+ portion: null == portion
+ ? _value.portion
+ : portion // ignore: cast_nullable_to_non_nullable
+ as String,
+ quantity: null == quantity
+ ? _value.quantity
+ : quantity // ignore: cast_nullable_to_non_nullable
+ as int,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$ProductInMealImpl implements _ProductInMeal {
+ const _$ProductInMealImpl(
+ {@JsonKey(name: 'product_name') required this.productName,
+ @JsonKey(name: 'portion') required this.portion,
+ @JsonKey(name: 'quantity') required this.quantity});
+
+ factory _$ProductInMealImpl.fromJson(Map json) =>
+ _$$ProductInMealImplFromJson(json);
+
+ @override
+ @JsonKey(name: 'product_name')
+ final String productName;
+ @override
+ @JsonKey(name: 'portion')
+ final String portion;
+ @override
+ @JsonKey(name: 'quantity')
+ final int quantity;
+
+ @override
+ String toString() {
+ return 'ProductInMeal(productName: $productName, portion: $portion, quantity: $quantity)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$ProductInMealImpl &&
+ (identical(other.productName, productName) ||
+ other.productName == productName) &&
+ (identical(other.portion, portion) || other.portion == portion) &&
+ (identical(other.quantity, quantity) ||
+ other.quantity == quantity));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, productName, portion, quantity);
+
+ /// Create a copy of ProductInMeal
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$ProductInMealImplCopyWith<_$ProductInMealImpl> get copyWith =>
+ __$$ProductInMealImplCopyWithImpl<_$ProductInMealImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$ProductInMealImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _ProductInMeal implements ProductInMeal {
+ const factory _ProductInMeal(
+ {@JsonKey(name: 'product_name') required final String productName,
+ @JsonKey(name: 'portion') required final String portion,
+ @JsonKey(name: 'quantity') required final int quantity}) =
+ _$ProductInMealImpl;
+
+ factory _ProductInMeal.fromJson(Map json) =
+ _$ProductInMealImpl.fromJson;
+
+ @override
+ @JsonKey(name: 'product_name')
+ String get productName;
+ @override
+ @JsonKey(name: 'portion')
+ String get portion;
+ @override
+ @JsonKey(name: 'quantity')
+ int get quantity;
+
+ /// Create a copy of ProductInMeal
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$ProductInMealImplCopyWith<_$ProductInMealImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/diary/models/diary_day_model.g.dart b/lib/features/diary/models/diary_day_model.g.dart
index 05d36efa..93ece876 100644
--- a/lib/features/diary/models/diary_day_model.g.dart
+++ b/lib/features/diary/models/diary_day_model.g.dart
@@ -74,10 +74,10 @@ Map _$$MealModelImplToJson(_$MealModelImpl instance) =>
_$DiaryPhotoModelImpl _$$DiaryPhotoModelImplFromJson(
Map json) =>
_$DiaryPhotoModelImpl(
- id: json['id'] as String,
+ id: (json['id'] as num).toInt(),
originalName: json['original_name'] as String,
path: json['path'] as String,
- size: json['size'] as String,
+ size: (json['size'] as num).toInt(),
);
Map _$$DiaryPhotoModelImplToJson(
@@ -120,3 +120,17 @@ Map _$$TypeOfDiaryModelImplToJson(
'has_chosen_diet': instance.hasChosenDiet,
'has_chosen_plan': instance.hasChosenPlan,
};
+
+_$ProductInMealImpl _$$ProductInMealImplFromJson(Map json) =>
+ _$ProductInMealImpl(
+ productName: json['product_name'] as String,
+ portion: json['portion'] as String,
+ quantity: (json['quantity'] as num).toInt(),
+ );
+
+Map _$$ProductInMealImplToJson(_$ProductInMealImpl instance) =>
+ {
+ 'product_name': instance.productName,
+ 'portion': instance.portion,
+ 'quantity': instance.quantity,
+ };
diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart
index d5b4fede..d7f31da6 100644
--- a/lib/features/diary/presentation/bloc/diary_cubit.dart
+++ b/lib/features/diary/presentation/bloc/diary_cubit.dart
@@ -1,3 +1,6 @@
+import 'dart:io';
+
+import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
@@ -41,7 +44,69 @@ class DiaryCubit extends Cubit {
await getDiaryDay(date, false);
}
+ updateProductInMeal({
+ required int diaryId,
+ required int mealId,
+ required int portionId,
+ required String productName,
+ required String portion,
+ required int quantity,
+ required String date,
+ }) async {
+ await diaryRepository.updateProductInMeal(
+ diaryId: diaryId,
+ mealId: mealId,
+ portionId: portionId,
+ productName: productName,
+ portion: portion,
+ quantity: quantity,
+ );
+ await getDiaryDay(date, false);
+ }
+
+ deleteProductFromMeal({
+ required int diaryId,
+ required int mealId,
+ required int portionId,
+ required String date,
+ }) async {
+ await diaryRepository.deleteProductFromMeal(
+ diaryId: diaryId,
+ mealId: mealId,
+ portionId: portionId,
+ );
+ await getDiaryDay(date, false);
+ }
+
Future> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
}
+
+ uploadPhotos({
+ required List files,
+ required int diaryId,
+ required int mealId,
+ required String date,
+ }) async {
+ await diaryRepository.uploadPhotos(
+ files: files,
+ diaryId: diaryId,
+ mealId: mealId,
+ );
+ await getDiaryDay(date, false);
+ }
+
+ deletePhoto({
+ required DiaryPhotoModel photo,
+ required int diaryId,
+ required int mealId,
+ required String date,
+ }) async {
+ await diaryRepository.deletePhoto(
+ photo: photo,
+ diaryId: diaryId,
+ mealId: mealId,
+ );
+ await getDiaryDay(date, false);
+ }
}
diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart
index 32b70516..bb9e530a 100644
--- a/lib/features/diary/presentation/pages/diary_screen.dart
+++ b/lib/features/diary/presentation/pages/diary_screen.dart
@@ -1,3 +1,6 @@
+import 'dart:io';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
@@ -5,9 +8,12 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/locator_service.dart';
import 'package:eatvio_app/theme/theme_helper.dart';
import 'package:eatvio_app/widgets/custom_text_form_field.dart';
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
+import 'package:image_picker/image_picker.dart';
import 'package:rive/rive.dart';
import 'package:table_calendar/table_calendar.dart';
@@ -27,6 +33,7 @@ class _DiaryScreenState extends State {
GlossaryModel? selectedGlossary;
final productNameController = TextEditingController();
final quantityController = TextEditingController();
+ int currentSelectedPhotoIndex = 0;
@override
Widget build(BuildContext context) {
@@ -85,12 +92,14 @@ class _DiaryScreenState extends State {
);
}
- _products(List portions) {
+ _products(List portions, DiaryDayModel diaryDay) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < portions.length; i++) ...[
_product(
+ diaryDay,
+ portions[i].id,
portions[i].productName,
portions[i].quantity,
portions[i].portion,
@@ -101,10 +110,16 @@ class _DiaryScreenState extends State {
);
}
- _product(String product, int quantity, String portion) {
+ _product(DiaryDayModel diaryDay, int id, String product, int quantity, String portion) {
portion == 'gr' ? portion = 'г' : portion = 'шт';
return ListTile(
- onTap: () {},
+ onTap: () {
+ setState(() {
+ productNameController.text = product;
+ quantityController.text = quantity.toString();
+ });
+ _modalBottomSheet(diaryDay, id);
+ },
trailing: Icon(Icons.more_vert_rounded),
title: Text(product),
subtitle: Text('$quantity $portion'),
@@ -157,15 +172,184 @@ class _DiaryScreenState extends State {
const SizedBox(height: 12),
_divider(context),
const SizedBox(height: 12),
- _products(diaryDay.meals[currentSelectedMealNumber - 1].portions),
+ _products(diaryDay.meals[currentSelectedMealNumber - 1].portions, diaryDay),
const SizedBox(height: 16),
_addProductButton(diaryDay),
+ const SizedBox(height: 24),
+ if (diaryDay.meals[currentSelectedMealNumber - 1].photos.isEmpty)
+ GestureDetector(
+ onTap: () => _showImagePicker(diaryDay),
+ child: SvgPicture.asset(
+ width: MediaQuery.of(context).size.width - 32,
+ 'assets/no_one_image_was_added.svg',
+ ),
+ ),
+ if (diaryDay.meals[currentSelectedMealNumber - 1].photos.isNotEmpty)
+ _photoGallery(
+ diaryDay.meals[currentSelectedMealNumber - 1].photos,
+ diaryDay,
+ ),
],
),
),
);
}
+ _showImagePicker(DiaryDayModel diaryDay) {
+ showCupertinoModalPopup(
+ context: context,
+ builder: (context) => CupertinoActionSheet(
+ actions: [
+ CupertinoActionSheetAction(
+ onPressed: () async {
+ final picker = ImagePicker();
+ XFile? image = await picker.pickImage(source: ImageSource.camera);
+ if (image != null)
+ diaryCubit.uploadPhotos(
+ files: [File(image.path)],
+ diaryId: diaryDay.id,
+ mealId: diaryDay.meals[currentSelectedMealNumber - 1].id,
+ date: _selectedDay.toString(),
+ );
+ Navigator.of(context).pop();
+ },
+ child: const Row(
+ children: const [
+ Icon(Icons.photo_camera),
+ SizedBox(width: 8),
+ Text('Фото'),
+ ],
+ ),
+ ),
+ CupertinoActionSheetAction(
+ onPressed: () async {
+ final picker = ImagePicker();
+ List? images = await picker.pickMultiImage();
+ if (images.isNotEmpty)
+ diaryCubit.uploadPhotos(
+ files: images.map((f) => File(f.path)).toList(),
+ diaryId: diaryDay.id,
+ mealId: diaryDay.meals[currentSelectedMealNumber - 1].id,
+ date: _selectedDay.toString(),
+ );
+ Navigator.of(context).pop();
+ },
+ child: const Row(
+ children: const [
+ Icon(Icons.photo),
+ SizedBox(width: 8),
+ Text('Галерея'),
+ ],
+ ),
+ ),
+ ],
+ cancelButton: CupertinoActionSheetAction(
+ onPressed: () => Navigator.of(context).pop(),
+ isDestructiveAction: true,
+ child: const Text('Отмена'),
+ ),
+ ),
+ );
+ }
+
+ _photoGallery(List photos, DiaryDayModel diaryDay) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Stack(
+ alignment: Alignment.topRight,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: CachedNetworkImage(
+ imageUrl: baseUrl + '/storage/' + photos[currentSelectedPhotoIndex].path,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(12),
+ child: IconButton.filled(
+ style: IconButton.styleFrom(
+ backgroundColor: Colors.white,
+ ),
+ onPressed: () {
+ setState(() {
+ if (currentSelectedPhotoIndex == photos.length - 1 && currentSelectedPhotoIndex != 0) {
+ currentSelectedPhotoIndex = photos.length - 2;
+ }
+ });
+ diaryCubit.deletePhoto(
+ photo: photos[currentSelectedPhotoIndex],
+ diaryId: diaryDay.id,
+ mealId: diaryDay.meals[currentSelectedMealNumber - 1].id,
+ date: _selectedDay.toString(),
+ );
+ },
+ icon: Icon(
+ Icons.close_rounded,
+ color: Colors.grey,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ SingleChildScrollView(
+ physics: const BouncingScrollPhysics(),
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ for (int i = 0; i < photos.length + 1; i++) ...[
+ if (i == photos.length)
+ Padding(
+ padding: const EdgeInsets.only(left: 8),
+ child: InkWell(
+ onTap: () => _showImagePicker(diaryDay),
+ borderRadius: BorderRadius.circular(8),
+ child: Ink(
+ height: 52,
+ width: 52,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8),
+ color: Color(0xFFF1F2F6),
+ ),
+ child: Icon(
+ Icons.add_rounded,
+ color: appTheme.blueA400,
+ ),
+ ),
+ ),
+ ),
+ if (i < photos.length)
+ GestureDetector(
+ onTap: () {
+ setState(() {
+ currentSelectedPhotoIndex = i;
+ });
+ },
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: SizedBox(
+ height: 52,
+ width: 52,
+ child: CachedNetworkImage(
+ imageUrl: baseUrl + '/storage/' + photos[i].path,
+ fit: BoxFit.fitWidth,
+ ),
+ ),
+ ),
+ ),
+ if (i < photos.length - 1) const SizedBox(width: 8),
+ ],
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
_mealButton(int mealNumber) {
bool isActive = currentSelectedMealNumber == mealNumber;
@@ -285,117 +469,152 @@ class _DiaryScreenState extends State {
);
}
- _addProductButton(DiaryDayModel diaryDay) {
- return InkWell(
- onTap: () {
- setState(() {
- selectedGlossary = null;
- productNameController.clear();
- quantityController.clear();
- });
- showModalBottomSheet(
- useRootNavigator: true,
- context: context,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
- ),
- isScrollControlled: true,
- builder: (context) {
- return StatefulBuilder(
- builder: (BuildContext context, void Function(void Function()) setState) {
- return Padding(
- padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- 'Добавление блюда',
- style: TextStyle(
- fontSize: 20,
- fontWeight: FontWeight.w600,
- color: Color(0xFF323F4B),
- ),
- ),
- const SizedBox(height: 16),
- TypeAheadField(
- controller: productNameController,
- itemBuilder: (context, glossary) {
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(
- glossary.name,
- style: TextStyle(
- height: 1,
- fontSize: 16,
- ),
- ),
- );
- },
- itemSeparatorBuilder: (context, index) => _divider(context),
- onSelected: (newSelectedGlossary) {
- setState(() {
- selectedGlossary = newSelectedGlossary;
- productNameController.text = selectedGlossary!.name;
- });
- },
- suggestionsCallback: (query) async {
- if (query.length < 1) return [];
- return await diaryCubit.getGlossaryListByQuery(query);
- },
- builder: (context, controller, focusNode) {
- return CustomTextFormField(
- controller: controller,
- focusNode: focusNode,
- autofocus: true,
- hintText: 'Введите название продукта/блюда',
- );
- },
- emptyBuilder: (context) => const SizedBox.shrink(),
- errorBuilder: (context, object) => Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text('Нет подходящих вариантов'),
- ),
- ),
- if (selectedGlossary != null)
- Padding(
- padding: const EdgeInsets.only(top: 8),
- child: CustomTextFormField(
- controller: quantityController,
- keyboardType: TextInputType.number,
- hintText: selectedGlossary!.grInPortion == 0 ? '100' : '1',
- suffixText: selectedGlossary != null
- ? selectedGlossary!.grInPortion == 0
- ? 'гр'
- : 'шт'
- : null,
+ _modalBottomSheet(DiaryDayModel diaryDay, [int? portionId]) {
+ return showModalBottomSheet(
+ useRootNavigator: true,
+ context: context,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
+ ),
+ isScrollControlled: true,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (BuildContext context, void Function(void Function()) setState) {
+ return Padding(
+ padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ 'Добавление блюда',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF323F4B),
+ ),
+ ),
+ const SizedBox(height: 16),
+ TypeAheadField(
+ controller: productNameController,
+ itemBuilder: (context, glossary) {
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ glossary.name,
+ style: TextStyle(
+ height: 1,
+ fontSize: 16,
),
),
- const SizedBox(height: 16),
+ );
+ },
+ itemSeparatorBuilder: (context, index) => _divider(context),
+ onSelected: (newSelectedGlossary) {
+ setState(() {
+ selectedGlossary = newSelectedGlossary;
+ productNameController.text = selectedGlossary!.name;
+ });
+ },
+ suggestionsCallback: (query) async {
+ if (query.length < 1) return [];
+ final suggestionsList = await diaryCubit.getGlossaryListByQuery(query);
+ if (suggestionsList.length == 1) {
+ setState(() {
+ selectedGlossary = suggestionsList.first;
+ });
+ }
+ return suggestionsList;
+ },
+ builder: (context, controller, focusNode) {
+ return CustomTextFormField(
+ controller: controller,
+ autofocus: true,
+ focusNode: focusNode,
+ hintText: 'Введите название продукта/блюда',
+ );
+ },
+ emptyBuilder: (context) => const SizedBox.shrink(),
+ errorBuilder: (context, object) => Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text('Нет подходящих вариантов'),
+ ),
+ ),
+ if (selectedGlossary != null)
+ Padding(
+ padding: const EdgeInsets.only(top: 8),
+ child: CustomTextFormField(
+ controller: quantityController,
+ keyboardType: TextInputType.number,
+ hintText: selectedGlossary!.grInPortion == 0 ? '100' : '1',
+ autofocus: true,
+ suffixText: selectedGlossary!.grInPortion == 0 ? 'гр' : 'шт',
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ if (portionId != null)
+ InkWell(
+ onTap: () {
+ diaryCubit.deleteProductFromMeal(
+ diaryId: diaryDay.id,
+ mealId: diaryDay.meals[currentSelectedMealNumber - 1].id,
+ portionId: portionId,
+ date: _selectedDay.toString(),
+ );
+ Navigator.of(context).pop();
+ },
+ borderRadius: BorderRadius.circular(20),
+ child: Ink(
+ width: 52,
+ height: 52,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20),
+ color: Color(0xFFF6F6F6),
+ ),
+ child: Icon(
+ Icons.delete_outline_rounded,
+ color: Colors.red,
+ ),
+ ),
+ ),
+ if (portionId != null) const SizedBox(width: 8),
InkWell(
onTap: () {
- diaryCubit.addProductToMeal(
- diaryId: diaryDay.id,
- mealId: diaryDay.meals[currentSelectedMealNumber - 1].id,
- productName: productNameController.text,
- portion: selectedGlossary!.grInPortion == 0 ? 'gr' : 'pcs',
- quantity: int.parse(quantityController.text),
- date: _selectedDay.toString(),
- );
+ if (portionId == null) {
+ diaryCubit.addProductToMeal(
+ diaryId: diaryDay.id,
+ mealId: diaryDay.meals[currentSelectedMealNumber - 1].id,
+ productName: productNameController.text,
+ portion: selectedGlossary!.grInPortion == 0 ? 'gr' : 'pcs',
+ quantity: int.parse(quantityController.text),
+ date: _selectedDay.toString(),
+ );
+ } else {
+ diaryCubit.updateProductInMeal(
+ diaryId: diaryDay.id,
+ mealId: diaryDay.meals[currentSelectedMealNumber - 1].id,
+ portionId: portionId,
+ productName: productNameController.text,
+ portion: selectedGlossary!.grInPortion == 0 ? 'gr' : 'pcs',
+ quantity: int.parse(quantityController.text),
+ date: _selectedDay.toString(),
+ );
+ }
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(20),
child: Ink(
+ width: MediaQuery.of(context).size.width - (portionId == null ? 32 : 92),
+ height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
- child: Container(
- width: MediaQuery.of(context).size.width - 32,
- padding: const EdgeInsets.all(16),
- alignment: Alignment.center,
+ child: Center(
child: Text(
- 'Добавить',
+ portionId == null ? 'Добавить' : 'Сохранить',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
@@ -407,12 +626,25 @@ class _DiaryScreenState extends State {
),
],
),
- );
- },
+ ],
+ ),
);
},
);
},
+ );
+ }
+
+ _addProductButton(DiaryDayModel diaryDay) {
+ return InkWell(
+ onTap: () {
+ setState(() {
+ selectedGlossary = null;
+ productNameController.clear();
+ quantityController.clear();
+ });
+ _modalBottomSheet(diaryDay);
+ },
borderRadius: BorderRadius.circular(20),
child: Ink(
padding: EdgeInsets.symmetric(horizontal: 16),
diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart
index dde8a702..b9b8243c 100644
--- a/lib/features/diary/repositories/diary_repository.dart
+++ b/lib/features/diary/repositories/diary_repository.dart
@@ -1,3 +1,4 @@
+import 'dart:io';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
@@ -34,4 +35,71 @@ class DiaryRepository {
quantity: quantity,
);
}
+
+ Future updateProductInMeal({
+ required int diaryId,
+ required int mealId,
+ required int portionId,
+ required String productName,
+ required String portion,
+ required int quantity,
+ }) async {
+ return await diaryService.updateProductInMeal(
+ diaryId: diaryId,
+ mealId: mealId,
+ portionId: portionId,
+ productInMeal: ProductInMeal(
+ productName: productName,
+ portion: portion,
+ quantity: quantity,
+ ),
+ );
+ }
+
+ Future deleteProductFromMeal({
+ required int diaryId,
+ required int mealId,
+ required int portionId,
+ }) async {
+ return await diaryService.deleteProductFromMeal(
+ diaryId: diaryId,
+ mealId: mealId,
+ portionId: portionId,
+ );
+ }
+
+ Future uploadPhotos({
+ required int diaryId,
+ required int mealId,
+ required List files,
+ }) async {
+ try {
+ for (int i = 0; i < files.length; i++) {
+ await diaryService.uploadPhotoOfMeal(
+ diaryId: diaryId,
+ mealId: mealId,
+ photo: files[i],
+ );
+ }
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future deletePhoto({
+ required int diaryId,
+ required int mealId,
+ required DiaryPhotoModel photo,
+ }) async {
+ try {
+ await diaryService.deletePhotoOfMeal(
+ diaryId: diaryId,
+ mealId: mealId,
+ id: photo.id,
+ path: photo.path,
+ );
+ } catch (e) {
+ rethrow;
+ }
+ }
}
diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart
index cdce2dd4..16990b1b 100644
--- a/lib/features/diary/services/diary_service.dart
+++ b/lib/features/diary/services/diary_service.dart
@@ -1,3 +1,4 @@
+import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/diary_index_model.dart';
@@ -19,6 +20,22 @@ abstract class DiaryService {
@Path('diary_id') required int index,
});
+ @POST('/api/diary/{diary_id}/meal/{meal_id}/photo/upload')
+ @MultiPart()
+ Future uploadPhotoOfMeal({
+ @Path('diary_id') required int diaryId,
+ @Path('meal_id') required int mealId,
+ @Part(name: 'photo') required File photo,
+ });
+
+ @DELETE('/api/diary/{diary_id}/meal/{meal_id}/photo/delete')
+ Future deletePhotoOfMeal({
+ @Path('diary_id') required int diaryId,
+ @Path('meal_id') required int mealId,
+ @Query('id') required int id,
+ @Query('path') required String path,
+ });
+
@POST('/api/diary/{diary_id}/meal/{meal_id}/portions/create')
Future addProductToMeal({
@Path('diary_id') required int diaryId,
@@ -27,4 +44,19 @@ abstract class DiaryService {
@Part(name: 'portion') required String portion,
@Part(name: 'quantity') required int quantity,
});
+
+ @PUT('/api/diary/{diary_id}/meal/{meal_id}/portions/{portion_id}/update')
+ Future updateProductInMeal({
+ @Path('diary_id') required int diaryId,
+ @Path('meal_id') required int mealId,
+ @Path('portion_id') required int portionId,
+ @Body() required ProductInMeal productInMeal,
+ });
+
+ @DELETE('/api/diary/{diary_id}/meal/{meal_id}/portions/{portion_id}/delete')
+ Future deleteProductFromMeal({
+ @Path('diary_id') required int diaryId,
+ @Path('meal_id') required int mealId,
+ @Path('portion_id') required int portionId,
+ });
}
diff --git a/lib/features/diary/services/diary_service.g.dart b/lib/features/diary/services/diary_service.g.dart
index b145757c..74de16af 100644
--- a/lib/features/diary/services/diary_service.g.dart
+++ b/lib/features/diary/services/diary_service.g.dart
@@ -87,6 +87,76 @@ class _DiaryService implements DiaryService {
return _value;
}
+ @override
+ Future uploadPhotoOfMeal({
+ required int diaryId,
+ required int mealId,
+ required File photo,
+ }) async {
+ final _extra = {};
+ final queryParameters = {};
+ final _headers = {};
+ final _data = FormData();
+ _data.files.add(MapEntry(
+ 'photo',
+ MultipartFile.fromFileSync(
+ photo.path,
+ filename: photo.path.split(Platform.pathSeparator).last,
+ ),
+ ));
+ final _options = _setStreamType(Options(
+ method: 'POST',
+ headers: _headers,
+ extra: _extra,
+ contentType: 'multipart/form-data',
+ )
+ .compose(
+ _dio.options,
+ '/api/diary/${diaryId}/meal/${mealId}/photo/upload',
+ queryParameters: queryParameters,
+ data: _data,
+ )
+ .copyWith(
+ baseUrl: _combineBaseUrls(
+ _dio.options.baseUrl,
+ baseUrl,
+ )));
+ await _dio.fetch(_options);
+ }
+
+ @override
+ Future deletePhotoOfMeal({
+ required int diaryId,
+ required int mealId,
+ required int id,
+ required String path,
+ }) async {
+ final _extra = {};
+ final queryParameters = {
+ r'id': id,
+ r'path': path,
+ };
+ final _headers = {};
+ const Map? _data = null;
+ final _options = _setStreamType(Options(
+ method: 'DELETE',
+ headers: _headers,
+ extra: _extra,
+ )
+ .compose(
+ _dio.options,
+ '/api/diary/${diaryId}/meal/${mealId}/photo/delete',
+ queryParameters: queryParameters,
+ data: _data,
+ )
+ .copyWith(
+ baseUrl: _combineBaseUrls(
+ _dio.options.baseUrl,
+ baseUrl,
+ )));
+ await _dio.fetch(_options);
+ }
+
@override
Future addProductToMeal({
required int diaryId,
@@ -130,6 +200,65 @@ class _DiaryService implements DiaryService {
await _dio.fetch(_options);
}
+ @override
+ Future updateProductInMeal({
+ required int diaryId,
+ required int mealId,
+ required int portionId,
+ required ProductInMeal productInMeal,
+ }) async {
+ final _extra = {};
+ final queryParameters = {};
+ final _headers = {};
+ final _data = productInMeal;
+ final _options = _setStreamType(Options(
+ method: 'PUT',
+ headers: _headers,
+ extra: _extra,
+ )
+ .compose(
+ _dio.options,
+ '/api/diary/${diaryId}/meal/${mealId}/portions/${portionId}/update',
+ queryParameters: queryParameters,
+ data: _data,
+ )
+ .copyWith(
+ baseUrl: _combineBaseUrls(
+ _dio.options.baseUrl,
+ baseUrl,
+ )));
+ await _dio.fetch(_options);
+ }
+
+ @override
+ Future deleteProductFromMeal({
+ required int diaryId,
+ required int mealId,
+ required int portionId,
+ }) async {
+ final _extra = {};
+ final queryParameters = {};
+ final _headers = {};
+ const Map? _data = null;
+ final _options = _setStreamType(Options(
+ method: 'DELETE',
+ headers: _headers,
+ extra: _extra,
+ )
+ .compose(
+ _dio.options,
+ '/api/diary/${diaryId}/meal/${mealId}/portions/${portionId}/delete',
+ queryParameters: queryParameters,
+ data: _data,
+ )
+ .copyWith(
+ baseUrl: _combineBaseUrls(
+ _dio.options.baseUrl,
+ baseUrl,
+ )));
+ await _dio.fetch(_options);
+ }
+
RequestOptions _setStreamType(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart
index b94f2f71..380d37c4 100644
--- a/lib/features/profile/presentation/pages/profile_screen.dart
+++ b/lib/features/profile/presentation/pages/profile_screen.dart
@@ -112,7 +112,7 @@ class _ProfileScreenState extends State {
),
),
_subscriptionsAndSubscribers(state),
- _badges(state),
+ if(state.profile.badges.isNotEmpty) _badges(state),
if (widget.author == null) const SizedBox(height: 16),
_socialLinks(state),
if (widget.author == null) const SizedBox(height: 16),
@@ -201,42 +201,39 @@ class _ProfileScreenState extends State {
}
_newBlogPostButton() {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 4),
- child: Material(
- color: Colors.white,
- borderRadius: BorderRadius.circular(12),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: InkWell(
- onTap: () {
- GoRouter.of(context).pushNamed('blogPostCreateScreen');
- },
- borderRadius: BorderRadius.circular(8),
- child: Ink(
- padding: EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: Color(0xFFF6FAFF),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.add_circle_outline_rounded,
+ return Material(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: InkWell(
+ onTap: () {
+ GoRouter.of(context).pushNamed('blogPostCreateScreen');
+ },
+ borderRadius: BorderRadius.circular(8),
+ child: Ink(
+ padding: EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Color(0xFFF6FAFF),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.add_circle_outline_rounded,
+ color: Color(0xFF2F80ED),
+ ),
+ const SizedBox(width: 12),
+ Text(
+ 'Новая запись',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
color: Color(0xFF2F80ED),
),
- const SizedBox(width: 12),
- Text(
- 'Новая запись',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w600,
- color: Color(0xFF2F80ED),
- ),
- ),
- ],
- ),
+ ),
+ ],
),
),
),
@@ -672,7 +669,8 @@ class _ProfileScreenState extends State {
state.profile.name,
style: TextStyle(
color: Colors.black,
- fontWeight: FontWeight.w500,
+ fontWeight: FontWeight.w600,
+ fontSize: 16,
height: 1,
),
),
@@ -689,6 +687,8 @@ class _ProfileScreenState extends State {
'Дополнительно',
style: TextStyle(
color: Color(0xFF6D6F71),
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
),
),
],