diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b185a7107..47d955f6f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -534,6 +534,38 @@ } } }, + "chartLastYearTitle": "{name} last year", + "@chartLastYearTitle": { + "description": "last year chart of 'name' (e.g. 'weight', 'body fat' etc.)", + "type": "text", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chartLast3MonthsTitle": "{name} last 3 months", + "@chartLast3MonthsTitle": { + "description": "last 3 months chart of 'name' (e.g. 'weight', 'body fat' etc.)", + "type": "text", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chartRangeAll": "All", + "@chartRangeAll": { + "description": "Label for the chart time-range selector option showing all data" + }, + "chartRangeLastYear": "Last year", + "@chartRangeLastYear": { + "description": "Label for the chart time-range selector option showing the last year" + }, + "chartRangeLast3Months": "Last 3 months", + "@chartRangeLast3Months": { + "description": "Label for the chart time-range selector option showing the last 3 months" + }, "measurement": "Measurement", "@measurement": {}, "measurements": "Measurements", diff --git a/lib/widgets/measurements/helpers.dart b/lib/widgets/measurements/helpers.dart index 00369b06d..67c75f891 100644 --- a/lib/widgets/measurements/helpers.dart +++ b/lib/widgets/measurements/helpers.dart @@ -42,12 +42,13 @@ List getOverviewWidgetsSeries( List entries7dAvg, List plans, String unit, - BuildContext context, -) { + BuildContext context, { + String? mainChartTitle, +}) { final monthAgo = DateTime.now().subtract(const Duration(days: 30)); return [ ...getOverviewWidgets( - AppLocalizations.of(context).chartAllTimeTitle(name), + mainChartTitle ?? AppLocalizations.of(context).chartAllTimeTitle(name), entriesAll, entries7dAvg, unit, diff --git a/lib/widgets/weight/weight_overview.dart b/lib/widgets/weight/weight_overview.dart index 53ef5db4a..0533860f5 100644 --- a/lib/widgets/weight/weight_overview.dart +++ b/lib/widgets/weight/weight_overview.dart @@ -19,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod; import 'package:intl/intl.dart'; +import 'package:wger/helpers/measurements.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/body_weight/weight_entry.dart'; import 'package:wger/providers/body_weight_notifier.dart'; @@ -31,11 +32,22 @@ import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/measurements/helpers.dart'; import 'package:wger/widgets/weight/forms.dart'; -class WeightOverview extends riverpod.ConsumerWidget { +/// Time range the user can pick to limit how far back the weight chart goes. +enum WeightChartRange { all, lastYear, last3Months } + +class WeightOverview extends riverpod.ConsumerStatefulWidget { const WeightOverview(); @override - Widget build(BuildContext context, riverpod.WidgetRef ref) { + riverpod.ConsumerState createState() => _WeightOverviewState(); +} + +class _WeightOverviewState extends riverpod.ConsumerState { + WeightChartRange _range = WeightChartRange.all; + + @override + Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); final profileAsync = ref.watch(userProfileProvider); final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); final plans = ref.watch(nutritionProvider).value?.plans ?? const []; @@ -54,17 +66,63 @@ class WeightOverview extends riverpod.ConsumerWidget { final entriesAll = entriesList.map((e) => MeasurementChartEntry(e.weight, e.date)).toList(); final entries7dAvg = moving7dAverage(entriesAll); + // Restrict the data to the selected range. The average is computed over + // the full history first and only filtered afterwards, matching how the + // per-plan and 30-day sub-charts are built. + final (DateTime? cutoff, String? mainChartTitle) = switch (_range) { + WeightChartRange.all => (null, null), + WeightChartRange.lastYear => ( + DateTime.now().subtract(const Duration(days: 365)), + i18n.chartLastYearTitle(i18n.weight), + ), + WeightChartRange.last3Months => ( + DateTime.now().subtract(const Duration(days: 90)), + i18n.chartLast3MonthsTitle(i18n.weight), + ), + }; + final entriesRange = cutoff == null ? entriesAll : entriesAll.whereDate(cutoff, null); + final entries7dAvgRange = + cutoff == null ? entries7dAvg : entries7dAvg.whereDate(cutoff, null); + final unit = weightUnit(profile.isMetric, context); return Column( children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SegmentedButton( + key: const ValueKey('weightChartRangeSelector'), + showSelectedIcon: false, + segments: [ + ButtonSegment( + value: WeightChartRange.all, + label: Text(i18n.chartRangeAll), + ), + ButtonSegment( + value: WeightChartRange.lastYear, + label: Text(i18n.chartRangeLastYear), + ), + ButtonSegment( + value: WeightChartRange.last3Months, + label: Text(i18n.chartRangeLast3Months), + ), + ], + selected: {_range}, + onSelectionChanged: (selection) => + setState(() => _range = selection.first), + ), + ), + ), ...getOverviewWidgetsSeries( - AppLocalizations.of(context).weight, - entriesAll, - entries7dAvg, + i18n.weight, + entriesRange, + entries7dAvgRange, plans, unit, context, + mainChartTitle: mainChartTitle, ), TextButton( onPressed: () => Navigator.pushNamed( diff --git a/test/weight/weight_screen_test.dart b/test/weight/weight_screen_test.dart index d6391e31e..22e15953d 100644 --- a/test/weight/weight_screen_test.dart +++ b/test/weight/weight_screen_test.dart @@ -94,6 +94,33 @@ void main() { expect(find.byType(ListTile), findsNWidgets(2)); }); + testWidgets('Weight chart range selector filters the data', (WidgetTester tester) async { + await tester.pumpWidget(createWeightScreen()); + await tester.pumpAndSettle(); + + // The range selector is shown with the three options + expect(find.byKey(const ValueKey('weightChartRangeSelector')), findsOneWidget); + expect(find.text('All'), findsOneWidget); + expect(find.text('Last year'), findsOneWidget); + expect(find.text('Last 3 months'), findsOneWidget); + + // By default (all time) the chart is shown for the seeded entries + expect(find.byType(MeasurementChartWidgetFl), findsOneWidget); + + // Restricting to the last 3 months excludes the (old) seeded data + await tester.ensureVisible(find.text('Last 3 months')); + await tester.tap(find.text('Last 3 months')); + await tester.pumpAndSettle(); + expect(find.byType(MeasurementChartWidgetFl), findsNothing); + expect(find.text('No data available'), findsOneWidget); + + // Switching back to all time restores the chart + await tester.ensureVisible(find.text('All')); + await tester.tap(find.text('All')); + await tester.pumpAndSettle(); + expect(find.byType(MeasurementChartWidgetFl), findsOneWidget); + }); + testWidgets('Test deleting an item using the Delete button', (WidgetTester tester) async { // Arrange await tester.pumpWidget(createWeightScreen());