Commit 1e8c2cb
Changed files (13)
app
lib
components
screens
elements
subsettings
export_import
test
model
ui
health_data_store
lib
src
types
app/lib/components/dialoges/add_export_column_dialoge.dart
@@ -181,7 +181,7 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge>
children: [
if (type == _FormatterType.record)
MeasurementListRow(
- record: record,
+ data: (record, Note(time: record.time), []),
settings: widget.settings,
) else Text(
DateFormat('MMM d, y - h:m.s')
app/lib/components/dialoges/add_measurement_dialoge.dart
@@ -4,11 +4,12 @@ import 'dart:math';
import 'package:blood_pressure_app/components/consistent_future_builder.dart';
import 'package:blood_pressure_app/components/date_time_picker.dart';
import 'package:blood_pressure_app/components/dialoges/fullscreen_dialoge.dart';
-import 'package:blood_pressure_app/model/blood_pressure/needle_pin.dart';
+import 'package:blood_pressure_app/components/settings/color_picker_list_tile.dart';
import 'package:blood_pressure_app/model/blood_pressure/pressure_unit.dart';
import 'package:blood_pressure_app/model/storage/storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:health_data_store/health_data_store.dart';
import 'package:intl/intl.dart';
@@ -35,7 +36,7 @@ class AddEntryDialoge extends StatefulWidget {
///
/// When an initial record is set medicine input is not possible because it is
/// saved separately.
- final BloodPressureRecord? initialRecord;
+ final FullEntry? initialRecord;
/// Repository that contains all selectable medicines.
final MedicineRepository medRepo;
@@ -57,8 +58,8 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
/// Currently selected time.
late DateTime time;
- /// Current selected needlePin.
- MeasurementNeedlePin? needlePin;
+ /// Current selected note color.
+ Color? color;
/// Last [FormState.save]d systolic value.
int? systolic;
@@ -98,7 +99,8 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
void initState() {
super.initState();
time = widget.initialRecord?.time ?? DateTime.now();
- // needlePin = widget.initialRecord?.needlePin; TODO
+ final int? colorVal = widget.initialRecord?.color;
+ color = colorVal == null ? null : Color(colorVal);
sysController = TextEditingController(
text: (widget.initialRecord?.sys ?? '').toString(),
);
@@ -227,10 +229,12 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
return FullscreenDialoge(
onActionButtonPressed: () {
BloodPressureRecord? record;
+ Note? note;
+ final List<MedicineIntake> intakes = [];
if (_measurementFormActive && (recordFormKey.currentState?.validate() ?? false)) {
recordFormKey.currentState?.save();
if (systolic != null || diastolic != null || pulse != null
- || (notes ?? '').isNotEmpty || needlePin != null) {
+ || (notes ?? '').isNotEmpty || color != null) {
final pressureUnit = context.read<Settings>().preferredPressureUnit;
// TODO: notes, needle pin
record = BloodPressureRecord(
@@ -239,31 +243,33 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
dia: diastolic == null ? null : pressureUnit.wrap(diastolic!),
pul: pulse,
);
+ note = Note(
+ time: time,
+ note: notes,
+ color: color?.value,
+ );
}
}
- MedicineIntake? intake;
if (_showMedicineDosisInput
&& (medicationFormKey.currentState?.validate() ?? false)) {
medicationFormKey.currentState?.save();
if (medicineDosis != null
&& selectedMed != null) {
- intake = MedicineIntake(
+ intakes.add(MedicineIntake(
time: time,
medicine: selectedMed!,
dosis: Weight.mg(medicineDosis!),
- );
+ ));
}
}
- if (record != null && intake != null) {
- Navigator.pop(context, (record, intake));
- }
- if (record == null && !_measurementFormActive && intake != null) {
- Navigator.pop(context, (record, intake));
- }
- if (record != null && intake == null && selectedMed == null) {
- Navigator.pop(context, (record, intake));
+ if ((record != null && intakes.isNotEmpty)
+ || (record == null && !_measurementFormActive && intakes.isNotEmpty)
+ || (record != null && intakes.isEmpty && selectedMed == null)) {
+ record ??= BloodPressureRecord(time: time);
+ note ??= Note(time: time);
+ Navigator.pop(context, (record, note, intakes));
}
},
actionButtonText: localizations.btnSave,
@@ -320,10 +326,10 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
),
],
),
- /*Padding( FIXME
+ Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: TextFormField(
- initialValue: widget.initialRecord?.notes,
+ initialValue: widget.initialRecord?.note,
focusNode: noteFocusNode,
decoration: InputDecoration(
labelText: localizations.addNote,
@@ -348,13 +354,12 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
onMainColorChanged: (Color value) {
setState(() {
_measurementFormActive = true;
- needlePin = (value == Colors.transparent) ? null
- : MeasurementNeedlePin(value);
+ color = (value == Colors.transparent) ? null : value;
});
},
- initialColor: needlePin?.color ?? Colors.transparent,
- shape: _buildShapeBorder(needlePin?.color),
- ),*/
+ initialColor: color ?? Colors.transparent,
+ shape: _buildShapeBorder(color),
+ ),
],
),
),
@@ -425,7 +430,6 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
},
),
),
-
],
),
),
@@ -439,12 +443,12 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
}
/// Shows a dialoge to input a blood pressure measurement or a medication.
-Future<(BloodPressureRecord?, MedicineIntake?)?> showAddEntryDialoge(
+Future<FullEntry?> showAddEntryDialoge(
BuildContext context,
Settings settings,
MedicineRepository medRepo,
- [BloodPressureRecord? initialRecord,]) =>
- showDialog<(BloodPressureRecord?, MedicineIntake?)>(
+ [FullEntry? initialRecord,]) =>
+ showDialog<FullEntry>(
context: context, builder: (context) =>
Dialog.fullscreen(
child: AddEntryDialoge(
@@ -454,3 +458,38 @@ Future<(BloodPressureRecord?, MedicineIntake?)?> showAddEntryDialoge(
),
),
);
+
+/// Allow correctly saving entries in the contexts repositories.
+extension AddEntries on BuildContext {
+ /// Open the [AddEntryDialoge] and save received entries.
+ ///
+ /// Follows [ExportSettings.exportAfterEveryEntry]. When [initial] is not null
+ /// the dialoge will be opened in edit mode.
+ Future<void> createEntry([FullEntry? initial]) async {
+ final recordRepo = RepositoryProvider.of<BloodPressureRepository>(this);
+ final noteRepo = RepositoryProvider.of<NoteRepository>(this);
+ final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(this);
+ final settings = Provider.of<Settings>(this, listen: false);
+ final exportSettings = Provider.of<ExportSettings>(this, listen: false);
+
+ final entry = await showAddEntryDialoge(this,
+ settings,
+ RepositoryProvider.of<MedicineRepository>(this),
+ initial,
+ );
+ if (entry != null) {
+ if (entry.sys != null || entry.dia != null || entry.pul != null) {
+ await recordRepo.add(entry.$1);
+ }
+ if (entry.note != null || entry.color != null) {
+ await noteRepo.add(entry.$2);
+ }
+ for (final intake in entry.$3) {
+ await intakeRepo.add(intake);
+ }
+ if (mounted && exportSettings.exportAfterEveryEntry) {
+ // FIXME: export if setting is set
+ }
+ }
+ }
+}
app/lib/components/measurement_list/measurement_list.dart
@@ -1,8 +1,7 @@
-import 'package:blood_pressure_app/components/measurement_list/intake_list_entry.dart';
import 'package:blood_pressure_app/components/measurement_list/measurement_list_entry.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:health_data_store/health_data_store.dart';
@@ -14,6 +13,7 @@ class MeasurementList extends StatelessWidget {
const MeasurementList({super.key,
required this.settings,
required this.records,
+ required this.notes,
required this.intakes,
});
@@ -23,25 +23,32 @@ class MeasurementList extends StatelessWidget {
/// Records to display.
final List<BloodPressureRecord> records;
- /// Medicine intakes to list.
- ///
- /// Will get merged with blood pressure records.
+ /// Complementary notes info to show.
+ final List<Note> notes;
+
+ /// Medicine intake info to show.
final List<MedicineIntake> intakes;
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
- final entries = [];
- entries.addAll(records);
- entries.addAll(intakes);
- entries.sort((e1, e2) {
- if (e2 is BloodPressureRecord && e1 is BloodPressureRecord) return e2.time.compareTo(e1.time);
- if (e2 is BloodPressureRecord && e1 is MedicineIntake) return e2.time.compareTo(e1.time);
- if (e2 is MedicineIntake && e1 is BloodPressureRecord) return e2.time.compareTo(e1.time);
- if (e2 is MedicineIntake && e1 is MedicineIntake) return e2.time.compareTo(e1.time);
- assert(false);
- return 0;
- });
+ final List<FullEntry> entries = [];
+ for (final r in records) {
+ final n = notes.where((n) => n.time == r.time).firstOrNull ?? Note(time: r.time);
+ final i = intakes.where((n) => n.time == r.time).toList();
+ entries.add((r, n, i));
+ }
+ Set<DateTime> times = entries.map((e) => e.time).toSet();
+ final remainingNotes = notes.where((n) => !times.contains(n.time));
+ for (final n in remainingNotes) {
+ final i = intakes.where((n) => n.time == n.time).toList();
+ entries.add((BloodPressureRecord(time: n.time), n, i));
+ }
+ times = entries.map((e) => e.time).toSet();
+ final remainingIntakes = intakes.where((i) => !times.contains(i.time));
+ for (final i in groupBy(remainingIntakes, (i) => i.time).values) {
+ entries.add((BloodPressureRecord(time: i.first.time), Note(time: i.first.time), i));
+ }
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -53,23 +60,23 @@ class MeasurementList extends StatelessWidget {
flex: 4,
child: SizedBox(),),
Expanded(
- flex: 30,
- child: Text(localizations.sysLong,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(fontWeight: FontWeight.bold, color: settings.sysColor),),),
+ flex: 30,
+ child: Text(localizations.sysLong,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontWeight: FontWeight.bold, color: settings.sysColor),),),
Expanded(
- flex: 30,
- child: Text(localizations.diaLong,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(fontWeight: FontWeight.bold, color: settings.diaColor),),),
+ flex: 30,
+ child: Text(localizations.diaLong,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontWeight: FontWeight.bold, color: settings.diaColor),),),
Expanded(
- flex: 30,
- child: Text(localizations.pulLong,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(fontWeight: FontWeight.bold, color: settings.pulColor),),),
+ flex: 30,
+ child: Text(localizations.pulLong,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontWeight: FontWeight.bold, color: settings.pulColor),),),
const Expanded(
- flex: 20,
- child: SizedBox(),),
+ flex: 20,
+ child: SizedBox(),),
],
),
const SizedBox(
@@ -88,25 +95,10 @@ class MeasurementList extends StatelessWidget {
// and font sizes by adding empty offset to bottom.
padding: const EdgeInsets.only(bottom: 300),
itemCount: entries.length,
- itemBuilder: (context, idx) {
- if (entries[idx] is BloodPressureRecord) {
- return MeasurementListRow(
- record: entries[idx],
- settings: settings,
- );
- } else {
- assert(entries[idx] is MedicineIntake);
- return IntakeListEntry(
- intake: entries[idx],
- settings: settings,
- delete: () {
- final repo = RepositoryProvider.of<MedicineIntakeRepository>(context);
- repo.remove(entries[idx]);
- },
- );
- }
-
- },
+ itemBuilder: (context, idx) => MeasurementListRow(
+ data: entries[idx],
+ settings: settings,
+ ),
),
),
],
app/lib/components/measurement_list/measurement_list_entry.dart
@@ -6,18 +6,17 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:health_data_store/health_data_store.dart';
import 'package:intl/intl.dart';
-import 'package:provider/provider.dart';
/// Display of a blood pressure measurement data.
class MeasurementListRow extends StatelessWidget {
/// Create a display of a measurements.
const MeasurementListRow({super.key,
- required this.record,
+ required this.data,
required this.settings,
});
/// The measurement to display.
- final BloodPressureRecord record;
+ final FullEntry data;
/// Settings that determine general behavior.
final Settings settings;
@@ -30,33 +29,19 @@ class MeasurementListRow extends StatelessWidget {
// Leading color possible
title: _buildRow(formatter),
childrenPadding: const EdgeInsets.only(bottom: 10),
- // backgroundColor: record.needlePin?.color.withAlpha(30), FIXME
- // collapsedShape: record.needlePin != null ? Border(left: BorderSide(color: record.needlePin!.color, width: 8)) : null,
+ backgroundColor: data.color == null ? null : Color(data.color!).withAlpha(30),
+ collapsedShape: data.color == null ? null : Border(
+ left: BorderSide(color: Color(data.color!), width: 8)
+ ),
children: [
ListTile(
- subtitle: Text(formatter.format(record.time)),
+ subtitle: Text(formatter.format(data.time)),
title: Text(localizations.timestamp),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
- onPressed: () async {
- final model = RepositoryProvider.of<BloodPressureRepository>(context);
- final entry = await showAddEntryDialoge(context,
- Provider.of<Settings>(context, listen: false),
- RepositoryProvider.of<MedicineRepository>(context),
- record,
- );
- if (entry?.$1 != null) {
- if (context.mounted) {
- // model.addAndExport(context, entry!.$1!); FIXME
- await model.add(entry!.$1!);
- } else {
- await model.add(entry!.$1!);
- }
- }
- assert(entry?.$2 == null);
- },
+ onPressed: () => context.createEntry(data),
icon: const Icon(Icons.edit),
tooltip: localizations.edit,
),
@@ -68,16 +53,35 @@ class MeasurementListRow extends StatelessWidget {
],
),
),
- /*if (record.notes.isNotEmpty) FIXME
+ if (data.note?.isNotEmpty ?? false)
ListTile(
title: Text(localizations.note),
- subtitle: Text(record.notes),
- ),*/
+ subtitle: Text(data.note!),
+ ),
+ for (final MedicineIntake intake in data.$3)
+ ListTile(
+ title: Text(intake.medicine.designation),
+ subtitle: Text('${intake.dosis.mg}mg'), // TODO: setting for unit
+ iconColor: intake.medicine.color == null ? null : Color(intake.medicine.color!),
+ leading: Icon(Icons.medication),
+ trailing: IconButton(
+ onPressed: () async {
+ final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
+ if (!settings.confirmDeletion || await showConfirmDeletionDialoge(context)) {
+ await intakeRepo.remove(intake);
+ }
+ // TODO: undo
+ },
+ color: Theme.of(context).listTileTheme.iconColor,
+ icon: const Icon(Icons.delete),
+ ),
+ ), // TODO: test
+ // FIXME: remove other medicine tiles
],
);
}
- Row _buildRow(DateFormat formatter) {
+ Row _buildRow(DateFormat formatter) { // TODO: is intake present
String formatNum(num? num) => (num ?? '-').toString();
String formatPressure(Pressure? num) => switch(settings.preferredPressureUnit) {
PressureUnit.mmHg => formatNum(num?.mmHg),
@@ -86,23 +90,29 @@ class MeasurementListRow extends StatelessWidget {
return Row(
children: [
Expanded(
- flex: 3,
- child: Text(formatPressure(record.sys)),
+ flex: 30,
+ child: Text(formatPressure(data.sys)),
),
Expanded(
- flex: 3,
- child: Text(formatPressure(record.dia)),
+ flex: 30,
+ child: Text(formatPressure(data.dia)),
),
Expanded(
- flex: 3,
- child: Text(formatNum(record.pul)),
+ flex: 30,
+ child: Text(formatNum(data.pul)),
),
+ if (data.$3.isNotEmpty)
+ Expanded(
+ flex: 10,
+ child: Icon(Icons.medication),
+ ),
],
);
}
void _deleteEntry(Settings settings, BuildContext context, AppLocalizations localizations) async {
- final model = RepositoryProvider.of<BloodPressureRepository>(context);
+ final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
+ final noteRepo = RepositoryProvider.of<NoteRepository>(context);
final messanger = ScaffoldMessenger.of(context);
bool confirmedDeletion = true;
if (settings.confirmDeletion) {
@@ -110,14 +120,17 @@ class MeasurementListRow extends StatelessWidget {
}
if (confirmedDeletion) { // TODO: move out of model
- await model.remove(record);
+ await bpRepo.remove(data.$1);
+ await noteRepo.remove(data.$2);
messanger.removeCurrentSnackBar();
messanger.showSnackBar(SnackBar(
- duration: const Duration(seconds: 5),
content: Text(localizations.deletionConfirmed),
action: SnackBarAction(
label: localizations.btnUndo,
- onPressed: () => model.add(record),
+ onPressed: () async {
+ await bpRepo.add(data.$1);
+ await noteRepo.add(data.$2);
+ },
),
),);
}
@@ -125,7 +138,9 @@ class MeasurementListRow extends StatelessWidget {
}
/// Show a dialoge that prompts the user to confirm a deletion.
-Future<bool> showConfirmDeletionDialoge(BuildContext context) async =>
+///
+/// Returns whether it is ok to proceed with deletion.
+Future<bool> showConfirmDeletionDialoge(BuildContext context) async => // TODO: move to own file
await showDialog<bool>(context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.confirmDelete),
@@ -135,9 +150,16 @@ Future<bool> showConfirmDeletionDialoge(BuildContext context) async =>
onPressed: () => Navigator.pop(context, false),
child: Text(AppLocalizations.of(context)!.btnCancel),
),
- ElevatedButton(
- onPressed: () => Navigator.pop(context, true),
- child: Text(AppLocalizations.of(context)!.btnConfirm),
+ Theme(
+ data: ThemeData.from(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.red, brightness: Theme.of(context).brightness),
+ useMaterial3: true,
+ ),
+ child: ElevatedButton.icon(
+ onPressed: () => Navigator.pop(context, true),
+ icon: const Icon(Icons.delete_forever),
+ label: Text(AppLocalizations.of(context)!.btnConfirm),
+ ),
),
],
),
app/lib/screens/elements/legacy_measurement_list.dart
@@ -1,7 +1,7 @@
-import 'dart:async';
import 'dart:collection';
import 'package:blood_pressure_app/components/dialoges/add_measurement_dialoge.dart';
+import 'package:blood_pressure_app/components/measurement_list/measurement_list_entry.dart';
import 'package:blood_pressure_app/model/storage/intervall_store.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:blood_pressure_app/screens/elements/blood_pressure_builder.dart';
@@ -67,147 +67,99 @@ class LegacyMeasurementsList extends StatelessWidget {
thickness: 2,
color: Theme.of(context).colorScheme.primaryContainer,
),
- Expanded(
+ Expanded(// TODO: intakes
child: BloodPressureBuilder(
rangeType: IntervallStoreManagerLocation.mainPage,
- onData: (BuildContext context, UnmodifiableListView<BloodPressureRecord> data) {
- if (data.isNotEmpty) {
- return ListView.builder(
- itemCount: data.length,
- shrinkWrap: true,
- padding: const EdgeInsets.all(2),
- itemBuilder: (context, index) {
- final formatter = DateFormat(settings.dateFormatString);
- return Column(
- children: [
- Dismissible(
- key: Key(data[index].time.toIso8601String()),
- confirmDismiss: (direction) async {
- final repo = RepositoryProvider.of<BloodPressureRepository>(context);
- if (direction == DismissDirection.startToEnd) { // edit
- final entry = await showAddEntryDialoge(context,
- Provider.of<Settings>(context, listen: false),
- RepositoryProvider.of<MedicineRepository>(context),
- data[index],
- );
- if (entry?.$1 != null) {
- if (context.mounted) {
- // FIXME
- // repo.addAndExport(context, entry!.$1!);
- throw UnimplementedError('addAndExport not supported');
- } else {
- await repo.add(entry!.$1!);
- }
- }
- assert(entry?.$2 == null);
- return false;
- } else { // delete
- bool dialogeDeletionConfirmed = false;
- if (settings.confirmDeletion) {
- await showDialog(
- context: context,
- builder: (context) => AlertDialog(
- title: Text(AppLocalizations.of(context)!.confirmDelete),
- content: Text(AppLocalizations.of(context)!.confirmDeleteDesc),
- actions: [
- ElevatedButton(
- onPressed: () => Navigator.pop(context, ),
- child: Text(AppLocalizations.of(context)!.btnCancel),),
- ElevatedButton(
- onPressed: () {
- unawaited(repo.remove(data[index]));
-
- dialogeDeletionConfirmed = true;
- Navigator.pop(context, );
- },
- child: Text(AppLocalizations.of(context)!.btnConfirm),),
- ],
- ),);
- } else {
- unawaited(repo.remove(data[index]));
- dialogeDeletionConfirmed = true;
- }
-
- if (dialogeDeletionConfirmed) {
- if (!context.mounted) return true;
- ScaffoldMessenger.of(context).removeCurrentSnackBar();
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- duration: const Duration(seconds: 5),
- content: Text(AppLocalizations.of(context)!.deletionConfirmed),
- action: SnackBarAction(
- label: AppLocalizations.of(context)!.btnUndo,
- onPressed: () async {
- // FIXME
- /*
- model.addAndExport(context, BloodPressureRecord(
- data[index].creationTime,
- data[index].systolic,
- data[index].diastolic,
- data[index].pulse,
- data[index].notes,),);
- */
- throw UnimplementedError('addAndExport not supported');
- },
- ),
- ),);
- }
- return dialogeDeletionConfirmed;
- }
- },
- onDismissed: (direction) {},
- background: Container(
- width: 10,
- decoration:
- BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(5)),
- child: const Align(alignment: Alignment(-0.95, 0), child: Icon(Icons.edit)),
- ),
- secondaryBackground: Container(
- width: 10,
- decoration:
- BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(5)),
- child: const Align(alignment: Alignment(0.95, 0), child: Icon(Icons.delete)),
- ),
- child: Container(
- constraints: const BoxConstraints(minHeight: 40),
- child: Row(children: [
- Expanded(
- flex: _sideFlex,
- child: const SizedBox(),
- ),
- Expanded(
- flex: _tableElementsSizes[0],
- child: Text(formatter.format(data[index].time)),),
- Expanded(
- flex: _tableElementsSizes[1],
- child: Text((data[index].sys ?? '').toString()),),
- Expanded(
- flex: _tableElementsSizes[2],
- child: Text((data[index].dia ?? '').toString()),),
- Expanded(
- flex: _tableElementsSizes[3],
- child: Text((data[index].pul ?? '').toString()),),
- // TODO: reimplement notes
- /*Expanded(
- flex: _tableElementsSizes[4],
- child: Text(data[index].),),*/
- Expanded(
- flex: _sideFlex,
- child: const SizedBox(),
+ onData: (BuildContext context, UnmodifiableListView<BloodPressureRecord> records) {
+ if (records.isEmpty) return Text(AppLocalizations.of(context)!.errNoData);
+ return ListView.builder(
+ itemCount: records.length,
+ shrinkWrap: true,
+ padding: const EdgeInsets.all(2),
+ itemBuilder: (context, index) {
+ final formatter = DateFormat(settings.dateFormatString);
+ return Column(
+ children: [
+ Dismissible(
+ key: Key(records[index].time.toIso8601String()),
+ confirmDismiss: (direction) async {
+ final repo = RepositoryProvider.of<BloodPressureRepository>(context);
+ if (direction == DismissDirection.startToEnd) {
+ // edit
+ await context.createEntry((records[index], Note(time: records[index].time), []));
+ return false;
+ } else { // delete
+ if (!settings.confirmDeletion || await showConfirmDeletionDialoge(context)) {
+ await repo.remove(records[index]);
+ if (!context.mounted) return true;
+ ScaffoldMessenger.of(context).removeCurrentSnackBar();
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content: Text(AppLocalizations.of(context)!.deletionConfirmed),
+ action: SnackBarAction(
+ label: AppLocalizations.of(context)!.btnUndo,
+ onPressed: () async {
+ await repo.add(records[index]);
+ },
),
- ],),
+ ),);
+ return true;
+ }
+ return false;
+ }
+ },
+ onDismissed: (direction) {},
+ background: Container(
+ width: 10,
+ decoration:
+ BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(5)),
+ child: const Align(alignment: Alignment(-0.95, 0), child: Icon(Icons.edit)),
+ ),
+ secondaryBackground: Container(
+ width: 10,
+ decoration:
+ BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(5)),
+ child: const Align(alignment: Alignment(0.95, 0), child: Icon(Icons.delete)),
+ ),
+ child: Container(
+ constraints: const BoxConstraints(minHeight: 40),
+ child: Row(children: [
+ Expanded(
+ flex: _sideFlex,
+ child: const SizedBox(),
+ ),
+ Expanded(
+ flex: _tableElementsSizes[0],
+ child: Text(formatter.format(records[index].time)),),
+ Expanded(
+ flex: _tableElementsSizes[1],
+ // FIXME: "Instance of Pressure"
+ child: Text((records[index].sys ?? '').toString()),),
+ Expanded(
+ flex: _tableElementsSizes[2],
+ child: Text((records[index].dia ?? '').toString()),),
+ Expanded(
+ flex: _tableElementsSizes[3],
+ child: Text((records[index].pul ?? '').toString()),),
+ // FIXME: reimplement notes
+ /*Expanded(
+ flex: _tableElementsSizes[4],
+ child: Text(data[index].),),*/
+ Expanded(
+ flex: _sideFlex,
+ child: const SizedBox(),
),
- ),
- const Divider(
- thickness: 1,
- height: 1,
- ),
- ],
- );
- },);
- } else {
- return Text(AppLocalizations.of(context)!.errNoData);
- }
- },),
+ ],),
+ ),
+ ),
+ const Divider(
+ thickness: 1,
+ height: 1,
+ ),
+ ],
+ );
+ },);
+ },
+ ),
),
],
),);
app/lib/screens/home_screen.dart
@@ -1,5 +1,3 @@
-import 'dart:async';
-
import 'package:blood_pressure_app/components/dialoges/add_measurement_dialoge.dart';
import 'package:blood_pressure_app/components/measurement_list/measurement_list.dart';
import 'package:blood_pressure_app/components/repository_builder.dart';
@@ -13,9 +11,8 @@ import 'package:blood_pressure_app/screens/statistics_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:health_data_store/health_data_store.dart' show BloodPressureRepository, MedicineIntake, MedicineIntakeRepository, MedicineRepository;
+import 'package:health_data_store/health_data_store.dart';
import 'package:provider/provider.dart';
/// Is true during the first [AppHome.build] before creating the widget.
@@ -33,27 +30,7 @@ class AppHome extends StatelessWidget {
// direct use of settings possible as no listening is required
if (_appStart) {
if (Provider.of<Settings>(context, listen: false).startWithAddMeasurementPage) {
- SchedulerBinding.instance.addPostFrameCallback((_) async {
- final repo = RepositoryProvider.of<BloodPressureRepository>(context);
- final intakes = RepositoryProvider.of<MedicineIntakeRepository>(context);
- final measurement = await showAddEntryDialoge(context,
- Provider.of<Settings>(context, listen: false),
- RepositoryProvider.of<MedicineRepository>(context),
- );
- if (measurement == null) return;
- if (measurement.$1 != null) {
- if (context.mounted) {
- // TODO: reimplement
- // model.addAndExport(context, measurement.$1!);
- throw UnimplementedError('addAndExport not supported');
- } else {
- unawaited(repo.add(measurement.$1!));
- }
- }
- if (measurement.$2 != null) {
- unawaited(intakes.add(measurement.$2!));
- }
- });
+ SchedulerBinding.instance.addPostFrameCallback((_) => context.createEntry());
}
}
_appStart = false;
@@ -80,16 +57,20 @@ class AppHome extends StatelessWidget {
rangeType: IntervallStoreManagerLocation.mainPage,
onData: (context, records) => RepositoryBuilder<MedicineIntake, MedicineIntakeRepository>(
rangeType: IntervallStoreManagerLocation.mainPage,
- onData: (BuildContext context, List<dynamic> list) => MeasurementList(
- settings: settings,
- records: records,
- // The following cast is necessary to avoid a type
- // error. I'm not sure why this is necessary as the
- // generics _should_ be typesafe. The safety of this
- // cast has been proven in practice.
- // TODO: Figure out why type safety isn't possible.
- intakes: list.cast(),
- ) as Widget,
+ onData: (BuildContext context, List<dynamic> intakes) => RepositoryBuilder<Note, NoteRepository>(
+ rangeType: IntervallStoreManagerLocation.mainPage,
+ onData: (BuildContext context, List<dynamic> notes) => MeasurementList(
+ settings: settings,
+ records: records,
+ // The following cast is necessary to avoid a type
+ // error. I'm not sure why this is necessary as the
+ // generics _should_ be typesafe. The safety of this
+ // cast has been proven in practice.
+ // TODO: Figure out why type safety isn't possible.
+ notes: notes.cast(),
+ intakes: intakes.cast(),
+ ) as Widget,
+ ),
),
),
),
@@ -116,26 +97,7 @@ class AppHome extends StatelessWidget {
heroTag: 'floatingActionAdd',
tooltip: localizations.addMeasurement,
autofocus: true,
- onPressed: () async {
- final model = Provider.of<BloodPressureRepository>(context, listen: false);
- final intakes = RepositoryProvider.of<MedicineIntakeRepository>(context);
- final measurement = await showAddEntryDialoge(context,
- Provider.of<Settings>(context, listen: false),
- RepositoryProvider.of<MedicineRepository>(context),
- );
- if (measurement == null) return;
- if (measurement.$1 != null) {
- if (context.mounted) {
- await model.add(measurement.$1!);
- // model.addAndExport(context, measurement.$1!); FIXME
- } else {
- await model.add(measurement.$1!);
- }
- }
- if (measurement.$2 != null) {
- await intakes.add(measurement.$2!);
- }
- },
+ onPressed: context.createEntry,
child: const Icon(Icons.add,),
),
),
app/lib/screens/statistics_screen.dart
@@ -88,7 +88,7 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
return const RadarChartTitle(text: '');
},
dataSets: [
- RadarDataSet(
+ RadarDataSet( // FIXME
dataEntries: _intListToRadarEntry(data[0]),
borderColor: settings.diaColor,
fillColor: settings.diaColor.withOpacity(opacity),
app/lib/main.dart
@@ -106,6 +106,7 @@ Future<Widget> _loadApp() async {
child: MultiRepositoryProvider(
providers: [
RepositoryProvider(create: (context) => db.bpRepo),
+ RepositoryProvider(create: (context) => db.noteRepo),
RepositoryProvider(create: (context) => db.medRepo),
RepositoryProvider(create: (context) => db.intakeRepo),
],
app/test/model/analyzer_test.dart
@@ -67,7 +67,19 @@ BloodPressureRecord mockRecordPos([
int? sys,
int? dia,
int? pul,
-]) => BloodPressureRecord(
+]) => mockRecord(
+ time: time ?? DateTime.now(),
+ sys: sys,
+ dia: dia,
+ pul: pul,
+);
+
+BloodPressureRecord mockRecord({
+ DateTime? time,
+ int? sys,
+ int? dia,
+ int? pul,
+}) => BloodPressureRecord(
time: time ?? DateTime.now(),
sys: sys == null ? null : Pressure.mmHg(sys),
dia: dia == null ? null : Pressure.mmHg(dia),
app/test/ui/components/measurement_list_entry_test.dart
@@ -4,28 +4,28 @@ import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
-import '../../model/export_import/record_formatter_test.dart';
+import '../../model/analyzer_test.dart';
import 'util.dart';
void main() {
testWidgets('should initialize without errors', (tester) async {
await tester.pumpWidget(materialApp(MeasurementListRow(
settings: Settings(),
- record: mockEntryPos(DateTime(2023), 123, 80, 60, 'test'),),),);
+ record: mockRecordPos(DateTime(2023), 123, 80, 60, 'test'),),),);
expect(tester.takeException(), isNull);
await tester.pumpWidget(materialApp(MeasurementListRow(
settings: Settings(),
- record: mockEntryPos(DateTime.fromMillisecondsSinceEpoch(31279811), null, null, null, 'null test'),),),);
+ record: mockRecordPos(DateTime.fromMillisecondsSinceEpoch(31279811), null, null, null, 'null test'),),),);
expect(tester.takeException(), isNull);
await tester.pumpWidget(materialApp(MeasurementListRow(
settings: Settings(),
- record: mockEntryPos(DateTime(2023), 124, 85, 63, 'color',Colors.cyan))));
+ record: mockRecordPos(DateTime(2023), 124, 85, 63, 'color',Colors.cyan))));
expect(tester.takeException(), isNull);
});
testWidgets('should expand correctly', (tester) async {
await tester.pumpWidget(materialApp(MeasurementListRow(
settings: Settings(),
- record: mockEntryPos(DateTime(2023), 123, 78, 56, 'Test texts'),),),);
+ record: mockRecordPos(DateTime(2023), 123, 78, 56),),),);
expect(find.byIcon(Icons.expand_more), findsOneWidget);
await tester.tap(find.byIcon(Icons.expand_more));
await tester.pumpAndSettle();
@@ -36,7 +36,7 @@ void main() {
testWidgets('should display correct information', (tester) async {
await tester.pumpWidget(materialApp(MeasurementListRow(
settings: Settings(),
- record: mockEntryPos(DateTime(2023), 123, 78, 56, 'Test text'),),),);
+ record: mockRecordPos(DateTime(2023), 123, 78, 56),),),);
expect(find.text('123'), findsOneWidget);
expect(find.text('78'), findsOneWidget);
expect(find.text('56'), findsOneWidget);
@@ -52,7 +52,7 @@ void main() {
});
testWidgets('should not display null values', (tester) async {
await tester.pumpWidget(materialApp(MeasurementListRow(
- settings: Settings(), record: mockEntry(time: DateTime(2023)),),),);
+ settings: Settings(), record: mockRecord(time: DateTime(2023)),),),);
expect(find.text('null'), findsNothing);
expect(find.byIcon(Icons.expand_more), findsOneWidget);
await tester.tap(find.byIcon(Icons.expand_more));
@@ -61,7 +61,7 @@ void main() {
});
testWidgets('should open edit dialoge', (tester) async {
await tester.pumpWidget(await appBase(MeasurementListRow(
- settings: Settings(), record: mockEntry(time: DateTime(2023),
+ settings: Settings(), record: mockRecord(time: DateTime(2023),
sys:1, dia: 2, pul: 3, note: 'testTxt',),),),);
expect(find.byIcon(Icons.expand_more), findsOneWidget);
await tester.tap(find.byIcon(Icons.expand_more));
app/test/ui/statistics_test.dart
@@ -9,7 +9,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:health_data_store/health_data_store.dart';
-import '../model/export_import/record_formatter_test.dart';
+import '../model/analyzer_test.dart';
import 'components/util.dart';
void main() {
@@ -26,7 +26,7 @@ void main() {
testWidgets('should report measurement count', (tester) async {
await _initStatsPage(tester, [
for (int i = 1; i<51; i++) // can't safe entries at or before epoch
- mockEntry(time: DateTime.fromMillisecondsSinceEpoch(1582991592 + i),
+ mockRecord(time: DateTime.fromMillisecondsSinceEpoch(1582991592 + i),
sys: i, dia: 60+i, pul: 110+i,),
], intervallStoreManager: IntervallStoreManager(IntervallStorage(),
IntervallStorage(), IntervallStorage(stepSize: TimeStep.lifetime,),),);
@@ -54,8 +54,8 @@ void main() {
});
testWidgets("should not display 'null' or -1", (tester) async {
await _initStatsPage(tester, [
- mockEntry(time: DateTime.fromMillisecondsSinceEpoch(1), sys: 40, dia: 60),
- mockEntry(time: DateTime.fromMillisecondsSinceEpoch(2),),
+ mockRecord(time: DateTime.fromMillisecondsSinceEpoch(1), sys: 40, dia: 60),
+ mockRecord(time: DateTime.fromMillisecondsSinceEpoch(2),),
], intervallStoreManager: IntervallStoreManager(IntervallStorage(),
IntervallStorage(), IntervallStorage(stepSize: TimeStep.lifetime),),);
expect(find.textContaining('-1'), findsNothing);
health_data_store/lib/src/types/full_entry.dart
@@ -4,6 +4,7 @@ import 'package:health_data_store/health_data_store.dart';
/// time.
typedef FullEntry = (BloodPressureRecord, Note, List<MedicineIntake>);
+/// Utility getters for nested attributes.
extension FastFullEntryGetters on FullEntry {
/// Timestamp when the entry occurred.
DateTime get time => this.$1.time;