diff --git a/.DS_Store b/.DS_Store index 3d1c4439..4bd8225b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 39b2d892..d7368d78 100644 --- a/README.md +++ b/README.md @@ -1,59 +1 @@ - -# eatvio_app -### Системные требования - -- Flutter (Channel stable, 3.19.0, on Ubuntu 22.04.3 LTS 6.2.0-39-generic, locale en_US.UTF-8) -- Android Studio (version 2023.1) -- VS Code (version 1.85.0) -- Android SDK (version 34.0.0) - устанавливается из Android Studio (Tools->SDK Manager->Android Command Line Tools) -- Эмулятор (2 available) - -### Структура приложения - -``` -. -├── android - Папка для крмпиляции Android приложения -├── assets - Медиа и шрифты -├── ios - Папка для крмпиляции iOS приложения -├── lib - Основной код - ├── main.dart - Файл из которого компилируется приложение - ├── presentation - Виджеты экранов (верстка) - ├── routes - Роутер (пути до экранов) - └── theme - Темы и оформление - └── widgets - Кастомные виджеты -├── pubspec.yaml - Пакеты - -``` - - -### Как запускать? - -Для запуска на эмуляторе в программе VS Code скомпилируйте и запустите файл main.dart (предварительно необходимо установить эмулятор в Android Studio или подключите устройство в режиме разработчика через USB) - -Для тестирования на устройствах Android в корневой папке проекта выполните команду - -`# flutter build apk` - -после чего `.apk` файл скомпилируется в директории `build/app/outputs/flutter-apk/app-release.apk` - -Данный файл установит приложение на устройство с актуальной версией OS - -### Библиотеки и инструменты - - - **flutter_bloc** - контроль состояний - - **cached_network_image** - кеширование картинок из веба - - **flutter_svg** - работа с изображениями - - **equatable** - сравнение без оператора "==" - - **validators** - валидация - - **hive** - локальная бд (1) - - **hive_flutter** - локальная бд (2) - - **isar_flutter_libs** - локальная бд (3) - - **path_provider** - динамичкские пути до файлов - - **fl_chart** - построение графиков - - **flutter_spinkit** - прелоадеры - - **flutter_dotenv** - переменные окружения - - -### Поддержка - -Telegram: @maxx_kim (https://t.me/maxx_kim) +# eatvio_app \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 83b4cf1d..5a6de5ce 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.1" apply false - id "org.jetbrains.kotlin.android" version "1.9.10" apply false + id "org.jetbrains.kotlin.android" version "2.0.20" apply false } include ":app" diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart index e1414dbe..d8fc116e 100644 --- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart +++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart @@ -78,7 +78,7 @@ class _BlogPostCreateScreenState extends State { floatingActionButton: SizedBox( width: MediaQuery.of(context).size.width - 32, child: FloatingActionButton.extended( - heroTag: 'nextButtonRegistration', + heroTag: 'createBlogPost', highlightElevation: 0, elevation: 0, focusElevation: 0, diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index 7556fece..b8635ec6 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -201,6 +201,7 @@ class DiaryScreen extends StatelessWidget { width: MediaQuery.of(context).size.width, padding: EdgeInsets.only(left: 4, right: 4), child: FloatingActionButton.extended( + heroTag: 'endDayButton', highlightElevation: 0, elevation: 0, focusElevation: 0, @@ -264,6 +265,7 @@ class DiaryScreen extends StatelessWidget { width: MediaQuery.of(context).size.width, padding: EdgeInsets.only(left: 4, right: 4), child: FloatingActionButton.extended( + heroTag: 'addProduct', highlightElevation: 0, elevation: 0, focusElevation: 0, diff --git a/lib/presentation/auth/auth_screen/auth_screen.dart b/lib/presentation/auth/auth_screen/auth_screen.dart index bd662937..2c738524 100644 --- a/lib/presentation/auth/auth_screen/auth_screen.dart +++ b/lib/presentation/auth/auth_screen/auth_screen.dart @@ -126,7 +126,7 @@ class _AuthScreenState extends State { height: 48, width: MediaQuery.of(context).size.width - 32, child: FloatingActionButton.extended( - heroTag: 'nextButtonRegistration', + heroTag: 'authButton', highlightElevation: 0, elevation: 0, focusElevation: 0, diff --git a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart index 306099e8..d10b70ab 100644 --- a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart +++ b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart @@ -1,24 +1,23 @@ import 'dart:convert'; +import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:carousel_slider_plus/carousel_slider_plus.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; import 'package:eatvio_app/widgets/custom_text_form_field.dart'; +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; + import 'bloc/my_profile_bloc.dart'; import 'models/my_profile_model.dart'; class MyProfileScreen extends StatelessWidget { - int valuteToInt(String value) { - // Преобразование строки в целое число - return int.tryParse(value) ?? 0; // Возвращает 0, если преобразование не удалось - } - static Widget builder(BuildContext context) { return BlocProvider( create: (context) => MyProfileBloc( @@ -193,7 +192,7 @@ class MyProfileScreen extends StatelessWidget { _buildCarousel(BuildContext contex, MyProfileLoadedState state) { return CarouselSlider( - options: CarouselOptions(height: 370), + options: CarouselOptions(height: 340), items: [1, 2, 3].map((chartType) { return Builder( builder: (BuildContext context) { @@ -210,11 +209,11 @@ class MyProfileScreen extends StatelessWidget { ), ), SizedBox(height: 50.v), - // _buildChart( - // context, - // state.chartPoints?.values.map((e) => e['weight']).toList().map((i) => i.toDouble()).toList() ?? [0.0, 0.0, 0.0, 0.0], - // state.chartDates, - // ), + _buildChart( + context, + state.chartPoints?.values.map((e) => e['weight']).toList().map((i) => i.toDouble()).toList() ?? [0.0, 0.0, 0.0, 0.0], + state.chartDates, + ), ], ), ); @@ -228,14 +227,14 @@ class MyProfileScreen extends StatelessWidget { ), ), SizedBox(height: 50.v), - // _buildChart( - // context, - // _bodyMassIndex( - // state.chartPoints?.values.map((e) => e['weight']).toList().map((i) => i.toDouble()).toList() ?? [0.0, 0.0, 0.0, 0.0], - // state.userDetails?['height'].toInt() ?? 160, - // ), - // state.chartDates, - // ), + _buildChart( + context, + _bodyMassIndex( + state.chartPoints?.values.map((e) => e['weight']).toList().map((i) => i.toDouble()).toList() ?? [0.0, 0.0, 0.0, 0.0], + state.userDetails?['height'].toInt() ?? 160, + ), + state.chartDates, + ), ], ); case 3: //График обхватов @@ -250,7 +249,7 @@ class MyProfileScreen extends StatelessWidget { ), ), SizedBox(height: 30.v), - // _buildCircumferenceChart(context, state.circumferences, state.chartDates), + _buildCircumferenceChart(context, state.circumferences, state.chartDates), _buildChartLegend(), ], ), @@ -296,34 +295,354 @@ class MyProfileScreen extends StatelessWidget { ); } - // Widget _buildChart(BuildContext context, List chartPoints, List? dates) { - // // Проверяем, что у нас есть 4 элемента в обоих списках - // if (chartPoints.length != 4 || (dates != null && dates.length != 4)) { - // return Center(child: Text('Должно быть ровно 4 точки и 4 даты.')); - // } + _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]), + ); + } - // // Преобразуем данные в список точек для графика - // List chartData = List.generate(4, (index) { - // return ChartData(dates![index], chartPoints[index]); - // }); + 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(); + } + } - // return Container( - // height: 300, // Задайте нужную высоту для графика - // child: SfCartesianChart( - // title: ChartTitle(text: 'График значений'), - // primaryXAxis: CategoryAxis(), - // series: >[ - // // Изменяем на CartesianSeries - // LineSeries( - // dataSource: chartData, - // xValueMapper: (ChartData data, _) => data.date, - // yValueMapper: (ChartData data, _) => data.value, - // dataLabelSettings: DataLabelSettings(isVisible: true), - // ), - // ], - // ), - // ); - // } + 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), + ), + ); + }, + ), + ), + ); + } + + valuteToInt() { + return (value) { + return value.toInt() == 0; + }; + } + + List Function(List) mapSpots() { + return (touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + final textStyle = TextStyle( + color: touchedSpot.bar.gradient?.colors[0] ?? touchedSpot.bar.color, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return LineTooltipItem( + '${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}', + textStyle, + ); + }).toList(); + }; + } + + _buildCircumferenceChart(BuildContext context, chartPoints, List? dates) { +// Определение осей графика + final chestCircumferences = chartPoints.values.map((e) => e['chest_circumference']).toList(); + List spotsChest = []; + for (int i = 0; i < 4; i++) { + chestCircumferences[i] = chestCircumferences[i] * 10 / 10; + spotsChest.add( + FlSpot((i * 10 / 10) + 1, chestCircumferences[i] * 10 / 10), + ); + } + final waistCircumferences = chartPoints.values.map((e) => e['waist_circumference']).toList(); + List spotsWaist = []; + for (int i = 0; i < 4; i++) { + waistCircumferences[i] = waistCircumferences[i] * 10 / 10; + spotsWaist.add( + FlSpot((i * 10 / 10) + 1, waistCircumferences[i] * 10 / 10), + ); + } + final hipCircumferences = chartPoints.values.map((e) => e['hip_circumference']).toList(); + List spotsHip = []; + for (int i = 0; i < 4; i++) { + hipCircumferences[i] = hipCircumferences[i] * 10 / 10; + spotsHip.add( + FlSpot((i * 10 / 10) + 1, hipCircumferences[i]), + ); + } + + final allCircumferences = [...chestCircumferences, ...hipCircumferences, ...waistCircumferences]; + + 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: mapTouchedSpots(), + ), + handleBuiltInTouches: true, + getTouchLineStart: (data, index) => 0, + ), + lineBarsData: [ + LineChartBarData( + color: Colors.pink, + spots: spotsChest, + isCurved: true, + isStrokeCapRound: true, + barWidth: 3, + belowBarData: BarAreaData( + show: false, + ), + dotData: const FlDotData(show: false), + ), + LineChartBarData( + color: Colors.green, + spots: spotsWaist, + isCurved: true, + isStrokeCapRound: true, + barWidth: 3, + belowBarData: BarAreaData( + show: false, + ), + dotData: const FlDotData(show: false), + ), + LineChartBarData( + color: Colors.purple, + spots: spotsHip, + isCurved: true, + isStrokeCapRound: true, + barWidth: 3, + belowBarData: BarAreaData( + show: false, + ), + dotData: const FlDotData(show: false), + ), + ], + minY: allCircumferences.reduce((value, element) => value < element ? value : element), + maxY: allCircumferences.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), + ), + ); + }, + ), + ), + ); + } + + List Function(List) mapTouchedSpots() { + return (touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + final textStyle = TextStyle( + color: touchedSpot.bar.gradient?.colors[0] ?? touchedSpot.bar.color, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return LineTooltipItem( + '${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}', + textStyle, + ); + }).toList(); + }; + } /// Section Widget _buildPlans(BuildContext context, List? plans, List? imagePaths, List? states) { @@ -643,16 +962,3 @@ class MyProfileScreen extends StatelessWidget { }; } } - -class ChartData { - final String date; - final double value; - - ChartData(this.date, this.value); - - valuteToInt() { - return (value) { - return value.toInt() == 0; - }; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 08cdef1d..d4b409ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: freezed_annotation: ^2.4.4 json_annotation: ^4.9.0 validators: ^3.0.0 - isar_flutter_libs: ^3.1.0 path_provider: ^2.1.4 fl_chart: ^0.68.0 flutter_spinkit: ^5.2.1 @@ -45,7 +44,7 @@ dependencies: badges: ^3.1.2 google_fonts: ^6.2.1 location: ^6.0.2 - # syncfusion_flutter_charts: ^26.2.9 + syncfusion_flutter_charts: ^26.2.10 table_calendar: ^3.1.2 flutter_keyboard_visibility: ^6.0.0 share_plus: ^10.0.0