import 'package:diameter/components/forms/boolean_form_field.dart'; import 'package:diameter/components/forms/number_form_field.dart'; import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class SettingsScreen extends StatefulWidget { static const String routeName = '/settings'; const SettingsScreen({Key? key}) : super(key: key); @override _SettingsScreenState createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { late Settings _settings; final ScrollController _scrollController = ScrollController(); bool _measurementsIsExpanded = true; bool _promptsIsExpanded = true; bool _formatIsExpanded = true; final _nutritionMeasurementLabelController = TextEditingController(text: ''); final _glucoseMeasurementLabelController = TextEditingController(text: ''); final _dateFormatController = TextEditingController(text: ''); final _longDateFormatController = TextEditingController(text: ''); final _timeFormatController = TextEditingController(text: ''); final _longTimeFormatController = TextEditingController(text: ''); final _insulinIncrementsController = TextEditingController(text: ''); final _nutritionIncrementsController = TextEditingController(text: ''); final _mmolPerLIncrementsController = TextEditingController(text: ''); final _targetGlucoseMgPerDlController = TextEditingController(text: ''); final _targetGlucoseMmolPerLController = TextEditingController(text: ''); late bool _onlyDisplayActiveGlucoseMeasurement; late bool _displayBothGlucoseMeasurementsInDetailView; late bool _displayBothGlucoseMeasurementsInListView; late bool _showConfirmationDialogOnCancel; late bool _showConfirmationDialogOnDelete; late bool _showConfirmationDialogOnStopEvent; @override void initState() { super.initState(); _settings = Settings.get(); _nutritionMeasurementLabelController.text = nutritionMeasurementLabels[_settings.nutritionMeasurementIndex]; _glucoseMeasurementLabelController.text = glucoseMeasurementLabels[_settings.glucoseMeasurementIndex]; _insulinIncrementsController.text = _settings.insulinIncrements.toString(); _nutritionIncrementsController.text = _settings.nutritionIncrements.toString(); _mmolPerLIncrementsController.text = _settings.mmolPerLIncrements.toString(); _targetGlucoseMgPerDlController.text = _settings.targetGlucoseMgPerDl.toInt().toString(); _targetGlucoseMmolPerLController.text = _settings.targetGlucoseMmolPerL.toString(); _onlyDisplayActiveGlucoseMeasurement = _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.activeOnly.index; _displayBothGlucoseMeasurementsInDetailView = _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.both.index || _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.bothForDetail.index; _displayBothGlucoseMeasurementsInListView = _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.both.index || _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.bothForList.index; _dateFormatController.text = _settings.dateFormat; _longDateFormatController.text = _settings.longDateFormat ?? ''; _timeFormatController.text = _settings.timeFormat; _longTimeFormatController.text = _settings.longTimeFormat ?? ''; _showConfirmationDialogOnCancel = _settings.showConfirmationDialogOnCancel; _showConfirmationDialogOnDelete = _settings.showConfirmationDialogOnDelete; _showConfirmationDialogOnStopEvent = _settings.showConfirmationDialogOnStopEvent; } @override void dispose() { _scrollController.dispose(); _nutritionMeasurementLabelController.dispose(); _glucoseMeasurementLabelController.dispose(); _dateFormatController.dispose(); _longDateFormatController.dispose(); _timeFormatController.dispose(); _longTimeFormatController.dispose(); _insulinIncrementsController.dispose(); _nutritionIncrementsController.dispose(); _mmolPerLIncrementsController.dispose(); _targetGlucoseMgPerDlController.dispose(); _targetGlucoseMmolPerLController.dispose(); super.dispose(); } void reload({String? message}) { setState(() { _settings = Settings.get(); }); setState(() { if (message != null) { var snackBar = SnackBar( content: Text(message), duration: const Duration(seconds: 2), ); ScaffoldMessenger.of(context) ..removeCurrentSnackBar() ..showSnackBar(snackBar); } }); } void saveSettings() { Settings.put(Settings( id: _settings.id, nutritionMeasurementIndex: nutritionMeasurementLabels .indexOf(_nutritionMeasurementLabelController.text), glucoseMeasurementIndex: glucoseMeasurementLabels .indexOf(_glucoseMeasurementLabelController.text), glucoseDisplayModeIndex: _onlyDisplayActiveGlucoseMeasurement ? GlucoseDisplayMode.activeOnly.index : _displayBothGlucoseMeasurementsInDetailView && _displayBothGlucoseMeasurementsInListView ? GlucoseDisplayMode.both.index : _displayBothGlucoseMeasurementsInDetailView ? GlucoseDisplayMode.bothForDetail.index : GlucoseDisplayMode.bothForList.index, targetGlucoseMgPerDl: int.tryParse(_targetGlucoseMgPerDlController.text) ?? _settings.targetGlucoseMgPerDl, targetGlucoseMmolPerL: double.tryParse(_targetGlucoseMmolPerLController.text) ?? _settings.targetGlucoseMmolPerL, insulinIncrements: double.tryParse(_insulinIncrementsController.text) ?? _settings.insulinIncrements, nutritionIncrements: double.tryParse(_nutritionIncrementsController.text) ?? _settings.nutritionIncrements, mmolPerLIncrements: double.tryParse(_mmolPerLIncrementsController.text) ?? _settings.mmolPerLIncrements, dateFormat: _dateFormatController.text, longDateFormat: _longDateFormatController.text, timeFormat: _timeFormatController.text, longTimeFormat: _longTimeFormatController.text, showConfirmationDialogOnCancel: _showConfirmationDialogOnCancel, showConfirmationDialogOnDelete: _showConfirmationDialogOnDelete, showConfirmationDialogOnStopEvent: _showConfirmationDialogOnStopEvent, )); reload(message: 'Settings updated'); } void onReset() { Settings.reset(); reload(message: 'Settings have been reset to default'); } void handleResetAction() async { DialogUtils.showConfirmationDialog( context: context, onConfirm: onReset, message: 'Are you sure you want to reset all settings?', ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Application Settings'), ), drawer: const Navigation(currentLocation: SettingsScreen.routeName), body: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(bottom: 10.0), child: GestureDetector( onTap: () => setState(() { _measurementsIsExpanded = !_measurementsIsExpanded; }), child: Row( children: [ Expanded( child: Text( 'MEASUREMENTS', style: Theme.of(context).textTheme.subtitle2, ), ), Icon(_measurementsIsExpanded ? Icons.expand_less : Icons.expand_more), ], ), ), ), Column( children: _measurementsIsExpanded ? [ AutoCompleteDropdownButton( controller: _nutritionMeasurementLabelController, selectedItem: _nutritionMeasurementLabelController.text, label: 'Preferred Nutrition Measurement', items: nutritionMeasurementLabels, onChanged: (value) { _nutritionMeasurementLabelController.text = value ?? ''; saveSettings(); }, ), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: AutoCompleteDropdownButton( controller: _glucoseMeasurementLabelController, selectedItem: _glucoseMeasurementLabelController.text, label: 'Preferred Glucose Measurement', items: glucoseMeasurementLabels, onChanged: (value) { _glucoseMeasurementLabelController.text = value ?? ''; saveSettings(); }, ), ), Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? NumberFormField( label: 'Target glucose', suffix: 'mg/dl', controller: _targetGlucoseMgPerDlController, showSteppers: false, onChanged: (_) async { await Future.delayed( const Duration(seconds: 1)); if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl) { final value = int.tryParse( _targetGlucoseMgPerDlController.text); _targetGlucoseMmolPerLController.text = Utils .toStringMatchingTemplateFractionPrecision( Utils.convertMgPerDlToMmolPerL(value ?? 0), Settings.mmolPerLSteps); await Future.delayed( const Duration(seconds: 1)); saveSettings(); } }, ) : NumberFormField( label: 'Target glucose', suffix: 'mmol/l', controller: _targetGlucoseMmolPerLController, showSteppers: false, onChanged: (_) async { await Future.delayed( const Duration(seconds: 1)); if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL) { final value = double.tryParse( _targetGlucoseMmolPerLController.text); _targetGlucoseMgPerDlController.text = Utils.convertMmolPerLToMgPerDl(value ?? 0) .toString(); saveSettings(); } }, ), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: NumberFormField( controller: _insulinIncrementsController, showSteppers: false, label: 'Insulin increment', onChanged: (value) { _insulinIncrementsController.text = (value ?? 0).toString(); saveSettings(); }), ), NumberFormField( controller: _nutritionIncrementsController, showSteppers: false, label: 'Nutrition increment', onChanged: (value) { _nutritionIncrementsController.text = (value ?? 0).toString(); saveSettings(); }), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: NumberFormField( controller: _mmolPerLIncrementsController, showSteppers: false, label: 'Mmol/L increment', onChanged: (value) { _mmolPerLIncrementsController.text = (value ?? 0).toString(); saveSettings(); }), ), BooleanFormField( value: _onlyDisplayActiveGlucoseMeasurement, label: 'only display active glucose measurement', onChanged: (value) { _onlyDisplayActiveGlucoseMeasurement = value; saveSettings(); }, ), BooleanFormField( value: _displayBothGlucoseMeasurementsInDetailView, enabled: !_onlyDisplayActiveGlucoseMeasurement, label: 'display both glucose measurements in detail view', onChanged: (value) { _displayBothGlucoseMeasurementsInDetailView = value; saveSettings(); }, ), BooleanFormField( value: _displayBothGlucoseMeasurementsInListView, enabled: !_onlyDisplayActiveGlucoseMeasurement, label: 'display both glucose measurements in list view', onChanged: (value) { _displayBothGlucoseMeasurementsInListView = value; saveSettings(); }, ), ] : [], ), const Divider(), Padding( padding: const EdgeInsets.only(bottom: 10.0), child: GestureDetector( onTap: () => setState(() { _promptsIsExpanded = !_promptsIsExpanded; }), child: Row( mainAxisSize: MainAxisSize.max, children: [ Expanded( child: Text( 'CONFIRMATION PROMPTS', style: Theme.of(context).textTheme.subtitle2, ), ), Icon(_promptsIsExpanded ? Icons.expand_less : Icons.expand_more), ], ), ), ), Column( children: _promptsIsExpanded ? [ BooleanFormField( value: _showConfirmationDialogOnCancel, label: 'on cancelling edit or creation of a record if changes have already been made', onChanged: (value) { _showConfirmationDialogOnCancel = value; saveSettings(); }, ), BooleanFormField( value: _showConfirmationDialogOnDelete, label: 'on deleting a record', onChanged: (value) { _showConfirmationDialogOnDelete = value; saveSettings(); }, ), BooleanFormField( value: _showConfirmationDialogOnStopEvent, label: 'on stopping (ending) an event', onChanged: (value) { _showConfirmationDialogOnStopEvent = value; saveSettings(); }, ), ] : [], ), const Divider(), Padding( padding: const EdgeInsets.only(bottom: 10.0), child: GestureDetector( onTap: () => setState(() { _formatIsExpanded = !_formatIsExpanded; }), child: Row( mainAxisSize: MainAxisSize.max, children: [ Expanded( child: Text( 'TIME & DATE FORMAT', style: Theme.of(context).textTheme.subtitle2, ), ), Icon(_formatIsExpanded ? Icons.expand_less : Icons.expand_more), ], ), ), ), Column( children: _formatIsExpanded ? [ Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextFormField( controller: _dateFormatController, decoration: const InputDecoration( labelText: 'Date Format', ), validator: (value) { if (value!.trim().isEmpty) { return 'Empty title'; } return null; }, ), ), Expanded( child: Padding( padding: const EdgeInsets.only( left: 5.0, bottom: 10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Example', textScaleFactor: 0.75), Text( DateFormat(_dateFormatController.text) .format(DateTime.now()), textScaleFactor: 1.25, ), ], ), ), ), ], ), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextFormField( controller: _longDateFormatController, decoration: const InputDecoration( labelText: 'Long Date Format', ), validator: (value) { if (value!.trim().isEmpty) { return 'Empty title'; } return null; }, ), ), Expanded( child: Padding( padding: const EdgeInsets.only( left: 5.0, bottom: 10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Example', textScaleFactor: 0.75), Text( DateFormat(_longDateFormatController.text) .format(DateTime.now()), textScaleFactor: 1.25, ), ], ), ), ), ], ), ), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextFormField( controller: _timeFormatController, decoration: const InputDecoration( labelText: 'Time Format', ), validator: (value) { if (value!.trim().isEmpty) { return 'Empty title'; } return null; }, ), ), Expanded( child: Padding( padding: const EdgeInsets.only( left: 5.0, bottom: 10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Example', textScaleFactor: 0.75), Text( DateFormat(_timeFormatController.text) .format(DateTime.now()), textScaleFactor: 1.25, ), ], ), ), ), ], ), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextFormField( controller: _longTimeFormatController, decoration: const InputDecoration( labelText: 'Long Time Format', ), validator: (value) { if (value!.trim().isEmpty) { return 'Empty title'; } return null; }, ), ), Expanded( child: Padding( padding: const EdgeInsets.only( left: 5.0, bottom: 10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Example', textScaleFactor: 0.75), Text( DateFormat(_longTimeFormatController.text) .format(DateTime.now()), textScaleFactor: 1.25, ), ], ), ), ), ], ), ), ] : [], ), ], ), ), bottomNavigationBar: BottomAppBar( child: Padding( padding: const EdgeInsets.all(10.0), child: Row( children: [ ElevatedButton.icon( onPressed: handleResetAction, icon: const Icon( Icons.settings_backup_restore, size: 18.0, ), label: const Text('RESET ALL'), ), const Spacer(), ], ), ), ), ); } }