diff --git a/TODO b/TODO index 34d2f76..043b8b7 100644 --- a/TODO +++ b/TODO @@ -1,53 +1,95 @@ MAIN TASKS: - General/Framework: - ☐ create app icon - ☐ show indicator and make all fields readonly if user somehow gets to a deleted record detail view - ☐ clean up controllers (dispose method of each stateful widget) - ☐ account for deleted/disabled elements in dropdowns - ☐ check through all detail forms and set required fields/according messages + Components/Framework: + ☐ update number fields to use corresponding components + ☐ meal detail (carbs ratio, portion size, carbs per portion) + ☐ log meal detail (amount, carbs ratio, portion size, carbs per portion) + ☐ add "set manually" switch (like in log bolus detail) wherever parameters can be calculated from others + ☐ meal detail + ☐ log meal detail + ☐ come up with new concept for duration component + ☐ update duration fields to use corresponding component + ☐ log event type detail (reminder duration) + ☐ log event detail (reminder duration) + ☐ meal (bolus delay) + ☐ log bolus (delay) + ☐ put dropdowns first if they override name field ☐ set name properties as unique (and add checks to forms) - ☐ implement component for durations + ☐ check through all detail forms and set required fields/according messages ☐ change placement of delete and floating button because its very easy to accidentally hit delete - Recipe: - ✔ recipe list screen @done(21-12-11 22:01) - ✔ recipe detail screen @done(21-12-11 22:01) - ☐ add functionality to create a meal from a recipe - Event Types: - ☐ add colors as indicators for log entries (and later graphs in reports) - Settings: - ☐ add setting for decimal places/unit steps - ☐ add fields for preferred date and time formats - ☐ add fields for glucose target (as map of cutoff glucose and colors) - ☐ add field for active insulin duration - ☐ add setting for carb units/bread units - ☐ add option to switch 'save' and 'save & close' buttons - ☐ add functionality to delete dead records (meaning: set deleted flag and no relations to undeleted records) + ☐ implement deletion by swiping left on item instead? + ☐ check for changes before navigating as well (not just on cancel) -FUTURE TASKS: - General/Framework: - ☐ setup objectbox sync server - ☐ add explanations to each section - ☐ evaluate if some fields should be readonly instead of completely hidden - ☐ alternate languages - ☐ log hba1c + Log Overview: + ☐ only show current day + ☐ add calendar field on top to navigate + ☐ use currently selected day when adding a log entry + Event Types: + ☐ add pagination Reports: ☐ evaluate what type of reports there should be + ☐ try out graph/diagram components + +FUTURE TASKS: + Features: + ☐ app icon + ☐ desktop version + ☐ add explanations to each section + ☐ alternate languages + ☐ log hba1c + ☐ indicate nested creation process (creating from dropdown etc) + ☐ enable restoring data from sync + ☐ indicate read only fields + Components/Framework: + ☐ show indicator and make all fields readonly if user somehow gets to a deleted record detail view + ☐ dropdown tweaks + ☐ edit item -> cancel: shouldn't clear dropdwon + ☐ keep focus on textfield when typing + ☐ account for deleted/disabled elements + Accuracy: + ☐ same icons in detail as in overview to indicate what's what + Recipe: + ☐ update to use correct components, init/dispose etc + ☐ change the entire concept of ingredients + ☐ add functionality to create a meal from a recipe + Reports: ☐ meal tweaking ☐ bolus tweaking + ☐ basal test ☐ daily graph (showing glucose curve, events, boli and meals) Log Overview: - ☐ add pagination ☐ add filters Log Entry: ☐ check if there is still an active bolus when suggesting glucose bolus Event Types: - ☐ add pagination + ☐ add colors as indicators for log entries (and later graphs in reports) ☐ implement reminders as push notifications Settings: ☐ add option to hide extra customization options (ie. changing pre calculated values)? ☐ option to switch theme + ☐ add fields for glucose target tiers (as map of cutoff glucose and colors) + ☐ add field for active insulin duration + ☐ add setting for carb units/bread units + ☐ add option to switch 'save' and 'save & close' buttons + ☐ add functionality to delete dead records (meaning: set deleted flag and no relations to undeleted records) Archive: + ✔ settings (target glucose, increments) @done(22-01-22 01:48) @project(MAIN TASKS.Components/Framework) + ✔ accuracy detail (confidence rating) @done(22-01-21 16:51) @project(MAIN TASKS.Components/Framework) + ✔ basal detail (units) @done(22-01-21 18:14) @project(MAIN TASKS.Components/Framework) + ✔ bolus detail (units, per carbs, per glucose) @done(22-01-21 20:35) @project(MAIN TASKS.Components/Framework) + ✔ log entry (glucose) @done(22-01-22 15:13) @project(MAIN TASKS.Components/Framework) + ✔ log bolus detail (units, current, target, correction, carbs) @done(22-01-22 22:59) @project(MAIN TASKS.Components/Framework) + ✔ add dispose methods everywhere and clean up controllers @done(22-01-21 17:55) @project(MAIN TASKS.Components/Framework) + ✔ fix spacing @done(22-01-21 17:20) @project(MAIN TASKS.Event Types) + ✔ calculation log meal carbs @done(22-01-08 22:21) @project(BUG FIXES.Log Entry) + ✔ implement component for durations @done(22-01-08 19:00) @project(MAIN TASKS.General/Framework) + ✔ make glucose optional @done(22-01-08 19:00) @project(MAIN TASKS.Log Entry) + ✔ add setting for decimal places/unit steps @done(22-01-08 22:18) @project(MAIN TASKS.Settings) + ✔ add fields for preferred date and time formats @done(22-01-07 21:06) @project(MAIN TASKS.Settings) + ✔ add field for glucose target @done(22-01-08 19:00) @project(MAIN TASKS.Settings) + ✔ setup objectbox sync server @done(21-12-22 15:21) @project(FUTURE TASKS.General/Framework) + ✔ recipe list screen @done(21-12-11 22:01) @project(MAIN TASKS.Recipe) + ✔ recipe detail screen @done(21-12-11 22:01) @project(MAIN TASKS.Recipe) ✔ add model for recipe @done(21-12-11 02:23) @project(MAIN TASKS.Recipe) ✔ add model for ingredient (relation betweeen recipe and meal) @done(21-12-11 02:23) @project(MAIN TASKS.Recipe) ✔ give option to specify quantity @done(21-12-11 01:28) @project(MAIN TASKS.Log Entry) diff --git a/android/app/build.gradle b/android/app/build.gradle index f609987..467296d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/android/build.gradle b/android/build.gradle index 44bf4e1..0e71045 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() diff --git a/lib/components/forms.dart b/lib/components/forms.dart deleted file mode 100644 index fc0d76d..0000000 --- a/lib/components/forms.dart +++ /dev/null @@ -1,239 +0,0 @@ -import 'package:flutter/material.dart'; - -class FormWrapper extends StatefulWidget { - final List? fields; - final List? buttons; - final GlobalKey? formState; - - const FormWrapper({Key? key, this.formState, this.fields, this.buttons}) - : super(key: key); - - @override - _FormWrapperState createState() => _FormWrapperState(); -} - -class _FormWrapperState extends State { - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Form( - key: widget.formState, - child: Column( - children: [ - Column( - children: widget.fields - ?.map((e) => Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: e)) - .toList() ?? - [], - ), - Container( - padding: const EdgeInsets.only(top: 10.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: widget.buttons ?? [], - ), - ), - ], - ), - ), - ); - } -} - -class BooleanFormField extends StatefulWidget { - final bool value; - final String label; - final void Function(bool) onChanged; - final bool? enabled; - final EdgeInsets? contentPadding; - - const BooleanFormField( - {Key? key, - required this.value, - required this.label, - required this.onChanged, - this.enabled, - this.contentPadding}) - : super(key: key); - - @override - _BooleanFormFieldState createState() => _BooleanFormFieldState(); -} - -class _BooleanFormFieldState extends State { - @override - Widget build(BuildContext context) { - return FormField(builder: (state) { - return ListTile( - contentPadding: widget.contentPadding, - onTap: () => widget.onChanged(!widget.value), - trailing: Switch( - value: widget.value, - onChanged: widget.onChanged, - ), - title: Text(widget.label), - enabled: widget.enabled ?? true, - ); - }); - } -} - -class DateTimeFormField extends StatefulWidget { - final DateTime date; - final DateTime? minDate; - final DateTime? maxDate; - final TextEditingController controller; - final String label; - final void Function(DateTime?) onChanged; - - const DateTimeFormField( - {Key? key, - required this.date, - this.minDate, - this.maxDate, - required this.controller, - required this.label, - required this.onChanged}) - : super(key: key); - - @override - _DateTimeFormFieldState createState() => _DateTimeFormFieldState(); -} - -class _DateTimeFormFieldState extends State { - @override - Widget build(BuildContext context) { - return TextFormField( - readOnly: true, - controller: widget.controller, - decoration: InputDecoration( - labelText: widget.label, - ), - onTap: () async { - final newTime = await showDatePicker( - context: context, - initialDate: widget.date, - firstDate: widget.minDate ?? DateTime(2000, 1, 1), - lastDate: - widget.maxDate ?? DateTime.now().add(const Duration(days: 365)), - ); - widget.onChanged(newTime); - }, - ); - } -} - -class TimeOfDayFormField extends StatefulWidget { - final TimeOfDay time; - final TextEditingController controller; - final String label; - final void Function(TimeOfDay?) onChanged; - - const TimeOfDayFormField( - {Key? key, - required this.time, - required this.controller, - required this.label, - required this.onChanged}) - : super(key: key); - - @override - _TimeOfDayFormFieldState createState() => _TimeOfDayFormFieldState(); -} - -class _TimeOfDayFormFieldState extends State { - @override - Widget build(BuildContext context) { - return TextFormField( - readOnly: true, - controller: widget.controller, - decoration: InputDecoration( - labelText: widget.label, - ), - onTap: () async { - final newTime = await showTimePicker( - context: context, - initialTime: widget.time, - ); - widget.onChanged(newTime); - }, - ); - } -} - -class NumberFormField extends StatefulWidget { - final TextEditingController controller; - final String label; - final String? suffix; - final void Function(double?) onChanged; - final double? min; - final double? max; - final double step; - - const NumberFormField( - {Key? key, - required this.controller, - required this.label, - this.suffix, - required this.onChanged, - this.min, - this.max, - this.step = 1}) - : super(key: key); - - @override - _NumberFormFieldState createState() => _NumberFormFieldState(); -} - -class _NumberFormFieldState extends State { - void onIncrease() { - double value = double.tryParse(widget.controller.text) ?? 0; - if (widget.max == null || value + widget.step <= widget.max!) { - value += widget.step; - widget.onChanged(value); - } - } - - void onDecrease() { - double value = double.tryParse(widget.controller.text) ?? 0; - if (widget.min == null || value - widget.step >= widget.min!) { - value -= widget.step; - widget.onChanged(value); - } - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: onDecrease, - icon: const Icon(Icons.remove), - ), - Expanded( - child: TextFormField( - controller: widget.controller, - decoration: InputDecoration( - labelText: widget.label, - suffixText: widget.suffix, - ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - onChanged: (value) async { - await Future.delayed(const Duration(seconds: 1)); - widget.onChanged(double.tryParse(value)); - }, - ), - ), - IconButton( - onPressed: onIncrease, - icon: const Icon(Icons.add), - ), - ], - ); - } -} diff --git a/lib/components/dropdown.dart b/lib/components/forms/auto_complete_dropdown_button.dart similarity index 100% rename from lib/components/dropdown.dart rename to lib/components/forms/auto_complete_dropdown_button.dart diff --git a/lib/components/forms/boolean_form_field.dart b/lib/components/forms/boolean_form_field.dart new file mode 100644 index 0000000..16f5e52 --- /dev/null +++ b/lib/components/forms/boolean_form_field.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class BooleanFormField extends StatefulWidget { + final bool value; + final String label; + final void Function(bool) onChanged; + final bool? enabled; + final EdgeInsets? contentPadding; + + const BooleanFormField( + {Key? key, + required this.value, + required this.label, + required this.onChanged, + this.enabled, + this.contentPadding}) + : super(key: key); + + @override + _BooleanFormFieldState createState() => _BooleanFormFieldState(); +} + +class _BooleanFormFieldState extends State { + @override + Widget build(BuildContext context) { + return FormField(builder: (state) { + return ListTile( + contentPadding: widget.contentPadding, + onTap: () => widget.onChanged(!widget.value), + trailing: Switch( + value: widget.value, + onChanged: widget.onChanged, + ), + title: Text(widget.label), + enabled: widget.enabled ?? true, + ); + }); + } +} \ No newline at end of file diff --git a/lib/components/forms/date_time_form_field.dart b/lib/components/forms/date_time_form_field.dart new file mode 100644 index 0000000..091202e --- /dev/null +++ b/lib/components/forms/date_time_form_field.dart @@ -0,0 +1,47 @@ + +import 'package:flutter/material.dart'; + +class DateTimeFormField extends StatefulWidget { + final DateTime date; + final DateTime? minDate; + final DateTime? maxDate; + final TextEditingController controller; + final String label; + final void Function(DateTime?) onChanged; + + const DateTimeFormField( + {Key? key, + required this.date, + this.minDate, + this.maxDate, + required this.controller, + required this.label, + required this.onChanged}) + : super(key: key); + + @override + _DateTimeFormFieldState createState() => _DateTimeFormFieldState(); +} + +class _DateTimeFormFieldState extends State { + @override + Widget build(BuildContext context) { + return TextFormField( + readOnly: true, + controller: widget.controller, + decoration: InputDecoration( + labelText: widget.label, + ), + onTap: () async { + final newTime = await showDatePicker( + context: context, + initialDate: widget.date, + firstDate: widget.minDate ?? DateTime(2000, 1, 1), + lastDate: + widget.maxDate ?? DateTime.now().add(const Duration(days: 365)), + ); + widget.onChanged(newTime); + }, + ); + } +} diff --git a/lib/components/forms/duration_form_field.dart b/lib/components/forms/duration_form_field.dart new file mode 100644 index 0000000..d6867cf --- /dev/null +++ b/lib/components/forms/duration_form_field.dart @@ -0,0 +1,123 @@ + +import 'package:flutter/material.dart'; + +class DurationFormField extends StatefulWidget { + final String label; + final int minutes; + final void Function(int?) onChanged; + final bool showSteppers; + final bool readOnly; + final int min; + final int? max; + final int step; + + const DurationFormField( + {Key? key, + required this.label, + this.minutes = 0, + required this.onChanged, + this.showSteppers = false, + this.readOnly = false, + this.min = 0, + this.max, + this.step = 5}) + : super(key: key); + + @override + _DurationFormFieldState createState() => _DurationFormFieldState(); +} + +class _DurationFormFieldState extends State { + late Duration duration; + final TextEditingController controller = TextEditingController(text: ''); + + @override + void initState() { + super.initState(); + updateDuration(); + } + + void updateDuration() { + duration = Duration(minutes: widget.minutes); + + int days = duration.inDays; + int hours = duration.inHours - days * 24; + int minutes = duration.inMinutes - hours * 60; + int seconds = duration.inSeconds - minutes * 60; + + String daysString = days > 9 ? '$days d' : days > 0 ? '0$days d' : '00 d'; + String hoursString = hours > 9 ? ' $hours h' : hours > 0 ? ' 0$hours h' : ' 00 h'; + String minutesString = minutes > 9 ? ' $minutes m' : minutes > 0 ? ' 0$minutes m' : ' 00 m'; + String secondsString = seconds > 9 ? ' $seconds s' : seconds > 0 ? ' 0$seconds s' : ' 00 s'; + controller.text = '$daysString $hoursString $minutesString $secondsString'.trim(); + } + + void handleChange(String value) async { + await Future.delayed(const Duration(seconds: 1)); + + int days = int.tryParse(value.split(' d')[0]) ?? 0; + int hours = int.tryParse(value.split('d')[1].split(' h')[0]) ?? 0; + int minutes = int.tryParse(value.split('h')[1].split(' m')[0]) ?? 0; + int seconds = int.tryParse(value.split('m')[1].split(' s')[0]) ?? 0; + int updatedMinutes = + Duration(days: days, hours: hours, minutes: minutes, seconds: seconds) + .inMinutes; + + widget.onChanged(updatedMinutes); + setState(() { + updateDuration(); + }); + } + + void onIncrease() { + if (widget.max == null || widget.minutes + widget.step <= widget.max!) { + int value = widget.minutes + widget.step; + widget.onChanged(value); + setState(() { + updateDuration(); + }); + } + } + + void onDecrease() { + if (widget.minutes - widget.step >= widget.min) { + int value = widget.minutes - widget.step; + widget.onChanged(value); + setState(() { + updateDuration(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + widget.showSteppers + ? IconButton( + onPressed: onDecrease, + icon: const Icon(Icons.remove), + ) + : Container(), + Expanded( + child: TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: widget.label, + ), + keyboardType: TextInputType.numberWithOptions( + decimal: true, signed: widget.min.isNegative), + onChanged: handleChange, + ), + ), + widget.showSteppers + ? IconButton( + onPressed: onIncrease, + icon: const Icon(Icons.add), + ) + : Container(), + ], + ); + } +} diff --git a/lib/components/forms/form_wrapper.dart b/lib/components/forms/form_wrapper.dart new file mode 100644 index 0000000..2010875 --- /dev/null +++ b/lib/components/forms/form_wrapper.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class FormWrapper extends StatefulWidget { + final List? fields; + final List? buttons; + final GlobalKey? formState; + + const FormWrapper({Key? key, this.formState, this.fields, this.buttons}) + : super(key: key); + + @override + _FormWrapperState createState() => _FormWrapperState(); +} + +class _FormWrapperState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Form( + key: widget.formState, + child: Column( + children: [ + Column( + children: widget.fields + ?.map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: e)) + .toList() ?? + [], + ), + Container( + padding: const EdgeInsets.only(top: 10.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: widget.buttons ?? [], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/forms/number_form_field.dart b/lib/components/forms/number_form_field.dart new file mode 100644 index 0000000..42e16ab --- /dev/null +++ b/lib/components/forms/number_form_field.dart @@ -0,0 +1,137 @@ +import 'package:diameter/components/repeat_on_hold_button.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:flutter/material.dart'; + +class NumberFormField extends StatefulWidget { + final TextEditingController controller; + final double min; + final double? max; + final double step; + final String label; + final String? suffix; + final void Function(double?) onChanged; + final bool readOnly; + final bool showSteppers; + final bool autoRoundToMultipleOfStep; + final String? Function(String?)? validator; + + const NumberFormField({ + Key? key, + required this.controller, + required this.label, + required this.onChanged, + this.suffix, + this.min = 0, + this.max, + this.step = 1, + this.readOnly = false, + this.showSteppers = true, + this.autoRoundToMultipleOfStep = false, + this.validator, + }) : super(key: key); + + @override + _NumberFormFieldState createState() => _NumberFormFieldState(); +} + +class _NumberFormFieldState extends State { + int precision = 1; + + @override + void initState() { + super.initState(); + precision = Utils.getFractionDigitsLength(widget.step) + 1; + } + + bool onIncrease() { + double? currentValue = double.tryParse(widget.controller.text); + + if (currentValue != null && + (widget.max == null || currentValue + widget.step <= widget.max!)) { + widget.onChanged( + Utils.addDoublesWithPrecision(currentValue, widget.step, precision)); + setState(() {}); + return true; + } + return false; + } + + bool onDecrease() { + double? currentValue = double.tryParse(widget.controller.text); + + if (currentValue != null && (currentValue - widget.step >= widget.min)) { + widget.onChanged( + Utils.addDoublesWithPrecision(currentValue, -widget.step, precision)); + setState(() {}); + return true; + } + return false; + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + widget.showSteppers + ? RepeatOnHoldButton( + onTap: onDecrease, + child: IconButton( + onPressed: double.tryParse(widget.controller.text) != null && + (double.parse(widget.controller.text) - widget.step >= + widget.min) + ? onDecrease + : null, + icon: const Icon(Icons.remove), + ), + ) + : Container(), + Expanded( + child: TextFormField( + readOnly: widget.readOnly, + controller: widget.controller, + decoration: InputDecoration( + labelText: widget.label, + suffixText: widget.suffix, + ), + keyboardType: TextInputType.numberWithOptions( + decimal: widget.step > 0 && widget.step < 1, + signed: widget.min.isNegative), + onChanged: (input) async { + await Future.delayed(const Duration(seconds: 1)); + double? value = double.tryParse(input); + if (value != null && + widget.autoRoundToMultipleOfStep && + (value % widget.step != 0)) { + double remainder = value % widget.step; + value = + Utils.addDoublesWithPrecision(value, -remainder, precision); + if (remainder > widget.step / 2) { + value = Utils.addDoublesWithPrecision( + value, widget.step, precision); + } + } + widget.onChanged(value); + }, + validator: widget.validator, + ), + ), + widget.showSteppers + ? RepeatOnHoldButton( + onTap: onIncrease, + child: IconButton( + onPressed: double.tryParse(widget.controller.text) != null && + (widget.max == null || + double.parse(widget.controller.text) + + widget.step <= + widget.max!) + ? onIncrease + : null, + icon: const Icon(Icons.add), + ), + ) + : Container(), + ], + ); + } +} diff --git a/lib/components/forms/time_of_day_form_field.dart b/lib/components/forms/time_of_day_form_field.dart new file mode 100644 index 0000000..1d7b1da --- /dev/null +++ b/lib/components/forms/time_of_day_form_field.dart @@ -0,0 +1,40 @@ + +import 'package:flutter/material.dart'; + +class TimeOfDayFormField extends StatefulWidget { + final TimeOfDay time; + final TextEditingController controller; + final String label; + final void Function(TimeOfDay?) onChanged; + + const TimeOfDayFormField( + {Key? key, + required this.time, + required this.controller, + required this.label, + required this.onChanged}) + : super(key: key); + + @override + _TimeOfDayFormFieldState createState() => _TimeOfDayFormFieldState(); +} + +class _TimeOfDayFormFieldState extends State { + @override + Widget build(BuildContext context) { + return TextFormField( + readOnly: true, + controller: widget.controller, + decoration: InputDecoration( + labelText: widget.label, + ), + onTap: () async { + final newTime = await showTimePicker( + context: context, + initialTime: widget.time, + ); + widget.onChanged(newTime); + }, + ); + } +} \ No newline at end of file diff --git a/lib/components/repeat_on_hold_button.dart b/lib/components/repeat_on_hold_button.dart new file mode 100644 index 0000000..d0c26f2 --- /dev/null +++ b/lib/components/repeat_on_hold_button.dart @@ -0,0 +1,74 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class RepeatOnHoldButton extends StatefulWidget { + /// Function to be called on tap and on long press. + /// Return [false] to signify that the loop should be broken after execution. + final bool? Function() onTap; + + /// Specifies whether repetition speeds up when the user keeps holding the button. + final bool increaseSpeed; + + /// Specifies how many ms should pass before action is repeated. + final int initialRepetitionIntervalMs; + + /// Specifies by how much the interval between actions should be divided after [speedUpAfterTimes] times. + final int speedUpFactor; + + /// Specifies how many times [onTap] will be called before increasing the speed. + final int speedUpAfterTimes; + + final Widget child; + + const RepeatOnHoldButton({ + Key? key, + required this.onTap, + this.increaseSpeed = true, + this.initialRepetitionIntervalMs = 250, + this.speedUpFactor = 2, + this.speedUpAfterTimes = 5, + required this.child, + }) : super(key: key); + + @override + _RepeatOnHoldButtonState createState() => _RepeatOnHoldButtonState(); +} + +class _RepeatOnHoldButtonState extends State { + bool _isHeld = false; + + void onLongPress() async { + setState(() { + _isHeld = true; + }); + int holdCycle = 0; + int speed = widget.initialRepetitionIntervalMs; + + while (true) { + final result = widget.onTap() ?? true; + if (!_isHeld || !result) { + break; + } + + holdCycle++; + if (speed > 1 && holdCycle % widget.speedUpAfterTimes == 0) { + speed = max(1, (speed ~/ widget.speedUpFactor)); + } + + await Future.delayed( + Duration( + milliseconds: speed, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPress: onLongPress, + onLongPressEnd: (_) => _isHeld = false, + child: widget.child, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 645000d..54b0044 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,7 +37,7 @@ Future main() async { Sync.isAvailable(); SyncClient syncClient = Sync.client( objectBox.store, - 'wss://127.0.0.1:9999', + 'ws://192.168.1.184:9999', SyncCredentials.sharedSecretString(secret) ); syncClient.start(); diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 7d56c16..e43acde 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -53,6 +53,11 @@ class Settings { int targetGlucoseMgPerDl; double targetGlucoseMmolPerL; + double insulinIncrements; + double nutritionIncrements; + double mmolPerLIncrements; + double amountIncrements; + String dateFormat; String? longDateFormat; String timeFormat; @@ -70,6 +75,10 @@ class Settings { this.nutritionMeasurementIndex = 0, this.glucoseDisplayModeIndex = 0, this.glucoseMeasurementIndex = 0, + this.insulinIncrements = 0.05, + this.nutritionIncrements = 0.01, + this.mmolPerLIncrements = 0.1, + this.amountIncrements = 0.05, this.dateFormat = 'MM/dd/yy', this.longDateFormat = 'MMMM dd, yyyy', this.timeFormat = 'HH:mm', @@ -78,7 +87,7 @@ class Settings { this.showConfirmationDialogOnDelete = true, this.showConfirmationDialogOnStopEvent = true, this.targetGlucoseMgPerDl = 100, - this.targetGlucoseMmolPerL = 5.49, + this.targetGlucoseMmolPerL = 5.5, this.useDarkTheme = false, }); @@ -105,6 +114,10 @@ class Settings { static int get targetMgPerDl => get().targetGlucoseMgPerDl; static double get targetMmolPerL => get().targetGlucoseMmolPerL; + static double get insulinSteps => get().insulinIncrements; + static double get nutritionSteps => get().nutritionIncrements; + static double get mmolPerLSteps => get().mmolPerLIncrements; + static ThemeMode get themeMode => get().useDarkTheme ? ThemeMode.dark : ThemeMode.light; diff --git a/lib/navigation.dart b/lib/navigation.dart index cbe5b4e..6d308ad 100644 --- a/lib/navigation.dart +++ b/lib/navigation.dart @@ -113,14 +113,14 @@ class _NavigationState extends State { }, selected: widget.currentLocation == Routes.events, ), - ListTile( - title: const Text('Recipes'), - leading: const Icon(Icons.local_dining), - onTap: () { - selectDestination(Routes.recipes); - }, - selected: Routes.recipeRoutes.contains(widget.currentLocation), - ), + // ListTile( + // title: const Text('Recipes'), + // leading: const Icon(Icons.local_dining), + // onTap: () { + // selectDestination(Routes.recipes); + // }, + // selected: Routes.recipeRoutes.contains(widget.currentLocation), + // ), ListTile( title: const Text('Meals'), leading: const Icon(Icons.dinner_dining), diff --git a/lib/screens/accuracy_detail.dart b/lib/screens/accuracy_detail.dart index 94f0beb..fb5b304 100644 --- a/lib/screens/accuracy_detail.dart +++ b/lib/screens/accuracy_detail.dart @@ -1,9 +1,11 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; +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/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:flutter/material.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/accuracy.dart'; class AccuracyDetailScreen extends StatefulWidget { @@ -25,8 +27,9 @@ class _AccuracyDetailScreenState extends State { final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); - final _confidenceRatingController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); + final _confidenceRatingController = + TextEditingController(text: Accuracy.getAll().length.toString()); bool _forCarbsRatio = true; bool _forPortionSize = true; @@ -39,11 +42,20 @@ class _AccuracyDetailScreenState extends State { _forCarbsRatio = _accuracy!.forCarbsRatio; _forPortionSize = _accuracy!.forPortionSize; _confidenceRatingController.text = - (_accuracy!.confidenceRating ?? '').toString(); + (_accuracy!.confidenceRating ?? Accuracy.getAll().length).toString(); _notesController.text = _accuracy!.notes ?? ''; } } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + _confidenceRatingController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -52,7 +64,7 @@ class _AccuracyDetailScreenState extends State { } _isNew = _accuracy == null; - setState(() { + setState(() { if (message != null) { var snackBar = SnackBar( content: Text(message), @@ -77,10 +89,11 @@ class _AccuracyDetailScreenState extends State { forPortionSize: _forPortionSize, notes: _notesController.text, ); - Accuracy.box.put(accuracy); + Accuracy.put(accuracy); Accuracy.reorder( accuracy, int.tryParse(_confidenceRatingController.text)); - Navigator.pop(context, ['${_isNew ? 'New' : ''} Accuracy saved', accuracy]); + Navigator.pop( + context, ['${_isNew ? 'New' : ''} Accuracy saved', accuracy]); } setState(() { _isSaving = false; @@ -93,7 +106,8 @@ class _AccuracyDetailScreenState extends State { (!_forCarbsRatio || !_forPortionSize || _valueController.text != '' || - int.tryParse(_confidenceRatingController.text) != null || + int.tryParse(_confidenceRatingController.text) != + Accuracy.getAll().length || _notesController.text != '')) || (!_isNew && (_forCarbsRatio != _accuracy!.forCarbsRatio || @@ -102,7 +116,7 @@ class _AccuracyDetailScreenState extends State { int.tryParse(_confidenceRatingController.text) != _accuracy!.confidenceRating || (_accuracy!.notes ?? '') != _notesController.text))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -159,12 +173,15 @@ class _AccuracyDetailScreenState extends State { }); }, ), - TextFormField( + NumberFormField( controller: _confidenceRatingController, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Confidence Rating', - ), + label: 'Confidence Rating', + onChanged: (value) { + setState(() { + _confidenceRatingController.text = + (value ?? 0).toInt().toString(); + }); + }, ), TextFormField( controller: _notesController, diff --git a/lib/screens/accuracy_list.dart b/lib/screens/accuracy_list.dart index 5a841d8..ad282be 100644 --- a/lib/screens/accuracy_list.dart +++ b/lib/screens/accuracy_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/screens/accuracy_detail.dart'; @@ -24,6 +24,12 @@ class _AccuracyListScreenState extends State { reload(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _accuracies = Accuracy.getAll(); @@ -49,7 +55,7 @@ class _AccuracyListScreenState extends State { void handleDeleteAction(Accuracy accuracy) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(accuracy), message: 'Are you sure you want to delete this Accuracy?', @@ -87,77 +93,77 @@ class _AccuracyListScreenState extends State { Expanded( child: _accuracies.isNotEmpty ? Scrollbar( - controller: _scrollController, - child: ReorderableListView.builder( - padding: const EdgeInsets.all(10.0), - scrollController: _scrollController, - itemCount: _accuracies.length, - onReorder: (oldIndex, newIndex) { - Accuracy.reorder(_accuracies[oldIndex], newIndex); - reload(); - }, - itemBuilder: (context, index) { - final accuracy = _accuracies[index]; - return Card( - key: Key(accuracy.id.toString()), - child: ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AccuracyDetailScreen(id: accuracy.id), - ), - ).then((result) => reload(message: result?[0])); - }, - title: Text( - accuracy.value.toUpperCase(), - style: Theme.of(context).textTheme.subtitle2, + controller: _scrollController, + child: ReorderableListView.builder( + padding: const EdgeInsets.all(10.0), + scrollController: _scrollController, + itemCount: _accuracies.length, + onReorder: (oldIndex, newIndex) { + Accuracy.reorder(_accuracies[oldIndex], newIndex); + reload(); + }, + itemBuilder: (context, index) { + final accuracy = _accuracies[index]; + return Card( + key: Key(accuracy.id.toString()), + child: ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AccuracyDetailScreen(id: accuracy.id), ), - leading: Row( - mainAxisSize: MainAxisSize.min, - children: const [ - Icon(Icons.reorder), - ], + ).then((result) => reload(message: result?[0])); + }, + title: Text( + accuracy.value.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + ), + leading: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.reorder), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + Icons.square_foot, + color: accuracy.forPortionSize + ? Theme.of(context) + .toggleableActiveColor + : Theme.of(context).highlightColor, + ), + onPressed: () => + handleToggleForPortionSizeAction( + accuracy), ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon( - Icons.square_foot, - color: accuracy.forPortionSize - ? Theme.of(context) - .toggleableActiveColor - : Theme.of(context).highlightColor, - ), - onPressed: () => - handleToggleForPortionSizeAction( - accuracy), - ), - IconButton( - icon: Icon( - Icons.pie_chart, - color: accuracy.forCarbsRatio - ? Theme.of(context) - .toggleableActiveColor - : Theme.of(context).highlightColor, - ), - onPressed: () => - handleToggleForCarbsRatioAction( - accuracy), - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => - handleDeleteAction(accuracy), - ) - ], + IconButton( + icon: Icon( + Icons.pie_chart, + color: accuracy.forCarbsRatio + ? Theme.of(context) + .toggleableActiveColor + : Theme.of(context).highlightColor, + ), + onPressed: () => + handleToggleForCarbsRatioAction( + accuracy), ), - ), - ); - }), - ) + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => + handleDeleteAction(accuracy), + ) + ], + ), + ), + ); + }), + ) : const Center( child: Text('You have not created any Accuracies yet!'), ), diff --git a/lib/screens/basal/basal_detail.dart b/lib/screens/basal/basal_detail.dart index 35ff3e1..0903211 100644 --- a/lib/screens/basal/basal_detail.dart +++ b/lib/screens/basal/basal_detail.dart @@ -1,10 +1,13 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/forms/number_form_field.dart'; +import 'package:diameter/components/forms/time_of_day_form_field.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/utils/date_time_utils.dart'; +import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/basal.dart'; import 'package:diameter/models/basal_profile.dart'; @@ -42,7 +45,8 @@ class _BasalDetailScreenState extends State { final _startTimeController = TextEditingController(text: ''); final _endTimeController = TextEditingController(text: ''); - final _unitsController = TextEditingController(text: ''); + final _unitsController = + TextEditingController(text: 0.toStringAsPrecision(3)); @override void initState() { @@ -59,13 +63,22 @@ class _BasalDetailScreenState extends State { if (_basal != null) { _startTime = TimeOfDay.fromDateTime(_basal!.startTime); _endTime = TimeOfDay.fromDateTime(_basal!.endTime); - _unitsController.text = _basal!.units.toString(); + _unitsController.text = _basal!.units.toStringAsPrecision(3); } _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); } + @override + void dispose() { + _scrollController.dispose(); + _startTimeController.dispose(); + _endTimeController.dispose(); + _unitsController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -207,13 +220,13 @@ class _BasalDetailScreenState extends State { _startTime.minute != (widget.suggestedStartTime?.minute ?? 0) || _endTime.minute != (widget.suggestedEndTime?.minute ?? 0) || - double.tryParse(_unitsController.text) != null)) || + double.tryParse(_unitsController.text) != 0)) || (!_isNew && (TimeOfDay.fromDateTime(_basal!.startTime) != _startTime || TimeOfDay.fromDateTime(_basal!.endTime) != _endTime || - (double.tryParse(_unitsController.text) ?? 0) != + double.tryParse(_unitsController.text) != _basal!.units)))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -266,21 +279,19 @@ class _BasalDetailScreenState extends State { ), ], ), - TextFormField( - controller: _unitsController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - decoration: const InputDecoration( - labelText: 'Units', - suffixText: 'U', - ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty amount of units'; - } - return null; - }, - ), + NumberFormField( + controller: _unitsController, + label: 'Units', + suffix: 'U', + autoRoundToMultipleOfStep: true, + step: Settings.insulinSteps, + onChanged: (value) { + if (value != null) { + _unitsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + value, Settings.insulinSteps); + } + }), ], ), ], diff --git a/lib/screens/basal/basal_list.dart b/lib/screens/basal/basal_list.dart index fda595a..7685693 100644 --- a/lib/screens/basal/basal_list.dart +++ b/lib/screens/basal/basal_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/utils/date_time_utils.dart'; import 'package:flutter/material.dart'; @@ -25,6 +25,12 @@ class BasalListScreen extends StatefulWidget { class _BasalListScreenState extends State { final ScrollController _scrollController = ScrollController(); + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { widget.reload(); @@ -60,7 +66,7 @@ class _BasalListScreenState extends State { void handleDeleteAction(Basal basal) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(basal), message: 'Are you sure you want to delete this Basal Rate?', @@ -106,70 +112,76 @@ class _BasalListScreenState extends State { .isNotEmpty) { return 'This rate\'s time period overlaps with another one'; } + + return null; } @override Widget build(BuildContext context) { - return widget.basalRates.isNotEmpty ? Scrollbar( - controller: _scrollController, - child: ListView.builder( - padding: const EdgeInsets.all(10.0), - controller: _scrollController, - shrinkWrap: true, - itemCount: widget.basalRates.length, - itemBuilder: (context, index) { - final basal = widget.basalRates[index]; - final error = validateTimePeriod(index); - return Card( - child: Column( - children: [ - error != null - ? Padding( - padding: const EdgeInsets.all(5.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.warning, color: Theme.of(context).errorColor), - Text( - error, style: TextStyle(color: Theme.of(context).errorColor) - ), - ], - ), - ) : Container(), - ListTile( - onTap: () { - handleEditAction(basal); - }, - title: Row( - mainAxisSize: MainAxisSize.max, + return widget.basalRates.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + padding: const EdgeInsets.all(10.0), + controller: _scrollController, + shrinkWrap: true, + itemCount: widget.basalRates.length, + itemBuilder: (context, index) { + final basal = widget.basalRates[index]; + final error = validateTimePeriod(index); + return Card( + child: Column( children: [ - Expanded( - child: Text( - '${DateTimeUtils.displayTime(basal.startTime)} - ${DateTimeUtils.displayTime(basal.endTime)}')), - const Spacer(), - Expanded(child: Text('${basal.units} U')), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, + error != null + ? Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.warning, + color: Theme.of(context).errorColor), + Text(error, + style: TextStyle( + color: Theme.of(context).errorColor)), + ], + ), + ) + : Container(), + ListTile( + onTap: () { + handleEditAction(basal); + }, + title: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + '${DateTimeUtils.displayTime(basal.startTime)} - ${DateTimeUtils.displayTime(basal.endTime)}')), + const Spacer(), + Expanded(child: Text('${basal.units} U')), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(basal), + ), + ], ), - onPressed: () => handleDeleteAction(basal), ), ], ), - ), - ], + ); + }, ), + ) + : const Center( + child: Text('You have not created any Basal Rates yet!'), ); - }, - ), - ) : const Center( - child: Text('You have not created any Basal Rates yet!'), - ); } } diff --git a/lib/screens/basal/basal_profile_detail.dart b/lib/screens/basal/basal_profile_detail.dart index a0e2d61..d558376 100644 --- a/lib/screens/basal/basal_profile_detail.dart +++ b/lib/screens/basal/basal_profile_detail.dart @@ -1,11 +1,12 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/forms/boolean_form_field.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/basal.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/screens/basal/basal_detail.dart'; import 'package:flutter/material.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/basal_profile.dart'; import 'package:diameter/screens/basal/basal_list.dart'; @@ -92,6 +93,14 @@ class _BasalProfileDetailScreenState extends State { bottomNav = detailBottomRow; } + @override + void dispose() { + _scrollController.dispose(); + _nameController.dispose(); + _notesController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -270,7 +279,7 @@ class _BasalProfileDetailScreenState extends State { (_basalProfile!.active != _active || _basalProfile!.name != _nameController.text || (_basalProfile!.notes ?? '') != _notesController.text))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, diff --git a/lib/screens/basal/basal_profile_list.dart b/lib/screens/basal/basal_profile_list.dart index 9729ab7..cfa561f 100644 --- a/lib/screens/basal/basal_profile_list.dart +++ b/lib/screens/basal/basal_profile_list.dart @@ -1,5 +1,5 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; import 'package:diameter/models/basal.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; @@ -17,13 +17,25 @@ class BasalProfileListScreen extends StatefulWidget { class _BasalProfileListScreenState extends State { final ScrollController _scrollController = ScrollController(); - + late List _basalProfiles; Widget banner = Container(); final BasalProfile? _activeProfile = BasalProfile.getActive(DateTime.now()); - void refresh({String? message}) { + @override + void initState() { + super.initState(); + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { setState(() { _basalProfiles = BasalProfile.getAll(); }); @@ -75,14 +87,35 @@ class _BasalProfileListScreenState extends State { }); } + void handleDuplicateAction(BasalProfile basalProfile) async { + final copy = BasalProfile( + active: false, + name: 'Copy of ${basalProfile.name}', + ); + BasalProfile.put(copy); + + final rates = Basal.getAllForProfile(basalProfile.id); + for (Basal rate in rates) { + final basal = Basal( + endTime: rate.endTime, + startTime: rate.startTime, + units: rate.units, + ); + basal.basalProfile.target = copy; + Basal.put(basal); + } + + reload(message: 'Added copy of ${basalProfile.name}'); + } + void onDelete(BasalProfile basalProfile) { BasalProfile.remove(basalProfile.id); - refresh(message: 'Basal Profile deleted'); + reload(message: 'Basal Profile deleted'); } void handleDeleteAction(BasalProfile basalProfile) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(basalProfile), message: 'Are you sure you want to delete this Basal Profile?', @@ -97,7 +130,7 @@ class _BasalProfileListScreenState extends State { BasalProfile.setAllInactive; basalProfile.active = true; BasalProfile.put(basalProfile); - refresh( + reload( message: '${basalProfile.name} has been set as your active Profile'); } } @@ -130,7 +163,7 @@ class _BasalProfileListScreenState extends State { builder: (context) => BasalProfileDetailScreen(id: basalProfile?.id ?? 0, active: active), ), - ).then((result) => refresh(message: result?[0])); + ).then((result) => reload(message: result?[0])); } void onNew(bool active) { @@ -141,19 +174,13 @@ class _BasalProfileListScreenState extends State { showDetailScreen(basalProfile: basalProfile); } - @override - void initState() { - super.initState(); - refresh(); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Basal Profiles'), actions: [ - IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) ], ), drawer: @@ -165,8 +192,8 @@ class _BasalProfileListScreenState extends State { Expanded( child: _basalProfiles.isNotEmpty ? Scrollbar( - controller: _scrollController, - child: ListView.builder( + controller: _scrollController, + child: ListView.builder( padding: const EdgeInsets.all(10.0), controller: _scrollController, itemCount: _basalProfiles.length, @@ -186,22 +213,23 @@ class _BasalProfileListScreenState extends State { basalProfile.id == _activeProfile?.id, onTap: () => onEdit(basalProfile), title: Text( - basalProfile.name.toUpperCase() + activeProfileText, + basalProfile.name.toUpperCase() + + activeProfileText, style: Theme.of(context).textTheme.subtitle2, ), subtitle: Padding( padding: const EdgeInsets.only(top: 10.0), child: Row( children: [ - Text( - basalProfile.notes ?? '' - ), + Text(basalProfile.notes ?? ''), Expanded( child: Column( children: dailyTotal > 0 ? [ - Text(dailyTotal.toStringAsPrecision(3)), - const Text('U/day', textScaleFactor: 0.75), + Text(dailyTotal + .toStringAsPrecision(3)), + const Text('U/day', + textScaleFactor: 0.75), ] : [], ), @@ -212,12 +240,21 @@ class _BasalProfileListScreenState extends State { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon( + Icons.copy, + color: Colors.blue, + ), + onPressed: () => + handleDuplicateAction(basalProfile), + ), IconButton( icon: const Icon( Icons.delete, color: Colors.blue, ), - onPressed: () => handleDeleteAction(basalProfile), + onPressed: () => + handleDeleteAction(basalProfile), ), ], ), @@ -225,7 +262,7 @@ class _BasalProfileListScreenState extends State { ); }, ), - ) + ) : const Center( child: Text('You have not created any Basal Profiles yet!'), ), diff --git a/lib/screens/bolus/bolus_detail.dart b/lib/screens/bolus/bolus_detail.dart index c95d61d..fb93f54 100644 --- a/lib/screens/bolus/bolus_detail.dart +++ b/lib/screens/bolus/bolus_detail.dart @@ -1,11 +1,13 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/forms/number_form_field.dart'; +import 'package:diameter/components/forms/time_of_day_form_field.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/utils/date_time_utils.dart'; import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/bolus_profile.dart'; @@ -43,10 +45,10 @@ class _BolusDetailScreenState extends State { final _startTimeController = TextEditingController(text: ''); final _endTimeController = TextEditingController(text: ''); - final _unitsController = TextEditingController(text: ''); - final _carbsController = TextEditingController(text: ''); - final _mgPerDlController = TextEditingController(text: ''); - final _mmolPerLController = TextEditingController(text: ''); + final _unitsController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.insulinSteps)); + final _carbsController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.nutritionSteps)); + final _mgPerDlController = TextEditingController(text: '0'); + final _mmolPerLController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.mmolPerLSteps)); @override void initState() { @@ -74,6 +76,18 @@ class _BolusDetailScreenState extends State { _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); } + @override + void dispose() { + _scrollController.dispose(); + _startTimeController.dispose(); + _endTimeController.dispose(); + _unitsController.dispose(); + _carbsController.dispose(); + _mgPerDlController.dispose(); + _mmolPerLController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -196,7 +210,8 @@ class _BolusDetailScreenState extends State { ).then((result) { Navigator.pop( context, - ['New Bolus Rate${result[1] != null ? 's' : ''} saved', bolus] + [result[1]], + ['New Bolus Rate${result[1] != null ? 's' : ''} saved', bolus] + + [result[1]], ); }); } else { @@ -235,7 +250,7 @@ class _BolusDetailScreenState extends State { _bolus!.mgPerDl || (double.tryParse(_mmolPerLController.text) ?? 0) != _bolus!.mmolPerL)))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -245,30 +260,26 @@ class _BolusDetailScreenState extends State { } } - void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { - int? mgPerDl; - double? mmolPerL; - - if (calculateFrom != GlucoseMeasurement.mmolPerL && - _mgPerDlController.text != '') { - mgPerDl = int.tryParse(_mgPerDlController.text); - } - if (calculateFrom != GlucoseMeasurement.mgPerDl && - _mmolPerLController.text != '') { - mmolPerL = double.tryParse(_mmolPerLController.text); - } - - if (mgPerDl != null && mmolPerL == null) { - setState(() { + void convertBetweenMgPerDlAndMmolPerL(double? value) async { + if (value != null) { + if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl && + _mgPerDlController.text != '') { + _mgPerDlController.text = value.toInt().toString(); + setState(() { + _mmolPerLController.text = + Utils.convertMgPerDlToMmolPerL(value.toInt()).toString(); + }); + } + if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL && + _mmolPerLController.text != '') { _mmolPerLController.text = - Utils.convertMgPerDlToMmolPerL(mgPerDl!).toString(); - }); - } - if (mmolPerL != null && mgPerDl == null) { - setState(() { - _mgPerDlController.text = - Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString(); - }); + Utils.toStringMatchingTemplateFractionPrecision( + value, Settings.mmolPerLSteps); + setState(() { + _mgPerDlController.text = + Utils.convertMmolPerLToMgPerDl(value.toDouble()).toString(); + }); + } } } @@ -315,34 +326,32 @@ class _BolusDetailScreenState extends State { ), ], ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Units', - suffixText: 'U', - ), + NumberFormField( controller: _unitsController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty amount of units'; + label: 'Units', + suffix: 'U', + autoRoundToMultipleOfStep: true, + step: Settings.insulinSteps, + onChanged: (value) { + if (value != null) { + _unitsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + value, Settings.insulinSteps); } - return null; }, ), - TextFormField( - decoration: InputDecoration( - labelText: 'per carbs', - suffixText: Settings.nutritionMeasurementSuffix, - ), + NumberFormField( controller: _carbsController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value!.trim().isEmpty) { - return 'How many carbs does the rate make up for?'; + label: 'per carbs', + suffix: Settings.nutritionMeasurementSuffix, + autoRoundToMultipleOfStep: true, + step: Settings.nutritionSteps, + onChanged: (value) { + if (value != null) { + _carbsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + value, Settings.nutritionSteps); } - return null; }, ), Row( @@ -354,42 +363,18 @@ class _BolusDetailScreenState extends State { Settings.glucoseDisplayMode == GlucoseDisplayMode.bothForDetail ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'per mg/dl', - suffixText: 'mg/dl', - ), + flex: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? 2 : 1, + child: NumberFormField( + label: 'per mg/dl', + suffix: 'mg/dl', readOnly: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL, + showSteppers: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl, controller: _mgPerDlController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - convertBetweenMgPerDlAndMmolPerL( - calculateFrom: - GlucoseMeasurement.mgPerDl); - }, - keyboardType: - const TextInputType.numberWithOptions(), - validator: (value) { - if (value!.trim().isEmpty && - _mmolPerLController.text.trim().isEmpty) { - return 'How many mg/dl does the rate make up for?'; - } - return null; - }, + onChanged: convertBetweenMgPerDlAndMmolPerL, ), ) : Container(), - Settings.glucoseDisplayMode == GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? IconButton( - onPressed: () => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mmolPerL), - icon: const Icon(Icons.calculate), - ) - : Container(), Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL || [ @@ -397,44 +382,19 @@ class _BolusDetailScreenState extends State { GlucoseDisplayMode.bothForDetail ].contains(Settings.glucoseDisplayMode) ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'per mmol/l', - suffixText: 'mmol/l', - ), + flex: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL ? 2 : 1, + child: NumberFormField( + label: 'per mmol/l', + suffix: 'mmol/l', readOnly: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl, + showSteppers: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL, controller: _mmolPerLController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - convertBetweenMgPerDlAndMmolPerL( - calculateFrom: - GlucoseMeasurement.mmolPerL); - }, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true), - validator: (value) { - if (value!.trim().isEmpty && - _mgPerDlController.text.trim().isEmpty) { - return 'How many mmol/l does rhe rate make up for?'; - } - return null; - }, + step: Settings.mmolPerLSteps, + onChanged: convertBetweenMgPerDlAndMmolPerL, ), ) : Container(), - [ - GlucoseDisplayMode.both, - GlucoseDisplayMode.bothForDetail - ].contains(Settings.glucoseDisplayMode) - ? IconButton( - onPressed: () => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mgPerDl), - icon: const Icon(Icons.calculate), - ) - : Container(), ], ), ], diff --git a/lib/screens/bolus/bolus_list.dart b/lib/screens/bolus/bolus_list.dart index d6218c1..828719a 100644 --- a/lib/screens/bolus/bolus_list.dart +++ b/lib/screens/bolus/bolus_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/utils/date_time_utils.dart'; import 'package:flutter/material.dart'; @@ -12,7 +12,10 @@ class BolusListScreen extends StatefulWidget { final Function() reload; const BolusListScreen( - {Key? key, required this.bolusProfile, this.bolusRates = const [], required this.reload}) + {Key? key, + required this.bolusProfile, + this.bolusRates = const [], + required this.reload}) : super(key: key); @override @@ -22,6 +25,12 @@ class BolusListScreen extends StatefulWidget { class _BolusListScreenState extends State { final ScrollController _scrollController = ScrollController(); + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { widget.reload(); @@ -57,7 +66,7 @@ class _BolusListScreenState extends State { void handleDeleteAction(Bolus bolus) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(bolus), message: 'Are you sure you want to delete this Bolus Rate?', @@ -102,100 +111,122 @@ class _BolusListScreenState extends State { .isNotEmpty) { return 'This rate\'s time period overlaps with another one'; } + + return null; } @override Widget build(BuildContext context) { - return widget.bolusRates.isNotEmpty ? Scrollbar( - controller: _scrollController, - child: ListView.builder( - padding: const EdgeInsets.all(10.0), - controller: _scrollController, - shrinkWrap: true, - itemCount: widget.bolusRates.length, - itemBuilder: (context, index) { - final bolus = widget.bolusRates[index]; - final error = validateTimePeriod(index); - return Card( - child: Column( - children: [ - error != null - ? Padding( - padding: const EdgeInsets.all(5.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.warning, color: Theme.of(context).errorColor), - Text( - error, style: TextStyle(color: Theme.of(context).errorColor) - ), - ], - ), - ) : Container(), - ListTile( - onTap: () { - handleEditAction(bolus); - }, - isThreeLine: true, - title: Text('${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}'), - subtitle: Row( - crossAxisAlignment: CrossAxisAlignment.start, + return widget.bolusRates.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + padding: const EdgeInsets.all(10.0), + controller: _scrollController, + shrinkWrap: true, + itemCount: widget.bolusRates.length, + itemBuilder: (context, index) { + final bolus = widget.bolusRates[index]; + final error = validateTimePeriod(index); + return Card( + child: Column( children: [ - Expanded( - child: Column( - children: (bolus.units > 0 && bolus.carbs > 0) - ? [ - Text((bolus.carbs / bolus.units).toStringAsPrecision(2)), - Text('${Settings.nutritionMeasurementSuffix} carbs per U', - textAlign: TextAlign.center, textScaleFactor: 0.75), - ] - : [], + error != null + ? Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.warning, + color: Theme.of(context).errorColor), + Text(error, + style: TextStyle( + color: Theme.of(context).errorColor)), + ], + ), + ) + : Container(), + ListTile( + onTap: () { + handleEditAction(bolus); + }, + isThreeLine: true, + title: Text( + '${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}'), + subtitle: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: (bolus.units > 0 && bolus.carbs > 0) + ? [ + Text((bolus.carbs / bolus.units) + .toStringAsPrecision(2)), + Text( + '${Settings.nutritionMeasurementSuffix} carbs per U', + textAlign: TextAlign.center, + textScaleFactor: 0.75), + ] + : [], + ), + ), + Expanded( + child: Column( + children: (bolus.units > 0 && bolus.carbs > 0) + ? [ + Text((bolus.units / bolus.carbs * 12) + .toStringAsPrecision(2)), + const Text('U per bread unit', + textAlign: TextAlign.center, + textScaleFactor: 0.75), + ] + : [], + ), + ), + Expanded( + child: Column( + children: (bolus.units > 0 && + (bolus.mgPerDl ?? bolus.mmolPerL ?? 0) > + 0) + ? [ + Text((((Settings.glucoseMeasurement == + GlucoseMeasurement + .mgPerDl + ? bolus.mgPerDl + : bolus.mmolPerL ?? 0)! / + bolus.units)) + .toString()), + Text( + '${Settings.glucoseMeasurementSuffix} per unit', + textAlign: TextAlign.center, + textScaleFactor: 0.75), + ] + : [], + ), + ), + ], ), - ), - Expanded( - child: Column( - children: (bolus.units > 0 && bolus.carbs > 0) - ? [ - Text((bolus.units / bolus.carbs * 12).toStringAsPrecision(2)), - const Text('U per bread unit', - textAlign: TextAlign.center, textScaleFactor: 0.75), - ] - : [], - ), - ), - Expanded( - child: Column( - children: (bolus.units > 0 && (bolus.mgPerDl ?? bolus.mmolPerL ?? 0) > 0) - ? [ - Text((((Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDl : bolus.mmolPerL ?? 0)! / bolus.units)).toString()), - Text('${Settings.glucoseMeasurementSuffix} per unit', - textAlign: TextAlign.center, textScaleFactor: 0.75), - ] - : [], + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(bolus), + ), + ], ), ), ], ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => handleDeleteAction(bolus), - ), - ], - ), - ), - ], + ); + }, ), + ) + : const Center( + child: Text('You have not created any Bolus Rates yet!'), ); - }, - ), - ) : const Center( - child: Text('You have not created any Bolus Rates yet!'), - ); } } diff --git a/lib/screens/bolus/bolus_profile_detail.dart b/lib/screens/bolus/bolus_profile_detail.dart index 794ecf3..bb0aeed 100644 --- a/lib/screens/bolus/bolus_profile_detail.dart +++ b/lib/screens/bolus/bolus_profile_detail.dart @@ -1,11 +1,12 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/forms/boolean_form_field.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/screens/bolus/bolus_detail.dart'; import 'package:flutter/material.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/bolus_profile.dart'; import 'package:diameter/screens/bolus/bolus_list.dart'; @@ -90,6 +91,14 @@ class _BolusProfileDetailScreenState extends State { bottomNav = detailBottomRow; } + @override + void dispose() { + _scrollController.dispose(); + _nameController.dispose(); + _notesController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -269,7 +278,7 @@ class _BolusProfileDetailScreenState extends State { (_bolusProfile!.active != _active || _bolusProfile!.name != _nameController.text || (_bolusProfile!.notes ?? '') != _notesController.text))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, diff --git a/lib/screens/bolus/bolus_profile_list.dart b/lib/screens/bolus/bolus_profile_list.dart index 504b246..2b9a617 100644 --- a/lib/screens/bolus/bolus_profile_list.dart +++ b/lib/screens/bolus/bolus_profile_list.dart @@ -1,5 +1,6 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; +import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:flutter/material.dart'; @@ -22,6 +23,18 @@ class _BolusProfileListScreenState extends State { final BolusProfile? _activeProfile = BolusProfile.getActive(DateTime.now()); + @override + void initState() { + super.initState(); + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _bolusProfiles = BolusProfile.getAll(); @@ -77,6 +90,30 @@ class _BolusProfileListScreenState extends State { }); } + void handleDuplicateAction(BolusProfile bolusProfile) async { + final copy = BolusProfile( + active: false, + name: 'Copy of ${bolusProfile.name}', + ); + BolusProfile.put(copy); + + final rates = Bolus.getAllForProfile(bolusProfile.id); + for (Bolus rate in rates) { + final bolus = Bolus( + endTime: rate.endTime, + startTime: rate.startTime, + units: rate.units, + carbs: rate.carbs, + mgPerDl: rate.mgPerDl, + mmolPerL: rate.mmolPerL, + ); + bolus.bolusProfile.target = copy; + Bolus.put(bolus); + } + + reload(message: 'Added copy of ${bolusProfile.name}'); + } + void onDelete(BolusProfile bolusProfile) { BolusProfile.remove(bolusProfile.id); reload(message: 'Bolus Profile deleted'); @@ -84,7 +121,7 @@ class _BolusProfileListScreenState extends State { void handleDeleteAction(BolusProfile bolusProfile) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(bolusProfile), message: 'Are you sure you want to delete this Bolus Profile?', @@ -143,12 +180,6 @@ class _BolusProfileListScreenState extends State { showDetailScreen(bolusProfile: bolusProfile); } - @override - void initState() { - super.initState(); - reload(); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -196,6 +227,14 @@ class _BolusProfileListScreenState extends State { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon( + Icons.copy, + color: Colors.blue, + ), + onPressed: () => + handleDuplicateAction(bolusProfile), + ), IconButton( icon: const Icon( Icons.delete, diff --git a/lib/screens/log/log.dart b/lib/screens/log/log.dart index de6cc16..4ef5bb1 100644 --- a/lib/screens/log/log.dart +++ b/lib/screens/log/log.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/glucose_target.dart'; import 'package:diameter/models/log_bolus.dart'; import 'package:diameter/models/log_entry.dart'; @@ -29,6 +29,12 @@ class _LogScreenState extends State { reload(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _logEntryDailyMap = LogEntry.getDailyEntryMap(); @@ -53,7 +59,7 @@ class _LogScreenState extends State { void handleDeleteAction(LogEntry logEntry) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(logEntry), message: 'Are you sure you want to delete this Log Entry?', diff --git a/lib/screens/log/log_entry/log_bolus_detail.dart b/lib/screens/log/log_entry/log_bolus_detail.dart index 6c24681..b9cc7b4 100644 --- a/lib/screens/log/log_entry/log_bolus_detail.dart +++ b/lib/screens/log/log_entry/log_bolus_detail.dart @@ -1,9 +1,11 @@ import 'dart:math'; import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; -import 'package:diameter/components/forms.dart'; +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/components/forms/form_wrapper.dart'; import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/log_bolus.dart'; import 'package:diameter/models/log_entry.dart'; @@ -136,6 +138,25 @@ class _LogBolusDetailScreenState extends State { updateDelayedRatio(); } + @override + void dispose() { + _scrollController.dispose(); + _unitsController.dispose(); + _carbsController.dispose(); + _mgPerDlCurrentController.dispose(); + _mgPerDlTargetController.dispose(); + _mgPerDlCorrectionController.dispose(); + _mmolPerLCurrentController.dispose(); + _mmolPerLTargetController.dispose(); + _mmolPerLCorrectionController.dispose(); + _delayController.dispose(); + _notesController.dispose(); + _delayedUnitsController.dispose(); + _immediateUnitsController.dispose(); + _mealController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -179,20 +200,73 @@ class _LogBolusDetailScreenState extends State { } } - void updateDelayedRatio() { - if (_unitsController.text != '') { - setState(() { - _delayedUnitsController.text = - ((double.tryParse(_unitsController.text) ?? 0) * - _delayPercentage / - 100) - .toString(); - _immediateUnitsController.text = - ((double.tryParse(_unitsController.text) ?? 0) * - (100 - _delayPercentage) / - 100) - .toString(); - }); + void updateDelayedRatio( + {double? totalUnitsUpdate, + double? delayedUnitsUpdate, + double? immediateUnitsUpdate, + double? percentageUpdate}) { + int precision = Utils.getFractionDigitsLength(Settings.insulinSteps); + double? totalUnits = + totalUnitsUpdate ?? double.tryParse(_unitsController.text); + double? delayedUnits; + double? immediateUnits; + + if (totalUnits == null) { + delayedUnits = + delayedUnitsUpdate ?? double.tryParse(_delayedUnitsController.text); + immediateUnits = immediateUnitsUpdate ?? + double.tryParse(_immediateUnitsController.text); + + if (percentageUpdate != null) { + if (delayedUnits != null) { + totalUnits = delayedUnits / percentageUpdate * 100; + } else if (immediateUnits != null) { + totalUnits = immediateUnits / percentageUpdate * 100; + } + } else if (delayedUnits != null && immediateUnits != null) { + totalUnits = Utils.addDoublesWithPrecision( + delayedUnits, immediateUnits, precision); + } + } + + setState(() { + _unitsController.text = (totalUnits ?? 0).toString(); + }); + + if (totalUnits != null) { + double percentage = percentageUpdate ?? _delayPercentage; + + if (totalUnitsUpdate != null || percentageUpdate != null) { + delayedUnits = totalUnits * percentage / 100; + } else if (delayedUnitsUpdate != null) { + delayedUnits = delayedUnitsUpdate; + } else if (immediateUnitsUpdate != null) { + delayedUnits = totalUnits - immediateUnitsUpdate; + } + + if (delayedUnits != null) { + double remainder = delayedUnits % Settings.insulinSteps; + int precision = Utils.getFractionDigitsLength(Settings.insulinSteps); + + if (remainder != 0) { + delayedUnits = Utils.addDoublesWithPrecision( + delayedUnits, -remainder, precision); + if (remainder > Settings.insulinSteps / 2) { + delayedUnits = Utils.addDoublesWithPrecision( + delayedUnits, Settings.insulinSteps, precision); + } + } + + setState(() { + _delayedUnitsController.text = delayedUnits.toString(); + _immediateUnitsController.text = Utils.addDoublesWithPrecision( + totalUnits!, -delayedUnits!, precision) + .toString(); + if (totalUnits != 0) { + _delayPercentage = delayedUnits * 100 / totalUnits; + } + }); + } } } @@ -207,28 +281,30 @@ class _LogBolusDetailScreenState extends State { } void calculateBolus() { - setState(() { - if (_rate != null && !_setManually) { - _unitsController.text = ((double.tryParse(_carbsController.text) ?? 0) / - (_rate!.carbs / _rate!.units)) - .toString(); - if (_unitsController.text != '') { - _delayedUnitsController.text = - ((double.tryParse(_unitsController.text) ?? 0) * - _delayPercentage / - 100) - .toString(); - _immediateUnitsController.text = - ((double.tryParse(_unitsController.text) ?? 0) * - (100 - _delayPercentage) / - 100) - .toString(); + if (_rate != null && !_setManually) { + double units = (double.tryParse(_carbsController.text) ?? 0) / + (_rate!.carbs / _rate!.units); + double remainder = units % Settings.insulinSteps; + int precision = Utils.getFractionDigitsLength(Settings.insulinSteps); + + if (remainder != 0) { + units = Utils.addDoublesWithPrecision(units, -remainder, precision); + if (remainder > Settings.insulinSteps / 2) { + units = Utils.addDoublesWithPrecision( + units, Settings.insulinSteps, precision); } } - }); + + setState(() { + _unitsController.text = units.toString(); + }); + + updateDelayedRatio( + totalUnitsUpdate: double.tryParse(_unitsController.text)); + } } - void onChangeGlucose({GlucoseMeasurement? calculateFrom}) { + void onChangeGlucose() { int? mgPerDlCurrent; int? mgPerDlTarget; int? mgPerDlCorrection; @@ -237,14 +313,14 @@ class _LogBolusDetailScreenState extends State { double? mmolPerLTarget; double? mmolPerLCorrection; - if (calculateFrom != GlucoseMeasurement.mmolPerL && + if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl && _mgPerDlCurrentController.text != '' && _mgPerDlTargetController.text != '') { mgPerDlCurrent = int.tryParse(_mgPerDlCurrentController.text); mgPerDlTarget = int.tryParse(_mgPerDlTargetController.text); mgPerDlCorrection = max((mgPerDlCurrent ?? 0) - (mgPerDlTarget ?? 0), 0); } - if (calculateFrom != GlucoseMeasurement.mgPerDl && + if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL && _mmolPerLCurrentController.text != '') { mmolPerLCurrent = double.tryParse(_mmolPerLCurrentController.text); mmolPerLTarget = double.tryParse(_mmolPerLTargetController.text); @@ -264,9 +340,9 @@ class _LogBolusDetailScreenState extends State { _mmolPerLCorrectionController.text = Utils.convertMgPerDlToMmolPerL(mgPerDlCorrection ?? 0).toString(); if (_rate != null && !_setManually) { - _unitsController.text = ((mgPerDlCorrection ?? 0) / - ((_rate!.mgPerDl ?? 0) / _rate!.units)) - .toString(); + updateDelayedRatio( + totalUnitsUpdate: (mgPerDlCorrection ?? 0) / + ((_rate!.mgPerDl ?? 0) / _rate!.units)); } }); } @@ -282,9 +358,9 @@ class _LogBolusDetailScreenState extends State { _mgPerDlCorrectionController.text = Utils.convertMmolPerLToMgPerDl(mmolPerLCorrection ?? 0).toString(); if (_rate != null && !_setManually) { - _unitsController.text = ((mmolPerLCorrection ?? 0) / - ((_rate!.mmolPerL ?? 0) / _rate!.units)) - .toString(); + updateDelayedRatio( + totalUnitsUpdate: (mmolPerLCorrection ?? 0) / + ((_rate!.mmolPerL ?? 0) / _rate!.units)); } }); } @@ -414,7 +490,7 @@ class _LogBolusDetailScreenState extends State { int.tryParse(_delayController.text) != _logBolus!.delay || _setManually != _logBolus!.setManually || _notesController.text != (_logBolus!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -444,20 +520,18 @@ class _LogBolusDetailScreenState extends State { Row( children: [ Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Bolus Units', - suffixText: ' U', - ), + child: NumberFormField( + label: 'Bolus Units', + suffix: ' U', controller: _unitsController, - onChanged: (_) { + step: Settings.insulinSteps, + autoRoundToMultipleOfStep: true, + onChanged: (value) { setState(() { _setManually = true; }); - updateDelayedRatio(); + updateDelayedRatio(totalUnitsUpdate: value); }, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), ), ), Expanded( @@ -486,6 +560,7 @@ class _LogBolusDetailScreenState extends State { onChanged: (_) { setState(() { _bolusType = BolusType.glucose; + onChangeGlucose(); }); }), ), @@ -497,6 +572,7 @@ class _LogBolusDetailScreenState extends State { onChanged: (value) { setState(() { _bolusType = BolusType.meal; + calculateBolus(); }); }), ), @@ -517,23 +593,13 @@ class _LogBolusDetailScreenState extends State { child: Padding( padding: const EdgeInsets.only(right: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Current', - suffixText: 'mg/dl', - ), + child: NumberFormField( + label: 'Current', + suffix: 'mg/dl', controller: _mgPerDlCurrentController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - onChangeGlucose( - calculateFrom: - GlucoseMeasurement - .mgPerDl); - }, - keyboardType: const TextInputType - .numberWithOptions(), + onChanged: (_) => onChangeGlucose(), + showSteppers: false, ), ), ), @@ -541,23 +607,13 @@ class _LogBolusDetailScreenState extends State { child: Padding( padding: const EdgeInsets.symmetric( horizontal: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Target', - suffixText: 'mg/dl', - ), + child: NumberFormField( + label: 'Target', + suffix: 'mg/dl', controller: _mgPerDlTargetController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - onChangeGlucose( - calculateFrom: - GlucoseMeasurement - .mgPerDl); - }, - keyboardType: const TextInputType - .numberWithOptions(), + onChanged: (_) => onChangeGlucose(), + showSteppers: false, ), ), ), @@ -576,106 +632,73 @@ class _LogBolusDetailScreenState extends State { ), ), ), - [ - GlucoseDisplayMode.both, - GlucoseDisplayMode.bothForDetail - ].contains(Settings.glucoseDisplayMode) - ? IconButton( - onPressed: () => onChangeGlucose( - calculateFrom: - GlucoseMeasurement - .mmolPerL), - icon: const Icon(Icons.calculate), - ) - : Container(), ] : [], ), - Row( - children: Settings.glucoseMeasurement == - GlucoseMeasurement.mmolPerL || - [ - GlucoseDisplayMode.both, - GlucoseDisplayMode.bothForDetail - ].contains(Settings.glucoseDisplayMode) - ? [ - Expanded( - child: Padding( - padding: - const EdgeInsets.only(right: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Current', - suffixText: 'mmol/l', + Padding( + padding: EdgeInsets.only( + top: [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? 10.0 + : 0.0), + child: Row( + children: Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL || + [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + right: 5.0), + child: NumberFormField( + label: 'Current', + suffix: 'mmol/l', + controller: + _mmolPerLCurrentController, + onChanged: (_) => + onChangeGlucose(), + showSteppers: false, ), - controller: - _mmolPerLCurrentController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - onChangeGlucose( - calculateFrom: - GlucoseMeasurement - .mmolPerL); - }, - keyboardType: const TextInputType - .numberWithOptions(), ), ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Target', - suffixText: 'mmol/l', + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5.0), + child: NumberFormField( + label: 'Target', + suffix: 'mmol/l', + controller: + _mmolPerLTargetController, + onChanged: (_) => + onChangeGlucose(), + showSteppers: false, ), - controller: - _mmolPerLTargetController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - onChangeGlucose( - calculateFrom: - GlucoseMeasurement - .mmolPerL); - }, - keyboardType: const TextInputType - .numberWithOptions(), ), ), - ), - Expanded( - child: Padding( - padding: - const EdgeInsets.only(left: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Correction', - suffixText: 'mmol/l', + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Correction', + suffixText: 'mmol/l', + ), + controller: + _mmolPerLCorrectionController, + readOnly: true, ), - controller: - _mmolPerLCorrectionController, - readOnly: true, ), ), - ), - [ - GlucoseDisplayMode.both, - GlucoseDisplayMode.bothForDetail - ].contains(Settings.glucoseDisplayMode) - ? IconButton( - onPressed: () => onChangeGlucose( - calculateFrom: - GlucoseMeasurement - .mgPerDl), - icon: const Icon(Icons.calculate), - ) - : Container(), - ] - : [], + ] + : [], + ), ), ] : [ @@ -712,17 +735,15 @@ class _LogBolusDetailScreenState extends State { ), Padding( padding: const EdgeInsets.only(top: 10.0), - child: TextFormField( - decoration: InputDecoration( - labelText: 'Carbs', - suffixText: - Settings.nutritionMeasurementSuffix, - ), + child: NumberFormField( + label: 'Carbs', + suffix: Settings.nutritionMeasurementSuffix, controller: _carbsController, - onChanged: (_) => calculateBolus(), - keyboardType: - const TextInputType.numberWithOptions( - decimal: true), + step: Settings.nutritionSteps, + onChanged: (value) { + _carbsController.text = (value ?? 0).toString(); + calculateBolus(); + }, ), ), ], @@ -749,10 +770,7 @@ class _LogBolusDetailScreenState extends State { max: 100, onChanged: _delayController.text != '' ? (value) { - setState(() { - _delayPercentage = value; - }); - updateDelayedRatio(); + updateDelayedRatio(percentageUpdate: value); } : null, ), @@ -766,34 +784,30 @@ class _LogBolusDetailScreenState extends State { Expanded( child: Padding( padding: const EdgeInsets.only(right: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Immediate Bolus', - suffixText: ' U', - ), + child: NumberFormField( + label: 'Immediate Bolus', + suffix: ' U', controller: _immediateUnitsController, + max: double.tryParse(_unitsController.text), + step: Settings.insulinSteps, readOnly: true, - enabled: - (int.tryParse(_delayController.text) ?? - 0) != - 0, + onChanged: (value) => updateDelayedRatio( + immediateUnitsUpdate: value), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus', - suffixText: ' U', - ), + child: NumberFormField( + label: 'Delayed Bolus', + suffix: ' U', controller: _delayedUnitsController, + max: double.tryParse(_unitsController.text), + step: Settings.insulinSteps, readOnly: true, - enabled: - (int.tryParse(_delayController.text) ?? - 0) != - 0, + onChanged: (value) => updateDelayedRatio( + delayedUnitsUpdate: value), ), ), ), diff --git a/lib/screens/log/log_entry/log_bolus_list.dart b/lib/screens/log/log_entry/log_bolus_list.dart index c109d08..8ee43e2 100644 --- a/lib/screens/log/log_entry/log_bolus_list.dart +++ b/lib/screens/log/log_entry/log_bolus_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/log_bolus.dart'; import 'package:diameter/models/log_entry.dart'; import 'package:diameter/models/settings.dart'; @@ -25,6 +25,12 @@ class LogBolusListScreen extends StatefulWidget { class _LogBolusListScreenState extends State { final ScrollController _scrollController = ScrollController(); + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { widget.reload(); @@ -60,7 +66,7 @@ class _LogBolusListScreenState extends State { void handleDeleteAction(LogBolus logBolus) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(logBolus), message: 'Are you sure you want to delete this Bolus?', diff --git a/lib/screens/log/log_entry/log_entry.dart b/lib/screens/log/log_entry/log_entry.dart index ba53236..2cdd87b 100644 --- a/lib/screens/log/log_entry/log_entry.dart +++ b/lib/screens/log/log_entry/log_entry.dart @@ -1,6 +1,9 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/components/forms/date_time_form_field.dart'; +import 'package:diameter/components/forms/number_form_field.dart'; +import 'package:diameter/components/forms/time_of_day_form_field.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/log_bolus.dart'; import 'package:diameter/models/log_entry.dart'; import 'package:diameter/models/log_meal.dart'; @@ -40,8 +43,10 @@ class _LogEntryScreenState extends State { final _timeController = TextEditingController(text: ''); final _dateController = TextEditingController(text: ''); - final _mgPerDlController = TextEditingController(text: ''); - final _mmolPerLController = TextEditingController(text: ''); + final _mgPerDlController = + TextEditingController(text: Settings.targetMgPerDl.toString()); + final _mmolPerLController = + TextEditingController(text: Settings.targetMmolPerL.toString()); final _notesController = TextEditingController(text: ''); late FloatingActionButton addMealButton; @@ -109,6 +114,17 @@ class _LogEntryScreenState extends State { updateTime(); } + @override + void dispose() { + _scrollController.dispose(); + _timeController.dispose(); + _dateController.dispose(); + _mgPerDlController.dispose(); + _mmolPerLController.dispose(); + _notesController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -137,25 +153,26 @@ class _LogEntryScreenState extends State { _dateController.text = DateTimeUtils.displayDate(_time); } - void convertBetweenMgPerDlAndMmolPerL() { - int? mgPerDl; - double? mmolPerL; - - if (Settings.glucoseMeasurement != GlucoseMeasurement.mmolPerL && - _mgPerDlController.text != '') { - mgPerDl = int.tryParse(_mgPerDlController.text); - setState(() { + void convertBetweenMgPerDlAndMmolPerL(double? value) async { + if (value != null) { + if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl && + _mgPerDlController.text != '') { + _mgPerDlController.text = value.toInt().toString(); + setState(() { + _mmolPerLController.text = + Utils.convertMgPerDlToMmolPerL(value.toInt()).toString(); + }); + } + if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL && + _mmolPerLController.text != '') { _mmolPerLController.text = - Utils.convertMgPerDlToMmolPerL(mgPerDl ?? 0).toString(); - }); - } - if (Settings.glucoseMeasurement != GlucoseMeasurement.mgPerDl && - _mmolPerLController.text != '') { - mmolPerL = double.tryParse(_mmolPerLController.text); - setState(() { - _mgPerDlController.text = - Utils.convertMmolPerLToMgPerDl(mmolPerL ?? 0).toString(); - }); + Utils.toStringMatchingTemplateFractionPrecision( + value, Settings.mmolPerLSteps); + setState(() { + _mgPerDlController.text = + Utils.convertMmolPerLToMgPerDl(value.toDouble()).toString(); + }); + } } } @@ -207,7 +224,7 @@ class _LogEntryScreenState extends State { double.tryParse(_mmolPerLController.text) != _logEntry!.mmolPerL || _notesController.text != (_logEntry!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -337,76 +354,52 @@ class _LogEntryScreenState extends State { children: [ Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl || - [ - GlucoseDisplayMode.both, - GlucoseDisplayMode.bothForDetail - ].contains(Settings.glucoseDisplayMode) - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'mg/dl', - suffixText: 'mg/dl', - ), - readOnly: Settings.glucoseMeasurement == - GlucoseMeasurement.mmolPerL, - controller: _mgPerDlController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - convertBetweenMgPerDlAndMmolPerL(); - }, - keyboardType: const TextInputType - .numberWithOptions(), - validator: (value) { - if (value!.trim().isEmpty && - _mmolPerLController.text - .trim() - .isEmpty) { - return 'How high is your blood sugar?'; - } - return null; - }, - ), - ) - : Container(), - [ - GlucoseDisplayMode.both, - GlucoseDisplayMode.bothForDetail - ].contains(Settings.glucoseDisplayMode) - ? const SizedBox(width: 10.0) - : Container(), - Settings.glucoseMeasurement == - GlucoseMeasurement.mmolPerL || Settings.glucoseDisplayMode == GlucoseDisplayMode.both || Settings.glucoseDisplayMode == GlucoseDisplayMode.bothForDetail ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'mmol/l', - suffixText: 'mmol/l', - ), + flex: Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl + ? 2 + : 1, + child: NumberFormField( + label: 'per mg/dl', + suffix: 'mg/dl', + readOnly: Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL, + showSteppers: + Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl, + controller: _mgPerDlController, + onChanged: + convertBetweenMgPerDlAndMmolPerL, + ), + ) + : Container(), + Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL || + [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? Expanded( + flex: Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL + ? 2 + : 1, + child: NumberFormField( + label: 'per mmol/l', + suffix: 'mmol/l', readOnly: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl, + showSteppers: + Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL, controller: _mmolPerLController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - convertBetweenMgPerDlAndMmolPerL(); - }, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true), - validator: (value) { - if (value!.trim().isEmpty && - _mgPerDlController.text - .trim() - .isEmpty) { - return 'How high is your blood sugar?'; - } - return null; - }, + step: Settings.mmolPerLSteps, + onChanged: + convertBetweenMgPerDlAndMmolPerL, ), ) : Container(), diff --git a/lib/screens/log/log_entry/log_meal_detail.dart b/lib/screens/log/log_entry/log_meal_detail.dart index 9a04d00..a0a2884 100644 --- a/lib/screens/log/log_entry/log_meal_detail.dart +++ b/lib/screens/log/log_entry/log_meal_detail.dart @@ -1,7 +1,8 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; -import 'package:diameter/components/forms.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/components/forms/form_wrapper.dart'; import 'package:diameter/models/accuracy.dart'; import 'package:diameter/models/log_meal.dart'; import 'package:diameter/models/meal.dart'; @@ -37,13 +38,10 @@ class _LogMealDetailScreenState extends State { bool _isSaving = false; bool _isExpanded = false; - double _amount = 1; - final GlobalKey _logMealForm = GlobalKey(); final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); - final _amountController = TextEditingController(text: ''); final _carbsRatioController = TextEditingController(text: ''); final _portionSizeController = TextEditingController(text: ''); final _totalCarbsController = TextEditingController(text: ''); @@ -62,6 +60,7 @@ class _LogMealDetailScreenState extends State { final _mealPortionTypeController = TextEditingController(text: ''); final _portionSizeAccuracyController = TextEditingController(text: ''); final _carbsRatioAccuracyController = TextEditingController(text: ''); + final _amountController = TextEditingController(text: '1'); List _meals = []; List _mealCategories = []; @@ -84,12 +83,11 @@ class _LogMealDetailScreenState extends State { if (widget.id != 0) { _valueController.text = _logMeal!.value; - _amountController.text = _logMeal!.amount.toString(); _carbsRatioController.text = (_logMeal!.carbsRatio ?? '').toString(); _portionSizeController.text = (_logMeal!.portionSize ?? '').toString(); _totalCarbsController.text = (_logMeal!.totalCarbs ?? '').toString(); + _amountController.text = (_logMeal!.amount).toString(); _notesController.text = _logMeal!.notes ?? ''; - _meal = _logMeal!.meal.target; _mealController.text = (_meal ?? '').toString(); _mealSource = _logMeal!.mealSource.target; @@ -105,10 +103,24 @@ class _LogMealDetailScreenState extends State { _carbsRatioAccuracyController.text = (_carbsRatioAccuracy ?? '').toString(); } + } - if (_amountController.text == '') { - _amountController.text = '1'; - } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _carbsRatioController.dispose(); + _portionSizeController.dispose(); + _totalCarbsController.dispose(); + _notesController.dispose(); + _mealController.dispose(); + _mealSourceController.dispose(); + _mealCategoryController.dispose(); + _mealPortionTypeController.dispose(); + _portionSizeAccuracyController.dispose(); + _carbsRatioAccuracyController.dispose(); + _amountController.dispose(); + super.dispose(); } void reload({String? message}) { @@ -177,7 +189,7 @@ class _LogMealDetailScreenState extends State { _carbsRatioController.text = (meal?.carbsRatio ?? '').toString(); _amountController.text = '1'; _portionSizeController.text = (meal?.portionSize ?? '').toString(); - _totalCarbsController.text = (meal?.carbsPerPortion ?? '').toString(); + _totalCarbsController.text = (meal?.carbsPerPortion ?? '').toString(); }); updateMealSource(meal?.mealSource.target); updateMealCategory(meal?.mealCategory.target); @@ -197,6 +209,7 @@ class _LogMealDetailScreenState extends State { carbsRatio: double.tryParse(_carbsRatioController.text), portionSize: double.tryParse(_portionSizeController.text), totalCarbs: double.tryParse(_totalCarbsController.text), + amount: double.parse(_amountController.text), ); logMeal.logEntry.targetId = widget.logEntryId; logMeal.meal.target = _meal; @@ -222,6 +235,7 @@ class _LogMealDetailScreenState extends State { _mealSource != null || _mealCategory != null || _mealPortionType != null || + double.tryParse(_amountController.text) != 1 || double.tryParse(_carbsRatioController.text) != null || double.tryParse(_portionSizeController.text) != null || double.tryParse(_totalCarbsController.text) != null || @@ -234,6 +248,8 @@ class _LogMealDetailScreenState extends State { _mealSource != _logMeal!.mealSource.target || _mealCategory != _logMeal!.mealCategory.target || _mealPortionType != _logMeal!.mealPortionType.target || + double.tryParse(_amountController.text) != + _logMeal!.amount || double.tryParse(_carbsRatioController.text) != _logMeal!.carbsRatio || double.tryParse(_portionSizeController.text) != @@ -245,7 +261,7 @@ class _LogMealDetailScreenState extends State { _portionSizeAccuracy != _logMeal!.portionSizeAccuracy.target || _notesController.text != (_logMeal!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -256,15 +272,16 @@ class _LogMealDetailScreenState extends State { } void updateAmount(double? amount) { - double? previousAmount; + double previousAmount; + double newAmount; double? portionSize; double? carbsRatio; - previousAmount = _amount; + previousAmount = double.tryParse(_amountController.text) ?? 1; + newAmount = amount?.toDouble() ?? 1; setState(() { - _amountController.text = (amount ?? '').toString(); - _amount = amount ?? 1; + _amountController.text = newAmount.toString(); }); if (_carbsRatioController.text != '') { @@ -274,9 +291,9 @@ class _LogMealDetailScreenState extends State { portionSize = double.tryParse(_portionSizeController.text); } - if (amount != null && portionSize != null) { + if (portionSize != null) { setState(() { - portionSize = portionSize! / (previousAmount ?? 1) * amount; + portionSize = portionSize! / previousAmount * newAmount; _portionSizeController.text = portionSize.toString(); }); if (carbsRatio != null) { @@ -289,14 +306,11 @@ class _LogMealDetailScreenState extends State { } void calculateThirdMeasurementOfPortionCarbsRelation() { - int? amount; + double? amount = double.tryParse(_amountController.text) ?? 1; double? carbsRatio; double? portionSize; double? carbsPerPortion; - if (_amountController.text != '') { - amount = int.tryParse(_amountController.text); - } if (_carbsRatioController.text != '') { carbsRatio = double.tryParse(_carbsRatioController.text); } @@ -310,15 +324,14 @@ class _LogMealDetailScreenState extends State { if (carbsRatio != null && portionSize != null && carbsPerPortion == null) { setState(() { _totalCarbsController.text = - Utils.calculateCarbs(carbsRatio!, portionSize! * (amount ?? 1)) - .toString(); + Utils.calculateCarbs(carbsRatio!, portionSize! * amount).toString(); }); } if (carbsRatio == null && portionSize != null && carbsPerPortion != null) { setState(() { - _carbsRatioController.text = Utils.calculateCarbsRatio( - carbsPerPortion!, portionSize! * (amount ?? 1)) - .toString(); + _carbsRatioController.text = + Utils.calculateCarbsRatio(carbsPerPortion!, portionSize! * amount) + .toString(); }); } if (carbsRatio != null && portionSize == null && carbsPerPortion != null) { @@ -388,12 +401,44 @@ class _LogMealDetailScreenState extends State { ), ], ), - NumberFormField( - controller: _amountController, - label: 'Amount', - suffix: _mealPortionType?.value, - min: 0, - onChanged: updateAmount, + Row( + children: [ + Expanded( + child: NumberFormField( + controller: _amountController, + label: 'Amount', + suffix: _mealPortionType?.value, + onChanged: updateAmount, + ), + ), + TextButton( + onPressed: () => updateAmount(0.5), + child: Column( + children: const [ + Text('1', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),), + Text('2'), + ], + ), + ), + TextButton( + onPressed: () => updateAmount(0.33), + child: Column( + children: const [ + Text('1', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),), + Text('3'), + ], + ), + ), + TextButton( + onPressed: () => updateAmount(0.67), + child: Column( + children: const [ + Text('2', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),), + Text('3'), + ], + ), + ), + ], ), Row( children: [ diff --git a/lib/screens/log/log_entry/log_meal_list.dart b/lib/screens/log/log_entry/log_meal_list.dart index 3bba521..b9a63ec 100644 --- a/lib/screens/log/log_entry/log_meal_list.dart +++ b/lib/screens/log/log_entry/log_meal_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/log_entry.dart'; import 'package:diameter/models/log_meal.dart'; import 'package:diameter/models/settings.dart'; @@ -21,6 +21,12 @@ class LogMealListScreen extends StatefulWidget { class _LogMealListScreenState extends State { final ScrollController _scrollController = ScrollController(); + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { widget.reload(); @@ -56,7 +62,7 @@ class _LogMealListScreenState extends State { void handleDeleteAction(LogMeal meal) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(meal), message: 'Are you sure you want to delete this Meal?', diff --git a/lib/screens/log/log_event/log_event_detail.dart b/lib/screens/log/log_event/log_event_detail.dart index 1d5c7cc..414db67 100644 --- a/lib/screens/log/log_event/log_event_detail.dart +++ b/lib/screens/log/log_event/log_event_detail.dart @@ -1,7 +1,10 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/components/forms/boolean_form_field.dart'; +import 'package:diameter/components/forms/date_time_form_field.dart'; +import 'package:diameter/components/forms/time_of_day_form_field.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/basal_profile.dart'; import 'package:diameter/models/bolus_profile.dart'; import 'package:diameter/models/log_event.dart'; @@ -99,6 +102,21 @@ class _LogEventDetailScreenState extends State { updateEndTime(); } + @override + void dispose() { + _scrollController.dispose(); + _timeController.dispose(); + _endTimeController.dispose(); + _dateController.dispose(); + _endDateController.dispose(); + _reminderDurationController.dispose(); + _notesController.dispose(); + _eventTypeController.dispose(); + _bolusProfileController.dispose(); + _basalProfileController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -242,7 +260,7 @@ class _LogEventDetailScreenState extends State { (_notesController.text != (_logEvent!.notes ?? '') || _eventType != _logEvent!.eventType.target || _hasEndTime != _logEvent!.hasEndTime)))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -382,14 +400,17 @@ class _LogEventDetailScreenState extends State { ), ], ), - TextFormField( - controller: _reminderDurationController, - keyboardType: - const TextInputType.numberWithOptions(), - decoration: InputDecoration( - labelText: 'Default Reminder Duration', - suffixText: ' min', - enabled: _hasEndTime, + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: TextFormField( + controller: _reminderDurationController, + keyboardType: + const TextInputType.numberWithOptions(), + decoration: InputDecoration( + labelText: 'Default Reminder Duration', + suffixText: ' min', + enabled: _hasEndTime, + ), ), ), Row( @@ -426,39 +447,42 @@ class _LogEventDetailScreenState extends State { ), ], ), - Row( - children: [ - Expanded( - child: AutoCompleteDropdownButton< - BasalProfile>( - controller: _basalProfileController, - selectedItem: _basalProfile, - label: 'Basal Profile', - items: _basalProfiles, - onChanged: updateBasalProfile, + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton< + BasalProfile>( + controller: _basalProfileController, + selectedItem: _basalProfile, + label: 'Basal Profile', + items: _basalProfiles, + onChanged: updateBasalProfile, + ), ), - ), - IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => _basalProfile == - null - ? const BasalProfileDetailScreen() - : BasalProfileDetailScreen( - id: _basalProfile!.id), - ), - ).then((result) { - updateBasalProfile(result?[1]); - reload(message: result?[0]); - }); - }, - icon: Icon(_basalProfile == null - ? Icons.add - : Icons.edit), - ), - ], + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _basalProfile == + null + ? const BasalProfileDetailScreen() + : BasalProfileDetailScreen( + id: _basalProfile!.id), + ), + ).then((result) { + updateBasalProfile(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_basalProfile == null + ? Icons.add + : Icons.edit), + ), + ], + ), ) ] : []), diff --git a/lib/screens/log/log_event/log_event_list.dart b/lib/screens/log/log_event/log_event_list.dart index a81d3b8..62fee4d 100644 --- a/lib/screens/log/log_event/log_event_list.dart +++ b/lib/screens/log/log_event/log_event_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/log_event.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/screens/log/log_event/log_event_detail.dart'; @@ -25,6 +25,12 @@ class _LogEventListScreenState extends State { reload(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _activeEvents = LogEvent.getAllActiveForTime(DateTime.now()); @@ -73,7 +79,7 @@ class _LogEventListScreenState extends State { void handleDeleteAction(LogEvent logEvent) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(logEvent), message: 'Are you sure you want to delete this Event?', @@ -91,7 +97,7 @@ class _LogEventListScreenState extends State { void handleStopAction(LogEvent event) async { if (Settings.get().showConfirmationDialogOnStopEvent) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onStop(event), message: 'Are you sure you want to end this Event?', diff --git a/lib/screens/log/log_event/log_event_type_detail.dart b/lib/screens/log/log_event/log_event_type_detail.dart index 3172e54..eb584c5 100644 --- a/lib/screens/log/log_event/log_event_type_detail.dart +++ b/lib/screens/log/log_event/log_event_type_detail.dart @@ -1,7 +1,9 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/components/forms/boolean_form_field.dart'; +import 'package:diameter/components/forms/duration_form_field.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/basal_profile.dart'; import 'package:diameter/models/bolus_profile.dart'; import 'package:diameter/models/log_event_type.dart'; @@ -32,10 +34,10 @@ class _EventTypeDetailScreenState extends State { final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); - final _defaultReminderDurationController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); bool _hasEndTime = false; + int _defaultReminderDuration = 0; BolusProfile? _bolusProfile; BasalProfile? _basalProfile; final _bolusProfileController = TextEditingController(text: ''); @@ -52,8 +54,8 @@ class _EventTypeDetailScreenState extends State { if (_logEventType != null) { _valueController.text = _logEventType!.value; - _defaultReminderDurationController.text = - (_logEventType!.defaultReminderDuration ?? '').toString(); + _defaultReminderDuration = + _logEventType!.defaultReminderDuration ?? 0; _hasEndTime = _logEventType!.hasEndTime; _notesController.text = _logEventType!.notes ?? ''; _basalProfile = _logEventType!.basalProfile.target; @@ -63,6 +65,16 @@ class _EventTypeDetailScreenState extends State { } } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + _bolusProfileController.dispose(); + _basalProfileController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -107,8 +119,7 @@ class _EventTypeDetailScreenState extends State { id: widget.id, value: _valueController.text, notes: _notesController.text, - defaultReminderDuration: - int.tryParse(_defaultReminderDurationController.text), + defaultReminderDuration: _defaultReminderDuration, hasEndTime: _hasEndTime, ); eventType.basalProfile.target = _basalProfile; @@ -127,17 +138,16 @@ class _EventTypeDetailScreenState extends State { if (Settings.get().showConfirmationDialogOnCancel && ((isNew && (_valueController.text != '' || - int.tryParse(_defaultReminderDurationController.text) != - null || + _defaultReminderDuration != 0 || _notesController.text != '' || _hasEndTime)) || (!isNew && (_valueController.text != _logEventType!.value || - int.tryParse(_defaultReminderDurationController.text) != + _defaultReminderDuration != _logEventType!.defaultReminderDuration || _notesController.text != (_logEventType!.notes ?? '') || _hasEndTime != _logEventType!.hasEndTime)))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: isNew, onSave: handleSaveAction, @@ -189,16 +199,12 @@ class _EventTypeDetailScreenState extends State { ? [ Padding( padding: const EdgeInsets.only(bottom: 10.0), - child: TextFormField( - controller: _defaultReminderDurationController, - keyboardType: - const TextInputType.numberWithOptions(), - decoration: InputDecoration( - labelText: 'Default Reminder Duration', - suffixText: ' min', - enabled: _hasEndTime, + child: DurationFormField( + minutes: _defaultReminderDuration, + label: 'Default Reminder Duration', + onChanged: (value) => _defaultReminderDuration = value ?? 0, + showSteppers: true, ), - ), ), Padding( padding: const EdgeInsets.only(bottom: 10.0), diff --git a/lib/screens/log/log_event/log_event_type_list.dart b/lib/screens/log/log_event/log_event_type_list.dart index 85711b1..07be148 100644 --- a/lib/screens/log/log_event/log_event_type_list.dart +++ b/lib/screens/log/log_event/log_event_type_list.dart @@ -22,6 +22,12 @@ class _LogEventTypeListScreenState extends State { reload(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _logEventTypes = LogEventType.getAll(); @@ -66,8 +72,8 @@ class _LogEventTypeListScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => - EventTypeDetailScreen(id: logEventType.id), + builder: (context) => EventTypeDetailScreen( + id: logEventType.id), ), ).then((result) => reload(message: result?[0])); }, diff --git a/lib/screens/meal/meal_category_detail.dart b/lib/screens/meal/meal_category_detail.dart index 0c02654..b498ac0 100644 --- a/lib/screens/meal/meal_category_detail.dart +++ b/lib/screens/meal/meal_category_detail.dart @@ -1,6 +1,6 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:flutter/material.dart'; @@ -38,6 +38,14 @@ class _MealCategoryDetailScreenState extends State { } } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -80,7 +88,7 @@ class _MealCategoryDetailScreenState extends State { (!_isNew && (_mealCategory!.value != _valueController.text || (_mealCategory!.notes ?? '') != _notesController.text))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, diff --git a/lib/screens/meal/meal_category_list.dart b/lib/screens/meal/meal_category_list.dart index e5acda3..d4c8211 100644 --- a/lib/screens/meal/meal_category_list.dart +++ b/lib/screens/meal/meal_category_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/screens/meal/meal_category_detail.dart'; @@ -25,6 +25,12 @@ class _MealCategoryListScreenState extends State { reload(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _mealCategories = MealCategory.getAll(); @@ -49,7 +55,7 @@ class _MealCategoryListScreenState extends State { void handleDeleteAction(MealCategory mealCategory) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(mealCategory), message: 'Are you sure you want to delete this Meal Category?', diff --git a/lib/screens/meal/meal_detail.dart b/lib/screens/meal/meal_detail.dart index 7ab2e28..a1ef3b8 100644 --- a/lib/screens/meal/meal_detail.dart +++ b/lib/screens/meal/meal_detail.dart @@ -1,7 +1,7 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/accuracy.dart'; import 'package:diameter/models/meal.dart'; import 'package:diameter/models/meal_category.dart'; @@ -101,6 +101,23 @@ class _MealDetailScreenState extends State { } } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _carbsRatioController.dispose(); + _portionSizeController.dispose(); + _carbsPerPortionController.dispose(); + _delayedBolusDurationController.dispose(); + _notesController.dispose(); + _mealSourceController.dispose(); + _mealCategoryController.dispose(); + _mealPortionTypeController.dispose(); + _portionSizeAccuracyController.dispose(); + _carbsRatioAccuracyController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -215,7 +232,7 @@ class _MealDetailScreenState extends State { _meal!.delayedBolusDuration || _delayedBolusPercentage != _meal!.delayedBolusPercentage || _notesController.text != (_meal!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -488,11 +505,12 @@ class _MealDetailScreenState extends State { child: Row( mainAxisSize: MainAxisSize.max, children: [ - Text( - 'ADDITIONAL FIELDS', - style: Theme.of(context).textTheme.subtitle2, + Expanded( + child: Text( + 'ADDITIONAL FIELDS', + style: Theme.of(context).textTheme.subtitle2, + ), ), - const Spacer(), Icon(_isExpanded ? Icons.expand_less : Icons.expand_more), diff --git a/lib/screens/meal/meal_list.dart b/lib/screens/meal/meal_list.dart index 45bfd31..579b77b 100644 --- a/lib/screens/meal/meal_list.dart +++ b/lib/screens/meal/meal_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/meal.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; @@ -25,6 +25,12 @@ class _MealListScreenState extends State { reload(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _meals = Meal.getAll(); @@ -49,7 +55,7 @@ class _MealListScreenState extends State { void handleDeleteAction(Meal meal) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(meal), message: 'Are you sure you want to delete this Meal?', @@ -97,42 +103,51 @@ class _MealListScreenState extends State { ), subtitle: Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Row( + child: Column( children: [ - Column( + Row( children: [ - Text(meal.mealSource.target?.value ?? ''), - Text(meal.notes ?? ''), + Expanded( + child: Text(meal.mealSource.target?.value ?? ''), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: ((meal.carbsPerPortion ?? 0) > 0) + ? [ + Text(meal.carbsPerPortion!.toStringAsPrecision(3)), + Text( + '${Settings.nutritionMeasurementSuffix} carbs', + textScaleFactor: 0.75), + ] + : [], + ), + ), + Expanded( + child: Column( + children: (meal.mealPortionType.hasValue) + ? [ + Text(meal.portionSize?.toStringAsPrecision(3) ?? ''), + Text( + '${Settings.nutritionMeasurementSuffix}$portionType', + textAlign: TextAlign.center, + textScaleFactor: 0.75 + ), + ] + : [], + ), + ), ], ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: ((meal.carbsPerPortion ?? 0) > 0) - ? [ - Text(meal.carbsPerPortion!.toStringAsPrecision(3)), - Text( - '${Settings.nutritionMeasurementSuffix} carbs', - textScaleFactor: 0.75), - ] - : [], + meal.notes != null && meal.notes!.trim() != '' ? Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded(child: Text(meal.notes ?? '')), + ], ), - ), - Expanded( - child: Column( - children: (meal.mealPortionType.hasValue) - ? [ - Text(meal.portionSize!.toStringAsPrecision(3)), - Text( - '${Settings.nutritionMeasurementSuffix}$portionType', - textAlign: TextAlign.center, - textScaleFactor: 0.75 - ), - ] - : [], - ), - ), + ) : Container(), ], ), ), diff --git a/lib/screens/meal/meal_portion_type_detail.dart b/lib/screens/meal/meal_portion_type_detail.dart index 85c774b..eb5ffb8 100644 --- a/lib/screens/meal/meal_portion_type_detail.dart +++ b/lib/screens/meal/meal_portion_type_detail.dart @@ -1,6 +1,6 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:flutter/material.dart'; @@ -40,6 +40,14 @@ class _MealPortionTypeDetailScreenState } } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -82,7 +90,7 @@ class _MealPortionTypeDetailScreenState (_valueController.text != _mealPortionType!.value || _notesController.text != (_mealPortionType!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, diff --git a/lib/screens/meal/meal_portion_type_list.dart b/lib/screens/meal/meal_portion_type_list.dart index 64c8463..7e453d5 100644 --- a/lib/screens/meal/meal_portion_type_list.dart +++ b/lib/screens/meal/meal_portion_type_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/screens/meal/meal_portion_type_detail.dart'; @@ -26,6 +26,12 @@ class _MealPortionTypeListScreenState extends State { reload(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _mealPortionTypes = MealPortionType.getAll(); @@ -50,7 +56,7 @@ class _MealPortionTypeListScreenState extends State { void handleDeleteAction(MealPortionType mealPortionType) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(mealPortionType), message: 'Are you sure you want to delete this Meal Portion Type?', diff --git a/lib/screens/meal/meal_source_detail.dart b/lib/screens/meal/meal_source_detail.dart index ac941cd..fb26892 100644 --- a/lib/screens/meal/meal_source_detail.dart +++ b/lib/screens/meal/meal_source_detail.dart @@ -1,7 +1,7 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/accuracy.dart'; import 'package:diameter/models/meal_category.dart'; import 'package:diameter/models/meal_portion_type.dart'; @@ -77,6 +77,18 @@ class _MealSourceDetailScreenState extends State { } } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + _defaultCarbsRatioAccuracyController.dispose(); + _defaultPortionSizeAccuracyController.dispose(); + _defaultMealCategoryController.dispose(); + _defaultMealPortionTypeController.dispose(); + super.dispose(); + } + void reload({String? message}) { if (widget.id != 0) { setState(() { @@ -132,7 +144,7 @@ class _MealSourceDetailScreenState extends State { _defaultMealPortionType != _mealSource!.defaultMealPortionType.target || _notesController.text != (_mealSource!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, diff --git a/lib/screens/meal/meal_source_list.dart b/lib/screens/meal/meal_source_list.dart index 543d8b3..479fcc9 100644 --- a/lib/screens/meal/meal_source_list.dart +++ b/lib/screens/meal/meal_source_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/meal_source.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; @@ -25,6 +25,12 @@ class _MealSourceListScreenState extends State { reload(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + void reload({String? message}) { setState(() { _mealSources = MealSource.getAll(); @@ -49,7 +55,7 @@ class _MealSourceListScreenState extends State { void handleDeleteAction(MealSource mealSource) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(mealSource), message: 'Are you sure you want to delete this Meal Source?', diff --git a/lib/screens/recipe/recipe_detail.dart b/lib/screens/recipe/recipe_detail.dart index 963c92d..6c08173 100644 --- a/lib/screens/recipe/recipe_detail.dart +++ b/lib/screens/recipe/recipe_detail.dart @@ -1,7 +1,7 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; -import 'package:diameter/components/forms.dart'; +import 'package:diameter/utils/dialog_utils.dart'; +import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; +import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/ingredient.dart'; import 'package:diameter/models/meal.dart'; import 'package:diameter/models/recipe.dart'; @@ -31,11 +31,11 @@ class _RecipeDetailScreenState extends State { final ScrollController _scrollController = ScrollController(); final _nameController = TextEditingController(text: ''); - final _servingsController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); + + double _servings = 1; final List _ingredientControllers = []; - final List _ingredientAmountControllers = []; List _meals = []; @@ -49,15 +49,13 @@ class _RecipeDetailScreenState extends State { if (_recipe != null) { _nameController.text = _recipe!.name; - _servingsController.text = (_recipe!.servings ?? '').toString(); + _servings = _recipe!.servings ?? 1; _notesController.text = _recipe!.notes ?? ''; if (_ingredients.isNotEmpty) { for (Ingredient ingredient in _ingredients) { _ingredientControllers.add( TextEditingController(text: ingredient.ingredient.target?.value)); - _ingredientAmountControllers - .add(TextEditingController(text: ingredient.amount.toString())); } } } @@ -91,7 +89,6 @@ class _RecipeDetailScreenState extends State { newIngredient.recipe.target = _recipe; _ingredients.add(newIngredient); _ingredientControllers.add(TextEditingController(text: '')); - _ingredientAmountControllers.add(TextEditingController(text: '')); }); } @@ -103,7 +100,7 @@ class _RecipeDetailScreenState extends State { Recipe recipe = Recipe( id: widget.id, name: _nameController.text, - servings: double.tryParse(_servingsController.text), + servings: _servings, notes: _notesController.text, ); Recipe.put(recipe); @@ -145,14 +142,13 @@ class _RecipeDetailScreenState extends State { if (Settings.get().showConfirmationDialogOnCancel && ((_isNew && (_nameController.text != '' || - _servingsController.text != '' || + _servings != 1 || _notesController.text != '')) || (!_isNew && (_nameController.text != _recipe!.name || - _servingsController.text != - (_recipe!.servings ?? '').toString() || + _servings != _recipe!.servings || _notesController.text != (_recipe!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -190,19 +186,19 @@ class _RecipeDetailScreenState extends State { return null; }, ), - NumberFormField( - controller: _servingsController, - label: 'Servings', - suffix: ' portions', - min: 0, - onChanged: (value) { - if ((value ?? 0) >= 0) { - setState(() { - _servingsController.text = (value ?? 0).toString(); - }); - } - }, - ), + // NumberFormField( + // value: _servings, + // label: 'Servings', + // suffix: ' portions', + // min: 0, + // onChanged: (value) { + // if (value != null && value >= 0) { + // setState(() { + // _servings = value.toDouble(); + // }); + // } + // }, + // ), TextFormField( keyboardType: TextInputType.multiline, controller: _notesController, @@ -285,25 +281,23 @@ class _RecipeDetailScreenState extends State { ), ], ), - Padding( - padding: const EdgeInsets.only(top: 10.0), - child: NumberFormField( - controller: - _ingredientAmountControllers[index], - label: 'Amount', - suffix: Settings.nutritionMeasurementSuffix, - min: 0, - onChanged: (value) { - if ((value ?? 0) >= 0) { - setState(() { - _ingredients[index].amount = value ?? 0; - _ingredientAmountControllers[index] - .text = (value ?? 0).toString(); - }); - } - }, - ), - ), + // Padding( + // padding: const EdgeInsets.only(top: 10.0), + // child: NumberFormField( + // controller: + // _ingredients[index].amount, + // label: 'Amount', + // suffix: Settings.nutritionMeasurementSuffix, + // min: 0, + // onChanged: (value) { + // if (value != null && value >= 0) { + // setState(() { + // _ingredients[index].amount = value.toDouble(); + // }); + // } + // }, + // ), + // ), ], ), ); diff --git a/lib/screens/recipe/recipe_list.dart b/lib/screens/recipe/recipe_list.dart index dc47f23..b4de28f 100644 --- a/lib/screens/recipe/recipe_list.dart +++ b/lib/screens/recipe/recipe_list.dart @@ -1,4 +1,4 @@ -import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/ingredient.dart'; import 'package:diameter/models/recipe.dart'; import 'package:diameter/models/settings.dart'; @@ -50,7 +50,7 @@ class _RecipeListScreenState extends State { void handleDeleteAction(Recipe recipe) async { if (Settings.get().showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(recipe), message: 'Are you sure you want to delete this Recipe?', diff --git a/lib/settings.dart b/lib/settings.dart index 74d7594..4c856da 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -1,9 +1,12 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/dropdown.dart'; -import 'package:diameter/components/forms.dart'; +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'; @@ -17,29 +20,33 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { late Settings _settings; - final TextEditingController _nutritionMeasurementLabelController = TextEditingController(text: ''); - final TextEditingController _glucoseMeasurementLabelController = TextEditingController(text: ''); + 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 String _dateFormat; - // late String? _longDateFormat; - // late String _timeFormat; - // late String? _longTimeFormat; - late bool _showConfirmationDialogOnCancel; late bool _showConfirmationDialogOnDelete; late bool _showConfirmationDialogOnStopEvent; - // late int _lowGlucoseMgPerDl; - // late int _moderateGlucoseMgPerDl; - // late int _highGlucoseMgPerDl; - // late double _lowGlucoseMmolPerL; - // late double _moderateGlucoseMmolPerL; - // late double _highGlucoseMmolPerDl; - @override void initState() { super.initState(); @@ -48,27 +55,50 @@ class _SettingsScreenState extends State { nutritionMeasurementLabels[_settings.nutritionMeasurementIndex]; _glucoseMeasurementLabelController.text = glucoseMeasurementLabels[_settings.glucoseMeasurementIndex]; - _onlyDisplayActiveGlucoseMeasurement = _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.activeOnly.index; + _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; + _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.both.index || + _settings.glucoseDisplayModeIndex == + GlucoseDisplayMode.bothForDetail.index; _displayBothGlucoseMeasurementsInListView = - _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.both.index || - _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.bothForList.index; - // _dateFormat = _settings.dateFormat; - // _longDateFormat = _settings.longDateFormat; - // _timeFormat = _settings.timeFormat; - // _longTimeFormat = _settings.longTimeFormat; + _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; - // _lowGlucoseMgPerDl = _settings.lowGlucoseMgPerDl; - // _moderateGlucoseMgPerDl = _settings.moderateGlucoseMgPerDl; - // _highGlucoseMgPerDl = _settings.highGlucoseMgPerDl; - // _lowGlucoseMmolPerL = _settings.lowGlucoseMmolPerL; - // _moderateGlucoseMmolPerL = _settings.moderateGlucoseMmolPerL; - // _highGlucoseMmolPerDl = _settings.highGlucoseMmolPerDl; + } + + @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}) { @@ -92,42 +122,35 @@ class _SettingsScreenState extends State { void saveSettings() { Settings.put(Settings( id: _settings.id, - nutritionMeasurementIndex: - nutritionMeasurementLabels.indexOf(_nutritionMeasurementLabelController.text), - glucoseMeasurementIndex: - glucoseMeasurementLabels.indexOf(_glucoseMeasurementLabelController.text), + nutritionMeasurementIndex: nutritionMeasurementLabels + .indexOf(_nutritionMeasurementLabelController.text), + glucoseMeasurementIndex: glucoseMeasurementLabels + .indexOf(_glucoseMeasurementLabelController.text), glucoseDisplayModeIndex: _onlyDisplayActiveGlucoseMeasurement - ? GlucoseDisplayMode.activeOnly.index - : _displayBothGlucoseMeasurementsInDetailView && _displayBothGlucoseMeasurementsInListView - ? GlucoseDisplayMode.both.index + ? GlucoseDisplayMode.activeOnly.index + : _displayBothGlucoseMeasurementsInDetailView && + _displayBothGlucoseMeasurementsInListView + ? GlucoseDisplayMode.both.index : _displayBothGlucoseMeasurementsInDetailView - ? GlucoseDisplayMode.bothForDetail.index + ? GlucoseDisplayMode.bothForDetail.index : GlucoseDisplayMode.bothForList.index, - // dateFormat: _dateFormat, - // longDateFormat: _longDateFormat, - // timeFormat: _timeFormat, - // longTimeFormat: _longTimeFormat, - // showConfirmationDialogOnCancel: _showConfirmationDialogOnCancel, - // showConfirmationDialogOnDelete: _showConfirmationDialogOnDelete, - // showConfirmationDialogOnStopEvent: _showConfirmationDialogOnStopEvent, - // lowGlucoseMgPerDl: _dateFormat: _dateFormat, - // longDateFormat: _longDateFormat, - // timeFormat: _timeFormat, - // longTimeFormat: _longTimeFormat, + 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, - // lowGlucoseMgPerDl: _lowGlucoseMgPerDl, - // moderateGlucoseMgPerDl: _moderateGlucoseMgPerDl, - // highGlucoseMgPerDl: _highGlucoseMgPerDl, - // lowGlucoseMmolPerL: _lowGlucoseMmolPerL, - // moderateGlucoseMmolPerL: _moderateGlucoseMmolPerL, - // highGlucoseMmolPerDl: _highGlucoseMmolPerDl,lowGlucoseMgPerDl, - // moderateGlucoseMgPerDl: _moderateGlucoseMgPerDl, - // highGlucoseMgPerDl: _highGlucoseMgPerDl, - // lowGlucoseMmolPerL: _lowGlucoseMmolPerL, - // moderateGlucoseMmolPerL: _moderateGlucoseMmolPerL, - // highGlucoseMmolPerDl: _highGlucoseMmolPerDl, )); reload(message: 'Settings updated'); } @@ -138,7 +161,7 @@ class _SettingsScreenState extends State { } void handleResetAction() async { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: onReset, message: 'Are you sure you want to reset all settings?', @@ -153,111 +176,399 @@ class _SettingsScreenState extends State { ), 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: Row( - children: [ - Text( - 'MEASUREMENTS', - style: Theme.of(context).textTheme.subtitle2, - ), - const Spacer(), - ], - ), - ), - AutoCompleteDropdownButton( - controller: _nutritionMeasurementLabelController, - selectedItem: _nutritionMeasurementLabelController.text, - label: 'Preferred Nutrition Measurement', - items: nutritionMeasurementLabels, - onChanged: (value) { - setState(() { - _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) { - setState(() { - _glucoseMeasurementLabelController.text = value ?? ''; - }); - 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: Row( - children: [ - Text( - 'CONFIRMATION PROMPTS', - style: Theme.of(context).textTheme.subtitle2, - ), - const Spacer(), - ], + child: GestureDetector( + onTap: () => setState(() { + _measurementsIsExpanded = !_measurementsIsExpanded; + }), + child: Row( + children: [ + Expanded( + child: Text( + 'MEASUREMENTS', + style: Theme.of(context).textTheme.subtitle2, + ), ), - ), - BooleanFormField( - value: _showConfirmationDialogOnCancel, - label: 'on cancelling edit or creation of a record if changes have already been made', - onChanged: (value) { - _showConfirmationDialogOnCancel = value; - saveSettings(); - }, + Icon(_measurementsIsExpanded + ? Icons.expand_less + : Icons.expand_more), + ], + ), + ), ), - BooleanFormField( - value: _showConfirmationDialogOnDelete, - label: 'on deleting a record', - onChanged: (value) { - _showConfirmationDialogOnDelete = value; - saveSettings(); - }, + 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(); + }, + ), + ] + : [], ), - 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(() { + _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, + ), + ], + ), + ), + ), + ], + ), + ), + ] + : [], ), ], ), diff --git a/lib/utils/date_time_utils.dart b/lib/utils/date_time_utils.dart index f997fd8..d213d58 100644 --- a/lib/utils/date_time_utils.dart +++ b/lib/utils/date_time_utils.dart @@ -10,7 +10,8 @@ class DateTimeUtils { return fallback; } DateTime localDate = date.toLocal(); - final DateFormat formatter = DateFormat('${Settings.get().dateFormat} ${Settings.get().timeFormat}'); + final DateFormat formatter = + DateFormat('${Settings.get().dateFormat} ${Settings.get().timeFormat}'); return formatter.format(localDate); } @@ -19,7 +20,8 @@ class DateTimeUtils { return fallback; } DateTime localDate = date.toLocal(); - final DateFormat formatter = DateFormat(Settings.get().longDateFormat ?? Settings.get().dateFormat); + final DateFormat formatter = + DateFormat(Settings.get().longDateFormat ?? Settings.get().dateFormat); return formatter.format(localDate); } @@ -29,8 +31,9 @@ class DateTimeUtils { return fallback; } DateTime localDate = date.toLocal(); - final DateFormat formatter = DateFormat( - longFormat == true ? Settings.get().longTimeFormat ?? Settings.get().timeFormat : Settings.get().timeFormat); + final DateFormat formatter = DateFormat(longFormat == true + ? Settings.get().longTimeFormat ?? Settings.get().timeFormat + : Settings.get().timeFormat); return formatter.format(localDate); } @@ -39,8 +42,9 @@ class DateTimeUtils { if (time == null) { return fallback; } - final DateFormat formatter = DateFormat( - longFormat == true ? Settings.get().longTimeFormat ?? Settings.get().timeFormat : Settings.get().timeFormat); + final DateFormat formatter = DateFormat(longFormat == true + ? Settings.get().longTimeFormat ?? Settings.get().timeFormat + : Settings.get().timeFormat); return formatter.format(convertTimeOfDayToDateTime(time)); } diff --git a/lib/components/dialogs.dart b/lib/utils/dialog_utils.dart similarity index 99% rename from lib/components/dialogs.dart rename to lib/utils/dialog_utils.dart index 79380fd..4be8235 100644 --- a/lib/components/dialogs.dart +++ b/lib/utils/dialog_utils.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -class Dialogs { +class DialogUtils { static void showCancelConfirmationDialog( {required BuildContext context, required bool isNew, diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 1b8e3eb..6ff0e66 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,11 +1,28 @@ import 'dart:math'; class Utils { - static double roundToDecimalPlaces(double value, int places) { - double mod = pow(10.0, places).toDouble(); + static double roundToDecimalPlaces(double value, int precision) { + double mod = pow(10.0, precision).toDouble(); return ((value * mod).round().toDouble() / mod); } + static double addDoublesWithPrecision(double a, double b, int precision) { + double mod = pow(10.0, precision).toDouble(); + double difference = (a * mod) + (b * mod); + return difference.round() / mod; + } + + static int getFractionDigitsLength(double value) { + final fractionDigits = value.toString().split('.'); + return fractionDigits[1].length; + } + + static String toStringMatchingTemplateFractionPrecision( + double value, double template) { + final precision = getFractionDigitsLength(template); + return value.toStringAsFixed(precision); + } + static double convertMgPerDlToMmolPerL(int mgPerDl) { return Utils.roundToDecimalPlaces(mgPerDl * 0.0555, 2); } @@ -14,18 +31,21 @@ class Utils { return (mmolPerL * 18.018).round(); } - static double calculateCarbs( - double carbsRatio, double portionSize) { + static double calculateCarbs(double carbsRatio, double portionSize) { return Utils.roundToDecimalPlaces(carbsRatio * portionSize / 100, 2); } static double calculateCarbsRatio( double carbsPerPortion, double portionSize) { - return portionSize > 0 ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2) : 0; + return portionSize > 0 + ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2) + : 0; } static double calculatePortionSize( double carbsRatio, double carbsPerPortion) { - return carbsRatio > 0 ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2) : 0; + return carbsRatio > 0 + ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2) + : 0; } } diff --git a/objectbox/data.mdb b/objectbox/data.mdb new file mode 100644 index 0000000..1d9c8cc Binary files /dev/null and b/objectbox/data.mdb differ diff --git a/objectbox/lock.mdb b/objectbox/lock.mdb new file mode 100644 index 0000000..94222e1 Binary files /dev/null and b/objectbox/lock.mdb differ diff --git a/pubspec.lock b/pubspec.lock index 5616526..7a848e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -141,48 +141,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" - connectivity_plus: - dependency: transitive - description: - name: connectivity_plus - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.0" - connectivity_plus_linux: - dependency: transitive - description: - name: connectivity_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - connectivity_plus_macos: - dependency: transitive - description: - name: connectivity_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - connectivity_plus_platform_interface: - dependency: transitive - description: - name: connectivity_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - connectivity_plus_web: - dependency: transitive - description: - name: connectivity_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0+1" - connectivity_plus_windows: - dependency: transitive - description: - name: connectivity_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" convert: dependency: transitive description: @@ -211,20 +169,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.0" - dbus: - dependency: transitive - description: - name: dbus - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.6" - dio: - dependency: transitive - description: - name: dio - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.4" fake_async: dependency: transitive description: @@ -277,11 +221,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -303,13 +242,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.4" http_multi_server: dependency: transitive description: @@ -324,13 +256,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.0" - idb_shim: - dependency: transitive - description: - name: idb_shim - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" intl: dependency: "direct main" description: @@ -380,6 +305,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -394,13 +326,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" - mime_type: - dependency: transitive - description: - name: mime_type - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" nested: dependency: transitive description: @@ -408,13 +333,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" - nm: - dependency: transitive - description: - name: nm - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.1" objectbox: dependency: "direct main" description: @@ -443,62 +361,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" - package_info_plus: - dependency: transitive - description: - name: package_info_plus - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - package_info_plus_linux: - dependency: transitive - description: - name: package_info_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - package_info_plus_macos: - dependency: transitive - description: - name: package_info_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - package_info_plus_web: - dependency: transitive - description: - name: package_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - package_info_plus_windows: - dependency: transitive - description: - name: package_info_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - parse_server_sdk: - dependency: transitive - description: - name: parse_server_sdk - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" - parse_server_sdk_flutter: - dependency: "direct main" - description: - name: parse_server_sdk_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" path: dependency: transitive description: @@ -555,13 +417,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.4" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.dartlang.org" - source: hosted - version: "4.4.0" platform: dependency: transitive description: @@ -611,76 +466,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" - sembast: - dependency: transitive - description: - name: sembast - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.1" - sembast_web: - dependency: transitive - description: - name: sembast_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1+1" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.9" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.9" - shared_preferences_ios: - dependency: transitive - description: - name: shared_preferences_ios - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.8" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.3" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.3" shelf: dependency: transitive description: @@ -714,20 +499,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" - sqflite: - dependency: "direct main" - description: - name: sqflite - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1+1" stack_trace: dependency: transitive description: @@ -756,13 +527,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" - synchronized: - dependency: transitive - description: - name: synchronized - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" term_glyph: dependency: transitive description: @@ -776,7 +540,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.8" timing: dependency: transitive description: @@ -791,13 +555,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" - uuid: - dependency: transitive - description: - name: uuid - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.5" vector_math: dependency: transitive description: @@ -833,20 +590,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" - xml: - dependency: transitive - description: - name: xml - url: "https://pub.dartlang.org" - source: hosted - version: "5.3.1" - xxtea: - dependency: transitive - description: - name: xxtea - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0da46cc..b200920 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,14 +9,11 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - parse_server_sdk_flutter: ^3.1.0 flutter: sdk: flutter - sqflite: ^2.0.0+4 path_provider: ^2.0.5 cupertino_icons: ^1.0.2 flex_color_scheme: ^3.0.1 - shared_preferences: ^2.0.8 intl: ^0.17.0 objectbox: ^1.2.0 objectbox_sync_flutter_libs: any diff --git a/sync-server b/sync-server new file mode 100755 index 0000000..39e5c76 Binary files /dev/null and b/sync-server differ diff --git a/sync-server-config.js b/sync-server-config.js new file mode 100644 index 0000000..e2c3132 --- /dev/null +++ b/sync-server-config.js @@ -0,0 +1,12 @@ +{ + "dbDirectory": "objectbox", + "dbMaxSize": "100G", + "modelFile": "lib/objectbox-model.json", + "bind": "ws://192.168.1.184:9999", + "browserBind": "http://127.0.0.1:9980", + "browserThreads": 4, + "certificatePath": "", + "auth": { + "sharedSecret": "m4Gwehzgv18jZ5gCVUBZl5li3Z0FX2Yb" + } +} \ No newline at end of file