diff --git a/.metadata b/.metadata new file mode 100644 index 00000000..aa90aa80 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ba393198430278b6595976de84fe170f553cc728" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: android + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: ios + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: linux + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: macos + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: web + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: windows + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/android/app/src/main/kotlin/com/eatvioapp/eatvio_app/MainActivity.kt b/android/app/src/main/kotlin/com/eatvioapp/eatvio_app/MainActivity.kt new file mode 100644 index 00000000..f3e4d58d --- /dev/null +++ b/android/app/src/main/kotlin/com/eatvioapp/eatvio_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.eatvioapp.eatvio_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/eatvio_app_android.iml b/android/eatvio_app_android.iml new file mode 100644 index 00000000..18999696 --- /dev/null +++ b/android/eatvio_app_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/eatvio_app.iml b/eatvio_app.iml new file mode 100644 index 00000000..f66303d5 --- /dev/null +++ b/eatvio_app.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart index 569164b1..be0a7a9d 100644 --- a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart +++ b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_bloc.dart @@ -18,8 +18,7 @@ class DiaryEditBloc extends Bloc { on(_onInitialize); on(_changeSatiety); // пользователь нажимает на шкалу сытости on(_changeMood); // пользователь нажимает на шкалу настроения - on( - _changeMealTime); // пользователь изменяет время приема пиши + on(_changeMealTime); // пользовательяет времяа пиши on(_changeMeal); on(_changeWakeup); // пользователь выбирает время пробуждения on(_changeRetire); // пользователь выбирает время отхода ко сну @@ -32,6 +31,10 @@ class DiaryEditBloc extends Bloc { on(_addComment); //пользователь нажимает "Добавить комментарий" on( _inputProduct); //пользователь нажимает "Добавить комментарий" + on(_finishDay); //пользователь нажимает "Добавить комментарий" + on(_sendModeration); + on( + reloadScreen); //пользователь нажимает "Добавить комментарий" } Future _onInitialize( @@ -40,7 +43,7 @@ class DiaryEditBloc extends Bloc { ) async { try { final token = - "laravel_sanctum_1lPFtd9tpGmHYS243C4K7YF8BrVhxxgXm8cTmjuZda1da694"; + "9|laravel_sanctum_KFXbccDcWEvTPfJmV99EQv1neAggmpR2TidVGDPN34fa201a"; final response = await http.get( Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/index'), headers: { @@ -48,20 +51,20 @@ class DiaryEditBloc extends Bloc { 'Authorization': 'Bearer $token', }, ); - if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data is Map && data.containsKey('id')) { final int diaryId = data['id']; try { final response = await http.get( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/show/$diaryId'), + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/$diaryId/show/'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); final diary = jsonDecode(response.body)['diary']; + print("Screen load success"); if (diary is Map) { final String diaryDate = diary['date'] ?? DateTime.now().toString(); @@ -73,13 +76,20 @@ class DiaryEditBloc extends Bloc { currentMeal: currentState.currentMeal, wakeupExpanded: currentState.wakeupExpanded, moodExpanded: currentState.moodExpanded, + moderationExpanded: currentState.moderationExpanded, token: currentState.token, meals: currentState.meals, + portions: currentState.portions, + photos: currentState.photos, wakeupController: currentState.wakeupController, retireController: currentState.retireController, commentController: currentState.commentController, mealTimeController: currentState.mealTimeController, productNameController: currentState.productNameController, + caloriesController: currentState.caloriesController, + proteinsController: currentState.proteinsController, + fatsController: currentState.fatsController, + carbsController: currentState.carbsController, selectedSatiety: currentState.selectedSatiety, selectedMood: currentState.selectedMood, diaryDate: currentState.diaryDate, @@ -95,9 +105,11 @@ class DiaryEditBloc extends Bloc { id: 1, title: "Прием пищи 1", isSelected: true), wakeupExpanded: true, moodExpanded: true, + moderationExpanded: false, token: token, meals: diary['meals'], portions: diary['meals'][0]['portions'], + photos: diary['meals'][0]['photos'], wakeupController: toTime(diary['wakeup'] ?? "09:00"), retireController: toTime(diary['retire'] ?? "23:00"), mealTimeController: @@ -108,6 +120,10 @@ class DiaryEditBloc extends Bloc { TextEditingController(text: diary['comment']), productNameController: TextEditingController(), productQuantityController: TextEditingController(), + caloriesController: TextEditingController(), + proteinsController: TextEditingController(), + fatsController: TextEditingController(), + carbsController: TextEditingController(), diaryId: diaryId, diaryDate: diaryDate, autocompleteList: [], @@ -135,6 +151,90 @@ class DiaryEditBloc extends Bloc { } } catch (e) { // Ошибка при получении данных дневника + emit(DiaryEditErrorState('Ошибка при получении данных о дневнике: $e')); + } + } + + reloadScreen(DiaryEditEvent event, Emitter emit) async { + print("Reloading..."); + try { + final token = + "9|laravel_sanctum_KFXbccDcWEvTPfJmV99EQv1neAggmpR2TidVGDPN34fa201a"; + + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/index'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map && data.containsKey('id')) { + final int diaryId = data['id']; + + final diaryResponse = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diary/$diaryId/show/'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + ); + + final diary = jsonDecode(diaryResponse.body)['diary']; + + if (diary is Map) { + final String diaryDate = diary['date'] ?? DateTime.now().toString(); + // Продолжайте обработку данных дневника + // Если текущее состояние - начальное, создаем новое загруженное состояние + try { + await Future(() => emit(DiaryEditLoadedState( + currentMeal: SelectionPopupModel( + id: 1, title: "Прием пищи 1", isSelected: true), + wakeupExpanded: true, + moodExpanded: true, + moderationExpanded: false, + token: token, + meals: diary['meals'], + portions: diary['meals'][0]['portions'], + photos: diary['meals'][0]['photos'], + wakeupController: toTime(diary['wakeup'] ?? "09:00"), + retireController: toTime(diary['retire'] ?? "23:00"), + mealTimeController: + toTime(diary['meals'][0]['time'] ?? "10:00"), + selectedSatiety: diary['meals'][0]['satiety'], + selectedMood: diary['meals'][0]['mood'], + commentController: + TextEditingController(text: diary['comment']), + productNameController: TextEditingController(), + productQuantityController: TextEditingController(), + caloriesController: TextEditingController(), + proteinsController: TextEditingController(), + fatsController: TextEditingController(), + carbsController: TextEditingController(), + diaryId: diaryId, + diaryDate: diaryDate, + autocompleteList: [], + diaryEditModelObj: state.diaryEditModelObj + ?.copyWith(mealList: fillMealList())))); + + print("Updated"); + } catch (e) { + print("Ошибка при создании нового состояния $e"); + } + } else { + emit(DiaryEditErrorState( + 'Ошибка при декодировании данных дневника')); + } + } else { + emit( + DiaryEditErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + emit(DiaryEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { emit(DiaryEditErrorState('Ошибка при получении данных дневника: $e')); } } @@ -229,18 +329,31 @@ class DiaryEditBloc extends Bloc { WakeupEvent event, Emitter emit) async { if (state is DiaryEditLoadedState) { var currentState = state as DiaryEditLoadedState; - var updatedState = //инициализируем новое состояние - currentState.copyWith(wakeupController: event.selectedTime); - - // Отправляем данные по HTTP маршруту и ждем ответ var response = await sendWakeupData( currentState.token, event.selectedTime, currentState.diaryId); - if (response.statusCode == 200) { - //Чекаем статус ответа - updatedState.wakeupController = event.selectedTime; + var updatedState = + currentState.copyWith(wakeupController: event.selectedTime); + print("Wakeup time changed to ${event.selectedTime}"); emit(updatedState); // Передаем обновленное состояние в emit - } + } else + print("Wakeup time change error"); + } + } + + FutureOr _changeRetire( + RetireEvent event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + var response = await sendRetireData( + currentState.token, event.selectedTime, currentState.diaryId); + if (response.statusCode == 200) { + var updatedState = + currentState.copyWith(retireController: event.selectedTime); + print("Retire time changed to ${event.selectedTime}"); + emit(updatedState); // Передаем обновленное состояние в emit + } else + print("Retire time change error"); } } @@ -254,39 +367,20 @@ class DiaryEditBloc extends Bloc { final wakeupString = formatter.format(DateTime(2022, 1, 1, wakeup.hour, wakeup.minute)); final Map credentials = { - 'wakeup': wakeupString, + 'wakeUp': wakeupString, }; - var response = await http.post( - Uri.parse(url), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $token', - }, - body: json.encode(credentials), - encoding: utf8, - ); - print(response.body); + + var response = await http.post(Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8); + return response; } - FutureOr _changeRetire( - RetireEvent event, Emitter emit) async { - if (state is DiaryEditLoadedState) { - var currentState = state as DiaryEditLoadedState; - var updatedState = - currentState.copyWith(retireController: event.selectedTime); - - // Отправляем данные по HTTP маршруту и ждем ответ - var response = await sendRetireData( - currentState.token, event.selectedTime, currentState.diaryId); - - if (response.statusCode == 200) { - updatedState.retireController = event.selectedTime; - emit(updatedState); // Передаем обновленное состояние в emit - } - } - } - Future sendRetireData( String? token, TimeOfDay retire, int diaryID) async { String url = dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/retire/fill'; @@ -342,18 +436,23 @@ class DiaryEditBloc extends Bloc { return response; } - FutureOr _changeMeal(ChangeMeal event, Emitter emit) { + FutureOr _changeMeal( + ChangeMeal event, Emitter emit) async { if (state is DiaryEditLoadedState) { var currentState = state as DiaryEditLoadedState; + final meal = currentState.meals?.firstWhere( + (meal) => meal['type'] == event.value.id, + orElse: () => null); + int? id = event.value.id; - emit(currentState.copyWith( + DiaryEditLoadedState updatedState = currentState.copyWith( currentMeal: event.value, - portions: event.meals?[event.value.id ?? 0]['portions'], - mealTimeController: - toTime(event.meals?[event.value.id ?? 0]['time'] ?? "1$id:00"), - selectedMood: event.meals?[event.value.id ?? 0]['mood'], - selectedSatiety: event.meals?[event.value.id ?? 0]['satiety'])); - currentState.currentMeal = event.value; + portions: meal['portions'], + photos: meal['photos'], + mealTimeController: toTime(meal['time'] ?? "1$id:00"), + selectedMood: meal['mood'], + selectedSatiety: meal['satiety']); + emit(updatedState); } } @@ -382,64 +481,60 @@ class DiaryEditBloc extends Bloc { if (state is DiaryEditLoadedState) { var currentState = state as DiaryEditLoadedState; - int? id = currentState.currentMeal?.id; - - // Отправляем данные по HTTP маршруту и ждем ответ var response = await sendPortionData( - currentState.token, - currentState.productNameController?.text, - 'pcs', - int.parse(currentState.productQuantityController!.text), - currentState.currentMeal!.id, - currentState.diaryId); + currentState.token, + currentState.productNameController?.text, + 'pcs', + int.parse(currentState.productQuantityController!.text), + currentState.currentMeal!.id, + currentState.diaryId, + ); + + print(response.body); + if (response.statusCode == 200) { - var updatedState = currentState.copyWith( - portions: currentState.meals?[id ?? 0]['portions'], - ); - emit(updatedState); // Передаем обновленное состояние в emit + await reloadScreen(event, emit); + } else { + print( + 'Failed to add portion. Status Code: ${response.statusCode} ${response.body}'); } } } - Future sendPortionData( - String? token, - String? name, - String portion, - int? quantity, - int? mealID, - int diaryID, - ) async { - String url = dotenv.env['SERVER_ROOT']! + - '/diary/$diaryID/meal/$mealID/portions/create'; + FutureOr sendPortionData(String? token, String? name, + String portion, int? quantity, int? mealID, int diaryID) async { + String url = + '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealID/portions/create'; + final Map credentials = { 'product_name': name, 'portion': portion, 'quantity': quantity, }; - print(credentials); - var response = await http.post(Uri.parse(url), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $token', - }, - body: json.encode(credentials), - encoding: utf8); - print(response.body); + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8, + ); + return response; } - FutureOr _inputProduct( + Future _inputProduct( ProductInput event, Emitter emit) async { if (state is DiaryEditLoadedState) { var currentState = state as DiaryEditLoadedState; String? token = currentState.token; - final Map credentials = { - 'name': event.input, - }; - final uri = Uri.parse(dotenv.env['SERVER_ROOT']! + - '/diary/product_nutritional_values/filter?' + - Uri(queryParameters: credentials).query); + + final Map credentials = {'name': event.input}; + final uri = Uri.parse( + '${dotenv.env['SERVER_ROOT']!}/diary/product_nutritional_values/filter?${Uri(queryParameters: credentials).query}'); + final response = await http.get( uri, headers: { @@ -447,29 +542,210 @@ class DiaryEditBloc extends Bloc { 'Authorization': 'Bearer $token', }, ); + final productNutritionalValues = jsonDecode(response.body)['product_nutritional_value']; - if (productNutritionalValues is List) { - var updatedState = currentState.copyWith( - productNameController: TextEditingController(text: event.input), - autocompleteList: productNutritionalValues - .map((product) => product['name']) - .toList()); - emit(updatedState); - } else { - print("Ошибка в декодировании ответа из API"); + if (productNutritionalValues is List) { + // Create a new controller with the input text and update the autocomplete list + var updatedState = currentState.copyWith( + productNameController: TextEditingController(text: event.input), + autocompleteList: productNutritionalValues + .map((product) => product['name']) + .toList(), + ); + if (event.input.length > 3) { + updatedState = updatedState.copyWith(moderationExpanded: true); + } else { + updatedState = updatedState.copyWith(moderationExpanded: false); + } + + emit(updatedState); // Emit the updated state } } } - FutureOr _removePortion( - RemovePortionEvent event, Emitter emit) {} + Future _removePortion( + RemovePortionEvent event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; - Future> _uploadImages( + int? mealId = + currentState.currentMeal?.id; // Rename id to mealId for clarity + + var response = await sendRemovePortion( + currentState.token, + event.portionId, + mealId, + currentState.diaryId, + ); + + if (response.statusCode == 204) { + await reloadScreen(event, emit); + } else { + print( + 'Failed to add portion. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + Future sendRemovePortion( + String? token, int? portionID, int? mealID, int diaryID) async { + String url = + '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealID/portions/$portionID/delete'; + + var response = await http.delete( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + encoding: utf8, + ); + + return response; + } + + Future _finishDay(FinishDay event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + var response = await sendFinishDay( + currentState.token, + currentState.diaryId, + currentState.wakeupController, + currentState.retireController, + currentState.commentController?.text, + ); + + if (response.statusCode == 200) { + print("finished"); + } + } + } + + Future sendFinishDay(String? token, int diaryID, + TimeOfDay? wakeup, TimeOfDay? retire, String? comment) async { + final formatter = DateFormat('HH:mm'); + final wakeupString = + formatter.format(DateTime(2022, 1, 1, wakeup!.hour, wakeup.minute)); + final retireString = + formatter.format(DateTime(2022, 1, 1, retire!.hour, retire.minute)); + + final Map credentials = { + 'wakeup': wakeupString, + 'retire': retireString, + 'comment': comment ?? " ", + }; + + String url = '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/finishDay'; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } + + Future _uploadImages( UploadImages event, Emitter emit) async { - String imageUrl = await uploadImageToServer(event.image.path); - print(imageUrl); + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + + await uploadImageToServer(event.image.path, currentState.diaryId, + currentState.currentMeal!.id!, currentState.token); + await reloadScreen(event, emit); + } + } + + Future uploadImageToServer( + String imagePath, int diaryID, int mealId, String? token) async { + String url = + '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealId/photo/upload'; + + var request = http.MultipartRequest('POST', Uri.parse(url)); + request.headers['Authorization'] = 'Bearer $token'; + + request.files.add(await http.MultipartFile.fromPath('photo', imagePath)); + + var client = http.Client(); + var streamedResponse = await client.send(request); + + if (streamedResponse.statusCode == 200) { + var response = await http.Response.fromStream(streamedResponse); + var imageUrl = response.body; + print('Image successfully uploaded'); + return imageUrl; + } else { + throw Exception( + 'Failed to upload image. Status code: ${streamedResponse.statusCode}}'); + } + } + + FutureOr _sendModeration( + SendModeration event, Emitter emit) async { + if (state is DiaryEditLoadedState) { + var currentState = state as DiaryEditLoadedState; + final response = await _sendModerationData( + event.name, + event.energyOn100gr, + event.proteinsOn100gr, + event.fatsOn100gr, + event.carbohydratesOn100gr, + currentState.token, + ); + + if (response.statusCode == 200) + print("Moderation success"); + else + print("Moderation error"); + } + } + + Future _sendModerationData( + String name, + String energy_on_100gr, + String proteins_on_100gr, + String fats_on_100gr, + String carbohydrates_on_100gr, + String? token) async { + final Uri url = Uri.parse( + '${dotenv.env['SERVER_ROOT']!}/diary/product_nutritional_values/sendToModeration'); + final Uri serverURL = Uri.parse('${dotenv.env['SERVER_ROOT']!}'); + String host = serverURL.host; + final Map credentials = { + 'name': name, + 'energy_on_100gr': energy_on_100gr, + 'proteins_on_100gr': proteins_on_100gr, + 'fats_on_100gr': fats_on_100gr, + 'carbohydrates_on_100gr': carbohydrates_on_100gr, + }; + + var headers = { + //'Content-Length': bodyBytes, + 'Authorization': 'Bearer $token', + 'Host': host, + }; + + var response = await http.post( + url, + headers: headers, + body: credentials, + encoding: utf8, + ); + + if (response.statusCode == 200) { + return response; + } else { + throw Exception( + 'Failed to send a product for moderation ${response.statusCode}'); + } } FutureOr _addComment( @@ -508,24 +784,6 @@ class DiaryEditBloc extends Bloc { return response; } - Future uploadImageToServer(String imagePath) async { - var request = http.MultipartRequest( - 'POST', - Uri.parse( - 'http://your-api-url.com/upload'), // Замените на ваш маршрут API - ); - - request.files.add(await http.MultipartFile.fromPath('image', imagePath)); - - var response = await request.send(); - if (response.statusCode == 200) { - var responseData = await response.stream.bytesToString(); - return responseData; - } else { - throw Exception('Ошибка при загрузке изображения'); - } - } - TimeOfDay toTime(String timeStr) { List timeParts = timeStr.split(":"); int hour = int.parse(timeParts[0]); diff --git a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_event.dart b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_event.dart index ed6f8e2f..6ab61626 100644 --- a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_event.dart +++ b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_event.dart @@ -113,3 +113,34 @@ class ChangeMeal extends DiaryEditEvent { @override List get props => [value, meals]; } + +class FinishDay extends DiaryEditEvent { + FinishDay(); + + @override + List get props => []; +} + +class RefreshEvent extends DiaryEditEvent { + RefreshEvent(); + + @override + List get props => []; +} + +class SendModeration extends DiaryEditEvent { + SendModeration( + {required this.name, + required this.energyOn100gr, + required this.proteinsOn100gr, + required this.fatsOn100gr, + required this.carbohydratesOn100gr}); + String name; + String energyOn100gr; + String proteinsOn100gr; + String fatsOn100gr; + String carbohydratesOn100gr; + @override + List get props => + [name, energyOn100gr, proteinsOn100gr, fatsOn100gr, carbohydratesOn100gr]; +} diff --git a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_state.dart b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_state.dart index c518e9af..abaed360 100644 --- a/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_state.dart +++ b/lib/presentation/diary/diary_edit_screen/bloc/diary_edit_state.dart @@ -24,6 +24,7 @@ class DiaryEditLoadedState extends DiaryEditState { this.token, this.meals, this.portions, + this.photos, this.autocompleteList, this.wakeupController, this.retireController, @@ -33,8 +34,13 @@ class DiaryEditLoadedState extends DiaryEditState { this.productQuantityController, this.selectedMood, this.currentMeal, + this.caloriesController, + this.fatsController, + this.proteinsController, + this.carbsController, required this.diaryId, required this.selectedSatiety, + required this.moderationExpanded, required this.wakeupExpanded, required this.moodExpanded}) : super(diaryEditModelObj: null); @@ -44,6 +50,7 @@ class DiaryEditLoadedState extends DiaryEditState { String? token; List? meals; List? portions; + List? photos; List? autocompleteList; SelectionPopupModel? currentMeal; TimeOfDay? wakeupController; @@ -52,10 +59,15 @@ class DiaryEditLoadedState extends DiaryEditState { TextEditingController? commentController; TextEditingController? productNameController; TextEditingController? productQuantityController; + TextEditingController? caloriesController; + TextEditingController? fatsController; + TextEditingController? proteinsController; + TextEditingController? carbsController; int? selectedMood; int selectedSatiety; int diaryId; bool wakeupExpanded; + bool moderationExpanded; bool moodExpanded; @override List get props => [ @@ -63,6 +75,8 @@ class DiaryEditLoadedState extends DiaryEditState { diaryDate, token, meals, + portions, + photos, autocompleteList, wakeupController, retireController, @@ -70,11 +84,16 @@ class DiaryEditLoadedState extends DiaryEditState { commentController, productNameController, productQuantityController, + caloriesController, + fatsController, + proteinsController, + carbsController, selectedMood, selectedSatiety, diaryId, wakeupExpanded, moodExpanded, + moderationExpanded, currentMeal ]; DiaryEditLoadedState copyWith({ @@ -84,6 +103,7 @@ class DiaryEditLoadedState extends DiaryEditState { String? token, List? meals, List? portions, + List? photos, List? autocompleteList, TimeOfDay? wakeupController, TimeOfDay? retireController, @@ -91,10 +111,15 @@ class DiaryEditLoadedState extends DiaryEditState { TextEditingController? commentController, TextEditingController? productNameController, TextEditingController? productQuantityController, + TextEditingController? caloriesController, + TextEditingController? fatsController, + TextEditingController? proteinsController, + TextEditingController? carbsController, int? selectedMood, int? selectedSatiety, int? diaryId, bool? wakeupExpanded, + bool? moderationExpanded, bool? moodExpanded, }) { return DiaryEditLoadedState( @@ -116,7 +141,12 @@ class DiaryEditLoadedState extends DiaryEditState { selectedSatiety: selectedSatiety ?? this.selectedSatiety, diaryId: diaryId ?? this.diaryId, currentMeal: currentMeal ?? this.currentMeal, + caloriesController: caloriesController ?? this.caloriesController, + fatsController: fatsController ?? this.fatsController, + proteinsController: proteinsController ?? this.proteinsController, + carbsController: carbsController ?? this.carbsController, wakeupExpanded: wakeupExpanded ?? this.wakeupExpanded, + moderationExpanded: moderationExpanded ?? this.moderationExpanded, moodExpanded: moodExpanded ?? this.moodExpanded); } } diff --git a/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart b/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart index 4c59ece9..7c839e7e 100644 --- a/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart +++ b/lib/presentation/diary/diary_edit_screen/diary_edit_screen.dart @@ -1,10 +1,13 @@ +// ignore_for_file: unused_local_variable + import 'bloc/diary_edit_bloc.dart'; import 'models/diary_edit_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; - +import 'package:fl_chart/fl_chart.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:eatvio_app/widgets/custom_edit_text_form_field.dart'; import 'package:eatvio_app/widgets/custom_edit_drop_down.dart'; @@ -13,17 +16,17 @@ import 'package:eatvio_app/widgets/custom_scale_element.dart'; // ignore: must_be_immutable class DiaryEditScreen extends StatelessWidget { - DiaryEditScreen({Key? key}) - : super( - key: key, - ); + final void Function() onInitialize; + DiaryEditScreen({Key? key, required this.onInitialize}) : super(key: key); static Widget builder(BuildContext context) { return BlocProvider( create: (context) => DiaryEditBloc(DiaryEditState(diaryEditModelObj: DiaryEditModel())) ..add(DiaryEditInitialEvent()), - child: DiaryEditScreen()); + child: DiaryEditScreen( + onInitialize: () {}, + )); } Widget build(BuildContext context) { @@ -31,9 +34,8 @@ class DiaryEditScreen extends StatelessWidget { builder: (context, state) { if (state is DiaryEditInitialState) { // Это начальное состояние, данные ещё не загружены. - return CircularProgressIndicator( + return Container( color: Colors.white, - backgroundColor: Colors.white, ); } else if (state is DiaryEditLoadedState) { // Данные загружены, можно строить экран. @@ -52,43 +54,51 @@ class DiaryEditScreen extends StatelessWidget { Widget _buildDiaryEditScreen( BuildContext context, DiaryEditLoadedState state) { mediaQueryData = MediaQuery.of(context); - return FormBuilder( - child: SafeArea( - child: Scaffold( - backgroundColor: Color.fromARGB(255, 240, 242, 245), - resizeToAvoidBottomInset: true, - body: SizedBox( - width: double.maxFinite, - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.only(bottom: 5.v), - child: Column( - children: [ - _buildHeaderRow(context, state), - SizedBox(height: 30.v), - _buildWakupRetire(context, state), - SizedBox(height: 30.v), - _buildMeal(context, state), - SizedBox(height: 20.v), - _buildMood(context, state), - SizedBox(height: 30.v), - _buildNutritionChart(context), - SizedBox(height: 30.v), - _buildComment(context, state), - ], + return RefreshIndicator( + onRefresh: refreshPage(context), + child: FormBuilder( + child: SafeArea( + child: Scaffold( + backgroundColor: Color.fromARGB(255, 240, 242, 245), + resizeToAvoidBottomInset: true, + body: SizedBox( + width: double.maxFinite, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(bottom: 5.v), + child: Column( + children: [ + _buildHeaderRow(context, state), + SizedBox(height: 30.v), + _buildWakupRetire(context, state), + SizedBox(height: 30.v), + _buildMeal(context, state), + SizedBox(height: 20.v), + _buildMood(context, state), + SizedBox(height: 30.v), + _buildNutritionChart(context), + SizedBox(height: 30.v), + _buildComment(context, state), + ], + ), + ), ), ), - ), + ], ), - ], + ), + bottomNavigationBar: _buildEndDay(context), ), - ), - bottomNavigationBar: _buildEndDay(context), - ), - )); + ))); + } + + refreshPage(BuildContext context) { + return () async { + context.read().add(RefreshEvent()); + }; } /// Section Widget @@ -217,9 +227,15 @@ class DiaryEditScreen extends StatelessWidget { final digitsOnly = FilteringTextInputFormatter.digitsOnly; List _options = state.autocompleteList!.map((dynamic item) => item as String).toList(); - print(_options); + //print(_options); + FocusNode focusNodeName = FocusNode(); + FocusNode focusNodeQuantity = FocusNode(); return Container( margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.all(Radius.circular(12)), @@ -294,15 +310,15 @@ class DiaryEditScreen extends StatelessWidget { optionsBuilder: buildOptions(_options), fieldViewBuilder: ( BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, + TextEditingController productEditingController, + FocusNode focusNodeName, VoidCallback onFieldSubmitted, ) { return CustomTextFormField( hintText: "Введите название продукта", - controller: textEditingController, - focusNode: focusNode, - onChanged: getAutocomplete(context), + controller: productEditingController, + focusNode: focusNodeName, + onChanged: getAutocomplete(context, focusNodeQuantity), /*onFieldSubmitted: (String value) { onFieldSubmitted(); },*/ @@ -338,6 +354,15 @@ class DiaryEditScreen extends StatelessWidget { }), ), SizedBox(height: 20.v), + if (state.moderationExpanded) + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: GestureDetector( + onTap: () => openCaloriesDialog(context, state), + child: Text("Отправить на модерацию"), + ), + ), + SizedBox(height: 20.v), Padding( padding: EdgeInsets.symmetric(horizontal: 10.h), child: Row( @@ -347,6 +372,7 @@ class DiaryEditScreen extends StatelessWidget { width: 146.h, hintText: "Кол-во", textInputAction: TextInputAction.done, + focusNode: focusNodeQuantity, controller: state.productQuantityController, keyboardType: TextInputType.number, inputFormatters: [digitsOnly], @@ -367,6 +393,12 @@ class DiaryEditScreen extends StatelessWidget { .map((portion) => _buildProduct(context, portion)) .toList(), SizedBox(height: 30.v), + ...state.photos != null + ? state.photos! + .map((photo) => _buildPhoto(context, photo)) + .toList() + : [], + SizedBox(height: 20.v), _buildAddPhoto(context), SizedBox(height: 30.v), Text( @@ -424,6 +456,94 @@ class DiaryEditScreen extends StatelessWidget { )); } + openCaloriesDialog(BuildContext context, DiaryEditLoadedState state) { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text('Введите информацию о продукте'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomTextFormField( + hintText: "Название", + controller: state.productNameController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Энергетическая ценность на 100", + keyboardType: TextInputType.number, + controller: state.caloriesController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Белки на 100г", + keyboardType: TextInputType.number, + controller: state.proteinsController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Жиры на 100г", + keyboardType: TextInputType.number, + controller: state.fatsController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Углеводы на 100г", + keyboardType: TextInputType.number, + controller: state.carbsController, + ), + ], + ), + actions: [ + TextButton( + onPressed: popDialog(context), + child: Text('Отмена'), + ), + TextButton( + onPressed: triggerModeration( + context, + state.productNameController!.text, + state.caloriesController!.text, + state.proteinsController!.text, + state.fatsController!.text, + state.carbsController!.text, + dialogContext), + child: Text('Отправить'), + ), + ], + ); + }, + ); + } + + triggerModeration( + BuildContext context, + String name, + String energyOn100gr, + String proteinsOn100gr, + String fatsOn100gr, + String carbohydratesOn100gr, + BuildContext dialogContext) { + return () { + // Call function to send moderation with the entered values + context.read().add(SendModeration( + name: name, + energyOn100gr: energyOn100gr, + proteinsOn100gr: proteinsOn100gr, + fatsOn100gr: fatsOn100gr, + carbohydratesOn100gr: carbohydratesOn100gr)); + Navigator.of(dialogContext).pop(); // Dismiss the dialog + }; + } + + popDialog(BuildContext dialogContext) { + return () { + Navigator.of(dialogContext) + .pop(); // Dismiss the dialog on Cancel button press + }; + } + changeSatiety(BuildContext context, int satiety) { return () { context.read().add(ChangeSatiety(satiety)); @@ -436,7 +556,7 @@ class DiaryEditScreen extends StatelessWidget { }; } - getAutocomplete(BuildContext context) { + getAutocomplete(BuildContext context, FocusNode focusNode) { return (value) { /*print(state.autocompleteList ?.map((dynamic item) => item as String) @@ -478,15 +598,16 @@ class DiaryEditScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: EdgeInsets.only( - left: 7.h, - top: 5.v, - ), - child: Text( - portion['product_name'], - style: CustomTextStyles.titleMedium16, - ), - ), + padding: EdgeInsets.only( + left: 7.h, + top: 5.v, + ), + child: Text( + portion['product_name'].length > 8 + ? portion['product_name'].substring(0, 8) + : portion['product_name'], + style: CustomTextStyles.titleMedium16, + )), Padding( padding: EdgeInsets.only( left: 20.h, @@ -514,6 +635,18 @@ class DiaryEditScreen extends StatelessWidget { ); } + _buildPhoto(BuildContext context, Map photo) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 10.h, vertical: 10.h), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Image.network( + dotenv.env['STORAGE_URL']! + photo['path'], + ), + ); + } + removePortion(BuildContext context, Map portion) { return () { context.read().add(RemovePortionEvent(portion['id'])); @@ -521,6 +654,13 @@ class DiaryEditScreen extends StatelessWidget { } _buildNutritionChart(BuildContext context) { + final List recommendedValues = [ + 2000, + 70, + 50, + 300 + ]; // Рекомендуемые значения: калории, жиры, белки, углеводы + final List consumedValues = [1800, 60, 40, 250]; // return Container( margin: EdgeInsets.symmetric(horizontal: 16.h), padding: EdgeInsets.symmetric( @@ -561,142 +701,96 @@ class DiaryEditScreen extends StatelessWidget { left: 30.h, right: 12.h, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - margin: EdgeInsets.only(top: 7.v), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder1, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox(height: 101.v), - Container( - height: 47.v, - width: 12.h, - decoration: BoxDecoration( - color: theme.colorScheme.errorContainer, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(1.h), - ), - ), - ), - ], - ), - ), - Spacer( - flex: 33, - ), - Container( - margin: EdgeInsets.only(top: 7.v), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder1, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 66.v), - Container( - height: 82.v, - width: 12.h, - decoration: BoxDecoration( - color: appTheme.lightGreenA700, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(1.h), - ), - ), - ), - ], - ), - ), - Spacer( - flex: 33, - ), - Container( - margin: EdgeInsets.only(top: 7.v), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder1, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 14.v), - Container( - height: 134.v, - width: 12.h, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(1.h), - ), - ), - ), - ], - ), - ), - Spacer( - flex: 33, - ), - Container( - margin: EdgeInsets.only(top: 7.v), - decoration: AppDecoration.outlinePrimary.copyWith( - borderRadius: BorderRadiusStyle.roundedBorder1, - ), - child: Container( - height: 148.v, - width: 12.h, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular( - 1.h, + child: AspectRatio( + aspectRatio: 1.5, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + barTouchData: BarTouchData(enabled: false), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + //getTitlesWidget: (value, meta) => newMethod(value), ), ), ), + borderData: FlBorderData(show: false), + barGroups: [ + BarChartGroupData(x: 0, barRods: [ + BarChartRodData( + toY: consumedValues[0] / recommendedValues[0] * 100, + color: Colors.blue, + ) + ]), + BarChartGroupData(x: 1, barRods: [ + BarChartRodData( + toY: consumedValues[1] / recommendedValues[1] * 100, + color: Colors.red, + ) + ]), + BarChartGroupData(x: 2, barRods: [ + BarChartRodData( + toY: consumedValues[2] / recommendedValues[2] * 100, + color: Colors.green, + ) + ]), + BarChartGroupData(x: 3, barRods: [ + BarChartRodData( + toY: consumedValues[3] / recommendedValues[3] * 100, + color: Colors.orange, + ) + ]), + ], ), - Padding( - padding: EdgeInsets.only( - left: 6.h, - bottom: 128.v, - ), - child: Text( - "*", - style: theme.textTheme.titleLarge, - ), - ), - ], + ), ), ), ), SizedBox(height: 8.v), + SizedBox(height: 2.v), Padding( - padding: EdgeInsets.only(left: 4.h), + padding: EdgeInsets.only( + left: 10.h, + right: 2.h, + ), child: Row( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Калории", - style: theme.textTheme.titleSmall, + "Калорий", + style: theme.textTheme.bodySmall, ), Padding( - padding: EdgeInsets.only(left: 28.h), + padding: EdgeInsets.only( + left: 25.h, + bottom: 11.v, + ), child: Text( - "Белки", - style: theme.textTheme.titleSmall, + "Белков", + style: theme.textTheme.bodySmall, ), ), - Spacer(), - Text( - "Жиры", - style: theme.textTheme.titleSmall, + Padding( + padding: EdgeInsets.only( + left: 19.h, + bottom: 11.v, + ), + child: Text( + "Жиров", + style: theme.textTheme.bodySmall, + ), ), Padding( - padding: EdgeInsets.only(left: 25.h), + padding: EdgeInsets.only( + left: 14.h, + bottom: 11.v, + ), child: Text( - "Углеводы", - style: theme.textTheme.titleSmall, + "Углеводов", + style: theme.textTheme.bodySmall, ), ), ], @@ -713,7 +807,7 @@ class DiaryEditScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "255 из 250Ккал", + "${consumedValues[0].toInt()} из ${recommendedValues[0].toInt()}", style: theme.textTheme.bodySmall, ), Padding( @@ -722,7 +816,7 @@ class DiaryEditScreen extends StatelessWidget { bottom: 11.v, ), child: Text( - "50 из 250 гр", + "${consumedValues[1].toInt()} из ${recommendedValues[1].toInt()}", style: theme.textTheme.bodySmall, ), ), @@ -732,7 +826,7 @@ class DiaryEditScreen extends StatelessWidget { bottom: 11.v, ), child: Text( - "270 из 250 гр", + "${consumedValues[2].toInt()} из ${recommendedValues[2].toInt()}", style: theme.textTheme.bodySmall, ), ), @@ -742,7 +836,7 @@ class DiaryEditScreen extends StatelessWidget { bottom: 11.v, ), child: Text( - "400 из 250 гр", + "${consumedValues[3].toInt()} из ${recommendedValues[3].toInt()}", style: theme.textTheme.bodySmall, ), ), @@ -750,10 +844,11 @@ class DiaryEditScreen extends StatelessWidget { ), ), SizedBox(height: 19.v), - Text( - "*Превышенная суточная норма углеводов", - style: theme.textTheme.labelLarge, - ), + if (checkConsumedValues(recommendedValues, consumedValues)) + Text( + "*Превышенная суточная норма углеводов", + style: theme.textTheme.labelLarge, + ) ], ), ); @@ -896,10 +991,51 @@ class DiaryEditScreen extends StatelessWidget { buttonStyle: CustomButtonStyles.outlinePrimaryTL15, decoration: BoxDecoration( color: Colors.transparent, - ), // Установите прозрачный фон + ), + onPressed: onFinishDayPressed(context), + // Установите прозрачный фон ); } + Function() onFinishDayPressed(BuildContext context) { + return () { + print("sad"); + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text("Подтвердите действие"), + content: Text("Вы уверены, что хотите завершить день?"), + actions: [ + TextButton( + child: Text("Отмена"), + onPressed: closeDialog(dialogContext), + ), + TextButton( + child: Text("Подтвердить"), + onPressed: sendFinishDayData(dialogContext, context), + ), + ], + ); + }, + ); + }; + } + + Function() sendFinishDayData( + BuildContext dialogContext, BuildContext context) { + return () { + closeDialog(dialogContext); + context.read().add(FinishDay()); + }; + } + + Function() closeDialog(BuildContext dialogContext) { + return () { + Navigator.of(dialogContext).pop(); + }; + } + _buildMood(BuildContext context, DiaryEditLoadedState state) { return Container( margin: EdgeInsets.symmetric(horizontal: 16.h), @@ -997,9 +1133,7 @@ class DiaryEditScreen extends StatelessWidget { ); if (pickedTime != null && pickedTime != state.wakeupController) { state.wakeupController = pickedTime; - context.read().add(WakeupEvent( - pickedTime, - )); + context.read().add(WakeupEvent(pickedTime)); } } @@ -1027,3 +1161,15 @@ class DiaryEditScreen extends StatelessWidget { } } } + +bool checkConsumedValues( + List recommendedValues, List consumedValues) { + for (int i = 0; i < recommendedValues.length; i++) { + if (consumedValues[i] > recommendedValues[i]) { + // Если хотя бы одно потребленное значение превышает рекомендованное + return true; // Возвращаем false + } + } + // Если ни одно потребленное значение не превышает рекомендованное с соответствующим индексом + return false; // Возвращаем true +} diff --git a/lib/widgets/chart_axis.dart b/lib/widgets/chart_axis.dart new file mode 100644 index 00000000..5cc64480 --- /dev/null +++ b/lib/widgets/chart_axis.dart @@ -0,0 +1,145 @@ +/*import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; + +buildChart( + BuildContext context, List chartPoints, List? dates) { +// Определение осей графика + List spots = []; + for (int i = 0; i < 4; i++) { + spots.add( + FlSpot((i * 10 / 10) + 1, chartPoints[i]), + ); + } + + Widget bottomTitleWidgets( + double value, TitleMeta meta, double chartWidth, List? dates) { + if (value % 1 != 0) { + return Container(); + } + final style = TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + fontSize: min(13, 13 * chartWidth / 300), + ); + int index = value.toInt(); + if (index >= 0 && index < dates!.length) { + // Проверка на допустимый индекс в массиве dates + String formattedDate = + DateFormat('dd.MM').format(DateTime.parse(dates[index])); + return SideTitleWidget( + axisSide: meta.axisSide, + space: 16, + child: Text(formattedDate, style: style), + ); + } else { + return Container(); + } + } + + Widget leftTitleWidgets(double value, TitleMeta meta, double chartWidth) { + final style = TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + fontSize: min(18, 18 * chartWidth / 300), + ); + return SideTitleWidget( + axisSide: meta.axisSide, + space: 16, + child: Text(meta.formattedValue, style: style), + ); + } + + return Padding( + padding: const EdgeInsets.only( + left: 12, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: LayoutBuilder( + builder: (context, constraints) { + return LineChart( + LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + maxContentWidth: 100, + tooltipBgColor: Colors.black, + getTooltipItems: mapSpots(), + ), + handleBuiltInTouches: true, + getTouchLineStart: (data, index) => 0, + ), + lineBarsData: [ + LineChartBarData( + color: Colors.pink, + spots: spots, + isCurved: true, + isStrokeCapRound: true, + barWidth: 3, + belowBarData: BarAreaData( + show: false, + ), + dotData: const FlDotData(show: false), + ), + ], + minY: chartPoints.reduce( + (value, element) => value < element ? value : element), + maxY: chartPoints.reduce( + (value, element) => value > element ? value : element), + minX: 1, + maxX: 4, + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) => + leftTitleWidgets(value, meta, constraints.maxWidth), + reservedSize: 56, + ), + drawBelowEverything: true, + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) => bottomTitleWidgets( + value, meta, constraints.maxWidth, dates), + reservedSize: 36, + interval: 1, + ), + drawBelowEverything: true, + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: true, + horizontalInterval: 1.5, + verticalInterval: 5, + checkToShowHorizontalLine: valuteToInt(), + getDrawingHorizontalLine: (_) => FlLine( + color: Colors.blue.withOpacity(1), + dashArray: [8, 2], + strokeWidth: 0.8, + ), + getDrawingVerticalLine: (_) => FlLine( + color: Colors.blue.withOpacity(1), + dashArray: [8, 2], + strokeWidth: 0.8, + ), + checkToShowVerticalLine: valuteToInt(), + ), + borderData: FlBorderData(show: false), + ), + ); + }, + ), + ), + ); +} +*/ \ No newline at end of file diff --git a/lib/widgets/chart_bars.dart b/lib/widgets/chart_bars.dart new file mode 100644 index 00000000..7f4fc342 --- /dev/null +++ b/lib/widgets/chart_bars.dart @@ -0,0 +1,79 @@ +/*import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: Text('Nutrition Chart'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 300, + width: 300, + child: BarChart( + BarChartData( + barTouchData: BarTouchData(enabled: false), + titlesData: FlTitlesData( + show: true, + bottomTitles: SideTitles( + showTitles: true, + getTextStyles: (value) => + const TextStyle(color: Colors.black, fontSize: 14), + margin: 10, + showTitles: (double value) { + switch (value.toInt()) { + case 0: + return 'Calories'; + case 1: + return 'Protein'; + case 2: + return 'Fat'; + case 3: + return 'Carbs'; + default: + return ''; + } + }, + ), + leftTitles: SideTitles(showTitles: false), + ), + borderData: FlBorderData(show: false), + barGroups: [ + BarChartGroupData(x: 0, barRods: [ + BarChartRodData(y: 200, color: Colors.blue), + BarChartRodData(y: 150, color: Colors.blueAccent), + ]), + BarChartGroupData(x: 1, barRods: [ + BarChartRodData(y: 50, color: Colors.red), + BarChartRodData(y: 60, color: Colors.deepOrange), + ]), + BarChartGroupData(x: 2, barRods: [ + BarChartRodData(y: 80, color: Colors.green), + BarChartRodData(y: 70, color: Colors.lightGreen), + ]), + BarChartGroupData(x: 3, barRods: [ + BarChartRodData(y: 100, color: Colors.orange), + BarChartRodData(y: 120, color: Colors.orangeAccent), + ]), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} +*/ \ No newline at end of file diff --git a/lib/widgets/nutrition_chart.dart b/lib/widgets/nutrition_chart.dart new file mode 100644 index 00000000..455f8f94 --- /dev/null +++ b/lib/widgets/nutrition_chart.dart @@ -0,0 +1,89 @@ +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/base_button.dart'; +import 'package:flutter/material.dart'; + +class NextStepButton extends BaseButton { + NextStepButton({ + Key? key, + this.decoration, + this.leftIcon, + this.rightIcon, + this.label, + VoidCallback? onPressed, + ButtonStyle? buttonStyle, + TextStyle? buttonTextStyle, + bool? isDisabled, + Alignment? alignment, + double? height, + double? width, + EdgeInsets? margin, + required String text, + }) : super( + text: text, + onPressed: onPressed, + buttonStyle: buttonStyle, + isDisabled: isDisabled, + buttonTextStyle: buttonTextStyle, + height: height, + alignment: alignment, + width: width, + margin: margin, + ); + + final BoxDecoration? decoration; + final Widget? leftIcon; + final Widget? rightIcon; + final Widget? label; + + @override + Widget build(BuildContext context) { + return (alignment != null) + ? Align( + alignment: alignment!, + child: Container( + height: this.height ?? 30.v, + width: this.width ?? double.maxFinite, + margin: margin, + decoration: decoration, + child: OutlinedButton( + style: buttonStyle, + onPressed: isDisabled ?? false ? null : onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + leftIcon ?? const SizedBox.shrink(), + Text( + text, + style: buttonTextStyle ?? theme.textTheme.titleSmall, + ), + rightIcon ?? const SizedBox.shrink(), + ], + ), + ), + ), + ) + : Container( + height: this.height ?? 30.v, + width: this.width ?? double.maxFinite, + margin: margin, + decoration: decoration, + child: OutlinedButton( + style: buttonStyle, + onPressed: isDisabled ?? false ? null : onPressed ?? () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + leftIcon ?? const SizedBox.shrink(), + Text( + text, + style: buttonTextStyle ?? theme.textTheme.titleSmall, + ), + rightIcon ?? const SizedBox.shrink(), + ], + ), + ), + ); + } +} diff --git a/test/diary_test.dart b/test/diary_test.dart index 7d5b9d3c..722c457d 100644 --- a/test/diary_test.dart +++ b/test/diary_test.dart @@ -26,13 +26,16 @@ void main() { autocompleteList: [], portions: [], wakeupExpanded: false, + moderationExpanded: false, moodExpanded: false); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( value: diaryEditBloc, - child: DiaryEditScreen(), + child: DiaryEditScreen( + onInitialize: () {}, + ), ), ), );