Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite form to add entries #459

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions app/lib/features/input/add_measurement_dialoge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:blood_pressure_app/components/fullscreen_dialoge.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/add_entry_form.dart';
import 'package:blood_pressure_app/features/input/forms/date_time_form.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';
Expand Down Expand Up @@ -42,6 +43,7 @@ class AddEntryDialoge extends StatefulWidget {
}

class _AddEntryDialogeState extends State<AddEntryDialoge> {
final dateTimeFormKey = GlobalKey<DateTimeFormState>();
final recordFormKey = GlobalKey<FormState>();
final medicationFormKey = GlobalKey<FormState>();

Expand Down Expand Up @@ -189,7 +191,14 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
);

@override
Widget build(BuildContext context) {
Widget build(BuildContext context) => FullscreenDialoge(
actionButtonText: AppLocalizations.of(context)!.btnSave,
bottomAppBar: false, // TODO
body: AddEntryForm(meds: widget.availableMeds),
);

@Deprecated('towards AddEntryForm')
Widget _buildOld(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
final settings = context.watch<Settings>();
return FullscreenDialoge(
Expand All @@ -202,6 +211,10 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
|| diaController.text.isNotEmpty
|| pulController.text.isNotEmpty);

final newTime = dateTimeFormKey.currentState?.save();
if (newTime == null) return;
time = newTime;

if (shouldHaveRecord && (recordFormKey.currentState?.validate() ?? false)) {
recordFormKey.currentState?.save();
if (systolic != null || diastolic != null || pulse != null) {
Expand Down Expand Up @@ -258,11 +271,8 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
),
if (settings.allowManualTimeInput)
DateTimeForm(
validate: settings.validateInputs,
initialTime: time,
onTimeSelected: (newTime) => setState(() {
time = newTime;
}),
initialValue: time,
key: dateTimeFormKey,
),
Form(
key: recordFormKey,
Expand Down
118 changes: 118 additions & 0 deletions app/lib/features/input/forms/add_entry_form.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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/model/storage/storage.dart';
import 'package:flutter/material.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> {
/// Create primary form to enter all types of entries.
const AddEntryForm({super.key, super.initialValue, required this.meds});

/// All medicines selectable.
///
/// Hides med input when this is empty.
final List<Medicine> meds;

@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>();

@override
bool validate() => (_timeForm.currentState?.validate() ?? false)
&& (_noteForm.currentState?.validate() ?? false)
&& ((_bpForm.currentState?.validate() ?? false)
|| (_weightForm.currentState?.validate() ?? false)
|| (_intakeForm.currentState?.validate() ?? false));

@override
AddEntryFormValue? save() {
if (!validate()) return null;
final time = _timeForm.currentState!.save()!;
Note? note;
BloodPressureRecord? record;
BodyweightRecord? weight;
MedicineIntake? intake;

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) {
// TODO
// intake = MedicineIntake(time: time, medicine: null, dosis: null);
}
return (
timestamp: time,
note: note,
record: record,
intake: intake,
weight: weight,
);
}

@override
Widget build(BuildContext context) { // TODO: initial values
final settings = context.watch<Settings>();
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 8),
children: [
DateTimeForm(key: _timeForm, initialValue: null),
SizedBox(height: 10),
SizedBox(
// TODO: remove when updating FormSwitcher code
height: MediaQuery.of(context).size.height * 0.2,
child: FormSwitcher(
subforms: [
(Icon(Icons.monitor_heart_outlined), BloodPressureForm(key: _bpForm,),),
if (widget.meds.isNotEmpty)
(Icon(Icons.medication_outlined), MedicineIntakeForm(key: _intakeForm,),),
if (settings.weightInput)
(Icon(Icons.scale), WeightForm(key: _weightForm,),),
],
),
),
NoteForm(key: _noteForm),
]
);
}
}

/// Types of entries supported by [AddEntryForm].
typedef AddEntryFormValue = ({
DateTime timestamp,
Note? note,
BloodPressureRecord? record,
MedicineIntake? intake,
BodyweightRecord? weight,
});
142 changes: 142 additions & 0 deletions app/lib/features/input/forms/blood_pressure_form.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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});

@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() => _formKey.currentState?.validate() ?? false;

@override
({int? sys, int? dia, int? pul})? save() {
if (!validate()
|| (_sysController.text.isEmpty
&& _diaController.text.isEmpty
&& _pulController.text.isEmpty)) {
return null;
}
return (
sys: int.tryParse(_sysController.text),
dia: int.tryParse(_diaController.text),
pul: int.tryParse(_pulController.text),
);
}

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,
),
],
),
);
}
Loading