Commit 0b4778a
Changed files (12)
app
lib
data_util
features
data_picker
export_import
measurement_list
settings
l10n
model
storage
test
data_util
screens
app/lib/data_util/full_entry_builder.dart
@@ -2,6 +2,7 @@ import 'package:blood_pressure_app/data_util/repository_builder.dart';
import 'package:blood_pressure_app/model/storage/interval_store.dart';
import 'package:flutter/material.dart';
import 'package:health_data_store/health_data_store.dart';
+import 'package:provider/provider.dart';
/// Shorthand class for getting a [rangeType]s [FullEntry] values.
class FullEntryBuilder extends StatelessWidget {
@@ -32,6 +33,23 @@ class FullEntryBuilder extends StatelessWidget {
onData: (BuildContext context, List<MedicineIntake> intakes) => RepositoryBuilder<Note, NoteRepository>(
rangeType: rangeType,
onData: (BuildContext context, List<Note> notes) {
+ final manager = context.watch<IntervalStoreManager>();
+ final timeLimitRange = manager.get(rangeType).timeLimitRange;
+ if (timeLimitRange != null) {
+ records = records.where((r) {
+ final time = TimeOfDay.fromDateTime(r.time);
+ return time.isAfter(timeLimitRange.start) && time.isBefore(timeLimitRange.end);
+ }).toList();
+ intakes = intakes.where((i) {
+ final time = TimeOfDay.fromDateTime(i.time);
+ return time.isAfter(timeLimitRange.start) && time.isBefore(timeLimitRange.end);
+ }).toList();
+ notes = notes.where((n) {
+ final time = TimeOfDay.fromDateTime(n.time);
+ return time.isAfter(timeLimitRange.start) && time.isBefore(timeLimitRange.end);
+ }).toList();
+ }
+
if (onData != null) return onData!(context, records, intakes, notes);
final entries = FullEntryList.merged(records, notes, intakes);
app/lib/data_util/interval_picker.dart → app/lib/features/data_picker/interval_picker.dart
@@ -1,7 +1,9 @@
+import 'package:blood_pressure_app/features/data_picker/filter_button.dart';
import 'package:blood_pressure_app/model/datarange_extension.dart';
import 'package:blood_pressure_app/model/storage/interval_store.dart';
import 'package:flutter/material.dart';
import 'package:blood_pressure_app/l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:week_of_year/date_week_extensions.dart';
@@ -28,15 +30,15 @@ class IntervalPicker extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer<IntervalStoreManager>(
builder: (context, intervallStoreManager, _) {
- final intervall = intervallStoreManager.get(type);
+ final interval = intervallStoreManager.get(type);
final loc = AppLocalizations.of(context)!;
- final start = intervall.currentRange.start;
- final end = intervall.currentRange.end;
- final String intervallDisplayText = switch (intervall.stepSize) {
+ final start = interval.currentRange.start;
+ final end = interval.currentRange.end;
+ final String intervallDisplayText = switch (interval.stepSize) {
TimeStep.day => DateFormat.yMMMd().format(start),
TimeStep.week => loc.weekOfYear(start.weekOfYear, start.year),
- TimeStep.month => DateFormat.yMMM().format(intervall.currentRange.start),
- TimeStep.year => DateFormat.y().format(intervall.currentRange.start),
+ TimeStep.month => DateFormat.yMMM().format(interval.currentRange.start),
+ TimeStep.year => DateFormat.y().format(interval.currentRange.start),
TimeStep.lifetime => '-',
TimeStep.last7Days || TimeStep.last30Days || TimeStep.custom =>
'${DateFormat.yMMMd().format(start)} - ${DateFormat.yMMMd().format(end)}',
@@ -48,12 +50,12 @@ class IntervalPicker extends StatelessWidget {
Row(
children: [
MaterialButton(
- onPressed: () => intervall.moveDataRangeByStep(-1),
+ onPressed: () => interval.moveDataRangeByStep(-1),
child: const Icon(Icons.chevron_left, size: 48),
),
Expanded(
child: DropdownButton<TimeStep>(
- value: intervall.stepSize,
+ value: interval.stepSize,
isExpanded: true,
onChanged: (TimeStep? value) async {
if (value == TimeStep.custom) {
@@ -64,14 +66,14 @@ class IntervalPicker extends StatelessWidget {
currentDate: customRangePickerCurrentDay,
);
if (res != null) {
- intervall.changeStepSize(value!);
+ interval.changeStepSize(value!);
final dateRange = res.dateRange.copyWith(
end: res.end.copyWith(hour: 23, minute: 59, second: 59),
);
- intervall.currentRange = dateRange;
+ interval.currentRange = dateRange;
}
} else if (value != null) {
- intervall.changeStepSize(value);
+ interval.changeStepSize(value);
}
},
items: [
@@ -80,8 +82,9 @@ class IntervalPicker extends StatelessWidget {
]
),
),
+ FilterButton(interval: interval),
MaterialButton(
- onPressed: () => intervall.moveDataRangeByStep(1),
+ onPressed: () => interval.moveDataRangeByStep(1),
child: const Icon(Icons.chevron_right, size: 48),
),
],
app/lib/features/measurement_list/weight_list.dart
@@ -23,6 +23,14 @@ class WeightList extends StatelessWidget {
return RepositoryBuilder<BodyweightRecord, BodyweightRepository>(
rangeType: rangeType,
onData: (context, records) {
+ final manager = context.watch<IntervalStoreManager>();
+ final timeLimitRange = manager.get(rangeType).timeLimitRange;
+ if (timeLimitRange != null) {
+ records = records.where((r) {
+ final time = TimeOfDay.fromDateTime(r.time);
+ return time.isAfter(timeLimitRange.start) && time.isBefore(timeLimitRange.end);
+ }).toList();
+ }
records.sort((a, b) => b.time.compareTo(a.time));
return ListView.builder(
itemCount: records.length,
app/lib/features/settings/export_import_screen.dart
@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:blood_pressure_app/components/disabled.dart';
-import 'package:blood_pressure_app/data_util/interval_picker.dart';
+import 'package:blood_pressure_app/features/data_picker/interval_picker.dart';
import 'package:blood_pressure_app/features/export_import/active_field_customization.dart';
import 'package:blood_pressure_app/features/export_import/export_button.dart';
import 'package:blood_pressure_app/features/export_import/export_warn_banner.dart';
app/lib/l10n/app_en.arb
@@ -572,5 +572,7 @@
"exportSuccess": "Export successful",
"@exportSuccess": {},
"xsl": "Excel (xsl)",
- "@xsl": {}
+ "@xsl": {},
+ "reset": "Reset",
+ "@reset": {}
}
app/lib/model/storage/interval_store.dart
@@ -1,9 +1,9 @@
import 'dart:convert';
+import 'package:blood_pressure_app/l10n/app_localizations.dart';
import 'package:blood_pressure_app/model/storage/convert_util.dart';
import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
import 'package:flutter/material.dart';
-import 'package:blood_pressure_app/l10n/app_localizations.dart';
import 'package:health_data_store/health_data_store.dart';
/// Class for storing the current interval, as it is needed in start page, statistics and export.
@@ -12,6 +12,7 @@ class IntervalStorage extends ChangeNotifier {
factory IntervalStorage.fromMap(Map<String, dynamic> map) => IntervalStorage(
stepSize: TimeStep.deserialize(map['stepSize']),
range: ConvertUtil.parseRange(map['start'], map['end']),
+ timeRange: TimeRange.fromJson(map['timeRange'])
);
/// Create a instance from a [String] created by [toJson].
@@ -24,20 +25,24 @@ class IntervalStorage extends ChangeNotifier {
}
/// Create a storage to interact with a display intervall.
- IntervalStorage({TimeStep? stepSize, DateRange? range}) :
+ IntervalStorage({TimeStep? stepSize, DateRange? range, TimeRange? timeRange}) :
_stepSize = stepSize ?? TimeStep.last7Days {
_currentRange = range ?? _getMostRecentDisplayIntervall();
+ _timeRange = timeRange;
}
TimeStep _stepSize;
late DateRange _currentRange;
+ TimeRange? _timeRange;
+
/// Serialize the object to a restoreable map.
Map<String, dynamic> toMap() => <String, dynamic>{
'stepSize': stepSize.serialize(),
'start': currentRange.start.millisecondsSinceEpoch,
'end': currentRange.end.millisecondsSinceEpoch,
+ 'timeRange': _timeRange?.toJson(),
};
/// Serialize the object to a restoreable string.
@@ -46,6 +51,17 @@ class IntervalStorage extends ChangeNotifier {
/// The stepSize gets set through the changeStepSize method.
TimeStep get stepSize => _stepSize;
+ // TODO: programmatically ensure this is respected:
+ /// The [TimeRange] used to limit data selection if non-null.
+ ///
+ /// Data points must fall on or between the start and end times to be selected.
+ TimeRange? get timeLimitRange => _timeRange;
+
+ set timeLimitRange(TimeRange? value) {
+ _timeRange = value;
+ notifyListeners();
+ }
+
/// sets the stepSize to the new value and resets the currentRange to the most recent one.
void changeStepSize(TimeStep value) {
_stepSize = value;
@@ -150,7 +166,7 @@ enum TimeStep {
custom;
/// Recreate a TimeStep from a number created with [TimeStep.serialize].
- factory TimeStep.deserialize(value) {
+ factory TimeStep.deserialize(Object? value) {
final int? intValue = ConvertUtil.parseInt(value);
assert(intValue == null || intValue >= 0 && intValue <= 7);
return switch (intValue) {
@@ -216,7 +232,7 @@ class IntervalStoreManager extends ChangeNotifier {
notifyListeners();
}
- // Copy all values from another instance.
+ /// Copy all values from another instance.
void copyFrom(IntervalStoreManager other) {
mainPage = other.mainPage;
exportPage = other.exportPage;
@@ -244,7 +260,70 @@ class IntervalStoreManager extends ChangeNotifier {
/// Locations supported by [IntervalStoreManager].
enum IntervalStoreManagerLocation {
+ /// List on home screen.
mainPage,
+ /// All exported data.
exportPage,
+ /// Data for all statistics.
statsPage,
-}
\ No newline at end of file
+}
+
+/// Represents an inclusive time span, defined by a [start] and an [end]
+/// [TimeOfDay].
+///
+/// **Serialization:**
+/// The class serializes the [TimeOfDay] objects into simple string representations
+/// of their hour and minute values (e.g., '14:30' for 2:30 PM).
+class TimeRange {
+ /// Creates a new [TimeRange] with a specified [start] and [end] time.
+ const TimeRange({
+ required this.start,
+ required this.end,
+ });
+
+ /// The starting time of the range (inclusive).
+ final TimeOfDay start;
+
+ /// The ending time of the range (inclusive).
+ final TimeOfDay end;
+
+ /// Serialization to JSON-compatible map
+ Map<String, dynamic> toJson() => {
+ 'start': _timeOfDayToString(start),
+ 'end': _timeOfDayToString(end),
+ };
+
+ /// Creates a [TimeRange] instance from a JSON map.
+ ///
+ /// Returns `null` if the input map is null or if the required keys ('start', 'end')
+ /// are missing or contain invalid time strings.
+ static TimeRange? fromJson(Map<String, dynamic>? json) {
+ if (json == null || json['start'] is! String || json['end'] is! String) {
+ return null;
+ }
+
+ try {
+ final start = _timeOfDayFromString(json['start'] as String);
+ final end = _timeOfDayFromString(json['end'] as String);
+ return TimeRange(start: start, end: end);
+ } catch (_) {
+ // Return null on parsing errors (e.g., non-numeric parts)
+ return null;
+ }
+ }
+
+ /// Converts a TimeOfDay to 'HH:MM' string.
+ static String _timeOfDayToString(TimeOfDay time) {
+ final hour = time.hour.toString().padLeft(2, '0');
+ final minute = time.minute.toString().padLeft(2, '0');
+ return '$hour:$minute';
+ }
+
+ /// Converts an 'HH:MM' string back to a TimeOfDay.
+ static TimeOfDay _timeOfDayFromString(String timeString) {
+ final parts = timeString.split(':');
+ final hour = int.parse(parts[0]);
+ final minute = int.parse(parts[1]);
+ return TimeOfDay(hour: hour, minute: minute);
+ }
+}
app/lib/screens/home_screen.dart
@@ -1,7 +1,7 @@
import 'package:blood_pressure_app/config.dart';
import 'package:blood_pressure_app/data_util/entry_context.dart';
import 'package:blood_pressure_app/data_util/full_entry_builder.dart';
-import 'package:blood_pressure_app/data_util/interval_picker.dart';
+import 'package:blood_pressure_app/features/data_picker/interval_picker.dart';
import 'package:blood_pressure_app/features/home/navigation_action_buttons.dart';
import 'package:blood_pressure_app/features/measurement_list/compact_measurement_list.dart';
import 'package:blood_pressure_app/features/measurement_list/measurement_list.dart';
app/lib/screens/statistics_screen.dart
@@ -1,12 +1,13 @@
-import 'package:blood_pressure_app/data_util/interval_picker.dart';
import 'package:blood_pressure_app/data_util/repository_builder.dart';
+import 'package:blood_pressure_app/features/data_picker/interval_picker.dart';
import 'package:blood_pressure_app/features/statistics/blood_pressure_distribution.dart';
import 'package:blood_pressure_app/features/statistics/clock_bp_graph.dart';
+import 'package:blood_pressure_app/l10n/app_localizations.dart';
import 'package:blood_pressure_app/model/blood_pressure_analyzer.dart';
import 'package:blood_pressure_app/model/storage/interval_store.dart';
import 'package:flutter/material.dart';
-import 'package:blood_pressure_app/l10n/app_localizations.dart';
import 'package:health_data_store/health_data_store.dart';
+import 'package:provider/provider.dart';
/// A page that shows statistics about stored blood pressure values.
class StatisticsScreen extends StatefulWidget {
@@ -27,15 +28,24 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
),
body: RepositoryBuilder<BloodPressureRecord, BloodPressureRepository>(
rangeType: IntervalStoreManagerLocation.statsPage,
- onData: (context, data) {
- final analyzer = BloodPressureAnalyser(data.toList());
+ onData: (context, records) {
+ final manager = context.watch<IntervalStoreManager>();
+ final timeLimitRange = manager.get(IntervalStoreManagerLocation.statsPage)
+ .timeLimitRange;
+ if (timeLimitRange != null) {
+ records = records.where((r) {
+ final time = TimeOfDay.fromDateTime(r.time);
+ return time.isAfter(timeLimitRange.start) && time.isBefore(timeLimitRange.end);
+ }).toList();
+ }
+ final analyzer = BloodPressureAnalyser(records.toList());
return ListView(
children: [
_buildSubTitle(localizations.statistics,),
ListTile(
title: Text(localizations.measurementCount),
trailing: Text(
- data.length.toString(),
+ records.length.toString(),
style: Theme.of(context).textTheme.headlineSmall,
),
),
@@ -51,11 +61,11 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
height: 260,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: BloodPressureDistribution(
- records: data,
+ records: records,
),
),
_buildSubTitle(localizations.timeResolvedMetrics),
- ClockBpGraph(measurements: data),
+ ClockBpGraph(measurements: records),
],
);
},
app/test/data_util/interval_picker_test.dart
@@ -1,4 +1,4 @@
-import 'package:blood_pressure_app/data_util/interval_picker.dart';
+import 'package:blood_pressure_app/features/data_picker/interval_picker.dart';
import 'package:blood_pressure_app/model/storage/interval_store.dart';
import 'package:flutter/material.dart';
import 'package:blood_pressure_app/l10n/app_localizations.dart';
app/test/screens/home_screen_test.dart
@@ -1,4 +1,4 @@
-import 'package:blood_pressure_app/data_util/interval_picker.dart';
+import 'package:blood_pressure_app/features/data_picker/interval_picker.dart';
import 'package:blood_pressure_app/features/home/navigation_action_buttons.dart';
import 'package:blood_pressure_app/features/measurement_list/compact_measurement_list.dart';
import 'package:blood_pressure_app/features/measurement_list/measurement_list.dart';