main
  1import 'package:blood_pressure_app/features/input/forms/form_base.dart';
  2import 'package:blood_pressure_app/model/storage/settings_store.dart';
  3import 'package:flutter/material.dart';
  4import 'package:flutter/services.dart';
  5import 'package:blood_pressure_app/l10n/app_localizations.dart';
  6import 'package:provider/provider.dart';
  7
  8/// Form to enter freeform text and select color.
  9class BloodPressureForm extends FormBase<({int? sys, int? dia, int? pul})> {
 10  /// Create form to enter freeform text and select color.
 11  const BloodPressureForm({super.key,
 12    super.initialValue,
 13  });
 14
 15  @override
 16  BloodPressureFormState createState() => BloodPressureFormState();
 17}
 18
 19/// State of form to enter freeform text and select color.
 20class BloodPressureFormState extends FormStateBase<({int? sys, int? dia, int? pul}), BloodPressureForm> {
 21  final _formKey = GlobalKey<FormState>();
 22
 23  final _sysFocusNode = FocusNode();
 24  final _diaFocusNode = FocusNode();
 25  final _pulFocusNode = FocusNode();
 26
 27  late final TextEditingController _sysController;
 28  late final TextEditingController _diaController;
 29  late final TextEditingController _pulController;
 30
 31  @override
 32  void initState() {
 33    super.initState();
 34    _sysController = TextEditingController(text: widget.initialValue?.sys?.toString() ?? '');
 35    _diaController = TextEditingController(text: widget.initialValue?.dia?.toString() ?? '');
 36    _pulController = TextEditingController(text: widget.initialValue?.pul?.toString() ?? '');
 37    _sysFocusNode.requestFocus();
 38  }
 39
 40  @override
 41  void dispose() {
 42    _sysFocusNode.dispose();
 43    _diaFocusNode.dispose();
 44    _pulFocusNode.dispose();
 45    _sysController.dispose();
 46    _diaController.dispose();
 47    _pulController.dispose();
 48    super.dispose();
 49  }
 50
 51  @override
 52  bool validate() {
 53    if (_sysController.text.isEmpty
 54        && _diaController.text.isEmpty
 55        && _pulController.text.isEmpty) {
 56      return true;
 57    }
 58    return _formKey.currentState?.validate() ?? false;
 59  }
 60
 61  @override
 62  ({int? sys, int? dia, int? pul})? save() {
 63    if (!validate()
 64      || (int.tryParse(_sysController.text) == null
 65      && int.tryParse(_diaController.text) == null
 66      && int.tryParse(_pulController.text) == null)) {
 67      return null;
 68    }
 69    return (
 70      sys: int.tryParse(_sysController.text),
 71      dia: int.tryParse(_diaController.text),
 72      pul: int.tryParse(_pulController.text),
 73    );
 74  }
 75
 76  @override
 77  bool isEmptyInputFocused() => (_diaFocusNode.hasFocus && _diaController.text.isEmpty)
 78   || (_pulFocusNode.hasFocus && _pulController.text.isEmpty);
 79
 80  @override
 81  void fillForm(({int? dia, int? pul, int? sys})? value) => setState(() {
 82    if (value == null) {
 83        _sysController.text = '';
 84        _diaController.text = '';
 85        _pulController.text = '';
 86    } else {
 87      if (value.dia != null) _diaController.text = value.dia.toString();
 88      if (value.pul != null) _pulController.text = value.pul.toString();
 89      if (value.sys != null) _sysController.text = value.sys.toString();
 90    }
 91  });
 92
 93  Widget _buildValueInput({
 94    String? labelText,
 95    FocusNode? focusNode,
 96    TextEditingController? controller,
 97    String? Function(String?)? validator,
 98  }) => Expanded(
 99    child: TextFormField(
100      focusNode: focusNode,
101      controller: controller,
102      keyboardType: TextInputType.number,
103      inputFormatters: [FilteringTextInputFormatter.digitsOnly],
104      onChanged: (String value) {
105        if (value.isNotEmpty
106            && (int.tryParse(value) ?? -1) > 40) {
107          FocusScope.of(context).nextFocus();
108        }
109      },
110      validator: (String? value) {
111        final settings = context.read<Settings>();
112        if (!settings.allowMissingValues
113            && (value == null
114                || value.isEmpty
115                || int.tryParse(value) == null)) {
116          return AppLocalizations.of(context)!.errNaN;
117        } else if (settings.validateInputs
118            && (int.tryParse(value ?? '') ?? -1) <= 30) {
119          return AppLocalizations.of(context)!.errLt30;
120        } else if (settings.validateInputs
121            && (int.tryParse(value ?? '') ?? 0) >= 400) {
122          // https://pubmed.ncbi.nlm.nih.gov/7741618/
123          return AppLocalizations.of(context)!.errUnrealistic;
124        }
125        return validator?.call(value);
126      },
127      decoration: InputDecoration(
128        labelText: labelText,
129      ),
130      style: Theme.of(context).textTheme.bodyLarge,
131    ),
132  );
133
134  @override
135  Widget build(BuildContext context) => Form(
136    key: _formKey,
137    child: Row(
138      mainAxisSize: MainAxisSize.min,
139      children: [
140        _buildValueInput(
141          focusNode: _sysFocusNode,
142          controller: _sysController,
143          labelText: AppLocalizations.of(context)!.sysLong,
144        ),
145        const SizedBox(width: 8,),
146        _buildValueInput(
147          labelText: AppLocalizations.of(context)!.diaLong,
148          controller: _diaController,
149          focusNode: _diaFocusNode,
150          validator: (value) {
151            if (context.read<Settings>().validateInputs
152              && (int.tryParse(value ?? '') ?? 0)
153                >= (int.tryParse(_sysController.text) ?? 1)) {
154              return AppLocalizations.of(context)?.errDiaGtSys;
155            }
156            return null;
157          },
158        ),
159        const SizedBox(width: 8,),
160        _buildValueInput(
161          controller: _pulController,
162          focusNode: _pulFocusNode,
163          labelText: AppLocalizations.of(context)!.pulLong,
164        ),
165      ],
166    ),
167  );
168}