Commit 1e8c2cb

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-06-20 13:20:09
fix core functionality
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent aac8db3
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/subsettings/export_import/export_button_bar.dart
@@ -170,6 +170,7 @@ void performExport(BuildContext context, [AppLocalizations? localizations]) asyn
 
 /// Get the records that should be exported.
 Future<List<FullEntry>> _getEntries(BuildContext context) async {
+  // TODO: unify with measurement list code
   // TODO: move function somewhere more practical
   final range = Provider.of<IntervallStoreManager>(context, listen: false).exportPage.currentRange;
 
@@ -188,6 +189,7 @@ Future<List<FullEntry>> _getEntries(BuildContext context) async {
   }
   for (final n in notes) {
     if(entryMap.containsKey(n.time)) {
+      // FIXME
       assert(entryMap[n.time]!.$2 != null, 'multiple notes at same time');
       entryMap[n.time] = (entryMap[n.time]!.$1, n, []);
     } else {
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;