Commit 2456a80

derdilla <82763757+derdilla@users.noreply.github.com>
2025-03-16 16:57:10
Rewrite form to add entries (#459)
* design form base api * use new form base api in date time form * implement note form * extract blood pressure form code * implement form switcher * implement weight form * reimplement form to add entries * Use inline tab view in form switcher * Update android files (#470) * Update android files as suggested by @pimlie * Follow suggestions in https://github.com/fluttercommunity/plus_plugins/issues/3303 (cherry picked from commit f53eec88c404ce4e29e6b4a91d47ffb597d2e1d5) * Implement focus traversal (apart from https://github.com/derdilla/inline_tab_view/issues/1) and remove old addEntryDialoge implementation * empty * reimplement entry saving * implement intitial values * only run golden updates on real PRs * implement medicine intake form * remove old weight dialogue * implement and test proper weight input * test blood pressure form * focus the correct tab when editing * Fix always adding 0kg weight * test form switcher * readd ble input ui * Implement field fill on bluetooth input * Finish DateTimeForm implementation * Test note form * make bluetooth input not fail during tests * fix conceptual medicine intake form bug prefilling the only intake leads to accidental medicine intake entries * fix fillForm for out of tree forms * fix saving empty forms * reimplement bottom app bars * implement backwards focus traversal * respect validateInputs setting * organize tests * fix missing removal of weights on edit * fix compact measurement list todo * cleanup add-entry_dialogue see: https://github.com/derdilla/blood-pressure-monitor-fl/compare/637ca463...469c93a2#diff-f5a9c061b27ded821ceaeed7087a58ec3a3217f6475e8299c6a0e3f223d50228 for removed todo * cleanup add_entry_form * cleanup dead form_base code * cleanup tests
1 parent 1c1a8e4
app/integration_test/add_measurement_test.dart
@@ -1,5 +1,5 @@
 import 'package:blood_pressure_app/app.dart';
-import 'package:blood_pressure_app/features/input/add_measurement_dialoge.dart';
+import 'package:blood_pressure_app/features/input/add_entry_dialogue.dart';
 import 'package:blood_pressure_app/features/measurement_list/measurement_list_entry.dart';
 import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
 import 'package:blood_pressure_app/screens/home_screen.dart';
@@ -20,13 +20,13 @@ void main() {
     await tester.pumpAndSettle();
     await tester.pumpUntil(() => find.byType(AppHome).hasFound);
     expect(find.byType(AppHome), findsOneWidget);
-    expect(find.byType(AddEntryDialoge), findsNothing);
+    expect(find.byType(AddEntryDialogue), findsNothing);
     expect(find.byType(MeasurementListRow), findsNothing);
 
     expect(find.byIcon(Icons.add), findsOneWidget);
     await tester.tap(find.byIcon(Icons.add));
     await tester.pumpAndSettle();
-    expect(find.byType(AddEntryDialoge), findsOneWidget);
+    expect(find.byType(AddEntryDialogue), findsOneWidget);
 
     await tester.enterText(find.byType(TextFormField).at(0), '123'); // sys
     await tester.enterText(find.byType(TextFormField).at(1), '67'); // dia
@@ -34,7 +34,7 @@ void main() {
 
     await tester.tap(find.text(localizations.btnSave));
     await tester.pumpAndSettle();
-    expect(find.byType(AddEntryDialoge), findsNothing);
+    expect(find.byType(AddEntryDialogue), findsNothing);
 
     await tester.pumpUntil(() => !find.text(localizations.loading).hasFound);
     expect(find.text(localizations.loading), findsNothing);
@@ -64,7 +64,7 @@ void main() {
 
     await tester.tap(find.byIcon(Icons.add));
     await tester.pumpAndSettle();
-    expect(find.byType(AddEntryDialoge), findsOneWidget);
+    expect(find.byType(AddEntryDialogue), findsOneWidget);
 
     await tester.enterText(find.byType(TextFormField).at(0), '123'); // sys
     await tester.enterText(find.byType(TextFormField).at(1), '67'); // dia
@@ -77,7 +77,7 @@ void main() {
 
     await tester.tap(find.text(localizations.btnSave));
     await tester.pumpAndSettle();
-    expect(find.byType(AddEntryDialoge), findsNothing);
+    expect(find.byType(AddEntryDialogue), findsNothing);
 
     await tester.pumpUntil(() => !find.text(localizations.loading).hasFound);
     expect(find.text(localizations.loading), findsNothing);
app/lib/data_util/entry_context.dart
@@ -1,6 +1,7 @@
 import 'package:blood_pressure_app/components/confirm_deletion_dialoge.dart';
+import 'package:blood_pressure_app/features/input/add_entry_dialogue.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
 import 'package:blood_pressure_app/features/export_import/export_button.dart';
-import 'package:blood_pressure_app/features/input/add_measurement_dialoge.dart';
 import 'package:blood_pressure_app/logging.dart';
 import 'package:blood_pressure_app/model/storage/storage.dart';
 import 'package:blood_pressure_app/screens/error_reporting_screen.dart';
@@ -12,43 +13,33 @@ import 'package:provider/provider.dart';
 
 /// Allow high level operations on the repositories in context.
 extension EntryUtils on BuildContext {
-  /// Open the [AddEntryDialoge] and save received entries.
+  /// Open the [AddEntryDialogue] 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 {
+  Future<void> createEntry([AddEntryFormValue? initial]) async {
     try {
       final recordRepo = RepositoryProvider.of<BloodPressureRepository>(this);
       final noteRepo = RepositoryProvider.of<NoteRepository>(this);
       final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(this);
+      final weightRepo = RepositoryProvider.of<BodyweightRepository>(this);
       final exportSettings = Provider.of<ExportSettings>(this, listen: false);
 
-      final entry = await showAddEntryDialoge(this,
+      final entry = await showAddEntryDialogue(this,
         RepositoryProvider.of<MedicineRepository>(this),
         initial,
       );
       if (entry != null) {
-        if (initial != null) {
-          if ((initial.sys != null || initial.dia != null || initial.pul != null)) {
-            await recordRepo.remove(initial.$1);
-          }
-          if ((initial.note != null || initial.color != null)) {
-            await noteRepo.remove(initial.$2);
-          }
-          for (final intake in initial.$3) {
-            await intakeRepo.remove(intake);
-          }
-        }
+        if (initial?.record != null) await recordRepo.remove(initial!.record!);
+        if (initial?.note != null) await noteRepo.remove(initial!.note!);
+        if (initial?.intake != null) await intakeRepo.remove(initial!.intake!);
+        if (initial?.weight != null) await weightRepo.remove(initial!.weight!);
+
+        if (entry.record != null) await recordRepo.add(entry.record!);
+        if (entry.note != null) await noteRepo.add(entry.note!);
+        if (entry.intake != null) await intakeRepo.add(entry.intake!);
+        if(entry.weight != null) await weightRepo.add(entry.weight!);
 
-        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) {
           read<IntervalStoreManager>().exportPage.setToMostRecentInterval();
           performExport(this);
app/lib/features/input/forms/add_entry_form.dart
@@ -0,0 +1,298 @@
+import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_backend.dart';
+import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart';
+import 'package:blood_pressure_app/features/bluetooth/logic/ble_read_cubit.dart';
+import 'package:blood_pressure_app/features/bluetooth/logic/bluetooth_cubit.dart';
+import 'package:blood_pressure_app/features/bluetooth/logic/device_scan_cubit.dart';
+import 'package:blood_pressure_app/features/input/forms/blood_pressure_form.dart';
+import 'package:blood_pressure_app/features/input/forms/date_time_form.dart';
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/features/input/forms/form_switcher.dart';
+import 'package:blood_pressure_app/features/input/forms/medicine_intake_form.dart';
+import 'package:blood_pressure_app/features/input/forms/note_form.dart';
+import 'package:blood_pressure_app/features/input/forms/weight_form.dart';
+import 'package:blood_pressure_app/features/old_bluetooth/bluetooth_input.dart';
+import 'package:blood_pressure_app/logging.dart';
+import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.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:health_data_store/health_data_store.dart';
+import 'package:provider/provider.dart';
+
+/// Primary form to enter all types of entries.
+class AddEntryForm extends FormBase<AddEntryFormValue> with TypeLogger {
+  /// Create primary form to enter all types of entries.
+  const AddEntryForm({super.key,
+    super.initialValue,
+    this.meds = const [],
+    this.bluetoothCubit,
+  });
+
+  /// All medicines selectable.
+  ///
+  /// Hides med input when this is empty.
+  final List<Medicine> meds;
+
+  /// Function to customize [BluetoothCubit] creation.
+  @visibleForTesting
+  final BluetoothCubit Function()? bluetoothCubit;
+
+  @override
+  FormStateBase createState() => AddEntryFormState();
+}
+
+/// State of primary form to enter all types of entries.
+class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm> {
+  final _timeForm = GlobalKey<DateTimeFormState>();
+  final _noteForm = GlobalKey<NoteFormState>();
+  final _bpForm = GlobalKey<BloodPressureFormState>();
+  final _weightForm = GlobalKey<WeightFormState>();
+  final _intakeForm = GlobalKey<MedicineIntakeFormState>();
+
+  final _controller = FormSwitcherController();
+
+  // because these values are no necessarily in tree a copy is needed to get
+  // overridden values.
+  BloodPressureRecord? _lastSavedPressure;
+  BodyweightRecord? _lastSavedWeight;
+  MedicineIntake? _lastSavedIntake;
+
+  @override
+  void initState() {
+    super.initState();
+    if (widget.initialValue != null) {
+      _lastSavedPressure = widget.initialValue?.record;
+      _lastSavedWeight = widget.initialValue?.weight;
+      _lastSavedIntake = widget.initialValue?.intake;
+      if (widget.initialValue!.record == null
+          && widget.initialValue!.intake == null
+          && widget.initialValue!.weight != null) {
+        _controller.animateTo(2);
+      } else if (widget.initialValue!.record == null
+          && widget.initialValue!.intake != null) {
+        _controller.animateTo(1);
+      }
+      // In all other cases we are at the correct position (0)
+      // or don't need to jump at all.
+    }
+    ServicesBinding.instance.keyboard.addHandler(_onKey);
+  }
+
+  @override
+  void dispose() {
+    ServicesBinding.instance.keyboard.removeHandler(_onKey);
+    super.dispose();
+  }
+
+  bool _onKey(KeyEvent event) {
+    if(event.logicalKey == LogicalKeyboardKey.backspace
+      && ((_bpForm.currentState?.isEmptyInputFocused() ?? false)
+          || (_noteForm.currentState?.isEmptyInputFocused() ?? false)
+          || (_weightForm.currentState?.isEmptyInputFocused() ?? false)
+          || (_intakeForm.currentState?.isEmptyInputFocused() ?? false))) {
+      FocusScope.of(context).previousFocus();
+    }
+    return false;
+  }
+
+  @override
+  bool validate() => !context.read<Settings>().validateInputs
+    || (_timeForm.currentState?.validate() ?? false)
+    && (_noteForm.currentState?.validate() ?? false)
+    // the following become null when unopened
+    && (_bpForm.currentState?.validate() ?? true)
+    && (_weightForm.currentState?.validate() ?? true)
+    && (_intakeForm.currentState?.validate() ?? true);
+
+  @override
+  AddEntryFormValue? save() {
+    if (!validate()) return null;
+    final time = _timeForm.currentState!.save()!;
+    Note? note;
+    BloodPressureRecord? record = _lastSavedPressure;
+    BodyweightRecord? weight = _lastSavedWeight;
+    MedicineIntake? intake = _lastSavedIntake;
+
+    final noteFormValue = _noteForm.currentState?.save();
+    if (noteFormValue != null) {
+      note = Note(time: time, note: noteFormValue.$1, color: noteFormValue.$2?.value);
+    }
+    final recordFormValue = _bpForm.currentState?.save();
+    if (recordFormValue != null) {
+      final unit = context.read<Settings>().preferredPressureUnit;
+      record = BloodPressureRecord(
+        time: time,
+        sys: recordFormValue.sys == null ? null : unit.wrap(recordFormValue.sys!),
+        dia: recordFormValue.dia == null ? null : unit.wrap(recordFormValue.dia!),
+        pul: recordFormValue.pul,
+      );
+    }
+    final weightFormValue = _weightForm.currentState?.save();
+    if (weightFormValue != null) {
+      weight = BodyweightRecord(time: time, weight: weightFormValue);
+    }
+    final intakeFormValue = _intakeForm.currentState?.save();
+    if (intakeFormValue != null) {
+      intake = MedicineIntake(
+        time: time,
+        medicine: intakeFormValue.$1,
+        dosis: intakeFormValue.$2,
+      );
+    }
+
+    if (note == null
+      && record == null
+      && weight == null
+      && intake == null) {
+      return null;
+    }
+    return (
+      timestamp: time,
+      note: note,
+      record: record,
+      intake: intake,
+      weight: weight,
+    );
+  }
+
+  @override
+  bool isEmptyInputFocused() => false; // doesn't contain text inputs
+
+  @override
+  void fillForm(AddEntryFormValue? value) {
+    _lastSavedPressure = value?.record;
+    _lastSavedWeight = value?.weight;
+    _lastSavedIntake = value?.intake;
+    if (value == null) {
+      _timeForm.currentState?.fillForm(null);
+      _noteForm.currentState?.fillForm(null);
+      _bpForm.currentState?.fillForm(null);
+      _weightForm.currentState?.fillForm(null);
+      _intakeForm.currentState?.fillForm(null);
+    } else {
+      _timeForm.currentState?.fillForm(value.timestamp);
+      if (value.note != null) {
+        final c = value.note?.color == null ? null : Color(value.note!.color!);
+        _noteForm.currentState?.fillForm((value.note!.note, c));
+      }
+      if (value.record != null) {
+        _bpForm.currentState?.fillForm((
+          sys: value.record?.sys?.mmHg,
+          dia: value.record?.dia?.mmHg,
+          pul: value.record?.pul,
+        ));
+      }
+      if (value.weight != null) {
+        _weightForm.currentState?.fillForm(value.weight!.weight);
+      }
+      if (value.intake != null) {
+        _intakeForm.currentState?.fillForm((
+          value.intake!.medicine,
+          value.intake!.dosis,
+        ));
+      }
+    }
+  }
+
+  void _onExternalMeasurement(BloodPressureRecord record) => fillForm((
+    timestamp: record.time,
+    note: null,
+    record: record,
+    intake: null,
+    weight: null,
+  ));
+
+  @override
+  Widget build(BuildContext context) {
+    final settings = context.watch<Settings>();
+    return ListView(
+      padding: const EdgeInsets.symmetric(horizontal: 8),
+      children: [
+        (() => switch (settings.bleInput) {
+          BluetoothInputMode.disabled => SizedBox.shrink(),
+          BluetoothInputMode.oldBluetoothInput => OldBluetoothInput(
+            onMeasurement: _onExternalMeasurement,
+          ),
+          BluetoothInputMode.newBluetoothInputOldLib => BluetoothInput(
+            manager: BluetoothManager.create(BluetoothBackend.flutterBluePlus),
+            onMeasurement: _onExternalMeasurement,
+            bluetoothCubit: widget.bluetoothCubit,
+          ),
+          BluetoothInputMode.newBluetoothInputCrossPlatform => BluetoothInput(
+            manager: BluetoothManager.create(BluetoothBackend.bluetoothLowEnergy),
+            onMeasurement: _onExternalMeasurement,
+            bluetoothCubit: widget.bluetoothCubit,
+          ),
+        })(),
+        if (settings.allowManualTimeInput)
+          DateTimeForm(
+            key: _timeForm,
+            initialValue: widget.initialValue?.timestamp,
+          ),
+        SizedBox(height: 10),
+        FormSwitcher(
+          key: Key('AddEntryFormSwitcher'), // ensures widgets are in tree
+          controller: _controller,
+          subForms: [
+            (Icon(Icons.monitor_heart_outlined), BloodPressureForm(
+              key: _bpForm,
+              initialValue: (
+                sys: widget.initialValue?.record?.sys?.mmHg,
+                dia: widget.initialValue?.record?.dia?.mmHg,
+                pul: widget.initialValue?.record?.pul,
+              ),
+            )),
+            if (widget.meds.isNotEmpty)
+              (Icon(Icons.medication_outlined), MedicineIntakeForm(
+                key: _intakeForm,
+                meds: widget.meds,
+                initialValue: widget.initialValue?.intake == null ? null : (
+                  widget.initialValue!.intake!.medicine,
+                  widget.initialValue!.intake!.dosis,
+                ),
+              )),
+            if (settings.weightInput)
+              (Icon(Icons.scale), WeightForm(
+                key: _weightForm,
+                initialValue: widget.initialValue?.weight?.weight,
+              ),),
+          ],
+        ),
+        NoteForm(
+          key: _noteForm,
+          initialValue: (){
+            if (widget.initialValue?.note?.note == null) return null;
+            final note = widget.initialValue!.note!;
+            final color = note.color == null ? null : Color(note.color!);
+            return (note.note, color);
+          }(),
+        ),
+      ]
+    );
+  }
+}
+
+/// Types of entries supported by [AddEntryForm].
+typedef AddEntryFormValue = ({
+  DateTime timestamp,
+  Note? note,
+  BloodPressureRecord? record,
+  MedicineIntake? intake,
+  BodyweightRecord? weight,
+});
+
+/// Compatibility extension for simpler API surface.
+extension AddEntryFormValueCompat on FullEntry {
+  /// Utility converter for the differences in API.
+  AddEntryFormValue get asAddEntry {
+    assert(intakes.length <= 1);
+    return (
+      timestamp: time,
+      note: (note != null && color == null) ? null : noteObj,
+      record: (sys == null && dia == null && pul == null) ? null : recordObj,
+      intake: intakes.firstOrNull,
+      weight: null,
+    );
+  }
+}
app/lib/features/input/forms/blood_pressure_form.dart
@@ -0,0 +1,168 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:provider/provider.dart';
+
+/// Form to enter freeform text and select color.
+class BloodPressureForm extends FormBase<({int? sys, int? dia, int? pul})> {
+  /// Create form to enter freeform text and select color.
+  const BloodPressureForm({super.key,
+    super.initialValue,
+  });
+
+  @override
+  BloodPressureFormState createState() => BloodPressureFormState();
+}
+
+/// State of form to enter freeform text and select color.
+class BloodPressureFormState extends FormStateBase<({int? sys, int? dia, int? pul}), BloodPressureForm> {
+  final _formKey = GlobalKey<FormState>();
+
+  final _sysFocusNode = FocusNode();
+  final _diaFocusNode = FocusNode();
+  final _pulFocusNode = FocusNode();
+
+  late final TextEditingController _sysController;
+  late final TextEditingController _diaController;
+  late final TextEditingController _pulController;
+
+  @override
+  void initState() {
+    super.initState();
+    _sysController = TextEditingController(text: widget.initialValue?.sys?.toString() ?? '');
+    _diaController = TextEditingController(text: widget.initialValue?.dia?.toString() ?? '');
+    _pulController = TextEditingController(text: widget.initialValue?.pul?.toString() ?? '');
+    _sysFocusNode.requestFocus();
+  }
+
+  @override
+  void dispose() {
+    _sysFocusNode.dispose();
+    _diaFocusNode.dispose();
+    _pulFocusNode.dispose();
+    _sysController.dispose();
+    _diaController.dispose();
+    _pulController.dispose();
+    super.dispose();
+  }
+
+  @override
+  bool validate() {
+    if (_sysController.text.isEmpty
+        && _diaController.text.isEmpty
+        && _pulController.text.isEmpty) {
+      return true;
+    }
+    return _formKey.currentState?.validate() ?? false;
+  }
+
+  @override
+  ({int? sys, int? dia, int? pul})? save() {
+    if (!validate()
+      || (int.tryParse(_sysController.text) == null
+      && int.tryParse(_diaController.text) == null
+      && int.tryParse(_pulController.text) == null)) {
+      return null;
+    }
+    return (
+      sys: int.tryParse(_sysController.text),
+      dia: int.tryParse(_diaController.text),
+      pul: int.tryParse(_pulController.text),
+    );
+  }
+
+  @override
+  bool isEmptyInputFocused() => (_diaFocusNode.hasFocus && _diaController.text.isEmpty)
+   || (_pulFocusNode.hasFocus && _pulController.text.isEmpty);
+
+  @override
+  void fillForm(({int? dia, int? pul, int? sys})? value) => setState(() {
+    if (value == null) {
+        _sysController.text = '';
+        _diaController.text = '';
+        _pulController.text = '';
+    } else {
+      if (value.dia != null) _diaController.text = value.dia.toString();
+      if (value.pul != null) _pulController.text = value.pul.toString();
+      if (value.sys != null) _sysController.text = value.sys.toString();
+    }
+  });
+
+  Widget _buildValueInput({
+    String? labelText,
+    FocusNode? focusNode,
+    TextEditingController? controller,
+    String? Function(String?)? validator,
+  }) => Expanded(
+    child: TextFormField(
+      focusNode: focusNode,
+      controller: controller,
+      keyboardType: TextInputType.number,
+      inputFormatters: [FilteringTextInputFormatter.digitsOnly],
+      onChanged: (String value) {
+        if (value.isNotEmpty
+            && (int.tryParse(value) ?? -1) > 40) {
+          FocusScope.of(context).nextFocus();
+        }
+      },
+      validator: (String? value) {
+        final settings = context.read<Settings>();
+        if (!settings.allowMissingValues
+            && (value == null
+                || value.isEmpty
+                || int.tryParse(value) == null)) {
+          return AppLocalizations.of(context)!.errNaN;
+        } else if (settings.validateInputs
+            && (int.tryParse(value ?? '') ?? -1) <= 30) {
+          return AppLocalizations.of(context)!.errLt30;
+        } else if (settings.validateInputs
+            && (int.tryParse(value ?? '') ?? 0) >= 400) {
+          // https://pubmed.ncbi.nlm.nih.gov/7741618/
+          return AppLocalizations.of(context)!.errUnrealistic;
+        }
+        return validator?.call(value);
+      },
+      decoration: InputDecoration(
+        labelText: labelText,
+      ),
+      style: Theme.of(context).textTheme.bodyLarge,
+    ),
+  );
+
+  @override
+  Widget build(BuildContext context) => Form(
+    key: _formKey,
+    child: Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        _buildValueInput(
+          focusNode: _sysFocusNode,
+          controller: _sysController,
+          labelText: AppLocalizations.of(context)!.sysLong,
+        ),
+        const SizedBox(width: 8,),
+        _buildValueInput(
+          labelText: AppLocalizations.of(context)!.diaLong,
+          controller: _diaController,
+          focusNode: _diaFocusNode,
+          validator: (value) {
+            if (context.read<Settings>().validateInputs
+              && (int.tryParse(value ?? '') ?? 0)
+                >= (int.tryParse(_sysController.text) ?? 1)) {
+              return AppLocalizations.of(context)?.errDiaGtSys;
+            }
+            return null;
+          },
+        ),
+        const SizedBox(width: 8,),
+        _buildValueInput(
+          controller: _pulController,
+          focusNode: _pulFocusNode,
+          labelText: AppLocalizations.of(context)!.pulLong,
+        ),
+      ],
+    ),
+  );
+}
app/lib/features/input/forms/date_time_form.dart
@@ -1,88 +1,109 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
 
 /// Input to allow date and time input.
-class DateTimeForm extends StatefulWidget {
+class DateTimeForm extends FormBase<DateTime> {
   /// Create input to allow date and time input.
   const DateTimeForm({super.key,
-    required this.initialTime,
-    required this.validate,
-    required this.onTimeSelected,
+    super.initialValue,
   });
 
-  /// Initial time to display
-  final DateTime initialTime;
+  @override
+  FormStateBase<DateTime, DateTimeForm> createState() => DateTimeFormState();
+}
 
-  /// Whether to validate whether the time is after now.
-  final bool validate;
+/// State of a [DateTimeForm].
+class DateTimeFormState extends FormStateBase<DateTime, DateTimeForm> {
+  late DateTime _time;
 
-  /// Call after a new time is successfully selected.
-  final void Function(DateTime time) onTimeSelected;
+  String? _error;
 
   @override
-  State<DateTimeForm> createState() => _DateTimeFormState();
-}
+  void initState() {
+    super.initState();
+    _time = widget.initialValue ?? DateTime.now();
+  }
+
+  @override
+  DateTime? save() => validate() ? _time : null;
+
+  @override
+  bool isEmptyInputFocused() => false;
+
+  @override
+  bool validate() {
+    if (context.read<Settings>().validateInputs && _time.isAfter(DateTime.now())) {
+      setState(() {
+        _error = AppLocalizations.of(context)!.errTimeAfterNow;
+      });
+      return false;
+    } else if (_error != null) {
+      setState(() {
+        _error = null;
+      });
+    }
+    return true;
+  }
+
+  @override
+  void fillForm(DateTime? value) => setState(() {
+    _time = value ?? DateTime.now();
+  });
 
-class _DateTimeFormState extends State<DateTimeForm> {
   Future<void> _openDatePicker() async {
     final now = DateTime.now();
     final date = await showDatePicker(
       context: context,
-      initialDate: widget.initialTime,
+      initialDate: _time,
       firstDate: DateTime.fromMillisecondsSinceEpoch(1),
-      lastDate: widget.initialTime.isAfter(now) ? widget.initialTime : now,
+      lastDate: _time.isAfter(now) ? _time : now,
     );
     if (date == null) return;
-    _validateAndInvoke(date.copyWith(
-      hour: widget.initialTime.hour,
-      minute: widget.initialTime.minute,
+    setState(() => _time = date.copyWith(
+      hour: _time.hour,
+      minute: _time.minute,
     ));
+
   }
 
   Future<void> _openTimePicker() async {
-    final time = await showTimePicker(
+    final timeOfDay = await showTimePicker(
       context: context,
-      initialTime: TimeOfDay.fromDateTime(widget.initialTime),
+      initialTime: TimeOfDay.fromDateTime(_time),
     );
-    if (time == null) return;
-    _validateAndInvoke(widget.initialTime.copyWith(
-      hour: time.hour,
-      minute: time.minute,
+    if (timeOfDay == null) return;
+    setState(() => _time = _time.copyWith(
+      hour: timeOfDay.hour,
+      minute: timeOfDay.minute,
     ));
   }
 
-  void _validateAndInvoke(DateTime time) {
-    if (widget.validate && time.isAfter(DateTime.now())) {
-      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
-        content: Text(AppLocalizations.of(context)!.errTimeAfterNow),
-      ));
-      return;
-    }
-    widget.onTimeSelected(time);
-  }
-
-  Widget _buildInput(String content, void Function() onTap, String label) => Expanded(
+  Widget _buildInput(String content, void Function() onTap, String label, [String? error]) => Expanded(
     child: InputDecorator(
+      decoration: InputDecoration(
+        labelText: label,
+        error: error == null ? null : Text(error),
+      ),
       child: GestureDetector(
         onTap: onTap,
         child: Text(content, style: Theme.of(context).textTheme.bodyLarge)
       ),
-      decoration: InputDecoration(
-        labelText: label,
-      ),
     ),
   );
 
   @override
   Widget build(BuildContext context) {
-    final date = DateFormat('yyyy-MM-dd').format(widget.initialTime);
-    final time = DateFormat('HH:mm').format(widget.initialTime);
+    final date = DateFormat('yyyy-MM-dd').format(_time);
+    final timeOfDay = DateFormat('HH:mm').format(_time);
     return Row(
       children: [
         _buildInput(date, _openDatePicker, AppLocalizations.of(context)!.date),
         SizedBox(width: 8,),
-        _buildInput(time, _openTimePicker, AppLocalizations.of(context)!.time),
+        _buildInput(timeOfDay, _openTimePicker, AppLocalizations.of(context)!.time, _error),
       ],
     );
   }
app/lib/features/input/forms/form_base.dart
@@ -0,0 +1,51 @@
+import 'package:flutter/material.dart';
+
+/// Base for a generic form with return value [T].
+abstract class FormBase<T> extends StatefulWidget {
+  /// Create a form with generic return value.
+  const FormBase({super.key, this.initialValue});
+
+  /// Initial value to prefill the form with.
+  final T? initialValue;
+
+  @override
+  FormStateBase createState();
+}
+
+/// State of a form allowing validation and result gathering using [GlobalKey].
+///
+/// ### Sample usage
+/// ```dart
+/// class SomeWidgetState extends State<SomeWidget> {
+///   final key = GlobalKey<FormStateBase>();
+///   ...
+///   FormBase(key: key),
+///   ...
+///   TextButton(
+///     child: Text('save'),
+///     onPressed: () => if (_timeFormState.currentState?.validate() ?? false) {
+///       Navigator.pop(context, _timeFormState.currentState!.save());
+///     },
+///   )
+/// ```
+abstract class FormStateBase<T, G extends FormBase> extends State<G> {
+  /// Validates all form fields and shows errors on failing form fields.
+  ///
+  /// Returns whether the all fields validated without error.
+  bool validate();
+
+  /// Parses and returns the forms current value, if [validate] passes.
+  T? save();
+
+  /// Whether an empty input field is focused.
+  ///
+  /// Used to automatically focus the last input field on back key.
+  bool isEmptyInputFocused();
+
+  /// Set the input fields with the [value].
+  ///
+  /// If [value} is null clear the form. If value contains attributes that
+  /// correspond to different fields, only the non null attributes change field
+  /// contents.
+  void fillForm(T? value);
+}
app/lib/features/input/forms/form_switcher.dart
@@ -0,0 +1,100 @@
+import 'dart:collection';
+
+import 'package:flutter/material.dart';
+import 'package:inline_tab_view/inline_tab_view.dart';
+
+/// A resizing view that associates a tab-bar and child widgets.
+class FormSwitcher extends StatefulWidget {
+  /// Create a resizing view that associates a tab-bar and child widgets-
+  const FormSwitcher({super.key,
+    required this.subForms,
+    this.controller,
+  });
+
+  /// List of (tab title, tab content) pairs.
+  final List<(Widget, Widget)> subForms;
+  
+  /// Controller to use to control the switcher from code. 
+  final FormSwitcherController? controller;
+
+  @override
+  State<FormSwitcher> createState() => _FormSwitcherState();
+}
+
+class _FormSwitcherState extends State<FormSwitcher>
+    with TickerProviderStateMixin {
+  late final TabController controller;
+
+  @override
+  void initState() {
+    super.initState();
+    controller = TabController(length: widget.subForms.length, vsync: this);
+    widget.controller?._initialize(controller);
+  }
+
+  @override
+  void dispose() {
+    controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    assert(widget.subForms.isNotEmpty);
+    if (widget.subForms.length == 1) {
+      return widget.subForms[0].$2;
+    }
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        TabBar.secondary(
+          controller: controller,
+          tabs: [
+            for (final f in widget.subForms)
+              Padding(
+                padding: EdgeInsets.all(8.0),
+                child: f.$1,
+              ),
+          ],
+        ),
+        InlineTabView(
+          controller: controller,
+          children: [
+            for (final f in widget.subForms)
+              Padding(
+                padding: EdgeInsets.only(top: 8.0),
+                child: f.$2,
+              ),
+          ],
+        ),
+      ],
+    );
+  }
+}
+
+/// Allows controlling a [FormSwitcher] from code.
+class FormSwitcherController {
+  final Queue<Function> _pendingActions = Queue();
+
+  TabController? _controller;
+
+  /// Add a reference to a TabController to control.
+  /// 
+  /// This does not mean this object is responsible for destroying it.
+  void _initialize(TabController controller) {
+    assert(_controller == null, 'FormSwitcherController was initialized twice');
+    _controller = controller;
+    while (_pendingActions.isNotEmpty) {
+      _pendingActions.removeFirst().call();
+    }
+  }
+
+  /// Animates to viewing the page at the specified index as soon as possible.
+  void animateTo(int index) {
+    if (_controller == null) {
+      _pendingActions.add(() => animateTo(index));
+    } else {
+      _controller!.animateTo(index);
+    }
+  }
+}
app/lib/features/input/forms/medicine_intake_form.dart
@@ -0,0 +1,114 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+/// Form to enter medicine intakes.
+class MedicineIntakeForm extends FormBase<(Medicine, Weight)> {
+  /// Create form to enter medicine intakes.
+  MedicineIntakeForm({super.key,
+    super.initialValue,
+    required this.meds,
+  }) : assert(meds.isNotEmpty);
+
+  /// All selectable medicines.
+  final List<Medicine> meds;
+
+  @override
+  FormStateBase<(Medicine, Weight), MedicineIntakeForm> createState() =>
+    MedicineIntakeFormState();
+}
+
+/// State of form to enter medicine intakes.
+class MedicineIntakeFormState extends FormStateBase<(Medicine, Weight), MedicineIntakeForm> {
+  final _controller = TextEditingController();
+
+  Medicine? _leadingMed;
+  String? _error;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller.text = _leadingMed?.dosis?.mg.toString() ?? '';
+
+    if (widget.initialValue != null) {
+      _leadingMed = widget.initialValue!.$1;
+      _controller.text = widget.initialValue!.$2.mg.toString();
+    }
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  bool validate() {
+    if (_leadingMed != null && double.tryParse(_controller.text) == null) {
+      setState(() => _error = AppLocalizations.of(context)!.errNaN);
+      return false;
+    }
+    setState(() => _error = null);
+    return true;
+  }
+
+  @override
+  (Medicine, Weight)? save() {
+    if (_leadingMed == null || !validate()) return null;
+    return (_leadingMed!, Weight.mg(double.parse(_controller.text)));
+  }
+
+  @override
+  bool isEmptyInputFocused() => false;
+
+  @override
+  void fillForm((Medicine, Weight)? value) => setState(() {
+    if (value == null) {
+      _leadingMed = null;
+      _controller.text = '';
+    } else {
+      _leadingMed = value.$1;
+      _controller.text = value.$2.mg.toString();
+    }
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    if (_leadingMed != null) {
+      return TextField(
+        decoration: InputDecoration(
+          helperText: _leadingMed!.designation,
+          labelText: AppLocalizations.of(context)!.dosis,
+          prefixIcon: Icon(Icons.medication,
+            color: _leadingMed!.color == null ? null : Color(_leadingMed!.color!)),
+          suffixIcon: IconButton(
+            onPressed: () => setState(() => _leadingMed = null),
+            icon: Icon(Icons.close),
+          ),
+          errorText: _error,
+        ),
+        controller: _controller,
+        inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9,.]'))],
+        keyboardType: const TextInputType.numberWithOptions(decimal: true),
+      );
+    }
+    return Column(
+      children: [
+        for (final m in widget.meds)
+          ListTile(
+            leading: Icon(Icons.medication, color: m.color == null ? null : Color(m.color!)),
+            title: Text(m.designation),
+            subtitle: (widget.meds.length == 1)
+                ? Text(AppLocalizations.of(context)!.tapToSelect)
+                : null,
+            onTap: () => setState(() {
+              _leadingMed = m;
+              _controller.text = _leadingMed?.dosis?.mg.toString() ?? '';
+            }),
+          ),
+      ],
+    );
+  }
+}
app/lib/features/input/forms/note_form.dart
@@ -0,0 +1,92 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+/// Form to enter freeform text and select color.
+class NoteForm extends FormBase<(String?, Color?)> {
+  /// Create form to enter freeform text and select color.
+  const NoteForm({super.key,
+    super.initialValue,
+  });
+
+  @override
+  NoteFormState createState() => NoteFormState();
+}
+
+/// State of form to enter freeform text and select color.
+class NoteFormState extends FormStateBase<(String?, Color?), NoteForm> {
+  late final TextEditingController _controller;
+
+  final FocusNode _focusNode = FocusNode();
+
+  Color? _color;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = TextEditingController(text: widget.initialValue?.$1);
+    _color = widget.initialValue?.$2;
+  }
+
+  @override
+  void dispose() {
+    _focusNode.dispose();
+    _controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  bool validate() => true;
+
+  @override
+  (String?, Color?)? save() {
+    final String? text = _controller.text.isEmpty ? null : _controller.text;
+    if (text == null && _color == null) return null;
+    return (text, _color);
+  }
+
+  @override
+  bool isEmptyInputFocused() => _focusNode.hasFocus && _controller.text.isEmpty;
+
+  @override
+  void fillForm((String?, Color?)? value) => setState(() {
+    if (value == null) {
+      _controller.text = '';
+      _color = null;
+    } else {
+      if (value.$1 != null) _controller.text = value.$1!;
+      if (value.$2 != null) _color = value.$2!;
+    }
+  });
+
+  @override
+  Widget build(BuildContext context) => Column(
+    children: [
+      Padding(
+        padding: const EdgeInsets.symmetric(vertical: 16),
+        child: TextFormField(
+          focusNode: _focusNode,
+          controller: _controller,
+          decoration: InputDecoration(
+            labelText: AppLocalizations.of(context)!.addNote,
+          ),
+          minLines: 1,
+          maxLines: 4,
+        ),
+      ),
+      InputDecorator(
+        decoration: InputDecoration(
+          contentPadding: EdgeInsets.zero,
+        ),
+        child: ColorSelectionListTile(
+          title: Text(AppLocalizations.of(context)!.color, style: Theme.of(context).textTheme.bodyLarge,),
+          onMainColorChanged: (Color value) => setState(() {
+            _color = (value == Colors.transparent) ? null : value;
+          }),
+          initialColor: _color ?? Colors.transparent,
+        ),
+      ),
+    ],
+  );
+}
app/lib/features/input/forms/weight_form.dart
@@ -0,0 +1,81 @@
+import 'package:blood_pressure_app/features/input/forms/form_base.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:provider/provider.dart';
+
+/// A form to enter [Weight] in the preferred unit.
+class WeightForm extends FormBase<Weight> {
+  /// Create a form to enter [Weight] in the preferred unit.
+  const WeightForm({super.key, super.initialValue});
+
+  @override
+  FormStateBase<Weight, WeightForm> createState() => WeightFormState();
+}
+
+/// State of a form to enter [Weight] in the preferred unit.
+class WeightFormState extends FormStateBase<Weight, WeightForm> {
+  final TextEditingController _controller = TextEditingController();
+
+  String? _error;
+
+  @override
+  void initState() {
+    super.initState();
+    if (widget.initialValue != null) {
+      final w = context.read<Settings>().weightUnit.extract(widget.initialValue!);
+      _controller.text = w.toString();
+    }
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  bool validate() {
+    if (_controller.text.isNotEmpty && double.tryParse(_controller.text) == null) {
+      setState(() => _error = AppLocalizations.of(context)!.errNaN);
+      return false;
+    }
+    setState(() => _error = null);
+    return true;
+  }
+
+  @override
+  Weight? save() {
+    if((validate(), double.tryParse(_controller.text)) case (true, final double x)) {
+      return context.read<Settings>().weightUnit.store(x);
+    }
+    return null;
+  }
+
+  @override
+  bool isEmptyInputFocused() => false;
+
+  @override
+  void fillForm(Weight? value) {
+    if (value == null) {
+      _controller.text = '';
+    } else {
+      final w = context.read<Settings>().weightUnit.extract(widget.initialValue!);
+      _controller.text = w.toString();
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) => TextField(
+    decoration: InputDecoration(
+      labelText: AppLocalizations.of(context)!.weight,
+      suffix: Text(context.select((Settings s) => s.weightUnit).name),
+      errorText: _error,
+    ),
+    controller: _controller,
+    inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9,.]'))],
+    keyboardType: const TextInputType.numberWithOptions(decimal: true),
+  );
+}
app/lib/features/input/add_bodyweight_dialoge.dart
@@ -1,47 +0,0 @@
-import 'package:blood_pressure_app/model/storage/settings_store.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';
-
-/// A simple dialoge to enter one weight value in kg.
-///
-/// Returns a [Weight] on submission.
-class AddBodyweightDialoge extends StatelessWidget {
-  /// Create a simple dialoge to enter one weight value in kg.
-  const AddBodyweightDialoge({super.key});
-
-  @override
-  Widget build(BuildContext context) => Dialog(
-    child: Padding(
-      padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 2.0),
-      child: TextFormField(
-        autofocus: true,
-        decoration: InputDecoration(
-          labelText: AppLocalizations.of(context)!.weight,
-          suffix: Text(context.select((Settings s) => s.weightUnit).name),
-        ),
-        inputFormatters: [FilteringTextInputFormatter.allow(RegExp('[0-9,.]'))],
-        keyboardType: const TextInputType.numberWithOptions(decimal: true),
-        autovalidateMode: AutovalidateMode.onUnfocus,
-        validator: (value) {
-          if (value == null
-            || value.isEmpty
-            || double.tryParse(value) == null
-          ) {
-            return AppLocalizations.of(context)!.errNaN;
-          }
-          return null;
-        },
-        onFieldSubmitted: (String text) {
-          final value = double.tryParse(text);
-          if (value != null) {
-            final weight = context.read<Settings>().weightUnit.store(value);
-            Navigator.of(context).pop(weight);
-          }
-        },
-      ),
-    ),
-  );
-}
app/lib/features/input/add_entry_dialogue.dart
@@ -0,0 +1,78 @@
+import 'dart:async';
+
+import 'package:blood_pressure_app/components/fullscreen_dialoge.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
+import 'package:blood_pressure_app/model/storage/storage.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';
+
+/// Input mask for entering measurements.
+class AddEntryDialogue extends StatefulWidget {
+  /// Create a input mask for entering measurements.
+  /// 
+  /// This is usually created through the [showAddEntryDialogue] function.
+  const AddEntryDialogue({super.key,
+    this.availableMeds,
+    this.initialRecord,
+  });
+
+  /// Values that are prefilled.
+  ///
+  /// When this is null the timestamp is [DateTime.now] and the other fields
+  /// will be empty.
+  final AddEntryFormValue? initialRecord;
+
+  /// All medicines selectable.
+  ///
+  /// Hides med input when this is empty or null.
+  final List<Medicine>? availableMeds;
+
+  @override
+  State<AddEntryDialogue> createState() => _AddEntryDialogueState();
+}
+
+class _AddEntryDialogueState extends State<AddEntryDialogue> {
+  final formKey = GlobalKey<AddEntryFormState>();
+
+  void _onSavePressed() {
+    if (formKey.currentState?.validate() ?? false) {
+      final AddEntryFormValue? result = formKey.currentState?.save();
+      Navigator.pop(context, result);
+    } else {
+      // Errors are displayed below their specific widgets
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) => FullscreenDialoge(
+    actionButtonText: AppLocalizations.of(context)!.btnSave,
+    onActionButtonPressed: _onSavePressed,
+    bottomAppBar: context.select((Settings s) => s.bottomAppBars),
+    body: AddEntryForm(
+      key: formKey,
+      initialValue: widget.initialRecord,
+      meds: widget.availableMeds ?? [],
+    ),
+  );
+}
+
+/// Shows a dialogue to input a blood pressure measurement or a medication.
+Future<AddEntryFormValue?> showAddEntryDialogue(
+  BuildContext context,
+  MedicineRepository medRepo,
+  [AddEntryFormValue? initialRecord,
+]) async {
+  final meds = await medRepo.getAll();
+  if (context.mounted) {
+    return showDialog<AddEntryFormValue>(
+      context: context, builder: (context) =>
+        AddEntryDialogue(
+          initialRecord: initialRecord,
+          availableMeds: meds,
+        ),
+    );
+  }
+  return null;
+}
app/lib/features/input/add_measurement_dialoge.dart
@@ -1,461 +0,0 @@
-import 'dart:async';
-import 'dart:io';
-
-import 'package:blood_pressure_app/components/fullscreen_dialoge.dart';
-import 'package:blood_pressure_app/config.dart';
-import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_backend.dart';
-import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart';
-import 'package:blood_pressure_app/features/input/add_bodyweight_dialoge.dart';
-import 'package:blood_pressure_app/features/input/forms/date_time_form.dart';
-import 'package:blood_pressure_app/features/old_bluetooth/bluetooth_input.dart';
-import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
-import 'package:blood_pressure_app/model/blood_pressure/pressure_unit.dart';
-import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.dart';
-import 'package:blood_pressure_app/model/storage/storage.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:health_data_store/health_data_store.dart';
-import 'package:provider/provider.dart';
-
-/// Input mask for entering measurements.
-class AddEntryDialoge extends StatefulWidget {
-  /// Create a input mask for entering measurements.
-  /// 
-  /// This is usually created through the [showAddEntryDialoge] function.
-  const AddEntryDialoge({super.key,
-    required this.availableMeds,
-    this.initialRecord,
-  });
-
-  /// Values that are prefilled.
-  ///
-  /// When this is null the timestamp is [DateTime.now] and the other fields
-  /// will be empty.
-  ///
-  /// When an initial record is set medicine input is not possible because it is
-  /// saved separately.
-  final FullEntry? initialRecord;
-
-  /// All medicines selectable.
-  ///
-  /// Hides med input when this is empty.
-  final List<Medicine> availableMeds;
-
-  @override
-  State<AddEntryDialoge> createState() => _AddEntryDialogeState();
-}
-
-class _AddEntryDialogeState extends State<AddEntryDialoge> {
-  final recordFormKey = GlobalKey<FormState>();
-  final medicationFormKey = GlobalKey<FormState>();
-
-  final sysFocusNode = FocusNode();
-  final diaFocusNode = FocusNode();
-  final pulFocusNode = FocusNode();
-  final noteFocusNode = FocusNode();
-  final dosisFocusNote = FocusNode();
-
-  late final TextEditingController sysController;
-  late final TextEditingController diaController;
-  late final TextEditingController pulController;
-  late final TextEditingController noteController;
-
-  /// Currently selected time.
-  late DateTime time;
-
-  /// Current selected note color.
-  Color? color;
-
-  /// Last [FormState.save]d systolic value.
-  int? systolic;
-
-  /// Last [FormState.save]d diastolic value.
-  int? diastolic;
-
-  /// Last [FormState.save]d pulse value.
-  int? pulse;
-  
-  /// Medicine to save.
-  Medicine? selectedMed;
-
-  /// Whether to show the medication dosis input
-  bool _showMedicineDosisInput = false;
-
-  /// Entered dosis of medication.
-  ///
-  /// Prefilled with default dosis of selected medicine.
-  double? medicineDosis;
-
-  @override
-  void initState() {
-    super.initState();
-    sysController = TextEditingController();
-    diaController = TextEditingController();
-    pulController = TextEditingController();
-    noteController = TextEditingController();
-    _loadFields(widget.initialRecord);
-
-    sysFocusNode.requestFocus();
-    ServicesBinding.instance.keyboard.addHandler(_onKey);
-  }
-
-
-  @override
-  void dispose() {
-    sysController.dispose();
-    diaController.dispose();
-    pulController.dispose();
-    noteController.dispose();
-
-    sysFocusNode.dispose();
-    diaFocusNode.dispose();
-    pulFocusNode.dispose();
-    noteFocusNode.dispose();
-    ServicesBinding.instance.keyboard.removeHandler(_onKey);
-    super.dispose();
-  }
-
-  /// Sets fields to values in a [record].
-  void _loadFields(FullEntry? entry) {
-    final settings = context.read<Settings>();
-    time = entry?.time ?? DateTime.now();
-    final int? colorValue = entry?.color;
-    final sysValue = switch(settings.preferredPressureUnit) {
-      PressureUnit.mmHg => entry?.sys?.mmHg,
-      PressureUnit.kPa => entry?.sys?.kPa.round(),
-    };
-    final diaValue = switch(settings.preferredPressureUnit) {
-      PressureUnit.mmHg => entry?.dia?.mmHg,
-      PressureUnit.kPa => entry?.dia?.kPa.round(),
-    };
-    if (colorValue != null) color = Color(colorValue);
-    if (entry?.sys != null) sysController.text = sysValue.toString();
-    if (entry?.dia != null) diaController.text = diaValue.toString();
-    if (entry?.pul != null) pulController.text = entry!.pul!.toString();
-    if (entry?.note != null) noteController.text = entry!.note!;
-  }
-
-  bool _onKey(KeyEvent event) {
-    if (event is! KeyDownEvent) return false;
-    final isBackspace = event.logicalKey.keyId == 0x00100000008;
-    if (!isBackspace) return false;
-    recordFormKey.currentState?.save();
-    if (diaFocusNode.hasFocus && diastolic == null 
-        || pulFocusNode.hasFocus && pulse == null
-        || noteFocusNode.hasFocus && noteController.text.isEmpty
-    ) {
-      FocusScope.of(context).previousFocus();
-    }
-    return false;
-  }
-
-  /// Build a input for values in the measurement form (sys, dia, pul).
-  Widget _buildValueInput(AppLocalizations localizations, Settings settings, {
-    String? labelText,
-    void Function(String?)? onSaved,
-    FocusNode? focusNode,
-    TextEditingController? controller,
-    String? Function(String?)? validator,
-  }) => Expanded(
-    child: TextFormField(
-      decoration: InputDecoration(
-        labelText: labelText,
-      ),
-      keyboardType: TextInputType.number,
-      focusNode: focusNode,
-      onSaved: onSaved,
-      controller: controller,
-      style: Theme.of(context).textTheme.bodyLarge,
-      inputFormatters: [FilteringTextInputFormatter.digitsOnly],
-      onChanged: (String value) {
-        if (value.isNotEmpty
-            && (int.tryParse(value) ?? -1) > 40) {
-          FocusScope.of(context).nextFocus();
-        }
-      },
-      validator: (String? value) {
-        if (!settings.allowMissingValues
-            && (value == null
-                || value.isEmpty
-                || int.tryParse(value) == null)) {
-          return localizations.errNaN;
-        } else if (settings.validateInputs
-            && (int.tryParse(value ?? '') ?? -1) <= 30) {
-          return localizations.errLt30;
-        } else if (settings.validateInputs
-            && (int.tryParse(value ?? '') ?? 0) >= 400) {
-          // https://pubmed.ncbi.nlm.nih.gov/7741618/
-          return localizations.errUnrealistic;
-        }
-        return validator?.call(value);
-      },
-    ),
-  );
-
-  void _onExternalMeasurement(BloodPressureRecord record) => setState(() {
-    final note = Note(time: record.time, note: noteController.text, color: color?.value);
-    _loadFields((record, note, []));
-  });
-
-  @override
-  Widget build(BuildContext context) {
-    final localizations = AppLocalizations.of(context)!;
-    final settings = context.watch<Settings>();
-    return FullscreenDialoge(
-      onActionButtonPressed: () {
-        BloodPressureRecord? record;
-        Note? note;
-        final List<MedicineIntake> intakes = [];
-
-        final bool shouldHaveRecord = (sysController.text.isNotEmpty
-          || diaController.text.isNotEmpty
-          || pulController.text.isNotEmpty);
-
-        if (shouldHaveRecord && (recordFormKey.currentState?.validate() ?? false)) {
-          recordFormKey.currentState?.save();
-          if (systolic != null || diastolic != null || pulse != null) {
-            final pressureUnit = settings.preferredPressureUnit;
-            record = BloodPressureRecord(
-              time: time,
-              sys: systolic == null ? null : pressureUnit.wrap(systolic!),
-              dia: diastolic == null ? null : pressureUnit.wrap(diastolic!),
-              pul: pulse,
-            );
-          }
-        }
-
-
-        if (noteController.text.isNotEmpty || color != null) {
-          note = Note(
-            time: time,
-            note: noteController.text.isEmpty ? null : noteController.text,
-            color: color?.value,
-          );
-        }
-        if (_showMedicineDosisInput
-            && (medicationFormKey.currentState?.validate() ?? false)) {
-          medicationFormKey.currentState?.save();
-          if (medicineDosis != null
-              && selectedMed != null) {
-            intakes.add(MedicineIntake(
-              time: time,
-              medicine: selectedMed!,
-              dosis: Weight.mg(medicineDosis!),
-            ));
-          }
-        }
-
-        if (record != null || intakes.isNotEmpty || note != null) {
-          if (record == null && shouldHaveRecord) return; // Errors are shown
-          if (intakes.isEmpty && _showMedicineDosisInput) return; // Errors are shown
-          record ??= BloodPressureRecord(time: time);
-          note ??= Note(time: time);
-          Navigator.pop(context, (record, note, intakes));
-        }
-      },
-      actionButtonText: localizations.btnSave,
-      bottomAppBar: settings.bottomAppBars,
-      body: SizeChangedLayoutNotifier(
-        child: ListView(
-          padding: const EdgeInsets.symmetric(horizontal: 8),
-          children: [
-            if (!isTestingEnvironment) // TODO: test feature (#494)
-              (() => switch (settings.bleInput) {
-                BluetoothInputMode.disabled => SizedBox.shrink(),
-                BluetoothInputMode.oldBluetoothInput => OldBluetoothInput(
-                  onMeasurement: _onExternalMeasurement,
-                ),
-                BluetoothInputMode.newBluetoothInputOldLib => BluetoothInput(
-                  manager: BluetoothManager.create(BluetoothBackend.flutterBluePlus),
-                  onMeasurement: _onExternalMeasurement,
-                ),
-                BluetoothInputMode.newBluetoothInputCrossPlatform => BluetoothInput(
-                  manager: BluetoothManager.create( BluetoothBackend.bluetoothLowEnergy),
-                  onMeasurement: _onExternalMeasurement,
-                ),
-              })(),
-            if (settings.allowManualTimeInput)
-              DateTimeForm(
-                validate: settings.validateInputs,
-                initialTime: time,
-                onTimeSelected: (newTime) => setState(() {
-                  time = newTime;
-                }),
-              ),
-            Form(
-              key: recordFormKey,
-              child: Column(
-                children: [
-                  const SizedBox(height: 16,),
-                  Row(
-                    mainAxisSize: MainAxisSize.min,
-                    children: [
-                      _buildValueInput(localizations, settings,
-                        focusNode: sysFocusNode,
-                        labelText: localizations.sysLong,
-                        controller: sysController,
-                        onSaved: (value) =>
-                            setState(() => systolic = int.tryParse(value ?? '')),
-                      ),
-                      const SizedBox(width: 8,),
-                      _buildValueInput(localizations, settings,
-                        labelText: localizations.diaLong,
-                        controller: diaController,
-                        onSaved: (value) =>
-                            setState(() => diastolic = int.tryParse(value ?? '')),
-                        focusNode: diaFocusNode,
-                        validator: (value) {
-                          if (settings.validateInputs
-                              && (int.tryParse(value ?? '') ?? 0)
-                                  >= (int.tryParse(sysController.text) ?? 1)
-                          ) {
-                            return AppLocalizations.of(context)?.errDiaGtSys;
-                          }
-                          return null;
-                        },
-                      ),
-                      const SizedBox(width: 8,),
-                      _buildValueInput(localizations, settings,
-                        labelText: localizations.pulLong,
-                        controller: pulController,
-                        focusNode: pulFocusNode,
-                        onSaved: (value) =>
-                            setState(() => pulse = int.tryParse(value ?? '')),
-                      ),
-                    ],
-                  ),
-                ],
-              ),
-            ),
-            Padding(
-              padding: const EdgeInsets.symmetric(vertical: 16),
-              child: TextFormField(
-                controller: noteController,
-                focusNode: noteFocusNode,
-                decoration: InputDecoration(
-                  labelText: localizations.addNote,
-                ),
-                minLines: 1,
-                maxLines: 4,
-              ),
-            ),
-            InputDecorator(
-              decoration: const InputDecoration(
-                contentPadding: EdgeInsets.zero,
-              ),
-              child: ColorSelectionListTile(
-                title: Text(localizations.color, style: Theme.of(context).textTheme.bodyLarge,),
-                onMainColorChanged: (Color value) => setState(() {
-                  color = (value == Colors.transparent) ? null : value;
-                }),
-                initialColor: color ?? Colors.transparent,
-              ),
-            ),
-            if (widget.initialRecord == null && widget.availableMeds.isNotEmpty)
-              Form(
-                key: medicationFormKey,
-                child: Padding(
-                  padding: const EdgeInsets.symmetric(vertical: 16),
-                  child: Row(
-                    children: [
-                      Expanded(
-                        child: DropdownButtonFormField<Medicine?>(
-                          isExpanded: true,
-                          value: selectedMed,
-                          items: [
-                            for (final med in widget.availableMeds)
-                              DropdownMenuItem(
-                                value: med,
-                                child: Text(med.designation, style: Theme.of(context).textTheme.bodyLarge,),
-                              ),
-                            DropdownMenuItem(
-                              child: Text(localizations.noMedication, style: Theme.of(context).textTheme.bodyLarge,),
-                            ),
-                          ],
-                          onChanged: (v) {
-                            setState(() {
-                              if (v != null) {
-                                _showMedicineDosisInput = true;
-                                selectedMed = v;
-                                medicineDosis = v.dosis?.mg;
-                                dosisFocusNote.requestFocus();
-                              } else {
-                                _showMedicineDosisInput = false;
-                                selectedMed = null;
-                              }
-                            });
-                          },
-                        ),
-                      ),
-                      if (_showMedicineDosisInput)
-                        const SizedBox(width: 14,),
-                      if (_showMedicineDosisInput)
-                        Expanded(
-                          child: TextFormField(
-                            initialValue: medicineDosis?.toString(),
-                            decoration: InputDecoration(
-                              labelText: localizations.dosis,
-                            ),
-                            style: Theme.of(context).textTheme.bodyLarge,
-                            focusNode: dosisFocusNote,
-                            keyboardType: TextInputType.number,
-                            onChanged: (value) {
-                              setState(() {
-                                final dosis = int.tryParse(value)?.toDouble()
-                                    ?? double.tryParse(value);
-                                if(dosis != null && dosis > 0) medicineDosis = dosis;
-                              });
-                            },
-                            inputFormatters: [FilteringTextInputFormatter.allow(
-                              RegExp(r'([0-9]+(\.([0-9]*))?)'),),],
-                            validator: (String? value) {
-                              if (!_showMedicineDosisInput) return null;
-                              if (((int.tryParse(value ?? '')?.toDouble()
-                                  ?? double.tryParse(value ?? '')) ?? 0) <= 0) {
-                                return localizations.errNaN;
-                              }
-                              return null;
-                            },
-                          ),
-                        ),
-                    ],
-                  ),
-                ),
-              ),
-
-            if (settings.weightInput)
-              ListTile(
-                title: Text(localizations.enterWeight),
-                leading: const Icon(Icons.scale),
-                trailing: const Icon(Icons.arrow_forward_ios),
-                onTap: () async {
-                  final repo = context.read<BodyweightRepository>();
-                  final weight = await showDialog<Weight>(context: context, builder: (_) => const AddBodyweightDialoge());
-                  if (weight != null) {
-                    await repo.add(BodyweightRecord(time: time, weight: weight));
-                    if (context.mounted) Navigator.pop(context);
-                  }
-                },
-              ),
-          ],
-        ),
-      ),
-    );
-  }
-}
-
-/// Shows a dialoge to input a blood pressure measurement or a medication.
-Future<FullEntry?> showAddEntryDialoge(
-  BuildContext context,
-  MedicineRepository medRepo,
-  [FullEntry? initialRecord,
-]) async {
-  final meds = await medRepo.getAll();
-  return showDialog<FullEntry>(
-    context: context, builder: (context) => AddEntryDialoge(
-      initialRecord: initialRecord,
-      availableMeds: meds,
-    ),
-  );
-}
app/lib/features/measurement_list/compact_measurement_list.dart
@@ -1,6 +1,7 @@
 import 'package:blood_pressure_app/components/nullable_text.dart';
 import 'package:blood_pressure_app/components/pressure_text.dart';
 import 'package:blood_pressure_app/data_util/entry_context.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
 import 'package:blood_pressure_app/model/storage/settings_store.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -70,7 +71,7 @@ class _CompactMeasurementListState extends State<CompactMeasurementList> {
         key: Key(widget.data[index].time.toIso8601String()),
         confirmDismiss: (direction) async {
           if (direction == DismissDirection.startToEnd) { // edit
-            await context.createEntry(widget.data[index]);
+            await context.createEntry(widget.data[index].asAddEntry);
             return false;
           } else { // delete
             await context.deleteEntry(widget.data[index]);
app/lib/features/measurement_list/measurement_list.dart
@@ -1,4 +1,5 @@
 import 'package:blood_pressure_app/data_util/entry_context.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
 import 'package:blood_pressure_app/features/measurement_list/measurement_list_entry.dart';
 import 'package:blood_pressure_app/model/storage/settings_store.dart';
 import 'package:flutter/material.dart';
@@ -70,7 +71,7 @@ class MeasurementList extends StatelessWidget {
             itemCount: entries.length,
             itemBuilder: (context, idx) => MeasurementListRow(
               data: entries[idx],
-              onRequestEdit: () => context.createEntry(entries[idx]),
+              onRequestEdit: () => context.createEntry(entries[idx].asAddEntry),
             ),
           ),
         ),
app/lib/features/measurement_list/weight_list.dart
@@ -1,4 +1,5 @@
 import 'package:blood_pressure_app/components/confirm_deletion_dialoge.dart';
+import 'package:blood_pressure_app/data_util/entry_context.dart';
 import 'package:blood_pressure_app/data_util/repository_builder.dart';
 import 'package:blood_pressure_app/model/storage/storage.dart';
 import 'package:blood_pressure_app/model/weight_unit.dart';
@@ -28,14 +29,29 @@ class WeightList extends StatelessWidget {
           itemBuilder: (context, idx) => ListTile(
             title: Text(_buildWeightText(weightUnit, records[idx].weight)),
             subtitle: Text(format.format(records[idx].time)),
-            trailing: IconButton(
-              icon: const Icon(Icons.delete),
-              onPressed: () async {
-                final repo = context.read<BodyweightRepository>();
-                if ((!context.read<Settings>().confirmDeletion) || await showConfirmDeletionDialoge(context)) {
-                  await repo.remove(records[idx]);
-                }
-              }
+            trailing: Row(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                IconButton(
+                  icon: Icon(Icons.edit),
+                  onPressed: () => context.createEntry((
+                    timestamp: records[idx].time,
+                    note: null,
+                    record: null,
+                    intake: null,
+                    weight: records[idx],
+                  )),
+                ),
+                IconButton(
+                  icon: Icon(Icons.delete),
+                  onPressed: () async {
+                    final repo = context.read<BodyweightRepository>();
+                    if ((!context.read<Settings>().confirmDeletion) || await showConfirmDeletionDialoge(context)) {
+                      await repo.remove(records[idx]);
+                    }
+                  },
+                ),
+              ],
             ),
           ),
         );
app/lib/features/old_bluetooth/bluetooth_input.dart
@@ -1,5 +1,6 @@
 import 'dart:async';
 
+import 'package:blood_pressure_app/config.dart';
 import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart' show BluetoothInput;
 import 'package:blood_pressure_app/features/old_bluetooth/logic/ble_read_cubit.dart';
 import 'package:blood_pressure_app/features/old_bluetooth/logic/bluetooth_cubit.dart';
@@ -23,25 +24,13 @@ import 'package:health_data_store/health_data_store.dart';
 /// This widget is superseded by [BluetoothInput].
 class OldBluetoothInput extends StatefulWidget {
   /// Create a measurement input through bluetooth.
-  const OldBluetoothInput({super.key,
+  OldBluetoothInput({super.key,
     required this.onMeasurement,
-    this.bluetoothCubit,
-    this.deviceScanCubit,
-    this.bleReadCubit,
-  });
+  }) : assert(!isTestingEnvironment, "OldBluetoothInput isn't maintained in tests");
 
   /// Called when a measurement was received through bluetooth.
   final void Function(BloodPressureRecord data) onMeasurement;
 
-  /// Function to customize [BluetoothCubit] creation.
-  final BluetoothCubit Function()? bluetoothCubit;
-
-  /// Function to customize [DeviceScanCubit] creation.
-  final DeviceScanCubit Function()? deviceScanCubit;
-
-  /// Function to customize [BleReadCubit] creation.
-  final BleReadCubit Function(BluetoothDevice dev)? bleReadCubit;
-
   @override
   State<OldBluetoothInput> createState() => _OldBluetoothInputState();
 }
@@ -64,7 +53,7 @@ class _OldBluetoothInputState extends State<OldBluetoothInput> with TypeLogger {
   @override
   void initState() {
     super.initState();
-    _bluetoothCubit = widget.bluetoothCubit?.call() ?? BluetoothCubit();
+    _bluetoothCubit = BluetoothCubit();
   }
 
   @override
@@ -103,7 +92,7 @@ class _OldBluetoothInputState extends State<OldBluetoothInput> with TypeLogger {
       }
     });
     final settings = context.watch<Settings>();
-    _deviceScanCubit ??= widget.deviceScanCubit?.call() ?? DeviceScanCubit(
+    _deviceScanCubit ??= DeviceScanCubit(
       service: serviceUUID,
       settings: settings,
     );
@@ -128,7 +117,7 @@ class _OldBluetoothInputState extends State<OldBluetoothInput> with TypeLogger {
             // distinction
           DeviceSelected() => BlocConsumer<BleReadCubit, BleReadState>(
             bloc: () {
-              _deviceReadCubit = widget.bleReadCubit?.call(state.device) ?? BleReadCubit(
+              _deviceReadCubit = BleReadCubit(
                 state.device,
                 characteristicUUID: characteristicUUID,
                 serviceUUID: serviceUUID,
app/lib/l10n/app_en.arb
@@ -297,7 +297,7 @@
   "@last30Days": {},
   "allowMissingValues": "Allow missing values",
   "@allowMissingValues": {},
-  "errTimeAfterNow": "The selected time of day was reset, as it occurs after this moment. You can turn off this validation in the settings.",
+  "errTimeAfterNow": "The selected time is in the future. You can turn off this validation in the settings.",
   "@errTimeAfterNow": {},
   "language": "Language",
   "@language": {},
@@ -552,5 +552,8 @@
   "newBluetoothInputCrossPlatform": "Beta cross-platform",
   "@newBluetoothInputCrossPlatform": {},
   "bluetoothInputDesc": "The beta backend works on more devices but is less tested. The cross-platform version may work on non-android and is planned to supersede the stable implementation once mature enough.",
-  "@bluetoothInputDesc": {}
+  "@bluetoothInputDesc": {},
+  "@bluetoothInputDesc": {},
+  "tapToSelect": "Tap to select",
+  "@tapToSelect": {}
 }
app/lib/model/storage/settings_store.dart
@@ -411,7 +411,7 @@ class Settings extends ChangeNotifier {
     notifyListeners();
   }
 
-  BluetoothInputMode _bleInput = BluetoothInputMode.oldBluetoothInput;
+  BluetoothInputMode _bleInput = BluetoothInputMode.disabled;
   /// Whether to show bluetooth input on add measurement page.
   BluetoothInputMode get bleInput => _bleInput;
   set bleInput(BluetoothInputMode value) {
app/test/features/input/forms/add_entry_form_test.dart
@@ -0,0 +1,431 @@
+import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart';
+import 'package:blood_pressure_app/features/bluetooth/logic/bluetooth_cubit.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
+import 'package:blood_pressure_app/features/input/forms/blood_pressure_form.dart';
+import 'package:blood_pressure_app/features/input/forms/date_time_form.dart';
+import 'package:blood_pressure_app/features/input/forms/medicine_intake_form.dart';
+import 'package:blood_pressure_app/features/input/forms/note_form.dart';
+import 'package:blood_pressure_app/features/input/forms/weight_form.dart';
+import 'package:blood_pressure_app/features/old_bluetooth/bluetooth_input.dart';
+import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+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 'package:intl/intl.dart';
+
+import '../../../model/analyzer_test.dart';
+import '../../../util.dart';
+import '../../measurement_list/measurement_list_entry_test.dart';
+
+void main() {
+  group('shows sub-forms depending on settings', () {
+    // always show NoteForm, BloodPressureForm
+    
+    testWidgets('show TimeForm if and only if setting is set (default true)', (tester) async {
+      final settings = Settings();
+      await tester.pumpWidget(materialApp(AddEntryForm(meds: []), settings: settings));
+
+      expect(find.byType(TabBar, skipOffstage: false), findsNothing);
+      expect(find.byType(NoteForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(BloodPressureForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(DateTimeForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(WeightForm, skipOffstage: false), findsNothing);
+      expect(find.byType(MedicineIntakeForm, skipOffstage: false), findsNothing);
+      expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+      expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+      
+      settings.allowManualTimeInput = false;
+      await tester.pumpAndSettle();
+
+      expect(find.byType(DateTimeForm, skipOffstage: false), findsNothing);
+    });
+    
+    testWidgets('show WeightForm if and only if setting is set', (tester) async {
+      final settings = Settings(weightInput: true);
+      await tester.pumpWidget(materialApp(AddEntryForm(meds: []), settings: settings));
+
+      expect(find.byType(TabBar, skipOffstage: false), findsOneWidget);
+      expect(find.byType(NoteForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(BloodPressureForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(DateTimeForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(WeightForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(MedicineIntakeForm, skipOffstage: false), findsNothing);
+      expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+      expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+
+      settings.weightInput = false;
+      await tester.pumpAndSettle();
+
+      expect(find.byType(WeightForm, skipOffstage: false), findsNothing);
+    });
+
+    testWidgets('show MedicineIntakeForm if medicines are available', (tester) async {
+      await tester.pumpWidget(materialApp(AddEntryForm(meds: [mockMedicine()])));
+
+      expect(find.byType(TabBar, skipOffstage: false), findsOneWidget);
+      expect(find.byType(NoteForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(BloodPressureForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(DateTimeForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(WeightForm, skipOffstage: false), findsNothing);
+      expect(find.byType(MedicineIntakeForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+      expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+    });
+
+    testWidgets('show the BluetoothInput specified by setting', (tester) async {
+      final settings = Settings(bleInput: BluetoothInputMode.disabled);
+      await tester.pumpWidget(materialApp(AddEntryForm(
+        bluetoothCubit: _MockBluetoothCubit.new
+      ), settings: settings));
+
+      expect(find.byType(TabBar, skipOffstage: false), findsNothing);
+      expect(find.byType(NoteForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(BloodPressureForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(DateTimeForm, skipOffstage: false), findsOneWidget);
+      expect(find.byType(WeightForm, skipOffstage: false), findsNothing);
+      expect(find.byType(MedicineIntakeForm, skipOffstage: false), findsNothing);
+      expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+      expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+
+      settings.bleInput = BluetoothInputMode.newBluetoothInputOldLib;
+      await tester.pumpAndSettle();
+
+      expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+      expect(find.byType(BluetoothInput, skipOffstage: false), findsOneWidget);
+
+      settings.bleInput = BluetoothInputMode.disabled;
+      await tester.pumpAndSettle();
+
+      expect(find.byType(OldBluetoothInput, skipOffstage: false), findsNothing);
+      expect(find.byType(BluetoothInput, skipOffstage: false), findsNothing);
+    });
+  });
+
+  testWidgets('saves all entered values', (tester) async {
+    final med1 = mockMedicine(color: Colors.blue, designation: 'med123', defaultDosis: 3.14);
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key, meds: [med1]),
+      settings: Settings(weightInput: true)
+    ));
+
+    final fields = find.byType(TextField);
+    await tester.enterText(fields.at(0), '123'); // sys
+    await tester.enterText(fields.at(1), '45'); // dia
+    await tester.enterText(fields.at(2), '67'); // pul
+
+    await tester.tap(find.byIcon(Icons.medication_outlined));
+    await tester.pumpAndSettle();
+    await tester.tap(find.text(med1.designation)); // med
+    await tester.pumpAndSettle();
+
+    await tester.tap(find.byIcon(Icons.scale));
+    await tester.pumpAndSettle();
+    await tester.enterText(find.byType(TextField).first, '65.4'); // weight
+
+    await tester.enterText(find.descendant(
+        of: find.byType(NoteForm),
+        matching: find.byType(TextField),
+    ), 'some note'); // note text
+    await tester.pumpAndSettle();
+
+    expect(key.currentState!.validate(), true);
+    final res = key.currentState!.save();
+    expect(res?.record?.sys?.mmHg, 123);
+    expect(res?.record?.dia?.mmHg, 45);
+    expect(res?.record?.pul, 67);
+    expect(res?.intake?.medicine, med1);
+    expect(res?.intake?.dosis, med1.dosis);
+    expect(res?.weight?.weight.kg, 65.4);
+    expect(res?.note?.note, 'some note');
+    expect(res?.note?.color, isNull);
+  });
+
+  testWidgets('saves partially entered values (blood pressure)', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+
+    final fields = find.byType(TextField);
+    await tester.enterText(fields.at(0), '123'); // sys
+    await tester.enterText(fields.at(1), '45'); // dia
+    await tester.enterText(fields.at(2), '67'); // pul
+
+    expect(key.currentState!.validate(), true);
+    final res = key.currentState!.save();
+    expect(res?.record?.sys?.mmHg, 123);
+    expect(res?.record?.dia?.mmHg, 45);
+    expect(res?.record?.pul, 67);
+    expect(res?.intake, isNull);
+    expect(res?.note, isNull);
+  });
+
+  testWidgets('saves partially entered values (note)', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+
+    await tester.enterText(find.descendant(
+      of: find.byType(NoteForm),
+      matching: find.byType(TextField),
+    ), 'some note'); // note text
+    await tester.pumpAndSettle();
+
+    expect(key.currentState!.validate(), true);
+    final res = key.currentState!.save();
+    expect(res?.record, isNull);
+    expect(res?.intake, isNull);
+    expect(res?.weight, isNull);
+    expect(res?.note?.note, 'some note');
+  });
+
+  testWidgets('saves partially entered values (intake)', (tester) async {
+    final med1 = mockMedicine(color: Colors.blue, designation: 'med123', defaultDosis: 3.14);
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key, meds: [med1])));
+
+    await tester.tap(find.byIcon(Icons.medication_outlined));
+    await tester.pumpAndSettle();
+    await tester.tap(find.text(med1.designation)); // med
+    await tester.pumpAndSettle();
+
+    expect(key.currentState!.validate(), true);
+    final res = key.currentState!.save();
+    expect(res?.record, isNull);
+    expect(res?.weight, isNull);
+    expect(res?.note, isNull);
+    expect(res?.intake?.medicine, med1);
+    expect(res?.intake?.dosis, med1.dosis);
+  });
+
+  testWidgets('initializes timestamp correctly', (tester) async {
+    final dateFormatter = DateFormat('yyyy-MM-dd');
+    final timeFormatter = DateFormat('HH:mm');
+
+    final start = DateTime.now();
+    await tester.pumpWidget(materialApp(AddEntryForm(meds: [])));
+
+    expect(find.text(dateFormatter.format(start)), findsOneWidget);
+    final allowedTimes = anyOf(timeFormatter.format(start), timeFormatter.format(start.add(Duration(minutes: 1))));
+    expect(find.byWidgetPredicate(
+            (w) => w is Text && allowedTimes.matches(w.data, {})),
+        findsOneWidget);
+  });
+
+  testWidgets('validates time form', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    final time = DateTime.now();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+    expect(key.currentState?.validate(), true);
+
+    key.currentState!.fillForm((
+      timestamp: time.add(Duration(hours: 1)),
+      intake: null, note: null, record: null, weight: null,
+    ));
+    await tester.pump();
+    expect(key.currentState?.validate(), false);
+  });
+
+  testWidgets('validates bp form', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+    expect(key.currentState?.validate(), true);
+
+    key.currentState!.fillForm((
+      timestamp: DateTime.now(),
+      record: mockRecord(sys: 123123),
+      note: null, intake: null, weight: null,
+    ));
+    await tester.pump();
+    expect(key.currentState?.validate(), false);
+  });
+
+  testWidgets('validates weight form', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key),
+      settings: Settings(weightInput: true),
+    ));
+    expect(key.currentState?.validate(), true);
+
+    await tester.tap(find.byIcon(Icons.scale));
+    await tester.pumpAndSettle();
+    await tester.enterText(find.byType(TextField).first, ',.,');
+    await tester.pump();
+    expect(key.currentState?.validate(), false);
+  });
+
+  testWidgets('validates intake form', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    final med = mockMedicine(designation: 'testmed');
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key, meds: [med])));
+    expect(key.currentState?.validate(), true);
+
+    await tester.tap(find.byIcon(Icons.medication_outlined));
+    await tester.pumpAndSettle();
+    await tester.tap(find.text(med.designation));
+    await tester.pump();
+    await tester.enterText(find.byType(TextField).first, ',.,');
+    await tester.pump();
+    expect(key.currentState?.validate(), false);
+  });
+
+  testWidgets('saves initial values as is', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    final med = mockMedicine(designation: 'somemed123');
+    final intake = mockIntake(med);
+    final value = (
+      timestamp: intake.time,
+      intake: intake,
+      note: Note(time: intake.time, note: '123test', color: Colors.teal.value),
+      record: mockRecord(time: intake.time, sys: 123, dia: 45, pul: 67),
+      weight: BodyweightRecord(time: intake.time, weight: Weight.kg(123.45))
+    );
+    await tester.pumpWidget(materialApp(AddEntryForm(
+      key: key,
+      meds: [med],
+      initialValue: value,
+    )));
+    await tester.pumpAndSettle();
+
+    expect(key.currentState?.validate(), true);
+    expect(key.currentState?.save(), isA<AddEntryFormValue>()
+      .having((e) => e.timestamp, 'timestamp', value.timestamp)
+      .having((e) => e.intake, 'intake', value.intake)
+      .having((e) => e.record, 'record', value.record)
+      .having((e) => e.weight, 'weight', value.weight)
+      .having((e) => e.note, 'note', value.note));
+  });
+
+  testWidgets('saves loaded values as is', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    final med = mockMedicine(designation: 'somemed123');
+    final intake = mockIntake(med);
+    final value = (
+      timestamp: intake.time,
+      intake: intake,
+      note: Note(time: intake.time, note: '123test', color: Colors.teal.value),
+      record: mockRecord(time: intake.time, sys: 123, dia: 45, pul: 67),
+      weight: BodyweightRecord(time: intake.time, weight: Weight.kg(123.45))
+    );
+    await tester.pumpWidget(materialApp(AddEntryForm(
+      key: key,
+      meds: [med],
+    )));
+    await tester.pumpAndSettle();
+    key.currentState!.fillForm(value);
+    await tester.pumpAndSettle();
+
+    expect(key.currentState?.validate(), true);
+    expect(key.currentState?.save(), isA<AddEntryFormValue>()
+        .having((e) => e.timestamp, 'timestamp', value.timestamp)
+        .having((e) => e.intake, 'intake', value.intake)
+        .having((e) => e.record, 'record', value.record)
+        .having((e) => e.weight, 'weight', value.weight)
+        .having((e) => e.note, 'note', value.note));
+  });
+
+  testWidgets("doesn't save empty forms", (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+    await tester.pumpAndSettle();
+    expect(key.currentState!.validate(), true);
+    expect(key.currentState!.save(), isNull);
+  });
+
+  testWidgets('focuses last input field on backspace', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    final fields = find.byType(TextField);
+    await tester.enterText(fields.at(0), '123'); // sys
+    await tester.enterText(fields.at(1), '45'); // dia
+    await tester.enterText(fields.at(2), '67'); // pul
+
+    await tester.tap(find.text('67'));
+
+    Finder focusedTextField() {
+      final firstFocused = FocusManager.instance.primaryFocus;
+      expect(firstFocused?.context?.widget, isNotNull);
+      return find.ancestor(
+        of: find.byWidget(FocusManager.instance.primaryFocus!.context!.widget),
+        matching: find.byType(TextField),
+      );
+    }
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.sysLong)), findsNothing);
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.diaLong)), findsNothing);
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.pulLong)), findsOneWidget);
+
+    await tester.enterText(focusedTextField(), '');
+    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+    await tester.pump();
+
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.sysLong)), findsNothing);
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.diaLong)), findsOneWidget);
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.pulLong)), findsNothing);
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+    await tester.pump();
+
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.sysLong)), findsOneWidget);
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.diaLong)), findsNothing);
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.pulLong)), findsNothing);
+
+    // doesn't focus last input on backspace if the current is still filled
+    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+    await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
+    await tester.pump();
+
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.sysLong)), findsOneWidget);
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.diaLong)), findsNothing);
+    expect(find.descendant(of: focusedTextField(), matching: find.text(localizations.pulLong)), findsNothing);
+  });
+
+  testWidgets('should allow invalid values when setting is set', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key),
+      settings: Settings(validateInputs: false)),
+    );
+
+    final fields = find.byType(TextField);
+    await tester.enterText(fields.at(0), '12'); // sys
+    await tester.enterText(fields.at(1), '450'); // dia
+    await tester.enterText(fields.at(2), '67123'); // pul
+
+    expect(key.currentState!.validate(), true);
+    final res = key.currentState!.save();
+    expect(res?.record?.sys?.mmHg, 12);
+    expect(res?.record?.dia?.mmHg, 450);
+    expect(res?.record?.pul, 67123);
+  });
+
+  testWidgets('starts with sys input focused', (tester) async {
+    final key = GlobalKey<AddEntryFormState>();
+    await tester.pumpWidget(materialApp(AddEntryForm(key: key)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    await tester.pump();
+    expect(find.descendant(
+      of: find.ancestor(
+        of: find.byWidget(FocusManager.instance.primaryFocus!.context!.widget),
+        matching: find.byType(TextField)
+      ),
+      matching: find.text(localizations.sysLong)
+    ), findsOneWidget);
+  });
+}
+
+class _MockBluetoothCubit extends Fake implements BluetoothCubit {
+  @override
+  Future<void> close() async {}
+
+  @override
+  BluetoothState get state => BluetoothStateDisabled();
+
+  @override
+  Stream<BluetoothState> get stream => Stream.empty();
+}
app/test/features/input/forms/blood_pressure_form_test.dart
@@ -0,0 +1,89 @@
+import 'package:blood_pressure_app/features/input/forms/blood_pressure_form.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../../util.dart';
+
+void main() {
+  testWidgets('saves entered values', (WidgetTester tester) async {
+    final key = GlobalKey<BloodPressureFormState>();
+    await tester.pumpWidget(materialApp(BloodPressureForm(key: key)));
+    await tester.pumpAndSettle();
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.text(localizations.sysLong), findsOneWidget);
+    expect(find.text(localizations.diaLong), findsOneWidget);
+    expect(find.text(localizations.pulLong), findsOneWidget);
+    expect(find.byType(TextField), findsNWidgets(3));
+    expect(key.currentState?.validate(), true);
+
+    await tester.enterText(find.ancestor(of: find.text(localizations.sysLong), matching: find.byType(TextField)), '123');
+    await tester.enterText(find.ancestor(of: find.text(localizations.diaLong), matching: find.byType(TextField)), '67');
+    await tester.enterText(find.ancestor(of: find.text(localizations.pulLong), matching: find.byType(TextField)), '89');
+
+    expect(key.currentState?.validate(), true);
+    expect(key.currentState?.save(), (sys: 123, dia: 67, pul: 89));
+  });
+
+  testWidgets('shows errors on bad inputs', (WidgetTester tester) async {
+    final key = GlobalKey<BloodPressureFormState>();
+    await tester.pumpWidget(materialApp(BloodPressureForm(key: key)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.text(localizations.errNaN), findsNothing);
+
+    await tester.enterText(find.byType(TextField).first, '..,..');
+    await tester.pump();
+    expect(find.text('..,..'), findsNothing);
+
+    expect(find.text(localizations.errNaN), findsNothing);
+    expect(find.text(localizations.errLt30), findsNothing);
+
+    await tester.enterText(find.byType(TextField).first, '13');
+    await tester.pump();
+    expect(key.currentState!.validate(), isFalse);
+    await tester.pumpAndSettle();
+
+    expect(find.text(localizations.errLt30), findsOneWidget);
+    expect(find.text(localizations.errUnrealistic), findsNothing);
+
+    await tester.enterText(find.byType(TextField).first, '500');
+    await tester.pump();
+    expect(key.currentState!.validate(), isFalse);
+    await tester.pumpAndSettle();
+
+    expect(find.text(localizations.errLt30), findsNothing);
+    expect(find.text(localizations.errUnrealistic), findsOneWidget);
+
+    await tester.enterText(find.ancestor(of: find.text(localizations.sysLong), matching: find.byType(TextField)), '123');
+    await tester.enterText(find.ancestor(of: find.text(localizations.diaLong), matching: find.byType(TextField)), '67');
+    await tester.enterText(find.ancestor(of: find.text(localizations.pulLong), matching: find.byType(TextField)), '');
+    await tester.pump();
+    expect(key.currentState!.validate(), isFalse);
+    await tester.pumpAndSettle();
+
+    expect(find.text(localizations.errUnrealistic), findsNothing);
+    expect(find.text(localizations.errNaN), findsOneWidget, reason: 'pul is null');
+
+    await tester.enterText(find.ancestor(of: find.text(localizations.sysLong), matching: find.byType(TextField)), '90');
+    await tester.enterText(find.ancestor(of: find.text(localizations.diaLong), matching: find.byType(TextField)), '130');
+    await tester.enterText(find.ancestor(of: find.text(localizations.pulLong), matching: find.byType(TextField)), '89');
+    await tester.pump();
+    expect(key.currentState!.validate(), isFalse);
+    await tester.pumpAndSettle();
+
+    expect(find.text(localizations.errUnrealistic), findsNothing);
+    expect(find.text(localizations.errDiaGtSys), findsOneWidget);
+  });
+
+  testWidgets('loads initial values', (WidgetTester tester) async {
+    await tester.pumpWidget(materialApp(BloodPressureForm(
+      initialValue: (sys: 123, dia: 67, pul: 89),
+    )));
+    await tester.pumpAndSettle();
+    expect(find.text('123'), findsOneWidget);
+    expect(find.text('67'), findsOneWidget);
+    expect(find.text('89'), findsOneWidget);
+  });
+}
app/test/features/input/forms/date_time_form_test.dart
@@ -0,0 +1,100 @@
+import 'package:blood_pressure_app/features/input/forms/date_time_form.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:intl/intl.dart';
+
+import '../../../util.dart';
+
+void main() {
+  testWidgets('saves entered values', (WidgetTester tester) async {
+    final key = GlobalKey<DateTimeFormState>();
+    final initialTime = DateTime(2025,02,28,10,10);
+    await tester.pumpWidget(materialApp(DateTimeForm(key: key, initialValue: initialTime)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.text(localizations.date), findsOneWidget);
+    expect(find.text(localizations.time), findsOneWidget);
+    expect(key.currentState!.validate(), true);
+    expect(find.byType(InputDecorator), findsNWidgets(2));
+
+    final dateFormated = DateFormat('yyyy-MM-dd').format(initialTime);
+    final timeOfDayFormated = DateFormat('HH:mm').format(initialTime);
+    expect(find.text(dateFormated), findsOneWidget);
+    expect(find.text(timeOfDayFormated), findsOneWidget);
+
+    await tester.tap(find.text(dateFormated));
+    await tester.pumpAndSettle();
+    expect(find.byType(DatePickerDialog), findsOneWidget);
+    await tester.tap(find.text('19'));
+    await tester.pumpAndSettle();
+    await tester.tap(find.text('OK'));
+    await tester.pumpAndSettle();
+
+    expect(find.text(dateFormated), findsNothing);
+    expect(find.text(DateFormat('yyyy-MM-dd').format(DateTime(2025,02,19))), findsOneWidget);
+    expect(find.text(timeOfDayFormated), findsOneWidget);
+
+    await tester.tap(find.text(timeOfDayFormated));
+    await tester.pumpAndSettle();
+    expect(find.byType(TimePickerDialog), findsOneWidget);
+    final dialCenter = tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
+    await tester.tapAt(Offset(dialCenter.dx, dialCenter.dy + 10)); // 6 AM
+    await tester.pumpAndSettle();
+    await tester.tapAt(Offset(dialCenter.dx - 10, dialCenter.dy));  // 45
+    await tester.pumpAndSettle();
+    await tester.tap(find.text('OK'));
+    await tester.pumpAndSettle();
+
+    expect(find.text(DateFormat('yyyy-MM-dd').format(DateTime(2025,02,19))), findsOneWidget);
+    expect(find.text(timeOfDayFormated), findsNothing);
+    expect(find.text(DateFormat('HH:mm').format(DateTime(2025,2,19,6,45))), findsOneWidget);
+
+    expect(key.currentState!.validate(), true);
+    expect(key.currentState!.save(), DateTime(2025,2,19,6,45));
+  });
+
+  testWidgets('shows errors on bad inputs', (WidgetTester tester) async {
+    final key = GlobalKey<DateTimeFormState>();
+    final initialTime = DateTime.now();
+    await tester.pumpWidget(materialApp(DateTimeForm(key: key, initialValue: initialTime)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.text(localizations.errTimeAfterNow), findsNothing);
+
+    await tester.tap(find.text(DateFormat('HH:mm').format(initialTime)));
+    await tester.pumpAndSettle();
+    final dialCenter = tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
+    await tester.tapAt(Offset(dialCenter.dx - 3, dialCenter.dy - 9.5));
+    await tester.tap(find.text('PM')); // 11 PM
+    await tester.pumpAndSettle();
+    await tester.tapAt(Offset(dialCenter.dx - 3, dialCenter.dy - 9.5)); // 55
+    await tester.tap(find.text('OK'));
+    await tester.pumpAndSettle();
+
+    expect(key.currentState!.save(), null);
+    expect(key.currentState!.validate(), false);
+    await tester.pumpAndSettle();
+    expect(find.text(localizations.errTimeAfterNow), findsOneWidget);
+  });
+
+  testWidgets('loads values into fields', (WidgetTester tester) async {
+    final key = GlobalKey<DateTimeFormState>();
+    final initialTime = DateTime(2025,02,28,10,10);
+    final newTime = DateTime(2022,11,1,2,3);
+    await tester.pumpWidget(materialApp(DateTimeForm(key: key, initialValue: initialTime)));
+
+    expect(find.text(DateFormat('yyyy-MM-dd').format(initialTime)), findsOneWidget);
+    expect(find.text(DateFormat('HH:mm').format(initialTime)), findsOneWidget);
+    expect(find.text(DateFormat('yyyy-MM-dd').format(newTime)), findsNothing);
+    expect(find.text(DateFormat('HH:mm').format(newTime)), findsNothing);
+
+    key.currentState!.fillForm(DateTime(2022,11,1,2,3));
+    await tester.pumpAndSettle();
+
+    expect(find.text(DateFormat('yyyy-MM-dd').format(initialTime)), findsNothing);
+    expect(find.text(DateFormat('HH:mm').format(initialTime)), findsNothing);
+    expect(find.text(DateFormat('yyyy-MM-dd').format(newTime)), findsOneWidget);
+    expect(find.text(DateFormat('HH:mm').format(newTime)), findsOneWidget);
+  });
+}
app/test/features/input/forms/form_switcher_test.dart
@@ -0,0 +1,72 @@
+import 'package:blood_pressure_app/features/input/forms/form_switcher.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:inline_tab_view/inline_tab_view.dart';
+
+void main() {
+  testWidgets('shows a InlineTabView', (tester) async {
+    await tester.pumpWidget(MaterialApp(
+        home: Scaffold(
+            body: FormSwitcher(
+      subForms: [
+        (SizedBox(width: 1, height: 1), SizedBox(width: 2, height: 2))
+      ],
+    ))));
+
+    expect(find.byType(TabBar), findsNothing, reason: 'only one tab present');
+    expect(find.byType(TabBarView), findsNothing);
+    expect(find.byType(InlineTabView), findsNothing, reason: 'only one tab present');
+  });
+
+  testWidgets('shows all passed tabs in TabBar', (tester) async {
+    await tester.pumpWidget(MaterialApp(
+        home: Scaffold(
+            body: FormSwitcher(
+      subForms: [
+        (Text('Tab 1'), SizedBox(width: 2, height: 2)),
+        (Text('Tab 2'), SizedBox(width: 2, height: 2)),
+        (Text('Tab 3'), SizedBox(width: 2, height: 2)),
+        (Text('Tab 4'), SizedBox(width: 2, height: 2)),
+      ],
+    ))));
+
+    expect(find.text('Tab 1'), findsOneWidget);
+    expect(find.text('Tab 2'), findsOneWidget);
+    expect(find.text('Tab 3'), findsOneWidget);
+    expect(find.text('Tab 4'), findsOneWidget);
+  });
+
+  testWidgets('associates title and widget correctly', (tester) async {
+    await tester.pumpWidget(MaterialApp(
+        home: Scaffold(
+            body: FormSwitcher(
+      subForms: [
+        (Text('Tab 1'), Text('Content 1')),
+        (Text('Tab 2'), Text('Content 2')),
+        (Text('Tab 3'), Text('Content 3')),
+        (Text('Tab 4'), Text('Content 4')),
+      ],
+    ))));
+
+    expect(find.text('Content 1'), findsOneWidget);
+    expect(find.text('Content 2'), findsNothing);
+    expect(find.text('Content 3'), findsNothing);
+    expect(find.text('Content 4'), findsNothing);
+
+    await tester.tap(find.text('Tab 2'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Content 1'), findsNothing);
+    expect(find.text('Content 2'), findsOneWidget);
+    expect(find.text('Content 3'), findsNothing);
+    expect(find.text('Content 4'), findsNothing);
+
+    await tester.tap(find.text('Tab 4'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Content 1'), findsNothing);
+    expect(find.text('Content 2'), findsNothing);
+    expect(find.text('Content 3'), findsNothing);
+    expect(find.text('Content 4'), findsOneWidget);
+  });
+}
app/test/features/input/forms/medicine_intake_form_test.dart
@@ -0,0 +1,114 @@
+import 'package:blood_pressure_app/features/input/forms/medicine_intake_form.dart';
+import 'package:flutter/material.dart';
+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 '../../../util.dart';
+
+void main() {
+  testWidgets('shows input list on single med', (WidgetTester tester) async {
+    final mockMed = mockMedicine(designation: 'monozernorditrocin');
+    await tester.pumpWidget(materialApp(MedicineIntakeForm(meds: [mockMed])));
+    final localizations = await AppLocalizations.delegate.load(Locale('en'));
+
+    expect(find.byType(TextField), findsNothing);
+    expect(find.text('monozernorditrocin'), findsOneWidget);
+    expect(find.text(localizations.tapToSelect), findsOneWidget);
+  });
+
+  testWidgets('shows input list on multiple meds', (WidgetTester tester) async {
+    final med1 = mockMedicine(designation: 'tetraebraphthyme');
+    final med2 = mockMedicine(designation: 'hypovonyhensas');
+    await tester.pumpWidget(materialApp(MedicineIntakeForm(meds: [med1,med2])));
+    final localizations = await AppLocalizations.delegate.load(Locale('en'));
+
+    expect(find.text('tetraebraphthyme'), findsOneWidget);
+    expect(find.text('hypovonyhensas'), findsOneWidget);
+    expect(find.byType(TextField), findsNothing);
+    expect(find.byIcon(Icons.close), findsNothing);
+    expect(find.text(localizations.tapToSelect), findsNothing);
+
+    await tester.tap(find.text('hypovonyhensas'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('tetraebraphthyme'), findsNothing);
+    expect(find.text('hypovonyhensas'), findsOneWidget);
+    expect(find.byType(TextField), findsOneWidget);
+    expect(find.byIcon(Icons.close), findsOneWidget);
+
+    await tester.tap(find.byIcon(Icons.close));
+    await tester.pumpAndSettle();
+
+    expect(find.text('tetraebraphthyme'), findsOneWidget);
+    expect(find.text('hypovonyhensas'), findsOneWidget);
+    expect(find.byType(TextField), findsNothing);
+    expect(find.byIcon(Icons.close), findsNothing);
+
+    await tester.tap(find.text('tetraebraphthyme'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('tetraebraphthyme'), findsOneWidget);
+    expect(find.text('hypovonyhensas'), findsNothing);
+    expect(find.byType(TextField), findsOneWidget);
+    expect(find.byIcon(Icons.close), findsOneWidget);
+  });
+
+  testWidgets('returns entered values', (WidgetTester tester) async {
+    final med1 = mockMedicine(designation: 'tetraebraphthyme');
+    final med2 = mockMedicine(designation: 'hypovonyhensas');
+    final key = GlobalKey<MedicineIntakeFormState>();
+
+    await tester.pumpWidget(materialApp(MedicineIntakeForm(
+      meds: [med1,med2],
+      key: key,
+    )));
+
+    expect(key.currentState!.validate(), isTrue);
+    expect(key.currentState!.save(), isNull);
+
+    await tester.tap(find.text(med1.designation));
+    await tester.pumpAndSettle();
+    await tester.enterText(find.byType(TextField), ',..,');
+
+    expect(key.currentState!.validate(), isFalse);
+    expect(key.currentState!.save(), isNull);
+
+    await tester.enterText(find.byType(TextField), '3.14');
+    expect(key.currentState!.validate(), isTrue);
+    expect(key.currentState!.save(), (med1, Weight.mg(3.14)));
+  });
+
+  testWidgets('prefills values when selecting med', (WidgetTester tester) async {
+    final med1 = mockMedicine(designation: 'tetraebraphthyme', defaultDosis: 3.141);
+    final med2 = mockMedicine(designation: 'hypovonyhensas');
+    await tester.pumpWidget(materialApp(MedicineIntakeForm(meds: [med1,med2])));
+
+    await tester.tap(find.text(med1.designation));
+    await tester.pumpAndSettle();
+
+    expect(find.text(med1.dosis!.mg.toString()), findsOneWidget);
+  });
+
+  testWidgets('returns passed values on edit', (WidgetTester tester) async {
+    final med1 = mockMedicine(designation: 'tetraebraphthyme');
+    final med2 = mockMedicine(designation: 'hypovonyhensas');
+    final key = GlobalKey<MedicineIntakeFormState>();
+
+    await tester.pumpWidget(materialApp(MedicineIntakeForm(
+      meds: [med1,med2],
+      key: key,
+      initialValue: (med2, Weight.mg(3.141)),
+    )));
+    await tester.pumpAndSettle();
+
+    expect(find.text(med2.designation), findsOneWidget);
+    expect(find.text('3.141'), findsOneWidget);
+    expect(find.byType(TextField), findsOneWidget);
+    expect(find.byIcon(Icons.close), findsOneWidget);
+
+    expect(key.currentState!.validate(), isTrue);
+    expect(key.currentState!.save(), (med2, Weight.mg(3.141)));
+  });
+
+}
app/test/features/input/forms/note_form_test.dart
@@ -0,0 +1,64 @@
+import 'package:blood_pressure_app/features/input/forms/note_form.dart';
+import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../../util.dart';
+import '../../settings/tiles/color_picker_list_tile_test.dart';
+
+void main() {
+  testWidgets('saves entered text', (WidgetTester tester) async {
+    final key = GlobalKey<NoteFormState>();
+    await tester.pumpWidget(materialApp(NoteForm(key: key)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.text(localizations.addNote), findsOneWidget);
+    expect(key.currentState!.validate(), true);
+
+    await tester.enterText(find.byType(TextField), 'some test note!');
+
+    expect(key.currentState!.validate(), true);
+    expect(key.currentState!.save(), ('some test note!', null));
+  });
+
+  testWidgets('saves entered color and text', (WidgetTester tester) async {
+    final key = GlobalKey<NoteFormState>();
+    await tester.pumpWidget(materialApp(NoteForm(key: key)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.text(localizations.addNote), findsOneWidget);
+    expect(key.currentState!.validate(), true);
+
+    await tester.enterText(find.byType(TextField), 'some test note!');
+    await tester.tap(find.byType(ColorSelectionListTile));
+    await tester.pump();
+    await tester.tap(find.byElementPredicate(findColored(Colors.red)));
+
+    expect(key.currentState!.validate(), true);
+    expect(key.currentState!.save(), ('some test note!', Colors.red));
+  });
+
+  testWidgets('loads initial values', (WidgetTester tester) async {
+    await tester.pumpWidget(materialApp(NoteForm(
+      initialValue: ('Some note text from test', Colors.cyan),
+    )));
+    await tester.pumpAndSettle();
+    expect(find.text('Some note text from test'), findsOneWidget);
+    await tester.tap(find.byElementPredicate(findColored(Colors.cyan)));
+  });
+
+  testWidgets('saves only filled inputs', (WidgetTester tester) async {
+    final key = GlobalKey<NoteFormState>();
+    await tester.pumpWidget(materialApp(NoteForm(key: key)));
+    expect(key.currentState!.save(), isNull);
+  });
+
+  testWidgets('saves prefilled inputs', (WidgetTester tester) async {
+    final v = ('Some note text from test', Colors.cyan);
+
+    final key = GlobalKey<NoteFormState>();
+    await tester.pumpWidget(materialApp(NoteForm(key: key, initialValue: v)));
+    expect(key.currentState!.save(), v);
+  });
+}
app/test/features/input/forms/weight_form_test.dart
@@ -0,0 +1,51 @@
+import 'package:blood_pressure_app/features/input/forms/weight_form.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../../util.dart';
+
+void main() {
+  testWidgets('saves entered values', (WidgetTester tester) async {
+    final key = GlobalKey<WeightFormState>();
+    await tester.pumpWidget(materialApp(WeightForm(key: key)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.text(localizations.weight), findsOneWidget);
+    expect(find.text(Settings().weightUnit.name), findsOneWidget);
+    expect(key.currentState!.validate(), true);
+
+    await tester.enterText(find.byType(TextField), '314.15');
+
+    expect(key.currentState!.validate(), true);
+    expect(key.currentState!.save(), Settings().weightUnit.store(314.15));
+  });
+
+  testWidgets('shows errors on bad inputs', (WidgetTester tester) async {
+    final key = GlobalKey<WeightFormState>();
+    await tester.pumpWidget(materialApp(WeightForm(key: key)));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.text(localizations.errNaN), findsNothing);
+
+    await tester.enterText(find.byType(TextField), '..,..');
+    expect(key.currentState!.validate(), false);
+    await tester.pumpAndSettle();
+    expect(find.text(localizations.errNaN), findsOneWidget);
+  });
+
+  testWidgets('loads initial values', (WidgetTester tester) async {
+    await tester.pumpWidget(materialApp(WeightForm(
+      initialValue: Settings().weightUnit.store(123.45),
+    )));
+    await tester.pumpAndSettle();
+    expect(find.text('123.45'), findsOneWidget);
+  });
+
+  testWidgets('saves only filled inputs', (WidgetTester tester) async {
+    final key = GlobalKey<WeightFormState>();
+    await tester.pumpWidget(materialApp(WeightForm(key: key)));
+    expect(key.currentState!.save(), isNull);
+  });
+}
app/test/features/input/add_bodyweight_dialoge_test.dart
@@ -1,68 +0,0 @@
-import 'package:blood_pressure_app/features/input/add_bodyweight_dialoge.dart';
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:blood_pressure_app/model/weight_unit.dart';
-import 'package:flutter/material.dart';
-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 '../../util.dart';
-
-void main() {
-  testWidgets('shows weight input weight', (tester) async {
-    await tester.pumpWidget(materialApp(const AddBodyweightDialoge()));
-    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
-    expect(find.byType(TextFormField), findsOneWidget);
-    expect(find.text(localizations.weight), findsOneWidget);
-    expect(find.text('kg'), findsOneWidget);
-
-    await tester.enterText(find.byType(TextFormField), '123.45');
-  });
-  testWidgets('error on invalid input', (tester) async {
-    await tester.pumpWidget(materialApp(const AddBodyweightDialoge()));
-    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
-    expect(find.text(localizations.errNaN), findsNothing);
-
-    await tester.enterText(find.byType(TextFormField), 'invalid input');
-    await tester.testTextInput.receiveAction(TextInputAction.done);
-    await tester.pumpAndSettle();
-
-    expect(find.text(localizations.errNaN), findsOneWidget);
-  });
-  testWidgets('creates weight from input', (tester) async {
-    Weight? res;
-    await tester.pumpWidget(materialApp(Builder(
-      builder: (context) => GestureDetector(
-        onTap: () async => res = await showDialog<Weight>(context: context, builder: (_) => const AddBodyweightDialoge()),
-        child: const Text('X'),
-      ),
-    )));
-    await tester.tap(find.text('X'));
-    await tester.pumpAndSettle();
-
-    await tester.enterText(find.byType(TextFormField), '123.45');
-    await tester.testTextInput.receiveAction(TextInputAction.done);
-    await tester.pumpAndSettle();
-
-    expect(res, Weight.kg(123.45));
-  });
-  testWidgets('respects preferred weight unit', (tester) async {
-    Weight? res;
-    await tester.pumpWidget(materialApp(Builder(
-      builder: (context) => GestureDetector(
-        onTap: () async => res = await showDialog<Weight>(context: context, builder: (_) => const AddBodyweightDialoge()),
-        child: const Text('X'),
-      ),
-    ), settings: Settings(weightUnit: WeightUnit.st)));
-    await tester.tap(find.text('X'));
-    await tester.pumpAndSettle();
-
-    await tester.enterText(find.byType(TextFormField), '123.45');
-    await tester.testTextInput.receiveAction(TextInputAction.done);
-    await tester.pumpAndSettle();
-
-    expect(res, WeightUnit.st.store(123.45));
-  });
-}
app/test/features/input/add_entry_dialogue_test.dart
@@ -0,0 +1,209 @@
+import 'package:blood_pressure_app/features/input/add_entry_dialogue.dart';
+import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
+import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:flutter/material.dart';
+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 '../../util.dart';
+
+void main() {
+  testWidgets('respects bottomAppBars', (tester) async {
+    final settings = Settings(bottomAppBars: false);
+    await tester.pumpWidget(materialApp(const AddEntryDialogue(),
+      settings: settings
+    ));
+    final initialHeights = tester.getCenter(find.byType(AppBar)).dy;
+
+    settings.bottomAppBars = true;
+    await tester.pump();
+
+    expect(tester.getCenter(find.byType(AppBar)).dy, greaterThan(initialHeights));
+  });
+
+  // TODO: update these old tests
+  testWidgets('should show everything on initial page', (tester) async {
+    await tester.pumpWidget(materialApp(const AddEntryDialogue()));
+    expect(tester.takeException(), isNull);
+
+    expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+    expect(find.text('SAVE'), findsOneWidget);
+    expect(find.byIcon(Icons.close), findsOneWidget);
+    expect(find.text('Systolic'), findsWidgets);
+    expect(find.text('Diastolic'), findsWidgets);
+    expect(find.text('Pulse'), findsWidgets);
+    expect(find.byType(ColorSelectionListTile), findsOneWidget);
+  },);
+  testWidgets('should prefill initialRecord values', (tester) async {
+    await tester.pumpWidget(materialApp(
+      AddEntryDialogue(
+        initialRecord: mockEntryPos(
+          DateTime.now(), 123, 56, 43, 'Test note', Colors.teal,
+        ).asAddEntry,
+        availableMeds: const [],
+      ),
+    ),);
+    await tester.pumpAndSettle();
+    expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+    expect(find.text('SAVE'), findsOneWidget);
+    expect(find.byIcon(Icons.close), findsOneWidget);
+    expect(find.text('Test note'), findsOneWidget);
+    expect(find.text('123'), findsOneWidget);
+    expect(find.text('56'), findsOneWidget);
+    expect(find.text('43'), findsOneWidget);
+    expect(find.byType(ColorSelectionListTile), findsOneWidget);
+    tester.widget<ColorSelectionListTile>(find.byType(ColorSelectionListTile)).initialColor == Colors.teal;
+  });
+  testWidgets('should return null on cancel', (tester) async {
+    dynamic result = 'result before save';
+    await loadDialoge(tester, (context) async
+    => result = await showAddEntryDialogue(context,
+        medRepo(),
+        mockEntry(sys: 123, dia: 56, pul: 43, note: 'Test note', pin: Colors.teal).asAddEntry));
+    expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+    expect(find.byType(AddEntryDialogue), findsOneWidget);
+    await tester.tap(find.byIcon(Icons.close));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialogue), findsNothing);
+
+    expect(result, null);
+  });
+  testWidgets('should not allow invalid values', (tester) async {
+    final mRep = medRepo();
+    await loadDialoge(tester, (context) => showAddEntryDialogue(context, mRep));
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+    expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+    expect(find.byType(AddEntryDialogue), findsOneWidget);
+    expect(find.text(localizations.errNaN), findsNothing);
+    expect(find.text(localizations.errLt30), findsNothing);
+    expect(find.text(localizations.errUnrealistic), findsNothing);
+    expect(find.text(localizations.errDiaGtSys), findsNothing);
+
+    await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '123');
+    await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '67');
+
+    await tester.tap(find.text('SAVE'));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialogue), findsOneWidget);
+    expect(find.text(localizations.errNaN), findsOneWidget);
+    expect(find.text(localizations.errLt30), findsNothing);
+    expect(find.text(localizations.errUnrealistic), findsNothing);
+    expect(find.text(localizations.errDiaGtSys), findsNothing);
+
+    await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextField)), '20');
+    await tester.tap(find.text('SAVE'));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialogue), findsOneWidget);
+    expect(find.text(localizations.errNaN), findsNothing);
+    expect(find.text(localizations.errLt30), findsOneWidget);
+    expect(find.text(localizations.errUnrealistic), findsNothing);
+    expect(find.text(localizations.errDiaGtSys), findsNothing);
+
+    await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextField)), '60');
+    await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '500');
+    await tester.tap(find.text('SAVE'));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialogue), findsOneWidget);
+    expect(find.text(localizations.errNaN), findsNothing);
+    expect(find.text(localizations.errLt30), findsNothing);
+    expect(find.text(localizations.errUnrealistic), findsOneWidget);
+    expect(find.text(localizations.errDiaGtSys), findsNothing);
+
+    await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '100');
+    await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '90');
+    await tester.tap(find.text('SAVE'));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialogue), findsOneWidget);
+    expect(find.text(localizations.errNaN), findsNothing);
+    expect(find.text(localizations.errLt30), findsNothing);
+    expect(find.text(localizations.errUnrealistic), findsNothing);
+    expect(find.text(localizations.errDiaGtSys), findsOneWidget);
+
+
+    await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '78');
+    await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '123');
+    await tester.tap(find.text('SAVE'));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialogue), findsNothing);
+    expect(find.text(localizations.errNaN), findsNothing);
+    expect(find.text(localizations.errLt30), findsNothing);
+    expect(find.text(localizations.errUnrealistic), findsNothing);
+    expect(find.text(localizations.errDiaGtSys), findsNothing);
+  });
+  testWidgets('should allow invalid values when setting is set', (tester) async {
+    final mRep = medRepo();
+    await loadDialoge(tester, (context) => showAddEntryDialogue(context, mRep),
+      settings: Settings(validateInputs: false, allowMissingValues: true),
+    );
+    expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+    await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '2');
+    await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '500');
+    await tester.tap(find.text('SAVE'));
+    await tester.pumpAndSettle();
+    expect(find.byType(AddEntryDialogue), findsNothing);
+  });
+  testWidgets('should start with sys input focused', (tester) async {
+    final mRep = medRepo();
+    await loadDialoge(tester, (context) =>
+        showAddEntryDialogue(context, mRep, mockEntry(sys: 12).asAddEntry));
+    expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+    final primaryFocus = FocusManager.instance.primaryFocus;
+    expect(primaryFocus?.context?.widget, isNotNull);
+    final focusedTextField = find.ancestor(
+      of: find.byWidget(primaryFocus!.context!.widget),
+      matching: find.byType(TextField),
+    );
+    expect(focusedTextField, findsOneWidget);
+    final field = tester.widget<TextField>(focusedTextField);
+    expect(field.controller?.text, '12');
+  });
+  testWidgets('should focus next on input finished', (tester) async {
+    final mRep = medRepo();
+    await loadDialoge(tester, (context) =>
+        showAddEntryDialogue(context, mRep, mockEntry(sys: 12, dia: 3, pul: 4, note: 'note').asAddEntry),);
+    expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
+
+    await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextField)), '123');
+
+    final firstFocused = FocusManager.instance.primaryFocus;
+    expect(firstFocused?.context?.widget, isNotNull);
+    final focusedTextField = find.ancestor(
+      of: find.byWidget(firstFocused!.context!.widget),
+      matching: find.byType(TextField),
+    );
+    expect(focusedTextField, findsOneWidget);
+    expect(focusedTextField.evaluate().first.widget, isA<TextField>()
+        .having((p0) => p0.controller?.text, 'diastolic content', '3'),);
+
+    await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextField)), '78');
+
+    final secondFocused = FocusManager.instance.primaryFocus;
+    expect(secondFocused?.context?.widget, isNotNull);
+    final secondFocusedTextField = find.ancestor(
+      of: find.byWidget(secondFocused!.context!.widget),
+      matching: find.byType(TextField),
+    );
+    expect(secondFocusedTextField, findsOneWidget);
+    expect(secondFocusedTextField.evaluate().first.widget, isA<TextField>()
+        .having((p0) => p0.controller?.text, 'pulse content', '4'),);
+
+    await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextField)), '60');
+
+    final thirdFocused = FocusManager.instance.primaryFocus;
+    expect(thirdFocused?.context?.widget, isNotNull);
+    final thirdFocusedTextField = find.ancestor(
+      of: find.byWidget(thirdFocused!.context!.widget),
+      matching: find.byType(TextField),
+    );
+    expect(thirdFocusedTextField, findsOneWidget);
+    expect(find.descendant(of: thirdFocusedTextField, matching: find.text('Note (optional)')), findsOneWidget);
+  });
+}
app/test/features/input/add_measurement_dialoge_test.dart
@@ -1,660 +0,0 @@
-import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart';
-import 'package:blood_pressure_app/features/input/add_bodyweight_dialoge.dart';
-import 'package:blood_pressure_app/features/input/add_measurement_dialoge.dart';
-import 'package:blood_pressure_app/features/settings/tiles/color_picker_list_tile.dart';
-import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.dart';
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-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 '../../util.dart';
-import '../settings/tiles/color_picker_list_tile_test.dart';
-
-void main() {
-  group('AddEntryDialoge', () {
-    testWidgets('should show everything on initial page', (tester) async {
-      await tester.pumpWidget(materialApp(const AddEntryDialoge(availableMeds: [])));
-      expect(tester.takeException(), isNull);
-
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-      expect(find.text('SAVE'), findsOneWidget);
-      expect(find.byIcon(Icons.close), findsOneWidget);
-      expect(find.text('Systolic'), findsWidgets);
-      expect(find.text('Diastolic'), findsWidgets);
-      expect(find.text('Pulse'), findsWidgets);
-      expect(find.byType(ColorSelectionListTile), findsOneWidget);
-    },);
-    testWidgets('should prefill initialRecord values', (tester) async {
-      await tester.pumpWidget(materialApp(
-        AddEntryDialoge(
-          initialRecord: mockEntryPos(
-            DateTime.now(), 123, 56, 43, 'Test note', Colors.teal,
-          ),
-          availableMeds: const [],
-        ),
-      ),);
-      await tester.pumpAndSettle();
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-      expect(find.text('SAVE'), findsOneWidget);
-      expect(find.byIcon(Icons.close), findsOneWidget);
-      expect(find.text('Test note'), findsOneWidget);
-      expect(find.text('123'), findsOneWidget);
-      expect(find.text('56'), findsOneWidget);
-      expect(find.text('43'), findsOneWidget);
-      expect(find.byType(ColorSelectionListTile), findsOneWidget);
-      tester.widget<ColorSelectionListTile>(find.byType(ColorSelectionListTile)).initialColor == Colors.teal;
-    });
-    testWidgets('should show medication picker when medications available', (tester) async {
-      await tester.pumpWidget(materialApp(
-        AddEntryDialoge(
-          availableMeds: [ mockMedicine(designation: 'testmed') ],
-        ),
-      ),);
-      await tester.pumpAndSettle();
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
-      expect(find.byType(DropdownButton<Medicine?>), findsOneWidget);
-      expect(find.text(localizations.noMedication), findsOneWidget);
-      expect(find.text('testmed'), findsNothing);
-
-      await tester.tap(find.byType(DropdownButton<Medicine?>));
-      await tester.pumpAndSettle();
-
-      expect(find.text('testmed'), findsOneWidget);
-    });
-    testWidgets('should reveal dosis on medication selection', (tester) async {
-      await tester.pumpWidget(materialApp(
-        AddEntryDialoge(
-          availableMeds: [ mockMedicine(designation: 'testmed') ],
-        ),
-      ),);
-      await tester.pumpAndSettle();
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
-      expect(find.byType(DropdownButton<Medicine?>), findsOneWidget);
-      expect(find.text(localizations.noMedication), findsOneWidget);
-      expect(find.text('testmed'), findsNothing);
-
-      await tester.tap(find.byType(DropdownButton<Medicine?>));
-      await tester.pumpAndSettle();
-
-      expect(find.text(localizations.dosis), findsNothing);
-      expect(find.text('testmed'), findsOneWidget);
-      await tester.tap(find.text('testmed'));
-      await tester.pumpAndSettle();
-
-      expect(
-        find.ancestor(
-          of: find.text(localizations.dosis,).first,
-          matching: find.byType(TextFormField),
-        ),
-        findsOneWidget,
-      );
-    });
-    testWidgets('should enter default dosis if available', (tester) async {
-      await tester.pumpWidget(materialApp(
-        AddEntryDialoge(
-          availableMeds: [ mockMedicine(designation: 'testmed', defaultDosis: 3.1415) ],
-        ),
-      ),);
-      await tester.pumpAndSettle();
-
-      await tester.tap(find.byType(DropdownButton<Medicine?>));
-      await tester.pumpAndSettle();
-
-      await tester.tap(find.text('testmed'));
-      await tester.pumpAndSettle();
-
-      expect(find.text('3.1415'), findsOneWidget);
-    });
-    testWidgets('should not quit when the measurement field is incorrectly filled, but a intake is added', (tester) async {
-      await tester.pumpWidget(materialApp(
-        AddEntryDialoge(
-          availableMeds: [ mockMedicine(designation: 'testmed', defaultDosis: 3.1415) ],
-        ),
-      ),);
-      await tester.pumpAndSettle();
-
-      await tester.tap(find.byType(DropdownButton<Medicine?>));
-      await tester.pumpAndSettle();
-
-      await tester.tap(find.text('testmed'));
-      await tester.pumpAndSettle();
-
-      await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '900');
-      await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '89');
-      await tester.pumpAndSettle();
-
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      expect(find.text(localizations.errUnrealistic), findsNothing);
-
-      await tester.tap(find.text(localizations.btnSave));
-      await tester.pumpAndSettle();
-
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      expect(find.text(localizations.errUnrealistic), findsOneWidget);
-    });
-    testWidgets('respects settings about showing bluetooth input', (tester) async {
-      final settings = Settings(
-        bleInput: BluetoothInputMode.newBluetoothInputCrossPlatform,
-      );
-      await tester.pumpWidget(materialApp(
-        const AddEntryDialoge(
-          availableMeds: [],
-        ),
-        settings: settings,
-      ),);
-      await tester.pumpAndSettle();
-      expect(find.byType(BluetoothInput, skipOffstage: false), findsOneWidget);
-
-      settings.bleInput = BluetoothInputMode.disabled;
-      await tester.pumpAndSettle();
-      expect(find.byType(BluetoothInput), findsNothing);
-    });
-  }, skip: true);
-  group('showAddEntryDialoge', () {
-    testWidgets('should return null on cancel', (tester) async {
-      dynamic result = 'result before save';
-      await loadDialoge(tester, (context) async
-      => result = await showAddEntryDialoge(context,
-        medRepo(),
-        mockEntry(sys: 123, dia: 56, pul: 43, note: 'Test note', pin: Colors.teal),),);
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      await tester.tap(find.byIcon(Icons.close));
-      await tester.pumpAndSettle();
-      expect(find.byType(AddEntryDialoge), findsNothing);
-
-      expect(result, null);
-    });
-    testWidgets('should return values on edit cancel', (tester) async {
-      dynamic result = 'result before save';
-      final record = mockEntry(sys: 123, dia: 56, pul: 43, note: 'Test note', pin: Colors.teal);
-      await loadDialoge(tester, (context) async {
-        result = await showAddEntryDialoge(context, medRepo(), record);
-      },);
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      await tester.tap(find.text('SAVE'));
-      await tester.pumpAndSettle();
-      expect(find.byType(AddEntryDialoge), findsNothing);
-      
-      expect(result, isA<FullEntry>());
-      final FullEntry res = result;
-      expect(res.time, record.time);
-      expect(res.sys, record.sys);
-      expect(res.dia, record.dia);
-      expect(res.pul, record.pul);
-      expect(res.note, record.note);
-      expect(res.color, record.color);
-    });
-    testWidgets('should be able to input records', (WidgetTester tester) async {
-      dynamic result = 'result before save';
-      await loadDialoge(tester, (context) async {
-        result = await showAddEntryDialoge(context, medRepo(),);
-      });
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '67');
-      await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '89');
-      await tester.enterText(find.ancestor(of: find.text('Note (optional)').first, matching: find.byType(TextFormField)), 'Test note');
-
-      await tester.tap(find.byType(ColorSelectionListTile));
-      await tester.pumpAndSettle();
-      await tester.tap(find.byElementPredicate(findColored(Colors.red)));
-      await tester.pumpAndSettle();
-
-      expect(find.text('SAVE'), findsOneWidget);
-      await tester.tap(find.text('SAVE'));
-      await tester.pumpAndSettle();
-
-      expect(result, isA<FullEntry>());
-      final FullEntry res = result;
-      expect(res.sys?.mmHg, 123);
-      expect(res.dia?.mmHg, 67);
-      expect(res.pul, 89);
-      expect(res.note, 'Test note');
-      expect(res.color, Colors.red.value);
-    });
-    testWidgets('should allow value only', (WidgetTester tester) async {
-      dynamic result = 'result before save';
-      await loadDialoge(tester, (context) async
-      => result = await showAddEntryDialoge(context, medRepo(),),);
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
-      await tester.enterText(find.ancestor(of: find.text(localizations.sysLong).first,
-        matching: find.byType(TextFormField),), '123',);
-      await tester.enterText(find.ancestor(of: find.text(localizations.diaLong).first,
-        matching: find.byType(TextFormField),), '67',);
-      await tester.enterText(find.ancestor(of: find.text(localizations.pulLong).first,
-        matching: find.byType(TextFormField),), '89',);
-
-      expect(find.text(localizations.btnSave), findsOneWidget);
-      await tester.tap(find.text(localizations.btnSave));
-      await tester.pumpAndSettle();
-
-      expect(result, isA<FullEntry>());
-      final FullEntry res = result;
-      expect(res.sys?.mmHg, 123);
-      expect(res.dia?.mmHg, 67);
-      expect(res.pul, 89);
-      expect(res.note, null);
-      expect(res.color, null);
-    });
-    testWidgets('should allow note only', (WidgetTester tester) async {
-      dynamic result = 'result before save';
-      await loadDialoge(tester, (context) async
-      => result = await showAddEntryDialoge(context, medRepo(),),);
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-      await tester.enterText(find.ancestor(of: find.text(localizations.addNote).first,
-        matching: find.byType(TextFormField),), 'test note',);
-
-      expect(find.text(localizations.btnSave), findsOneWidget);
-      await tester.tap(find.text(localizations.btnSave));
-      await tester.pumpAndSettle();
-
-      expect(result, isA<FullEntry>()
-        .having((p0) => p0.sys, 'systolic', null)
-        .having((p0) => p0.dia, 'diastolic', null)
-        .having((p0) => p0.pul, 'pulse', null)
-        .having((p0) => p0.note, 'note', 'test note')
-        .having((p0) => p0.color, 'needlePin', null),
-      );
-    });
-    testWidgets('should be able to input medicines', (WidgetTester tester) async {
-      final med2 = mockMedicine(designation: 'medication2', defaultDosis: 31.415);
-
-      dynamic result = 'result before save';
-      await loadDialoge(tester, (context) async
-      => result = await showAddEntryDialoge(context, medRepo([
-        mockMedicine(designation: 'medication1'),
-        med2,
-      ],),),);
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
-      await tester.tap(find.byType(DropdownButton<Medicine?>));
-      final openDialogeTimeStamp = DateTime.now();
-      await tester.pumpAndSettle();
-
-      expect(find.text('medication1'), findsOneWidget);
-      expect(find.text('medication2'), findsOneWidget);
-      await tester.tap(find.text('medication2'));
-      await tester.pumpAndSettle();
-
-      await tester.enterText(
-        find.ancestor(
-          of: find.text(localizations.dosis).first,
-          matching: find.byType(TextFormField),
-        ),
-        '123.456',
-      );
-
-      expect(find.text(localizations.btnSave), findsOneWidget);
-      await tester.tap(find.text(localizations.btnSave));
-      await tester.pumpAndSettle();
-
-      expect(result, isA<FullEntry>());
-      final FullEntry res = result;
-      expect(res.time.millisecondsSinceEpoch, inInclusiveRange(
-        openDialogeTimeStamp.millisecondsSinceEpoch - 2000,
-        openDialogeTimeStamp.millisecondsSinceEpoch + 2000)
-      );
-      expect(res.sys, null);
-      expect(res.dia, null);
-      expect(res.pul, null);
-      expect(res.note, null);
-      expect(res.color, null);
-      expect(res.intakes, hasLength(1));
-      expect(res.intakes.first.medicine, med2);
-      expect(res.intakes.first.dosis.mg, 123.456);
-    });
-    testWidgets('should not allow invalid values', (tester) async {
-      final mRep = medRepo();
-      await loadDialoge(tester, (context) => showAddEntryDialoge(context, mRep));
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      expect(find.text(localizations.errNaN), findsNothing);
-      expect(find.text(localizations.errLt30), findsNothing);
-      expect(find.text(localizations.errUnrealistic), findsNothing);
-      expect(find.text(localizations.errDiaGtSys), findsNothing);
-
-      await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '67');
-
-      await tester.tap(find.text('SAVE'));
-      await tester.pumpAndSettle();
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      expect(find.text(localizations.errNaN), findsOneWidget);
-      expect(find.text(localizations.errLt30), findsNothing);
-      expect(find.text(localizations.errUnrealistic), findsNothing);
-      expect(find.text(localizations.errDiaGtSys), findsNothing);
-
-      await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '20');
-      await tester.tap(find.text('SAVE'));
-      await tester.pumpAndSettle();
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      expect(find.text(localizations.errNaN), findsNothing);
-      expect(find.text(localizations.errLt30), findsOneWidget);
-      expect(find.text(localizations.errUnrealistic), findsNothing);
-      expect(find.text(localizations.errDiaGtSys), findsNothing);
-
-      await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '60');
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '500');
-      await tester.tap(find.text('SAVE'));
-      await tester.pumpAndSettle();
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      expect(find.text(localizations.errNaN), findsNothing);
-      expect(find.text(localizations.errLt30), findsNothing);
-      expect(find.text(localizations.errUnrealistic), findsOneWidget);
-      expect(find.text(localizations.errDiaGtSys), findsNothing);
-
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '100');
-      await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '90');
-      await tester.tap(find.text('SAVE'));
-      await tester.pumpAndSettle();
-      expect(find.byType(AddEntryDialoge), findsOneWidget);
-      expect(find.text(localizations.errNaN), findsNothing);
-      expect(find.text(localizations.errLt30), findsNothing);
-      expect(find.text(localizations.errUnrealistic), findsNothing);
-      expect(find.text(localizations.errDiaGtSys), findsOneWidget);
-
-
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '78');
-      await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
-      await tester.tap(find.text('SAVE'));
-      await tester.pumpAndSettle();
-      expect(find.byType(AddEntryDialoge), findsNothing);
-      expect(find.text(localizations.errNaN), findsNothing);
-      expect(find.text(localizations.errLt30), findsNothing);
-      expect(find.text(localizations.errUnrealistic), findsNothing);
-      expect(find.text(localizations.errDiaGtSys), findsNothing);
-    });
-    testWidgets('should allow invalid values when setting is set', (tester) async {
-      final mRep = medRepo();
-      await loadDialoge(tester, (context) => showAddEntryDialoge(context, mRep),
-        settings: Settings(validateInputs: false, allowMissingValues: true),
-      );
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '2');
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '500');
-      await tester.tap(find.text('SAVE'));
-      await tester.pumpAndSettle();
-      expect(find.byType(AddEntryDialoge), findsNothing);
-    });
-    testWidgets('should respect settings.allowManualTimeInput', (tester) async {
-      final mRep = medRepo();
-      await loadDialoge(tester, (context) => showAddEntryDialoge(context, mRep),
-        settings: Settings(allowManualTimeInput: false),
-      );
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      expect(find.byIcon(Icons.edit), findsNothing);
-    });
-    testWidgets('should start with sys input focused', (tester) async {
-      final mRep = medRepo();
-      await loadDialoge(tester, (context) =>
-          showAddEntryDialoge(context, mRep, mockEntry(sys: 12)),);
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      final primaryFocus = FocusManager.instance.primaryFocus;
-      expect(primaryFocus?.context?.widget, isNotNull);
-      final focusedTextFormField = find.ancestor(
-        of: find.byWidget(primaryFocus!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(focusedTextFormField, findsOneWidget);
-      final field = tester.widget<TextFormField>(focusedTextFormField);
-      expect(field.initialValue, '12');
-    });
-    testWidgets('should focus next on input finished', (tester) async {
-      final mRep = medRepo();
-      await loadDialoge(tester, (context) =>
-          showAddEntryDialoge(context, mRep, mockEntry(sys: 12, dia: 3, pul: 4, note: 'note')),);
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '123');
-
-      final firstFocused = FocusManager.instance.primaryFocus;
-      expect(firstFocused?.context?.widget, isNotNull);
-      final focusedTextFormField = find.ancestor(
-        of: find.byWidget(firstFocused!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(focusedTextFormField, findsOneWidget);
-      expect(focusedTextFormField.evaluate().first.widget, isA<TextFormField>()
-          .having((p0) => p0.initialValue, 'diastolic content', '3'),);
-
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '78');
-
-      final secondFocused = FocusManager.instance.primaryFocus;
-      expect(secondFocused?.context?.widget, isNotNull);
-      final secondFocusedTextFormField = find.ancestor(
-        of: find.byWidget(secondFocused!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(secondFocusedTextFormField, findsOneWidget);
-      expect(secondFocusedTextFormField.evaluate().first.widget, isA<TextFormField>()
-          .having((p0) => p0.initialValue, 'pulse content', '4'),);
-
-      await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '60');
-
-      final thirdFocused = FocusManager.instance.primaryFocus;
-      expect(thirdFocused?.context?.widget, isNotNull);
-      final thirdFocusedTextFormField = find.ancestor(
-        of: find.byWidget(thirdFocused!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(thirdFocusedTextFormField, findsOneWidget);
-      expect(thirdFocusedTextFormField.evaluate().first.widget, isA<TextFormField>()
-          .having((p0) => p0.initialValue, 'note input content', 'note'),);
-    });
-    testWidgets('should focus last input field on backspace pressed in empty input field', (tester) async {
-      final mRep = medRepo();
-      await loadDialoge(tester, (context) =>
-          showAddEntryDialoge(context, mRep, mockEntry(sys: 12, dia: 3, pul: 4, note: 'note')),);
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      await tester.enterText(find.ancestor(of: find.text('note').first, matching: find.byType(TextFormField)), '');
-      await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
-      final firstFocused = FocusManager.instance.primaryFocus;
-      expect(firstFocused?.context?.widget, isNotNull);
-      final focusedTextFormField = find.ancestor(
-        of: find.byWidget(firstFocused!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(focusedTextFormField, findsOneWidget);
-      expect(find.descendant(of: focusedTextFormField, matching: find.text('Note (optional)')), findsNothing);
-      expect(find.descendant(of: focusedTextFormField, matching: find.text('Pulse')), findsWidgets);
-
-
-      await tester.enterText(find.ancestor(of: find.text('Pulse').first, matching: find.byType(TextFormField)), '');
-      await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
-      final secondFocused = FocusManager.instance.primaryFocus;
-      expect(secondFocused?.context?.widget, isNotNull);
-      final secondFocusedTextFormField = find.ancestor(
-        of: find.byWidget(secondFocused!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(secondFocusedTextFormField, findsOneWidget);
-      expect(find.descendant(of: secondFocusedTextFormField, matching: find.text('Pulse')), findsNothing);
-      expect(find.descendant(of: secondFocusedTextFormField, matching: find.text('Diastolic')), findsWidgets);
-
-
-      await tester.enterText(find.ancestor(of: find.text('Diastolic').first, matching: find.byType(TextFormField)), '');
-      await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
-      final thirdFocused = FocusManager.instance.primaryFocus;
-      expect(thirdFocused?.context?.widget, isNotNull);
-      final thirdFocusedTextFormField = find.ancestor(
-        of: find.byWidget(thirdFocused!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(thirdFocusedTextFormField, findsOneWidget);
-      expect(find.descendant(of: thirdFocusedTextFormField, matching: find.text('Diastolic')), findsNothing);
-      expect(find.descendant(of: thirdFocusedTextFormField, matching: find.text('Systolic')), findsWidgets);
-
-
-      // should not go back further than systolic
-      await tester.enterText(find.ancestor(of: find.text('Systolic').first, matching: find.byType(TextFormField)), '');
-      await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
-      final fourthFocused = FocusManager.instance.primaryFocus;
-      expect(fourthFocused?.context?.widget, isNotNull);
-      final fourthFocusedTextFormField = find.ancestor(
-        of: find.byWidget(fourthFocused!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(fourthFocusedTextFormField, findsOneWidget);
-      expect(find.descendant(of: fourthFocusedTextFormField, matching: find.text('Systolic')), findsWidgets);
-    });
-    testWidgets('should allow entering custom dosis', (tester) async {
-      final mRep = medRepo([mockMedicine(designation: 'testmed')]);
-      dynamic result;
-      await loadDialoge(tester, (context) async =>
-        result = await showAddEntryDialoge(context, mRep),
-      );
-
-      await tester.tap(find.byType(DropdownButton<Medicine?>));
-      await tester.pumpAndSettle();
-
-      await tester.tap(find.text('testmed'));
-      await tester.pumpAndSettle();
-
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-      await tester.tap(find.text(localizations.btnSave));
-      await tester.pumpAndSettle();
-
-      expect(find.text(localizations.errNaN), findsOneWidget);
-
-      await tester.enterText(
-        find.ancestor(
-          of: find.text(localizations.dosis).first,
-          matching: find.byType(TextFormField),
-        ),
-        '654.321',
-      );
-      await tester.tap(find.text(localizations.btnSave));
-      await tester.pumpAndSettle();
-      
-      expect(result, isA<FullEntry>());
-      final FullEntry res = result; 
-      expect(res.sys, null);
-      expect(res.dia, null);
-      expect(res.pul, null);
-      expect(res.note, null);
-      expect(res.color, null);
-      
-      expect(res.intakes, hasLength(1));
-      expect(res.intakes.first.dosis.mg, 654.321);
-    });
-    testWidgets('should allow modifying entered dosis', (tester) async {
-      final mRep = medRepo([mockMedicine(designation: 'testmed')]);
-      dynamic result;
-      await loadDialoge(tester, (context) async =>
-        result = await showAddEntryDialoge(context, mRep),
-      );
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-
-      await tester.tap(find.byType(DropdownButton<Medicine?>));
-      await tester.pumpAndSettle();
-
-      await tester.tap(find.text('testmed'));
-      await tester.pumpAndSettle();
-
-      await tester.enterText(
-        find.ancestor(
-          of: find.text(localizations.dosis).first,
-          matching: find.byType(TextFormField),
-        ),
-        '654.321',
-      );
-      await tester.pumpAndSettle();
-      await tester.enterText(
-        find.ancestor(
-          of: find.text(localizations.dosis).first,
-          matching: find.byType(TextFormField),
-        ),
-        '654.322',
-      );
-      await tester.pumpAndSettle();
-
-      await tester.tap(find.text(localizations.btnSave));
-      await tester.pumpAndSettle();
-
-      expect(result, isA<FullEntry>());
-      final FullEntry res = result;
-      expect(res.sys, null);
-      expect(res.dia, null);
-      expect(res.pul, null);
-      expect(res.note, null);
-      expect(res.color, null);
-
-      expect(res.intakes, hasLength(1));
-      expect(res.intakes.first.dosis.mg, 654.322);
-    });
-    testWidgets('should not go back to last field when the current field is still filled', (tester) async {
-      final mRep = medRepo([mockMedicine(designation: 'testmed')]);
-      await loadDialoge(tester, (context) =>
-          showAddEntryDialoge(context, mRep, mockEntry(sys: 12, dia: 3, pul: 4, note: 'note')),);
-      expect(find.byType(DropdownButton<Medicine?>), findsNothing, reason: 'No medication in settings.');
-
-      await tester.enterText(find.ancestor(
-          of: find.text('note').first,
-          matching: find.byType(TextFormField),),
-        'not empty',);
-      await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
-
-      final firstFocused = FocusManager.instance.primaryFocus;
-      expect(firstFocused?.context?.widget, isNotNull);
-      final focusedTextFormField = find.ancestor(
-        of: find.byWidget(firstFocused!.context!.widget),
-        matching: find.byType(TextFormField),
-      );
-      expect(focusedTextFormField, findsOneWidget);
-      expect(find.descendant(of: focusedTextFormField, matching: find.text('Pulse')), findsNothing);
-      expect(find.descendant(of: focusedTextFormField, matching: find.text('Note (optional)')), findsWidgets);
-    });
-    testWidgets('opens weight input if necessary', (tester) async {
-      final repo = MockBodyweightRepository();
-      await tester.pumpWidget(appBase(Builder(
-        builder: (context) => TextButton(onPressed: () => showAddEntryDialoge(context, MockMedRepo([])), child: const Text('X'))
-      ), settings: Settings(weightInput: true), weightRepo: repo));
-      final localizations = await AppLocalizations.delegate.load(const Locale('en'));
-      await tester.tap(find.text('X'));
-      await tester.pumpAndSettle();
-
-      expect(find.text(localizations.enterWeight), findsOneWidget);
-      expect(find.byIcon(Icons.scale), findsOneWidget);
-      await tester.tap(find.text(localizations.enterWeight));
-      await tester.pumpAndSettle();
-      
-      expect(repo.data, isEmpty);
-      await tester.enterText(find.descendant(
-        of: find.byType(AddBodyweightDialoge),
-        matching: find.byType(TextFormField)
-      ), '123.45');
-      await tester.testTextInput.receiveAction(TextInputAction.done);
-      await tester.pumpAndSettle();
-      expect(repo.data, hasLength(1));
-      expect(repo.data[0].weight, Weight.kg(123.45));
-    });
-  });
-}
app/pubspec.lock
@@ -563,6 +563,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.5.2"
+  inline_tab_view:
+    dependency: "direct main"
+    description:
+      name: inline_tab_view
+      sha256: f7cc58e3253b9a9c1e45ad2eaba96ab01ce8b7fe0034e453e6e178ee97fde02d
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.1"
   integration_test:
     dependency: "direct dev"
     description: flutter
app/pubspec.yaml
@@ -42,6 +42,7 @@ dependencies:
 
   # desktop only
   sqflite_common_ffi: ^2.3.4+4
+  inline_tab_view: ^1.0.1
 
 dev_dependencies:
   integration_test:
health_data_store/lib/src/repositories/note_repository_impl.dart
@@ -23,7 +23,7 @@ class NoteRepositoryImpl extends NoteRepository {
   Future<void> add(Note note) async {
     _controller.add(null);
     if (note.note == null && note.color == null) {
-      assert(false);
+      assert(false, 'Attempting to store a note without content and color');
       return;
     }
     await _db.transaction((txn) async {