diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b77669497..dcc30a8eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: - '**.dart' - 'pubspec.yaml' pull_request: - branches: [ pull_request, master ] + branches: [ master, ] paths: - '**.dart' - 'pubspec.yaml' diff --git a/android/build.gradle b/android/build.gradle index 58a8c74b1..713d7f6e6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8169503c4..d8fa7f919 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -61,7 +61,7 @@ SPEC CHECKSUMS: image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - rive_common: 60ae7896ab40f9513974f36f015de33f70d2c5c5 + rive_common: b5b1aa30c63b8f0f00f32cddc9ea394d3d3473b5 shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 diff --git a/lib/exceptions/http_exception.dart b/lib/exceptions/http_exception.dart index 01be9d444..b442e34a2 100644 --- a/lib/exceptions/http_exception.dart +++ b/lib/exceptions/http_exception.dart @@ -29,12 +29,11 @@ class WgerHttpException implements Exception { errors = {'unknown_error': 'An unknown error occurred, no further information available'}; } else { try { - errors = json.decode(responseBody); + errors = {'unknown_error': json.decode(responseBody)}; } catch (e) { - errors = responseBody; + errors = {'unknown_error': responseBody}; } } - errors = errors; } @override diff --git a/lib/helpers/charts.dart b/lib/helpers/charts.dart new file mode 100644 index 000000000..64a0b6e84 --- /dev/null +++ b/lib/helpers/charts.dart @@ -0,0 +1,5 @@ +double chartGetInterval(DateTime first, DateTime last, {divider = 3}) { + final dayDiff = last.difference(first); + + return dayDiff.inMilliseconds == 0 ? 1000 : dayDiff.inMilliseconds.abs() / divider; +} diff --git a/lib/helpers/colors.dart b/lib/helpers/colors.dart new file mode 100644 index 000000000..62cb1cf49 --- /dev/null +++ b/lib/helpers/colors.dart @@ -0,0 +1,43 @@ +import 'dart:math'; +import 'dart:ui'; + +const LIST_OF_COLORS8 = [ + Color(0xFF2A4C7D), + Color(0xFF5B5291), + Color(0xFF8E5298), + Color(0xFFBF5092), + Color(0xFFE7537E), + Color(0xFFFF6461), + Color(0xFFFF813D), + Color(0xFFFFA600), +]; + +const LIST_OF_COLORS5 = [ + Color(0xFF2A4C7D), + Color(0xFF825298), + Color(0xFFD45089), + Color(0xFFFF6A59), + Color(0xFFFFA600), +]; + +const LIST_OF_COLORS3 = [ + Color(0xFF2A4C7D), + Color(0xFFD45089), + Color(0xFFFFA600), +]; + +Iterable generateChartColors(int nrOfItems) sync* { + final List colors; + + if (nrOfItems <= 3) { + colors = LIST_OF_COLORS3; + } else if (nrOfItems <= 5) { + colors = LIST_OF_COLORS5; + } else { + colors = LIST_OF_COLORS8; + } + + for (final color in colors) { + yield color; + } +} \ No newline at end of file diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index 053fecc28..978304c23 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -82,6 +82,30 @@ class NutritionalPlan { return out; } + NutritionalValues get nutritionalValuesToday { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + + return logEntriesValues.containsKey(today) ? logEntriesValues[today]! : NutritionalValues(); + } + + NutritionalValues get nutritionalValues7DayAvg { + final currentDate = DateTime.now(); + final sevenDaysAgo = currentDate.subtract(Duration(days: 7)); + + final entries = logs.where((obj) { + DateTime objDate = obj.datetime; + return objDate.isAfter(sevenDaysAgo) && objDate.isBefore(currentDate); + }).toList(); + + var out = NutritionalValues(); + entries.forEach((log) { + out = out + log.nutritionalValues; + }); + + return out; + } + /// Calculates the percentage each macro nutrient adds to the total energy BaseNutritionalValues energyPercentage(NutritionalValues values) { return BaseNutritionalValues( diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index f6f17e8f9..789008a30 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -415,7 +415,7 @@ class NutritionPlansProvider with ChangeNotifier { final data = await baseProvider.fetchPaginated( baseProvider.makeUrl( _nutritionDiaryPath, - query: {'plan': plan.id.toString(), 'limit': '999'}, + query: {'plan': plan.id.toString(), 'limit': '999', 'ordering': 'datetime'}, ), ); diff --git a/lib/providers/workout_plans.dart b/lib/providers/workout_plans.dart index 664468850..c5d2406aa 100644 --- a/lib/providers/workout_plans.dart +++ b/lib/providers/workout_plans.dart @@ -18,7 +18,6 @@ import 'dart:convert'; import 'dart:developer' as dev; -import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 89f98fb93..1ba04fac7 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import 'package:charts_flutter/flutter.dart' as charts; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:table_calendar/table_calendar.dart'; @@ -30,8 +29,6 @@ const Color wgerTextMuted = Colors.black38; const Color wgerBackground = Color(0xfff4f4f6); // Chart colors -const charts.Color wgerChartPrimaryColor = charts.Color(r: 0x2a, g: 0x4c, b: 0x7d); -const charts.Color wgerChartSecondaryColor = charts.Color(r: 0xe6, g: 0x39, b: 0x46); /// Original sizes for the material text theme /// https://api.flutter.dev/flutter/material/TextTheme-class.html diff --git a/lib/widgets/core/app_bar.dart b/lib/widgets/core/app_bar.dart index 6e420c794..8f618cd33 100644 --- a/lib/widgets/core/app_bar.dart +++ b/lib/widgets/core/app_bar.dart @@ -29,7 +29,7 @@ import 'package:wger/screens/form_screen.dart'; import 'package:wger/widgets/core/about.dart'; import 'package:wger/widgets/user/forms.dart'; -class MainAppBar extends StatelessWidget with PreferredSizeWidget { +class MainAppBar extends StatelessWidget implements PreferredSizeWidget { final String _title; MainAppBar(this._title); @@ -113,7 +113,7 @@ class MainAppBar extends StatelessWidget with PreferredSizeWidget { } /// App bar that only displays a title -class EmptyAppBar extends StatelessWidget with PreferredSizeWidget { +class EmptyAppBar extends StatelessWidget implements PreferredSizeWidget { final String _title; EmptyAppBar(this._title); diff --git a/lib/widgets/core/charts.dart b/lib/widgets/core/charts.dart deleted file mode 100644 index d1bddd668..000000000 --- a/lib/widgets/core/charts.dart +++ /dev/null @@ -1,59 +0,0 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * wger Workout Manager is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import 'package:charts_flutter/flutter.dart' as charts; -import 'package:flutter/widgets.dart'; -import 'package:wger/theme/theme.dart'; - -class MeasurementChartEntry { - num value; - DateTime date; - - MeasurementChartEntry(this.value, this.date); -} - -/// Weight chart widget -class MeasurementChartWidget extends StatelessWidget { - final List _entries; - final String unit; - - /// [_entries] is a list of [MeasurementChartEntry] - const MeasurementChartWidget(this._entries, {this.unit = 'kg'}); - - @override - Widget build(BuildContext context) { - final unitTickFormatter = charts.BasicNumericTickFormatterSpec((num? value) => '$value $unit'); - - return charts.TimeSeriesChart( - [ - charts.Series( - id: 'Measurement', - colorFn: (_, __) => wgerChartSecondaryColor, - domainFn: (MeasurementChartEntry entry, _) => entry.date, - measureFn: (MeasurementChartEntry entry, _) => entry.value, - data: _entries, - ) - ], - defaultRenderer: charts.LineRendererConfig(includePoints: true), - primaryMeasureAxis: charts.NumericAxisSpec( - tickProviderSpec: const charts.BasicNumericTickProviderSpec(zeroBound: false), - tickFormatterSpec: unitTickFormatter, - ), - ); - } -} diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index 441eda169..c1e02b026 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -18,7 +18,6 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; @@ -36,7 +35,7 @@ import 'package:wger/screens/nutritional_plan_screen.dart'; import 'package:wger/screens/weight_screen.dart'; import 'package:wger/screens/workout_plan_screen.dart'; import 'package:wger/theme/theme.dart'; -import 'package:wger/widgets/core/charts.dart'; +import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/measurements/categories_card.dart'; import 'package:wger/widgets/measurements/forms.dart'; @@ -183,18 +182,15 @@ class _DashboardNutritionWidgetState extends State { }, ), if (_hasContent) - Container( - padding: const EdgeInsets.only(left: 10), - child: Column( - children: [ - ...getContent(), - Container( - padding: const EdgeInsets.all(15), - height: 180, - child: NutritionalPlanPieChartWidget(_plan!.nutritionalValues), - ) - ], - ), + Column( + children: [ + ...getContent(), + Container( + padding: const EdgeInsets.all(15), + height: 180, + child: FlNutritionalPlanPieChartWidget(_plan!.nutritionalValues), + ) + ], ) else NothingFound( @@ -268,9 +264,8 @@ class _DashboardWeightWidgetState extends State { Column( children: [ Container( - padding: const EdgeInsets.all(15), - height: 180, - child: MeasurementChartWidget(weightEntriesData.items + height: 200, + child: MeasurementChartWidgetFl(weightEntriesData.items .map((e) => MeasurementChartEntry(e.weight, e.date)) .toList()), ), @@ -345,81 +340,82 @@ class _DashboardMeasurementWidgetState extends State ); } return Consumer( - builder: (context, workoutProvider, child) => Card( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text( - AppLocalizations.of(context).measurements, - style: Theme.of(context).textTheme.headline4, - ), - leading: const FaIcon( - FontAwesomeIcons.weight, - color: Colors.black, - ), - trailing: IconButton( - icon: const Icon(Icons.arrow_forward), - onPressed: () => Navigator.pushNamed( - context, - MeasurementCategoriesScreen.routeName, - ), - ), - ), - Column( - children: [ - if (items.isNotEmpty) - Column(children: [ - CarouselSlider( - items: items, - carouselController: _controller, - options: CarouselOptions( - autoPlay: false, - enlargeCenterPage: false, - viewportFraction: 1, - enableInfiniteScroll: false, - aspectRatio: 1.1, - onPageChanged: (index, reason) { - setState(() { - _current = index; - }); - }), + builder: (context, workoutProvider, child) => Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + AppLocalizations.of(context).measurements, + style: Theme.of(context).textTheme.headline4, ), - Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: items.asMap().entries.map((entry) { - return GestureDetector( - onTap: () => _controller.animateToPage(entry.key), - child: Container( - width: 12.0, - height: 12.0, - margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: (Theme.of(context).brightness == Brightness.dark - ? Colors.white - : wgerPrimaryColor) - .withOpacity(_current == entry.key ? 0.9 : 0.4)), - ), - ); - }).toList(), + leading: const FaIcon( + FontAwesomeIcons.weight, + color: Colors.black, + ), + trailing: IconButton( + icon: const Icon(Icons.arrow_forward), + onPressed: () => Navigator.pushNamed( + context, + MeasurementCategoriesScreen.routeName, ), ), - ]) - else - NothingFound( - AppLocalizations.of(context).noMeasurementEntries, - AppLocalizations.of(context).newEntry, - MeasurementCategoryForm(), ), - ], - ), - ], - ), - ), - ); + Column( + children: [ + if (items.isNotEmpty) + Column(children: [ + CarouselSlider( + items: items, + carouselController: _controller, + options: CarouselOptions( + autoPlay: false, + enlargeCenterPage: false, + viewportFraction: 1, + enableInfiniteScroll: false, + aspectRatio: 1.1, + onPageChanged: (index, reason) { + setState(() { + _current = index; + }); + }), + ), + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: items.asMap().entries.map( + (entry) { + return GestureDetector( + onTap: () => _controller.animateToPage(entry.key), + child: Container( + width: 12.0, + height: 12.0, + margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: (Theme.of(context).brightness == Brightness.dark + ? Colors.white + : wgerPrimaryColor) + .withOpacity(_current == entry.key ? 0.9 : 0.4)), + ), + ); + }, + ).toList(), + ), + ), + ]) + else + NothingFound( + AppLocalizations.of(context).noMeasurementEntries, + AppLocalizations.of(context).newEntry, + MeasurementCategoryForm(), + ), + ], + ), + ], + ), + )); } } diff --git a/lib/widgets/measurements/categories_card.dart b/lib/widgets/measurements/categories_card.dart index 772a02dd9..80e746381 100644 --- a/lib/widgets/measurements/categories_card.dart +++ b/lib/widgets/measurements/categories_card.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../models/measurements/measurement_category.dart'; import '../../screens/form_screen.dart'; import '../../screens/measurement_entries_screen.dart'; -import '../core/charts.dart'; +import 'charts.dart'; import 'forms.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class CategoriesCard extends StatelessWidget { MeasurementCategory currentCategory; @@ -27,10 +27,9 @@ class CategoriesCard extends StatelessWidget { ), ), Container( - color: Colors.white, padding: const EdgeInsets.all(10), height: 220, - child: MeasurementChartWidget( + child: MeasurementChartWidgetFl( currentCategory.entries.map((e) => MeasurementChartEntry(e.value, e.date)).toList(), unit: currentCategory.unit, ), diff --git a/lib/widgets/measurements/charts.dart b/lib/widgets/measurements/charts.dart new file mode 100644 index 000000000..fdbae75f4 --- /dev/null +++ b/lib/widgets/measurements/charts.dart @@ -0,0 +1,200 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (C) 2020, 2021 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * wger Workout Manager is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:wger/helpers/charts.dart'; +import 'package:wger/theme/theme.dart'; + +class MeasurementChartWidgetFl extends StatefulWidget { + final List _entries; + final String unit; + + const MeasurementChartWidgetFl(this._entries, {this.unit = 'kg'}); + + @override + State createState() => _MeasurementChartWidgetFlState(); +} + +class _MeasurementChartWidgetFlState extends State { + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.70, + child: Padding( + padding: const EdgeInsets.only( + right: 18, + left: 12, + top: 24, + bottom: 12, + ), + child: LineChart( + mainData(), + ), + ), + ); + } + + LineTouchData tooltipData() { + return LineTouchData(touchTooltipData: LineTouchTooltipData(getTooltipItems: (touchedSpots) { + return touchedSpots.map((touchedSpot) { + return LineTooltipItem( + '${touchedSpot.y} kg', + const TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + ); + }).toList(); + })); + } + + LineChartData mainData() { + return LineChartData( + lineTouchData: tooltipData(), + gridData: FlGridData( + show: true, + drawVerticalLine: true, + //horizontalInterval: 1, + //verticalInterval: interval, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey, + strokeWidth: 1, + ); + }, + getDrawingVerticalLine: (value) { + return FlLine( + color: Colors.grey, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + // Don't show the first and last entries, otherwise they'll overlap with the + // calculated interval + if (value == meta.min || value == meta.max) { + return const Text(''); + } + final DateTime date = DateTime.fromMillisecondsSinceEpoch(value.toInt()); + return Text( + DateFormat.yMd(Localizations.localeOf(context).languageCode).format(date), + ); + }, + interval: widget._entries.isNotEmpty + ? chartGetInterval(widget._entries.last.date, widget._entries.first.date) + : 1000, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 65, + getTitlesWidget: (value, meta) { + return Text( + '$value ${widget.unit}', + ); + }, + ), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: const Color(0xff37434d)), + ), + lineBarsData: [ + LineChartBarData( + spots: [ + ...widget._entries + .map((e) => FlSpot(e.date.millisecondsSinceEpoch.toDouble(), e.value.toDouble())) + ], + isCurved: false, + color: wgerSecondaryColor, + barWidth: 2, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + ), + ), + ], + ); + } +} + +class MeasurementChartEntry { + num value; + DateTime date; + + MeasurementChartEntry(this.value, this.date); +} + +class Indicator extends StatelessWidget { + const Indicator({ + super.key, + required this.color, + required this.text, + required this.isSquare, + this.size = 16, + this.marginRight = 15, + this.textColor, + }); + + final Color color; + final String text; + final bool isSquare; + final double size; + final double marginRight; + final Color? textColor; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: isSquare ? BoxShape.rectangle : BoxShape.circle, + color: color, + ), + ), + const SizedBox( + width: 4, + ), + Text( + text, + style: TextStyle( + color: textColor, + ), + ), + SizedBox( + width: marginRight, + ), + ], + ); + } +} diff --git a/lib/widgets/measurements/entries.dart b/lib/widgets/measurements/entries.dart index 416159ed0..55b2023f2 100644 --- a/lib/widgets/measurements/entries.dart +++ b/lib/widgets/measurements/entries.dart @@ -24,7 +24,7 @@ import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/providers/measurement.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/theme/theme.dart'; -import 'package:wger/widgets/core/charts.dart'; +import 'package:wger/widgets/measurements/charts.dart'; import 'forms.dart'; @@ -40,7 +40,7 @@ class EntriesList extends StatelessWidget { color: Theme.of(context).cardColor, padding: const EdgeInsets.all(10), height: 220, - child: MeasurementChartWidget( + child: MeasurementChartWidgetFl( _category.entries.map((e) => MeasurementChartEntry(e.value, e.date)).toList(), unit: _category.unit, ), diff --git a/lib/widgets/nutrition/charts.dart b/lib/widgets/nutrition/charts.dart index 00588d2c4..24e605ad0 100644 --- a/lib/widgets/nutrition/charts.dart +++ b/lib/widgets/nutrition/charts.dart @@ -16,13 +16,14 @@ * along with this program. If not, see . */ -import 'package:charts_flutter/flutter.dart' as charts; -import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; +import 'package:wger/helpers/colors.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; -import 'package:wger/theme/theme.dart'; +import 'package:wger/widgets/measurements/charts.dart'; class NutritionData { final String name; @@ -31,61 +32,130 @@ class NutritionData { NutritionData(this.name, this.value); } -/// Nutritional plan pie chart widget -class NutritionalPlanPieChartWidget extends StatelessWidget { - final NutritionalValues _nutritionalValues; +class FlNutritionalPlanPieChartWidget extends StatefulWidget { + final NutritionalValues nutritionalValues; - /// [_nutritionalValues] are the calculated [NutritionalValues] for the wanted - /// plan. - const NutritionalPlanPieChartWidget(this._nutritionalValues); + const FlNutritionalPlanPieChartWidget(this.nutritionalValues); @override - Widget build(BuildContext context) { - if (_nutritionalValues.energy == 0) { - return Container(); - } + State createState() => FlNutritionalPlanPieChartState(); +} - return charts.PieChart([ - charts.Series( - id: 'NutritionalValues', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - data: [ - NutritionData( - AppLocalizations.of(context).protein, - _nutritionalValues.protein, - ), - NutritionData( - AppLocalizations.of(context).fat, - _nutritionalValues.fat, - ), - NutritionData( - AppLocalizations.of(context).carbohydrates, - _nutritionalValues.carbohydrates, +class FlNutritionalPlanPieChartState extends State { + int touchedIndex = -1; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const SizedBox( + height: 18, + ), + Expanded( + child: AspectRatio( + aspectRatio: 1, + child: PieChart( + PieChartData( + pieTouchData: PieTouchData( + touchCallback: (FlTouchEvent event, pieTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + touchedIndex = -1; + return; + } + touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex; + }); + }, + ), + borderData: FlBorderData( + show: false, + ), + sectionsSpace: 0, + centerSpaceRadius: 0, + sections: showingSections(), + ), + ), ), - ], - labelAccessorFn: (NutritionData row, _) => - '${row.name}\n${row.value.toStringAsFixed(0)}${AppLocalizations.of(context).g}', - ) - ], - defaultRenderer: charts.ArcRendererConfig(arcWidth: 60, arcRendererDecorators: [ - charts.ArcLabelDecorator( - labelPosition: charts.ArcLabelPosition.outside, - ) - ]) - /* - defaultRenderer: new charts.ArcRendererConfig( - arcWidth: 60, - arcRendererDecorators: [new charts.ArcLabelDecorator()], - ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Indicator( + color: LIST_OF_COLORS3[0], + text: AppLocalizations.of(context).fat, + isSquare: true, + ), + const SizedBox( + height: 4, + ), + Indicator( + color: LIST_OF_COLORS3[1], + text: AppLocalizations.of(context).protein, + isSquare: true, + ), + const SizedBox( + height: 4, + ), + Indicator( + color: LIST_OF_COLORS3[2], + text: AppLocalizations.of(context).carbohydrates, + isSquare: true, + ), + ], + ), + const SizedBox( + width: 28, + ), + ], + ); + } - */ - ); + List showingSections() { + final colors = generateChartColors(3).iterator; + + return List.generate(3, (i) { + final isTouched = i == touchedIndex; + final radius = isTouched ? 92.0 : 80.0; + colors.moveNext(); + + switch (i) { + case 0: + return PieChartSectionData( + color: colors.current, + value: widget.nutritionalValues.fat, + title: '${widget.nutritionalValues.fat.toStringAsFixed(0)}g', + titlePositionPercentageOffset: 0.5, + radius: radius, + titleStyle: const TextStyle(color: Colors.white70)); + case 1: + return PieChartSectionData( + color: colors.current, + value: widget.nutritionalValues.protein, + title: '${widget.nutritionalValues.protein.toStringAsFixed(0)}g', + titlePositionPercentageOffset: 0.5, + radius: radius, + ); + case 2: + return PieChartSectionData( + color: colors.current, + value: widget.nutritionalValues.carbohydrates, + title: '${widget.nutritionalValues.carbohydrates.toStringAsFixed(0)}g', + titlePositionPercentageOffset: 0.5, + radius: radius, + ); + + default: + throw Error(); + } + }); } } -class NutritionalDiaryChartWidget extends StatelessWidget { - const NutritionalDiaryChartWidget({ +class NutritionalDiaryChartWidgetFl extends StatefulWidget { + const NutritionalDiaryChartWidgetFl({ Key? key, required NutritionalPlan nutritionalPlan, }) : _nutritionalPlan = nutritionalPlan, @@ -94,337 +164,421 @@ class NutritionalDiaryChartWidget extends StatelessWidget { final NutritionalPlan _nutritionalPlan; @override - Widget build(BuildContext context) { - return charts.TimeSeriesChart( - [ - charts.Series, DateTime>( - id: 'NutritionDiary', - colorFn: (datum, index) => wgerChartSecondaryColor, - domainFn: (datum, index) => datum[1], - measureFn: (datum, index) => datum[0].energy, - data: _nutritionalPlan.logEntriesValues.keys - .map((e) => [_nutritionalPlan.logEntriesValues[e], e]) - .toList(), - ) - ], - defaultRenderer: charts.BarRendererConfig(), - behaviors: [ - charts.RangeAnnotation([ - charts.LineAnnotationSegment( - _nutritionalPlan.nutritionalValues.energy, - charts.RangeAnnotationAxisType.measure, - strokeWidthPx: 2, - color: charts.MaterialPalette.gray.shade600, - ), - ]), - ], - ); - } + State createState() => NutritionalDiaryChartWidgetFlState(); } -/// Nutritional plan hatch bar chart widget -class NutritionalPlanHatchBarChartWidget extends StatelessWidget { - final NutritionalPlan _nutritionalPlan; - - /// [_nutritionalPlan] is current opened nutrition plan as plan detail. - const NutritionalPlanHatchBarChartWidget(this._nutritionalPlan); - - NutritionalValues nutritionalValuesFromPlanLogsSevenDayAvg() { - NutritionalValues sevenDaysAvg = NutritionalValues(); - int count = 0; - - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - - _nutritionalPlan.logEntriesValues.forEach((key, value) { - if (key.difference(today).inDays >= -7) { - sevenDaysAvg += value; - count++; - } - }); - - if (count != 0) { - sevenDaysAvg.energy = sevenDaysAvg.energy / count; - sevenDaysAvg.protein = sevenDaysAvg.protein / count; - sevenDaysAvg.carbohydrates = sevenDaysAvg.carbohydrates / count; - sevenDaysAvg.carbohydratesSugar = sevenDaysAvg.carbohydratesSugar / count; - sevenDaysAvg.fat = sevenDaysAvg.fat / count; - sevenDaysAvg.fatSaturated = sevenDaysAvg.fatSaturated / count; - sevenDaysAvg.fibres = sevenDaysAvg.fibres / count; - sevenDaysAvg.sodium = sevenDaysAvg.sodium / count; +class NutritionalDiaryChartWidgetFlState extends State { + Widget bottomTitles(double value, TitleMeta meta) { + const style = TextStyle(fontSize: 10); + String text; + switch (value.toInt()) { + case 0: + text = AppLocalizations.of(context).protein; + break; + case 1: + text = AppLocalizations.of(context).carbohydrates; + break; + case 2: + text = AppLocalizations.of(context).sugars; + break; + case 3: + text = AppLocalizations.of(context).fat; + break; + case 4: + text = AppLocalizations.of(context).saturatedFat; + break; + + default: + text = ''; + break; } - - return sevenDaysAvg; + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text(text, style: style), + ); } - NutritionalValues nutritionalValuesFromPlanLogsToday() { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - - return _nutritionalPlan.logEntriesValues[_nutritionalPlan.logEntriesValues.keys - .firstWhereOrNull((d) => d.difference(today).inDays == 0)] ?? - NutritionalValues(); + Widget leftTitles(double value, TitleMeta meta) { + if (value == meta.max) { + return Container(); + } + const style = TextStyle( + fontSize: 10, + ); + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + meta.formattedValue, + style: style, + ), + ); } @override Widget build(BuildContext context) { - final NutritionalValues loggedNutritionalValues = nutritionalValuesFromPlanLogsToday(); - final NutritionalValues sevenDayAvg = nutritionalValuesFromPlanLogsSevenDayAvg(); - - if (_nutritionalPlan.nutritionalValues.energy == 0) { - return Container(); - } - - return Column( - children: [ - Container( - padding: const EdgeInsets.all(15), - height: 220, - child: charts.BarChart( - [ - charts.Series( - id: 'Planned', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - data: [ - NutritionData(AppLocalizations.of(context).energy, - _nutritionalPlan.nutritionalValues.energy), - ], - ), - charts.Series( - id: 'Logged', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - fillPatternFn: (nutritionEntry, index) => charts.FillPatternType.forwardHatch, - data: [ - NutritionData( - AppLocalizations.of(context).energy, loggedNutritionalValues.energy), - ], - ), - charts.Series( - id: 'Avg', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - data: [ - NutritionData(AppLocalizations.of(context).energy, sevenDayAvg.energy), - ], - ), - ], - animate: true, - domainAxis: const charts.OrdinalAxisSpec( - ///labelRotation was added to rotate text of X Axis. Without that, - ///titles would overlap each other - renderSpec: charts.SmallTickRendererSpec(labelRotation: 60), - ), - barGroupingType: charts.BarGroupingType.grouped, - defaultRenderer: charts.BarRendererConfig( - groupingType: charts.BarGroupingType.grouped, strokeWidthPx: 0.0, maxBarWidthPx: 8), - primaryMeasureAxis: const charts.NumericAxisSpec( - tickProviderSpec: charts.BasicNumericTickProviderSpec(desiredTickCount: 5), - ), - ), - ), - Container( - padding: const EdgeInsets.all(15), - height: 300, - child: charts.BarChart( - [ - charts.Series( - id: 'Planned', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - data: [ - // NutritionData( - // AppLocalizations.of(context).energy, - // _nutritionalPlan.nutritionalValues.energy, - // ), - NutritionData( - AppLocalizations.of(context).protein, - _nutritionalPlan.nutritionalValues.protein, + final planned = widget._nutritionalPlan.nutritionalValues; + final loggedToday = widget._nutritionalPlan.nutritionalValuesToday; + final logged7DayAvg = widget._nutritionalPlan.nutritionalValues7DayAvg; + + final colorPlanned = LIST_OF_COLORS3[0]; + final colorLoggedToday = LIST_OF_COLORS3[1]; + final colorLogged7Day = LIST_OF_COLORS3[2]; + + return AspectRatio( + aspectRatio: 1.66, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: LayoutBuilder( + builder: (context, constraints) { + final barsSpace = 4.0 * constraints.maxWidth / 400; + final barsWidth = 8.0 * constraints.maxWidth / 400; + return BarChart( + BarChartData( + alignment: BarChartAlignment.center, + barTouchData: BarTouchData( + enabled: false, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 48, + getTitlesWidget: bottomTitles, + ), ), - NutritionData( - AppLocalizations.of(context).carbohydrates, - _nutritionalPlan.nutritionalValues.carbohydrates, + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: leftTitles, + ), ), - NutritionData( - AppLocalizations.of(context).sugars, - _nutritionalPlan.nutritionalValues.carbohydratesSugar, + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), ), - NutritionData( - AppLocalizations.of(context).fat, - _nutritionalPlan.nutritionalValues.fat, + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), ), - NutritionData( - AppLocalizations.of(context).saturatedFat, - _nutritionalPlan.nutritionalValues.fatSaturated, + ), + gridData: FlGridData( + show: true, + checkToShowHorizontalLine: (value) => value % 10 == 0, + getDrawingHorizontalLine: (value) => FlLine( + color: Colors.black, + strokeWidth: 1, ), - NutritionData( - AppLocalizations.of(context).fibres, - _nutritionalPlan.nutritionalValues.fibres, + drawVerticalLine: false, + ), + borderData: FlBorderData( + show: false, + ), + groupsSpace: 30, + // groupsSpace: barsSpace, + barGroups: [ + BarChartGroupData( + x: 0, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: planned.protein, + color: colorPlanned, + width: barsWidth, + ), + BarChartRodData( + toY: loggedToday.protein, + color: colorLoggedToday, + width: barsWidth, + ), + BarChartRodData( + toY: logged7DayAvg.protein, + color: colorLogged7Day, + width: barsWidth, + ), + ], ), - NutritionData( - AppLocalizations.of(context).sodium, - _nutritionalPlan.nutritionalValues.sodium, + BarChartGroupData( + x: 1, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: planned.carbohydrates, + color: colorPlanned, + width: barsWidth, + ), + BarChartRodData( + toY: loggedToday.carbohydrates, + color: colorLoggedToday, + width: barsWidth, + ), + BarChartRodData( + toY: logged7DayAvg.carbohydrates, + color: colorLogged7Day, + width: barsWidth, + ), + ], + ), + BarChartGroupData( + x: 2, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: planned.carbohydratesSugar, + color: colorPlanned, + width: barsWidth, + ), + BarChartRodData( + toY: loggedToday.carbohydratesSugar, + color: colorLoggedToday, + width: barsWidth, + ), + BarChartRodData( + toY: logged7DayAvg.carbohydratesSugar, + color: colorLogged7Day, + width: barsWidth, + ), + ], + ), + BarChartGroupData( + x: 3, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: planned.fat, + color: colorPlanned, + width: barsWidth, + ), + BarChartRodData( + toY: loggedToday.fat, + color: colorLoggedToday, + width: barsWidth, + ), + BarChartRodData( + toY: logged7DayAvg.fat, + color: colorLogged7Day, + width: barsWidth, + ), + ], + ), + BarChartGroupData( + x: 4, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: planned.fatSaturated, + color: colorPlanned, + width: barsWidth, + ), + BarChartRodData( + toY: loggedToday.fatSaturated, + color: colorLoggedToday, + width: barsWidth, + ), + BarChartRodData( + toY: logged7DayAvg.fatSaturated, + color: colorLogged7Day, + width: barsWidth, + ), + ], ), ], + // barGroups: getData(barsWidth, barsSpace), ), - charts.Series( - id: 'Logged', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - fillPatternFn: (nutritionEntry, index) => charts.FillPatternType.forwardHatch, - data: [ - // NutritionData( - // AppLocalizations.of(context).energy, - // loggedNutritionalValues.energy - // ), - - NutritionData( - AppLocalizations.of(context).protein, loggedNutritionalValues.protein), - NutritionData(AppLocalizations.of(context).carbohydrates, - loggedNutritionalValues.carbohydrates), - NutritionData(AppLocalizations.of(context).sugars, - loggedNutritionalValues.carbohydratesSugar), - NutritionData(AppLocalizations.of(context).fat, loggedNutritionalValues.fat), - NutritionData(AppLocalizations.of(context).saturatedFat, - loggedNutritionalValues.fatSaturated), - NutritionData( - AppLocalizations.of(context).fibres, loggedNutritionalValues.fibres), - NutritionData( - AppLocalizations.of(context).sodium, loggedNutritionalValues.sodium), - ], - ), - charts.Series( - id: 'Avg', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - data: [ - // NutritionData(AppLocalizations.of(context).energy, sevenDayAvg.energy), - NutritionData(AppLocalizations.of(context).protein, sevenDayAvg.protein), - NutritionData( - AppLocalizations.of(context).carbohydrates, sevenDayAvg.carbohydrates), - NutritionData( - AppLocalizations.of(context).sugars, sevenDayAvg.carbohydratesSugar), - NutritionData(AppLocalizations.of(context).fat, sevenDayAvg.fat), - NutritionData( - AppLocalizations.of(context).saturatedFat, sevenDayAvg.fatSaturated), - NutritionData(AppLocalizations.of(context).fibres, sevenDayAvg.fibres), - NutritionData(AppLocalizations.of(context).sodium, sevenDayAvg.sodium), - ], - ), - ], - animate: true, - domainAxis: const charts.OrdinalAxisSpec( - ///labelRotation was added to rotate text of X Axis. Without that, - ///titles would overlap each other - renderSpec: charts.SmallTickRendererSpec(labelRotation: 60), - ), - barGroupingType: charts.BarGroupingType.grouped, - primaryMeasureAxis: const charts.NumericAxisSpec( - tickProviderSpec: charts.BasicNumericTickProviderSpec( - desiredTickCount: 5, - ), - ), - ), + ); + }, ), - ], + ), ); } } -//creating a seperate chart for energy as the energy value and other nutrient's value is not compatable to show in one graph -class EnergyChart extends StatelessWidget { - const EnergyChart({Key? key, required this.nutritionalPlan}) : super(key: key); - final NutritionalPlan nutritionalPlan; - NutritionalValues nutritionalValuesFromPlanLogsSevenDayAvg() { - NutritionalValues sevenDaysAvg = NutritionalValues(); - int count = 0; - - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - - nutritionalPlan.logEntriesValues.forEach((key, value) { - if (key.difference(today).inDays >= -7) { - sevenDaysAvg += value; - count++; - } - }); +class FlNutritionalDiaryChartWidget extends StatefulWidget { + final NutritionalPlan _nutritionalPlan; - if (count != 0) { - sevenDaysAvg.energy = sevenDaysAvg.energy / count; - sevenDaysAvg.protein = sevenDaysAvg.protein / count; - sevenDaysAvg.carbohydrates = sevenDaysAvg.carbohydrates / count; - sevenDaysAvg.carbohydratesSugar = sevenDaysAvg.carbohydratesSugar / count; - sevenDaysAvg.fat = sevenDaysAvg.fat / count; - sevenDaysAvg.fatSaturated = sevenDaysAvg.fatSaturated / count; - sevenDaysAvg.fibres = sevenDaysAvg.fibres / count; - sevenDaysAvg.sodium = sevenDaysAvg.sodium / count; - } + const FlNutritionalDiaryChartWidget({ + Key? key, + required NutritionalPlan nutritionalPlan, + }) : _nutritionalPlan = nutritionalPlan, + super(key: key); - return sevenDaysAvg; - } + final Color barBackgroundColor = Colors.black12; + final Color barColor = Colors.red; + final Color touchedBarColor = Colors.deepOrange; - NutritionalValues nutritionalValuesFromPlanLogsToday() { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); + @override + State createState() => FlNutritionalDiaryChartWidgetState(); +} - return nutritionalPlan.logEntriesValues[nutritionalPlan.logEntriesValues.keys - .firstWhereOrNull((d) => d.difference(today).inDays == 0)] ?? - NutritionalValues(); - } +class FlNutritionalDiaryChartWidgetState extends State { + final Duration animDuration = const Duration(milliseconds: 250); + + int touchedIndex = -1; @override Widget build(BuildContext context) { - final NutritionalValues loggedNutritionalValues = nutritionalValuesFromPlanLogsToday(); - final NutritionalValues sevenDayAvg = nutritionalValuesFromPlanLogsSevenDayAvg(); + return AspectRatio( + aspectRatio: 1, + child: BarChart( + mainBarData(), + swapAnimationDuration: animDuration, + ), + ); + } - if (nutritionalPlan.nutritionalValues.energy == 0) { + List getDatesBetween(DateTime startDate, DateTime endDate) { + final List dateList = []; + DateTime currentDate = startDate; + + while (currentDate.isBefore(endDate) || currentDate.isAtSameMomentAs(endDate)) { + dateList.add(currentDate); + currentDate = currentDate.add(const Duration(days: 1)); + } + + return dateList; + } + + BarChartGroupData makeGroupData( + int x, + double y, { + bool isTouched = false, + Color? barColor, + double width = 1.5, + List showTooltips = const [], + }) { + barColor ??= widget.barColor; + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + toY: isTouched ? y + 1 : y, + color: isTouched ? widget.touchedBarColor : barColor, + width: width, + borderSide: isTouched + ? const BorderSide(color: Colors.black54) + : const BorderSide(color: Colors.white, width: 0), + backDrawRodData: BackgroundBarChartRodData( + show: true, + toY: 20, + color: widget.barBackgroundColor, + ), + ), + ], + showingTooltipIndicators: showTooltips, + ); + } + + List showingGroups() { + final logEntries = widget._nutritionalPlan.logEntriesValues; + final List out = []; + final dateList = getDatesBetween(logEntries.keys.first, logEntries.keys.last); + + for (final date in dateList.reversed) { + out.add( + makeGroupData( + date.millisecondsSinceEpoch, + logEntries.containsKey(date) ? logEntries[date]!.energy : 0, + isTouched: date.millisecondsSinceEpoch == touchedIndex, + ), + ); + } + + return out; + } + + Widget leftTitles(double value, TitleMeta meta) { + if (value == meta.max) { return Container(); } + const style = TextStyle( + fontSize: 10, + ); + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + '${meta.formattedValue} kcal', + style: style, + ), + ); + } - return charts.BarChart( - [ - charts.Series( - id: 'Planned', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - data: [ - NutritionData( - AppLocalizations.of(context).energy, - nutritionalPlan.nutritionalValues.energy, - ), - ], + BarChartData mainBarData() { + return BarChartData( + barTouchData: BarTouchData( + touchTooltipData: BarTouchTooltipData( + tooltipBgColor: Colors.blueGrey, + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipMargin: -10, + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final date = DateTime.fromMillisecondsSinceEpoch(group.x); + + return BarTooltipItem( + '${DateFormat.yMMMd(Localizations.localeOf(context).languageCode).format(date)}\n', + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: '${(rod.toY - 1).toStringAsFixed(0)} kcal', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + }, ), - charts.Series( - id: 'Logged', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - fillPatternFn: (nutritionEntry, index) => charts.FillPatternType.forwardHatch, - data: [ - NutritionData(AppLocalizations.of(context).energy, loggedNutritionalValues.energy), - ], + touchCallback: (FlTouchEvent event, barTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + barTouchResponse == null || + barTouchResponse.spot == null) { + touchedIndex = -1; + return; + } + touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex; + }); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), ), - charts.Series( - id: 'Avg', - domainFn: (nutritionEntry, index) => nutritionEntry.name, - measureFn: (nutritionEntry, index) => nutritionEntry.value, - data: [ - NutritionData(AppLocalizations.of(context).energy, sevenDayAvg.energy), - ], + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), ), - ], - animate: true, - domainAxis: const charts.OrdinalAxisSpec( - ///labelRotation was added to rotate text of X Axis. Without that, - ///titles would overlap each other - renderSpec: charts.SmallTickRendererSpec(labelRotation: 60), + bottomTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 60, + getTitlesWidget: leftTitles, + ), + ), + ), + borderData: FlBorderData( + show: false, ), - barGroupingType: charts.BarGroupingType.grouped, - defaultRenderer: charts.BarRendererConfig( - groupingType: charts.BarGroupingType.grouped, strokeWidthPx: 0.0, maxBarWidthPx: 8), - primaryMeasureAxis: const charts.NumericAxisSpec( - tickProviderSpec: charts.BasicNumericTickProviderSpec(desiredTickCount: 5), + gridData: FlGridData( + show: true, + getDrawingHorizontalLine: (value) => FlLine( + color: Colors.grey, + strokeWidth: 1, + ), + drawVerticalLine: false, ), + barGroups: showingGroups(), + ); + } + + Future refreshState() async { + setState(() {}); + await Future.delayed( + animDuration + const Duration(milliseconds: 50), ); } } diff --git a/lib/widgets/nutrition/nutritional_diary_detail.dart b/lib/widgets/nutrition/nutritional_diary_detail.dart index fff02c400..6368fbe78 100644 --- a/lib/widgets/nutrition/nutritional_diary_detail.dart +++ b/lib/widgets/nutrition/nutritional_diary_detail.dart @@ -32,6 +32,7 @@ class NutritionalDiaryDetailWidget extends StatelessWidget { final NutritionalPlan _nutritionalPlan; final DateTime _date; static const double tablePadding = 7; + const NutritionalDiaryDetailWidget(this._nutritionalPlan, this._date); Widget getTable( @@ -223,7 +224,7 @@ class NutritionalDiaryDetailWidget extends StatelessWidget { Container( padding: const EdgeInsets.all(15), height: 220, - child: NutritionalPlanPieChartWidget(valuesDate), + child: FlNutritionalPlanPieChartWidget(valuesDate), ), Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/widgets/nutrition/nutritional_plan_detail.dart b/lib/widgets/nutrition/nutritional_plan_detail.dart index 671d0b4fe..993a28630 100644 --- a/lib/widgets/nutrition/nutritional_plan_detail.dart +++ b/lib/widgets/nutrition/nutritional_plan_detail.dart @@ -20,19 +20,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:wger/helpers/colors.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; import 'package:wger/providers/body_weight.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/screens/nutritional_diary_screen.dart'; import 'package:wger/theme/theme.dart'; +import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/nutrition/charts.dart'; import 'package:wger/widgets/nutrition/forms.dart'; import 'package:wger/widgets/nutrition/meal.dart'; class NutritionalPlanDetailWidget extends StatelessWidget { final NutritionalPlan _nutritionalPlan; + const NutritionalPlanDetailWidget(this._nutritionalPlan); + static const double tablePadding = 7; @override @@ -70,7 +74,7 @@ class NutritionalPlanDetailWidget extends StatelessWidget { Container( padding: const EdgeInsets.all(15), height: 220, - child: NutritionalPlanPieChartWidget(nutritionalValues), // chart + child: FlNutritionalPlanPieChartWidget(nutritionalValues), // chart ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), @@ -210,53 +214,82 @@ class NutritionalPlanDetailWidget extends StatelessWidget { textAlign: TextAlign.center, style: Theme.of(context).textTheme.headline6, ), - - NutritionalPlanHatchBarChartWidget(_nutritionalPlan), - // Container( - // padding: const EdgeInsets.all(15), - // height: 300, - // child: NutritionalPlanHatchBarChartWidget(_nutritionalPlan), // chart - // ), - const Padding(padding: EdgeInsets.all(8.0)), - Text( - AppLocalizations.of(context).nutritionalDiary, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline6, - ), Container( - padding: const EdgeInsets.all(15), - height: 220, - child: NutritionalDiaryChartWidget(nutritionalPlan: _nutritionalPlan), // chart + padding: const EdgeInsets.only(top: 15, left: 15, right: 15), + height: 300, + child: NutritionalDiaryChartWidgetFl(nutritionalPlan: _nutritionalPlan), // chart + ), + Padding( + padding: const EdgeInsets.only(bottom: 40, left: 25, right: 25), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Indicator( + color: LIST_OF_COLORS3[0], + text: AppLocalizations.of(context).planned, + isSquare: true, + marginRight: 0, + ), + Indicator( + color: LIST_OF_COLORS3[1], + text: AppLocalizations.of(context).logged, + isSquare: true, + marginRight: 0, + ), + Indicator( + color: LIST_OF_COLORS3[2], + text: AppLocalizations.of(context).weekAverage, + isSquare: true, + marginRight: 0, + ), + ], + ), ), - SizedBox( - height: 200, - child: ListView( - scrollDirection: Axis.horizontal, + if (_nutritionalPlan.logEntriesValues.isNotEmpty) + Column( children: [ - Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, + Text( + AppLocalizations.of(context).nutritionalDiary, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline6, + ), + Container( + padding: const EdgeInsets.all(15), + height: 220, + child: FlNutritionalDiaryChartWidget(nutritionalPlan: _nutritionalPlan), // chart + ), + SizedBox( + height: 200, + child: ListView( + scrollDirection: Axis.horizontal, children: [ - TextButton(onPressed: () {}, child: const Text('')), - Text( - '${AppLocalizations.of(context).energyShort} (${AppLocalizations.of(context).kcal})'), - Text( - '${AppLocalizations.of(context).proteinShort} (${AppLocalizations.of(context).g})'), - Text( - '${AppLocalizations.of(context).carbohydratesShort} (${AppLocalizations.of(context).g})'), - Text( - '${AppLocalizations.of(context).fatShort} (${AppLocalizations.of(context).g})'), + Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + TextButton(onPressed: () {}, child: const Text('')), + Text( + '${AppLocalizations.of(context).energyShort} (${AppLocalizations.of(context).kcal})'), + Text( + '${AppLocalizations.of(context).proteinShort} (${AppLocalizations.of(context).g})'), + Text( + '${AppLocalizations.of(context).carbohydratesShort} (${AppLocalizations.of(context).g})'), + Text( + '${AppLocalizations.of(context).fatShort} (${AppLocalizations.of(context).g})'), + ], + ), + ), + ..._nutritionalPlan.logEntriesValues.entries + .map((entry) => + NutritionDiaryEntry(entry.key, entry.value, _nutritionalPlan)) + .toList() + .reversed, ], ), - ), - ..._nutritionalPlan.logEntriesValues.entries - .map((entry) => NutritionDiaryEntry(entry.key, entry.value, _nutritionalPlan)) - .toList() - .reversed, + ) ], ), - ) ], ), ); diff --git a/lib/widgets/weight/entries_list.dart b/lib/widgets/weight/entries_list.dart index 5d55b0317..10e51dcca 100644 --- a/lib/widgets/weight/entries_list.dart +++ b/lib/widgets/weight/entries_list.dart @@ -24,7 +24,7 @@ import 'package:wger/providers/body_weight.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/screens/measurement_categories_screen.dart'; import 'package:wger/theme/theme.dart'; -import 'package:wger/widgets/core/charts.dart'; +import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/weight/forms.dart'; class WeightEntriesList extends StatelessWidget { @@ -38,7 +38,7 @@ class WeightEntriesList extends StatelessWidget { color: Theme.of(context).cardColor, padding: const EdgeInsets.all(15), height: 220, - child: MeasurementChartWidget( + child: MeasurementChartWidgetFl( _weightProvider.items.map((e) => MeasurementChartEntry(e.weight, e.date)).toList()), ), TextButton( diff --git a/lib/widgets/workouts/app_bar.dart b/lib/widgets/workouts/app_bar.dart index ab8bd1480..73ce790d4 100644 --- a/lib/widgets/workouts/app_bar.dart +++ b/lib/widgets/workouts/app_bar.dart @@ -26,7 +26,7 @@ enum _WorkoutAppBarOptions { contribute, } -class WorkoutOverviewAppBar extends StatelessWidget with PreferredSizeWidget { +class WorkoutOverviewAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { return AppBar( diff --git a/lib/widgets/workouts/charts.dart b/lib/widgets/workouts/charts.dart index 6cfd628e9..cbf5bbd57 100644 --- a/lib/widgets/workouts/charts.dart +++ b/lib/widgets/workouts/charts.dart @@ -16,66 +16,148 @@ * along with this program. If not, see . */ -import 'package:charts_flutter/flutter.dart' as charts; +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; +import 'package:wger/helpers/charts.dart'; +import 'package:wger/helpers/colors.dart'; -/// Sample time series data type. -class TimeSeriesLog { - final DateTime time; - final double weight; - - TimeSeriesLog(this.time, this.weight); -} - -class LogChartWidget extends StatelessWidget { +class LogChartWidgetFl extends StatefulWidget { final Map _data; final DateTime _currentDate; - const LogChartWidget(this._data, this._currentDate); + + const LogChartWidgetFl(this._data, this._currentDate); + @override + State createState() => _LogChartWidgetFlState(); +} + +class _LogChartWidgetFlState extends State { @override Widget build(BuildContext context) { - return _data.containsKey('chart_data') && _data['chart_data'].length > 0 - ? charts.TimeSeriesChart( - [ - ..._data['chart_data'].map((e) { - return charts.Series( - id: '${e.first['reps']} ${AppLocalizations.of(context).reps}', - domainFn: (datum, index) => datum.time, - measureFn: (datum, index) => datum.weight, - data: [ - ...e.map( - (entry) => TimeSeriesLog( - DateTime.parse(entry['date']), - double.parse(entry['weight']), - ), - ), - ], - ); - }), - ], - primaryMeasureAxis: const charts.NumericAxisSpec( - tickProviderSpec: charts.BasicNumericTickProviderSpec(zeroBound: false), + return AspectRatio( + aspectRatio: 1.70, + child: Padding( + padding: const EdgeInsets.only( + top: 24, + bottom: 12, + ), + child: LineChart( + mainData(), + ), + ), + ); + } + + LineTouchData tooltipData() { + return LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (touchedSpots) { + return touchedSpots.map((touchedSpot) { + final reps = widget._data['chart_data'][touchedSpot.barIndex].first['reps']; + + return LineTooltipItem( + '$reps × ${touchedSpot.y} kg', + const TextStyle(color: Colors.white), + ); + }).toList(); + }, + ), + ); + } + + LineChartData mainData() { + final colors = generateChartColors(widget._data['chart_data'].length).iterator; + + return LineChartData( + lineTouchData: tooltipData(), + gridData: FlGridData( + show: true, + drawVerticalLine: true, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey, + strokeWidth: 1, + ); + }, + getDrawingVerticalLine: (value) { + return FlLine( + color: Colors.grey, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + // Don't show the first and last entries, otherwise they'll overlap with the + // calculated interval + if (value == meta.min || value == meta.max) { + return const Text(''); + } + + final DateTime date = DateTime.fromMillisecondsSinceEpoch(value.toInt()); + return Text( + DateFormat.yMd(Localizations.localeOf(context).languageCode).format(date), + ); + }, + interval: chartGetInterval( + DateTime.parse(widget._data['logs'].keys.first), + DateTime.parse(widget._data['logs'].keys.last), ), - behaviors: [ - charts.SeriesLegend( - position: charts.BehaviorPosition.bottom, - desiredMaxColumns: 4, - ), - charts.RangeAnnotation([ - charts.LineAnnotationSegment( - _currentDate, charts.RangeAnnotationAxisType.domain, - strokeWidthPx: 2, - labelPosition: charts.AnnotationLabelPosition.margin, - color: charts.Color.black, - dashPattern: [0, 1, 1, 1], - //startLabel: DateFormat.yMd(Localizations.localeOf(context).languageCode) - // .format(_currentDate), - ) - ]), + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 70, + getTitlesWidget: (value, meta) { + return Text('$value ${AppLocalizations.of(context).kg}'); + }, + ), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: const Color(0xff37434d)), + ), + lineBarsData: [ + ...widget._data['chart_data'].map((e) { + colors.moveNext(); + return LineChartBarData( + spots: [ + ...e.map( + (entry) => FlSpot( + DateTime.parse(entry['date']).millisecondsSinceEpoch.toDouble(), + double.parse(entry['weight']), + ), + ) ], - ) - : Container(); + isCurved: true, + color: colors.current, + barWidth: 2, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + getDotPainter: (p0, p1, p2, p3) => FlDotCirclePainter( + radius: 2, + color: Colors.black, + strokeWidth: 0, + ), + ), + ); + }) + ], + ); } } diff --git a/lib/widgets/workouts/log.dart b/lib/widgets/workouts/log.dart index c0fd30c5b..c9feff23f 100644 --- a/lib/widgets/workouts/log.dart +++ b/lib/widgets/workouts/log.dart @@ -17,14 +17,15 @@ */ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:wger/helpers/colors.dart'; import 'package:wger/helpers/ui.dart'; import 'package:wger/models/exercises/base.dart'; import 'package:wger/models/workouts/log.dart'; import 'package:wger/models/workouts/session.dart'; import 'package:wger/providers/workout_plans.dart'; +import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/workouts/charts.dart'; class ExerciseLogChart extends StatelessWidget { @@ -37,20 +38,51 @@ class ExerciseLogChart extends StatelessWidget { Widget build(BuildContext context) { final workoutPlansData = Provider.of(context, listen: false); final workout = workoutPlansData.currentPlan; + var colors = generateChartColors(1).iterator; Future> getChartEntries(BuildContext context) async { return workoutPlansData.fetchLogData(workout!, _base); } return FutureBuilder( - future: getChartEntries(context), - builder: (context, AsyncSnapshot> snapshot) => SizedBox( - height: 150, - child: snapshot.connectionState == ConnectionState.waiting - ? const Center(child: CircularProgressIndicator()) - : LogChartWidget(snapshot.data!, _currentDate), - ), - ); + future: getChartEntries(context), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + colors = generateChartColors(snapshot.data!['chart_data'].length).iterator; + } + + return SizedBox( + height: 260, + child: snapshot.connectionState == ConnectionState.waiting + ? const Center(child: CircularProgressIndicator()) + : Column( + mainAxisSize: MainAxisSize.max, + children: [ + LogChartWidgetFl(snapshot.data!, _currentDate), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...snapshot.data!['chart_data'].map((e) { + // e is the list of logs with the same reps, so we can just take the + // first entry and read the reps from it. Yes, this is an amazingly ugly hack + final reps = e.first['reps']; + + colors.moveNext(); + return Indicator( + color: colors.current, + text: reps.toString(), + isSquare: false, + ); + }).toList(), + ], + ), + const SizedBox( + height: 15, + ) + ], + ), + ); + }); } } @@ -109,8 +141,10 @@ class _DayLogWidgetState extends State { ), ) .toList(), - ExerciseLogChart(base, widget._date), - const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: ExerciseLogChart(base, widget._date), + ) ], ); }).toList() diff --git a/pubspec.lock b/pubspec.lock index 3aebf247e..4415434e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" charcode: dependency: transitive description: @@ -201,22 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" - charts_common: - dependency: transitive - description: - name: charts_common - sha256: "7b8922f9b0d9b134122756a787dab1c3946ae4f3fc5022ff323ba0014998ea02" - url: "https://pub.dev" - source: hosted - version: "0.12.0" - charts_flutter: - dependency: "direct main" - description: - name: charts_flutter - sha256: "4172c3f4b85322fdffe1896ffbed79ae4689ae72cb6fe6690dcaaea620a9c558" - url: "https://pub.dev" - source: hosted - version: "0.12.0" checked_yaml: dependency: transitive description: @@ -269,10 +253,10 @@ packages: dependency: "direct main" description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.2" conventional_commit: dependency: transitive description: @@ -401,6 +385,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "48a1b69be9544e2b03d9a8e843affd89e43f3194c9248776222efcb4206bb1ec" + url: "https://pub.dev" + source: hosted + version: "0.62.0" flutter: dependency: "direct main" description: flutter @@ -792,18 +784,18 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" melos: dependency: transitive description: @@ -816,10 +808,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -832,10 +824,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 + sha256: "8b46d7eb40abdda92d62edd01546051f0c27365e65608c284de336dccfef88cc" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" multi_select_flutter: dependency: "direct main" description: @@ -896,10 +888,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_parsing: dependency: transitive description: @@ -1165,10 +1157,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -1229,10 +1221,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.6.0" timing: dependency: transitive description: @@ -1413,10 +1405,10 @@ packages: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "11.7.1" watcher: dependency: transitive description: @@ -1425,6 +1417,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1437,10 +1437,10 @@ packages: dependency: transitive description: name: webdriver - sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" win32: dependency: transitive description: @@ -1482,5 +1482,5 @@ packages: source: hosted version: "2.1.1" sdks: - dart: ">=2.19.0 <3.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 52a4a99b7..929009f94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.5.6+34 environment: - sdk: '>=2.17.0 <3.0.0' + sdk: ">=2.17.0 <3.0.0" dependencies: flutter: @@ -33,7 +33,6 @@ dependencies: sdk: flutter android_metadata: ^0.2.1 - charts_flutter: ^0.12.0 collection: ^1.17.0 cupertino_icons: ^1.0.5 equatable: ^2.0.5 @@ -58,8 +57,12 @@ dependencies: carousel_slider: ^4.2.1 multi_select_flutter: ^4.1.3 flutter_svg: ^2.0.5 + fl_chart: ^0.62.0 flutter_zxing: ^1.1.2 +dependency_overrides: + intl: any + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/measurements/measurement_categories_screen_test.dart b/test/measurements/measurement_categories_screen_test.dart index e1d59cd48..1fa899e4d 100644 --- a/test/measurements/measurement_categories_screen_test.dart +++ b/test/measurements/measurement_categories_screen_test.dart @@ -26,7 +26,7 @@ import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; import 'package:wger/providers/measurement.dart'; import 'package:wger/screens/measurement_categories_screen.dart'; -import 'package:wger/widgets/core/charts.dart'; +import 'package:wger/widgets/measurements/charts.dart'; import 'measurement_categories_screen_test.mocks.dart'; @@ -68,6 +68,6 @@ void main() { expect(find.text('body fat'), findsOneWidget); expect(find.text('biceps'), findsOneWidget); expect(find.byType(Card), findsNWidgets(2)); - expect(find.byType(MeasurementChartWidget), findsNWidgets(2)); + expect(find.byType(MeasurementChartWidgetFl), findsNWidgets(2)); }); } diff --git a/test/measurements/measurement_entries_screen_test.dart b/test/measurements/measurement_entries_screen_test.dart index 58572d614..20b9b5dcd 100644 --- a/test/measurements/measurement_entries_screen_test.dart +++ b/test/measurements/measurement_entries_screen_test.dart @@ -73,8 +73,8 @@ void main() { expect(find.text('body fat'), findsOneWidget); // Entries - expect(find.text('10.2 %'), findsOneWidget); - expect(find.text('18.1 %'), findsOneWidget); + expect(find.text('10.2 %'), findsNWidgets(2)); + expect(find.text('18.1 %'), findsNWidgets(2)); }); testWidgets('Tests the localization of dates - EN', (WidgetTester tester) async { @@ -82,8 +82,9 @@ void main() { await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); - expect(find.text('8/1/2021'), findsOneWidget); - expect(find.text('8/10/2021'), findsOneWidget); + // From the entries list and from the chart + expect(find.text('8/1/2021'), findsWidgets); + expect(find.text('8/10/2021'), findsWidgets); }); testWidgets('Tests the localization of dates - DE', (WidgetTester tester) async { @@ -91,7 +92,7 @@ void main() { await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); - expect(find.text('1.8.2021'), findsOneWidget); - expect(find.text('10.8.2021'), findsOneWidget); + expect(find.text('1.8.2021'), findsWidgets); + expect(find.text('10.8.2021'), findsWidgets); }); } diff --git a/test/nutrition/nutritional_diary_test.dart b/test/nutrition/nutritional_diary_test.dart index d65029a76..a1372b7db 100644 --- a/test/nutrition/nutritional_diary_test.dart +++ b/test/nutrition/nutritional_diary_test.dart @@ -41,7 +41,7 @@ void main() { testWidgets('Test the detail view for the nutritional plan', (WidgetTester tester) async { await tester.pumpWidget(getWidget()); - expect(find.byType(NutritionalPlanPieChartWidget), findsOneWidget); + expect(find.byType(FlNutritionalPlanPieChartWidget), findsOneWidget); expect(find.byType(Table), findsOneWidget); expect(find.text('519kcal'), findsOneWidget, reason: 'find total energy'); diff --git a/test/nutrition/nutritional_plan_screen_test.dart b/test/nutrition/nutritional_plan_screen_test.dart index b2419ddad..af66d0056 100644 --- a/test/nutrition/nutritional_plan_screen_test.dart +++ b/test/nutrition/nutritional_plan_screen_test.dart @@ -81,7 +81,7 @@ void main() { expect(find.text('300g Broccoli cake'), findsOneWidget); expect(find.byType(Dismissible), findsNWidgets(2)); - expect(find.byType(NutritionalDiaryChartWidget), findsNothing); + expect(find.byType(FlNutritionalDiaryChartWidget), findsNothing); }); testWidgets('Tests the localization of times - EN', (WidgetTester tester) async { diff --git a/test/utils/colors.dart b/test/utils/colors.dart new file mode 100644 index 000000000..d3e48d7d8 --- /dev/null +++ b/test/utils/colors.dart @@ -0,0 +1,49 @@ +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:wger/helpers/colors.dart'; + +void main() { + group('test the color utility', () { + test('3 items or less', () { + final result = generateChartColors(2).iterator; + expect(result.current, equals(const Color(0xFF2A4C7D))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFD45089))); + }); + + test('5 items or less', () { + final result = generateChartColors(5).iterator; + expect(result.current, equals(const Color(0xFF2A4C7D))); + result.moveNext(); + expect(result.current, equals(const Color(0xFF825298))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFD45089))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFF6A59))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFFA600))); + }); + + test('8 items or more - last ones undefined', () { + final result = generateChartColors(8).iterator; + expect(result.current, equals(const Color(0xFF2A4C7D))); + result.moveNext(); + expect(result.current, equals(const Color(0xFF5B5291))); + result.moveNext(); + expect(result.current, equals(const Color(0xFF8E5298))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFBF5092))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFE7537E))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFF6461))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFF813D))); + result.moveNext(); + expect(result.current, equals(const Color(0xFFFFA600))); + result.moveNext(); + expect(result.current, isNull); + }); + }); +} diff --git a/test/weight/weight_screen_test.dart b/test/weight/weight_screen_test.dart index 3ebceef53..10bd75bd0 100644 --- a/test/weight/weight_screen_test.dart +++ b/test/weight/weight_screen_test.dart @@ -25,7 +25,7 @@ import 'package:provider/provider.dart'; import 'package:wger/providers/body_weight.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/screens/weight_screen.dart'; -import 'package:wger/widgets/core/charts.dart'; +import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/weight/forms.dart'; import '../../test_data/body_weight.dart'; @@ -57,7 +57,7 @@ void main() { await tester.pumpWidget(createWeightScreen()); expect(find.text('Weight'), findsOneWidget); - expect(find.byType(MeasurementChartWidget), findsOneWidget); + expect(find.byType(MeasurementChartWidgetFl), findsOneWidget); expect(find.byType(Dismissible), findsNWidgets(2)); expect(find.byType(ListTile), findsNWidgets(2)); }); @@ -83,6 +83,7 @@ void main() { testWidgets('Tests the localization of dates - EN', (WidgetTester tester) async { await tester.pumpWidget(createWeightScreen()); + // One in the entries list, one in the chart expect(find.text('1/1/2021'), findsOneWidget); expect(find.text('1/10/2021'), findsOneWidget); }); diff --git a/test_driver/screenshot_driver.dart b/test_driver/screenshot_driver.dart index 476709ee4..f98d10c7f 100644 --- a/test_driver/screenshot_driver.dart +++ b/test_driver/screenshot_driver.dart @@ -6,7 +6,7 @@ import 'package:integration_test/integration_test_driver_extended.dart'; Future main() async { try { await integrationDriver( - onScreenshot: (String screenshotName, List screenshotBytes) async { + onScreenshot: (String screenshotName, List screenshotBytes, [_]) async { final File image = await File(screenshotName).create(recursive: true); image.writeAsBytesSync(screenshotBytes); return true;