diff --git a/TODO b/TODO index a522ce5..a7fccd9 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,150 @@ +MAIN TASKS: + Components/Framework: + ☐ 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) + ☐ set name properties as unique (and add checks to forms) + ☐ 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 + ☐ implement deletion by swiping left on item instead? + ☐ check for changes before navigating as well (not just on cancel) + Reports: + ☐ evaluate what type of reports there should be + ☐ try out graph/diagram components -Todo: - ☐ add active/deleted flag to all data models - ☐ account for deleted/disabled elements in dropdowns - ☐ place dropdown items right below their input - ✔ use local database instead of back4app @done(21-11-07 18:53) - \ No newline at end of file +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 filters + Log Entry: + ☐ check if there is still an active bolus when suggesting glucose bolus + Event Types: + ☐ 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: + ✔ only show current day @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview) + ✔ add calendar field on top to navigate @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview) + ✔ use currently selected day when adding a log entry @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview) + ✔ only show current day @done(22-01-24 05:39) @project(MAIN TASKS.Event Types) + ✔ add calendar field on top to navigate @done(22-01-24 05:39) @project(MAIN TASKS.Event Types) + ✔ use currently selected day when adding a log event @done(22-01-24 05:39) @project(MAIN TASKS.Event Types) + ✔ update number fields to use corresponding components @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework) + ✔ meal detail (carbs ratio, portion size, carbs per portion) @done(22-01-24 03:12) @project(MAIN TASKS.Components/Framework) + ✔ log meal detail (amount, carbs ratio, portion size, carbs per portion) @done(22-01-24 03:12) @project(MAIN TASKS.Components/Framework) + ✔ add "set manually" switch (like in log bolus detail) wherever parameters can be calculated from others @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework) + ✔ meal detail @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework) + ✔ log meal detail @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework) + ✔ put dropdowns first if they override name field @done(22-01-24 03:17) @project(MAIN TASKS.Components/Framework) + ✔ 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) + ✔ give option to pick meal from a different log entry (that doesn't have an associated bolus yet and within certain time span) @done(21-12-11 02:22) @project(MAIN TASKS.Log Entry) + ✔ find a better way to work with multiple glucose measurements @done(21-12-11 02:23) @project(FUTURE TASKS.General/Framework) + ✔ make components rounder/nicer/closer to new material style @done(21-12-10 04:10) @project(MAIN TASKS.Layout) + ✔ make sure 'null' isn't shown in text fields @done(21-12-10 04:23) @project(MAIN TASKS.General/Framework) + ✔ hide details like accuracies etc when picking meals @done(21-12-10 06:12) @project(MAIN TASKS.General/Framework) + ✔ add save and close and next buttons on rate creations @done(21-12-10 06:12) @project(MAIN TASKS.Basal/Bolus) + ✔ always calculate other glucose measurement from active one and make other one readonly @done(21-12-10 04:33) @project(MAIN TASKS.Basal/Bolus) + ✔ add save and close button @done(21-12-10 06:11) @project(MAIN TASKS.Log Entry) + ✔ move on to newly created entry after saving @done(21-12-10 06:11) @project(MAIN TASKS.Log Entry) + ✔ recalculate bolus upon deactivating 'set manually' option @done(21-12-10 06:18) @project(MAIN TASKS.Log Entry) + ✔ account for delayed percentage setting on choosing meals @done(21-12-10 06:39) @project(MAIN TASKS.Log Entry) + ✔ fix preloading of dropdown values (appear blank at first as of now) @done(21-12-09 05:31) @project(BUGFIXES.General/Framework) + ✔ glucose target isn't displayed correctly anymore @done(21-12-09 05:31) @project(BUGFIXES.Log Entry) + ✔ hide dropdown overlay on tapping anywhere else (especially menu) @done(21-12-07 21:04) @project(MAIN TASKS.General/Framework) + ✔ add clear button to dropdown @done(21-12-07 21:21) @project(MAIN TASKS.General/Framework) + ✔ add option to specify trend for blood sugar @done(21-12-07 14:20) @project(MAIN TASKS.Log Entry) + ✔ always calculate other glucose measurement from active one and make other one readonly @done(21-12-07 14:33) @project(MAIN TASKS.Log Entry) + ✔ scrollbars in rate overview not showing @done(21-12-06 20:01) @project(BUGFIXES.Basal/Bolus) + ✔ order category lists (meals, meal sources,...) alphabetically @done(21-12-06 20:34) @project(MAIN TASKS.General/Framework) + ✔ add delay to auto conversions @done(21-12-06 20:25) @project(MAIN TASKS.General/Framework) + ✔ show daily Basal sum in overview @done(21-12-06 21:09) @project(MAIN TASKS.Basal/Bolus) + ✔ show KI and stuff for Bolus in overview @done(21-12-06 21:44) @project(MAIN TASKS.Basal/Bolus) + ✔ apply target color settings to glucose @done(21-12-06 22:57) @project(MAIN TASKS.Log Overview) + ✔ improve log meal list display @done(21-12-06 20:25) @project(MAIN TASKS.Log Entry) + ✔ change delayed bolus rate to percentage @done(21-12-06 20:47) @project(MAIN TASKS.Meal) + ✔ add meal source, carbs and portion size as subtitle in list @done(21-12-06 22:01) @project(MAIN TASKS.Meal) + ✔ add option to hide warning dialogs on cancel, delete or event stop @done(21-12-05 19:18) @project(FUTURE TASKS.Settings) + ✔ fix settings saving @done(21-12-05 19:08) @project(MAIN TASKS.Settings) + ✔ add objectbox settings class and use instead of shared preferences @done(21-12-05 00:41) @project(MAIN TASKS.Settings) + ✔ provide percentage functionality for delayed bolus @done(21-12-04 21:39) @project(MAIN TASKS.Log Entry) + ✔ create two bolus entries accordingly @done(21-12-04 22:12) @project(MAIN TASKS.Log Entry) + ✔ replace active profile picking mode with simple dropdown @done(21-12-04 20:10) @project(MAIN TASKS.Basal/Bolus) + ✔ indicate both the default rate and the currently active one (according to event) @done(21-12-04 20:10) @project(MAIN TASKS.Basal/Bolus) + ✔ get rid of excessive cancellation warnings @done(21-12-04 19:09) @project(MAIN TASKS.Log Entry) + ✔ give a warning if event of same type is already running @done(21-12-04 18:50) @project(MAIN TASKS.Events) + ✔ implement reordering @started(21-12-03 23:12) @done(21-12-04 17:01) @lasted(17h49m38s) @project(MAIN TASKS.Accuracies) + ✔ show event start AND end times in list @done(21-12-03 22:04) @project(MAIN TASKS.Events) + ✔ separate events from log entries @done(21-12-01 23:37) @project(MAIN TASKS.Events) + ✔ show total bolus and carbs per entry @done(21-12-01 19:50) @project(MAIN TASKS.Log Overview) + ✔ display boli correctly @done(21-11-30 04:14) @project(MAIN TASKS.Log Entry) + ✔ replace meal and glucose boli with logbolus entities @done(21-11-30 03:56) @project(MAIN TASKS.Log Entry) + ✔ adjust/debug active events view @done(21-11-26 22:54) @project(MAIN TASKS.Log Overview) + ✔ show all active events, not just those assigned to the entry @done(21-11-26 22:12) @project(MAIN TASKS.Log Entry) + ✔ add active events view (as main menu item) @done(21-11-26 21:28) @project(MAIN TASKS.Log Overview) + ✔ add option to change bolus/basal profile for event duration @done(21-11-26 21:13) @project(MAIN TASKS.Event Types) + ✔ add deleted flag to all data models @done(21-11-26 18:56) @project(MAIN TASKS.General/Framework) + ✔ adjust remove and fetch methods accordingly @done(21-11-26 20:52) @project(MAIN TASKS.General/Framework) + ✔ implement tostring methods for all models @done(21-11-26 20:52) @project(MAIN TASKS.General/Framework) + ✔ fix logmeals/logboli/logevents @done(21-11-25 17:10) @project(MAIN TASKS.Log Entry) + ✔ add tab for bolus overview @done(21-11-24 22:05) @project(MAIN TASKS.Log Entry) + ✔ calculate bolus suggestions according to active profile @done(21-11-24 22:05) @project(MAIN TASKS.Log Entry) + ✔ place dropdown items right below their input @done(21-11-23 20:33) @project(MAIN TASKS.General/Framework) + ✔ add autocomplete function to dropdowns @done(21-11-23 20:33) @project(MAIN TASKS.General/Framework) + ✔ use local database instead of back4app @done(21-11-07 18:53) @project(MAIN TASKS.General/Framework) + ✔ use ids instead of passing entities around where possible @done(21-11-10 00:06) @project(MAIN TASKS.General/Framework) + ✔ add time picker for entry date/time @done(21-11-10 00:06) @project(MAIN TASKS.Log Entry) diff --git a/android/app/build.gradle b/android/app/build.gradle index e8ef626..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 @@ -44,7 +44,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.diameter" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName 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/app_theme.dart b/lib/components/app_theme.dart index 658b771..e46df56 100644 --- a/lib/components/app_theme.dart +++ b/lib/components/app_theme.dart @@ -5,19 +5,47 @@ class AppTheme { AppTheme._(); static ThemeData lightTheme = FlexColorScheme.light( - scheme: FlexScheme.mandyRed, - fontFamily: 'RobotoCondensed', + surfaceStyle: FlexSurface.medium, + scheme: FlexScheme.aquaBlue, + fontFamily: 'Roboto', ).toTheme; - static ThemeData darkTheme = FlexColorScheme.light( - scheme: FlexScheme.mandyRed, - fontFamily: 'RobotoCondensed', + static ThemeData darkTheme = FlexColorScheme.dark( + scheme: FlexScheme.aquaBlue, + fontFamily: 'Roboto', ).toTheme; static ThemeData makeTheme(ThemeData baseThemeData) { return baseThemeData.copyWith( - visualDensity: VisualDensity.compact, - bottomNavigationBarTheme: BottomNavigationBarThemeData( - backgroundColor: baseThemeData.primaryColor)); + cardTheme: baseThemeData.cardTheme.copyWith( + color: baseThemeData.bottomAppBarColor, + elevation: 1, + margin: const EdgeInsets.only(bottom: 10.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + ), + scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( + isAlwaysShown: true, + ), + textTheme: baseThemeData.textTheme.copyWith( + subtitle2: TextStyle( + color: baseThemeData.primaryColor, + letterSpacing: 2.0, + ), + ), + inputDecorationTheme: baseThemeData.inputDecorationTheme.copyWith( + fillColor: baseThemeData.textSelectionTheme.selectionColor, + border: const UnderlineInputBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + ), + ), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: baseThemeData.primaryColor, + ), + ); } } diff --git a/lib/components/data_table.dart b/lib/components/data_table.dart deleted file mode 100644 index 90c2101..0000000 --- a/lib/components/data_table.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; - -abstract class DataTableContent { - bool selected = false; - List asDataTableCells(List actions) => []; - static List asDataTableColumns() => []; -} - -class DataTableSourceBuilder extends DataTableSource { - final List data; - final BuildContext context; - - DataTableSourceBuilder(this.context, this.data); - - @override - bool get isRowCountApproximate => false; - - @override - int get rowCount => data.length; - - @override - int get selectedRowCount { - int count = 0; - for (var element in data) { - if (element.selected) { - count++; - } - } - return count; - } - - @override - DataRow? getRow(int index) { - assert(index >= 0); - if (index >= data.length) return null; - final rowData = data[index]; - return DataRow.byIndex( - index: index, - selected: rowData.selected, - cells: rowData.asDataTableCells([]), - ); - } -} diff --git a/lib/components/detail.dart b/lib/components/detail.dart index 160a853..8beb917 100644 --- a/lib/components/detail.dart +++ b/lib/components/detail.dart @@ -2,10 +2,22 @@ import 'package:flutter/material.dart'; class DetailBottomRow extends StatefulWidget { final void Function()? onCancel; - final void Function()? onSave; + final void Function()? onAction; + final void Function()? onMiddleAction; + final String actionText; + final String middleActionText; + final IconData actionIcon; + final IconData middleActionIcon; const DetailBottomRow( - {Key? key, required this.onCancel, required this.onSave}) + {Key? key, + required this.onCancel, + required this.onAction, + this.onMiddleAction, + this.actionText = 'SAVE', + this.actionIcon = Icons.save, + this.middleActionText = 'SAVE & CLOSE', + this.middleActionIcon = Icons.done}) : super(key: key); @override @@ -19,6 +31,7 @@ class _DetailBottomRowState extends State { child: Padding( padding: const EdgeInsets.all(10.0), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ElevatedButton.icon( onPressed: widget.onCancel, @@ -28,14 +41,23 @@ class _DetailBottomRowState extends State { ), label: const Text('CANCEL'), ), - const Spacer(), + widget.onMiddleAction != null + ? ElevatedButton.icon( + onPressed: widget.onMiddleAction, + icon: Icon( + widget.middleActionIcon, + size: 18.0, + ), + label: Text(widget.middleActionText), + ) + : const Spacer(), ElevatedButton.icon( - onPressed: widget.onSave, - icon: const Icon( - Icons.save, + onPressed: widget.onAction, + icon: Icon( + widget.actionIcon, size: 18.0, ), - label: const Text('SAVE'), + label: Text(widget.actionText), ), ], ), diff --git a/lib/components/forms.dart b/lib/components/forms.dart deleted file mode 100644 index a09f32c..0000000 --- a/lib/components/forms.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'package:diameter/components/progress_indicator.dart'; -import 'package:flutter/material.dart'; - -class StyledForm extends StatefulWidget { - final List? fields; - final List? buttons; - final GlobalKey? formState; - - const StyledForm({Key? key, this.formState, this.fields, this.buttons}) - : super(key: key); - - @override - _StyledFormState createState() => _StyledFormState(); -} - -class _StyledFormState 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 StyledBooleanFormField extends StatefulWidget { - final bool value; - final String label; - final void Function(bool) onChanged; - final bool? enabled; - - const StyledBooleanFormField( - {Key? key, - required this.value, - required this.label, - required this.onChanged, - this.enabled}) - : super(key: key); - - @override - _StyledBooleanFormFieldState createState() => _StyledBooleanFormFieldState(); -} - -class _StyledBooleanFormFieldState extends State { - @override - Widget build(BuildContext context) { - return FormField(builder: (context) { - return ListTile( - onTap: () => widget.onChanged(!widget.value), - trailing: Switch( - value: widget.value, - onChanged: widget.onChanged, - ), - title: Text(widget.label), - enabled: widget.enabled ?? true, - ); - }); - } -} - -class StyledTimeOfDayFormField extends StatefulWidget { - final TimeOfDay time; - final TextEditingController controller; - final String label; - final void Function(TimeOfDay?) onChanged; - - const StyledTimeOfDayFormField( - {Key? key, - required this.time, - required this.controller, - required this.label, - required this.onChanged}) - : super(key: key); - - @override - _StyledTimeOfDayFormFieldState createState() => - _StyledTimeOfDayFormFieldState(); -} - -class _StyledTimeOfDayFormFieldState 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 StyledDropdownButton extends StatefulWidget { - final String label; - final T? selectedItem; - final List items; - final Widget Function(T item) renderItem; - final void Function(T? value) onChanged; - - const StyledDropdownButton( - {Key? key, - this.selectedItem, - required this.label, - required this.items, - required this.renderItem, - required this.onChanged}) - : super(key: key); - - @override - _StyledDropdownButtonState createState() => _StyledDropdownButtonState(); -} - -class _StyledDropdownButtonState extends State> { - @override - Widget build(BuildContext context) { - return DropdownButtonFormField( - decoration: InputDecoration( - labelText: widget.label, - ), - value: widget.selectedItem, - onChanged: widget.onChanged, - items: widget.items - .map((item) => DropdownMenuItem( - value: item, - child: widget.renderItem(item), - )) - .toList(), - ); - } -} - -class StyledFutureDropdownButton extends StatefulWidget { - final String label; - final String? selectedItem; - final Future> items; - final String? Function(T item) getItemValue; - final Widget Function(T item) renderItem; - final void Function(String? value) onChanged; - - const StyledFutureDropdownButton( - {Key? key, - this.selectedItem, - required this.label, - required this.items, - required this.getItemValue, - required this.renderItem, - required this.onChanged}) - : super(key: key); - - @override - _StyledFutureDropdownButtonState createState() => - _StyledFutureDropdownButtonState(); -} - -class _StyledFutureDropdownButtonState - extends State> { - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: widget.items, - builder: (context, snapshot) { - return ViewWithProgressIndicator( - snapshot: snapshot, - padding: const EdgeInsets.all(10.0), - progressIndicatorSize: 44, - child: snapshot.data == null || snapshot.data!.isEmpty - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Padding( - padding: EdgeInsets.all(10.0), - child: Text('No Meal Sources'), - ) - ], - ) - : DropdownButtonFormField( - decoration: InputDecoration( - labelText: widget.label, - ), - value: widget.selectedItem, - onChanged: widget.onChanged, - items: snapshot.data! - .map((item) => DropdownMenuItem( - value: widget.getItemValue(item), - child: widget.renderItem(item), - )) - .toList(), - ), - ); - }, - ); - } -} diff --git a/lib/components/forms/auto_complete_dropdown_button.dart b/lib/components/forms/auto_complete_dropdown_button.dart new file mode 100644 index 0000000..cd858e6 --- /dev/null +++ b/lib/components/forms/auto_complete_dropdown_button.dart @@ -0,0 +1,230 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class AutoCompleteDropdownButton extends StatefulWidget { + final String label; + final T? selectedItem; + final List items; + final void Function(T? value) onChanged; + final List Function(String? value)? applyQuery; + final TextEditingController controller; + + const AutoCompleteDropdownButton( + {Key? key, + this.selectedItem, + required this.label, + required this.items, + required this.onChanged, + this.applyQuery, + required this.controller}) + : super(key: key); + + @override + _AutoCompleteDropdownButtonState createState() => + _AutoCompleteDropdownButtonState(); +} + +class _AutoCompleteDropdownButtonState + extends State> { + late List options; + late List suggestions; + + final FocusNode focusNode = FocusNode(); + final LayerLink layerLink = LayerLink(); + OverlayEntry? entry; + bool isOpen = false; + + @override + void initState() { + super.initState(); + + setState(() { + options = widget.items; + suggestions = []; + }); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + void toggleOverlay() { + isOpen ? hideOverlay() : showOverlay(); + } + + void showOverlay() { + hideOverlay(); + focusNode.requestFocus(); + + List items = []; + Divider? divider; + + ScrollController _scrollController = ScrollController(); + + final overlay = Overlay.of(context)!; + final renderBox = context.findRenderObject() as RenderBox; + + if (widget.selectedItem != null) { + items.add(buildListTile(widget.selectedItem!)); + } + + if (suggestions.isNotEmpty) { + items.addAll(suggestions + .where((item) => item != widget.selectedItem) + .map((item) => buildListTile(item))); + } + + if ((widget.selectedItem != null || suggestions.isNotEmpty) && + (options.length - + suggestions.length - + (widget.selectedItem == null ? 0 : 1) > + 0)) { + divider = const Divider(height: 10); + items.add(divider); + } + + items.addAll(options + .where((item) => + !(widget.selectedItem != null && + item.toString() == widget.selectedItem!.toString()) && + !suggestions.contains(item)) + .map((item) => buildListTile(item)) + .toList()); + + final screenHeight = MediaQuery.of(context).size.height; + final neededHeight = renderBox.size.height * (items.length - 1) + + (divider?.height ?? renderBox.size.height); + final availableHeight = screenHeight - + (renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height); + bool displayAbove = neededHeight > availableHeight && + screenHeight - availableHeight > availableHeight; + final height = min(neededHeight, + max(availableHeight, screenHeight - availableHeight - 55) - 55); + + entry = OverlayEntry( + builder: (context) => Positioned( + width: renderBox.size.width, + height: height, + child: CompositedTransformFollower( + link: layerLink, + targetAnchor: displayAbove ? Alignment.bottomLeft : Alignment.topLeft, + followerAnchor: + displayAbove ? Alignment.bottomLeft : Alignment.topLeft, + offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)), + showWhenUnlinked: false, + child: Scrollbar( + controller: _scrollController, + isAlwaysShown: true, + child: Material( + elevation: 8, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: items, + ), + ), + ), + ), + ), + ), + ); + + overlay.insert(entry!); + isOpen = true; + } + + ListTile buildListTile(T item) { + return ListTile( + onTap: () => handleChanged(item), + selected: item == widget.selectedItem, + title: Row( + children: [ + Expanded( + child: Text(item.toString()), + ), + ], + ), + ); + } + + void hideOverlay() { + entry?.remove(); + setState(() { + entry = null; + isOpen = false; + }); + } + + void handleChanged(T? item) { + widget.onChanged(item); + hideOverlay(); + } + + void onChangeQuery(String value) { + if (value.trim() == '' || + (widget.selectedItem != null && + value == widget.selectedItem!.toString())) { + setState(() { + suggestions = []; + }); + } else { + if (widget.applyQuery == null) { + setState(() { + suggestions = widget.items.where((item) { + String itemString = item.toString().toLowerCase(); + String valueLowercase = value.toLowerCase(); + return itemString.contains(valueLowercase); + }).toList(); + }); + } else { + setState(() { + suggestions = widget.applyQuery!(value) + .where((item) => item != widget.selectedItem) + .toList(); + }); + } + showOverlay(); + } + } + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: focusNode, + onFocusChange: (isFocused) { + if (!isFocused) { + hideOverlay(); + } + }, + child: CompositedTransformTarget( + link: layerLink, + child: TextFormField( + onChanged: onChangeQuery, + onTap: toggleOverlay, + controller: widget.controller, + decoration: InputDecoration( + labelText: widget.label, + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + widget.selectedItem != null + ? IconButton( + onPressed: () => handleChanged(null), + icon: const Icon(Icons.close), + iconSize: 20.0, + ) + : Container(), + IconButton( + onPressed: toggleOverlay, + icon: const Icon(Icons.arrow_drop_down), + ), + ], + ), + ), + ), + ), + ); + } +} 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..9b41cb1 --- /dev/null +++ b/lib/components/forms/number_form_field.dart @@ -0,0 +1,129 @@ +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 (widget.autoRoundToMultipleOfStep) { + value = value != null ? Utils.roundToMultipleOfBase(value, widget.step) : null; + } + 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/progress_indicator.dart b/lib/components/progress_indicator.dart deleted file mode 100644 index f9a69e9..0000000 --- a/lib/components/progress_indicator.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -// import 'package:flutter/widgets.dart'; - -class ViewWithProgressIndicator extends StatefulWidget { - final AsyncSnapshot snapshot; - final Widget child; - final double progressIndicatorSize; - final EdgeInsets padding; - - const ViewWithProgressIndicator( - {Key? key, - required this.snapshot, - required this.child, - this.progressIndicatorSize = 100, - this.padding = const EdgeInsets.all(0)}) - : super(key: key); - - @override - _ViewWithProgressIndicatorState createState() => - _ViewWithProgressIndicatorState(); -} - -class _ViewWithProgressIndicatorState extends State { - - - @override - Widget build(BuildContext context) { - switch (widget.snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - return Container( - alignment: Alignment.center, - padding: widget.padding, - child: Center( - child: SizedBox( - width: widget.progressIndicatorSize, - height: widget.progressIndicatorSize, - child: FutureBuilder( - future: Future.delayed(const Duration(seconds: 1)), - builder: (context, wait) { - if (wait.connectionState != ConnectionState.waiting) { - return const CircularProgressIndicator(); - } - return Container(); - } - ), - ), - ), - ); - default: - if (widget.snapshot.hasError) { - return Center( - child: Text(widget.snapshot.error.toString()), - ); - } - if (!widget.snapshot.hasData) { - return const Center( - child: Text("No data"), - ); - } else { - return widget.child; - } - } - } -} 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/config.dart b/lib/config.dart index d242e7e..094eda9 100644 --- a/lib/config.dart +++ b/lib/config.dart @@ -1,28 +1 @@ -import 'package:diameter/settings.dart'; - -const keyApplicationId = 'DFfD2aeppmqQnVmox02kUZhYOUc7vAtGfunAP7hn'; -const keyClientKey = '0ROGEVQP0Id21EMEqK05wJP3nBDuOW5DM5Cpzdt3'; -const keyParseServerUrl = 'https://parseapi.back4app.com'; - -// settings -NutritionMeasurement nutritionMeasurement = NutritionMeasurement.grams; -GlucoseMeasurement glucoseMeasurement = GlucoseMeasurement.mgPerDl; -GlucoseDisplayMode glucoseDisplayMode = GlucoseDisplayMode.bothForList; - -DateTime dummyDate = DateTime(2000); -String dateFormat = 'MM/dd/yy'; -String? longDateFormat = 'MMMM dd, yyyy'; -String timeFormat = 'HH:mm'; -String? longTimeFormat = 'HH:mm:ss'; - -bool showConfirmationDialogOnCancel = true; -bool showConfirmationDialogOnDelete = true; -bool showConfirmationDialogOnStopEvent = true; - -int lowGlucoseMgPerDl = 80; -int moderateGlucoseMgPerDl = 140; -int highGlucoseMgPerDl = 240; - -double lowGlucoseMmolPerL = 4.44; -double moderateGlucoseMmolPerL = 7.77; -double highGlucoseMmolPerDl = 13.32; +String secret = 'm4Gwehzgv18jZ5gCVUBZl5li3Z0FX2Yb'; diff --git a/lib/main.dart b/lib/main.dart index b1b4763..54b0044 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,16 @@ import 'package:diameter/components/app_theme.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/models/settings.dart'; import 'package:diameter/object_box.dart'; import 'package:diameter/screens/accuracy_detail.dart'; import 'package:diameter/screens/basal/basal_profile_detail.dart'; import 'package:diameter/screens/bolus/bolus_profile_detail.dart'; import 'package:diameter/screens/log/log.dart'; -import 'package:diameter/screens/log/log_entry.dart'; -import 'package:diameter/screens/log/log_event_detail.dart'; -import 'package:diameter/screens/log/log_event_type_detail.dart'; -import 'package:diameter/screens/log/log_event_type_list.dart'; +import 'package:diameter/screens/log/log_entry/log_entry.dart'; +import 'package:diameter/screens/log/log_event/log_event_detail.dart'; +import 'package:diameter/screens/log/log_event/log_event_list.dart'; +import 'package:diameter/screens/log/log_event/log_event_type_detail.dart'; +import 'package:diameter/screens/log/log_event/log_event_type_list.dart'; import 'package:diameter/screens/meal/meal_category_detail.dart'; import 'package:diameter/screens/meal/meal_category_list.dart'; import 'package:diameter/screens/meal/meal_detail.dart'; @@ -16,61 +19,67 @@ import 'package:diameter/screens/meal/meal_portion_type_detail.dart'; import 'package:diameter/screens/meal/meal_portion_type_list.dart'; import 'package:diameter/screens/meal/meal_source_detail.dart'; import 'package:diameter/screens/meal/meal_source_list.dart'; +import 'package:diameter/screens/recipe/recipe_detail.dart'; +import 'package:diameter/screens/recipe/recipe_list.dart'; import 'package:diameter/settings.dart'; import 'package:flutter/material.dart'; -import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; import 'package:diameter/screens/accuracy_list.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/screens/basal/basal_profiles_list.dart'; +import 'package:diameter/screens/basal/basal_profile_list.dart'; import 'package:diameter/screens/bolus/bolus_profile_list.dart'; import 'package:diameter/navigation.dart'; +import 'package:objectbox/objectbox.dart'; late ObjectBox objectBox; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - - await Parse().initialize( - keyApplicationId, - keyParseServerUrl, - clientKey: keyClientKey, - debug: true, - coreStore: await CoreStoreSharedPrefsImp.getInstance(), - ); - - Settings.loadSettingsIntoConfig(); - objectBox = await ObjectBox.create(); + + Sync.isAvailable(); + SyncClient syncClient = Sync.client( + objectBox.store, + 'ws://192.168.1.184:9999', + SyncCredentials.sharedSecretString(secret) + ); + syncClient.start(); + syncClient.requestUpdates(subscribeForFuturePushes: false); runApp( - MaterialApp( - theme: AppTheme.makeTheme(AppTheme.lightTheme), - darkTheme: AppTheme.makeTheme(AppTheme.darkTheme), - themeMode: ThemeMode.system, - initialRoute: '/', - routes: { - '/': (context) => const LogScreen(), - Routes.log: (context) => const LogScreen(), - Routes.logEntry: (context) => const LogEntryScreen(), - Routes.logEvent: (context) => const LogEventDetailScreen(), - Routes.logEventTypes: (context) => const LogEventTypeListScreen(), - Routes.logEventType: (context) => const LogEventTypeDetailScreen(), - Routes.accuracies: (context) => const AccuracyListScreen(), - Routes.accuracy: (context) => const AccuracyDetailScreen(), - Routes.meals: (context) => const MealListScreen(), - Routes.meal: (context) => const MealDetailScreen(), - Routes.mealCategories: (context) => const MealCategoryListScreen(), - Routes.mealCategory: (context) => const MealCategoryDetailScreen(), - Routes.mealPortionTypes: (context) => const MealPortionTypeListScreen(), - Routes.mealPortionType: (context) => - const MealPortionTypeDetailScreen(), - Routes.mealSources: (context) => const MealSourceListScreen(), - Routes.mealSource: (context) => const MealSourceDetailScreen(), - Routes.bolusProfiles: (context) => const BolusProfileListScreen(), - Routes.bolusProfile: (context) => const BolusProfileDetailScreen(), - Routes.basalProfiles: (context) => const BasalProfileListScreen(), - Routes.basalProfile: (context) => const BasalProfileDetailScreen(), - Routes.settings: (context) => const SettingsScreen(), - }, + GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + child: MaterialApp( + theme: AppTheme.makeTheme(AppTheme.lightTheme), + darkTheme: AppTheme.makeTheme(AppTheme.darkTheme), + themeMode: Settings.themeMode, + initialRoute: '/', + routes: { + '/': (context) => const LogScreen(), + Routes.log: (context) => const LogScreen(), + Routes.logEntry: (context) => const LogEntryScreen(), + Routes.logEvent: (context) => const LogEventDetailScreen(), + Routes.logEventTypes: (context) => const LogEventTypeListScreen(), + Routes.logEventType: (context) => const EventTypeDetailScreen(), + Routes.events: (context) => const LogEventListScreen(), + Routes.accuracies: (context) => const AccuracyListScreen(), + Routes.accuracy: (context) => const AccuracyDetailScreen(), + Routes.meals: (context) => const MealListScreen(), + Routes.meal: (context) => const MealDetailScreen(), + Routes.recipes: (context) => const RecipeListScreen(), + Routes.recipe: (context) => const RecipeDetailScreen(), + Routes.mealCategories: (context) => const MealCategoryListScreen(), + Routes.mealCategory: (context) => const MealCategoryDetailScreen(), + Routes.mealPortionTypes: (context) => + const MealPortionTypeListScreen(), + Routes.mealPortionType: (context) => + const MealPortionTypeDetailScreen(), + Routes.mealSources: (context) => const MealSourceListScreen(), + Routes.mealSource: (context) => const MealSourceDetailScreen(), + Routes.bolusProfiles: (context) => const BolusProfileListScreen(), + Routes.bolusProfile: (context) => const BolusProfileDetailScreen(), + Routes.basalProfiles: (context) => const BasalProfileListScreen(), + Routes.basalProfile: (context) => const BasalProfileDetailScreen(), + Routes.settings: (context) => const SettingsScreen(), + }, + ), ), ); } diff --git a/lib/models/accuracy.dart b/lib/models/accuracy.dart index 5391c84..680e377 100644 --- a/lib/models/accuracy.dart +++ b/lib/models/accuracy.dart @@ -1,19 +1,25 @@ import 'package:diameter/main.dart'; -import 'package:diameter/objectbox.g.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show Accuracy_; -@Entity() +@Entity(uid: 291512798403320400) +@Sync() class Accuracy { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String value; bool forCarbsRatio; bool forPortionSize; int? confidenceRating; String? notes; + // constructor Accuracy({ this.id = 0, + this.deleted = false, this.value = '', this.forCarbsRatio = false, this.forPortionSize = false, @@ -21,25 +27,51 @@ class Accuracy { this.notes, }); + // methods static Accuracy? get(int id) => box.get(id); + static void put(Accuracy accuracy) => box.put(accuracy); + static List getAll() { - QueryBuilder all = box.query()..order(Accuracy_.confidenceRating); + QueryBuilder all = box.query(Accuracy_.deleted.equals(false)) + ..order(Accuracy_.confidenceRating); return all.build().find(); } - static void put(Accuracy accuracy) => box.put(accuracy); - static void remove(int id) => box.remove(id); + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } static List getAllForPortionSize() { - QueryBuilder allForPortionSize = box - .query(Accuracy_.forPortionSize.equals(true)) + QueryBuilder allForPortionSize = box.query( + Accuracy_.forPortionSize.equals(true) & Accuracy_.deleted.equals(false)) ..order(Accuracy_.confidenceRating); return allForPortionSize.build().find(); } static List getAllForCarbsRatio() { - QueryBuilder allForCarbsRatio = box - .query(Accuracy_.forCarbsRatio.equals(true)) + QueryBuilder allForCarbsRatio = box.query( + Accuracy_.forCarbsRatio.equals(true) & Accuracy_.deleted.equals(false)) ..order(Accuracy_.confidenceRating); return allForCarbsRatio.build().find(); } + + static void reorder(Accuracy accuracy, int? newPosition) { + QueryBuilder all = box.query(Accuracy_.deleted.equals(false).and(Accuracy_.id.notEquals(accuracy.id))) + ..order(Accuracy_.confidenceRating); + List accuracies = all.build().find(); + newPosition == null || newPosition >= accuracies.length ? accuracies.add(accuracy) : accuracies.insert(newPosition, accuracy); + box.putMany(accuracies.map((item) { + item.confidenceRating = accuracies.indexOf(item); + return item; + }).toList()); + } + + @override + String toString() { + return value; + } } diff --git a/lib/models/basal.dart b/lib/models/basal.dart index cb547be..9ccd952 100644 --- a/lib/models/basal.dart +++ b/lib/models/basal.dart @@ -1,37 +1,75 @@ import 'package:diameter/main.dart'; import 'package:diameter/models/basal_profile.dart'; -import 'package:diameter/objectbox.g.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show Basal_, BasalProfile_; -@Entity() +@Entity(uid: 1467758525778521891) +@Sync() class Basal { static final Box box = objectBox.store.box(); - - int id; + // properties + int id; + bool deleted; @Property(type: PropertyType.date) DateTime startTime; - @Property(type: PropertyType.date) DateTime endTime; - double units; - + + // relations final basalProfile = ToOne(); + // constructor Basal({ this.id = 0, + this.deleted = false, required this.startTime, required this.endTime, this.units = 0, }); + // methods static Basal? get(int id) => box.get(id); static void put(Basal basal) => box.put(basal); - static void remove(int id) => box.remove(id); + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } static List getAllForProfile(int id) { - QueryBuilder builder = box.query()..order(Basal_.startTime); + QueryBuilder builder = box.query(Basal_.deleted.equals(false)) + ..order(Basal_.startTime); builder.link(Basal_.basalProfile, BasalProfile_.id.equals(id)); return builder.build().find(); } + + static double getDailyTotalForProfile(int id) { + double sum = 0.0; + + QueryBuilder builder = box.query(Basal_.deleted.equals(false)); + builder.link(Basal_.basalProfile, BasalProfile_.id.equals(id)); + + List basalRates = builder.build().find(); + for (Basal basal in basalRates) { + double rateDuration = + basal.endTime.difference(basal.startTime).inMinutes / 60; + if (rateDuration < 0) { + rateDuration += 24; + } + sum += basal.units * rateDuration; + } + + return sum; + } + + @override + String toString() { + return DateTimeUtils.displayTime(startTime); + } } diff --git a/lib/models/basal_profile.dart b/lib/models/basal_profile.dart index ceb9bff..54a3df3 100644 --- a/lib/models/basal_profile.dart +++ b/lib/models/basal_profile.dart @@ -1,37 +1,87 @@ import 'package:diameter/main.dart'; -import 'package:diameter/objectbox.g.dart'; +import 'package:diameter/models/log_event.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show BasalProfile_; -@Entity() +@Entity(uid: 3613736032926903785) +@Sync() class BasalProfile { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String name; bool active; String? notes; + // constructor BasalProfile({ this.id = 0, + this.deleted = false, this.name = '', this.active = false, this.notes, }); + // methods static BasalProfile? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(BasalProfile basalProfile) => box.put(basalProfile); - static void remove(int id) => box.remove(id); + + static List getAll() { + QueryBuilder all = box.query(BasalProfile_.deleted.equals(false)) + ..order(BasalProfile_.name); + return all.build().find(); + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } static int activeCount() { - Query query = - box.query(BasalProfile_.active.equals(true)).build(); + Query query = box + .query(BasalProfile_.active.equals(true) & BasalProfile_.deleted.equals(false)).build(); return query.find().length; } static void setAllInactive() { - box.putMany(box.getAll().map((element) { - element.active = false; - return element; + box.putMany(box.getAll().map((item) { + item.active = false; + return item; }).toList()); } + + static BasalProfile? getActive(DateTime? dateTime) { + if (dateTime != null) { + List activeEvents = LogEvent.getAllActiveForTime(dateTime) + .where((event) => event.basalProfile.target != null).toList(); + if (activeEvents.length > 1) { + final now = DateTime.now(); + activeEvents = + activeEvents.where((item) => !activeEvents.any((other) => + item.time.isBefore(other.time) || (item.endTime ?? now).isAfter(other.endTime ?? now) + )).toList(); + } + if (activeEvents.length == 1) { + return activeEvents.single.basalProfile.target; + } + } + + Query query = box + .query(BasalProfile_.active + .equals(true) + .and(BasalProfile_.deleted.equals(false))) + .build(); + final result = query.find(); + return result.length != 1 ? null : result.single; + } + + @override + String toString() { + return name; + } } diff --git a/lib/models/bolus.dart b/lib/models/bolus.dart index 975f461..ed043a0 100644 --- a/lib/models/bolus.dart +++ b/lib/models/bolus.dart @@ -1,12 +1,18 @@ import 'package:diameter/main.dart'; import 'package:diameter/models/bolus_profile.dart'; -import 'package:diameter/objectbox.g.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show Bolus_, BolusProfile_; -@Entity() +@Entity(uid: 3417770529060202389) +@Sync() class Bolus { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; @Property(type: PropertyType.date) DateTime startTime; @Property(type: PropertyType.date) @@ -16,10 +22,13 @@ class Bolus { int? mgPerDl; double? mmolPerL; + // relations final bolusProfile = ToOne(); + // constructor Bolus({ this.id = 0, + this.deleted = false, required this.startTime, required this.endTime, this.units = 0, @@ -28,13 +37,48 @@ class Bolus { this.mmolPerL, }); + // methods static Bolus? get(int id) => box.get(id); static void put(Bolus bolus) => box.put(bolus); - static void remove(int id) => box.remove(id); static List getAllForProfile(int id) { - QueryBuilder builder = box.query()..order(Bolus_.startTime); + QueryBuilder builder = box.query(Bolus_.deleted.equals(false)) + ..order(Bolus_.startTime); builder.link(Bolus_.bolusProfile, BolusProfile_.id.equals(id)); return builder.build().find(); } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + static Bolus? getRateForTime(DateTime? dateTime) { + if (dateTime != null) { + final bolusProfile = BolusProfile.getActive(dateTime); + final time = DateTimeUtils.convertTimeOfDayToDateTime( + TimeOfDay.fromDateTime(dateTime)); + if (bolusProfile != null) { + final rates = Bolus.getAllForProfile(bolusProfile.id); + final result = rates.where((rate) { + DateTime endTime = rate.endTime == dummyDate + ? rate.endTime.add(const Duration(days: 1)) + : rate.endTime; + return (time.isAfter(rate.startTime) || + time.isAtSameMomentAs(rate.startTime)) && + time.isBefore(endTime); + }); + return result.length != 1 ? null : result.single; + } + } + return null; + } + + @override + String toString() { + return DateTimeUtils.displayTime(startTime); + } } diff --git a/lib/models/bolus_profile.dart b/lib/models/bolus_profile.dart index 36966b5..3e0408f 100644 --- a/lib/models/bolus_profile.dart +++ b/lib/models/bolus_profile.dart @@ -1,30 +1,53 @@ import 'package:diameter/main.dart'; -import 'package:diameter/objectbox.g.dart'; +import 'package:diameter/models/log_event.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show BolusProfile_; -@Entity() +@Entity(uid: 8812452529027052317) +@Sync() class BolusProfile { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String name; bool active; String? notes; + // constructor BolusProfile({ this.id = 0, + this.deleted = false, this.name = '', this.active = false, this.notes, }); + // methods static BolusProfile? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(BolusProfile bolusProfile) => box.put(bolusProfile); - static void remove(int id) => box.remove(id); + + static List getAll() { + QueryBuilder all = + box.query(BolusProfile_.deleted.equals(false))..order(BolusProfile_.name); + return all.build().find(); + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } static int activeCount() { - Query query = - box.query(BolusProfile_.active.equals(true)).build(); + Query query = box + .query(BolusProfile_.active + .equals(true) + .and(BolusProfile_.deleted.equals(false))) + .build(); return query.find().length; } @@ -34,4 +57,34 @@ class BolusProfile { return element; }).toList()); } + + static BolusProfile? getActive(DateTime? dateTime) { + if (dateTime != null) { + List activeEvents = LogEvent.getAllActiveForTime(dateTime) + .where((event) => event.bolusProfile.target != null).toList(); + if (activeEvents.length > 1) { + final now = DateTime.now(); + activeEvents = + activeEvents.where((item) => !activeEvents.any((other) => + item.time.isBefore(other.time) || (item.endTime ?? now).isAfter(other.endTime ?? now) + )).toList(); + } + if (activeEvents.length == 1) { + return activeEvents.single.bolusProfile.target; + } + } + + Query query = box + .query(BolusProfile_.active + .equals(true) + .and(BolusProfile_.deleted.equals(false))) + .build(); + final result = query.find(); + return result.length != 1 ? null : result.single; + } + + @override + String toString() { + return name; + } } diff --git a/lib/models/glucose_target.dart b/lib/models/glucose_target.dart new file mode 100644 index 0000000..890df51 --- /dev/null +++ b/lib/models/glucose_target.dart @@ -0,0 +1,122 @@ +import 'package:diameter/main.dart'; +import 'package:diameter/models/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show GlucoseTarget_; + +@Entity(uid: 5041265995704044399) +@Sync() +class GlucoseTarget { + static final Box box = objectBox.store.box(); + + // properties + int id; + bool deleted; + int fromMgPerDL; + int toMgPerDl; + double fromMmolPerL; + double toMmolPerL; + int color; + + // constructor + GlucoseTarget({ + this.id = 0, + this.deleted = false, + required this.fromMgPerDL, + required this.toMgPerDl, + required this.fromMmolPerL, + required this.toMmolPerL, + required this.color, + }); + + // methods + static GlucoseTarget? get(int id) => box.get(id); + + // methods + static List getAll() { + if (box.getAll().isEmpty) { + reset(); + } + return box.getAll(); + } + + static Color getColorForGlucose({int mgPerDl = 0, double mmolPerL = 0}) { + if (box.getAll().isEmpty) { + reset(); + } + + Condition condition; + if (mgPerDl > 0 && + (mmolPerL == 0 || + Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl)) { + condition = GlucoseTarget_.fromMgPerDL.lessOrEqual(mgPerDl).and(GlucoseTarget_.toMgPerDl.greaterOrEqual(mgPerDl)); + } else if (mmolPerL > 0 && + (mgPerDl == 0 || + Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL)) { + condition = GlucoseTarget_.fromMmolPerL.lessOrEqual(mmolPerL).and(GlucoseTarget_.toMmolPerL.greaterOrEqual(mmolPerL)); + } else { + return Colors.black; + } + + List result = box + .query(GlucoseTarget_.deleted.equals(false) & condition) + .build() + .find(); + + if (result.length != 1) { + return Colors.black; + } + return Color(result.single.color); + } + + static void put(GlucoseTarget glucoseTarget) => box.put(glucoseTarget); + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + static void reset() { + box.removeAll(); + List defaultTargets = [ + GlucoseTarget( + fromMgPerDL: 0, + toMgPerDl: 69, + fromMmolPerL: 0, + toMmolPerL: 3.83, + color: Colors.red.value, + ), + GlucoseTarget( + fromMgPerDL: 70, + toMgPerDl: 99, + fromMmolPerL: 3.84, + toMmolPerL: 5.48, + color: Colors.orange.value, + ), + GlucoseTarget( + fromMgPerDL: 100, + toMgPerDl: 140, + fromMmolPerL: 5.49, + toMmolPerL: 7.77, + color: Colors.green.value, + ), + GlucoseTarget( + fromMgPerDL: 141, + toMgPerDl: 240, + fromMmolPerL: 7.78, + toMmolPerL: 13.32, + color: Colors.orange.value, + ), + GlucoseTarget( + fromMgPerDL: 241, + toMgPerDl: 999, + fromMmolPerL: 13.33, + toMmolPerL: 55.44, + color: Colors.deepOrange.value, + ), + ]; + box.putMany(defaultTargets); + } +} diff --git a/lib/models/ingredient.dart b/lib/models/ingredient.dart new file mode 100644 index 0000000..486a178 --- /dev/null +++ b/lib/models/ingredient.dart @@ -0,0 +1,79 @@ +import 'package:diameter/main.dart'; +import 'package:diameter/models/meal.dart'; +import 'package:diameter/models/recipe.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show Ingredient_, Recipe_; + +@Entity(uid: 6950311793136068892) +@Sync() +class Ingredient { + static final Box box = objectBox.store.box(); + + // properties + int id; + bool deleted; + double amount; + + // relations + final recipe = ToOne(); + final ingredient = ToOne(); + + // constructor + Ingredient({ + this.id = 0, + this.deleted = false, + required this.amount, + }); + + // methods + static Ingredient? get(int id) => box.get(id); + static void put(Ingredient ingredient) => box.put(ingredient); + static void putMany(List ingredients) => box.putMany(ingredients); + + static List getAllForRecipe(int id) { + QueryBuilder builder = + box.query(Ingredient_.deleted.equals(false)); + builder.link(Ingredient_.recipe, Recipe_.id.equals(id)); + return builder.build().find(); + } + + static double? getCarbsRatioForRecipe(int id) { + double carbsSum = 0.0; + double totalWeight = 0.0; + + List ingredients = getAllForRecipe(id); + + for (Ingredient ingredient in ingredients) { + if ((ingredient.ingredient.target?.carbsRatio ?? 0) <= 0) { + return null; + } + totalWeight += ingredient.amount; + carbsSum += + Utils.calculateCarbs(ingredient.ingredient.target!.carbsRatio!, ingredient.amount); + } + return totalWeight > 0 + ? Utils.calculateCarbsRatio(carbsSum, totalWeight) + : null; + } + + static double? getTotalWeightForRecipe(int id) { + double totalWeight = 0.0; + + List ingredients = getAllForRecipe(id); + + for (Ingredient ingredient in ingredients) { + if (ingredient.ingredient.target?.carbsRatio == null) { + return null; + } + totalWeight += ingredient.amount; + } + + return totalWeight; + } + + @override + String toString() { + return ingredient.target?.value ?? ''; + } +} diff --git a/lib/models/log_bolus.dart b/lib/models/log_bolus.dart new file mode 100644 index 0000000..1b45234 --- /dev/null +++ b/lib/models/log_bolus.dart @@ -0,0 +1,93 @@ +import 'package:diameter/main.dart'; +import 'package:diameter/models/bolus.dart'; +import 'package:diameter/models/log_entry.dart'; +import 'package:diameter/models/log_meal.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show LogBolus_, LogEntry_, LogMeal_; + +@Entity(uid: 8033487006694871160) +@Sync() +class LogBolus { + static final Box box = objectBox.store.box(); + + // properties + int id; + bool deleted; + double units; + double? carbs; + int? delay; + int? mgPerDlCurrent; + int? mgPerDlTarget; + int? mgPerDlCorrection; + double? mmolPerLCurrent; + double? mmolPerLTarget; + double? mmolPerLCorrection; + bool setManually; + String? notes; + + // relations + final logEntry = ToOne(); + final rate = ToOne(); + final meal = ToOne(); + + // constructor + LogBolus({ + this.id = 0, + this.deleted = false, + this.units = 0, + this.carbs, + this.delay, + this.mgPerDlCurrent, + this.mgPerDlTarget, + this.mgPerDlCorrection, + this.mmolPerLCurrent, + this.mmolPerLTarget, + this.mmolPerLCorrection, + this.setManually = false, + this.notes, + }); + + // methods + static LogBolus? get(int id) => box.get(id); + static void put(LogBolus logBolus) => box.put(logBolus); + + static List getAllForEntry(int id) { + QueryBuilder builder = box.query(LogBolus_.deleted.equals(false)); + builder.link(LogBolus_.logEntry, LogEntry_.id.equals(id)); + return builder.build().find(); + } + + static double getTotalBolusForEntry(int id) { + QueryBuilder builder = box.query(LogBolus_.deleted.equals(false)); + builder.link(LogBolus_.logEntry, LogEntry_.id.equals(id)); + return builder.build().property(LogBolus_.units).sum(); + } + + static bool glucoseBolusForEntryExists(int id) { + QueryBuilder builder = box.query(LogBolus_.deleted + .equals(false) + .and(LogBolus_.mgPerDlCorrection.notNull())); + builder.link(LogBolus_.meal, LogMeal_.id.equals(id)); + return builder.build().find().isNotEmpty; + } + + static bool bolusForMealExists(int id) { + QueryBuilder builder = box.query(LogBolus_.deleted + .equals(false)); + builder.link(LogBolus_.meal, LogMeal_.id.equals(id)); + return builder.build().find().isNotEmpty; + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + @override + String toString() { + return units.toString(); + } +} diff --git a/lib/models/log_entry.dart b/lib/models/log_entry.dart index eaf4ac2..3891aeb 100644 --- a/lib/models/log_entry.dart +++ b/lib/models/log_entry.dart @@ -1,54 +1,64 @@ import 'package:diameter/main.dart'; -import 'package:diameter/models/log_event.dart'; -import 'package:diameter/models/log_meal.dart'; -import 'package:diameter/objectbox.g.dart'; +import 'package:diameter/models/log_bolus.dart'; +import 'package:diameter/models/settings.dart'; +import 'package:diameter/utils/date_time_utils.dart'; import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show LogEntry_; -@Entity() +@Entity(uid: 752131069307970560) +@Sync() class LogEntry { static final Box box = objectBox.store.box(); + // properties int id; - + bool deleted; @Property(type: PropertyType.date) DateTime time; - int? mgPerDl; double? mmolPerL; - double? bolusGlucose; - int? delayedBolusDuration; - double? delayedBolusRate; + double? glucoseTrend; String? notes; - @Backlink('logEntry') - final events = ToMany(); - - @Backlink('endLogEntry') - final endedEvents = ToMany(); - - @Backlink('logEntry') - final meals = ToMany(); - + // constructor LogEntry({ this.id = 0, + this.deleted = false, required this.time, this.mgPerDl, this.mmolPerL, - this.bolusGlucose, - this.delayedBolusDuration, - this.delayedBolusRate, + this.glucoseTrend, this.notes, }); - static LogEntry? get(int id) => box.get(id); + // methods + static LogEntry? get(int id) => id == 0 ? null : box.get(id); static List getAll() => box.getAll(); static void put(LogEntry logEntry) => box.put(logEntry); - static void remove(int id) => box.remove(id); + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + static bool hasUncorrectedGlucose(int id) { + final entry = box.get(id); + if (((entry?.mgPerDl ?? 0) > Settings.targetMgPerDl || + (entry?.mmolPerL ?? 0) > Settings.targetMmolPerL)) { + return !LogBolus.glucoseBolusForEntryExists(id); + } + return false; + } static Map> getDailyEntryMap() { Map> dateMap = >{}; - QueryBuilder allByDate = box.query()..order(LogEntry_.time, flags: Order.descending); + QueryBuilder allByDate = box + .query(LogEntry_.deleted.equals(false)) + ..order(LogEntry_.time, flags: Order.descending); List entries = allByDate.build().find(); DateTime? date; @@ -59,4 +69,19 @@ class LogEntry { return dateMap; } + + static List getAllForDate(DateTime date) { + DateTime startOfDay = DateTime(date.year, date.month, date.day); + DateTime endOfDay = startOfDay.add(const Duration(days: 1)); + QueryBuilder builder = box.query(LogEntry_.deleted.equals(false)) + ..order(LogEntry_.time, flags: Order.descending); + return builder.build().find().where((entry) { + return (entry.time.compareTo(startOfDay) >= 0 && entry.time.isBefore(endOfDay)); + }).toList(); + } + + @override + String toString() { + return DateTimeUtils.displayDateTime(time); + } } diff --git a/lib/models/log_event.dart b/lib/models/log_event.dart index 4db1bec..d35d5f3 100644 --- a/lib/models/log_event.dart +++ b/lib/models/log_event.dart @@ -1,43 +1,195 @@ import 'package:diameter/main.dart'; -import 'package:diameter/models/log_entry.dart'; +import 'package:diameter/models/basal_profile.dart'; +import 'package:diameter/models/bolus_profile.dart'; import 'package:diameter/models/log_event_type.dart'; -import 'package:diameter/objectbox.g.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show LogEvent_, LogEventType_; -@Entity() +@Entity(uid: 4303325892753185970) +@Sync() class LogEvent { static final Box box = objectBox.store.box(); + // properties int id; - + bool deleted; @Property(type: PropertyType.date) DateTime time; - @Property(type: PropertyType.date) DateTime? endTime; - bool hasEndTime; + int? reminderDuration; String? notes; - final logEntry = ToOne(); - final endLogEntry = ToOne(); - final eventType = ToOne(); + @Transient() + String? title; + @Transient() + bool isEndEvent = false; + // relations + final eventType = ToOne(); + final bolusProfile = ToOne(); + final basalProfile = ToOne(); + + // constructor LogEvent({ this.id = 0, + this.deleted = false, required this.time, this.endTime, this.hasEndTime = false, + this.reminderDuration, this.notes, }); + // methods static LogEvent? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(LogEvent logEvent) => box.put(logEvent); - static void remove(int id) => box.remove(id); + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } static List getAllOngoing() { - QueryBuilder query = - box.query(LogEvent_.hasEndTime.equals(true) & LogEvent_.endTime.isNull())..order(LogEvent_.time); + QueryBuilder query = box.query(LogEvent_.hasEndTime.equals(true) & + LogEvent_.endTime.isNull() & + LogEvent_.deleted.equals(false)) + ..order(LogEvent_.time); return query.build().find(); } + + static List getAllActiveForTime(DateTime? dateTime) { + if (dateTime != null) { + QueryBuilder builder = box.query( + LogEvent_.hasEndTime.equals(true) & LogEvent_.deleted.equals(false)) + ..order(LogEvent_.time, flags: Order.descending); + final eventsWithEndTime = builder.build().find(); + return eventsWithEndTime.where((event) { + return (!dateTime.isBefore(event.time)) && + !dateTime.isAfter(event.endTime ?? DateTime.now()); + }).toList(); + } + return []; + } + + static bool eventTypeExistsForTime(int id, DateTime? dateTime) { + QueryBuilder builder = box.query( + LogEvent_.hasEndTime.equals(true) & LogEvent_.deleted.equals(false)) + ..order(LogEvent_.time, flags: Order.descending); + builder.link(LogEvent_.eventType, LogEventType_.id.equals(id)); + final eventsWithEndTime = builder.build().find(); + if (dateTime != null) { + return eventsWithEndTime.where((event) { + return (!dateTime.isBefore(event.time)) && + !dateTime.isAfter(event.endTime ?? DateTime.now()); + }).isNotEmpty; + } + return eventsWithEndTime.isNotEmpty; + } + + static Map> getDailyEntryMap() { + Map> dateMap = >{}; + Map> sortedDateMap = >{}; + + QueryBuilder allByDate = box + .query(LogEvent_.deleted.equals(false)) + ..order(LogEvent_.time, flags: Order.descending); + List events = allByDate.build().find(); + + DateTime? date; + + for (LogEvent event in events) { + date = DateTime.utc(event.time.year, event.time.month, event.time.day); + LogEvent startEvent = event; + startEvent.title = + '${event.toString()} ${event.hasEndTime ? '(Start)' : ''}'; + dateMap.putIfAbsent(date, () => []).add(startEvent); + } + + QueryBuilder allByEndDate = box + .query(LogEvent_.deleted.equals(false).and(LogEvent_.endTime.notNull())) + ..order(LogEvent_.endTime, flags: Order.descending); + List endEvents = allByEndDate.build().find(); + + for (LogEvent event in endEvents) { + date = DateTime.utc( + event.endTime!.year, event.endTime!.month, event.endTime!.day); + LogEvent endEvent = event; + endEvent.isEndEvent = true; + endEvent.title = '${event.toString()} (End)'; + dateMap.putIfAbsent(date, () => []).add(endEvent); + } + + final dates = dateMap.keys.toList(); + dates.sort(); + for (DateTime date in dates.reversed) { + dateMap[date]!.sort((LogEvent a, LogEvent b) { + final dateA = a.isEndEvent ? a.endTime : a.time; + final dateB = b.isEndEvent ? b.endTime : b.time; + return -(dateA!.compareTo(dateB!)); + }); + sortedDateMap + .putIfAbsent(date, () => []) + .addAll(dateMap[date]!); + } + + return sortedDateMap; + } + + static List getAllForDate(DateTime date) { + DateTime startOfDay = DateTime(date.year, date.month, date.day); + DateTime endOfDay = startOfDay.add(const Duration(days: 1)); + + List events = []; + + QueryBuilder allByDate = box + .query(LogEvent_.deleted.equals(false)) + ..order(LogEvent_.time, flags: Order.descending); + List startEvents = allByDate.build().find().where((event) { + return (event.time.compareTo(startOfDay) >= 0 && + event.time.isBefore(endOfDay)); + }).toList(); + + for (LogEvent event in startEvents) { + date = DateTime.utc(event.time.year, event.time.month, event.time.day); + LogEvent startEvent = event; + startEvent.title = + '${event.toString()} ${event.hasEndTime ? '(Start)' : ''}'; + events.add(startEvent); + } + + QueryBuilder allByEndDate = box + .query(LogEvent_.deleted.equals(false).and(LogEvent_.endTime.notNull())) + ..order(LogEvent_.endTime, flags: Order.descending); + List endEvents = allByEndDate.build().find().where((event) { + return (event.endTime!.compareTo(startOfDay) >= 0 && + event.endTime!.isBefore(endOfDay)); + }).toList(); + + for (LogEvent event in endEvents) { + date = DateTime.utc( + event.endTime!.year, event.endTime!.month, event.endTime!.day); + LogEvent endEvent = event; + endEvent.isEndEvent = true; + endEvent.title = '${event.toString()} (End)'; + events.add(endEvent); + } + + events.sort((LogEvent a, LogEvent b) { + final dateA = a.isEndEvent ? a.endTime : a.time; + final dateB = b.isEndEvent ? b.endTime : b.time; + return -(dateA!.compareTo(dateB!)); + }); + + return events; + } + + @override + String toString() { + return eventType.target?.value ?? ''; + } } diff --git a/lib/models/log_event_type.dart b/lib/models/log_event_type.dart index 857813d..e1683fb 100644 --- a/lib/models/log_event_type.dart +++ b/lib/models/log_event_type.dart @@ -1,26 +1,55 @@ import 'package:diameter/main.dart'; +import 'package:diameter/models/basal_profile.dart'; +import 'package:diameter/models/bolus_profile.dart'; import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show LogEventType_; -@Entity() +@Entity(uid: 8362795406595606110) +@Sync() class LogEventType { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String value; bool hasEndTime; int? defaultReminderDuration; String? notes; + // constructor LogEventType({ this.id = 0, + this.deleted = false, this.value = '', this.hasEndTime = false, this.defaultReminderDuration, this.notes, }); + // relations + final bolusProfile = ToOne(); + final basalProfile = ToOne(); + + // methods static LogEventType? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(LogEventType logEventType) => box.put(logEventType); - static void remove(int id) => box.remove(id); + + static List getAll() { + QueryBuilder builder = box.query(LogEventType_.deleted.equals(false))..order(LogEventType_.value); + return builder.build().find(); + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + @override + String toString() { + return value; + } } diff --git a/lib/models/log_meal.dart b/lib/models/log_meal.dart index e1fea66..8c9c471 100644 --- a/lib/models/log_meal.dart +++ b/lib/models/log_meal.dart @@ -1,4 +1,5 @@ import 'package:diameter/main.dart'; +import 'package:diameter/models/log_bolus.dart'; import 'package:diameter/models/log_entry.dart'; import 'package:diameter/models/meal.dart'; import 'package:diameter/models/meal_category.dart'; @@ -6,21 +7,25 @@ import 'package:diameter/models/meal_portion_type.dart'; import 'package:diameter/models/meal_source.dart'; import 'package:diameter/models/accuracy.dart'; import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show LogMeal_, LogEntry_; -@Entity() +@Entity(uid: 411177866700467286) +@Sync() class LogMeal { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String value; double? carbsRatio; double? portionSize; - double? carbsPerPortion; - double? bolus; - int? delayedBolusDuration; - double? delayedBolusRate; + double? totalCarbs; String? notes; + double? bolus; + double amount; + // relations final logEntry = ToOne(); final meal = ToOne(); final mealSource = ToOne(); @@ -29,20 +34,56 @@ class LogMeal { final portionSizeAccuracy = ToOne(); final carbsRatioAccuracy = ToOne(); + // constructor LogMeal({ this.id = 0, + this.deleted = false, this.value = '', + this.amount = 1, this.carbsRatio, this.portionSize, - this.carbsPerPortion, - this.bolus, - this.delayedBolusDuration, - this.delayedBolusRate, + this.totalCarbs, this.notes, }); + // methods static LogMeal? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(LogMeal logMeal) => box.put(logMeal); - static void remove(int id) => box.remove(id); + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + static List getAllForEntry(int id) { + QueryBuilder builder = box.query(LogMeal_.deleted.equals(false)); + builder.link(LogMeal_.logEntry, LogEntry_.id.equals(id)); + return builder.build().find(); + } + + static List getRecentWithoutBolus(int id) { + final dateTime = LogEntry.get(id)?.time ?? DateTime.now(); + QueryBuilder builder = box.query(LogMeal_.deleted.equals(false)); + builder.link(LogMeal_.logEntry); + List results = builder.build().find(); + results.retainWhere((logMeal) { + final entryTime = logMeal.logEntry.target!.time; + return entryTime.isAfter(dateTime.subtract(const Duration(hours: 12))) && + entryTime.isBefore(dateTime.add(const Duration(hours: 12))) && !LogBolus.bolusForMealExists(logMeal.id); + }); + return results; + } + + static double getTotalCarbsForEntry(int id) { + QueryBuilder builder = box.query(LogMeal_.deleted.equals(false)); + builder.link(LogMeal_.logEntry, LogEntry_.id.equals(id)); + return builder.build().property(LogMeal_.totalCarbs).sum(); + } + + @override + String toString() { + return value; + } } diff --git a/lib/models/meal.dart b/lib/models/meal.dart index eb763e2..c168dc0 100644 --- a/lib/models/meal.dart +++ b/lib/models/meal.dart @@ -3,43 +3,66 @@ import 'package:diameter/models/accuracy.dart'; import 'package:diameter/models/meal_category.dart'; import 'package:diameter/models/meal_portion_type.dart'; import 'package:diameter/models/meal_source.dart'; +import 'package:diameter/objectbox.g.dart' show Meal_; import 'package:objectbox/objectbox.dart'; enum PortionCarbsParameter { carbsRatio, portionSize, carbsPerPortion } -@Entity() +@Entity(uid: 382130101578692012) +@Sync() class Meal { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String value; double? carbsRatio; double? portionSize; double? carbsPerPortion; int? delayedBolusDuration; - double? delayedBolusRate; + double? delayedBolusPercentage; String? notes; - + // relations final mealSource = ToOne(); final mealCategory = ToOne(); final mealPortionType = ToOne(); final portionSizeAccuracy = ToOne(); final carbsRatioAccuracy = ToOne(); + // constructor Meal({ this.id = 0, + this.deleted = false, this.value = '', this.carbsRatio, this.portionSize, this.carbsPerPortion, this.delayedBolusDuration, - this.delayedBolusRate, + this.delayedBolusPercentage, this.notes, }); + // methods static Meal? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(Meal meal) => box.put(meal); - static void remove(int id) => box.remove(id); + + static List getAll() { + QueryBuilder builder = box.query(Meal_.deleted.equals(false))..order(Meal_.value); + return builder.build().find(); + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + @override + String toString() { + return value; + } } diff --git a/lib/models/meal_category.dart b/lib/models/meal_category.dart index 298dac0..80a44a4 100644 --- a/lib/models/meal_category.dart +++ b/lib/models/meal_category.dart @@ -1,22 +1,45 @@ import 'package:diameter/main.dart'; import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show MealCategory_; -@Entity() +@Entity(uid: 3158200688796904913) +@Sync() class MealCategory { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String value; String? notes; + // constructor MealCategory({ this.id = 0, + this.deleted = false, this.value = '', this.notes, }); + // methods static MealCategory? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(MealCategory mealCategory) => box.put(mealCategory); - static void remove(int id) => box.remove(id); + + static List getAll() { + QueryBuilder builder = box.query(MealCategory_.deleted.equals(false))..order(MealCategory_.value); + return builder.build().find(); + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + @override + String toString() { + return value; + } } diff --git a/lib/models/meal_portion_type.dart b/lib/models/meal_portion_type.dart index 43d8734..f337b5b 100644 --- a/lib/models/meal_portion_type.dart +++ b/lib/models/meal_portion_type.dart @@ -1,22 +1,45 @@ import 'package:diameter/main.dart'; import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show MealPortionType_; -@Entity() +@Entity(uid: 2111511899235985637) +@Sync() class MealPortionType { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String value; String? notes; + // constructor MealPortionType({ this.id = 0, + this.deleted = false, this.value = '', this.notes, }); + // methods static MealPortionType? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(MealPortionType mealPortionType) => box.put(mealPortionType); - static void remove(int id) => box.remove(id); + + static List getAll() { + QueryBuilder builder = box.query(MealPortionType_.deleted.equals(false))..order(MealPortionType_.value); + return builder.build().find(); + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + @override + String toString() { + return value; + } } diff --git a/lib/models/meal_source.dart b/lib/models/meal_source.dart index 03c1801..df007b0 100644 --- a/lib/models/meal_source.dart +++ b/lib/models/meal_source.dart @@ -3,28 +3,52 @@ import 'package:diameter/models/accuracy.dart'; import 'package:diameter/models/meal_category.dart'; import 'package:diameter/models/meal_portion_type.dart'; import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show MealSource_; -@Entity() +@Entity(uid: 1283034494527412242) +@Sync() class MealSource { static final Box box = objectBox.store.box(); + // properties int id; + bool deleted; String value; String? notes; + // relations final defaultMealCategory = ToOne(); final defaultMealPortionType = ToOne(); final defaultCarbsRatioAccuracy = ToOne(); final defaultPortionSizeAccuracy = ToOne(); + // constructor MealSource({ this.id = 0, + this.deleted = false, this.value = '', this.notes, }); + // methods static MealSource? get(int id) => box.get(id); - static List getAll() => box.getAll(); static void put(MealSource mealSource) => box.put(mealSource); - static void remove(int id) => box.remove(id); + + static List getAll() { + QueryBuilder builder = box.query(MealSource_.deleted.equals(false))..order(MealSource_.value); + return builder.build().find(); + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + @override + String toString() { + return value; + } } diff --git a/lib/models/recipe.dart b/lib/models/recipe.dart new file mode 100644 index 0000000..24b8bc6 --- /dev/null +++ b/lib/models/recipe.dart @@ -0,0 +1,66 @@ +import 'package:diameter/main.dart'; +import 'package:diameter/models/ingredient.dart'; +import 'package:diameter/models/meal.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:objectbox/objectbox.dart'; +import 'package:diameter/objectbox.g.dart' show Recipe_; + +@Entity(uid: 6497942314956341514) +@Sync() +class Recipe { + static final Box box = objectBox.store.box(); + + // properties + int id; + bool deleted; + String name; + double? servings; + String? notes; + + // relations + final portion = ToOne(); + + // constructor + Recipe({ + this.id = 0, + this.deleted = false, + this.name = '', + this.servings, + this.notes, + }); + + // methods + static Recipe? get(int id) => box.get(id); + static void put(Recipe recipe) => box.put(recipe); + + static List getAll() { + QueryBuilder builder = box.query(Recipe_.deleted.equals(false)) + ..order(Recipe_.name); + return builder.build().find(); + } + + static double? getCarbsPerPortion(int id) { + final servings = Recipe.get(id)?.servings; + final totalWeight = Ingredient.getTotalWeightForRecipe(id); + final carbsRatio = Ingredient.getCarbsRatioForRecipe(id); + + if (servings != null && totalWeight != null && carbsRatio != null) { + final portionSize = totalWeight / servings; + return Utils.calculateCarbs(carbsRatio, portionSize); + } + return null; + } + + static void remove(int id) { + final item = box.get(id); + if (item != null) { + item.deleted = true; + box.put(item); + } + } + + @override + String toString() { + return name; + } +} diff --git a/lib/models/settings.dart b/lib/models/settings.dart new file mode 100644 index 0000000..e43acde --- /dev/null +++ b/lib/models/settings.dart @@ -0,0 +1,130 @@ +import 'package:diameter/main.dart'; +import 'package:flutter/material.dart'; +import 'package:objectbox/objectbox.dart'; + +enum GlucoseDisplayMode { activeOnly, bothForList, bothForDetail, both } +List glucoseDisplayModeLabels = [ + 'activeOnly', + 'bothForList', + 'bothForDetail', + 'both', +]; + +enum GlucoseMeasurement { + mgPerDl, + mmolPerL, +} +List glucoseMeasurementSuffixes = [ + 'mg/dl', + 'mmol/l', +]; +List glucoseMeasurementLabels = [ + 'mgPerDl', + 'mmolPerL', +]; + +enum NutritionMeasurement { + grams, + ounces, + lbs, +} +List nutritionMeasurementSuffixes = [ + 'g', + 'oz', + 'lbs', +]; +List nutritionMeasurementLabels = [ + 'grams', + 'ounces', + 'lbs', +]; + +@Entity(uid: 3989341091218179227) +@Sync() +class Settings { + static final Box box = objectBox.store.box(); + + // properties + int id; + + int nutritionMeasurementIndex; + int glucoseDisplayModeIndex; + int glucoseMeasurementIndex; + int targetGlucoseMgPerDl; + double targetGlucoseMmolPerL; + + double insulinIncrements; + double nutritionIncrements; + double mmolPerLIncrements; + double amountIncrements; + + String dateFormat; + String? longDateFormat; + String timeFormat; + String? longTimeFormat; + + bool showConfirmationDialogOnCancel; + bool showConfirmationDialogOnDelete; + bool showConfirmationDialogOnStopEvent; + + bool useDarkTheme; + + // constructor + Settings({ + this.id = 0, + 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', + this.longTimeFormat = 'HH:mm:ss', + this.showConfirmationDialogOnCancel = true, + this.showConfirmationDialogOnDelete = true, + this.showConfirmationDialogOnStopEvent = true, + this.targetGlucoseMgPerDl = 100, + this.targetGlucoseMmolPerL = 5.5, + this.useDarkTheme = false, + }); + + // methods + static Settings get() { + if (box.getAll().length != 1) { + reset(); + } + return box.getAll().single; + } + + static NutritionMeasurement get nutritionMeasurement => + NutritionMeasurement.values[get().nutritionMeasurementIndex]; + static GlucoseMeasurement get glucoseMeasurement => + GlucoseMeasurement.values[get().glucoseMeasurementIndex]; + static GlucoseDisplayMode get glucoseDisplayMode => + GlucoseDisplayMode.values[get().glucoseDisplayModeIndex]; + + static String get nutritionMeasurementSuffix => + nutritionMeasurementSuffixes[get().nutritionMeasurementIndex]; + static String get glucoseMeasurementSuffix => + glucoseMeasurementSuffixes[get().glucoseMeasurementIndex]; + + 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; + + static void put(Settings settings) => box.put(settings); + + static void reset() { + box.removeAll(); + box.put(Settings(useDarkTheme: ThemeMode.system == ThemeMode.dark)); + } +} diff --git a/lib/navigation.dart b/lib/navigation.dart index 1b16cc4..6d308ad 100644 --- a/lib/navigation.dart +++ b/lib/navigation.dart @@ -2,16 +2,17 @@ import 'package:diameter/screens/accuracy_detail.dart'; import 'package:diameter/screens/accuracy_list.dart'; import 'package:diameter/screens/basal/basal_detail.dart'; import 'package:diameter/screens/basal/basal_profile_detail.dart'; -import 'package:diameter/screens/basal/basal_profiles_list.dart'; +import 'package:diameter/screens/basal/basal_profile_list.dart'; import 'package:diameter/screens/bolus/bolus_detail.dart'; import 'package:diameter/screens/bolus/bolus_profile_detail.dart'; import 'package:diameter/screens/bolus/bolus_profile_list.dart'; import 'package:diameter/screens/log/log.dart'; -import 'package:diameter/screens/log/log_entry.dart'; -import 'package:diameter/screens/log/log_event_detail.dart'; -import 'package:diameter/screens/log/log_event_type_detail.dart'; -import 'package:diameter/screens/log/log_event_type_list.dart'; -import 'package:diameter/screens/log/log_meal_detail.dart'; +import 'package:diameter/screens/log/log_entry/log_entry.dart'; +import 'package:diameter/screens/log/log_event/log_event_detail.dart'; +import 'package:diameter/screens/log/log_event/log_event_list.dart'; +import 'package:diameter/screens/log/log_event/log_event_type_detail.dart'; +import 'package:diameter/screens/log/log_event/log_event_type_list.dart'; +import 'package:diameter/screens/log/log_entry/log_meal_detail.dart'; import 'package:diameter/screens/meal/meal_category_detail.dart'; import 'package:diameter/screens/meal/meal_category_list.dart'; import 'package:diameter/screens/meal/meal_detail.dart'; @@ -20,6 +21,8 @@ import 'package:diameter/screens/meal/meal_portion_type_detail.dart'; import 'package:diameter/screens/meal/meal_portion_type_list.dart'; import 'package:diameter/screens/meal/meal_source_detail.dart'; import 'package:diameter/screens/meal/meal_source_list.dart'; +import 'package:diameter/screens/recipe/recipe_detail.dart'; +import 'package:diameter/screens/recipe/recipe_list.dart'; import 'package:diameter/settings.dart'; import 'package:flutter/material.dart'; @@ -39,9 +42,14 @@ class Routes { static const String logEvent = LogEventDetailScreen.routeName; static const String logMeal = LogMealDetailScreen.routeName; static const List logEntryRoutes = [logEntry, logEvent, logMeal]; - static const String logEventType = LogEventTypeDetailScreen.routeName; + static const String logEventType = EventTypeDetailScreen.routeName; static const String logEventTypes = LogEventTypeListScreen.routeName; static const List logEventTypeRoutes = [logEventType, logEventTypes]; + static const String events = LogEventListScreen.routeName; + + static const String recipe = RecipeDetailScreen.routeName; + static const String recipes = RecipeListScreen.routeName; + static const List recipeRoutes = [recipe, recipes]; static const String meal = MealDetailScreen.routeName; static const String meals = MealListScreen.routeName; @@ -98,16 +106,24 @@ class _NavigationState extends State { selected: widget.currentLocation == Routes.log, ), ListTile( - title: const Text('Log Entry'), - leading: const Icon(Icons.description), + title: const Text('Log Events'), + leading: const Icon(Icons.event), onTap: () { - selectDestination(Routes.logEntry); + selectDestination(Routes.events); }, - selected: Routes.logEntryRoutes.contains(widget.currentLocation), + 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('Meals'), - leading: const Icon(Icons.restaurant), + leading: const Icon(Icons.dinner_dining), onTap: () { selectDestination(Routes.meals); }, diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json index 65b048e..e0a1e35 100644 --- a/lib/objectbox-model.json +++ b/lib/objectbox-model.json @@ -3,49 +3,11 @@ "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ - { - "id": "1:3095978685310268382", - "lastPropertyId": "6:5471636804765937328", - "name": "Accuracy", - "properties": [ - { - "id": "1:3455702077061719523", - "name": "id", - "type": 6, - "flags": 1 - }, - { - "id": "2:1048198814030724077", - "name": "value", - "type": 9 - }, - { - "id": "3:9003780003858349085", - "name": "forCarbsRatio", - "type": 1 - }, - { - "id": "4:5421422436108145565", - "name": "forPortionSize", - "type": 1 - }, - { - "id": "5:7741631874181070179", - "name": "confidenceRating", - "type": 6 - }, - { - "id": "6:5471636804765937328", - "name": "notes", - "type": 9 - } - ], - "relations": [] - }, { "id": "2:1467758525778521891", - "lastPropertyId": "5:3908367275335317130", + "lastPropertyId": "6:3409466778841164684", "name": "Basal", + "flags": 2, "properties": [ { "id": "1:4281816825522738642", @@ -75,14 +37,20 @@ "flags": 520, "indexId": "1:8279975749291974737", "relationTarget": "BasalProfile" + }, + { + "id": "6:3409466778841164684", + "name": "deleted", + "type": 1 } ], "relations": [] }, { "id": "3:3613736032926903785", - "lastPropertyId": "4:6719547342639071472", + "lastPropertyId": "5:8140071977687660397", "name": "BasalProfile", + "flags": 2, "properties": [ { "id": "1:353771983641472117", @@ -104,14 +72,20 @@ "id": "4:6719547342639071472", "name": "notes", "type": 9 + }, + { + "id": "5:8140071977687660397", + "name": "deleted", + "type": 1 } ], "relations": [] }, { "id": "4:3417770529060202389", - "lastPropertyId": "8:7679622918986671917", + "lastPropertyId": "9:7440090146687096977", "name": "Bolus", + "flags": 2, "properties": [ { "id": "1:8141647919190345775", @@ -156,14 +130,20 @@ "flags": 520, "indexId": "2:1936045997906240691", "relationTarget": "BolusProfile" + }, + { + "id": "9:7440090146687096977", + "name": "deleted", + "type": 1 } ], "relations": [] }, { "id": "5:8812452529027052317", - "lastPropertyId": "4:3030493484602726372", + "lastPropertyId": "5:8082994824481464395", "name": "BolusProfile", + "flags": 2, "properties": [ { "id": "1:4233863196673391978", @@ -185,14 +165,20 @@ "id": "4:3030493484602726372", "name": "notes", "type": 9 + }, + { + "id": "5:8082994824481464395", + "name": "deleted", + "type": 1 } ], "relations": [] }, { "id": "6:752131069307970560", - "lastPropertyId": "8:6492273995038150006", + "lastPropertyId": "10:2505303363495348118", "name": "LogEntry", + "flags": 2, "properties": [ { "id": "1:5528657304180237933", @@ -215,33 +201,29 @@ "name": "mmolPerL", "type": 8 }, - { - "id": "5:3678829169126156351", - "name": "bolusGlucose", - "type": 8 - }, - { - "id": "6:1568597071506264632", - "name": "delayedBolusDuration", - "type": 6 - }, - { - "id": "7:8795268969829293398", - "name": "delayedBolusRate", - "type": 8 - }, { "id": "8:6492273995038150006", "name": "notes", "type": 9 + }, + { + "id": "9:1692732373071965573", + "name": "deleted", + "type": 1 + }, + { + "id": "10:2505303363495348118", + "name": "glucoseTrend", + "type": 8 } ], "relations": [] }, { "id": "7:4303325892753185970", - "lastPropertyId": "8:2514297323717317184", + "lastPropertyId": "12:3041952167628926163", "name": "LogEvent", + "flags": 2, "properties": [ { "id": "1:6648501734758557663", @@ -269,22 +251,6 @@ "name": "notes", "type": 9 }, - { - "id": "6:7838546213550447420", - "name": "logEntryId", - "type": 11, - "flags": 520, - "indexId": "3:3670661188280692002", - "relationTarget": "LogEntry" - }, - { - "id": "7:8031421171668506924", - "name": "endLogEntryId", - "type": 11, - "flags": 520, - "indexId": "4:7379712902406481832", - "relationTarget": "LogEntry" - }, { "id": "8:2514297323717317184", "name": "eventTypeId", @@ -292,14 +258,41 @@ "flags": 520, "indexId": "5:1417691902662024007", "relationTarget": "LogEventType" + }, + { + "id": "9:8477413048577624801", + "name": "deleted", + "type": 1 + }, + { + "id": "10:987218091728524211", + "name": "bolusProfileId", + "type": 11, + "flags": 520, + "indexId": "25:2500612771974500993", + "relationTarget": "BolusProfile" + }, + { + "id": "11:2013538196800336796", + "name": "basalProfileId", + "type": 11, + "flags": 520, + "indexId": "26:4562998391990896273", + "relationTarget": "BasalProfile" + }, + { + "id": "12:3041952167628926163", + "name": "reminderDuration", + "type": 6 } ], "relations": [] }, { "id": "8:8362795406595606110", - "lastPropertyId": "5:7361377572496986196", + "lastPropertyId": "8:1869014400856897151", "name": "LogEventType", + "flags": 2, "properties": [ { "id": "1:1430413826199774000", @@ -326,14 +319,36 @@ "id": "5:7361377572496986196", "name": "notes", "type": 9 + }, + { + "id": "6:5428344494256722438", + "name": "deleted", + "type": 1 + }, + { + "id": "7:9194648252717310397", + "name": "bolusProfileId", + "type": 11, + "flags": 520, + "indexId": "27:758221514459743282", + "relationTarget": "BolusProfile" + }, + { + "id": "8:1869014400856897151", + "name": "basalProfileId", + "type": 11, + "flags": 520, + "indexId": "28:4563029809754152081", + "relationTarget": "BasalProfile" } ], "relations": [] }, { "id": "9:411177866700467286", - "lastPropertyId": "16:7121997990741934484", + "lastPropertyId": "19:8965198821438347033", "name": "LogMeal", + "flags": 2, "properties": [ { "id": "1:962999525294133158", @@ -356,26 +371,11 @@ "name": "portionSize", "type": 8 }, - { - "id": "5:2215708755581938580", - "name": "carbsPerPortion", - "type": 8 - }, { "id": "6:8074052538574863399", "name": "bolus", "type": 8 }, - { - "id": "7:3247926313599127440", - "name": "delayedBolusDuration", - "type": 6 - }, - { - "id": "8:8789440370359282572", - "name": "delayedBolusRate", - "type": 8 - }, { "id": "9:1920579694098037947", "name": "notes", @@ -436,14 +436,30 @@ "flags": 520, "indexId": "12:35287836658362611", "relationTarget": "Accuracy" + }, + { + "id": "17:7341439841011629937", + "name": "deleted", + "type": 1 + }, + { + "id": "18:7405129785654054238", + "name": "amount", + "type": 8 + }, + { + "id": "19:8965198821438347033", + "name": "totalCarbs", + "type": 8 } ], "relations": [] }, { "id": "10:382130101578692012", - "lastPropertyId": "13:4890778480468380841", + "lastPropertyId": "15:8283810711091063880", "name": "Meal", + "flags": 2, "properties": [ { "id": "1:612386612600420389", @@ -476,11 +492,6 @@ "name": "delayedBolusDuration", "type": 6 }, - { - "id": "7:2172890064639236018", - "name": "delayedBolusRate", - "type": 8 - }, { "id": "8:6111684052388229887", "name": "notes", @@ -525,14 +536,25 @@ "flags": 520, "indexId": "17:9108886538013386415", "relationTarget": "Accuracy" + }, + { + "id": "14:3567196286623536415", + "name": "deleted", + "type": 1 + }, + { + "id": "15:8283810711091063880", + "name": "delayedBolusPercentage", + "type": 8 } ], "relations": [] }, { "id": "11:3158200688796904913", - "lastPropertyId": "3:3543757971350345683", + "lastPropertyId": "4:824435977543069541", "name": "MealCategory", + "flags": 2, "properties": [ { "id": "1:3678943122076184840", @@ -549,14 +571,20 @@ "id": "3:3543757971350345683", "name": "notes", "type": 9 + }, + { + "id": "4:824435977543069541", + "name": "deleted", + "type": 1 } ], "relations": [] }, { "id": "12:2111511899235985637", - "lastPropertyId": "3:1950852666001613408", + "lastPropertyId": "4:5680236937391945907", "name": "MealPortionType", + "flags": 2, "properties": [ { "id": "1:65428405312238271", @@ -573,14 +601,20 @@ "id": "3:1950852666001613408", "name": "notes", "type": 9 + }, + { + "id": "4:5680236937391945907", + "name": "deleted", + "type": 1 } ], "relations": [] }, { "id": "13:1283034494527412242", - "lastPropertyId": "7:5852853174931678667", + "lastPropertyId": "8:4547899751779962180", "name": "MealSource", + "flags": 2, "properties": [ { "id": "1:7205380295259922130", @@ -629,20 +663,415 @@ "flags": 520, "indexId": "21:1931330716440762729", "relationTarget": "Accuracy" + }, + { + "id": "8:4547899751779962180", + "name": "deleted", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "14:8033487006694871160", + "lastPropertyId": "18:7503231998671134983", + "name": "LogBolus", + "flags": 2, + "properties": [ + { + "id": "1:8254237730262024662", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:7669701519569266656", + "name": "units", + "type": 8 + }, + { + "id": "3:1967840431906109999", + "name": "carbs", + "type": 8 + }, + { + "id": "4:5520321978435312625", + "name": "delay", + "type": 6 + }, + { + "id": "7:3065420032567707091", + "name": "setManually", + "type": 1 + }, + { + "id": "8:2967613978873295525", + "name": "notes", + "type": 9 + }, + { + "id": "9:5454965717985089938", + "name": "logEntryId", + "type": 11, + "flags": 520, + "indexId": "22:5852072074740543047", + "relationTarget": "LogEntry" + }, + { + "id": "10:4105009806564072037", + "name": "rateId", + "type": 11, + "flags": 520, + "indexId": "23:1594553054621930876", + "relationTarget": "Bolus" + }, + { + "id": "11:4818762109001810295", + "name": "mealId", + "type": 11, + "flags": 520, + "indexId": "24:4224983816051843140", + "relationTarget": "LogMeal" + }, + { + "id": "12:4765038304548427459", + "name": "deleted", + "type": 1 + }, + { + "id": "13:2530431967957143684", + "name": "mgPerDlCurrent", + "type": 6 + }, + { + "id": "14:5210229118898251877", + "name": "mgPerDlTarget", + "type": 6 + }, + { + "id": "15:657840864788362204", + "name": "mgPerDlCorrection", + "type": 6 + }, + { + "id": "16:3999403624434995450", + "name": "mmolPerLCurrent", + "type": 8 + }, + { + "id": "17:2852253735546692099", + "name": "mmolPerLTarget", + "type": 8 + }, + { + "id": "18:7503231998671134983", + "name": "mmolPerLCorrection", + "type": 8 + } + ], + "relations": [] + }, + { + "id": "15:291512798403320400", + "lastPropertyId": "7:6675647182186603076", + "name": "Accuracy", + "flags": 2, + "properties": [ + { + "id": "1:8405388350474524599", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:1919049381880760479", + "name": "value", + "type": 9 + }, + { + "id": "3:7181081526218678274", + "name": "forCarbsRatio", + "type": 1 + }, + { + "id": "4:3576006369067328383", + "name": "forPortionSize", + "type": 1 + }, + { + "id": "5:7027546512578846894", + "name": "confidenceRating", + "type": 6 + }, + { + "id": "6:6625101003527710274", + "name": "notes", + "type": 9 + }, + { + "id": "7:6675647182186603076", + "name": "deleted", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "16:3989341091218179227", + "lastPropertyId": "23:3611447442844013652", + "name": "Settings", + "flags": 2, + "properties": [ + { + "id": "1:7803753645747063723", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:4703380985530623101", + "name": "dateFormat", + "type": 9 + }, + { + "id": "3:2983395924801005937", + "name": "longDateFormat", + "type": 9 + }, + { + "id": "4:2579032794029389590", + "name": "timeFormat", + "type": 9 + }, + { + "id": "5:3970690908108519507", + "name": "longTimeFormat", + "type": 9 + }, + { + "id": "6:349893175332801783", + "name": "showConfirmationDialogOnCancel", + "type": 1 + }, + { + "id": "7:4049915860178079910", + "name": "showConfirmationDialogOnDelete", + "type": 1 + }, + { + "id": "8:3088241443557186512", + "name": "showConfirmationDialogOnStopEvent", + "type": 1 + }, + { + "id": "18:1203593429961092769", + "name": "nutritionMeasurementIndex", + "type": 6 + }, + { + "id": "19:8895176254912253797", + "name": "glucoseDisplayModeIndex", + "type": 6 + }, + { + "id": "20:6560414475711071975", + "name": "glucoseMeasurementIndex", + "type": 6 + }, + { + "id": "21:7934134105044248002", + "name": "targetGlucoseMgPerDl", + "type": 6 + }, + { + "id": "22:3595473653451456068", + "name": "targetGlucoseMmolPerL", + "type": 8 + }, + { + "id": "23:3611447442844013652", + "name": "useDarkTheme", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "17:5041265995704044399", + "lastPropertyId": "7:1333487551279074696", + "name": "GlucoseTarget", + "flags": 2, + "properties": [ + { + "id": "1:4322960567133959537", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:7533461804561299987", + "name": "deleted", + "type": 1 + }, + { + "id": "3:4949963248761074916", + "name": "fromMgPerDL", + "type": 6 + }, + { + "id": "4:8685380695305799464", + "name": "toMgPerDl", + "type": 6 + }, + { + "id": "5:2925449628924807050", + "name": "fromMmolPerL", + "type": 8 + }, + { + "id": "6:3244873743284485064", + "name": "toMmolPerL", + "type": 8 + }, + { + "id": "7:1333487551279074696", + "name": "color", + "type": 6 + } + ], + "relations": [] + }, + { + "id": "18:6497942314956341514", + "lastPropertyId": "11:8488657312300528492", + "name": "Recipe", + "flags": 2, + "properties": [ + { + "id": "1:6426741154282018946", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:1167304402395485629", + "name": "deleted", + "type": 1 + }, + { + "id": "3:1244733840071626966", + "name": "name", + "type": 9 + }, + { + "id": "9:8593446427752839266", + "name": "notes", + "type": 9 + }, + { + "id": "10:4370359747396560337", + "name": "portionId", + "type": 11, + "flags": 520, + "indexId": "29:5110151182694376118", + "relationTarget": "Meal" + }, + { + "id": "11:8488657312300528492", + "name": "servings", + "type": 8 + } + ], + "relations": [] + }, + { + "id": "19:6950311793136068892", + "lastPropertyId": "5:6495065881132428893", + "name": "Ingredient", + "flags": 2, + "properties": [ + { + "id": "1:7766569281758551418", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:3830559702655088692", + "name": "deleted", + "type": 1 + }, + { + "id": "3:602057803225843875", + "name": "amount", + "type": 8 + }, + { + "id": "4:26686399245586953", + "name": "recipeId", + "type": 11, + "flags": 520, + "indexId": "30:5492781242713788590", + "relationTarget": "Recipe" + }, + { + "id": "5:6495065881132428893", + "name": "ingredientId", + "type": 11, + "flags": 520, + "indexId": "31:3277019237664417023", + "relationTarget": "Meal" } ], "relations": [] } ], - "lastEntityId": "13:1283034494527412242", - "lastIndexId": "21:1931330716440762729", + "lastEntityId": "19:6950311793136068892", + "lastIndexId": "31:3277019237664417023", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, - "retiredEntityUids": [], - "retiredIndexUids": [], - "retiredPropertyUids": [], + "retiredEntityUids": [ + 3095978685310268382 + ], + "retiredIndexUids": [ + 3670661188280692002, + 7379712902406481832 + ], + "retiredPropertyUids": [ + 3455702077061719523, + 1048198814030724077, + 9003780003858349085, + 5421422436108145565, + 7741631874181070179, + 5471636804765937328, + 6855574218883169324, + 5313708456544000157, + 3678829169126156351, + 1568597071506264632, + 8795268969829293398, + 3247926313599127440, + 8789440370359282572, + 7838546213550447420, + 8031421171668506924, + 1614362036318874174, + 1675040259141389754, + 7518219134349037920, + 2172890064639236018, + 310032577683835406, + 5588897884422150510, + 7638848982383620744, + 3282706593658092097, + 596980591281311896, + 3633551763915044903, + 2215708755581938580, + 241621230513128588, + 4678123663117222609, + 780211923138281722, + 763575433624979013, + 1225271130099322691 + ], "retiredRelationUids": [], "version": 1 } \ No newline at end of file diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index 4517098..ce823fb 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -7,13 +7,16 @@ import 'dart:typed_data'; import 'package:objectbox/flatbuffers/flat_buffers.dart' as fb; import 'package:objectbox/internal.dart'; // generated code can access "internal" functionality import 'package:objectbox/objectbox.dart'; -import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart'; +import 'package:objectbox_sync_flutter_libs/objectbox_sync_flutter_libs.dart'; import 'models/accuracy.dart'; import 'models/basal.dart'; import 'models/basal_profile.dart'; import 'models/bolus.dart'; import 'models/bolus_profile.dart'; +import 'models/glucose_target.dart'; +import 'models/ingredient.dart'; +import 'models/log_bolus.dart'; import 'models/log_entry.dart'; import 'models/log_event.dart'; import 'models/log_event_type.dart'; @@ -22,54 +25,17 @@ import 'models/meal.dart'; import 'models/meal_category.dart'; import 'models/meal_portion_type.dart'; import 'models/meal_source.dart'; +import 'models/recipe.dart'; +import 'models/settings.dart'; export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file final _entities = [ - ModelEntity( - id: const IdUid(1, 3095978685310268382), - name: 'Accuracy', - lastPropertyId: const IdUid(6, 5471636804765937328), - flags: 0, - properties: [ - ModelProperty( - id: const IdUid(1, 3455702077061719523), - name: 'id', - type: 6, - flags: 1), - ModelProperty( - id: const IdUid(2, 1048198814030724077), - name: 'value', - type: 9, - flags: 0), - ModelProperty( - id: const IdUid(3, 9003780003858349085), - name: 'forCarbsRatio', - type: 1, - flags: 0), - ModelProperty( - id: const IdUid(4, 5421422436108145565), - name: 'forPortionSize', - type: 1, - flags: 0), - ModelProperty( - id: const IdUid(5, 7741631874181070179), - name: 'confidenceRating', - type: 6, - flags: 0), - ModelProperty( - id: const IdUid(6, 5471636804765937328), - name: 'notes', - type: 9, - flags: 0) - ], - relations: [], - backlinks: []), ModelEntity( id: const IdUid(2, 1467758525778521891), name: 'Basal', - lastPropertyId: const IdUid(5, 3908367275335317130), - flags: 0, + lastPropertyId: const IdUid(6, 3409466778841164684), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 4281816825522738642), @@ -97,15 +63,20 @@ final _entities = [ type: 11, flags: 520, indexId: const IdUid(1, 8279975749291974737), - relationTarget: 'BasalProfile') + relationTarget: 'BasalProfile'), + ModelProperty( + id: const IdUid(6, 3409466778841164684), + name: 'deleted', + type: 1, + flags: 0) ], relations: [], backlinks: []), ModelEntity( id: const IdUid(3, 3613736032926903785), name: 'BasalProfile', - lastPropertyId: const IdUid(4, 6719547342639071472), - flags: 0, + lastPropertyId: const IdUid(5, 8140071977687660397), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 353771983641472117), @@ -126,6 +97,11 @@ final _entities = [ id: const IdUid(4, 6719547342639071472), name: 'notes', type: 9, + flags: 0), + ModelProperty( + id: const IdUid(5, 8140071977687660397), + name: 'deleted', + type: 1, flags: 0) ], relations: [], @@ -133,8 +109,8 @@ final _entities = [ ModelEntity( id: const IdUid(4, 3417770529060202389), name: 'Bolus', - lastPropertyId: const IdUid(8, 7679622918986671917), - flags: 0, + lastPropertyId: const IdUid(9, 7440090146687096977), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 8141647919190345775), @@ -177,15 +153,20 @@ final _entities = [ type: 11, flags: 520, indexId: const IdUid(2, 1936045997906240691), - relationTarget: 'BolusProfile') + relationTarget: 'BolusProfile'), + ModelProperty( + id: const IdUid(9, 7440090146687096977), + name: 'deleted', + type: 1, + flags: 0) ], relations: [], backlinks: []), ModelEntity( id: const IdUid(5, 8812452529027052317), name: 'BolusProfile', - lastPropertyId: const IdUid(4, 3030493484602726372), - flags: 0, + lastPropertyId: const IdUid(5, 8082994824481464395), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 4233863196673391978), @@ -206,6 +187,11 @@ final _entities = [ id: const IdUid(4, 3030493484602726372), name: 'notes', type: 9, + flags: 0), + ModelProperty( + id: const IdUid(5, 8082994824481464395), + name: 'deleted', + type: 1, flags: 0) ], relations: [], @@ -213,8 +199,8 @@ final _entities = [ ModelEntity( id: const IdUid(6, 752131069307970560), name: 'LogEntry', - lastPropertyId: const IdUid(8, 6492273995038150006), - flags: 0, + lastPropertyId: const IdUid(10, 2505303363495348118), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 5528657304180237933), @@ -236,42 +222,29 @@ final _entities = [ name: 'mmolPerL', type: 8, flags: 0), - ModelProperty( - id: const IdUid(5, 3678829169126156351), - name: 'bolusGlucose', - type: 8, - flags: 0), - ModelProperty( - id: const IdUid(6, 1568597071506264632), - name: 'delayedBolusDuration', - type: 6, - flags: 0), - ModelProperty( - id: const IdUid(7, 8795268969829293398), - name: 'delayedBolusRate', - type: 8, - flags: 0), ModelProperty( id: const IdUid(8, 6492273995038150006), name: 'notes', type: 9, + flags: 0), + ModelProperty( + id: const IdUid(9, 1692732373071965573), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(10, 2505303363495348118), + name: 'glucoseTrend', + type: 8, flags: 0) ], relations: [], - backlinks: [ - ModelBacklink( - name: 'events', srcEntity: 'LogEvent', srcField: 'logEntry'), - ModelBacklink( - name: 'endedEvents', - srcEntity: 'LogEvent', - srcField: 'endLogEntry'), - ModelBacklink(name: 'meals', srcEntity: 'LogMeal', srcField: 'logEntry') - ]), + backlinks: []), ModelEntity( id: const IdUid(7, 4303325892753185970), name: 'LogEvent', - lastPropertyId: const IdUid(8, 2514297323717317184), - flags: 0, + lastPropertyId: const IdUid(12, 3041952167628926163), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 6648501734758557663), @@ -298,35 +271,45 @@ final _entities = [ name: 'notes', type: 9, flags: 0), - ModelProperty( - id: const IdUid(6, 7838546213550447420), - name: 'logEntryId', - type: 11, - flags: 520, - indexId: const IdUid(3, 3670661188280692002), - relationTarget: 'LogEntry'), - ModelProperty( - id: const IdUid(7, 8031421171668506924), - name: 'endLogEntryId', - type: 11, - flags: 520, - indexId: const IdUid(4, 7379712902406481832), - relationTarget: 'LogEntry'), ModelProperty( id: const IdUid(8, 2514297323717317184), name: 'eventTypeId', type: 11, flags: 520, indexId: const IdUid(5, 1417691902662024007), - relationTarget: 'LogEventType') + relationTarget: 'LogEventType'), + ModelProperty( + id: const IdUid(9, 8477413048577624801), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(10, 987218091728524211), + name: 'bolusProfileId', + type: 11, + flags: 520, + indexId: const IdUid(25, 2500612771974500993), + relationTarget: 'BolusProfile'), + ModelProperty( + id: const IdUid(11, 2013538196800336796), + name: 'basalProfileId', + type: 11, + flags: 520, + indexId: const IdUid(26, 4562998391990896273), + relationTarget: 'BasalProfile'), + ModelProperty( + id: const IdUid(12, 3041952167628926163), + name: 'reminderDuration', + type: 6, + flags: 0) ], relations: [], backlinks: []), ModelEntity( id: const IdUid(8, 8362795406595606110), name: 'LogEventType', - lastPropertyId: const IdUid(5, 7361377572496986196), - flags: 0, + lastPropertyId: const IdUid(8, 1869014400856897151), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 1430413826199774000), @@ -352,15 +335,34 @@ final _entities = [ id: const IdUid(5, 7361377572496986196), name: 'notes', type: 9, - flags: 0) + flags: 0), + ModelProperty( + id: const IdUid(6, 5428344494256722438), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(7, 9194648252717310397), + name: 'bolusProfileId', + type: 11, + flags: 520, + indexId: const IdUid(27, 758221514459743282), + relationTarget: 'BolusProfile'), + ModelProperty( + id: const IdUid(8, 1869014400856897151), + name: 'basalProfileId', + type: 11, + flags: 520, + indexId: const IdUid(28, 4563029809754152081), + relationTarget: 'BasalProfile') ], relations: [], backlinks: []), ModelEntity( id: const IdUid(9, 411177866700467286), name: 'LogMeal', - lastPropertyId: const IdUid(16, 7121997990741934484), - flags: 0, + lastPropertyId: const IdUid(19, 8965198821438347033), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 962999525294133158), @@ -382,26 +384,11 @@ final _entities = [ name: 'portionSize', type: 8, flags: 0), - ModelProperty( - id: const IdUid(5, 2215708755581938580), - name: 'carbsPerPortion', - type: 8, - flags: 0), ModelProperty( id: const IdUid(6, 8074052538574863399), name: 'bolus', type: 8, flags: 0), - ModelProperty( - id: const IdUid(7, 3247926313599127440), - name: 'delayedBolusDuration', - type: 6, - flags: 0), - ModelProperty( - id: const IdUid(8, 8789440370359282572), - name: 'delayedBolusRate', - type: 8, - flags: 0), ModelProperty( id: const IdUid(9, 1920579694098037947), name: 'notes', @@ -455,15 +442,30 @@ final _entities = [ type: 11, flags: 520, indexId: const IdUid(12, 35287836658362611), - relationTarget: 'Accuracy') + relationTarget: 'Accuracy'), + ModelProperty( + id: const IdUid(17, 7341439841011629937), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(18, 7405129785654054238), + name: 'amount', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(19, 8965198821438347033), + name: 'totalCarbs', + type: 8, + flags: 0) ], relations: [], backlinks: []), ModelEntity( id: const IdUid(10, 382130101578692012), name: 'Meal', - lastPropertyId: const IdUid(13, 4890778480468380841), - flags: 0, + lastPropertyId: const IdUid(15, 8283810711091063880), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 612386612600420389), @@ -495,11 +497,6 @@ final _entities = [ name: 'delayedBolusDuration', type: 6, flags: 0), - ModelProperty( - id: const IdUid(7, 2172890064639236018), - name: 'delayedBolusRate', - type: 8, - flags: 0), ModelProperty( id: const IdUid(8, 6111684052388229887), name: 'notes', @@ -539,15 +536,25 @@ final _entities = [ type: 11, flags: 520, indexId: const IdUid(17, 9108886538013386415), - relationTarget: 'Accuracy') + relationTarget: 'Accuracy'), + ModelProperty( + id: const IdUid(14, 3567196286623536415), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(15, 8283810711091063880), + name: 'delayedBolusPercentage', + type: 8, + flags: 0) ], relations: [], backlinks: []), ModelEntity( id: const IdUid(11, 3158200688796904913), name: 'MealCategory', - lastPropertyId: const IdUid(3, 3543757971350345683), - flags: 0, + lastPropertyId: const IdUid(4, 824435977543069541), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 3678943122076184840), @@ -563,6 +570,11 @@ final _entities = [ id: const IdUid(3, 3543757971350345683), name: 'notes', type: 9, + flags: 0), + ModelProperty( + id: const IdUid(4, 824435977543069541), + name: 'deleted', + type: 1, flags: 0) ], relations: [], @@ -570,8 +582,8 @@ final _entities = [ ModelEntity( id: const IdUid(12, 2111511899235985637), name: 'MealPortionType', - lastPropertyId: const IdUid(3, 1950852666001613408), - flags: 0, + lastPropertyId: const IdUid(4, 5680236937391945907), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 65428405312238271), @@ -587,6 +599,11 @@ final _entities = [ id: const IdUid(3, 1950852666001613408), name: 'notes', type: 9, + flags: 0), + ModelProperty( + id: const IdUid(4, 5680236937391945907), + name: 'deleted', + type: 1, flags: 0) ], relations: [], @@ -594,8 +611,8 @@ final _entities = [ ModelEntity( id: const IdUid(13, 1283034494527412242), name: 'MealSource', - lastPropertyId: const IdUid(7, 5852853174931678667), - flags: 0, + lastPropertyId: const IdUid(8, 4547899751779962180), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 7205380295259922130), @@ -639,7 +656,353 @@ final _entities = [ type: 11, flags: 520, indexId: const IdUid(21, 1931330716440762729), - relationTarget: 'Accuracy') + relationTarget: 'Accuracy'), + ModelProperty( + id: const IdUid(8, 4547899751779962180), + name: 'deleted', + type: 1, + flags: 0) + ], + relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(14, 8033487006694871160), + name: 'LogBolus', + lastPropertyId: const IdUid(18, 7503231998671134983), + flags: 2, + properties: [ + ModelProperty( + id: const IdUid(1, 8254237730262024662), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 7669701519569266656), + name: 'units', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(3, 1967840431906109999), + name: 'carbs', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(4, 5520321978435312625), + name: 'delay', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(7, 3065420032567707091), + name: 'setManually', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(8, 2967613978873295525), + name: 'notes', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(9, 5454965717985089938), + name: 'logEntryId', + type: 11, + flags: 520, + indexId: const IdUid(22, 5852072074740543047), + relationTarget: 'LogEntry'), + ModelProperty( + id: const IdUid(10, 4105009806564072037), + name: 'rateId', + type: 11, + flags: 520, + indexId: const IdUid(23, 1594553054621930876), + relationTarget: 'Bolus'), + ModelProperty( + id: const IdUid(11, 4818762109001810295), + name: 'mealId', + type: 11, + flags: 520, + indexId: const IdUid(24, 4224983816051843140), + relationTarget: 'LogMeal'), + ModelProperty( + id: const IdUid(12, 4765038304548427459), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(13, 2530431967957143684), + name: 'mgPerDlCurrent', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(14, 5210229118898251877), + name: 'mgPerDlTarget', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(15, 657840864788362204), + name: 'mgPerDlCorrection', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(16, 3999403624434995450), + name: 'mmolPerLCurrent', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(17, 2852253735546692099), + name: 'mmolPerLTarget', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(18, 7503231998671134983), + name: 'mmolPerLCorrection', + type: 8, + flags: 0) + ], + relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(15, 291512798403320400), + name: 'Accuracy', + lastPropertyId: const IdUid(7, 6675647182186603076), + flags: 2, + properties: [ + ModelProperty( + id: const IdUid(1, 8405388350474524599), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 1919049381880760479), + name: 'value', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(3, 7181081526218678274), + name: 'forCarbsRatio', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(4, 3576006369067328383), + name: 'forPortionSize', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(5, 7027546512578846894), + name: 'confidenceRating', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(6, 6625101003527710274), + name: 'notes', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(7, 6675647182186603076), + name: 'deleted', + type: 1, + flags: 0) + ], + relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(16, 3989341091218179227), + name: 'Settings', + lastPropertyId: const IdUid(23, 3611447442844013652), + flags: 2, + properties: [ + ModelProperty( + id: const IdUid(1, 7803753645747063723), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 4703380985530623101), + name: 'dateFormat', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(3, 2983395924801005937), + name: 'longDateFormat', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(4, 2579032794029389590), + name: 'timeFormat', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(5, 3970690908108519507), + name: 'longTimeFormat', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(6, 349893175332801783), + name: 'showConfirmationDialogOnCancel', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(7, 4049915860178079910), + name: 'showConfirmationDialogOnDelete', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(8, 3088241443557186512), + name: 'showConfirmationDialogOnStopEvent', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(18, 1203593429961092769), + name: 'nutritionMeasurementIndex', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(19, 8895176254912253797), + name: 'glucoseDisplayModeIndex', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(20, 6560414475711071975), + name: 'glucoseMeasurementIndex', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(21, 7934134105044248002), + name: 'targetGlucoseMgPerDl', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(22, 3595473653451456068), + name: 'targetGlucoseMmolPerL', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(23, 3611447442844013652), + name: 'useDarkTheme', + type: 1, + flags: 0) + ], + relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(17, 5041265995704044399), + name: 'GlucoseTarget', + lastPropertyId: const IdUid(7, 1333487551279074696), + flags: 2, + properties: [ + ModelProperty( + id: const IdUid(1, 4322960567133959537), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 7533461804561299987), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(3, 4949963248761074916), + name: 'fromMgPerDL', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(4, 8685380695305799464), + name: 'toMgPerDl', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(5, 2925449628924807050), + name: 'fromMmolPerL', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(6, 3244873743284485064), + name: 'toMmolPerL', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(7, 1333487551279074696), + name: 'color', + type: 6, + flags: 0) + ], + relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(18, 6497942314956341514), + name: 'Recipe', + lastPropertyId: const IdUid(11, 8488657312300528492), + flags: 2, + properties: [ + ModelProperty( + id: const IdUid(1, 6426741154282018946), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 1167304402395485629), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(3, 1244733840071626966), + name: 'name', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(9, 8593446427752839266), + name: 'notes', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(10, 4370359747396560337), + name: 'portionId', + type: 11, + flags: 520, + indexId: const IdUid(29, 5110151182694376118), + relationTarget: 'Meal'), + ModelProperty( + id: const IdUid(11, 8488657312300528492), + name: 'servings', + type: 8, + flags: 0) + ], + relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(19, 6950311793136068892), + name: 'Ingredient', + lastPropertyId: const IdUid(5, 6495065881132428893), + flags: 2, + properties: [ + ModelProperty( + id: const IdUid(1, 7766569281758551418), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 3830559702655088692), + name: 'deleted', + type: 1, + flags: 0), + ModelProperty( + id: const IdUid(3, 602057803225843875), + name: 'amount', + type: 8, + flags: 0), + ModelProperty( + id: const IdUid(4, 26686399245586953), + name: 'recipeId', + type: 11, + flags: 520, + indexId: const IdUid(30, 5492781242713788590), + relationTarget: 'Recipe'), + ModelProperty( + id: const IdUid(5, 6495065881132428893), + name: 'ingredientId', + type: 11, + flags: 520, + indexId: const IdUid(31, 3277019237664417023), + relationTarget: 'Meal') ], relations: [], backlinks: []) @@ -665,62 +1028,53 @@ Future openStore( ModelDefinition getObjectBoxModel() { final model = ModelInfo( entities: _entities, - lastEntityId: const IdUid(18, 1283034494527412242), - lastIndexId: const IdUid(25, 1931330716440762729), + lastEntityId: const IdUid(19, 6950311793136068892), + lastIndexId: const IdUid(31, 3277019237664417023), lastRelationId: const IdUid(0, 0), lastSequenceId: const IdUid(0, 0), - retiredEntityUids: const [], - retiredIndexUids: const [], - retiredPropertyUids: const [], + retiredEntityUids: const [3095978685310268382], + retiredIndexUids: const [3670661188280692002, 7379712902406481832], + retiredPropertyUids: const [ + 3455702077061719523, + 1048198814030724077, + 9003780003858349085, + 5421422436108145565, + 7741631874181070179, + 5471636804765937328, + 6855574218883169324, + 5313708456544000157, + 3678829169126156351, + 1568597071506264632, + 8795268969829293398, + 3247926313599127440, + 8789440370359282572, + 7838546213550447420, + 8031421171668506924, + 1614362036318874174, + 1675040259141389754, + 7518219134349037920, + 2172890064639236018, + 310032577683835406, + 5588897884422150510, + 7638848982383620744, + 3282706593658092097, + 596980591281311896, + 3633551763915044903, + 2215708755581938580, + 241621230513128588, + 4678123663117222609, + 780211923138281722, + 763575433624979013, + 1225271130099322691 + ], retiredRelationUids: const [], modelVersion: 5, modelVersionParserMinimum: 5, version: 1); final bindings = { - Accuracy: EntityDefinition( - model: _entities[0], - toOneRelations: (Accuracy object) => [], - toManyRelations: (Accuracy object) => {}, - getId: (Accuracy object) => object.id, - setId: (Accuracy object, int id) { - object.id = id; - }, - objectToFB: (Accuracy object, fb.Builder fbb) { - final valueOffset = fbb.writeString(object.value); - final notesOffset = - object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(7); - fbb.addInt64(0, object.id); - fbb.addOffset(1, valueOffset); - fbb.addBool(2, object.forCarbsRatio); - fbb.addBool(3, object.forPortionSize); - fbb.addInt64(4, object.confidenceRating); - fbb.addOffset(5, notesOffset); - fbb.finish(fbb.endTable()); - return object.id; - }, - objectFromFB: (Store store, ByteData fbData) { - final buffer = fb.BufferContext(fbData); - final rootOffset = buffer.derefObject(0); - - final object = Accuracy( - id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), - value: - const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), - forCarbsRatio: - const fb.BoolReader().vTableGet(buffer, rootOffset, 8, false), - forPortionSize: const fb.BoolReader() - .vTableGet(buffer, rootOffset, 10, false), - confidenceRating: const fb.Int64Reader() - .vTableGetNullable(buffer, rootOffset, 12), - notes: const fb.StringReader() - .vTableGetNullable(buffer, rootOffset, 14)); - - return object; - }), Basal: EntityDefinition( - model: _entities[1], + model: _entities[0], toOneRelations: (Basal object) => [object.basalProfile], toManyRelations: (Basal object) => {}, getId: (Basal object) => object.id, @@ -728,12 +1082,13 @@ ModelDefinition getObjectBoxModel() { object.id = id; }, objectToFB: (Basal object, fb.Builder fbb) { - fbb.startTable(6); + fbb.startTable(7); fbb.addInt64(0, object.id); fbb.addInt64(1, object.startTime.millisecondsSinceEpoch); fbb.addInt64(2, object.endTime.millisecondsSinceEpoch); fbb.addFloat64(3, object.units); fbb.addInt64(4, object.basalProfile.targetId); + fbb.addBool(5, object.deleted); fbb.finish(fbb.endTable()); return object.id; }, @@ -743,6 +1098,8 @@ ModelDefinition getObjectBoxModel() { final object = Basal( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 14, false), startTime: DateTime.fromMillisecondsSinceEpoch( const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0)), endTime: DateTime.fromMillisecondsSinceEpoch( @@ -755,7 +1112,7 @@ ModelDefinition getObjectBoxModel() { return object; }), BasalProfile: EntityDefinition( - model: _entities[2], + model: _entities[1], toOneRelations: (BasalProfile object) => [], toManyRelations: (BasalProfile object) => {}, getId: (BasalProfile object) => object.id, @@ -766,11 +1123,12 @@ ModelDefinition getObjectBoxModel() { final nameOffset = fbb.writeString(object.name); final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(5); + fbb.startTable(6); fbb.addInt64(0, object.id); fbb.addOffset(1, nameOffset); fbb.addBool(2, object.active); fbb.addOffset(3, notesOffset); + fbb.addBool(4, object.deleted); fbb.finish(fbb.endTable()); return object.id; }, @@ -780,6 +1138,8 @@ ModelDefinition getObjectBoxModel() { final object = BasalProfile( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 12, false), name: const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), active: @@ -790,7 +1150,7 @@ ModelDefinition getObjectBoxModel() { return object; }), Bolus: EntityDefinition( - model: _entities[3], + model: _entities[2], toOneRelations: (Bolus object) => [object.bolusProfile], toManyRelations: (Bolus object) => {}, getId: (Bolus object) => object.id, @@ -798,7 +1158,7 @@ ModelDefinition getObjectBoxModel() { object.id = id; }, objectToFB: (Bolus object, fb.Builder fbb) { - fbb.startTable(9); + fbb.startTable(10); fbb.addInt64(0, object.id); fbb.addInt64(1, object.startTime.millisecondsSinceEpoch); fbb.addInt64(2, object.endTime.millisecondsSinceEpoch); @@ -807,6 +1167,7 @@ ModelDefinition getObjectBoxModel() { fbb.addInt64(5, object.mgPerDl); fbb.addFloat64(6, object.mmolPerL); fbb.addInt64(7, object.bolusProfile.targetId); + fbb.addBool(8, object.deleted); fbb.finish(fbb.endTable()); return object.id; }, @@ -816,6 +1177,8 @@ ModelDefinition getObjectBoxModel() { final object = Bolus( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 20, false), startTime: DateTime.fromMillisecondsSinceEpoch( const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0)), endTime: DateTime.fromMillisecondsSinceEpoch( @@ -834,7 +1197,7 @@ ModelDefinition getObjectBoxModel() { return object; }), BolusProfile: EntityDefinition( - model: _entities[4], + model: _entities[3], toOneRelations: (BolusProfile object) => [], toManyRelations: (BolusProfile object) => {}, getId: (BolusProfile object) => object.id, @@ -845,11 +1208,12 @@ ModelDefinition getObjectBoxModel() { final nameOffset = fbb.writeString(object.name); final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(5); + fbb.startTable(6); fbb.addInt64(0, object.id); fbb.addOffset(1, nameOffset); fbb.addBool(2, object.active); fbb.addOffset(3, notesOffset); + fbb.addBool(4, object.deleted); fbb.finish(fbb.endTable()); return object.id; }, @@ -859,6 +1223,8 @@ ModelDefinition getObjectBoxModel() { final object = BolusProfile( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 12, false), name: const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), active: @@ -869,19 +1235,9 @@ ModelDefinition getObjectBoxModel() { return object; }), LogEntry: EntityDefinition( - model: _entities[5], + model: _entities[4], toOneRelations: (LogEntry object) => [], - toManyRelations: (LogEntry object) => { - RelInfo.toOneBacklink( - 6, object.id, (LogEvent srcObject) => srcObject.logEntry): - object.events, - RelInfo.toOneBacklink(7, object.id, - (LogEvent srcObject) => srcObject.endLogEntry): - object.endedEvents, - RelInfo.toOneBacklink( - 10, object.id, (LogMeal srcObject) => srcObject.logEntry): - object.meals - }, + toManyRelations: (LogEntry object) => {}, getId: (LogEntry object) => object.id, setId: (LogEntry object, int id) { object.id = id; @@ -889,15 +1245,14 @@ ModelDefinition getObjectBoxModel() { objectToFB: (LogEntry object, fb.Builder fbb) { final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(9); + fbb.startTable(11); fbb.addInt64(0, object.id); fbb.addInt64(1, object.time.millisecondsSinceEpoch); fbb.addInt64(2, object.mgPerDl); fbb.addFloat64(3, object.mmolPerL); - fbb.addFloat64(4, object.bolusGlucose); - fbb.addInt64(5, object.delayedBolusDuration); - fbb.addFloat64(6, object.delayedBolusRate); fbb.addOffset(7, notesOffset); + fbb.addBool(8, object.deleted); + fbb.addFloat64(9, object.glucoseTrend); fbb.finish(fbb.endTable()); return object.id; }, @@ -907,44 +1262,25 @@ ModelDefinition getObjectBoxModel() { final object = LogEntry( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 20, false), time: DateTime.fromMillisecondsSinceEpoch( const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0)), mgPerDl: const fb.Int64Reader() .vTableGetNullable(buffer, rootOffset, 8), mmolPerL: const fb.Float64Reader() .vTableGetNullable(buffer, rootOffset, 10), - bolusGlucose: const fb.Float64Reader() - .vTableGetNullable(buffer, rootOffset, 12), - delayedBolusDuration: const fb.Int64Reader() - .vTableGetNullable(buffer, rootOffset, 14), - delayedBolusRate: const fb.Float64Reader() - .vTableGetNullable(buffer, rootOffset, 16), + glucoseTrend: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 22), notes: const fb.StringReader() .vTableGetNullable(buffer, rootOffset, 18)); - InternalToManyAccess.setRelInfo( - object.events, - store, - RelInfo.toOneBacklink( - 6, object.id, (LogEvent srcObject) => srcObject.logEntry), - store.box()); - InternalToManyAccess.setRelInfo( - object.endedEvents, - store, - RelInfo.toOneBacklink( - 7, object.id, (LogEvent srcObject) => srcObject.endLogEntry), - store.box()); - InternalToManyAccess.setRelInfo( - object.meals, - store, - RelInfo.toOneBacklink( - 10, object.id, (LogMeal srcObject) => srcObject.logEntry), - store.box()); + return object; }), LogEvent: EntityDefinition( - model: _entities[6], + model: _entities[5], toOneRelations: (LogEvent object) => - [object.logEntry, object.endLogEntry, object.eventType], + [object.eventType, object.bolusProfile, object.basalProfile], toManyRelations: (LogEvent object) => {}, getId: (LogEvent object) => object.id, setId: (LogEvent object, int id) { @@ -953,15 +1289,17 @@ ModelDefinition getObjectBoxModel() { objectToFB: (LogEvent object, fb.Builder fbb) { final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(9); + fbb.startTable(13); fbb.addInt64(0, object.id); fbb.addInt64(1, object.time.millisecondsSinceEpoch); fbb.addInt64(2, object.endTime?.millisecondsSinceEpoch); fbb.addBool(3, object.hasEndTime); fbb.addOffset(4, notesOffset); - fbb.addInt64(5, object.logEntry.targetId); - fbb.addInt64(6, object.endLogEntry.targetId); fbb.addInt64(7, object.eventType.targetId); + fbb.addBool(8, object.deleted); + fbb.addInt64(9, object.bolusProfile.targetId); + fbb.addInt64(10, object.basalProfile.targetId); + fbb.addInt64(11, object.reminderDuration); fbb.finish(fbb.endTable()); return object.id; }, @@ -972,6 +1310,8 @@ ModelDefinition getObjectBoxModel() { const fb.Int64Reader().vTableGetNullable(buffer, rootOffset, 8); final object = LogEvent( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 20, false), time: DateTime.fromMillisecondsSinceEpoch( const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0)), endTime: endTimeValue == null @@ -979,22 +1319,25 @@ ModelDefinition getObjectBoxModel() { : DateTime.fromMillisecondsSinceEpoch(endTimeValue), hasEndTime: const fb.BoolReader() .vTableGet(buffer, rootOffset, 10, false), + reminderDuration: const fb.Int64Reader() + .vTableGetNullable(buffer, rootOffset, 26), notes: const fb.StringReader() .vTableGetNullable(buffer, rootOffset, 12)); - object.logEntry.targetId = - const fb.Int64Reader().vTableGet(buffer, rootOffset, 14, 0); - object.logEntry.attach(store); - object.endLogEntry.targetId = - const fb.Int64Reader().vTableGet(buffer, rootOffset, 16, 0); - object.endLogEntry.attach(store); object.eventType.targetId = const fb.Int64Reader().vTableGet(buffer, rootOffset, 18, 0); object.eventType.attach(store); + object.bolusProfile.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 22, 0); + object.bolusProfile.attach(store); + object.basalProfile.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 24, 0); + object.basalProfile.attach(store); return object; }), LogEventType: EntityDefinition( - model: _entities[7], - toOneRelations: (LogEventType object) => [], + model: _entities[6], + toOneRelations: (LogEventType object) => + [object.bolusProfile, object.basalProfile], toManyRelations: (LogEventType object) => {}, getId: (LogEventType object) => object.id, setId: (LogEventType object, int id) { @@ -1004,12 +1347,15 @@ ModelDefinition getObjectBoxModel() { final valueOffset = fbb.writeString(object.value); final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(6); + fbb.startTable(9); fbb.addInt64(0, object.id); fbb.addOffset(1, valueOffset); fbb.addBool(2, object.hasEndTime); fbb.addInt64(3, object.defaultReminderDuration); fbb.addOffset(4, notesOffset); + fbb.addBool(5, object.deleted); + fbb.addInt64(6, object.bolusProfile.targetId); + fbb.addInt64(7, object.basalProfile.targetId); fbb.finish(fbb.endTable()); return object.id; }, @@ -1019,6 +1365,8 @@ ModelDefinition getObjectBoxModel() { final object = LogEventType( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 14, false), value: const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), hasEndTime: @@ -1027,11 +1375,16 @@ ModelDefinition getObjectBoxModel() { .vTableGetNullable(buffer, rootOffset, 10), notes: const fb.StringReader() .vTableGetNullable(buffer, rootOffset, 12)); - + object.bolusProfile.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 16, 0); + object.bolusProfile.attach(store); + object.basalProfile.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 18, 0); + object.basalProfile.attach(store); return object; }), LogMeal: EntityDefinition( - model: _entities[8], + model: _entities[7], toOneRelations: (LogMeal object) => [ object.logEntry, object.meal, @@ -1050,15 +1403,12 @@ ModelDefinition getObjectBoxModel() { final valueOffset = fbb.writeString(object.value); final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(17); + fbb.startTable(20); fbb.addInt64(0, object.id); fbb.addOffset(1, valueOffset); fbb.addFloat64(2, object.carbsRatio); fbb.addFloat64(3, object.portionSize); - fbb.addFloat64(4, object.carbsPerPortion); fbb.addFloat64(5, object.bolus); - fbb.addInt64(6, object.delayedBolusDuration); - fbb.addFloat64(7, object.delayedBolusRate); fbb.addOffset(8, notesOffset); fbb.addInt64(9, object.logEntry.targetId); fbb.addInt64(10, object.meal.targetId); @@ -1067,6 +1417,9 @@ ModelDefinition getObjectBoxModel() { fbb.addInt64(13, object.mealPortionType.targetId); fbb.addInt64(14, object.portionSizeAccuracy.targetId); fbb.addInt64(15, object.carbsRatioAccuracy.targetId); + fbb.addBool(16, object.deleted); + fbb.addFloat64(17, object.amount); + fbb.addFloat64(18, object.totalCarbs); fbb.finish(fbb.endTable()); return object.id; }, @@ -1076,22 +1429,22 @@ ModelDefinition getObjectBoxModel() { final object = LogMeal( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 36, false), value: const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), + amount: + const fb.Float64Reader().vTableGet(buffer, rootOffset, 38, 0), carbsRatio: const fb.Float64Reader() .vTableGetNullable(buffer, rootOffset, 8), portionSize: const fb.Float64Reader() .vTableGetNullable(buffer, rootOffset, 10), - carbsPerPortion: const fb.Float64Reader() - .vTableGetNullable(buffer, rootOffset, 12), - bolus: const fb.Float64Reader() - .vTableGetNullable(buffer, rootOffset, 14), - delayedBolusDuration: const fb.Int64Reader() - .vTableGetNullable(buffer, rootOffset, 16), - delayedBolusRate: const fb.Float64Reader() - .vTableGetNullable(buffer, rootOffset, 18), + totalCarbs: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 40), notes: const fb.StringReader() - .vTableGetNullable(buffer, rootOffset, 20)); + .vTableGetNullable(buffer, rootOffset, 20)) + ..bolus = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 14); object.logEntry.targetId = const fb.Int64Reader().vTableGet(buffer, rootOffset, 22, 0); object.logEntry.attach(store); @@ -1116,7 +1469,7 @@ ModelDefinition getObjectBoxModel() { return object; }), Meal: EntityDefinition( - model: _entities[9], + model: _entities[8], toOneRelations: (Meal object) => [ object.mealSource, object.mealCategory, @@ -1133,20 +1486,21 @@ ModelDefinition getObjectBoxModel() { final valueOffset = fbb.writeString(object.value); final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(14); + fbb.startTable(16); fbb.addInt64(0, object.id); fbb.addOffset(1, valueOffset); fbb.addFloat64(2, object.carbsRatio); fbb.addFloat64(3, object.portionSize); fbb.addFloat64(4, object.carbsPerPortion); fbb.addInt64(5, object.delayedBolusDuration); - fbb.addFloat64(6, object.delayedBolusRate); fbb.addOffset(7, notesOffset); fbb.addInt64(8, object.mealSource.targetId); fbb.addInt64(9, object.mealCategory.targetId); fbb.addInt64(10, object.mealPortionType.targetId); fbb.addInt64(11, object.portionSizeAccuracy.targetId); fbb.addInt64(12, object.carbsRatioAccuracy.targetId); + fbb.addBool(13, object.deleted); + fbb.addFloat64(14, object.delayedBolusPercentage); fbb.finish(fbb.endTable()); return object.id; }, @@ -1156,6 +1510,8 @@ ModelDefinition getObjectBoxModel() { final object = Meal( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 30, false), value: const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), carbsRatio: const fb.Float64Reader() @@ -1166,8 +1522,8 @@ ModelDefinition getObjectBoxModel() { .vTableGetNullable(buffer, rootOffset, 12), delayedBolusDuration: const fb.Int64Reader() .vTableGetNullable(buffer, rootOffset, 14), - delayedBolusRate: const fb.Float64Reader() - .vTableGetNullable(buffer, rootOffset, 16), + delayedBolusPercentage: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 32), notes: const fb.StringReader() .vTableGetNullable(buffer, rootOffset, 18)); object.mealSource.targetId = @@ -1188,7 +1544,7 @@ ModelDefinition getObjectBoxModel() { return object; }), MealCategory: EntityDefinition( - model: _entities[10], + model: _entities[9], toOneRelations: (MealCategory object) => [], toManyRelations: (MealCategory object) => {}, getId: (MealCategory object) => object.id, @@ -1199,10 +1555,11 @@ ModelDefinition getObjectBoxModel() { final valueOffset = fbb.writeString(object.value); final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(4); + fbb.startTable(5); fbb.addInt64(0, object.id); fbb.addOffset(1, valueOffset); fbb.addOffset(2, notesOffset); + fbb.addBool(3, object.deleted); fbb.finish(fbb.endTable()); return object.id; }, @@ -1212,6 +1569,8 @@ ModelDefinition getObjectBoxModel() { final object = MealCategory( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 10, false), value: const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), notes: const fb.StringReader() @@ -1220,7 +1579,7 @@ ModelDefinition getObjectBoxModel() { return object; }), MealPortionType: EntityDefinition( - model: _entities[11], + model: _entities[10], toOneRelations: (MealPortionType object) => [], toManyRelations: (MealPortionType object) => {}, getId: (MealPortionType object) => object.id, @@ -1231,10 +1590,11 @@ ModelDefinition getObjectBoxModel() { final valueOffset = fbb.writeString(object.value); final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(4); + fbb.startTable(5); fbb.addInt64(0, object.id); fbb.addOffset(1, valueOffset); fbb.addOffset(2, notesOffset); + fbb.addBool(3, object.deleted); fbb.finish(fbb.endTable()); return object.id; }, @@ -1244,6 +1604,8 @@ ModelDefinition getObjectBoxModel() { final object = MealPortionType( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 10, false), value: const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), notes: const fb.StringReader() @@ -1252,7 +1614,7 @@ ModelDefinition getObjectBoxModel() { return object; }), MealSource: EntityDefinition( - model: _entities[12], + model: _entities[11], toOneRelations: (MealSource object) => [ object.defaultMealCategory, object.defaultMealPortionType, @@ -1268,7 +1630,7 @@ ModelDefinition getObjectBoxModel() { final valueOffset = fbb.writeString(object.value); final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(8); + fbb.startTable(9); fbb.addInt64(0, object.id); fbb.addOffset(1, valueOffset); fbb.addOffset(2, notesOffset); @@ -1276,6 +1638,7 @@ ModelDefinition getObjectBoxModel() { fbb.addInt64(4, object.defaultMealPortionType.targetId); fbb.addInt64(5, object.defaultCarbsRatioAccuracy.targetId); fbb.addInt64(6, object.defaultPortionSizeAccuracy.targetId); + fbb.addBool(7, object.deleted); fbb.finish(fbb.endTable()); return object.id; }, @@ -1285,6 +1648,8 @@ ModelDefinition getObjectBoxModel() { final object = MealSource( id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 18, false), value: const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), notes: const fb.StringReader() @@ -1302,393 +1667,952 @@ ModelDefinition getObjectBoxModel() { const fb.Int64Reader().vTableGet(buffer, rootOffset, 16, 0); object.defaultPortionSizeAccuracy.attach(store); return object; + }), + LogBolus: EntityDefinition( + model: _entities[12], + toOneRelations: (LogBolus object) => + [object.logEntry, object.rate, object.meal], + toManyRelations: (LogBolus object) => {}, + getId: (LogBolus object) => object.id, + setId: (LogBolus object, int id) { + object.id = id; + }, + objectToFB: (LogBolus object, fb.Builder fbb) { + final notesOffset = + object.notes == null ? null : fbb.writeString(object.notes!); + fbb.startTable(19); + fbb.addInt64(0, object.id); + fbb.addFloat64(1, object.units); + fbb.addFloat64(2, object.carbs); + fbb.addInt64(3, object.delay); + fbb.addBool(6, object.setManually); + fbb.addOffset(7, notesOffset); + fbb.addInt64(8, object.logEntry.targetId); + fbb.addInt64(9, object.rate.targetId); + fbb.addInt64(10, object.meal.targetId); + fbb.addBool(11, object.deleted); + fbb.addInt64(12, object.mgPerDlCurrent); + fbb.addInt64(13, object.mgPerDlTarget); + fbb.addInt64(14, object.mgPerDlCorrection); + fbb.addFloat64(15, object.mmolPerLCurrent); + fbb.addFloat64(16, object.mmolPerLTarget); + fbb.addFloat64(17, object.mmolPerLCorrection); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + + final object = LogBolus( + id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 26, false), + units: + const fb.Float64Reader().vTableGet(buffer, rootOffset, 6, 0), + carbs: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 8), + delay: const fb.Int64Reader() + .vTableGetNullable(buffer, rootOffset, 10), + mgPerDlCurrent: const fb.Int64Reader() + .vTableGetNullable(buffer, rootOffset, 28), + mgPerDlTarget: const fb.Int64Reader() + .vTableGetNullable(buffer, rootOffset, 30), + mgPerDlCorrection: const fb.Int64Reader() + .vTableGetNullable(buffer, rootOffset, 32), + mmolPerLCurrent: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 34), + mmolPerLTarget: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 36), + mmolPerLCorrection: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 38), + setManually: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 16, false), + notes: const fb.StringReader() + .vTableGetNullable(buffer, rootOffset, 18)); + object.logEntry.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 20, 0); + object.logEntry.attach(store); + object.rate.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 22, 0); + object.rate.attach(store); + object.meal.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 24, 0); + object.meal.attach(store); + return object; + }), + Accuracy: EntityDefinition( + model: _entities[13], + toOneRelations: (Accuracy object) => [], + toManyRelations: (Accuracy object) => {}, + getId: (Accuracy object) => object.id, + setId: (Accuracy object, int id) { + object.id = id; + }, + objectToFB: (Accuracy object, fb.Builder fbb) { + final valueOffset = fbb.writeString(object.value); + final notesOffset = + object.notes == null ? null : fbb.writeString(object.notes!); + fbb.startTable(8); + fbb.addInt64(0, object.id); + fbb.addOffset(1, valueOffset); + fbb.addBool(2, object.forCarbsRatio); + fbb.addBool(3, object.forPortionSize); + fbb.addInt64(4, object.confidenceRating); + fbb.addOffset(5, notesOffset); + fbb.addBool(6, object.deleted); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + + final object = Accuracy( + id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 16, false), + value: + const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), + forCarbsRatio: + const fb.BoolReader().vTableGet(buffer, rootOffset, 8, false), + forPortionSize: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 10, false), + confidenceRating: const fb.Int64Reader() + .vTableGetNullable(buffer, rootOffset, 12), + notes: const fb.StringReader() + .vTableGetNullable(buffer, rootOffset, 14)); + + return object; + }), + Settings: EntityDefinition( + model: _entities[14], + toOneRelations: (Settings object) => [], + toManyRelations: (Settings object) => {}, + getId: (Settings object) => object.id, + setId: (Settings object, int id) { + object.id = id; + }, + objectToFB: (Settings object, fb.Builder fbb) { + final dateFormatOffset = fbb.writeString(object.dateFormat); + final longDateFormatOffset = object.longDateFormat == null + ? null + : fbb.writeString(object.longDateFormat!); + final timeFormatOffset = fbb.writeString(object.timeFormat); + final longTimeFormatOffset = object.longTimeFormat == null + ? null + : fbb.writeString(object.longTimeFormat!); + fbb.startTable(24); + fbb.addInt64(0, object.id); + fbb.addOffset(1, dateFormatOffset); + fbb.addOffset(2, longDateFormatOffset); + fbb.addOffset(3, timeFormatOffset); + fbb.addOffset(4, longTimeFormatOffset); + fbb.addBool(5, object.showConfirmationDialogOnCancel); + fbb.addBool(6, object.showConfirmationDialogOnDelete); + fbb.addBool(7, object.showConfirmationDialogOnStopEvent); + fbb.addInt64(17, object.nutritionMeasurementIndex); + fbb.addInt64(18, object.glucoseDisplayModeIndex); + fbb.addInt64(19, object.glucoseMeasurementIndex); + fbb.addInt64(20, object.targetGlucoseMgPerDl); + fbb.addFloat64(21, object.targetGlucoseMmolPerL); + fbb.addBool(22, object.useDarkTheme); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + + final object = Settings( + id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + nutritionMeasurementIndex: + const fb.Int64Reader().vTableGet(buffer, rootOffset, 38, 0), + glucoseDisplayModeIndex: + const fb.Int64Reader().vTableGet(buffer, rootOffset, 40, 0), + glucoseMeasurementIndex: + const fb.Int64Reader().vTableGet(buffer, rootOffset, 42, 0), + dateFormat: + const fb.StringReader().vTableGet(buffer, rootOffset, 6, ''), + longDateFormat: const fb.StringReader() + .vTableGetNullable(buffer, rootOffset, 8), + timeFormat: + const fb.StringReader().vTableGet(buffer, rootOffset, 10, ''), + longTimeFormat: const fb.StringReader() + .vTableGetNullable(buffer, rootOffset, 12), + showConfirmationDialogOnCancel: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 14, false), + showConfirmationDialogOnDelete: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 16, false), + showConfirmationDialogOnStopEvent: const fb.BoolReader() + .vTableGet(buffer, rootOffset, 18, false), + targetGlucoseMgPerDl: + const fb.Int64Reader().vTableGet(buffer, rootOffset, 44, 0), + targetGlucoseMmolPerL: + const fb.Float64Reader().vTableGet(buffer, rootOffset, 46, 0), + useDarkTheme: const fb.BoolReader().vTableGet(buffer, rootOffset, 48, false)); + + return object; + }), + GlucoseTarget: EntityDefinition( + model: _entities[15], + toOneRelations: (GlucoseTarget object) => [], + toManyRelations: (GlucoseTarget object) => {}, + getId: (GlucoseTarget object) => object.id, + setId: (GlucoseTarget object, int id) { + object.id = id; + }, + objectToFB: (GlucoseTarget object, fb.Builder fbb) { + fbb.startTable(8); + fbb.addInt64(0, object.id); + fbb.addBool(1, object.deleted); + fbb.addInt64(2, object.fromMgPerDL); + fbb.addInt64(3, object.toMgPerDl); + fbb.addFloat64(4, object.fromMmolPerL); + fbb.addFloat64(5, object.toMmolPerL); + fbb.addInt64(6, object.color); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + + final object = GlucoseTarget( + id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: + const fb.BoolReader().vTableGet(buffer, rootOffset, 6, false), + fromMgPerDL: + const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0), + toMgPerDl: + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0), + fromMmolPerL: + const fb.Float64Reader().vTableGet(buffer, rootOffset, 12, 0), + toMmolPerL: + const fb.Float64Reader().vTableGet(buffer, rootOffset, 14, 0), + color: + const fb.Int64Reader().vTableGet(buffer, rootOffset, 16, 0)); + + return object; + }), + Recipe: EntityDefinition( + model: _entities[16], + toOneRelations: (Recipe object) => [object.portion], + toManyRelations: (Recipe object) => {}, + getId: (Recipe object) => object.id, + setId: (Recipe object, int id) { + object.id = id; + }, + objectToFB: (Recipe object, fb.Builder fbb) { + final nameOffset = fbb.writeString(object.name); + final notesOffset = + object.notes == null ? null : fbb.writeString(object.notes!); + fbb.startTable(12); + fbb.addInt64(0, object.id); + fbb.addBool(1, object.deleted); + fbb.addOffset(2, nameOffset); + fbb.addOffset(8, notesOffset); + fbb.addInt64(9, object.portion.targetId); + fbb.addFloat64(10, object.servings); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + + final object = Recipe( + id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: + const fb.BoolReader().vTableGet(buffer, rootOffset, 6, false), + name: + const fb.StringReader().vTableGet(buffer, rootOffset, 8, ''), + servings: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 24), + notes: const fb.StringReader() + .vTableGetNullable(buffer, rootOffset, 20)); + object.portion.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 22, 0); + object.portion.attach(store); + return object; + }), + Ingredient: EntityDefinition( + model: _entities[17], + toOneRelations: (Ingredient object) => + [object.recipe, object.ingredient], + toManyRelations: (Ingredient object) => {}, + getId: (Ingredient object) => object.id, + setId: (Ingredient object, int id) { + object.id = id; + }, + objectToFB: (Ingredient object, fb.Builder fbb) { + fbb.startTable(6); + fbb.addInt64(0, object.id); + fbb.addBool(1, object.deleted); + fbb.addFloat64(2, object.amount); + fbb.addInt64(3, object.recipe.targetId); + fbb.addInt64(4, object.ingredient.targetId); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + + final object = Ingredient( + id: const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0), + deleted: + const fb.BoolReader().vTableGet(buffer, rootOffset, 6, false), + amount: + const fb.Float64Reader().vTableGet(buffer, rootOffset, 8, 0)); + object.recipe.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0); + object.recipe.attach(store); + object.ingredient.targetId = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0); + object.ingredient.attach(store); + return object; }) }; return ModelDefinition(model, bindings); } -/// [Accuracy] entity fields to define ObjectBox queries. -class Accuracy_ { - /// see [Accuracy.id] - static final id = QueryIntegerProperty(_entities[0].properties[0]); - - /// see [Accuracy.value] - static final value = - QueryStringProperty(_entities[0].properties[1]); - - /// see [Accuracy.forCarbsRatio] - static final forCarbsRatio = - QueryBooleanProperty(_entities[0].properties[2]); - - /// see [Accuracy.forPortionSize] - static final forPortionSize = - QueryBooleanProperty(_entities[0].properties[3]); - - /// see [Accuracy.confidenceRating] - static final confidenceRating = - QueryIntegerProperty(_entities[0].properties[4]); - - /// see [Accuracy.notes] - static final notes = - QueryStringProperty(_entities[0].properties[5]); -} - /// [Basal] entity fields to define ObjectBox queries. class Basal_ { /// see [Basal.id] - static final id = QueryIntegerProperty(_entities[1].properties[0]); + static final id = QueryIntegerProperty(_entities[0].properties[0]); /// see [Basal.startTime] static final startTime = - QueryIntegerProperty(_entities[1].properties[1]); + QueryIntegerProperty(_entities[0].properties[1]); /// see [Basal.endTime] static final endTime = - QueryIntegerProperty(_entities[1].properties[2]); + QueryIntegerProperty(_entities[0].properties[2]); /// see [Basal.units] - static final units = QueryDoubleProperty(_entities[1].properties[3]); + static final units = QueryDoubleProperty(_entities[0].properties[3]); /// see [Basal.basalProfile] static final basalProfile = - QueryRelationToOne(_entities[1].properties[4]); + QueryRelationToOne(_entities[0].properties[4]); + + /// see [Basal.deleted] + static final deleted = + QueryBooleanProperty(_entities[0].properties[5]); } /// [BasalProfile] entity fields to define ObjectBox queries. class BasalProfile_ { /// see [BasalProfile.id] static final id = - QueryIntegerProperty(_entities[2].properties[0]); + QueryIntegerProperty(_entities[1].properties[0]); /// see [BasalProfile.name] static final name = - QueryStringProperty(_entities[2].properties[1]); + QueryStringProperty(_entities[1].properties[1]); /// see [BasalProfile.active] static final active = - QueryBooleanProperty(_entities[2].properties[2]); + QueryBooleanProperty(_entities[1].properties[2]); /// see [BasalProfile.notes] static final notes = - QueryStringProperty(_entities[2].properties[3]); + QueryStringProperty(_entities[1].properties[3]); + + /// see [BasalProfile.deleted] + static final deleted = + QueryBooleanProperty(_entities[1].properties[4]); } /// [Bolus] entity fields to define ObjectBox queries. class Bolus_ { /// see [Bolus.id] - static final id = QueryIntegerProperty(_entities[3].properties[0]); + static final id = QueryIntegerProperty(_entities[2].properties[0]); /// see [Bolus.startTime] static final startTime = - QueryIntegerProperty(_entities[3].properties[1]); + QueryIntegerProperty(_entities[2].properties[1]); /// see [Bolus.endTime] static final endTime = - QueryIntegerProperty(_entities[3].properties[2]); + QueryIntegerProperty(_entities[2].properties[2]); /// see [Bolus.units] - static final units = QueryDoubleProperty(_entities[3].properties[3]); + static final units = QueryDoubleProperty(_entities[2].properties[3]); /// see [Bolus.carbs] - static final carbs = QueryDoubleProperty(_entities[3].properties[4]); + static final carbs = QueryDoubleProperty(_entities[2].properties[4]); /// see [Bolus.mgPerDl] static final mgPerDl = - QueryIntegerProperty(_entities[3].properties[5]); + QueryIntegerProperty(_entities[2].properties[5]); /// see [Bolus.mmolPerL] static final mmolPerL = - QueryDoubleProperty(_entities[3].properties[6]); + QueryDoubleProperty(_entities[2].properties[6]); /// see [Bolus.bolusProfile] static final bolusProfile = - QueryRelationToOne(_entities[3].properties[7]); + QueryRelationToOne(_entities[2].properties[7]); + + /// see [Bolus.deleted] + static final deleted = + QueryBooleanProperty(_entities[2].properties[8]); } /// [BolusProfile] entity fields to define ObjectBox queries. class BolusProfile_ { /// see [BolusProfile.id] static final id = - QueryIntegerProperty(_entities[4].properties[0]); + QueryIntegerProperty(_entities[3].properties[0]); /// see [BolusProfile.name] static final name = - QueryStringProperty(_entities[4].properties[1]); + QueryStringProperty(_entities[3].properties[1]); /// see [BolusProfile.active] static final active = - QueryBooleanProperty(_entities[4].properties[2]); + QueryBooleanProperty(_entities[3].properties[2]); /// see [BolusProfile.notes] static final notes = - QueryStringProperty(_entities[4].properties[3]); + QueryStringProperty(_entities[3].properties[3]); + + /// see [BolusProfile.deleted] + static final deleted = + QueryBooleanProperty(_entities[3].properties[4]); } /// [LogEntry] entity fields to define ObjectBox queries. class LogEntry_ { /// see [LogEntry.id] - static final id = QueryIntegerProperty(_entities[5].properties[0]); + static final id = QueryIntegerProperty(_entities[4].properties[0]); /// see [LogEntry.time] static final time = - QueryIntegerProperty(_entities[5].properties[1]); + QueryIntegerProperty(_entities[4].properties[1]); /// see [LogEntry.mgPerDl] static final mgPerDl = - QueryIntegerProperty(_entities[5].properties[2]); + QueryIntegerProperty(_entities[4].properties[2]); /// see [LogEntry.mmolPerL] static final mmolPerL = - QueryDoubleProperty(_entities[5].properties[3]); - - /// see [LogEntry.bolusGlucose] - static final bolusGlucose = - QueryDoubleProperty(_entities[5].properties[4]); - - /// see [LogEntry.delayedBolusDuration] - static final delayedBolusDuration = - QueryIntegerProperty(_entities[5].properties[5]); - - /// see [LogEntry.delayedBolusRate] - static final delayedBolusRate = - QueryDoubleProperty(_entities[5].properties[6]); + QueryDoubleProperty(_entities[4].properties[3]); /// see [LogEntry.notes] static final notes = - QueryStringProperty(_entities[5].properties[7]); + QueryStringProperty(_entities[4].properties[4]); + + /// see [LogEntry.deleted] + static final deleted = + QueryBooleanProperty(_entities[4].properties[5]); + + /// see [LogEntry.glucoseTrend] + static final glucoseTrend = + QueryDoubleProperty(_entities[4].properties[6]); } /// [LogEvent] entity fields to define ObjectBox queries. class LogEvent_ { /// see [LogEvent.id] - static final id = QueryIntegerProperty(_entities[6].properties[0]); + static final id = QueryIntegerProperty(_entities[5].properties[0]); /// see [LogEvent.time] static final time = - QueryIntegerProperty(_entities[6].properties[1]); + QueryIntegerProperty(_entities[5].properties[1]); /// see [LogEvent.endTime] static final endTime = - QueryIntegerProperty(_entities[6].properties[2]); + QueryIntegerProperty(_entities[5].properties[2]); /// see [LogEvent.hasEndTime] static final hasEndTime = - QueryBooleanProperty(_entities[6].properties[3]); + QueryBooleanProperty(_entities[5].properties[3]); /// see [LogEvent.notes] static final notes = - QueryStringProperty(_entities[6].properties[4]); - - /// see [LogEvent.logEntry] - static final logEntry = - QueryRelationToOne(_entities[6].properties[5]); - - /// see [LogEvent.endLogEntry] - static final endLogEntry = - QueryRelationToOne(_entities[6].properties[6]); + QueryStringProperty(_entities[5].properties[4]); /// see [LogEvent.eventType] static final eventType = - QueryRelationToOne(_entities[6].properties[7]); + QueryRelationToOne(_entities[5].properties[5]); + + /// see [LogEvent.deleted] + static final deleted = + QueryBooleanProperty(_entities[5].properties[6]); + + /// see [LogEvent.bolusProfile] + static final bolusProfile = + QueryRelationToOne(_entities[5].properties[7]); + + /// see [LogEvent.basalProfile] + static final basalProfile = + QueryRelationToOne(_entities[5].properties[8]); + + /// see [LogEvent.reminderDuration] + static final reminderDuration = + QueryIntegerProperty(_entities[5].properties[9]); } /// [LogEventType] entity fields to define ObjectBox queries. class LogEventType_ { /// see [LogEventType.id] static final id = - QueryIntegerProperty(_entities[7].properties[0]); + QueryIntegerProperty(_entities[6].properties[0]); /// see [LogEventType.value] static final value = - QueryStringProperty(_entities[7].properties[1]); + QueryStringProperty(_entities[6].properties[1]); /// see [LogEventType.hasEndTime] static final hasEndTime = - QueryBooleanProperty(_entities[7].properties[2]); + QueryBooleanProperty(_entities[6].properties[2]); /// see [LogEventType.defaultReminderDuration] static final defaultReminderDuration = - QueryIntegerProperty(_entities[7].properties[3]); + QueryIntegerProperty(_entities[6].properties[3]); /// see [LogEventType.notes] static final notes = - QueryStringProperty(_entities[7].properties[4]); + QueryStringProperty(_entities[6].properties[4]); + + /// see [LogEventType.deleted] + static final deleted = + QueryBooleanProperty(_entities[6].properties[5]); + + /// see [LogEventType.bolusProfile] + static final bolusProfile = QueryRelationToOne( + _entities[6].properties[6]); + + /// see [LogEventType.basalProfile] + static final basalProfile = QueryRelationToOne( + _entities[6].properties[7]); } /// [LogMeal] entity fields to define ObjectBox queries. class LogMeal_ { /// see [LogMeal.id] - static final id = QueryIntegerProperty(_entities[8].properties[0]); + static final id = QueryIntegerProperty(_entities[7].properties[0]); /// see [LogMeal.value] - static final value = QueryStringProperty(_entities[8].properties[1]); + static final value = QueryStringProperty(_entities[7].properties[1]); /// see [LogMeal.carbsRatio] static final carbsRatio = - QueryDoubleProperty(_entities[8].properties[2]); + QueryDoubleProperty(_entities[7].properties[2]); /// see [LogMeal.portionSize] static final portionSize = - QueryDoubleProperty(_entities[8].properties[3]); - - /// see [LogMeal.carbsPerPortion] - static final carbsPerPortion = - QueryDoubleProperty(_entities[8].properties[4]); + QueryDoubleProperty(_entities[7].properties[3]); /// see [LogMeal.bolus] - static final bolus = QueryDoubleProperty(_entities[8].properties[5]); - - /// see [LogMeal.delayedBolusDuration] - static final delayedBolusDuration = - QueryIntegerProperty(_entities[8].properties[6]); - - /// see [LogMeal.delayedBolusRate] - static final delayedBolusRate = - QueryDoubleProperty(_entities[8].properties[7]); + static final bolus = QueryDoubleProperty(_entities[7].properties[4]); /// see [LogMeal.notes] - static final notes = QueryStringProperty(_entities[8].properties[8]); + static final notes = QueryStringProperty(_entities[7].properties[5]); /// see [LogMeal.logEntry] static final logEntry = - QueryRelationToOne(_entities[8].properties[9]); + QueryRelationToOne(_entities[7].properties[6]); /// see [LogMeal.meal] static final meal = - QueryRelationToOne(_entities[8].properties[10]); + QueryRelationToOne(_entities[7].properties[7]); /// see [LogMeal.mealSource] static final mealSource = - QueryRelationToOne(_entities[8].properties[11]); + QueryRelationToOne(_entities[7].properties[8]); /// see [LogMeal.mealCategory] static final mealCategory = - QueryRelationToOne(_entities[8].properties[12]); + QueryRelationToOne(_entities[7].properties[9]); /// see [LogMeal.mealPortionType] static final mealPortionType = - QueryRelationToOne(_entities[8].properties[13]); + QueryRelationToOne(_entities[7].properties[10]); /// see [LogMeal.portionSizeAccuracy] static final portionSizeAccuracy = - QueryRelationToOne(_entities[8].properties[14]); + QueryRelationToOne(_entities[7].properties[11]); /// see [LogMeal.carbsRatioAccuracy] static final carbsRatioAccuracy = - QueryRelationToOne(_entities[8].properties[15]); + QueryRelationToOne(_entities[7].properties[12]); + + /// see [LogMeal.deleted] + static final deleted = + QueryBooleanProperty(_entities[7].properties[13]); + + /// see [LogMeal.amount] + static final amount = + QueryDoubleProperty(_entities[7].properties[14]); + + /// see [LogMeal.totalCarbs] + static final totalCarbs = + QueryDoubleProperty(_entities[7].properties[15]); } /// [Meal] entity fields to define ObjectBox queries. class Meal_ { /// see [Meal.id] - static final id = QueryIntegerProperty(_entities[9].properties[0]); + static final id = QueryIntegerProperty(_entities[8].properties[0]); /// see [Meal.value] - static final value = QueryStringProperty(_entities[9].properties[1]); + static final value = QueryStringProperty(_entities[8].properties[1]); /// see [Meal.carbsRatio] static final carbsRatio = - QueryDoubleProperty(_entities[9].properties[2]); + QueryDoubleProperty(_entities[8].properties[2]); /// see [Meal.portionSize] static final portionSize = - QueryDoubleProperty(_entities[9].properties[3]); + QueryDoubleProperty(_entities[8].properties[3]); /// see [Meal.carbsPerPortion] static final carbsPerPortion = - QueryDoubleProperty(_entities[9].properties[4]); + QueryDoubleProperty(_entities[8].properties[4]); /// see [Meal.delayedBolusDuration] static final delayedBolusDuration = - QueryIntegerProperty(_entities[9].properties[5]); - - /// see [Meal.delayedBolusRate] - static final delayedBolusRate = - QueryDoubleProperty(_entities[9].properties[6]); + QueryIntegerProperty(_entities[8].properties[5]); /// see [Meal.notes] - static final notes = QueryStringProperty(_entities[9].properties[7]); + static final notes = QueryStringProperty(_entities[8].properties[6]); /// see [Meal.mealSource] static final mealSource = - QueryRelationToOne(_entities[9].properties[8]); + QueryRelationToOne(_entities[8].properties[7]); /// see [Meal.mealCategory] static final mealCategory = - QueryRelationToOne(_entities[9].properties[9]); + QueryRelationToOne(_entities[8].properties[8]); /// see [Meal.mealPortionType] static final mealPortionType = - QueryRelationToOne(_entities[9].properties[10]); + QueryRelationToOne(_entities[8].properties[9]); /// see [Meal.portionSizeAccuracy] static final portionSizeAccuracy = - QueryRelationToOne(_entities[9].properties[11]); + QueryRelationToOne(_entities[8].properties[10]); /// see [Meal.carbsRatioAccuracy] static final carbsRatioAccuracy = - QueryRelationToOne(_entities[9].properties[12]); + QueryRelationToOne(_entities[8].properties[11]); + + /// see [Meal.deleted] + static final deleted = + QueryBooleanProperty(_entities[8].properties[12]); + + /// see [Meal.delayedBolusPercentage] + static final delayedBolusPercentage = + QueryDoubleProperty(_entities[8].properties[13]); } /// [MealCategory] entity fields to define ObjectBox queries. class MealCategory_ { /// see [MealCategory.id] static final id = - QueryIntegerProperty(_entities[10].properties[0]); + QueryIntegerProperty(_entities[9].properties[0]); /// see [MealCategory.value] static final value = - QueryStringProperty(_entities[10].properties[1]); + QueryStringProperty(_entities[9].properties[1]); /// see [MealCategory.notes] static final notes = - QueryStringProperty(_entities[10].properties[2]); + QueryStringProperty(_entities[9].properties[2]); + + /// see [MealCategory.deleted] + static final deleted = + QueryBooleanProperty(_entities[9].properties[3]); } /// [MealPortionType] entity fields to define ObjectBox queries. class MealPortionType_ { /// see [MealPortionType.id] static final id = - QueryIntegerProperty(_entities[11].properties[0]); + QueryIntegerProperty(_entities[10].properties[0]); /// see [MealPortionType.value] static final value = - QueryStringProperty(_entities[11].properties[1]); + QueryStringProperty(_entities[10].properties[1]); /// see [MealPortionType.notes] static final notes = - QueryStringProperty(_entities[11].properties[2]); + QueryStringProperty(_entities[10].properties[2]); + + /// see [MealPortionType.deleted] + static final deleted = + QueryBooleanProperty(_entities[10].properties[3]); } /// [MealSource] entity fields to define ObjectBox queries. class MealSource_ { /// see [MealSource.id] static final id = - QueryIntegerProperty(_entities[12].properties[0]); + QueryIntegerProperty(_entities[11].properties[0]); /// see [MealSource.value] static final value = - QueryStringProperty(_entities[12].properties[1]); + QueryStringProperty(_entities[11].properties[1]); /// see [MealSource.notes] static final notes = - QueryStringProperty(_entities[12].properties[2]); + QueryStringProperty(_entities[11].properties[2]); /// see [MealSource.defaultMealCategory] static final defaultMealCategory = - QueryRelationToOne(_entities[12].properties[3]); + QueryRelationToOne(_entities[11].properties[3]); /// see [MealSource.defaultMealPortionType] static final defaultMealPortionType = QueryRelationToOne( - _entities[12].properties[4]); + _entities[11].properties[4]); /// see [MealSource.defaultCarbsRatioAccuracy] static final defaultCarbsRatioAccuracy = - QueryRelationToOne(_entities[12].properties[5]); + QueryRelationToOne(_entities[11].properties[5]); /// see [MealSource.defaultPortionSizeAccuracy] static final defaultPortionSizeAccuracy = - QueryRelationToOne(_entities[12].properties[6]); + QueryRelationToOne(_entities[11].properties[6]); + + /// see [MealSource.deleted] + static final deleted = + QueryBooleanProperty(_entities[11].properties[7]); +} + +/// [LogBolus] entity fields to define ObjectBox queries. +class LogBolus_ { + /// see [LogBolus.id] + static final id = QueryIntegerProperty(_entities[12].properties[0]); + + /// see [LogBolus.units] + static final units = + QueryDoubleProperty(_entities[12].properties[1]); + + /// see [LogBolus.carbs] + static final carbs = + QueryDoubleProperty(_entities[12].properties[2]); + + /// see [LogBolus.delay] + static final delay = + QueryIntegerProperty(_entities[12].properties[3]); + + /// see [LogBolus.setManually] + static final setManually = + QueryBooleanProperty(_entities[12].properties[4]); + + /// see [LogBolus.notes] + static final notes = + QueryStringProperty(_entities[12].properties[5]); + + /// see [LogBolus.logEntry] + static final logEntry = + QueryRelationToOne(_entities[12].properties[6]); + + /// see [LogBolus.rate] + static final rate = + QueryRelationToOne(_entities[12].properties[7]); + + /// see [LogBolus.meal] + static final meal = + QueryRelationToOne(_entities[12].properties[8]); + + /// see [LogBolus.deleted] + static final deleted = + QueryBooleanProperty(_entities[12].properties[9]); + + /// see [LogBolus.mgPerDlCurrent] + static final mgPerDlCurrent = + QueryIntegerProperty(_entities[12].properties[10]); + + /// see [LogBolus.mgPerDlTarget] + static final mgPerDlTarget = + QueryIntegerProperty(_entities[12].properties[11]); + + /// see [LogBolus.mgPerDlCorrection] + static final mgPerDlCorrection = + QueryIntegerProperty(_entities[12].properties[12]); + + /// see [LogBolus.mmolPerLCurrent] + static final mmolPerLCurrent = + QueryDoubleProperty(_entities[12].properties[13]); + + /// see [LogBolus.mmolPerLTarget] + static final mmolPerLTarget = + QueryDoubleProperty(_entities[12].properties[14]); + + /// see [LogBolus.mmolPerLCorrection] + static final mmolPerLCorrection = + QueryDoubleProperty(_entities[12].properties[15]); +} + +/// [Accuracy] entity fields to define ObjectBox queries. +class Accuracy_ { + /// see [Accuracy.id] + static final id = QueryIntegerProperty(_entities[13].properties[0]); + + /// see [Accuracy.value] + static final value = + QueryStringProperty(_entities[13].properties[1]); + + /// see [Accuracy.forCarbsRatio] + static final forCarbsRatio = + QueryBooleanProperty(_entities[13].properties[2]); + + /// see [Accuracy.forPortionSize] + static final forPortionSize = + QueryBooleanProperty(_entities[13].properties[3]); + + /// see [Accuracy.confidenceRating] + static final confidenceRating = + QueryIntegerProperty(_entities[13].properties[4]); + + /// see [Accuracy.notes] + static final notes = + QueryStringProperty(_entities[13].properties[5]); + + /// see [Accuracy.deleted] + static final deleted = + QueryBooleanProperty(_entities[13].properties[6]); +} + +/// [Settings] entity fields to define ObjectBox queries. +class Settings_ { + /// see [Settings.id] + static final id = QueryIntegerProperty(_entities[14].properties[0]); + + /// see [Settings.dateFormat] + static final dateFormat = + QueryStringProperty(_entities[14].properties[1]); + + /// see [Settings.longDateFormat] + static final longDateFormat = + QueryStringProperty(_entities[14].properties[2]); + + /// see [Settings.timeFormat] + static final timeFormat = + QueryStringProperty(_entities[14].properties[3]); + + /// see [Settings.longTimeFormat] + static final longTimeFormat = + QueryStringProperty(_entities[14].properties[4]); + + /// see [Settings.showConfirmationDialogOnCancel] + static final showConfirmationDialogOnCancel = + QueryBooleanProperty(_entities[14].properties[5]); + + /// see [Settings.showConfirmationDialogOnDelete] + static final showConfirmationDialogOnDelete = + QueryBooleanProperty(_entities[14].properties[6]); + + /// see [Settings.showConfirmationDialogOnStopEvent] + static final showConfirmationDialogOnStopEvent = + QueryBooleanProperty(_entities[14].properties[7]); + + /// see [Settings.nutritionMeasurementIndex] + static final nutritionMeasurementIndex = + QueryIntegerProperty(_entities[14].properties[8]); + + /// see [Settings.glucoseDisplayModeIndex] + static final glucoseDisplayModeIndex = + QueryIntegerProperty(_entities[14].properties[9]); + + /// see [Settings.glucoseMeasurementIndex] + static final glucoseMeasurementIndex = + QueryIntegerProperty(_entities[14].properties[10]); + + /// see [Settings.targetGlucoseMgPerDl] + static final targetGlucoseMgPerDl = + QueryIntegerProperty(_entities[14].properties[11]); + + /// see [Settings.targetGlucoseMmolPerL] + static final targetGlucoseMmolPerL = + QueryDoubleProperty(_entities[14].properties[12]); + + /// see [Settings.useDarkTheme] + static final useDarkTheme = + QueryBooleanProperty(_entities[14].properties[13]); +} + +/// [GlucoseTarget] entity fields to define ObjectBox queries. +class GlucoseTarget_ { + /// see [GlucoseTarget.id] + static final id = + QueryIntegerProperty(_entities[15].properties[0]); + + /// see [GlucoseTarget.deleted] + static final deleted = + QueryBooleanProperty(_entities[15].properties[1]); + + /// see [GlucoseTarget.fromMgPerDL] + static final fromMgPerDL = + QueryIntegerProperty(_entities[15].properties[2]); + + /// see [GlucoseTarget.toMgPerDl] + static final toMgPerDl = + QueryIntegerProperty(_entities[15].properties[3]); + + /// see [GlucoseTarget.fromMmolPerL] + static final fromMmolPerL = + QueryDoubleProperty(_entities[15].properties[4]); + + /// see [GlucoseTarget.toMmolPerL] + static final toMmolPerL = + QueryDoubleProperty(_entities[15].properties[5]); + + /// see [GlucoseTarget.color] + static final color = + QueryIntegerProperty(_entities[15].properties[6]); +} + +/// [Recipe] entity fields to define ObjectBox queries. +class Recipe_ { + /// see [Recipe.id] + static final id = QueryIntegerProperty(_entities[16].properties[0]); + + /// see [Recipe.deleted] + static final deleted = + QueryBooleanProperty(_entities[16].properties[1]); + + /// see [Recipe.name] + static final name = QueryStringProperty(_entities[16].properties[2]); + + /// see [Recipe.notes] + static final notes = QueryStringProperty(_entities[16].properties[3]); + + /// see [Recipe.portion] + static final portion = + QueryRelationToOne(_entities[16].properties[4]); + + /// see [Recipe.servings] + static final servings = + QueryDoubleProperty(_entities[16].properties[5]); +} + +/// [Ingredient] entity fields to define ObjectBox queries. +class Ingredient_ { + /// see [Ingredient.id] + static final id = + QueryIntegerProperty(_entities[17].properties[0]); + + /// see [Ingredient.deleted] + static final deleted = + QueryBooleanProperty(_entities[17].properties[1]); + + /// see [Ingredient.amount] + static final amount = + QueryDoubleProperty(_entities[17].properties[2]); + + /// see [Ingredient.recipe] + static final recipe = + QueryRelationToOne(_entities[17].properties[3]); + + /// see [Ingredient.ingredient] + static final ingredient = + QueryRelationToOne(_entities[17].properties[4]); } diff --git a/lib/screens/accuracy_detail.dart b/lib/screens/accuracy_detail.dart index 33cdb48..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/config.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 { @@ -22,12 +24,14 @@ class _AccuracyDetailScreenState extends State { bool _isSaving = false; final GlobalKey _accuracyForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); - final _confidenceRatingController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); - bool _forCarbsRatio = false; - bool _forPortionSize = false; + final _confidenceRatingController = + TextEditingController(text: Accuracy.getAll().length.toString()); + bool _forCarbsRatio = true; + bool _forPortionSize = true; @override void initState() { @@ -38,18 +42,39 @@ 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 ?? ''; } } - void reload() { + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + _confidenceRatingController.dispose(); + super.dispose(); + } + + void reload({String? message}) { if (widget.id != 0) { setState(() { _accuracy = Accuracy.get(widget.id); }); } _isNew = _accuracy == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); } void handleSaveAction() async { @@ -57,15 +82,18 @@ class _AccuracyDetailScreenState extends State { _isSaving = true; }); if (_accuracyForm.currentState!.validate()) { - Accuracy.box.put(Accuracy( + Accuracy accuracy = Accuracy( id: widget.id, value: _valueController.text, forCarbsRatio: _forCarbsRatio, forPortionSize: _forPortionSize, - confidenceRating: int.tryParse(_confidenceRatingController.text), notes: _notesController.text, - )); - Navigator.pop(context, '${_isNew ? 'New' : ''} Accuracy saved'); + ); + Accuracy.put(accuracy); + Accuracy.reorder( + accuracy, int.tryParse(_confidenceRatingController.text)); + Navigator.pop( + context, ['${_isNew ? 'New' : ''} Accuracy saved', accuracy]); } setState(() { _isSaving = false; @@ -73,21 +101,22 @@ class _AccuracyDetailScreenState extends State { } void handleCancelAction() { - if (showConfirmationDialogOnCancel && - (_isNew && - (_forCarbsRatio || - _forPortionSize || - _valueController.text != '' || - int.tryParse(_confidenceRatingController.text) != null || - _notesController.text != '')) || - (!_isNew && - (_forCarbsRatio != _accuracy!.forCarbsRatio || - _forPortionSize != _accuracy!.forPortionSize || - _accuracy!.value != _valueController.text || - int.tryParse(_confidenceRatingController.text) != - _accuracy!.confidenceRating || - (_accuracy!.notes ?? '') != _notesController.text))) { - Dialogs.showCancelConfirmationDialog( + if (Settings.get().showConfirmationDialogOnCancel && + (_isNew && + (!_forCarbsRatio || + !_forPortionSize || + _valueController.text != '' || + int.tryParse(_confidenceRatingController.text) != + Accuracy.getAll().length || + _notesController.text != '')) || + (!_isNew && + (_forCarbsRatio != _accuracy!.forCarbsRatio || + _forPortionSize != _accuracy!.forPortionSize || + _accuracy!.value != _valueController.text || + int.tryParse(_confidenceRatingController.text) != + _accuracy!.confidenceRating || + (_accuracy!.notes ?? '') != _notesController.text))) { + DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, @@ -104,66 +133,74 @@ class _AccuracyDetailScreenState extends State { title: Text(_isNew ? 'New Accuracy' : _accuracy!.value), ), drawer: const Navigation(currentLocation: AccuracyDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _accuracyForm, - fields: [ - TextFormField( - controller: _valueController, - decoration: const InputDecoration( - labelText: 'Name', + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _accuracyForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty name'; - } - return null; - }, - ), - StyledBooleanFormField( - value: _forCarbsRatio, - label: 'for carbs ratio', - onChanged: (value) { - setState(() { - _forCarbsRatio = value; - }); - }, - ), - StyledBooleanFormField( - value: _forPortionSize, - label: 'for portion size', - onChanged: (value) { - setState(() { - _forPortionSize = value; - }); - }, - ), - TextFormField( - controller: _confidenceRatingController, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Confidence Rating', + BooleanFormField( + value: _forCarbsRatio, + label: 'for carbs ratio', + onChanged: (value) { + setState(() { + _forCarbsRatio = value; + }); + }, ), - ), - TextFormField( - controller: _notesController, - keyboardType: TextInputType.multiline, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + BooleanFormField( + value: _forPortionSize, + label: 'for portion size', + onChanged: (value) { + setState(() { + _forPortionSize = value; + }); + }, ), - ), - ], - ), - ], + NumberFormField( + controller: _confidenceRatingController, + label: 'Confidence Rating', + onChanged: (value) { + setState(() { + _confidenceRatingController.text = + (value ?? 0).toInt().toString(); + }); + }, + ), + TextFormField( + controller: _notesController, + keyboardType: TextInputType.multiline, + decoration: const InputDecoration( + labelText: 'Notes', + ), + minLines: 2, + maxLines: 5, + ), + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, + onAction: _isSaving ? null : handleSaveAction, ), ); } diff --git a/lib/screens/accuracy_list.dart b/lib/screens/accuracy_list.dart index 8e0c4da..ad282be 100644 --- a/lib/screens/accuracy_list.dart +++ b/lib/screens/accuracy_list.dart @@ -1,5 +1,5 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.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'; import 'package:flutter/material.dart'; @@ -16,13 +16,21 @@ class AccuracyListScreen extends StatefulWidget { class _AccuracyListScreenState extends State { List _accuracies = Accuracy.getAll(); + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); - refresh(); + reload(); } - void refresh({String? message}) { + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { setState(() { _accuracies = Accuracy.getAll(); }); @@ -42,12 +50,12 @@ class _AccuracyListScreenState extends State { void onDelete(Accuracy accuracy) { Accuracy.remove(accuracy.id); - refresh(); + reload(); } void handleDeleteAction(Accuracy accuracy) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(accuracy), message: 'Are you sure you want to delete this Accuracy?', @@ -60,13 +68,13 @@ class _AccuracyListScreenState extends State { void handleToggleForPortionSizeAction(Accuracy accuracy) async { accuracy.forPortionSize = !accuracy.forPortionSize; Accuracy.put(accuracy); - refresh(); + reload(); } void handleToggleForCarbsRatioAction(Accuracy accuracy) async { accuracy.forCarbsRatio = !accuracy.forCarbsRatio; Accuracy.put(accuracy); - refresh(); + reload(); } @override @@ -75,7 +83,7 @@ class _AccuracyListScreenState extends State { appBar: AppBar( title: const Text('Accuracies'), actions: [ - IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) ], ), drawer: const Navigation(currentLocation: AccuracyListScreen.routeName), @@ -83,67 +91,83 @@ class _AccuracyListScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: _accuracies.isNotEmpty ? ListView.builder( - padding: const EdgeInsets.only(top: 10.0), - itemCount: _accuracies.length, - itemBuilder: (context, index) { - final accuracy = _accuracies[index]; - return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AccuracyDetailScreen(id: accuracy.id), - ), - ).then((message) => refresh(message: message)); - }, - title: Text(accuracy.value), - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.reorder), - onPressed: () { - // TODO: implement reordering + 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, + ), + 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), + ), + 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!'), ), - 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), - ), - const SizedBox(width: 24), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => handleDeleteAction(accuracy), - ) - ], - ), - ); - } - ) : const Center( - child: Text('You have not created any Accuracies yet!'), - ), - ), + ), ], ), floatingActionButton: FloatingActionButton( @@ -153,7 +177,7 @@ class _AccuracyListScreenState extends State { MaterialPageRoute( builder: (context) => const AccuracyDetailScreen(), ), - ).then((message) => refresh(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/screens/basal/basal_detail.dart b/lib/screens/basal/basal_detail.dart index 80f8c9a..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/config.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'; @@ -32,15 +35,18 @@ class _BasalDetailScreenState extends State { Basal? _basal; bool _isNew = true; bool _isSaving = false; + bool _isFinalRate = true; final GlobalKey _basalForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); final _startTimeController = TextEditingController(text: ''); final _endTimeController = TextEditingController(text: ''); - final _unitsController = TextEditingController(text: ''); + final _unitsController = + TextEditingController(text: 0.toStringAsPrecision(3)); @override void initState() { @@ -57,36 +63,67 @@ 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); } - updateStartTime(); - updateEndTime(); + _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); + _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); } - void reload() { + @override + void dispose() { + _scrollController.dispose(); + _startTimeController.dispose(); + _endTimeController.dispose(); + _unitsController.dispose(); + super.dispose(); + } + + void reload({String? message}) { if (widget.id != 0) { setState(() { _basal = Basal.get(widget.id); }); } _isNew = _basal == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); } - void updateStartTime() { - _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); + void updateStartTime(TimeOfDay? value) { + if (value != null) { + setState(() { + _startTime = value; + _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); + }); + } } - void updateEndTime() { - _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); + void updateEndTime(TimeOfDay? value) { + if (value != null) { + setState(() { + _endTime = value; + _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); + _isFinalRate = widget.suggestedEndTime == null || + _endTime == widget.suggestedEndTime!; + }); + } } Future validateTimePeriod() async { String? error; List basalRates = Basal.getAllForProfile(widget.basalProfileId); - // TODO use a query for the following checks instead? - // check for duplicates if (basalRates .where((other) => (widget.id != other.id) && @@ -128,7 +165,7 @@ class _BasalDetailScreenState extends State { }); } - void handleSaveAction() async { + void handleSaveAction({bool next = true}) async { setState(() { _isSaving = true; }); @@ -143,7 +180,30 @@ class _BasalDetailScreenState extends State { ); basal.basalProfile.targetId = widget.basalProfileId; Basal.put(basal); - Navigator.pop(context, '${_isNew ? 'New' : ''} Basal Rate saved'); + + if (next) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return BasalDetailScreen( + basalProfileId: widget.basalProfileId, + suggestedStartTime: _endTime, + suggestedEndTime: widget.suggestedEndTime, + ); + }, + ), + ).then((result) { + Navigator.pop( + context, + ['New Basal Rate${result[1] != null ? 's' : ''} saved', basal] + + [result[1]], + ); + }); + } else { + Navigator.pop( + context, ['${_isNew ? 'New' : ''} Basal Rate saved', basal]); + } } }); } @@ -153,21 +213,20 @@ class _BasalDetailScreenState extends State { } void handleCancelAction() { - if (showConfirmationDialogOnCancel && + if (Settings.get().showConfirmationDialogOnCancel && ((_isNew && (_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) || _endTime.hour != (widget.suggestedEndTime?.hour ?? 0) || _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!.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, @@ -185,73 +244,69 @@ class _BasalDetailScreenState extends State { '${_isNew ? 'New' : 'Edit'} Basal Rate for ${BasalProfile.get(widget.basalProfileId)?.name}'), ), drawer: const Navigation(currentLocation: BasalDetailScreen.routeName), - body: Column( - children: [ - StyledForm( - formState: _basalForm, - fields: [ - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 5), - child: StyledTimeOfDayFormField( - label: 'Start Time', - controller: _startTimeController, - time: _startTime, - onChanged: (newStartTime) { - if (newStartTime != null) { - setState(() { - _startTime = newStartTime; - }); - updateStartTime(); - } - }, + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: [ + FormWrapper( + formState: _basalForm, + fields: [ + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: TimeOfDayFormField( + label: 'Start Time', + controller: _startTimeController, + time: _startTime, + onChanged: updateStartTime, + ), + ), ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 5), - child: StyledTimeOfDayFormField( - label: 'End Time', - controller: _endTimeController, - time: _endTime, - onChanged: (newEndTime) { - if (newEndTime != null) { - setState(() { - _endTime = newEndTime; - }); - updateEndTime(); - } - }, + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5), + child: TimeOfDayFormField( + label: 'End Time', + controller: _endTimeController, + time: _endTime, + onChanged: updateEndTime, + ), + ), ), - ), + ], ), + NumberFormField( + controller: _unitsController, + label: 'Units', + suffix: 'U', + autoRoundToMultipleOfStep: true, + step: Settings.insulinSteps, + onChanged: (value) { + if (value != null) { + _unitsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + value, Settings.insulinSteps); + } + }), ], ), - 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; - }, - ), ], ), - ], + ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, + onAction: + _isSaving ? null : () => handleSaveAction(next: !_isFinalRate), + onMiddleAction: _isSaving || _isFinalRate + ? null + : () => handleSaveAction(next: false), + actionText: _isFinalRate ? 'SAVE & CLOSE' : 'NEXT', + middleActionText: 'SAVE & CLOSE', ), ); } diff --git a/lib/screens/basal/basal_list.dart b/lib/screens/basal/basal_list.dart index 96a3c5c..7685693 100644 --- a/lib/screens/basal/basal_list.dart +++ b/lib/screens/basal/basal_list.dart @@ -1,5 +1,5 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.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'; import 'package:diameter/models/basal.dart'; @@ -23,6 +23,14 @@ 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(); @@ -48,7 +56,7 @@ class _BasalListScreenState extends State { id: basal.id, ), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onDelete(Basal basal) { @@ -57,8 +65,8 @@ class _BasalListScreenState extends State { } void handleDeleteAction(Basal basal) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(basal), message: 'Are you sure you want to delete this Basal Rate?', @@ -72,7 +80,6 @@ class _BasalListScreenState extends State { List basalRates = widget.basalRates; Basal basal = basalRates[index]; - // TODO: use queries for all this // check for gaps if (index == 0 && (basal.startTime.hour != 0 || basal.startTime.minute != 0)) { @@ -105,56 +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 SingleChildScrollView( - child: Column( - children: [ - widget.basalRates.isNotEmpty ? ListView.builder( - shrinkWrap: true, - itemCount: widget.basalRates.length, - itemBuilder: (context, index) { - final basal = widget.basalRates[index]; - final error = validateTimePeriod(index); - return ListTile( - tileColor: error != null ? Colors.red.shade100 : null, - 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')), - ], - ), - subtitle: error != null - ? Text(error, style: const TextStyle(color: Colors.red)) - : Container(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, + 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, + 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 13af0bf..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/config.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'; @@ -27,14 +28,15 @@ class _BasalProfileDetailScreenState extends State { BasalProfile? _basalProfile; List _basalRates = []; bool _isNew = true; - bool _isSaving = false; final GlobalKey _basalProfileForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); late FloatingActionButton addBasalButton; late IconButton refreshButton; late IconButton closeButton; late DetailBottomRow detailBottomRow; + late DetailBottomRow detailBottomRowWhileSaving; FloatingActionButton? actionButton; List appBarActions = []; @@ -77,7 +79,13 @@ class _BasalProfileDetailScreenState extends State { detailBottomRow = DetailBottomRow( onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, + onAction: handleSaveAction, + onMiddleAction: () => handleSaveAction(close: true), + ); + + detailBottomRowWhileSaving = DetailBottomRow( + onCancel: handleCancelAction, + onAction: null, ); actionButton = null; @@ -85,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(() { @@ -146,7 +162,7 @@ class _BasalProfileDetailScreenState extends State { }); } else if (!_active && ((_isNew && _activeCount == 0) || - (_activeCount == 1 && _basalProfile!.active))) { + (!_isNew && _activeCount == 1 && _basalProfile!.active))) { await showDialog( context: context, builder: (BuildContext context) { @@ -178,25 +194,30 @@ class _BasalProfileDetailScreenState extends State { TimeOfDay? suggestedStartTime; TimeOfDay? suggestedEndTime; - _basalRates.asMap().forEach((index, basal) { - if (suggestedStartTime == null && suggestedEndTime == null) { - if (index == 0 && - (basal.startTime.hour != 0 || basal.startTime.minute != 0)) { - suggestedStartTime = const TimeOfDay(hour: 0, minute: 0); - suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime); - } else if ((index == _basalRates.length - 1) && - (basal.endTime.hour != 0 || basal.endTime.minute != 0)) { - suggestedStartTime = TimeOfDay.fromDateTime(basal.endTime); - suggestedEndTime = const TimeOfDay(hour: 0, minute: 0); - } else if (index != 0) { - var lastEndTime = _basalRates[index - 1].endTime; - if (basal.startTime.isAfter(lastEndTime)) { - suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime); + if (_basalRates.isEmpty) { + suggestedStartTime = const TimeOfDay(hour: 0, minute: 0); + suggestedEndTime = const TimeOfDay(hour: 0, minute: 0); + } else { + _basalRates.asMap().forEach((index, basal) { + if (suggestedStartTime == null && suggestedEndTime == null) { + if (index == 0 && + (basal.startTime.hour != 0 || basal.startTime.minute != 0)) { + suggestedStartTime = const TimeOfDay(hour: 0, minute: 0); suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime); + } else if ((index == _basalRates.length - 1) && + (basal.endTime.hour != 0 || basal.endTime.minute != 0)) { + suggestedStartTime = TimeOfDay.fromDateTime(basal.endTime); + suggestedEndTime = const TimeOfDay(hour: 0, minute: 0); + } else if (index != 0) { + var lastEndTime = _basalRates[index - 1].endTime; + if (basal.startTime.isAfter(lastEndTime)) { + suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime); + suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime); + } } } - } - }); + }); + } Navigator.push( context, @@ -209,30 +230,47 @@ class _BasalProfileDetailScreenState extends State { ); }, ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } - void handleSaveAction() async { + void handleSaveAction({bool close = false}) async { setState(() { - _isSaving = true; + bottomNav = detailBottomRowWhileSaving; }); if (_basalProfileForm.currentState!.validate()) { await checkActiveProfiles(); - BasalProfile.put(BasalProfile( + BasalProfile basalProfile = BasalProfile( id: widget.id, name: _nameController.text, active: _active, notes: _notesController.text, - )); - Navigator.pop(context, '${_isNew ? 'New' : ''} Basal Profile saved'); + ); + BasalProfile.put(basalProfile); + + if (close) { + Navigator.pop(context, + ['${_isNew ? 'New' : ''} Basal Profile saved', basalProfile]); + } else { + if (_isNew) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + BasalProfileDetailScreen(id: basalProfile.id), + ), + ).then((result) => Navigator.pop(context, result)); + } else { + reload(message: 'Basal Profile saved'); + } + } } setState(() { - _isSaving = false; + bottomNav = detailBottomRow; }); } void handleCancelAction() { - if (showConfirmationDialogOnCancel && + if (Settings.get().showConfirmationDialogOnCancel && (_isNew && (_active != widget.active || _nameController.text != '' || @@ -241,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, @@ -279,52 +317,59 @@ class _BasalProfileDetailScreenState extends State { renderTabButtons(tabController.index); }); List tabs = [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _basalProfileForm, - fields: [ - TextFormField( - controller: _nameController, - decoration: const InputDecoration( - labelText: 'Name', + Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _basalProfileForm, + fields: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty title'; + } + return null; + }, ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty title'; - } - return null; - }, - ), - TextFormField( - keyboardType: TextInputType.multiline, - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - suffixText: '', - alignLabelWithHint: true, + TextFormField( + keyboardType: TextInputType.multiline, + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + minLines: 2, + maxLines: 5, ), - ), - StyledBooleanFormField( - value: _active, - onChanged: (value) { - setState(() { - _active = value; - }); - }, - label: 'active', - ), - ], - ), - ], + BooleanFormField( + value: _active, + onChanged: (value) { + setState(() { + _active = value; + }); + }, + label: 'active', + ), + ], + ), + ], + ), ), ), ]; if (!_isNew) { - tabs.add(BasalListScreen(basalProfile: _basalProfile!, basalRates: _basalRates, reload: reload)); + tabs.add(BasalListScreen( + basalProfile: _basalProfile!, + basalRates: _basalRates, + reload: reload)); } return Scaffold( diff --git a/lib/screens/basal/basal_profile_list.dart b/lib/screens/basal/basal_profile_list.dart new file mode 100644 index 0000000..cfa561f --- /dev/null +++ b/lib/screens/basal/basal_profile_list.dart @@ -0,0 +1,278 @@ +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'; +import 'package:flutter/material.dart'; +import 'package:diameter/models/basal_profile.dart'; +import 'package:diameter/screens/basal/basal_profile_detail.dart'; + +class BasalProfileListScreen extends StatefulWidget { + static const String routeName = '/basal-profiles'; + const BasalProfileListScreen({Key? key}) : super(key: key); + + @override + _BasalProfileListScreenState createState() => _BasalProfileListScreenState(); +} + +class _BasalProfileListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + late List _basalProfiles; + Widget banner = Container(); + + final BasalProfile? _activeProfile = BasalProfile.getActive(DateTime.now()); + + @override + void initState() { + super.initState(); + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { + setState(() { + _basalProfiles = BasalProfile.getAll(); + }); + updateBanner(); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void updateBanner() { + int activeProfileCount = BasalProfile.activeCount(); + setState(() { + banner = activeProfileCount != 1 + ? MaterialBanner( + content: Text(activeProfileCount == 0 + ? 'You currently do not have an active Basal Profile.' + : 'More than one active Basal Profile has been found.'), + leading: const CircleAvatar(child: Icon(Icons.warning)), + forceActionsBelow: true, + actions: activeProfileCount == 0 + ? [ + _basalProfiles.isNotEmpty + ? TextButton( + child: const Text('ACTIVATE A PROFILE'), + onPressed: handlePickActiveProfileAction, + ) + : Container(), + TextButton( + child: const Text('CREATE A NEW PROFILE'), + onPressed: () => onNew(true), + ), + ] + : [ + TextButton( + child: const Text('PICK A PROFILE'), + onPressed: handlePickActiveProfileAction, + ), + ], + ) + : Container(); + }); + } + + 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); + reload(message: 'Basal Profile deleted'); + } + + void handleDeleteAction(BasalProfile basalProfile) async { + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(basalProfile), + message: 'Are you sure you want to delete this Basal Profile?', + ); + } else { + onDelete(basalProfile); + } + } + + void onPickActive(BasalProfile? basalProfile) { + if (basalProfile != null) { + BasalProfile.setAllInactive; + basalProfile.active = true; + BasalProfile.put(basalProfile); + reload( + message: '${basalProfile.name} has been set as your active Profile'); + } + } + + void handlePickActiveProfileAction() { + setState(() { + banner = MaterialBanner( + content: AutoCompleteDropdownButton( + controller: TextEditingController(text: ''), + items: _basalProfiles, + label: 'Default Basal Profile', + onChanged: onPickActive, + ), + leading: const CircleAvatar(child: Icon(Icons.info)), + forceActionsBelow: true, + actions: [ + TextButton( + child: const Text('CREATE A NEW PROFILE INSTEAD'), + onPressed: () => onNew(true), + ), + ], + ); + }); + } + + void showDetailScreen({BasalProfile? basalProfile, bool active = false}) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + BasalProfileDetailScreen(id: basalProfile?.id ?? 0, active: active), + ), + ).then((result) => reload(message: result?[0])); + } + + void onNew(bool active) { + showDetailScreen(active: active); + } + + void onEdit(BasalProfile basalProfile) { + showDetailScreen(basalProfile: basalProfile); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Basal Profiles'), + actions: [ + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) + ], + ), + drawer: + const Navigation(currentLocation: BasalProfileListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + banner, + Expanded( + child: _basalProfiles.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + padding: const EdgeInsets.all(10.0), + controller: _scrollController, + itemCount: _basalProfiles.length, + itemBuilder: (context, index) { + final basalProfile = _basalProfiles[index]; + double dailyTotal = + Basal.getDailyTotalForProfile(basalProfile.id); + String activeProfileText = basalProfile.active + ? ' (Default Profile)' + : basalProfile.id == _activeProfile?.id + ? ' (Current Active Profile)' + : ''; + return Card( + child: ListTile( + isThreeLine: true, + selected: basalProfile.active || + basalProfile.id == _activeProfile?.id, + onTap: () => onEdit(basalProfile), + title: Text( + 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 ?? ''), + Expanded( + child: Column( + children: dailyTotal > 0 + ? [ + Text(dailyTotal + .toStringAsPrecision(3)), + const Text('U/day', + textScaleFactor: 0.75), + ] + : [], + ), + ), + ], + ), + ), + 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), + ), + ], + ), + ), + ); + }, + ), + ) + : const Center( + child: Text('You have not created any Basal Profiles yet!'), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => onNew(false), + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/basal/basal_profiles_list.dart b/lib/screens/basal/basal_profiles_list.dart deleted file mode 100644 index c2be7a7..0000000 --- a/lib/screens/basal/basal_profiles_list.dart +++ /dev/null @@ -1,203 +0,0 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/navigation.dart'; -import 'package:flutter/material.dart'; -// import 'package:diameter/components/progress_indicator.dart'; -import 'package:diameter/models/basal_profile.dart'; -import 'package:diameter/screens/basal/basal_profile_detail.dart'; - -class BasalProfileListScreen extends StatefulWidget { - static const String routeName = '/basal-profiles'; - const BasalProfileListScreen({Key? key}) : super(key: key); - - @override - _BasalProfileListScreenState createState() => _BasalProfileListScreenState(); -} - -class _BasalProfileListScreenState extends State { - late List _basalProfiles; - Widget banner = Container(); - bool pickActiveProfileMode = false; - - void refresh({String? message}) { - setState(() { - pickActiveProfileMode = false; - _basalProfiles = BasalProfile.getAll(); - }); - // _basalProfiles.then((list) => - updateBanner(); - setState(() { - if (message != null) { - var snackBar = SnackBar( - content: Text(message), - duration: const Duration(seconds: 2), - ); - ScaffoldMessenger.of(context) - ..removeCurrentSnackBar() - ..showSnackBar(snackBar); - } - }); - } - - void updateBanner() { - int activeProfileCount = BasalProfile.activeCount(); - setState(() { - banner = activeProfileCount != 1 - ? MaterialBanner( - content: Text(activeProfileCount == 0 - ? 'You currently do not have an active Basal Profile.' - : 'More than one active Basal Profile has been found.'), - leading: const CircleAvatar(child: Icon(Icons.warning)), - forceActionsBelow: true, - actions: activeProfileCount == 0 - ? [ - _basalProfiles.isNotEmpty - ? TextButton( - child: const Text('ACTIVATE A PROFILE'), - onPressed: handlePickActiveProfileAction, - ) - : Container(), - TextButton( - child: const Text('CREATE A NEW PROFILE'), - onPressed: () => onNew(true), - ), - ] - : [ - TextButton( - child: const Text('PICK A PROFILE'), - onPressed: handlePickActiveProfileAction, - ), - ], - ) - : Container(); - }); - } - - void onDelete(BasalProfile basalProfile) { - BasalProfile.remove(basalProfile.id); - refresh(message: 'Basal Profile deleted'); - } - - void handleDeleteAction(BasalProfile basalProfile) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( - context: context, - onConfirm: () => onDelete(basalProfile), - message: 'Are you sure you want to delete this Basal Profile?', - ); - } else { - onDelete(basalProfile); - } - } - - void onPickActive(BasalProfile basalProfile) { - BasalProfile.setAllInactive; - basalProfile.active = true; - BasalProfile.put(basalProfile); - // (exception: basalProfile.objectId!).then((_) => - refresh(message: '${basalProfile.name} has been set as your active Profile'); - } - - void handlePickActiveProfileAction() { - setState(() { - banner = MaterialBanner( - content: const Text('Click one of the profiles to active it.'), - leading: const CircleAvatar(child: Icon(Icons.info)), - forceActionsBelow: true, - actions: [ - TextButton( - child: const Text('CREATE A NEW PROFILE INSTEAD'), - onPressed: () => onNew(true), - ), - ], - ); - pickActiveProfileMode = true; - }); - } - - void showDetailScreen({BasalProfile? basalProfile, bool active = false}) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BasalProfileDetailScreen( - id: basalProfile?.id ?? 0, active: active), - ), - ).then((message) => refresh(message: message)); - } - - void onNew(bool active) { - showDetailScreen(active: active); - } - - void onEdit(BasalProfile basalProfile) { - 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)) - ], - ), - drawer: - const Navigation(currentLocation: BasalProfileListScreen.routeName), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - banner, - Expanded( - child: _basalProfiles.isNotEmpty ? ListView.builder( - itemCount: _basalProfiles.length, - itemBuilder: (context, index) { - final basalProfile = _basalProfiles[index]; - - return ListTile( - tileColor: basalProfile.active - ? Colors.green.shade100 - : null, - onTap: () { - pickActiveProfileMode - ? onPickActive(basalProfile) - : onEdit(basalProfile); - }, - title: Text( - basalProfile.name, - ), - subtitle: Text(basalProfile.notes!), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => - handleDeleteAction(basalProfile), - ), - ], - ), - ); - }, - ) : const Center( - child: Text('You have not created any Basal Profiles yet!'), - ), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () => onNew(false), - child: const Icon(Icons.add), - ), - ); - } -} diff --git a/lib/screens/bolus/bolus_detail.dart b/lib/screens/bolus/bolus_detail.dart index 917f7e7..fb93f54 100644 --- a/lib/screens/bolus/bolus_detail.dart +++ b/lib/screens/bolus/bolus_detail.dart @@ -1,12 +1,13 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.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/settings.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'; @@ -34,18 +35,20 @@ class _BolusDetailScreenState extends State { Bolus? _bolus; bool _isNew = true; bool _isSaving = false; + bool _isFinalRate = true; final GlobalKey _bolusForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); 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() { @@ -61,41 +64,75 @@ class _BolusDetailScreenState extends State { if (_bolus != null) { _startTime = TimeOfDay.fromDateTime(_bolus!.startTime); + _endTime = TimeOfDay.fromDateTime(_bolus!.endTime); _unitsController.text = _bolus!.units.toString(); _carbsController.text = _bolus!.carbs.toString(); - _mgPerDlController.text = _bolus!.mgPerDl.toString(); - _mmolPerLController.text = _bolus!.mmolPerL.toString(); + _mgPerDlController.text = (_bolus!.mgPerDl ?? '').toString(); + _mmolPerLController.text = (_bolus!.mmolPerL ?? '').toString(); } - - updateStartTime(); - updateEndTime(); + _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); + _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); } - void reload() { + @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(() { _bolus = Bolus.get(widget.id); }); } _isNew = _bolus == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); } - void updateStartTime() { - _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); + void updateStartTime(TimeOfDay? value) { + if (value != null) { + setState(() { + _startTime = value; + _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); + }); + } } - void updateEndTime() { - _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); + void updateEndTime(TimeOfDay? value) { + if (value != null) { + setState(() { + _endTime = value; + _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); + _isFinalRate = widget.suggestedEndTime == null || + _endTime == widget.suggestedEndTime!; + }); + } } Future validateTimePeriod() async { String? error; List bolusRates = Bolus.getAllForProfile(widget.bolusProfileId); - // BolusProfile.get(widget.bolusProfileId)?.bolusRates ?? []; - // TODO use a query for the following checks instead? // check for duplicates if (bolusRates .where((other) => @@ -138,7 +175,7 @@ class _BolusDetailScreenState extends State { }); } - void handleSaveAction() async { + void handleSaveAction({bool next = true}) async { setState(() { _isSaving = true; }); @@ -153,11 +190,34 @@ class _BolusDetailScreenState extends State { units: double.tryParse(_unitsController.text) ?? 0, carbs: double.tryParse(_carbsController.text) ?? 0, mgPerDl: int.tryParse(_mgPerDlController.text), - mmolPerL: double.parse(_mmolPerLController.text), + mmolPerL: double.tryParse(_mmolPerLController.text), ); bolus.bolusProfile.targetId = widget.bolusProfileId; Bolus.put(bolus); - Navigator.pop(context, '${_isNew ? 'New' : ''} Bolus Rate saved'); + + if (next) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return BolusDetailScreen( + bolusProfileId: widget.bolusProfileId, + suggestedStartTime: _endTime, + suggestedEndTime: widget.suggestedEndTime, + ); + }, + ), + ).then((result) { + Navigator.pop( + context, + ['New Bolus Rate${result[1] != null ? 's' : ''} saved', bolus] + + [result[1]], + ); + }); + } else { + Navigator.pop( + context, ['${_isNew ? 'New' : ''} Bolus Rate saved', bolus]); + } } }); } @@ -168,7 +228,7 @@ class _BolusDetailScreenState extends State { } void handleCancelAction() { - if (showConfirmationDialogOnCancel && + if (Settings.get().showConfirmationDialogOnCancel && ((_isNew && (_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) || _endTime.hour != (widget.suggestedEndTime?.hour ?? 0) || @@ -190,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, @@ -200,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(); + }); + } } } @@ -235,172 +291,127 @@ class _BolusDetailScreenState extends State { '${_isNew ? 'New' : 'Edit'} Bolus Rate for ${BolusProfile.get(widget.bolusProfileId)?.name}'), ), drawer: const Navigation(currentLocation: BolusDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - children: [ - StyledForm( - formState: _bolusForm, - fields: [ - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 5), - child: StyledTimeOfDayFormField( - label: 'Start Time', - controller: _startTimeController, - time: _startTime, - onChanged: (newStartTime) { - if (newStartTime != null) { - setState(() { - _startTime = newStartTime; - }); - updateStartTime(); - } - }, + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: [ + FormWrapper( + formState: _bolusForm, + fields: [ + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: TimeOfDayFormField( + label: 'Start Time', + controller: _startTimeController, + time: _startTime, + onChanged: updateStartTime, + ), ), ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 5), - child: StyledTimeOfDayFormField( - label: 'End Time', - controller: _endTimeController, - time: _endTime, - onChanged: (newEndTime) { - if (newEndTime != null) { - setState(() { - _endTime = newEndTime; - }); - updateEndTime(); - } - }, + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5), + child: TimeOfDayFormField( + label: 'End Time', + controller: _endTimeController, + time: _endTime, + onChanged: updateEndTime, + ), ), ), - ), - ], - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Units', - suffixText: 'U', + ], ), - controller: _unitsController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty amount of units'; - } - return null; - }, - ), - TextFormField( - decoration: InputDecoration( - labelText: 'per carbs', - suffixText: nutritionMeasurement == - NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == NutritionMeasurement.ounces - ? 'oz' - : '', + NumberFormField( + controller: _unitsController, + label: 'Units', + suffix: 'U', + autoRoundToMultipleOfStep: true, + step: Settings.insulinSteps, + onChanged: (value) { + if (value != null) { + _unitsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + value, Settings.insulinSteps); + } + }, ), - controller: _carbsController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value!.trim().isEmpty) { - return 'How many carbs does the rate make up for?'; - } - return null; - }, - ), - Row( - children: [ - glucoseMeasurement == GlucoseMeasurement.mgPerDl || - glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'per mg/dl', - suffixText: 'mg/dl', + NumberFormField( + controller: _carbsController, + label: 'per carbs', + suffix: Settings.nutritionMeasurementSuffix, + autoRoundToMultipleOfStep: true, + step: Settings.nutritionSteps, + onChanged: (value) { + if (value != null) { + _carbsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + value, Settings.nutritionSteps); + } + }, + ), + Row( + children: [ + Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? Expanded( + 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, ), - controller: _mgPerDlController, - onChanged: (_) => - 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; - }, - ), - ) - : Container(), - glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == + ) + : Container(), + Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL || + [ + GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail - ? IconButton( - onPressed: () => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mmolPerL), - icon: const Icon(Icons.calculate), - ) - : Container(), - glucoseMeasurement == GlucoseMeasurement.mmolPerL || - glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'per mmol/l', - suffixText: 'mmol/l', + ].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, + step: Settings.mmolPerLSteps, + onChanged: convertBetweenMgPerDlAndMmolPerL, ), - controller: _mmolPerLController, - onChanged: (_) => - 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; - }, - ), - ) - : Container(), - glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? IconButton( - onPressed: () => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mgPerDl), - icon: const Icon(Icons.calculate), - ) - : Container(), - ], - ), - ], - ), - ], + ) + : Container(), + ], + ), + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, + onAction: + _isSaving ? null : () => handleSaveAction(next: !_isFinalRate), + onMiddleAction: _isSaving || _isFinalRate + ? null + : () => handleSaveAction(next: false), + actionText: _isFinalRate ? 'SAVE & CLOSE' : 'NEXT', + middleActionText: 'SAVE & CLOSE', ), ); } diff --git a/lib/screens/bolus/bolus_list.dart b/lib/screens/bolus/bolus_list.dart index 2c2576e..828719a 100644 --- a/lib/screens/bolus/bolus_list.dart +++ b/lib/screens/bolus/bolus_list.dart @@ -1,6 +1,5 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/settings.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'; import 'package:diameter/models/bolus.dart'; @@ -13,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 @@ -21,6 +23,14 @@ 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(); @@ -46,7 +56,7 @@ class _BolusListScreenState extends State { id: bolus.id, ), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onDelete(Bolus bolus) { @@ -55,8 +65,8 @@ class _BolusListScreenState extends State { } void handleDeleteAction(Bolus bolus) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(bolus), message: 'Are you sure you want to delete this Bolus Rate?', @@ -70,8 +80,6 @@ class _BolusListScreenState extends State { List bolusRates = widget.bolusRates; Bolus bolus = bolusRates[index]; - // TODO: use queries for all this - // check for gaps if (index == 0 && (bolus.startTime.toLocal().hour != 0 || bolus.startTime.minute != 0)) { return 'First Bolus of the day needs to start at 00:00'; @@ -103,58 +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 SingleChildScrollView( - padding: const EdgeInsets.only(top: 10.0), - child: Column( - children: [ - widget.bolusRates.isNotEmpty ? ListView.builder( - shrinkWrap: true, - itemCount: widget.bolusRates.length, - itemBuilder: (context, index) { - final bolus = widget.bolusRates[index]; - final error = validateTimePeriod(index); - return ListTile( - isThreeLine: true, - tileColor: error != null ? Colors.red.shade100 : null, - onTap: () { - handleEditAction(bolus); - }, - title: Text( - '${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}'), - subtitle: Column( - mainAxisSize: MainAxisSize.max, - 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: [ - Text( - '${bolus.units} U per ${bolus.carbs}${nutritionMeasurement == NutritionMeasurement.grams ? ' g' : ' oz'} carbs/${glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDl : bolus.mmolPerL} ${glucoseMeasurement == GlucoseMeasurement.mgPerDl ? 'mg/dl' : 'mmol/l'}'), error != null - ? Text(error, - style: const TextStyle(color: Colors.red)) - : const Text('') - ]), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, + ? 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), + ] + : [], + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(bolus), + ), + ], + ), ), - 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 4e87dd4..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/config.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'; @@ -26,14 +27,15 @@ class _BolusProfileDetailScreenState extends State { BolusProfile? _bolusProfile; List _bolusRates = []; bool _isNew = true; - bool _isSaving = false; final GlobalKey _bolusProfileForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); late FloatingActionButton addBolusButton; late IconButton refreshButton; late IconButton closeButton; late DetailBottomRow detailBottomRow; + late DetailBottomRow detailBottomRowWhileSaving; FloatingActionButton? actionButton; List appBarActions = []; @@ -73,10 +75,15 @@ class _BolusProfileDetailScreenState extends State { icon: const Icon(Icons.close), ); - // TODO: fix (saving button doesnt get disabled) detailBottomRow = DetailBottomRow( onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, + onAction: handleSaveAction, + onMiddleAction: () => handleSaveAction(close: true), + ); + + detailBottomRowWhileSaving = DetailBottomRow( + onCancel: handleCancelAction, + onAction: null, ); actionButton = null; @@ -84,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(() { @@ -144,7 +159,7 @@ class _BolusProfileDetailScreenState extends State { }); } else if (!_active && ((_isNew && _activeCount == 0) || - (_activeCount == 1 && _bolusProfile!.active))) { + (!_isNew && _activeCount == 1 && _bolusProfile!.active))) { await showDialog( context: context, builder: (BuildContext context) { @@ -176,25 +191,30 @@ class _BolusProfileDetailScreenState extends State { TimeOfDay? suggestedStartTime; TimeOfDay? suggestedEndTime; - _bolusRates.asMap().forEach((index, bolus) { - if (suggestedStartTime == null && suggestedEndTime == null) { - if (index == 0 && - (bolus.startTime.hour != 0 || bolus.startTime.minute != 0)) { - suggestedStartTime = const TimeOfDay(hour: 0, minute: 0); - suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime); - } else if ((index == _bolusRates.length - 1) && - (bolus.endTime.hour != 0 || bolus.endTime.minute != 0)) { - suggestedStartTime = TimeOfDay.fromDateTime(bolus.endTime); - suggestedEndTime = const TimeOfDay(hour: 0, minute: 0); - } else if (index != 0) { - var lastEndTime = _bolusRates[index - 1].endTime; - if (bolus.startTime.isAfter(lastEndTime)) { - suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime); + if (_bolusRates.isEmpty) { + suggestedStartTime = const TimeOfDay(hour: 0, minute: 0); + suggestedEndTime = const TimeOfDay(hour: 0, minute: 0); + } else { + _bolusRates.asMap().forEach((index, bolus) { + if (suggestedStartTime == null && suggestedEndTime == null) { + if (index == 0 && + (bolus.startTime.hour != 0 || bolus.startTime.minute != 0)) { + suggestedStartTime = const TimeOfDay(hour: 0, minute: 0); suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime); + } else if ((index == _bolusRates.length - 1) && + (bolus.endTime.hour != 0 || bolus.endTime.minute != 0)) { + suggestedStartTime = TimeOfDay.fromDateTime(bolus.endTime); + suggestedEndTime = const TimeOfDay(hour: 0, minute: 0); + } else if (index != 0) { + var lastEndTime = _bolusRates[index - 1].endTime; + if (bolus.startTime.isAfter(lastEndTime)) { + suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime); + suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime); + } } } - } - }); + }); + } Navigator.push( context, @@ -207,34 +227,49 @@ class _BolusProfileDetailScreenState extends State { ); }, ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } - void handleSaveAction() async { + void handleSaveAction({bool close = false}) async { setState(() { - _isSaving = true; + bottomNav = detailBottomRowWhileSaving; }); if (_bolusProfileForm.currentState!.validate()) { await checkActiveProfiles(); - BolusProfile.put( - BolusProfile( - id: widget.id, - name: _nameController.text, - active: _active, - notes: _notesController.text, - ), + BolusProfile bolusProfile = BolusProfile( + id: widget.id, + name: _nameController.text, + active: _active, + notes: _notesController.text, ); - Navigator.pop(context, '${_isNew ? 'New' : ''} Bolus Profile saved'); + BolusProfile.put(bolusProfile); + + if (close) { + Navigator.pop(context, + ['${_isNew ? 'New' : ''} Bolus Profile saved', bolusProfile]); + } else { + if (_isNew) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + BolusProfileDetailScreen(id: bolusProfile.id), + ), + ).then((result) => Navigator.pop(context, result)); + } else { + reload(message: 'Bolus Profile saved'); + } + } } setState(() { - _isSaving = false; + bottomNav = detailBottomRow; }); - } + } void handleCancelAction() { - if (showConfirmationDialogOnCancel && + if (Settings.get().showConfirmationDialogOnCancel && (_isNew && (_active != widget.active || _nameController.text != '' || @@ -243,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, @@ -282,52 +317,59 @@ class _BolusProfileDetailScreenState extends State { }); List tabs = [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _bolusProfileForm, - fields: [ - TextFormField( - controller: _nameController, - decoration: const InputDecoration( - labelText: 'Name', + Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _bolusProfileForm, + fields: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty title'; + } + return null; + }, ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty title'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + TextFormField( + decoration: const InputDecoration( + labelText: 'Notes', + ), + controller: _notesController, + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, ), - controller: _notesController, - keyboardType: TextInputType.multiline, - ), - StyledBooleanFormField( - value: _active, - onChanged: (value) { - setState(() { - _active = value; - }); - }, - label: 'active', - ), - ], - ), - ], + BooleanFormField( + value: _active, + onChanged: (value) { + setState(() { + _active = value; + }); + }, + label: 'active', + ), + ], + ), + ], + ), ), ), ]; if (!_isNew) { - tabs.add( - BolusListScreen(bolusProfile: _bolusProfile!, bolusRates: _bolusRates, reload: reload)); + tabs.add(BolusListScreen( + bolusProfile: _bolusProfile!, + bolusRates: _bolusRates, + reload: reload)); } return Scaffold( diff --git a/lib/screens/bolus/bolus_profile_list.dart b/lib/screens/bolus/bolus_profile_list.dart index d6579a9..2b9a617 100644 --- a/lib/screens/bolus/bolus_profile_list.dart +++ b/lib/screens/bolus/bolus_profile_list.dart @@ -1,5 +1,7 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.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'; import 'package:diameter/models/bolus_profile.dart'; @@ -14,18 +16,32 @@ class BolusProfileListScreen extends StatefulWidget { } class _BolusProfileListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + List _bolusProfiles = []; Widget banner = Container(); - bool pickActiveProfileMode = false; + + 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(() { - pickActiveProfileMode = false; _bolusProfiles = BolusProfile.getAll(); }); updateBanner(); - + setState(() { if (message != null) { var snackBar = SnackBar( @@ -74,14 +90,38 @@ 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'); } void handleDeleteAction(BolusProfile bolusProfile) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(bolusProfile), message: 'Are you sure you want to delete this Bolus Profile?', @@ -91,18 +131,25 @@ class _BolusProfileListScreenState extends State { } } - void onPickActive(BolusProfile bolusProfile) { - BolusProfile.setAllInactive; - bolusProfile.active = true; - BolusProfile.put(bolusProfile); - reload( - message: '${bolusProfile.name} has been set as your active Profile'); + void onPickActive(BolusProfile? bolusProfile) { + if (bolusProfile != null) { + BolusProfile.setAllInactive; + bolusProfile.active = true; + BolusProfile.put(bolusProfile); + reload( + message: '${bolusProfile.name} has been set as your active Profile'); + } } void handlePickActiveProfileAction() { setState(() { banner = MaterialBanner( - content: const Text('Click one of the profiles to active it.'), + content: AutoCompleteDropdownButton( + controller: TextEditingController(text: ''), + items: _bolusProfiles, + label: 'Default Basal Profile', + onChanged: onPickActive, + ), leading: const CircleAvatar(child: Icon(Icons.info)), forceActionsBelow: true, actions: [ @@ -112,7 +159,6 @@ class _BolusProfileListScreenState extends State { ), ], ); - pickActiveProfileMode = true; }); } @@ -120,10 +166,10 @@ class _BolusProfileListScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => BolusProfileDetailScreen( - id: bolusProfile?.id ?? 0, active: active), + builder: (context) => + BolusProfileDetailScreen(id: bolusProfile?.id ?? 0, active: active), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onNew(bool active) { @@ -134,12 +180,6 @@ class _BolusProfileListScreenState extends State { showDetailScreen(bolusProfile: bolusProfile); } - @override - void initState() { - super.initState(); - reload(); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -156,42 +196,63 @@ class _BolusProfileListScreenState extends State { children: [ banner, Expanded( - child: _bolusProfiles.isNotEmpty ? ListView.builder( - itemCount: _bolusProfiles.length, - itemBuilder: (context, index) { - final bolusProfile = _bolusProfiles[index]; - return ListTile( - tileColor: bolusProfile.active - ? Colors.green.shade100 - : null, - onTap: () { - // TODO: make pick active profile visually distinct - pickActiveProfileMode - ? onPickActive(bolusProfile) - : onEdit(bolusProfile); - }, - title: Text( - bolusProfile.name, + child: _bolusProfiles.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + padding: const EdgeInsets.all(10.0), + controller: _scrollController, + itemCount: _bolusProfiles.length, + itemBuilder: (context, index) { + final bolusProfile = _bolusProfiles[index]; + String activeProfileText = bolusProfile.active + ? ' (Default Profile)' + : bolusProfile.id == _activeProfile?.id + ? ' (Current Active Profile)' + : ''; + return Card( + child: ListTile( + selected: bolusProfile.active || + bolusProfile.id == _activeProfile?.id, + onTap: () => onEdit(bolusProfile), + title: Text( + bolusProfile.name.toUpperCase() + + activeProfileText, + style: Theme.of(context).textTheme.subtitle2, + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Text(bolusProfile.notes ?? ''), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.copy, + color: Colors.blue, + ), + onPressed: () => + handleDuplicateAction(bolusProfile), + ), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => + handleDeleteAction(bolusProfile), + ), + ], + ), + ), + ); + }, + ), + ) + : const Center( + child: Text('You have not created any Bolus Profiles yet!'), ), - subtitle: Text(bolusProfile.notes!), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => - handleDeleteAction(bolusProfile), - ), - ], - ), - ); - }, - ) : const Center( - child: Text('You have not created any Bolus Profiles yet!'), - ), ), ], ), diff --git a/lib/screens/log/active_log_event_list.dart b/lib/screens/log/active_log_event_list.dart deleted file mode 100644 index b16c82d..0000000 --- a/lib/screens/log/active_log_event_list.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/models/log_entry.dart'; -import 'package:diameter/models/log_event.dart'; -// import 'package:diameter/models/log_event_type.dart'; -import 'package:diameter/screens/log/log_event_detail.dart'; -import 'package:diameter/utils/date_time_utils.dart'; -import 'package:flutter/material.dart'; -// import 'package:diameter/components/progress_indicator.dart'; - -class ActiveLogEventListScreen extends StatefulWidget { - static const String routeName = '/active-log-events'; - - final LogEntry? endLogEntry; - final Function()? onSetEndTime; - - const ActiveLogEventListScreen( - {Key? key, this.endLogEntry, this.onSetEndTime}) - : super(key: key); - - @override - _ActiveLogEventListScreenState createState() => - _ActiveLogEventListScreenState(); -} - -class _ActiveLogEventListScreenState extends State { - List _activeLogEvents = []; - - void refresh({String? message}) { - setState(() { - _activeLogEvents = LogEvent.getAllOngoing(); - }); - - setState(() { - if (message != null) { - var snackBar = SnackBar( - content: Text(message), - duration: const Duration(seconds: 2), - ); - ScaffoldMessenger.of(context) - ..removeCurrentSnackBar() - ..showSnackBar(snackBar); - } - }); - } - - void onStop(LogEvent event) async { - event.endTime = DateTime.now(); - event.endLogEntry.target = - widget.endLogEntry ?? LogEntry(time: DateTime.now()); - LogEvent.put(event); - refresh(); - if (widget.onSetEndTime != null) { - widget.onSetEndTime!(); - } - } - - void handleStopAction(LogEvent event) async { - if (showConfirmationDialogOnStopEvent) { - Dialogs.showConfirmationDialog( - context: context, - onConfirm: () => onStop(event), - message: 'Are you sure you want to end this Event?', - ); - } else { - onStop(event); - } - } - - void onDelete(LogEvent event) { - LogEvent.remove(event.id); - refresh(message: 'Event deleted'); - } - - void handleDeleteAction(LogEvent event) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( - context: context, - onConfirm: () => onDelete(event), - message: 'Are you sure you want to delete this Event?', - ); - } else { - onDelete(event); - } - } - - @override - void initState() { - super.initState(); - refresh(); - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.only(top: 10.0), - child: Column( - children: [ - // TODO: make action button instead of appbar - AppBar( - title: const Text('Active Events'), - primary: false, - automaticallyImplyLeading: false, - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LogEventDetailScreen( - logEntry: widget.endLogEntry!, - ), - ), - ).then((message) => refresh(message: message)); - }, - ), - IconButton(icon: const Icon(Icons.refresh), onPressed: refresh), - ], - ), - _activeLogEvents.isNotEmpty ? - ListView.builder( - shrinkWrap: true, - itemCount: _activeLogEvents.length, - itemBuilder: (context, index) { - final event = _activeLogEvents[index]; - return ListTile( - title: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Text(event.eventType.target?.value ?? ''), - ), - ], - ), - subtitle: Text( - '${DateTimeUtils.displayDateTime(event.time)}${event.hasEndTime ? ' - ${DateTimeUtils.displayDateTime(event.endTime)}' : ''}'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => handleStopAction(event), - ), - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => handleDeleteAction(event), - ), - ], - ), - ); - }, - ) : const Center( - child: Text('There are no currently ongoing events!'), - ), - ], - ), - ); - } -} diff --git a/lib/screens/log/log.dart b/lib/screens/log/log.dart index 910477d..c1d9b5e 100644 --- a/lib/screens/log/log.dart +++ b/lib/screens/log/log.dart @@ -1,10 +1,14 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.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'; +import 'package:diameter/models/log_meal.dart'; +import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; -import 'package:diameter/screens/log/log_entry.dart'; +import 'package:diameter/screens/log/log_entry/log_entry.dart'; import 'package:diameter/utils/date_time_utils.dart'; import 'package:flutter/material.dart'; +import 'dart:math' as math; class LogScreen extends StatefulWidget { static const String routeName = '/log'; @@ -15,11 +19,33 @@ class LogScreen extends StatefulWidget { } class _LogScreenState extends State { - late Map> _logEntryDailyMap; + late List _logEntries; - void refresh({String? message}) { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _dateController = TextEditingController(text: ''); + + late DateTime _date; + + @override + void initState() { + super.initState(); + + _date = DateTime.now(); + _dateController.text = DateTimeUtils.displayDate(_date); + + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + _dateController.dispose(); + super.dispose(); + } + + void reload({String? message}) { setState(() { - _logEntryDailyMap = LogEntry.getDailyEntryMap(); + _logEntries = LogEntry.getAllForDate(_date); }); setState(() { if (message != null) { @@ -36,12 +62,12 @@ class _LogScreenState extends State { void onDelete(LogEntry logEntry) { LogEntry.remove(logEntry.id); - refresh(message: 'Log Entry deleted'); + reload(message: 'Log Entry deleted'); } void handleDeleteAction(LogEntry logEntry) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(logEntry), message: 'Are you sure you want to delete this Log Entry?', @@ -51,10 +77,14 @@ class _LogScreenState extends State { } } - @override - void initState() { - super.initState(); - refresh(); + void onChangeDate(DateTime? date) { + if (date != null) { + setState(() { + _date = DateTime(date.year, date.month, date.day); + _dateController.text = DateTimeUtils.displayDate(date); + }); + reload(); + } } @override @@ -64,92 +94,235 @@ class _LogScreenState extends State { title: const Text('Log Entries'), actions: [ IconButton( - onPressed: refresh, - icon: const Icon(Icons.refresh) - ), + onPressed: () => onChangeDate(DateTime.now()), + icon: const Icon(Icons.today)), + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)), ], ), drawer: const Navigation(currentLocation: LogScreen.routeName), body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: SingleChildScrollView( - child: _logEntryDailyMap.isNotEmpty ? ListView.builder( - shrinkWrap: true, - padding: const EdgeInsets.all(10.0), - itemCount: _logEntryDailyMap.length, - itemBuilder: (context, dateIndex) { - List dateList = _logEntryDailyMap.keys.toList(); - final date = dateList[dateIndex]; - final entryList = _logEntryDailyMap[date]; - return ListBody( - children: [ - Text(DateTimeUtils.displayDate(date)), - entryList != null && entryList.isNotEmpty - ? ListView.builder( - shrinkWrap: true, - itemCount: entryList.length, - itemBuilder: (context, index) { - final logEntry = entryList[index]; - return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - LogEntryScreen( - id: logEntry.id), - ), - ).then((message) => refresh( - message: message)); - }, - title: Text( - DateTimeUtils.displayTime( - logEntry.time)), - // TODO: add additional fields (event icons...) - // TODO: display glucose in colors according to target settings - subtitle: Text(logEntry - .mgPerDl != - null - ? '${logEntry.mgPerDl.toString()} mg/dl' - : ''), - trailing: Row( - mainAxisSize: - MainAxisSize.min, - children: [ - IconButton( - onPressed: () => - handleDeleteAction( - logEntry), - icon: const Icon( - Icons.delete, - color: Colors.blue), - ) - ], - ), - ); - } - ) : Container(), - ], - ); - }, - ) : const Center( - child: Text('You have not created any Log Entries yet!'), + children: [ + Row( + children: [ + IconButton( + onPressed: _date.isAtSameMomentAs(DateTime(2000, 1, 1)) + ? null + : () => + onChangeDate(_date.subtract(const Duration(days: 1))), + icon: const Icon(Icons.arrow_back), ), - ), + Expanded( + child: GestureDetector( + onTap: () async { + final newTime = await showDatePicker( + context: context, + initialDate: _date, + firstDate: DateTime(2000, 1, 1), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + onChangeDate(newTime); + }, + child: Expanded( + child: Text( + DateTimeUtils.displayDate(_date).toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + textAlign: TextAlign.center, + ), + ), + ), + ), + IconButton( + onPressed: + _date.add(const Duration(days: 1)).isBefore(DateTime.now()) + ? () => onChangeDate(_date.add(const Duration(days: 1))) + : null, + icon: const Icon(Icons.arrow_forward), + ), + ], + ), + Expanded( + child: _logEntries.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(10.0), + shrinkWrap: true, + itemCount: _logEntries.length, + itemBuilder: (context, index) { + LogEntry logEntry = _logEntries[index]; + double bolus = + LogBolus.getTotalBolusForEntry(logEntry.id); + double carbs = + LogMeal.getTotalCarbsForEntry(logEntry.id); + TextStyle glucoseStyle = TextStyle( + color: GlucoseTarget.getColorForGlucose( + mgPerDl: logEntry.mgPerDl ?? 0, + mmolPerL: logEntry.mmolPerL ?? 0)); + return Card( + child: ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + LogEntryScreen(id: logEntry.id), + ), + ).then((result) => reload(message: result?[0])); + }, + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + DateTimeUtils.displayTime(logEntry.time), + ), + ), + Expanded( + child: Column( + children: logEntry.mgPerDl != null && + (Settings.glucoseMeasurement == + GlucoseMeasurement + .mgPerDl || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode + .bothForList) + ? [ + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + logEntry.mgPerDl.toString(), + style: glucoseStyle), + logEntry.glucoseTrend != null + ? Transform.rotate( + angle: logEntry + .glucoseTrend! * + math.pi / + 180, + child: Icon( + Icons.arrow_upward, + color: glucoseStyle + .color, + size: 16.0, + ), + ) + : Container(), + ], + ), + const Text( + 'mg/dl', + textScaleFactor: 0.75, + ), + ] + : [], + ), + ), + Expanded( + child: Column( + children: logEntry.mmolPerL != null && + (Settings.glucoseMeasurement == + GlucoseMeasurement + .mmolPerL || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode + .bothForList) + ? [ + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + logEntry.mmolPerL + .toString(), + style: glucoseStyle), + logEntry.glucoseTrend != null + ? Transform.rotate( + angle: logEntry + .glucoseTrend! * + math.pi / + 180, + child: Icon( + Icons.arrow_upward, + color: glucoseStyle + .color, + size: 16.0, + ), + ) + : Container(), + ], + ), + const Text( + 'mmol/l', + textScaleFactor: 0.75, + ), + ] + : [], + ), + ), + Expanded( + child: Column( + children: (bolus > 0) + ? [ + Text(bolus.toStringAsPrecision(3)), + const Text('U', + textScaleFactor: 0.75), + ] + : [], + ), + ), + Expanded( + child: Column( + children: (carbs > 0) + ? [ + Text(carbs.toStringAsPrecision(3)), + Text( + '${Settings.nutritionMeasurementSuffix} carbs', + textScaleFactor: 0.75), + ] + : [], + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => handleDeleteAction(logEntry), + icon: const Icon(Icons.delete, + color: Colors.blue), + ) + ], + ), + ), + ); + }, + ), + ) + : const Center( + child: Text( + 'You have not created any Log Entries for this date yet!'), + ), ), ], ), - // TODO: add button for active events floatingActionButton: FloatingActionButton( onPressed: () { + final now = DateTime.now(); Navigator.push( context, MaterialPageRoute( - builder: (context) => const LogEntryScreen(), + builder: (context) => LogEntryScreen( + suggestedDate: _date.isAtSameMomentAs(DateTime(now.year, now.month, now.day)) ? now : _date), ), - ).then((message) => refresh(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/screens/log/log_entry.dart b/lib/screens/log/log_entry.dart deleted file mode 100644 index 37ac86e..0000000 --- a/lib/screens/log/log_entry.dart +++ /dev/null @@ -1,281 +0,0 @@ -import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/models/log_entry.dart'; -import 'package:diameter/navigation.dart'; -import 'package:diameter/screens/log/log_entry_form.dart'; -import 'package:diameter/screens/log/log_event_detail.dart'; -import 'package:diameter/screens/log/log_event_list.dart'; -import 'package:diameter/screens/log/log_meal_detail.dart'; -import 'package:diameter/screens/log/log_meal_list.dart'; -import 'package:flutter/material.dart'; - -class LogEntryScreen extends StatefulWidget { - static const String routeName = '/log-entry'; - final int id; - - const LogEntryScreen({Key? key, this.id = 0}) : super(key: key); - - @override - _LogEntryScreenState createState() => _LogEntryScreenState(); -} - -class _LogEntryScreenState extends State { - LogEntry? _logEntry; - bool _isNew = true; - bool _isSaving = false; - - final GlobalKey logEntryForm = GlobalKey(); - - late FloatingActionButton addMealButton; - late FloatingActionButton addEventButton; - late IconButton refreshButton; - late IconButton closeButton; - late DetailBottomRow detailBottomRow; - - FloatingActionButton? actionButton; - List appBarActions = []; - DetailBottomRow? bottomNav; - - final formDataControllers = { - 'time': TextEditingController(text: ''), - 'mgPerDl': TextEditingController(text: ''), - 'mmolPerL': TextEditingController(text: ''), - 'bolusGlucose': TextEditingController(text: ''), - 'delayedBolusRate': TextEditingController(text: ''), - 'delayedBolusDuration': TextEditingController(text: ''), - 'notes': TextEditingController(text: ''), - }; - - @override - void initState() { - super.initState(); - - reload(); - - if (_logEntry != null) { - formDataControllers['time']!.text = _logEntry!.time.toString(); - formDataControllers['mgPerDl']!.text = - (_logEntry!.mgPerDl ?? '').toString(); - formDataControllers['mmolPerL']!.text = - (_logEntry!.mmolPerL ?? '').toString(); - formDataControllers['bolusGlucose']!.text = - (_logEntry!.bolusGlucose ?? '').toString(); - formDataControllers['delayedBolusRate']!.text = - (_logEntry!.delayedBolusRate ?? '').toString(); - formDataControllers['delayedBolusDuration']!.text = - (_logEntry!.delayedBolusDuration ?? '').toString(); - formDataControllers['notes']!.text = _logEntry!.notes ?? ''; - } else { - formDataControllers['time']!.text = DateTime.now().toString(); - } - - addMealButton = FloatingActionButton( - onPressed: handleAddNewMeal, - child: const Icon(Icons.add), - ); - - addEventButton = FloatingActionButton( - onPressed: handleAddNewEvent, - child: const Icon(Icons.add), - ); - - refreshButton = IconButton( - icon: const Icon(Icons.refresh), - onPressed: reload, - ); - - closeButton = IconButton( - onPressed: handleCancelAction, - icon: const Icon(Icons.close), - ); - - detailBottomRow = DetailBottomRow( - onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, - ); - - actionButton = null; - appBarActions = [closeButton]; - bottomNav = detailBottomRow; - } - - void reload({String? message}) { - if (widget.id != 0) { - setState(() { - _logEntry = LogEntry.get(widget.id); - }); - _isNew = _logEntry == null; - } - - setState(() { - if (message != null) { - var snackBar = SnackBar( - content: Text(message), - duration: const Duration(seconds: 2), - ); - ScaffoldMessenger.of(context) - ..removeCurrentSnackBar() - ..showSnackBar(snackBar); - } - }); - } - - void handleAddNewMeal() async { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return LogMealDetailScreen(logEntry: _logEntry!); - }, - ), - ).then((message) => reload(message: message)); - } - - void handleAddNewEvent() async { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return LogEventDetailScreen(logEntry: _logEntry!); - }, - ), - ).then((message) => reload(message: message)); - } - - void handleSaveAction() async { - setState(() { - _isSaving = true; - }); - if (logEntryForm.currentState!.validate()) { - LogEntry.put(LogEntry( - id: widget.id, - time: DateTime.parse(formDataControllers['time']!.text), - mgPerDl: int.tryParse(formDataControllers['mgPerDl']!.text), - mmolPerL: double.tryParse(formDataControllers['mmolPerL']!.text), - bolusGlucose: - double.tryParse(formDataControllers['delayedBolusRate']!.text), - delayedBolusDuration: - int.tryParse(formDataControllers['delayedBolusDuration']!.text), - delayedBolusRate: - double.tryParse(formDataControllers['delayedBolusRate']!.text), - notes: formDataControllers['notes']!.text, - )); - Navigator.pushReplacementNamed(context, '/log', - arguments: '${_isNew ? 'New' : ''} Log Entry Saved'); - } - setState(() { - _isSaving = false; - }); - } - - void handleCancelAction() { - if (showConfirmationDialogOnCancel && - ((_isNew && - (int.tryParse(formDataControllers['mgPerDl']?.text ?? '') != - null || - double.tryParse(formDataControllers['mmolPerL']?.text ?? '') != - null || - double.tryParse(formDataControllers['bolusGlucose']?.text ?? '') != - null || - int.tryParse(formDataControllers['delayedBolusDuration']?.text ?? '') != - null || - double.tryParse(formDataControllers['delayedBolusRate']?.text ?? '') != - null || - formDataControllers['notes']?.text != '')) || - (!_isNew && - (int.tryParse(formDataControllers['mgPerDl']?.text ?? '') != - _logEntry!.mgPerDl || - double.tryParse(formDataControllers['mmolPerL']?.text ?? '') != - _logEntry!.mmolPerL || - double.tryParse(formDataControllers['bolusGlucose']?.text ?? '') != - _logEntry!.bolusGlucose || - int.tryParse( - formDataControllers['delayedBolusDuration']?.text ?? - '') != - _logEntry!.delayedBolusDuration || - double.tryParse(formDataControllers['delayedBolusRate']?.text ?? '') != - _logEntry!.delayedBolusRate || - formDataControllers['notes']?.text != - (_logEntry!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( - context: context, - isNew: _isNew, - onSave: handleSaveAction, - onDiscard: (context) => Navigator.pushReplacementNamed(context, '/log'), - ); - } else { - Navigator.pushReplacementNamed(context, '/log', - arguments: '${_isNew ? 'New' : ''} Log Entry Saved'); - } - } - - void renderTabButtons(index) { - if (_logEntry != null) { - setState(() { - switch (index) { - case 1: - actionButton = addMealButton; - appBarActions = [refreshButton, closeButton]; - bottomNav = null; - break; - case 2: - actionButton = addEventButton; - appBarActions = [refreshButton, closeButton]; - bottomNav = null; - break; - default: - actionButton = null; - appBarActions = [closeButton]; - bottomNav = detailBottomRow; - } - }); - } - } - - @override - Widget build(BuildContext context) { - return DefaultTabController( - length: _isNew ? 1 : 3, - child: Builder(builder: (BuildContext context) { - final TabController tabController = DefaultTabController.of(context)!; - tabController.addListener(() { - renderTabButtons(tabController.index); - }); - List tabs = [ - LogEntryForm( - formState: logEntryForm, controllers: formDataControllers), - ]; - - if (!_isNew) { - tabs.add(LogMealListScreen(logEntry: _logEntry!, reload: reload)); - tabs.add(LogEventListScreen(logEntry: _logEntry!, reload: reload)); - } - - return Scaffold( - appBar: AppBar( - title: Text(_isNew ? 'New Log Entry' : 'Edit Log Entry'), - bottom: _isNew - ? PreferredSize(child: Container(), preferredSize: Size.zero) - : const TabBar( - tabs: [ - Tab(text: 'GENERAL'), - Tab(text: 'MEALS'), - Tab(text: 'EVENTS'), - ], - ), - actions: appBarActions, - ), - drawer: const Navigation(currentLocation: LogEntryScreen.routeName), - body: TabBarView( - children: tabs, - ), - bottomNavigationBar: bottomNav, - floatingActionButton: actionButton, - floatingActionButtonLocation: - FloatingActionButtonLocation.endFloat, - ); - }), - ); - } -} diff --git a/lib/screens/log/log_entry/log_bolus_detail.dart b/lib/screens/log/log_entry/log_bolus_detail.dart new file mode 100644 index 0000000..bb70fcd --- /dev/null +++ b/lib/screens/log/log_entry/log_bolus_detail.dart @@ -0,0 +1,843 @@ +import 'dart:math'; + +import 'package:diameter/components/detail.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'; +import 'package:diameter/models/log_meal.dart'; +import 'package:diameter/models/settings.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/log/log_entry/log_meal_detail.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:flutter/material.dart'; + +enum BolusType { + meal, + glucose, +} + +enum GlucoseParameter { + mgdlCurrent, + mgdlTarget, + mgdlCorrection, + mmolCurrent, + mmolTarget, + mmolCorrection, +} + +class LogBolusDetailScreen extends StatefulWidget { + static const String routeName = '/log-bolus'; + + final int logEntryId; + final int id; + + const LogBolusDetailScreen({Key? key, this.logEntryId = 0, this.id = 0}) + : super(key: key); + + @override + _LogBolusDetailScreenState createState() => _LogBolusDetailScreenState(); +} + +class _LogBolusDetailScreenState extends State { + LogEntry? _logEntry; + LogBolus? _logBolus; + + bool _isNew = true; + bool _isSaving = false; + + final GlobalKey _logBolusForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + final _unitsController = TextEditingController(text: ''); + final _carbsController = TextEditingController(text: ''); + final _mgPerDlCurrentController = TextEditingController(text: ''); + final _mgPerDlTargetController = TextEditingController(text: ''); + final _mgPerDlCorrectionController = TextEditingController(text: ''); + final _mmolPerLCurrentController = TextEditingController(text: ''); + final _mmolPerLTargetController = TextEditingController(text: ''); + final _mmolPerLCorrectionController = TextEditingController(text: ''); + final _delayController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + + final _delayedUnitsController = TextEditingController(text: ''); + final _immediateUnitsController = TextEditingController(text: ''); + + final _mealController = TextEditingController(text: ''); + + bool _setManually = false; + BolusType _bolusType = BolusType.meal; + LogMeal? _meal; + Bolus? _rate; + double _delayPercentage = 0; + + List _logMeals = []; + + @override + void initState() { + super.initState(); + reload(); + + _logEntry = LogEntry.get(widget.logEntryId); + _logMeals = LogMeal.getRecentWithoutBolus(widget.logEntryId); + + if (widget.id != 0) { + _carbsController.text = (_logBolus!.carbs ?? '').toString(); + _delayController.text = (_logBolus!.delay ?? '').toString(); + _notesController.text = _logBolus!.notes ?? ''; + _setManually = _logBolus!.setManually; + _meal = _logBolus!.meal.target; + _mealController.text = (_meal ?? '').toString(); + _rate = _logBolus!.rate.target; + } + + _rate ??= Bolus.getRateForTime(_logEntry?.time); + + _mgPerDlCurrentController.text = (_logBolus?.mgPerDlCurrent ?? + (LogEntry.hasUncorrectedGlucose(widget.logEntryId) + ? _logEntry?.mgPerDl ?? 0 + : 0)) + .toString(); + _mgPerDlTargetController.text = + (_logBolus?.mgPerDlTarget ?? Settings.targetMgPerDl).toString(); + _mgPerDlCorrectionController.text = (_logBolus?.mgPerDlCorrection ?? + max( + (int.tryParse(_mgPerDlCurrentController.text) ?? 0) - + (int.tryParse(_mgPerDlTargetController.text) ?? 0), + 0)) + .toString(); + _mmolPerLCurrentController.text = (_logBolus?.mmolPerLCurrent ?? + (LogEntry.hasUncorrectedGlucose(widget.logEntryId) + ? _logEntry?.mmolPerL ?? 0 + : 0)) + .toString(); + _mmolPerLTargetController.text = + (_logBolus?.mmolPerLTarget ?? Settings.targetMmolPerL).toString(); + _mmolPerLCorrectionController.text = (_logBolus?.mmolPerLCorrection ?? + max( + (double.tryParse(_mmolPerLCurrentController.text) ?? 0) - + (double.tryParse(_mmolPerLTargetController.text) ?? 0), + 0)) + .toString(); + + _unitsController.text = (_logBolus?.units ?? + (_rate != null && !_setManually + ? ((int.tryParse(_mgPerDlCorrectionController.text) ?? 0) / + ((_rate!.mgPerDl ?? 0) / _rate!.units)) + : 0)) + .toString(); + + if (widget.id == 0 && LogEntry.hasUncorrectedGlucose(widget.logEntryId)) { + _bolusType = BolusType.glucose; + } + + calculateBolus(); + } + + @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(() { + _logBolus = LogBolus.get(widget.id); + }); + } + _isNew = _logBolus == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void updateLogMeal(LogMeal? value) { + setState(() { + _meal = value; + _mealController.text = (_meal ?? '').toString(); + }); + if (_meal != null) { + if (_meal!.totalCarbs != null) { + _carbsController.text = (_meal!.totalCarbs).toString(); + } + if (_meal!.meal.hasValue) { + if (_meal!.meal.target!.delayedBolusDuration != null) { + _delayController.text = + (_meal!.meal.target?.delayedBolusDuration).toString(); + } + if (_meal!.meal.target!.delayedBolusDuration != null) { + _delayPercentage = _meal!.meal.target!.delayedBolusPercentage!; + } + } + calculateBolus(); + } + } + + void onSelectMeal(LogMeal? meal) { + updateLogMeal(meal); + if (meal != null && meal.totalCarbs != null) { + setState(() { + _carbsController.text = meal.totalCarbs.toString(); + calculateBolus(); + }); + } + } + + void calculateBolus() { + if (_rate != null && !_setManually) { + double? units; + + if (_bolusType == BolusType.glucose) { + if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl) { + units = (int.tryParse(_mgPerDlCorrectionController.text) ?? 0) / + (_rate!.mgPerDl ?? 1) / + _rate!.units; + } + if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL) { + units = (int.tryParse(_mmolPerLCorrectionController.text) ?? 0) / + (_rate!.mmolPerL ?? 1) / + _rate!.units; + } + } + if (_bolusType == BolusType.meal) { + units = (double.tryParse(_carbsController.text) ?? 0) / + (_rate!.carbs / _rate!.units); + } + + updateDelayedRatio(totalUnitsUpdate: units); + } + } + + void onChangeGlucose() { + int? mgPerDlCurrent; + int? mgPerDlTarget; + int? mgPerDlCorrection; + + double? mmolPerLCurrent; + double? mmolPerLTarget; + double? mmolPerLCorrection; + + 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 (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL && + _mmolPerLCurrentController.text != '') { + mmolPerLCurrent = double.tryParse(_mmolPerLCurrentController.text); + mmolPerLTarget = double.tryParse(_mmolPerLTargetController.text); + mmolPerLCorrection = + max((mmolPerLCurrent ?? 0) - (mmolPerLTarget ?? 0), 0); + } + + if ((mgPerDlCurrent != null && mmolPerLCurrent == null) || + (mgPerDlTarget != null && mmolPerLTarget == null) || + (mgPerDlCorrection != null && mmolPerLCorrection == null)) { + setState(() { + _mgPerDlCorrectionController.text = (mgPerDlCorrection ?? 0).toString(); + _mmolPerLCurrentController.text = + Utils.convertMgPerDlToMmolPerL(mgPerDlCurrent ?? 0).toString(); + _mmolPerLTargetController.text = + Utils.convertMgPerDlToMmolPerL(mgPerDlTarget ?? 0).toString(); + _mmolPerLCorrectionController.text = + Utils.convertMgPerDlToMmolPerL(mgPerDlCorrection ?? 0).toString(); + calculateBolus(); + }); + } + if ((mmolPerLCurrent != null && mgPerDlCurrent == null) || + (mmolPerLTarget != null && mgPerDlTarget == null) || + (mmolPerLCorrection != null && mgPerDlCorrection == null)) { + setState(() { + _mmolPerLCurrentController.text = (mmolPerLCorrection ?? 0).toString(); + _mgPerDlCurrentController.text = + Utils.convertMmolPerLToMgPerDl(mmolPerLCurrent ?? 0).toString(); + _mgPerDlTargetController.text = + Utils.convertMmolPerLToMgPerDl(mmolPerLTarget ?? 0).toString(); + _mgPerDlCorrectionController.text = + Utils.convertMmolPerLToMgPerDl(mmolPerLCorrection ?? 0).toString(); + calculateBolus(); + }); + } + } + + void updateDelayedRatio({ + double? totalUnitsUpdate, + double? delayedUnitsUpdate, + double? immediateUnitsUpdate, + double? percentageUpdate + }) { + int precision = Utils.getFractionDigitsLength(Settings.insulinSteps); + + double? totalUnits; + double? delayedUnits; + double? immediateUnits; + + if (totalUnitsUpdate != null) { + totalUnits = Utils.roundToMultipleOfBase(totalUnitsUpdate, Settings.insulinSteps); + } else if (double.tryParse(_unitsController.text) != null) { + totalUnits = Utils.roundToMultipleOfBase(double.tryParse(_unitsController.text)!, Settings.insulinSteps); + } + + if (delayedUnitsUpdate != null) { + delayedUnits = Utils.roundToMultipleOfBase(delayedUnitsUpdate, Settings.insulinSteps); + } else if (double.tryParse(_delayedUnitsController.text) != null) { + delayedUnits = Utils.roundToMultipleOfBase(double.tryParse(_delayedUnitsController.text)!, Settings.insulinSteps); + } + + if (immediateUnitsUpdate != null) { + immediateUnits = Utils.roundToMultipleOfBase(immediateUnitsUpdate, Settings.insulinSteps); + } else if (double.tryParse(_immediateUnitsController.text) != null) { + immediateUnits = Utils.roundToMultipleOfBase(double.tryParse(_immediateUnitsController.text)!, Settings.insulinSteps); + } + + if (totalUnits == null) { + if (percentageUpdate != null) { + if (immediateUnits != null) { + totalUnits = immediateUnits / (100 - percentageUpdate) * 100; + } else if (delayedUnits != null) { + totalUnits = delayedUnits / percentageUpdate * 100; + } + } else if (delayedUnits != null && immediateUnits != null) { + totalUnits = Utils.addDoublesWithPrecision( + delayedUnits, immediateUnits, precision); + } + + if (totalUnits != null) { + totalUnits = + Utils.roundToMultipleOfBase(totalUnits, Settings.insulinSteps); + } + } + + setState(() { + _unitsController.text = Utils.toStringMatchingTemplateFractionPrecision( + totalUnits ?? 0, Settings.insulinSteps); + }); + + if (totalUnits != null) { + double percentage = percentageUpdate ?? _delayPercentage; + + if (totalUnitsUpdate != null || percentageUpdate != null) { + immediateUnits = Utils.roundToMultipleOfBase( + totalUnits * (100 - percentage) / 100, Settings.insulinSteps); + } else if (delayedUnitsUpdate != null) { + immediateUnits = totalUnits - delayedUnits!; + } + + if (immediateUnits != null) { + delayedUnits = Utils.addDoublesWithPrecision( + totalUnits, -immediateUnits, precision); + + setState(() { + _immediateUnitsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + immediateUnits!, Settings.insulinSteps); + _delayedUnitsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + delayedUnits!, Settings.insulinSteps); + if (totalUnits != 0) { + _delayPercentage = delayedUnits / totalUnits! * 100; + } + }); + } + } + } + + void handleSaveAction() async { + setState(() { + _isSaving = true; + }); + if (_logBolusForm.currentState!.validate()) { + LogBolus logBolus; + LogBolus? delayedBolus; + + if ((int.tryParse(_delayController.text) ?? 0) != 0 && + _delayPercentage != 0 && + _delayPercentage != 100) { + logBolus = LogBolus( + id: widget.id, + units: double.tryParse(_immediateUnitsController.text) ?? 0, + setManually: _setManually, + notes: _notesController.text, + ); + delayedBolus = LogBolus( + delay: int.tryParse(_delayController.text), + units: double.tryParse(_delayedUnitsController.text) ?? 0, + setManually: _setManually, + notes: _notesController.text, + ); + } else { + logBolus = LogBolus( + id: widget.id, + units: double.tryParse(_unitsController.text) ?? 0, + delay: _delayPercentage == 100 + ? int.tryParse(_delayController.text) + : null, + setManually: _setManually, + notes: _notesController.text, + ); + } + + if (_bolusType == BolusType.meal) { + logBolus.carbs = double.tryParse(_carbsController.text); + if (delayedBolus != null) { + delayedBolus.carbs = double.tryParse(_carbsController.text); + } + logBolus.mgPerDlCurrent = null; + logBolus.mmolPerLCurrent = null; + } else { + logBolus.carbs = null; + logBolus.mgPerDlCurrent = int.tryParse(_mgPerDlCurrentController.text); + logBolus.mmolPerLCurrent = + double.tryParse(_mmolPerLCurrentController.text); + logBolus.mgPerDlTarget = int.tryParse(_mgPerDlTargetController.text); + logBolus.mmolPerLTarget = + double.tryParse(_mmolPerLTargetController.text); + logBolus.mgPerDlCorrection = + int.tryParse(_mgPerDlCorrectionController.text); + logBolus.mmolPerLCorrection = + double.tryParse(_mmolPerLCorrectionController.text); + if (delayedBolus != null) { + delayedBolus.mgPerDlCurrent = + int.tryParse(_mgPerDlCurrentController.text); + delayedBolus.mmolPerLCurrent = + double.tryParse(_mmolPerLCurrentController.text); + delayedBolus.mgPerDlTarget = + int.tryParse(_mgPerDlTargetController.text); + delayedBolus.mmolPerLTarget = + double.tryParse(_mmolPerLTargetController.text); + delayedBolus.mgPerDlCorrection = + int.tryParse(_mgPerDlCorrectionController.text); + delayedBolus.mmolPerLCorrection = + double.tryParse(_mmolPerLCorrectionController.text); + } + } + logBolus.logEntry.target = _logEntry; + logBolus.meal.target = _meal; + logBolus.rate.target = _rate; + LogBolus.put(logBolus); + + if (delayedBolus != null) { + delayedBolus.logEntry.target = _logEntry; + delayedBolus.meal.target = _meal; + delayedBolus.rate.target = _rate; + LogBolus.put(delayedBolus); + } + + Navigator.pop(context, + ['${_isNew ? 'New' : ''} Bolus Saved', logBolus, delayedBolus]); + } + setState(() { + _isSaving = false; + }); + } + + void handleCancelAction() { + if (Settings.get().showConfirmationDialogOnCancel && + ((_isNew && + (_carbsController.text != '' || + (_bolusType == BolusType.glucose && + (_mgPerDlCurrentController.text != + (_logEntry?.mgPerDl.toString() ?? '') || + _mmolPerLCurrentController.text != + (_logEntry?.mmolPerL.toString() ?? ''))) || + _mgPerDlTargetController.text != + Settings.targetMgPerDl.toString() || + _mmolPerLTargetController.text != + Settings.targetMmolPerL.toString() || + _delayController.text != '' || + _setManually || + _notesController.text != '')) || + (!_isNew && + (double.tryParse(_unitsController.text) != _logBolus!.units || + double.tryParse(_carbsController.text) != + _logBolus!.carbs || + int.tryParse(_mgPerDlCurrentController.text) != + _logBolus!.mgPerDlCurrent || + int.tryParse(_mgPerDlTargetController.text) != + _logBolus!.mgPerDlTarget || + int.tryParse(_mgPerDlCorrectionController.text) != + _logBolus!.mgPerDlCorrection || + double.tryParse(_mmolPerLCurrentController.text) != + _logBolus!.mmolPerLCurrent || + double.tryParse(_mmolPerLTargetController.text) != + _logBolus!.mmolPerLTarget || + double.tryParse(_mmolPerLCorrectionController.text) != + _logBolus!.mmolPerLCorrection || + int.tryParse(_delayController.text) != _logBolus!.delay || + _setManually != _logBolus!.setManually || + _notesController.text != (_logBolus!.notes ?? ''))))) { + DialogUtils.showCancelConfirmationDialog( + context: context, + isNew: _isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_isNew ? 'New Bolus' : 'Edit Bolus'), + ), + drawer: const Navigation(currentLocation: LogBolusDetailScreen.routeName), + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _logBolusForm, + fields: [ + Row( + children: [ + Expanded( + child: NumberFormField( + label: 'Bolus Units', + suffix: ' U', + controller: _unitsController, + step: Settings.insulinSteps, + autoRoundToMultipleOfStep: true, + onChanged: (value) { + setState(() { + _setManually = true; + }); + updateDelayedRatio(totalUnitsUpdate: value); + }, + ), + ), + Expanded( + child: BooleanFormField( + contentPadding: const EdgeInsets.only( + left: 10.0, right: 10.0, top: 10.0), + value: _setManually, + label: 'set manually', + onChanged: (value) { + setState(() { + _setManually = value; + calculateBolus(); + }); + }, + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: RadioListTile( + title: const Text('for glucose'), + groupValue: _bolusType, + value: BolusType.glucose, + onChanged: (_) { + setState(() { + _bolusType = BolusType.glucose; + calculateBolus(); + }); + }), + ), + Expanded( + child: RadioListTile( + title: const Text('for meal'), + groupValue: _bolusType, + value: BolusType.meal, + onChanged: (value) { + setState(() { + _bolusType = BolusType.meal; + calculateBolus(); + }); + }), + ), + ], + ), + Column( + children: _bolusType == BolusType.glucose + ? [ + Row( + children: Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl || + [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? [ + Expanded( + child: Padding( + padding: + const EdgeInsets.only(right: 5.0), + child: NumberFormField( + label: 'Current', + suffix: 'mg/dl', + controller: + _mgPerDlCurrentController, + onChanged: (_) => onChangeGlucose(), + showSteppers: false, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5.0), + child: NumberFormField( + label: 'Target', + suffix: 'mg/dl', + controller: + _mgPerDlTargetController, + onChanged: (_) => onChangeGlucose(), + showSteppers: false, + ), + ), + ), + Expanded( + child: Padding( + padding: + const EdgeInsets.only(left: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Correction', + suffixText: 'mg/dl', + ), + controller: + _mgPerDlCorrectionController, + readOnly: true, + ), + ), + ), + ] + : [], + ), + 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, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5.0), + child: NumberFormField( + label: 'Target', + suffix: 'mmol/l', + controller: + _mmolPerLTargetController, + onChanged: (_) => + onChangeGlucose(), + showSteppers: false, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Correction', + suffixText: 'mmol/l', + ), + controller: + _mmolPerLCorrectionController, + readOnly: true, + ), + ), + ), + ] + : [], + ), + ), + ] + : [ + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _mealController, + selectedItem: _meal, + label: 'Meal', + items: _logMeals, + onChanged: onSelectMeal, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _meal == null + ? const LogMealDetailScreen() + : LogMealDetailScreen( + id: _meal!.id), + ), + ).then((result) { + updateLogMeal(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon( + _meal == null ? Icons.add : Icons.edit), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: NumberFormField( + label: 'Carbs', + suffix: Settings.nutritionMeasurementSuffix, + controller: _carbsController, + step: Settings.nutritionSteps, + onChanged: (value) { + _carbsController.text = + (value ?? 0).toString(); + calculateBolus(); + }, + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Delayed Bolus Duration', + suffixText: ' min', + ), + controller: _delayController, + onChanged: (value) => setState(() {}), + keyboardType: const TextInputType.numberWithOptions(), + ), + ), + Expanded( + child: Slider( + label: '${_delayPercentage.floor().toString()}%', + divisions: 100, + value: _delayPercentage, + min: 0, + max: 100, + onChanged: (value) { + updateDelayedRatio(percentageUpdate: value); + }, + ), + ), + const Text('%', textScaleFactor: 1.5), + ], + ), + Row( + children: (int.tryParse(_delayController.text) ?? 0) != 0 + ? [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 5.0), + child: NumberFormField( + label: 'Immediate Bolus', + suffix: ' U', + controller: _immediateUnitsController, + max: double.tryParse(_unitsController.text), + step: Settings.insulinSteps, + readOnly: true, + onChanged: (value) => updateDelayedRatio( + immediateUnitsUpdate: value), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5.0), + child: NumberFormField( + label: 'Delayed Bolus', + suffix: ' U', + controller: _delayedUnitsController, + max: double.tryParse(_unitsController.text), + step: Settings.insulinSteps, + readOnly: true, + onChanged: (value) => { + updateDelayedRatio( + delayedUnitsUpdate: value), + }), + ), + ), + ] + : [], + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, + ), + ], + ), + ], + ), + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onAction: _isSaving ? null : handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/log/log_entry/log_bolus_list.dart b/lib/screens/log/log_entry/log_bolus_list.dart new file mode 100644 index 0000000..8ee43e2 --- /dev/null +++ b/lib/screens/log/log_entry/log_bolus_list.dart @@ -0,0 +1,146 @@ +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'; +import 'package:diameter/screens/log/log_entry/log_bolus_detail.dart'; +import 'package:diameter/screens/log/log_entry/log_meal_detail.dart'; +import 'package:flutter/material.dart'; + +class LogBolusListScreen extends StatefulWidget { + final LogEntry logEntry; + final List logBoli; + final Function() reload; + + const LogBolusListScreen( + {Key? key, + required this.logEntry, + this.logBoli = const [], + required this.reload}) + : super(key: key); + + @override + _LogBolusListScreenState createState() => _LogBolusListScreenState(); +} + +class _LogBolusListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { + widget.reload(); + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void handleEditAction(LogBolus logBolus) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LogBolusDetailScreen( + logEntryId: widget.logEntry.id, + id: logBolus.id, + ), + ), + ).then((result) => reload(message: result?[0])); + } + + void onDelete(LogBolus logBolus) { + LogBolus.remove(logBolus.id); + reload(message: 'Bolus deleted'); + } + + void handleDeleteAction(LogBolus logBolus) async { + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(logBolus), + message: 'Are you sure you want to delete this Bolus?', + ); + } else { + onDelete(logBolus); + } + } + + void handleEditMealAction(int mealId) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LogMealDetailScreen( + logEntryId: widget.logEntry.id, + id: mealId, + ), + ), + ).then((result) => reload(message: result?[0])); + } + + @override + Widget build(BuildContext context) { + return widget.logBoli.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + padding: const EdgeInsets.all(10.0), + controller: _scrollController, + shrinkWrap: true, + itemCount: widget.logBoli.length, + itemBuilder: (context, index) { + final bolus = widget.logBoli[index]; + String titleText = '${bolus.units} U ${(bolus.delay ?? 0) != 0 + ? ' (delayed by ${bolus.delay} min)' + : ''}'; + return Card( + child: ListTile( + onTap: () => handleEditAction(bolus), + title: Text( + titleText.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + ), + subtitle: Text(bolus.carbs != null ? + 'for ${(bolus.meal.target ?? '').toString()} (${bolus.carbs}${Settings.nutritionMeasurementSuffix} carbs)' + : 'to correct ${Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDlCorrection : bolus.mmolPerLCorrection} ${Settings.glucoseMeasurementSuffix}'), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + bolus.meal.target != null + ? IconButton( + icon: const Icon(Icons.restaurant), + onPressed: () => + handleEditMealAction(bolus.meal.targetId), + ) + : Container(), + const SizedBox(width: 24), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(bolus), + ), + ], + ), + ), + ); + }, + ), + ) + : const Center( + child: Text( + 'You have not added any Boli to this Log Entry yet!'), + ); + } +} diff --git a/lib/screens/log/log_entry/log_entry.dart b/lib/screens/log/log_entry/log_entry.dart new file mode 100644 index 0000000..5878ec6 --- /dev/null +++ b/lib/screens/log/log_entry/log_entry.dart @@ -0,0 +1,470 @@ +import 'package:diameter/components/detail.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'; +import 'package:diameter/models/settings.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/log/log_entry/log_bolus_detail.dart'; +import 'package:diameter/screens/log/log_entry/log_bolus_list.dart'; +import 'package:diameter/screens/log/log_entry/log_meal_detail.dart'; +import 'package:diameter/screens/log/log_entry/log_meal_list.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class LogEntryScreen extends StatefulWidget { + static const String routeName = '/log-entry'; + final int id; + final DateTime? suggestedDate; + + const LogEntryScreen({Key? key, this.id = 0, this.suggestedDate}) : super(key: key); + + @override + _LogEntryScreenState createState() => _LogEntryScreenState(); +} + +class _LogEntryScreenState extends State { + LogEntry? _logEntry; + List _logMeals = []; + List _logBoli = []; + + bool _isNew = true; + + final GlobalKey logEntryForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + late DateTime _time; + double? _glucoseTrend; + + final _timeController = TextEditingController(text: ''); + final _dateController = TextEditingController(text: ''); + final _mgPerDlController = TextEditingController(text: ''); + final _mmolPerLController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + + late FloatingActionButton addMealButton; + late FloatingActionButton addBolusButton; + late IconButton refreshButton; + late IconButton closeButton; + late DetailBottomRow detailBottomRow; + late DetailBottomRow detailBottomRowWhileSaving; + + FloatingActionButton? actionButton; + List appBarActions = []; + DetailBottomRow? bottomNav; + + @override + void initState() { + super.initState(); + + reload(); + + addMealButton = FloatingActionButton( + onPressed: handleAddNewMeal, + child: const Icon(Icons.add), + ); + + addBolusButton = FloatingActionButton( + onPressed: handleAddNewBolus, + child: const Icon(Icons.add), + ); + + refreshButton = IconButton( + icon: const Icon(Icons.refresh), + onPressed: reload, + ); + + closeButton = IconButton( + onPressed: handleCancelAction, + icon: const Icon(Icons.close), + ); + + detailBottomRow = DetailBottomRow( + onCancel: handleCancelAction, + onAction: handleSaveAction, + onMiddleAction: () => handleSaveAction(close: true), + ); + + detailBottomRowWhileSaving = DetailBottomRow( + onCancel: handleCancelAction, + onAction: null, + ); + + actionButton = null; + appBarActions = [closeButton]; + bottomNav = detailBottomRow; + + if (_logEntry != null) { + _time = _logEntry!.time; + _mgPerDlController.text = (_logEntry!.mgPerDl ?? '').toString(); + _mmolPerLController.text = (_logEntry!.mmolPerL ?? '').toString(); + _glucoseTrend = _logEntry!.glucoseTrend; + _notesController.text = _logEntry!.notes ?? ''; + } else { + _time = widget.suggestedDate ?? DateTime.now(); + } + + 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(() { + _logEntry = LogEntry.get(widget.id); + _logMeals = LogMeal.getAllForEntry(widget.id); + _logBoli = LogBolus.getAllForEntry(widget.id); + }); + _isNew = _logEntry == null; + } + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void updateTime() { + _timeController.text = DateTimeUtils.displayTime(_time); + _dateController.text = DateTimeUtils.displayDate(_time); + } + + 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.toStringMatchingTemplateFractionPrecision( + value, Settings.mmolPerLSteps); + setState(() { + _mgPerDlController.text = + Utils.convertMmolPerLToMgPerDl(value.toDouble()).toString(); + }); + } + } + } + + void handleSaveAction({bool close = false}) async { + setState(() { + bottomNav = detailBottomRowWhileSaving; + }); + if (logEntryForm.currentState!.validate()) { + LogEntry logEntry = LogEntry( + id: widget.id, + time: _time, + mgPerDl: int.tryParse(_mgPerDlController.text), + mmolPerL: double.tryParse(_mmolPerLController.text), + glucoseTrend: _glucoseTrend, + notes: _notesController.text, + ); + LogEntry.put(logEntry); + + if (close) { + Navigator.pop( + context, ['${_isNew ? 'New' : ''} Log Entry Saved', logEntry]); + } else { + if (_isNew) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LogEntryScreen(id: logEntry.id), + ), + ).then((result) => Navigator.pop(context, result)); + } else { + reload(message: 'Log Entry Saved'); + } + } + } + + setState(() { + bottomNav = detailBottomRow; + }); + } + + void handleCancelAction() { + if (Settings.get().showConfirmationDialogOnCancel && + ((_isNew && + (int.tryParse(_mgPerDlController.text) != null || + double.tryParse(_mmolPerLController.text) != null || + _notesController.text != '')) || + (!_isNew && + (int.tryParse(_mgPerDlController.text) != _logEntry!.mgPerDl || + double.tryParse(_mmolPerLController.text) != + _logEntry!.mmolPerL || + _notesController.text != (_logEntry!.notes ?? ''))))) { + DialogUtils.showCancelConfirmationDialog( + context: context, + isNew: _isNew, + onSave: handleSaveAction, + onDiscard: (context) => Navigator.pop(context), + ); + } else { + Navigator.pop(context); + } + } + + void handleAddNewMeal() async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return LogMealDetailScreen(logEntryId: _logEntry!.id); + }, + ), + ).then((result) => reload(message: result?[0])); + } + + void handleAddNewBolus() async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return LogBolusDetailScreen(logEntryId: _logEntry!.id); + }, + ), + ).then((result) => reload(message: result?[0])); + } + + void renderTabButtons(index) { + if (_logEntry != null) { + setState(() { + switch (index) { + case 1: + actionButton = addMealButton; + appBarActions = [refreshButton, closeButton]; + bottomNav = null; + break; + case 2: + actionButton = addBolusButton; + appBarActions = [refreshButton, closeButton]; + bottomNav = null; + break; + default: + actionButton = null; + appBarActions = [closeButton]; + bottomNav = detailBottomRow; + } + }); + } + } + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: _isNew ? 1 : 3, + child: Builder(builder: (BuildContext context) { + final TabController tabController = DefaultTabController.of(context)!; + tabController.addListener(() { + renderTabButtons(tabController.index); + }); + List tabs = [ + Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: logEntryForm, + fields: [ + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: DateTimeFormField( + date: _time, + label: 'Date', + controller: _dateController, + onChanged: (newTime) { + if (newTime != null) { + setState(() { + _time = DateTime( + newTime.year, + newTime.month, + newTime.day, + _time.hour, + _time.minute); + }); + updateTime(); + } + }, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5), + child: TimeOfDayFormField( + time: TimeOfDay.fromDateTime(_time), + label: 'Time', + controller: _timeController, + onChanged: (newTime) { + if (newTime != null) { + setState(() { + _time = DateTime( + _time.year, + _time.month, + _time.day, + newTime.hour, + newTime.minute); + }); + updateTime(); + } + }, + ), + ), + ), + ], + ), + Row( + children: [ + Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? Expanded( + flex: Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl + ? 2 + : 1, + child: NumberFormField( + label: 'Blood Glucose', + 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: 'Blood Glucose', + suffix: 'mmol/l', + readOnly: Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl, + showSteppers: + Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL, + controller: _mmolPerLController, + step: Settings.mmolPerLSteps, + onChanged: + convertBetweenMgPerDlAndMmolPerL, + ), + ) + : Container(), + Transform.rotate( + angle: (_glucoseTrend ?? 90) * math.pi / 180, + child: IconButton( + onPressed: () => setState(() { + _glucoseTrend = (_glucoseTrend ?? -45) + 45; + if (_glucoseTrend! > 180) { + _glucoseTrend = null; + } + }), + icon: Icon(Icons.arrow_upward, + color: _glucoseTrend != null + ? Theme.of(context).iconTheme.color + : Theme.of(context).disabledColor), + ), + ), + ], + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, + ), + ], + ), + ]), + ), + ), + ]; + + if (!_isNew) { + tabs.add(LogMealListScreen( + logEntry: _logEntry!, logMeals: _logMeals, reload: reload)); + tabs.add(LogBolusListScreen( + logEntry: _logEntry!, logBoli: _logBoli, reload: reload)); + } + + return Scaffold( + appBar: AppBar( + title: Text(_isNew ? 'New Log Entry' : 'Edit Log Entry'), + bottom: _isNew + ? PreferredSize(child: Container(), preferredSize: Size.zero) + : const TabBar( + tabs: [ + Tab(text: 'GENERAL'), + Tab(text: 'MEALS'), + Tab(text: 'BOLI'), + ], + ), + actions: appBarActions, + ), + drawer: const Navigation(currentLocation: LogEntryScreen.routeName), + body: TabBarView( + children: tabs, + ), + bottomNavigationBar: bottomNav, + floatingActionButton: actionButton, + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + ); + }), + ); + } +} diff --git a/lib/screens/log/log_entry/log_meal_detail.dart b/lib/screens/log/log_entry/log_meal_detail.dart new file mode 100644 index 0000000..6704d9a --- /dev/null +++ b/lib/screens/log/log_entry/log_meal_detail.dart @@ -0,0 +1,804 @@ +import 'package:diameter/components/detail.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/accuracy.dart'; +import 'package:diameter/models/log_meal.dart'; +import 'package:diameter/models/meal.dart'; +import 'package:diameter/models/meal_category.dart'; +import 'package:diameter/models/meal_portion_type.dart'; +import 'package:diameter/models/meal_source.dart'; +import 'package:diameter/models/settings.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/accuracy_detail.dart'; +import 'package:diameter/screens/meal/meal_category_detail.dart'; +import 'package:diameter/screens/meal/meal_detail.dart'; +import 'package:diameter/screens/meal/meal_portion_type_detail.dart'; +import 'package:diameter/screens/meal/meal_source_detail.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:flutter/material.dart'; + +class LogMealDetailScreen extends StatefulWidget { + static const String routeName = '/log-meal'; + + final int logEntryId; + final int id; + + const LogMealDetailScreen({Key? key, this.logEntryId = 0, this.id = 0}) + : super(key: key); + + @override + _LogMealDetailScreenState createState() => _LogMealDetailScreenState(); +} + +class _LogMealDetailScreenState extends State { + LogMeal? _logMeal; + bool _isNew = true; + bool _isSaving = false; + bool _isExpanded = false; + bool _setManually = false; + + final GlobalKey _logMealForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + double _amount = 1; + + final _valueController = TextEditingController(text: ''); + final _carbsRatioController = TextEditingController(text: ''); + final _portionSizeController = TextEditingController(text: ''); + final _totalCarbsController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + + Meal? _meal; + MealSource? _mealSource; + MealCategory? _mealCategory; + MealPortionType? _mealPortionType; + Accuracy? _portionSizeAccuracy; + Accuracy? _carbsRatioAccuracy; + + final _mealController = TextEditingController(text: ''); + final _mealSourceController = TextEditingController(text: ''); + final _mealCategoryController = TextEditingController(text: ''); + final _mealPortionTypeController = TextEditingController(text: ''); + final _portionSizeAccuracyController = TextEditingController(text: ''); + final _carbsRatioAccuracyController = TextEditingController(text: ''); + final _amountController = TextEditingController(text: '1'); + + List _meals = []; + List _mealCategories = []; + List _mealPortionTypes = []; + List _mealSources = []; + List _portionSizeAccuracies = []; + List _carbsRatioAccuracies = []; + + @override + void initState() { + super.initState(); + reload(); + + _portionSizeAccuracies = Accuracy.getAllForPortionSize(); + _carbsRatioAccuracies = Accuracy.getAllForCarbsRatio(); + _meals = Meal.getAll(); + _mealCategories = MealCategory.getAll(); + _mealPortionTypes = MealPortionType.getAll(); + _mealSources = MealSource.getAll(); + + if (widget.id != 0) { + _valueController.text = _logMeal!.value; + _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; + _mealSourceController.text = (_mealSource ?? '').toString(); + _mealCategory = _logMeal!.mealCategory.target; + _mealCategoryController.text = (_mealCategory ?? '').toString(); + _mealPortionType = _logMeal!.mealPortionType.target; + _mealPortionTypeController.text = (_mealPortionType ?? '').toString(); + _portionSizeAccuracy = _logMeal!.portionSizeAccuracy.target; + _portionSizeAccuracyController.text = + (_portionSizeAccuracy ?? '').toString(); + _carbsRatioAccuracy = _logMeal!.carbsRatioAccuracy.target; + _carbsRatioAccuracyController.text = + (_carbsRatioAccuracy ?? '').toString(); + } + } + + @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}) { + if (widget.id != 0) { + setState(() { + _logMeal = LogMeal.get(widget.id); + }); + } + _isNew = _logMeal == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void updateCarbsRatioAccuracy(Accuracy? value) { + setState(() { + _carbsRatioAccuracy = value; + _carbsRatioAccuracyController.text = + (_carbsRatioAccuracy ?? '').toString(); + }); + } + + void updatePortionSizeAccuracy(Accuracy? value) { + setState(() { + _portionSizeAccuracy = value; + _portionSizeAccuracyController.text = + (_portionSizeAccuracy ?? '').toString(); + }); + } + + void updateMealCategory(MealCategory? value) { + setState(() { + _mealCategory = value; + _mealCategoryController.text = (_mealCategory ?? '').toString(); + }); + } + + void updateMealPortionType(MealPortionType? value) { + setState(() { + _mealPortionType = value; + _mealPortionTypeController.text = (_mealPortionType ?? '').toString(); + }); + } + + void updateMealSource(MealSource? value) { + setState(() { + _mealSource = value; + _mealSourceController.text = (_mealSource ?? '').toString(); + }); + } + + Future onSelectMeal(Meal? meal) async { + setState(() { + _meal = meal; + _mealController.text = (_meal ?? '').toString(); + _valueController.text = _mealController.text; + _carbsRatioController.text = (meal?.carbsRatio ?? '').toString(); + _amountController.text = '1'; + _portionSizeController.text = (meal?.portionSize ?? '').toString(); + _totalCarbsController.text = (meal?.carbsPerPortion ?? '').toString(); + }); + updateMealSource(meal?.mealSource.target); + updateMealCategory(meal?.mealCategory.target); + updateMealPortionType(meal?.mealPortionType.target); + updatePortionSizeAccuracy(meal?.portionSizeAccuracy.target); + updateCarbsRatioAccuracy(meal?.carbsRatioAccuracy.target); + } + + void handleSaveAction() async { + setState(() { + _isSaving = true; + }); + if (_logMealForm.currentState!.validate()) { + LogMeal logMeal = LogMeal( + id: widget.id, + value: _valueController.text, + 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; + logMeal.mealSource.target = _mealSource; + logMeal.mealCategory.target = _mealCategory; + logMeal.mealPortionType.target = _mealPortionType; + logMeal.portionSizeAccuracy.target = _portionSizeAccuracy; + logMeal.carbsRatioAccuracy.target = _carbsRatioAccuracy; + + LogMeal.put(logMeal); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Saved', logMeal]); + } + setState(() { + _isSaving = false; + }); + } + + void handleCancelAction() { + if (Settings.get().showConfirmationDialogOnCancel && + ((_isNew && + (_valueController.text != '' || + _meal != null || + _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 || + _carbsRatioAccuracy != null || + _portionSizeAccuracy != null || + _notesController.text != '')) || + (!_isNew && + (_valueController.text != _logMeal!.value || + _meal != _logMeal!.meal.target || + _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) != + _logMeal!.portionSize || + double.tryParse(_totalCarbsController.text) != + _logMeal!.totalCarbs || + _carbsRatioAccuracy != + _logMeal!.carbsRatioAccuracy.target || + _portionSizeAccuracy != + _logMeal!.portionSizeAccuracy.target || + _notesController.text != (_logMeal!.notes ?? ''))))) { + DialogUtils.showCancelConfirmationDialog( + context: context, + isNew: _isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + void updateAmount(double? newAmount) { + if (newAmount != null) { + setState(() { + _amountController.text = Utils.getFractionDigitsLength(newAmount) == 0 + ? newAmount.toInt().toString() + : newAmount.toString(); + }); + + double? portionSize; + double? basePortionSize; + + if (_portionSizeController.text != '') { + portionSize = double.tryParse(_portionSizeController.text); + } + + if (portionSize != null && portionSize != 0) { + basePortionSize = portionSize / _amount; + } else if (_meal != null) { + basePortionSize = _meal!.portionSize; + } + + if (basePortionSize != null) { + setState(() { + portionSize = basePortionSize! * newAmount; + _portionSizeController.text = + Utils.toStringMatchingTemplateFractionPrecision( + portionSize!, Settings.nutritionSteps); + }); + calculateThirdMeasurementOfPortionCarbsRelation( + portionSizeUpdate: portionSize); + } + + setState(() { + _amount = newAmount; + }); + } + } + + void calculateThirdMeasurementOfPortionCarbsRelation( + {double? carbsRatioUpdate, + double? portionSizeUpdate, + double? totalCarbsUpdate}) { + if (!_setManually) { + double? carbsRatio = + carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text); + double? portionSize = + portionSizeUpdate ?? double.tryParse(_portionSizeController.text); + double? totalCarbs = + totalCarbsUpdate ?? double.tryParse(_totalCarbsController.text); + + int toCalculate = 0; + const calcCarbsRatio = 1; + const calcTotalCarbs = 2; + const calcPortionSize = 3; + + if (carbsRatioUpdate != null) { + if (portionSize != null && portionSize != 0) { + toCalculate = calcTotalCarbs; + } else if (totalCarbs != null && totalCarbs != 0) { + toCalculate = calcPortionSize; + } + } else if (portionSizeUpdate != null) { + if (carbsRatio != null && carbsRatio != 0) { + toCalculate = calcTotalCarbs; + } else if (totalCarbs != null && totalCarbs != 0) { + toCalculate = calcCarbsRatio; + } + } else if (totalCarbsUpdate != null) { + if (carbsRatio != null && carbsRatio != 0) { + toCalculate = calcPortionSize; + } else if (portionSize != null && portionSize != 0) { + toCalculate = calcCarbsRatio; + } + } else { + if (carbsRatio != null && carbsRatio != 0) { + if (portionSize != null && portionSize != 0) { + toCalculate = calcTotalCarbs; + } else if (totalCarbs != null && totalCarbs != 0) { + toCalculate = calcPortionSize; + } + } else if (portionSize != null && + portionSize != 0 && + totalCarbs != null && + totalCarbs != 0) { + toCalculate = calcCarbsRatio; + } + } + + setState(() { + if (toCalculate == calcCarbsRatio) { + _carbsRatioController.text = + Utils.calculateCarbsRatio(totalCarbs!, portionSize!).toString(); + } else if (toCalculate == calcTotalCarbs) { + _totalCarbsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + Utils.calculateCarbs(carbsRatio!, portionSize!, + step: Settings.nutritionSteps), + Settings.nutritionSteps); + } else if (toCalculate == calcPortionSize) { + _portionSizeController.text = + Utils.toStringMatchingTemplateFractionPrecision( + Utils.calculatePortionSize(carbsRatio!, totalCarbs!, + step: Settings.nutritionSteps), + Settings.nutritionSteps); + } + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_isNew ? 'New Meal for Log Entry' : _logMeal!.value), + ), + drawer: const Navigation(currentLocation: LogMealDetailScreen.routeName), + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _logMealForm, + fields: [ + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _mealController, + selectedItem: _meal, + label: 'Meal', + items: _meals, + onChanged: onSelectMeal, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _meal == null + ? const MealDetailScreen() + : MealDetailScreen(id: _meal!.id), + ), + ).then((result) { + onSelectMeal(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_meal == null ? Icons.add : Icons.edit), + ), + ], + ), + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), + Row( + children: [ + Expanded( + flex: 10, + child: NumberFormField( + controller: _amountController, + label: 'Amount', + suffix: _mealPortionType?.value, + onChanged: updateAmount, + ), + ), + Expanded( + child: TextButton( + onPressed: () => updateAmount(0.5), + child: Column( + children: const [ + Text( + '1', + style: TextStyle( + decoration: TextDecoration.underline, + decorationThickness: 2), + ), + Text('2'), + ], + ), + ), + ), + Expanded( + child: TextButton( + onPressed: () => updateAmount(0.33), + child: Column( + children: const [ + Text( + '1', + style: TextStyle( + decoration: TextDecoration.underline, + decorationThickness: 2), + ), + Text('3'), + ], + ), + ), + ), + Expanded( + child: TextButton( + onPressed: () => updateAmount(0.67), + child: Column( + children: const [ + Text( + '2', + style: TextStyle( + decoration: TextDecoration.underline, + decorationThickness: 2), + ), + Text('3'), + ], + ), + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: NumberFormField( + label: 'Portion size', + suffix: Settings.nutritionMeasurementSuffix, + controller: _portionSizeController, + showSteppers: false, + autoRoundToMultipleOfStep: true, + step: Settings.nutritionSteps, + onChanged: (value) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation( + portionSizeUpdate: value); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: NumberFormField( + label: 'Carbs ratio', + suffix: '%', + controller: _carbsRatioController, + showSteppers: false, + onChanged: (value) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation( + carbsRatioUpdate: value); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: NumberFormField( + label: 'Total carbs', + suffix: Settings.nutritionMeasurementSuffix, + controller: _totalCarbsController, + showSteppers: false, + autoRoundToMultipleOfStep: true, + step: Settings.nutritionSteps, + onChanged: (value) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation( + totalCarbsUpdate: value); + }, + ), + ), + ], + ), + Expanded( + child: BooleanFormField( + value: _setManually, + label: 'set carbs ratio manually', + onChanged: (value) { + setState(() { + _setManually = value; + calculateThirdMeasurementOfPortionCarbsRelation(); + }); + }, + ), + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, + ), + const Divider(), + GestureDetector( + onTap: () => setState(() { + _isExpanded = !_isExpanded; + }), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text( + 'ADDITIONAL FIELDS', + style: Theme.of(context).textTheme.subtitle2, + ), + const Spacer(), + Icon(_isExpanded + ? Icons.expand_less + : Icons.expand_more), + ], + ), + ), + Column( + children: _isExpanded + ? [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + Expanded( + child: + AutoCompleteDropdownButton( + controller: _mealSourceController, + selectedItem: _mealSource, + label: 'Meal Source', + items: _mealSources, + onChanged: updateMealSource, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _mealSource == + null + ? const MealSourceDetailScreen() + : MealSourceDetailScreen( + id: _mealSource!.id), + ), + ).then((result) { + updateMealSource(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_mealSource == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton< + MealCategory>( + controller: _mealCategoryController, + selectedItem: _mealCategory, + label: 'Meal Category', + items: _mealCategories, + onChanged: updateMealCategory, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _mealCategory == + null + ? const MealCategoryDetailScreen() + : MealCategoryDetailScreen( + id: _mealCategory!.id), + ), + ).then((result) { + updateMealCategory(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_mealCategory == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton< + MealPortionType>( + controller: _mealPortionTypeController, + selectedItem: _mealPortionType, + label: 'Meal Portion Type', + items: _mealPortionTypes, + onChanged: updateMealPortionType, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _mealPortionType == + null + ? const MealPortionTypeDetailScreen() + : MealPortionTypeDetailScreen( + id: _mealPortionType!.id), + ), + ).then((result) { + updateMealPortionType(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_mealPortionType == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: + _portionSizeAccuracyController, + selectedItem: _portionSizeAccuracy, + label: 'Portion Size Accuracy', + items: _portionSizeAccuracies, + onChanged: updatePortionSizeAccuracy, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + _portionSizeAccuracy == null + ? const AccuracyDetailScreen() + : AccuracyDetailScreen( + id: _portionSizeAccuracy! + .id), + ), + ).then((result) { + updatePortionSizeAccuracy(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_portionSizeAccuracy == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _carbsRatioAccuracyController, + selectedItem: _carbsRatioAccuracy, + label: 'Carbs Ratio Accuracy', + items: _carbsRatioAccuracies, + onChanged: updateCarbsRatioAccuracy, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + _carbsRatioAccuracy == null + ? const AccuracyDetailScreen() + : AccuracyDetailScreen( + id: _carbsRatioAccuracy! + .id), + ), + ).then((result) { + updateCarbsRatioAccuracy(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_carbsRatioAccuracy == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + ] + : [], + ), + ], + ), + ], + ), + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onAction: _isSaving ? null : handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/log/log_entry/log_meal_list.dart b/lib/screens/log/log_entry/log_meal_list.dart new file mode 100644 index 0000000..b9a63ec --- /dev/null +++ b/lib/screens/log/log_entry/log_meal_list.dart @@ -0,0 +1,127 @@ +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'; +import 'package:diameter/screens/log/log_entry/log_meal_detail.dart'; +import 'package:flutter/material.dart'; + +class LogMealListScreen extends StatefulWidget { + final LogEntry logEntry; + final List logMeals; + final Function() reload; + + const LogMealListScreen( + {Key? key, required this.logEntry, this.logMeals = const [], required this.reload}) + : super(key: key); + + @override + _LogMealListScreenState createState() => _LogMealListScreenState(); +} + +class _LogMealListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { + widget.reload(); + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void handleEditAction(LogMeal meal) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LogMealDetailScreen( + logEntryId: widget.logEntry.id, + id: meal.id, + ), + ), + ).then((result) => reload(message: result?[0])); + } + + void onDelete(LogMeal logMeal) { + LogMeal.remove(logMeal.id); + reload(message: 'Meal deleted'); + } + + void handleDeleteAction(LogMeal meal) async { + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(meal), + message: 'Are you sure you want to delete this Meal?', + ); + } else { + onDelete(meal); + } + } + + @override + Widget build(BuildContext context) { + return widget.logMeals.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + padding: const EdgeInsets.all(10.0), + controller: _scrollController, + shrinkWrap: true, + itemCount: widget.logMeals.length, + itemBuilder: (context, index) { + final meal = widget.logMeals[index]; + return Card( + child: ListTile( + onTap: () => handleEditAction(meal), + title: Row( + children: [ + Expanded(child: Text( + meal.value.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + )), + Expanded( + child: Column( + children: ((meal.totalCarbs ?? 0) > 0) + ? [ + Text(meal.totalCarbs!.toStringAsPrecision(3)), + Text( + '${Settings.nutritionMeasurementSuffix} carbs', + textScaleFactor: 0.75), + ] + : [], + ), + ), + ], + ), + trailing: IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(meal), + ), + ), + ); + }, + ), + ) + : const Center( + child: Text( + 'You have not added any Meals to this Log Entry yet!'), + ); + } +} diff --git a/lib/screens/log/log_entry_form.dart b/lib/screens/log/log_entry_form.dart deleted file mode 100644 index c969c2b..0000000 --- a/lib/screens/log/log_entry_form.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/settings.dart'; -import 'package:diameter/utils/utils.dart'; -import 'package:flutter/material.dart'; - -class LogEntryForm extends StatefulWidget { - final GlobalKey formState; - final Map controllers; - - const LogEntryForm( - {Key? key, required this.formState, required this.controllers}) - : super(key: key); - - @override - _LogEntryFormState createState() => _LogEntryFormState(); -} - -class _LogEntryFormState extends State { - void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { - int? mgPerDl; - double? mmolPerL; - final _mgPerDlController = widget.controllers['mgPerDl']; - final _mmolPerLController = widget.controllers['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(() { - _mmolPerLController!.text = - Utils.convertMgPerDlToMmolPerL(mgPerDl!).toString(); - }); - } - if (mmolPerL != null && mgPerDl == null) { - setState(() { - _mgPerDlController!.text = - Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString(); - }); - } - } - - @override - Widget build(BuildContext context) { - // final _timeController = widget.controllers['time']; - final _mgPerDlController = widget.controllers['mgPerDl']; - final _mmolPerLController = widget.controllers['mmolPerL']; - final _bolusGlucoseController = widget.controllers['bolusGlucose']; - final _delayedBolusRateController = widget.controllers['delayedBolusRate']; - final _delayedBolusDurationController = - widget.controllers['delayedBolusDuration']; - final _notesController = widget.controllers['notes']; - - return SingleChildScrollView( - child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: < - Widget>[ - StyledForm( - formState: widget.formState, - fields: [ - // TODO: insert time picker - // Expanded( - // child: StyledTimeOfDayFormField( - // label: 'Time', - // controller: _timeController, - // onChanged: (newEndTime) { - // if (newEndTime != null) { - // setState(() { - // _endTime = newEndTime; - // }); - // updateEndTime(); - // } - //), - Row( - children: [ - glucoseMeasurement == GlucoseMeasurement.mgPerDl || - glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == GlucoseDisplayMode.bothForDetail - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'mg/dl', - suffixText: 'mg/dl', - ), - controller: _mgPerDlController, - onChanged: (_) => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mgPerDl), - keyboardType: const TextInputType.numberWithOptions(), - validator: (value) { - if (value!.trim().isEmpty && - _mmolPerLController!.text.trim().isEmpty) { - return 'How many mg/dl or mmol/l does the rate make up for?'; - } - return null; - }, - ), - ) - : Container(), - glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == GlucoseDisplayMode.bothForDetail - ? IconButton( - onPressed: () => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mmolPerL), - icon: const Icon(Icons.calculate), - ) - : Container(), - glucoseMeasurement == GlucoseMeasurement.mmolPerL || - glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == GlucoseDisplayMode.bothForDetail - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'mmol/l', - suffixText: 'mmol/l', - ), - controller: _mmolPerLController, - onChanged: (_) => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mmolPerL), - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - validator: (value) { - if (value!.trim().isEmpty && - _mgPerDlController!.text.trim().isEmpty) { - return 'How many mg/dl or mmol/l does rhe rate make up for?'; - } - return null; - }, - ), - ) - : Container(), - glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == GlucoseDisplayMode.bothForDetail - ? IconButton( - onPressed: () => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mgPerDl), - icon: const Icon(Icons.calculate), - ) - : Container(), - ], - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Bolus Units', - suffixText: 'U', - ), - controller: _bolusGlucoseController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - ), - // TODO: change field functionality according to time format - TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus Duration', - suffixText: ' min', - ), - controller: _delayedBolusDurationController, - keyboardType: TextInputType.number, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus Units', - ), - controller: _delayedBolusRateController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, - ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ]), - ); - } -} diff --git a/lib/screens/log/log_event/log_event_detail.dart b/lib/screens/log/log_event/log_event_detail.dart new file mode 100644 index 0000000..93ec4ab --- /dev/null +++ b/lib/screens/log/log_event/log_event_detail.dart @@ -0,0 +1,511 @@ +import 'package:diameter/components/detail.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'; +import 'package:diameter/models/log_event_type.dart'; +import 'package:diameter/models/settings.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/basal/basal_profile_detail.dart'; +import 'package:diameter/screens/bolus/bolus_profile_detail.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:flutter/material.dart'; + +class LogEventDetailScreen extends StatefulWidget { + static const String routeName = '/log-event'; + + final int logEntryId; + final int endLogEntryId; + final int id; + final DateTime? suggestedDate; + + const LogEventDetailScreen( + {Key? key, this.logEntryId = 0, this.endLogEntryId = 0, this.id = 0, this.suggestedDate}) + : super(key: key); + + @override + _LogEventDetailScreenState createState() => _LogEventDetailScreenState(); +} + +class _LogEventDetailScreenState extends State { + LogEvent? _logEvent; + bool _isNew = true; + bool _isSaving = false; + + List _bolusProfiles = []; + List _basalProfiles = []; + + final GlobalKey _logEventForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + late DateTime _time; + DateTime? _endTime; + + final _timeController = TextEditingController(text: ''); + final _endTimeController = TextEditingController(text: ''); + + final _dateController = TextEditingController(text: ''); + final _endDateController = TextEditingController(text: ''); + + final _reminderDurationController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + + LogEventType? _eventType; + final _eventTypeController = TextEditingController(text: ''); + + bool _hasEndTime = false; + + BolusProfile? _bolusProfile; + BasalProfile? _basalProfile; + final _bolusProfileController = TextEditingController(text: ''); + final _basalProfileController = TextEditingController(text: ''); + + List _logEventTypes = []; + + @override + void initState() { + super.initState(); + + reload(); + + _bolusProfiles = BolusProfile.getAll(); + _basalProfiles = BasalProfile.getAll(); + + if (widget.id != 0) { + _reminderDurationController.text = + (_logEvent!.reminderDuration ?? '').toString(); + _hasEndTime = _logEvent!.hasEndTime; + _notesController.text = _logEvent!.notes ?? ''; + + _eventType = _logEvent!.eventType.target; + _eventTypeController.text = (_eventType ?? '').toString(); + + _basalProfile = _logEvent!.basalProfile.target; + _basalProfileController.text = (_basalProfile ?? '').toString(); + + _bolusProfile = _logEvent!.bolusProfile.target; + _bolusProfileController.text = (_bolusProfile ?? '').toString(); + + _time = _logEvent!.time; + _endTime = _logEvent!.endTime; + } else { + _time = widget.suggestedDate ?? DateTime.now(); + } + + _logEventTypes = LogEventType.getAll(); + + updateTime(); + 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(() { + _logEvent = LogEvent.get(widget.id); + }); + } + _isNew = _logEvent == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void updateTime() { + _timeController.text = DateTimeUtils.displayTime(_time); + _dateController.text = DateTimeUtils.displayDate(_time); + } + + void updateEndTime() { + _endTimeController.text = DateTimeUtils.displayTime(_endTime); + _endDateController.text = DateTimeUtils.displayDate(_endTime); + } + + void updateBasalProfile(BasalProfile? value) { + setState(() { + _basalProfile = value; + _basalProfileController.text = (_basalProfile ?? '').toString(); + }); + } + + void updateBolusProfile(BolusProfile? value) { + setState(() { + _bolusProfile = value; + _bolusProfileController.text = (_bolusProfile ?? '').toString(); + }); + } + + void onSelectEventType(LogEventType? eventType) { + setState(() { + _eventType = eventType; + _eventTypeController.text = (_eventType ?? '').toString(); + }); + + if (eventType != null) { + setState(() { + _hasEndTime = eventType.hasEndTime; + if (eventType.defaultReminderDuration != null) { + _reminderDurationController.text = + eventType.defaultReminderDuration.toString(); + } + }); + + if (eventType.basalProfile.target != null) { + updateBasalProfile(eventType.basalProfile.target); + } + if (eventType.bolusProfile.target != null) { + updateBolusProfile(eventType.bolusProfile.target); + } + } + } + + Future checkIfActiveEventOfTypeExistsBeforeSaving() async { + if (_eventType != null && + LogEvent.eventTypeExistsForTime(_eventType!.id, _time)) { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: const Text( + 'An Event of this type is already active within the set time frame. What would you like to do?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'DISCARD'), + child: const Text('DISCARD'), + ), + TextButton( + onPressed: () => Navigator.pop(context, 'EDIT'), + child: const Text('KEEP EDITING'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, 'SAVE'), + child: const Text('SAVE'), + ) + ], + ); + }).then((value) async { + if (value == 'DISCARD') { + Navigator.pop(context); + } else if (value == 'SAVE') { + onSave(); + } + }); + } else { + onSave(); + } + } + + void onSave() { + LogEvent event = LogEvent( + id: widget.id, + time: _time, + endTime: _endTime, + hasEndTime: _hasEndTime, + reminderDuration: int.tryParse(_reminderDurationController.text), + notes: _notesController.text, + ); + event.eventType.target = _eventType; + event.basalProfile.target = _basalProfile; + event.bolusProfile.target = _bolusProfile; + LogEvent.put(event); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Event Saved', event]); + } + + void handleSaveAction() async { + setState(() { + _isSaving = true; + }); + if (_logEventForm.currentState!.validate()) { + await checkIfActiveEventOfTypeExistsBeforeSaving(); + } + setState(() { + _isSaving = false; + }); + } + + void handleCancelAction() { + if (Settings.get().showConfirmationDialogOnCancel && + ((_isNew && + (_notesController.text != '' || + _eventType != null || + _hasEndTime)) || + (!_isNew && + (_notesController.text != (_logEvent!.notes ?? '') || + _eventType != _logEvent!.eventType.target || + _hasEndTime != _logEvent!.hasEndTime)))) { + DialogUtils.showCancelConfirmationDialog( + context: context, + isNew: _isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + final now = DateTime.now(); + return Scaffold( + appBar: AppBar( + title: Text(_isNew ? 'New Event' : 'Edit Event'), + ), + drawer: const Navigation(currentLocation: LogEventDetailScreen.routeName), + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _logEventForm, + fields: [ + AutoCompleteDropdownButton( + controller: _eventTypeController, + selectedItem: _eventType, + label: 'Event Type', + items: _logEventTypes, + onChanged: onSelectEventType, + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: DateTimeFormField( + date: _time, + label: _hasEndTime ? 'Start Date' : 'Date', + controller: _dateController, + onChanged: (newTime) { + if (newTime != null) { + setState(() { + _time = DateTime(newTime.year, newTime.month, + newTime.day, _time.hour, _time.minute); + }); + updateTime(); + } + }, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5), + child: TimeOfDayFormField( + time: TimeOfDay.fromDateTime(_time), + label: _hasEndTime ? 'Start Time' : 'Time', + controller: _timeController, + onChanged: (newTime) { + if (newTime != null) { + setState(() { + _time = DateTime(_time.year, _time.month, + _time.day, newTime.hour, newTime.minute); + }); + updateTime(); + } + }, + ), + ), + ), + ], + ), + BooleanFormField( + value: _hasEndTime, + onChanged: (value) { + setState(() { + _hasEndTime = value; + }); + }, + label: 'has end time', + ), + Column( + children: _hasEndTime + ? [ + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: DateTimeFormField( + date: _endTime ?? now, + label: 'End Date', + controller: _endDateController, + onChanged: (newTime) { + if (newTime != null) { + setState(() { + _endTime = DateTime( + newTime.year, + newTime.month, + newTime.day, + _endTime?.hour ?? 0, + _endTime?.minute ?? 0); + }); + updateEndTime(); + } + }, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5), + child: TimeOfDayFormField( + time: TimeOfDay.fromDateTime( + _endTime ?? now), + label: 'End Time', + controller: _endTimeController, + onChanged: (newTime) { + if (newTime != null) { + setState(() { + _endTime = DateTime( + _endTime?.year ?? now.year, + _endTime?.month ?? now.month, + _endTime?.day ?? now.day, + newTime.hour, + newTime.minute); + }); + updateEndTime(); + } + }, + ), + ), + ), + ], + ), + 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( + children: [ + Expanded( + child: AutoCompleteDropdownButton< + BolusProfile>( + controller: _bolusProfileController, + selectedItem: _bolusProfile, + label: 'Bolus Profile', + items: _bolusProfiles, + onChanged: updateBolusProfile, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _bolusProfile == + null + ? const BolusProfileDetailScreen() + : BolusProfileDetailScreen( + id: _basalProfile!.id), + ), + ).then((result) { + updateBolusProfile(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_bolusProfile == null + ? Icons.add + : Icons.edit), + ), + ], + ), + 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), + ), + ], + ), + ) + ] + : []), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, + ), + ], + ), + ], + ), + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onAction: _isSaving ? null : handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/log/log_event/log_event_list.dart b/lib/screens/log/log_event/log_event_list.dart new file mode 100644 index 0000000..6ab1dc9 --- /dev/null +++ b/lib/screens/log/log_event/log_event_list.dart @@ -0,0 +1,366 @@ +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'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/navigation.dart'; + +class LogEventListScreen extends StatefulWidget { + static const String routeName = '/log-events'; + const LogEventListScreen({Key? key}) : super(key: key); + + @override + _LogEventListScreenState createState() => _LogEventListScreenState(); +} + +class _LogEventListScreenState extends State { + List _activeEvents = []; + late List _logEvents; + + final ScrollController _scrollController = ScrollController(); + + final TextEditingController _dateController = TextEditingController(text: ''); + + late DateTime _date; + bool _showActive = true; + + @override + void initState() { + super.initState(); + + _date = DateTime.now(); + _dateController.text = DateTimeUtils.displayDate(_date); + + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + _dateController.dispose(); + super.dispose(); + } + + void reload({String? message}) { + setState(() { + _activeEvents = LogEvent.getAllActiveForTime(DateTime.now()); + _logEvents = LogEvent.getAllForDate(_date); + }); + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void handleAddNewEvent() async { + final now = DateTime.now(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return LogEventDetailScreen( + suggestedDate: _date.isAtSameMomentAs(DateTime(now.year, now.month, now.day)) ? now : _date, + ); + }, + ), + ).then((result) => reload(message: result?[0])); + } + + void handleEditAction(LogEvent event) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LogEventDetailScreen( + id: event.id, + ), + ), + ).then((result) => reload(message: result?[0])); + } + + void onDelete(LogEvent logEvent) { + LogEvent.remove(logEvent.id); + reload(message: 'Event deleted'); + } + + void handleDeleteAction(LogEvent logEvent) async { + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(logEvent), + message: 'Are you sure you want to delete this Event?', + ); + } else { + onDelete(logEvent); + } + } + + void onStop(LogEvent event) async { + event.endTime = DateTime.now(); + LogEvent.put(event); + reload(message: 'Event ended'); + } + + void handleStopAction(LogEvent event) async { + if (Settings.get().showConfirmationDialogOnStopEvent) { + DialogUtils.showConfirmationDialog( + context: context, + onConfirm: () => onStop(event), + message: 'Are you sure you want to end this Event?', + confirmationLabel: 'END EVENT', + ); + } else { + onStop(event); + } + } + + void onChangeDate(DateTime? date) { + if (date != null) { + setState(() { + _date = DateTime(date.year, date.month, date.day); + _dateController.text = DateTimeUtils.displayDate(date); + }); + reload(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Log Events'), + actions: [ + IconButton( + onPressed: () => onChangeDate(DateTime.now()), + icon: const Icon(Icons.today)), + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) + ], + ), + drawer: const Navigation(currentLocation: LogEventListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () => setState(() { + _showActive = !_showActive; + }), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + 'ACTIVE EVENTS', + style: Theme.of(context).textTheme.subtitle2, + textAlign: TextAlign.center, + ), + ), + Icon(_showActive + ? Icons.expand_less + : Icons.expand_more), + ], + ), + ), + ), + !_showActive ? Container() : + _activeEvents.isNotEmpty + ? ListView.builder( + shrinkWrap: true, + padding: const EdgeInsets.all(10.0), + itemCount: _activeEvents.length, + itemBuilder: (context, index) { + LogEvent event = _activeEvents[index]; + return Card( + child: ListTile( + onTap: () { + handleEditAction(event); + }, + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + DateTimeUtils.displayDateTime(event.time), + ), + const SizedBox(width: 24), + Expanded( + child: Text( + (event.title ?? + event.eventType.target?.value ?? + '') + .toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + event.hasEndTime && event.endTime == null + ? IconButton( + icon: const Icon( + Icons.stop, + color: Colors.blue, + ), + onPressed: () => handleStopAction(event), + ) + : const SizedBox(width: 50), + IconButton( + icon: const Icon( + Icons.edit, + color: Colors.blue, + ), + onPressed: () => handleEditAction(event), + ), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(event), + ), + ], + ), + ), + ); + }, + ) + : const Center( + child: Text('There are no Active Events!'), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider(), + ), + Row( + children: [ + IconButton( + onPressed: _date.isAtSameMomentAs(DateTime(2000, 1, 1)) + ? null + : () => + onChangeDate(_date.subtract(const Duration(days: 1))), + icon: const Icon(Icons.arrow_back), + ), + Expanded( + child: GestureDetector( + onTap: () async { + final newTime = await showDatePicker( + context: context, + initialDate: _date, + firstDate: DateTime(2000, 1, 1), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + onChangeDate(newTime); + }, + child: Expanded( + child: Text( + DateTimeUtils.displayDate(_date).toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + textAlign: TextAlign.center, + ), + ), + ), + ), + IconButton( + onPressed: + _date.add(const Duration(days: 1)).isBefore(DateTime.now()) + ? () => onChangeDate(_date.add(const Duration(days: 1))) + : null, + icon: const Icon(Icons.arrow_forward), + ), + ], + ), + Expanded( + child: _logEvents.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + shrinkWrap: true, + padding: const EdgeInsets.all(10.0), + itemCount: _logEvents.length, + itemBuilder: (context, index) { + LogEvent event = _logEvents[index]; + return Card( + child: ListTile( + onTap: () { + handleEditAction(event); + }, + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + DateTimeUtils.displayTime(event.isEndEvent + ? event.endTime + : event.time), + ), + const SizedBox(width: 24), + Expanded( + child: Text( + (event.title ?? + event.eventType.target?.value ?? + '') + .toUpperCase(), + style: + Theme.of(context).textTheme.subtitle2, + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + event.hasEndTime && event.endTime == null + ? IconButton( + icon: const Icon( + Icons.stop, + color: Colors.blue, + ), + onPressed: () => + handleStopAction(event), + ) + : const SizedBox(width: 50), + IconButton( + icon: const Icon( + Icons.edit, + color: Colors.blue, + ), + onPressed: () => handleEditAction(event), + ), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(event), + ), + ], + ), + ), + ); + }, + )) + : const Center( + child: Text('There are no Events for that date!'), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: handleAddNewEvent, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/log/log_event/log_event_type_detail.dart b/lib/screens/log/log_event/log_event_type_detail.dart new file mode 100644 index 0000000..eb584c5 --- /dev/null +++ b/lib/screens/log/log_event/log_event_type_detail.dart @@ -0,0 +1,304 @@ +import 'package:diameter/components/detail.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'; +import 'package:diameter/models/settings.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/basal/basal_profile_detail.dart'; +import 'package:diameter/screens/bolus/bolus_profile_detail.dart'; +import 'package:flutter/material.dart'; + +class EventTypeDetailScreen extends StatefulWidget { + static const String routeName = '/log-event-type'; + final int id; + const EventTypeDetailScreen({Key? key, this.id = 0}) : super(key: key); + + @override + _EventTypeDetailScreenState createState() => _EventTypeDetailScreenState(); +} + +class _EventTypeDetailScreenState extends State { + LogEventType? _logEventType; + bool _isNew = true; + bool _isSaving = false; + + List _bolusProfiles = []; + List _basalProfiles = []; + + final GlobalKey _logEventTypeForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + final _valueController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + + bool _hasEndTime = false; + int _defaultReminderDuration = 0; + BolusProfile? _bolusProfile; + BasalProfile? _basalProfile; + final _bolusProfileController = TextEditingController(text: ''); + final _basalProfileController = TextEditingController(text: ''); + + @override + void initState() { + super.initState(); + + reload(); + + _bolusProfiles = BolusProfile.getAll(); + _basalProfiles = BasalProfile.getAll(); + + if (_logEventType != null) { + _valueController.text = _logEventType!.value; + _defaultReminderDuration = + _logEventType!.defaultReminderDuration ?? 0; + _hasEndTime = _logEventType!.hasEndTime; + _notesController.text = _logEventType!.notes ?? ''; + _basalProfile = _logEventType!.basalProfile.target; + _basalProfileController.text = (_basalProfile ?? '').toString(); + _bolusProfile = _logEventType!.bolusProfile.target; + _bolusProfileController.text = (_bolusProfile ?? '').toString(); + } + } + + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + _bolusProfileController.dispose(); + _basalProfileController.dispose(); + super.dispose(); + } + + void reload({String? message}) { + if (widget.id != 0) { + setState(() { + _logEventType = LogEventType.get(widget.id); + }); + } + _isNew = _logEventType == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void updateBasalProfile(BasalProfile? value) { + setState(() { + _basalProfile = value; + _basalProfileController.text = (_basalProfile ?? '').toString(); + }); + } + + void updateBolusProfile(BolusProfile? value) { + setState(() { + _bolusProfile = value; + _bolusProfileController.text = (_bolusProfile ?? '').toString(); + }); + } + + void handleSaveAction() async { + setState(() { + _isSaving = true; + }); + if (_logEventTypeForm.currentState!.validate()) { + LogEventType eventType = LogEventType( + id: widget.id, + value: _valueController.text, + notes: _notesController.text, + defaultReminderDuration: _defaultReminderDuration, + hasEndTime: _hasEndTime, + ); + eventType.basalProfile.target = _basalProfile; + eventType.bolusProfile.target = _bolusProfile; + LogEventType.put(eventType); + Navigator.pop( + context, ['${_isNew ? 'New' : ''} Log Event Type Saved', eventType]); + } + setState(() { + _isSaving = false; + }); + } + + void handleCancelAction() { + bool isNew = _logEventType == null; + if (Settings.get().showConfirmationDialogOnCancel && + ((isNew && + (_valueController.text != '' || + _defaultReminderDuration != 0 || + _notesController.text != '' || + _hasEndTime)) || + (!isNew && + (_valueController.text != _logEventType!.value || + _defaultReminderDuration != + _logEventType!.defaultReminderDuration || + _notesController.text != (_logEventType!.notes ?? '') || + _hasEndTime != _logEventType!.hasEndTime)))) { + DialogUtils.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_isNew ? 'New Log Event Type' : _logEventType!.value), + ), + drawer: + const Navigation(currentLocation: EventTypeDetailScreen.routeName), + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper(formState: _logEventTypeForm, fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), + BooleanFormField( + value: _hasEndTime, + label: 'has end time', + onChanged: (value) { + setState(() { + _hasEndTime = value; + }); + }, + ), + Column( + children: _hasEndTime + ? [ + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: DurationFormField( + minutes: _defaultReminderDuration, + label: 'Default Reminder Duration', + onChanged: (value) => _defaultReminderDuration = value ?? 0, + showSteppers: true, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton< + BolusProfile>( + selectedItem: _bolusProfile, + controller: _bolusProfileController, + label: 'Bolus Profile', + items: _bolusProfiles, + onChanged: updateBolusProfile, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _bolusProfile == + null + ? const BolusProfileDetailScreen() + : BolusProfileDetailScreen( + id: _bolusProfile!.id), + ), + ).then((result) { + setState(() { + updateBolusProfile(result?[1]); + }); + reload(message: result?[0]); + }); + }, + icon: Icon(_bolusProfile == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + Row( + children: [ + Expanded( + child: + AutoCompleteDropdownButton( + 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), + ), + ], + ), + ] + : []), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, + ), + ]), + ], + ), + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onAction: _isSaving ? null : handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/log/log_event/log_event_type_list.dart b/lib/screens/log/log_event/log_event_type_list.dart new file mode 100644 index 0000000..07be148 --- /dev/null +++ b/lib/screens/log/log_event/log_event_type_list.dart @@ -0,0 +1,123 @@ +import 'package:diameter/models/log_event_type.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/log/log_event/log_event_type_detail.dart'; +import 'package:flutter/material.dart'; + +class LogEventTypeListScreen extends StatefulWidget { + static const String routeName = '/log-event-types'; + const LogEventTypeListScreen({Key? key}) : super(key: key); + + @override + _LogEventTypeListScreenState createState() => _LogEventTypeListScreenState(); +} + +class _LogEventTypeListScreenState extends State { + List _logEventTypes = []; + + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { + setState(() { + _logEventTypes = LogEventType.getAll(); + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Log Event Types'), actions: [ + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) + ]), + drawer: + const Navigation(currentLocation: LogEventTypeListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: _logEventTypes.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(10.0), + itemCount: _logEventTypes.length, + itemBuilder: (context, index) { + final logEventType = _logEventTypes[index]; + return Card( + child: ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EventTypeDetailScreen( + id: logEventType.id), + ), + ).then((result) => reload(message: result?[0])); + }, + title: Text( + logEventType.value.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + ), + subtitle: Text(logEventType.notes ?? ''), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () async { + LogEventType.remove(logEventType.id); + reload(message: 'Log Event Type deleted'); + }, + icon: const Icon(Icons.delete, + color: Colors.blue), + ) + ], + ), + ), + ); + }, + ), + ) + : const Center( + child: + Text('You have not created any Log Event Types yet!'), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const EventTypeDetailScreen(), + ), + ).then((result) => reload(message: result?[0])); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/log/log_event_detail.dart b/lib/screens/log/log_event_detail.dart deleted file mode 100644 index 61145d9..0000000 --- a/lib/screens/log/log_event_detail.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/models/log_entry.dart'; -import 'package:diameter/models/log_event.dart'; -import 'package:diameter/models/log_event_type.dart'; -import 'package:diameter/navigation.dart'; -import 'package:flutter/material.dart'; - -class LogEventDetailScreen extends StatefulWidget { - static const String routeName = '/log-event'; - final LogEntry? logEntry; - final LogEntry? endLogEntry; - final LogEvent? logEvent; - const LogEventDetailScreen( - {Key? key, this.logEntry, this.endLogEntry, this.logEvent}) - : super(key: key); - - @override - _LogEventDetailScreenState createState() => _LogEventDetailScreenState(); -} - -class _LogEventDetailScreenState extends State { - final GlobalKey _logEventForm = GlobalKey(); - - final _notesController = TextEditingController(text: ''); - LogEventType? _eventType; - bool _hasEndTime = false; - - List _logEventTypes = []; - - bool _isSaving = false; - - @override - void initState() { - super.initState(); - - if (widget.logEvent != null) { - _notesController.text = widget.logEvent!.notes ?? ''; - _eventType = widget.logEvent!.eventType.target; - _hasEndTime = widget.logEvent!.hasEndTime; - } - - _logEventTypes = LogEventType.getAll(); - } - - void handleSaveAction() async { - setState(() { - _isSaving = true; - }); - if (_logEventForm.currentState!.validate()) { - bool isNew = widget.logEvent == null; - // isNew - // ? await LogEvent.save( - // logEntry: widget.logEntry!.objectId!, - // eventType: _eventType!, - // time: widget.logEntry!.time, - // hasEndTime: _hasEndTime, - // notes: _notesController.text, - // ) - // : await LogEvent.update( - // widget.logEvent!.objectId!, - // eventType: _eventType!, - // time: widget.logEntry!.time, - // hasEndTime: _hasEndTime, - // notes: _notesController.text, - // ); - LogEvent event = LogEvent( - id: widget.logEvent?.id ?? 0, - time: widget.logEntry!.time, - hasEndTime: _hasEndTime, - notes: _notesController.text, - ); - event.eventType.target = _eventType; - LogEvent.put(event); - Navigator.pop(context, '${isNew ? 'New' : ''} Event Saved'); - } - setState(() { - _isSaving = false; - }); - } - - void handleCancelAction() { - bool isNew = widget.logEvent == null; - if (showConfirmationDialogOnCancel && - ((isNew && - (_notesController.text != '' || - _eventType != null || - _hasEndTime)) || - (!isNew && - (_notesController.text != (widget.logEvent!.notes ?? '') || - _eventType != widget.logEvent!.eventType.target || - _hasEndTime != widget.logEvent!.hasEndTime)))) { - Dialogs.showCancelConfirmationDialog( - context: context, - isNew: isNew, - onSave: handleSaveAction, - ); - } else { - Navigator.pop(context); - } - } - - @override - Widget build(BuildContext context) { - bool isNew = widget.logEvent == null; - return Scaffold( - appBar: AppBar( - title: Text(isNew ? 'New Event' : 'Edit Event'), - ), - drawer: const Navigation(currentLocation: LogEventDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _logEventForm, - fields: [ - StyledDropdownButton( - selectedItem: _eventType, - label: 'Event Type', - items: _logEventTypes, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _eventType = value; - }); - }, - ), - // StyledFutureDropdownButton( - // selectedItem: _eventType, - // label: 'Event Type', - // items: _logEventTypes, - // getItemValue: (item) => item.objectId, - // renderItem: (item) => Text(item.value), - // onChanged: (value) { - // setState(() { - // _eventType = value; - // }); - // }, - // ), - StyledBooleanFormField( - value: _hasEndTime, - onChanged: (value) { - setState(() { - _hasEndTime = value; - }); - }, - label: 'active', - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, - ), - keyboardType: TextInputType.multiline, - ), - ], - ), - // ActiveLogEventListScreen(onSetEndTime: onSetEndTime) - ], - ), - ), - bottomNavigationBar: DetailBottomRow( - onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, - ), - ); - } -} diff --git a/lib/screens/log/log_event_list.dart b/lib/screens/log/log_event_list.dart deleted file mode 100644 index bd9503d..0000000 --- a/lib/screens/log/log_event_list.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/models/log_entry.dart'; -import 'package:diameter/models/log_event.dart'; -import 'package:diameter/screens/log/log_event_detail.dart'; -import 'package:diameter/utils/date_time_utils.dart'; -import 'package:flutter/material.dart'; - -class LogEventListScreen extends StatefulWidget { - final LogEntry logEntry; - final Function() reload; - - const LogEventListScreen({Key? key, required this.logEntry, required this.reload}) - : super(key: key); - - @override - _LogEventListScreenState createState() => _LogEventListScreenState(); -} - -class _LogEventListScreenState extends State { - void reload({String? message}) { - widget.reload(); - - setState(() { - if (message != null) { - var snackBar = SnackBar( - content: Text(message), - duration: const Duration(seconds: 2), - ); - ScaffoldMessenger.of(context) - ..removeCurrentSnackBar() - ..showSnackBar(snackBar); - } - }); - } - - void handleEditAction(LogEvent event) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LogEventDetailScreen( - endLogEntry: widget.logEntry, - logEvent: event, - ), - ), - ).then((message) => reload(message: message)); - } - - void onDelete(LogEvent logEvent) { - LogEvent.remove(logEvent.id); - reload(message: 'Event deleted'); - } - - void handleDeleteAction(LogEvent logEvent) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( - context: context, - onConfirm: () => onDelete(logEvent), - message: 'Are you sure you want to delete this Event?', - ); - } else { - onDelete(logEvent); - } - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // TODO: add button for active events - Expanded( - child: (widget.logEntry.events.isNotEmpty || widget.logEntry.endedEvents.isNotEmpty) - ? ListView.builder( - shrinkWrap: true, - itemCount: widget.logEntry.events.length + widget.logEntry.endedEvents.length, - itemBuilder: (context, index) { - final event = (widget.logEntry.events + widget.logEntry.endedEvents)[index]; - return ListTile( - onTap: () { - handleEditAction(event); - }, - title: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Text(event.eventType.target?.value ?? '')), - ], - ), - subtitle: Text( - '${DateTimeUtils.displayDateTime(event.time)}${event.hasEndTime ? ' - ${DateTimeUtils.displayDateTime(event.endTime, fallback: '(ongoing)')}' : ''}'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => handleDeleteAction(event), - ), - ], - ), - ); - }) - : const Center( - child: Text('You have not added any Events to this Log Entry yet!'), - ), - ), - ], - ); - } -} diff --git a/lib/screens/log/log_event_type_detail.dart b/lib/screens/log/log_event_type_detail.dart deleted file mode 100644 index 9bfa4db..0000000 --- a/lib/screens/log/log_event_type_detail.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/models/log_event_type.dart'; -import 'package:diameter/navigation.dart'; -import 'package:flutter/material.dart'; - -class LogEventTypeDetailScreen extends StatefulWidget { - static const String routeName = '/log-event-type'; - final LogEventType? logEventType; - const LogEventTypeDetailScreen({Key? key, this.logEventType}) - : super(key: key); - - @override - _LogEventTypeDetailScreenState createState() => - _LogEventTypeDetailScreenState(); -} - -class _LogEventTypeDetailScreenState extends State { - final GlobalKey _logEventTypeForm = GlobalKey(); - final _valueController = TextEditingController(text: ''); - final _defaultReminderDurationController = TextEditingController(text: ''); - final _notesController = TextEditingController(text: ''); - bool _hasEndTime = false; - - bool _isSaving = false; - - @override - void initState() { - super.initState(); - - if (widget.logEventType != null) { - _valueController.text = widget.logEventType!.value; - _defaultReminderDurationController.text = - (widget.logEventType!.defaultReminderDuration ?? '').toString(); - _notesController.text = widget.logEventType!.notes ?? ''; - _hasEndTime = widget.logEventType!.hasEndTime; - } - } - - void handleSaveAction() async { - setState(() { - _isSaving = true; - }); - if (_logEventTypeForm.currentState!.validate()) { - bool isNew = widget.logEventType == null; - // isNew - // ? await LogEventType.save( - // value: _valueController.text, - // notes: _notesController.text, - // defaultReminderDuration: - // int.tryParse(_defaultReminderDurationController.text), - // hasEndTime: _hasEndTime, - // ) - // : await LogEventType.update( - // widget.logEventType!.objectId!, - // value: _valueController.text, - // notes: _notesController.text, - // defaultReminderDuration: - // int.tryParse(_defaultReminderDurationController.text), - // hasEndTime: _hasEndTime, - // ); - LogEventType.put(LogEventType( - id: widget.logEventType?.id ?? 0, - value: _valueController.text, - notes: _notesController.text, - defaultReminderDuration: - int.tryParse(_defaultReminderDurationController.text), - hasEndTime: _hasEndTime, - )); - Navigator.pop(context, '${isNew ? 'New' : ''} Log Event Type Saved'); - } - setState(() { - _isSaving = false; - }); - } - - void handleCancelAction() { - bool isNew = widget.logEventType == null; - if (showConfirmationDialogOnCancel && - ((isNew && - (_valueController.text != '' || - int.tryParse(_defaultReminderDurationController.text) != - null || - _notesController.text != '' || - _hasEndTime)) || - (!isNew && - (_valueController.text != widget.logEventType!.value || - int.tryParse(_defaultReminderDurationController.text) != - widget.logEventType!.defaultReminderDuration || - _notesController.text != - (widget.logEventType!.notes ?? '') || - _hasEndTime != widget.logEventType!.hasEndTime)))) { - Dialogs.showCancelConfirmationDialog( - context: context, - isNew: isNew, - onSave: handleSaveAction, - ); - } else { - Navigator.pop(context); - } - } - - @override - Widget build(BuildContext context) { - bool isNew = widget.logEventType == null; - return Scaffold( - appBar: AppBar( - title: Text(isNew ? 'New Log Event Type' : widget.logEventType!.value), - ), - drawer: - const Navigation(currentLocation: LogEventTypeDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _logEventTypeForm, - fields: [ - TextFormField( - controller: _valueController, - decoration: const InputDecoration( - labelText: 'Name', - alignLabelWithHint: true, - ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty name'; - } - return null; - }, - ), - StyledBooleanFormField( - value: _hasEndTime, - label: 'has end time', - onChanged: (value) { - setState(() { - _hasEndTime = value; - }); - }, - ), - TextFormField( - controller: _defaultReminderDurationController, - keyboardType: const TextInputType.numberWithOptions(), - decoration: InputDecoration( - labelText: 'Default Reminder Duration', - suffixText: ' min', - enabled: _hasEndTime, - ), - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, - ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ], - ), - ), - bottomNavigationBar: DetailBottomRow( - onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, - ), - ); - } -} diff --git a/lib/screens/log/log_event_type_list.dart b/lib/screens/log/log_event_type_list.dart deleted file mode 100644 index e6c7cfb..0000000 --- a/lib/screens/log/log_event_type_list.dart +++ /dev/null @@ -1,109 +0,0 @@ -// import 'package:diameter/components/progress_indicator.dart'; -import 'package:diameter/models/log_event_type.dart'; -import 'package:diameter/navigation.dart'; -import 'package:diameter/screens/log/log_event_type_detail.dart'; -import 'package:flutter/material.dart'; - -class LogEventTypeListScreen extends StatefulWidget { - static const String routeName = '/log-event-types'; - const LogEventTypeListScreen({Key? key}) : super(key: key); - - @override - _LogEventTypeListScreenState createState() => _LogEventTypeListScreenState(); -} - -class _LogEventTypeListScreenState extends State { - List _logEventTypes = []; - - void refresh({String? message}) { - setState(() { - _logEventTypes = LogEventType.getAll(); - }); - setState(() { - if (message != null) { - var snackBar = SnackBar( - content: Text(message), - duration: const Duration(seconds: 2), - ); - ScaffoldMessenger.of(context) - ..removeCurrentSnackBar() - ..showSnackBar(snackBar); - } - }); - } - - @override - void initState() { - super.initState(); - refresh(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Log Event Types'), actions: [ - IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) - ]), - drawer: - const Navigation(currentLocation: LogEventTypeListScreen.routeName), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: _logEventTypes.isNotEmpty ? ListView.builder( - padding: const EdgeInsets.all(10.0), - itemCount: _logEventTypes.length, - itemBuilder: (context, index) { - // final logEventType = snapshot.data![index]; - final logEventType = _logEventTypes[index]; - return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - LogEventTypeDetailScreen( - logEventType: logEventType), - ), - ).then((message) => refresh(message: message)); - }, - title: Text(logEventType.value), - subtitle: Text(logEventType.notes ?? ''), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () async { - LogEventType.remove(logEventType.id); - // await logEventType.delete().then((_) { - refresh( - message: 'Log Event Type deleted'); - // }); - }, - icon: const Icon(Icons.delete, - color: Colors.blue), - ) - ], - ), - ); - }, - ) : const Center( - child: Text('You have not created any Log Event Types yet!'), - ), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const LogEventTypeDetailScreen(), - ), - ).then((message) => refresh(message: message)); - }, - child: const Icon(Icons.add), - ), - ); - } -} diff --git a/lib/screens/log/log_meal_detail.dart b/lib/screens/log/log_meal_detail.dart deleted file mode 100644 index 540e302..0000000 --- a/lib/screens/log/log_meal_detail.dart +++ /dev/null @@ -1,561 +0,0 @@ -import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/models/accuracy.dart'; -import 'package:diameter/models/log_entry.dart'; -import 'package:diameter/models/log_meal.dart'; -import 'package:diameter/models/meal.dart'; -import 'package:diameter/models/meal_category.dart'; -import 'package:diameter/models/meal_portion_type.dart'; -import 'package:diameter/models/meal_source.dart'; -import 'package:diameter/navigation.dart'; -import 'package:diameter/settings.dart'; -import 'package:diameter/utils/utils.dart'; -import 'package:flutter/material.dart'; - -class LogMealDetailScreen extends StatefulWidget { - static const String routeName = '/log-meal'; - final LogEntry logEntry; - final LogMeal? logMeal; - const LogMealDetailScreen({Key? key, required this.logEntry, this.logMeal}) - : super(key: key); - - @override - _LogMealDetailScreenState createState() => _LogMealDetailScreenState(); -} - -class _LogMealDetailScreenState extends State { - final GlobalKey _logMealForm = GlobalKey(); - - final _valueController = TextEditingController(text: ''); - final _carbsRatioController = TextEditingController(text: ''); - final _portionSizeController = TextEditingController(text: ''); - final _carbsPerPortionController = TextEditingController(text: ''); - final _bolusController = TextEditingController(text: ''); - final _delayedBolusRateController = TextEditingController(text: ''); - final _delayedBolusDurationController = TextEditingController(text: ''); - final _notesController = TextEditingController(text: ''); - Meal? _meal; - MealSource? _mealSource; - MealCategory? _mealCategory; - MealPortionType? _mealPortionType; - Accuracy? _portionSizeAccuracy; - Accuracy? _carbsRatioAccuracy; - - List _meals = []; - List _mealCategories = []; - List _mealPortionTypes = []; - List _mealSources = []; - List _portionSizeAccuracies = []; - List _carbsRatioAccuracies = []; - - bool _isSaving = false; - - @override - void initState() { - super.initState(); - - _portionSizeAccuracies = Accuracy.getAllForPortionSize(); - _carbsRatioAccuracies = Accuracy.getAllForCarbsRatio(); - _meals = Meal.getAll(); - _mealCategories = MealCategory.getAll(); - _mealPortionTypes = MealPortionType.getAll(); - _mealSources = MealSource.getAll(); - - if (widget.logMeal != null) { - _valueController.text = widget.logMeal!.value; - _carbsRatioController.text = - (widget.logMeal!.carbsRatio ?? '').toString(); - _portionSizeController.text = - (widget.logMeal!.portionSize ?? '').toString(); - _carbsPerPortionController.text = - (widget.logMeal!.carbsPerPortion ?? '').toString(); - _bolusController.text = (widget.logMeal!.bolus ?? '').toString(); - _delayedBolusRateController.text = - (widget.logMeal!.delayedBolusRate ?? '').toString(); - _delayedBolusDurationController.text = - (widget.logMeal!.delayedBolusDuration ?? '').toString(); - _notesController.text = widget.logMeal!.notes ?? ''; - - // _meal = widget.logMeal!.meal; - // _source = widget.logMeal!.source; - // _category = widget.logMeal!.category; - // _portionType = widget.logMeal!.portionType; - // _portionSizeAccuracy = _portionSizeAccuracies.firstWhere((element) => - // element.id == - // int.tryParse(widget.logMeal!.portionSizeAccuracy ?? '')); - // _carbsRatioAccuracy = _carbsRatioAccuracies.firstWhere((element) => - // element.id == int.tryParse(widget.logMeal!.carbsRatioAccuracy ?? '')); - // _portionSizeAccuracy = widget.meal!.portionSizeAccuracy; - // _carbsRatioAccuracy = widget.meal!.carbsRatioAccuracy; - } - } - - Future onSelectMeal(Meal meal) async { - setState(() { - _meal = meal; - _valueController.text = meal.value; - if (meal.carbsRatio != null) { - _carbsRatioController.text = meal.carbsRatio.toString(); - } - if (meal.portionSize != null) { - _portionSizeController.text = meal.portionSize.toString(); - } - if (meal.carbsPerPortion != null) { - _carbsPerPortionController.text = meal.carbsPerPortion.toString(); - } - if (meal.delayedBolusRate != null) { - _delayedBolusRateController.text = meal.delayedBolusRate.toString(); - } - if (meal.delayedBolusDuration != null) { - _delayedBolusDurationController.text = - meal.delayedBolusDuration.toString(); - } - if (meal.mealSource.hasValue) { - _mealSource = meal.mealSource.target; - } - if (meal.mealCategory.hasValue) { - _mealCategory = meal.mealCategory.target; - } - if (meal.mealPortionType.hasValue) { - _mealPortionType = meal.mealPortionType.target; - } - if (meal.portionSizeAccuracy.hasValue) { - _portionSizeAccuracy = meal.portionSizeAccuracy.target; - } - if (meal.carbsRatioAccuracy.hasValue) { - _carbsRatioAccuracy = meal.carbsRatioAccuracy.target; - } - }); - } - - void handleSaveAction() async { - setState(() { - _isSaving = true; - }); - if (_logMealForm.currentState!.validate()) { - bool isNew = widget.logMeal == null; - // isNew - // ? await LogMeal.save( - // logEntry: widget.logEntry.objectId!, - // meal: _meal, - // value: _valueController.text, - // source: _mealSource, - // category: _category, - // portionType: _portionType, - // carbsRatio: double.tryParse(_carbsRatioController.text), - // portionSize: double.tryParse(_portionSizeController.text), - // carbsPerPortion: double.tryParse(_carbsPerPortionController.text), - // // portionSizeAccuracy: _portionSizeAccuracy, - // // carbsRatioAccuracy: _carbsRatioAccuracy, - // portionSizeAccuracy: _portionSizeAccuracy?.id.toString(), - // carbsRatioAccuracy: _carbsRatioAccuracy?.id.toString(), - // bolus: double.tryParse(_bolusController.text), - // delayedBolusDuration: - // int.tryParse(_delayedBolusDurationController.text), - // delayedBolusRate: - // double.tryParse(_delayedBolusRateController.text), - // notes: _notesController.text, - // ) - // : await LogMeal.update( - // widget.logMeal!.objectId!, - // meal: _meal, - // value: _valueController.text, - // source: _mealSource, - // category: _category, - // portionType: _portionType, - // carbsRatio: double.tryParse(_carbsRatioController.text), - // portionSize: double.tryParse(_portionSizeController.text), - // carbsPerPortion: double.tryParse(_carbsPerPortionController.text), - // // portionSizeAccuracy: _portionSizeAccuracy, - // // carbsRatioAccuracy: _carbsRatioAccuracy, - // portionSizeAccuracy: _portionSizeAccuracy?.id.toString(), - // carbsRatioAccuracy: _carbsRatioAccuracy?.id.toString(), - // bolus: double.tryParse(_bolusController.text), - // delayedBolusDuration: - // int.tryParse(_delayedBolusDurationController.text), - // delayedBolusRate: - // double.tryParse(_delayedBolusRateController.text), - // notes: _notesController.text, - // ); - LogMeal logMeal = LogMeal( - id: widget.logMeal?.id ?? 0, - value: _valueController.text, - carbsRatio: double.tryParse(_carbsRatioController.text), - portionSize: double.tryParse(_portionSizeController.text), - carbsPerPortion: double.tryParse(_carbsPerPortionController.text), - bolus: double.tryParse(_bolusController.text), - delayedBolusDuration: - int.tryParse(_delayedBolusDurationController.text), - delayedBolusRate: double.tryParse(_delayedBolusRateController.text), - notes: _notesController.text, - ); - logMeal.meal.target = _meal; - logMeal.mealSource.target = _mealSource; - logMeal.mealCategory.target = _mealCategory; - logMeal.mealPortionType.target = _mealPortionType; - logMeal.portionSizeAccuracy.target = _portionSizeAccuracy; - logMeal.carbsRatioAccuracy.target = _carbsRatioAccuracy; - - LogMeal.put(logMeal); - Navigator.pop(context, '${isNew ? 'New' : ''} Meal Saved'); - } - setState(() { - _isSaving = false; - }); - } - - void handleCancelAction() { - bool isNew = widget.logMeal == null; - if (showConfirmationDialogOnCancel && - ((isNew && - (_valueController.text != '' || - _meal != null || - _mealSource != null || - _mealCategory != null || - _mealPortionType != null || - double.tryParse(_carbsRatioController.text) != null || - double.tryParse(_portionSizeController.text) != null || - double.tryParse(_carbsPerPortionController.text) != null || - _carbsRatioAccuracy != null || - _portionSizeAccuracy != null || - double.tryParse(_bolusController.text) != null || - int.tryParse(_delayedBolusDurationController.text) != - null || - double.tryParse(_delayedBolusRateController.text) != null || - _notesController.text != '')) || - (!isNew && - (_valueController.text != widget.logMeal!.value || - _meal != widget.logMeal!.meal.target || - _mealSource != widget.logMeal!.mealSource.target || - _mealCategory != widget.logMeal!.mealCategory.target || - _mealPortionType != widget.logMeal!.mealPortionType.target || - double.tryParse(_carbsRatioController.text) != - widget.logMeal!.carbsRatio || - double.tryParse(_portionSizeController.text) != - widget.logMeal!.portionSize || - double.tryParse(_carbsPerPortionController.text) != - widget.logMeal!.carbsPerPortion || - // _carbsRatioAccuracy != widget.logMeal!.carbsRatioAccuracy || - // _portionSizeAccuracy != - // widget.logMeal!.portionSizeAccuracy || - _carbsRatioAccuracy != - widget.logMeal!.carbsRatioAccuracy.target || - _portionSizeAccuracy != - widget.logMeal!.portionSizeAccuracy.target || - double.tryParse(_bolusController.text) != - widget.logMeal!.bolus || - int.tryParse(_delayedBolusDurationController.text) != - widget.logMeal!.delayedBolusDuration || - double.tryParse(_delayedBolusRateController.text) != - widget.logMeal!.delayedBolusRate || - _notesController.text != (widget.logMeal!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( - context: context, - isNew: isNew, - onSave: handleSaveAction, - ); - } else { - Navigator.pop(context); - } - } - - void calculateThirdMeasurementOfPortionCarbsRelation( - {PortionCarbsParameter? parameterToBeCalculated}) { - double? carbsRatio; - double? portionSize; - double? carbsPerPortion; - - if (parameterToBeCalculated != PortionCarbsParameter.carbsRatio && - _carbsRatioController.text != '') { - carbsRatio = double.tryParse(_carbsRatioController.text); - } - if (parameterToBeCalculated != PortionCarbsParameter.portionSize && - _portionSizeController.text != '') { - portionSize = double.tryParse(_portionSizeController.text); - } - if (parameterToBeCalculated != PortionCarbsParameter.carbsPerPortion && - _carbsRatioController.text != '') { - carbsPerPortion = double.tryParse(_carbsPerPortionController.text); - } - - if (carbsRatio != null && portionSize != null && carbsPerPortion == null) { - setState(() { - _carbsPerPortionController.text = - Utils.calculateCarbsPerPortion(carbsRatio!, portionSize!) - .toString(); - }); - } - if (carbsRatio == null && portionSize != null && carbsPerPortion != null) { - setState(() { - _carbsRatioController.text = - Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!) - .toString(); - }); - } - if (carbsRatio != null && portionSize == null && carbsPerPortion != null) { - setState(() { - _portionSizeController.text = - Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!) - .toString(); - }); - } - } - - @override - Widget build(BuildContext context) { - bool isNew = widget.logMeal == null; - return Scaffold( - appBar: AppBar( - title: Text(isNew ? 'New Meal' : widget.logMeal!.value), - ), - drawer: const Navigation(currentLocation: LogMealDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _logMealForm, - fields: [ - TextFormField( - controller: _valueController, - decoration: const InputDecoration( - labelText: 'Name', - ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty name'; - } - return null; - }, - ), - StyledDropdownButton( - selectedItem: _meal, - label: 'Meal', - items: _meals, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - if (value != null) { - onSelectMeal(value); - } - }, - ), - StyledDropdownButton( - selectedItem: _mealSource, - label: 'Meal Source', - items: _mealSources, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _mealSource = value; - }); - }, - ), - StyledDropdownButton( - selectedItem: _mealCategory, - label: 'Meal Category', - items: _mealCategories, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _mealCategory = value; - }); - }, - ), - StyledDropdownButton( - selectedItem: _mealPortionType, - label: 'Meal Portion Type', - items: _mealPortionTypes, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _mealPortionType = value; - }); - }, - ), - Row( - children: [ - Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Carbs ratio', - suffixText: '%', - ), - controller: _carbsRatioController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) => - calculateThirdMeasurementOfPortionCarbsRelation(), - ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.carbsRatio), - icon: const Icon(Icons.calculate), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Portion size', - suffixText: - nutritionMeasurement == NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == - NutritionMeasurement.ounces - ? 'oz' - : '', - alignLabelWithHint: true, - ), - controller: _portionSizeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) => - calculateThirdMeasurementOfPortionCarbsRelation(), - ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.portionSize), - icon: const Icon(Icons.calculate), - ), - ], - ), - StyledDropdownButton( - selectedItem: _portionSizeAccuracy, - label: 'Portion Size Accuracy', - items: _portionSizeAccuracies, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _portionSizeAccuracy = value; - }); - }, - ), - // StyledFutureDropdownButton( - // selectedItem: _portionSizeAccuracy, - // label: 'Portion Size Accuracy', - // items: _portionSizeAccuracies, - // getItemValue: (item) => item.objectId, - // renderItem: (item) => Text(item.value), - // onChanged: (value) { - // setState(() { - // _portionSizeAccuracy = value; - // }); - // }, - // ), - Row( - children: [ - Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Carbs per portion', - suffixText: - nutritionMeasurement == NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == - NutritionMeasurement.ounces - ? 'oz' - : '', - ), - controller: _carbsPerPortionController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) => - calculateThirdMeasurementOfPortionCarbsRelation(), - ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.carbsPerPortion), - icon: const Icon(Icons.calculate), - ), - ], - ), - StyledDropdownButton( - selectedItem: _carbsRatioAccuracy, - label: 'Carbs Ratio Accuracy', - items: _carbsRatioAccuracies, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _carbsRatioAccuracy = value; - }); - }, - ), - // StyledFutureDropdownButton( - // selectedItem: _carbsRatioAccuracy, - // label: 'Carbs Ratio Accuracy', - // items: _carbsRatioAccuracies, - // getItemValue: (item) => item.objectId, - // renderItem: (item) => Text(item.value), - // onChanged: (value) { - // setState(() { - // _carbsRatioAccuracy = value; - // }); - // }, - // ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Bolus Units', - suffixText: ' U', - ), - controller: _bolusController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus Duration', - suffixText: ' min', - ), - controller: _delayedBolusDurationController, - keyboardType: const TextInputType.numberWithOptions(), - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus Units', - suffixText: ' U', - alignLabelWithHint: true, - ), - controller: _delayedBolusRateController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, - ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ], - ), - ), - bottomNavigationBar: DetailBottomRow( - onCancel: handleCancelAction, - onSave: _isSaving ? null : handleSaveAction, - ), - ); - } -} diff --git a/lib/screens/log/log_meal_list.dart b/lib/screens/log/log_meal_list.dart deleted file mode 100644 index cb0c0ea..0000000 --- a/lib/screens/log/log_meal_list.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.dart'; -import 'package:diameter/models/log_entry.dart'; -import 'package:diameter/models/log_meal.dart'; -import 'package:diameter/screens/log/log_meal_detail.dart'; -import 'package:flutter/material.dart'; - -class LogMealListScreen extends StatefulWidget { - final LogEntry logEntry; - final Function() reload; - - const LogMealListScreen({Key? key, required this.logEntry, required this.reload}) - : super(key: key); - - @override - _LogMealListScreenState createState() => _LogMealListScreenState(); -} - -class _LogMealListScreenState extends State { - void reload({String? message}) { - widget.reload(); - - setState(() { - if (message != null) { - var snackBar = SnackBar( - content: Text(message), - duration: const Duration(seconds: 2), - ); - ScaffoldMessenger.of(context) - ..removeCurrentSnackBar() - ..showSnackBar(snackBar); - } - }); - } - - void handleEditAction(LogMeal meal) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LogMealDetailScreen( - logEntry: widget.logEntry, - logMeal: meal, - ), - ), - ).then((message) => reload(message: message)); - } - - void onDelete(LogMeal logMeal) { - LogMeal.remove(logMeal.id); - reload(message: 'Meal deleted'); - } - - void handleDeleteAction(LogMeal meal) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( - context: context, - onConfirm: () => onDelete(meal), - message: 'Are you sure you want to delete this Meal?', - ); - } else { - onDelete(meal); - } - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: widget.logEntry.meals.isNotEmpty ? ListView.builder( - shrinkWrap: true, - itemCount: widget.logEntry.meals.length, - itemBuilder: (context, index) { - final meal = widget.logEntry.meals[index]; - return ListTile( - onTap: () => handleEditAction(meal), - title: Row( - children: [ - Expanded(child: Text(meal.value)), - Expanded( - child: Text(meal.carbsPerPortion != null - ? '${meal.carbsPerPortion} g carbs' - : '')), - Expanded( - child: Text(meal.bolus != null - ? '${meal.bolus} U' - : '')) - ], - ), - trailing: IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => handleDeleteAction(meal), - ), - ); - }, - ) : const Center( - child: Text( - 'You have not added any Meals to this Log Entry yet!'), - ), - ), - ], - ); - } -} diff --git a/lib/screens/meal/meal_category_detail.dart b/lib/screens/meal/meal_category_detail.dart index 0414f45..b498ac0 100644 --- a/lib/screens/meal/meal_category_detail.dart +++ b/lib/screens/meal/meal_category_detail.dart @@ -1,17 +1,16 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.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'; import 'package:diameter/models/meal_category.dart'; class MealCategoryDetailScreen extends StatefulWidget { static const String routeName = '/meal-category'; - final MealCategory? mealCategory; + final int id; - const MealCategoryDetailScreen({Key? key, this.mealCategory}) - : super(key: key); + const MealCategoryDetailScreen({Key? key, this.id = 0}) : super(key: key); @override _MealCategoryDetailScreenState createState() => @@ -19,45 +18,79 @@ class MealCategoryDetailScreen extends StatefulWidget { } class _MealCategoryDetailScreenState extends State { + MealCategory? _mealCategory; + bool _isNew = true; + final GlobalKey _mealCategoryForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + final _valueController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); @override void initState() { super.initState(); - if (widget.mealCategory != null) { - _valueController.text = widget.mealCategory!.value; - _notesController.text = widget.mealCategory!.notes ?? ''; + reload(); + + if (_mealCategory != null) { + _valueController.text = _mealCategory!.value; + _notesController.text = _mealCategory!.notes ?? ''; } } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + super.dispose(); + } + + void reload({String? message}) { + if (widget.id != 0) { + setState(() { + _mealCategory = MealCategory.get(widget.id); + }); + } + _isNew = _mealCategory == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + void handleSaveAction() async { if (_mealCategoryForm.currentState!.validate()) { - bool isNew = widget.mealCategory == null; - // isNew - // ? await MealCategory.save( - // value: _valueController.text, notes: _notesController.text) - // : await MealCategory.update(widget.mealCategory!.objectId!, - // value: _valueController.text, notes: _notesController.text); - MealCategory.put(MealCategory(id: widget.mealCategory?.id ?? 0, - value: _valueController.text, notes: _notesController.text)); - Navigator.pop(context, '${isNew ? 'New' : ''} Meal Category saved'); + MealCategory mealCategory = MealCategory( + id: widget.id, + value: _valueController.text, + notes: _notesController.text, + ); + MealCategory.put(mealCategory); + Navigator.pop(context, [ + '${_isNew ? 'New' : ''} Meal Category saved', mealCategory + ]); } } void handleCancelAction() { - bool isNew = widget.mealCategory == null; - - if (showConfirmationDialogOnCancel && - (isNew && + if (Settings.get().showConfirmationDialogOnCancel && + (_isNew && (_valueController.text != '' || _notesController.text != '')) || - (!isNew && - (widget.mealCategory!.value != _valueController.text || - (widget.mealCategory!.notes ?? '') != _notesController.text))) { - Dialogs.showCancelConfirmationDialog( + (!_isNew && + (_mealCategory!.value != _valueController.text || + (_mealCategory!.notes ?? '') != _notesController.text))) { + DialogUtils.showCancelConfirmationDialog( context: context, - isNew: isNew, + isNew: _isNew, onSave: handleSaveAction, ); } else { @@ -67,48 +100,52 @@ class _MealCategoryDetailScreenState extends State { @override Widget build(BuildContext context) { - bool isNew = widget.mealCategory == null; return Scaffold( appBar: AppBar( - title: Text(isNew ? 'New Meal Category' : widget.mealCategory!.value), + title: Text(_isNew ? 'New Meal Category' : _mealCategory!.value), ), drawer: const Navigation(currentLocation: MealCategoryDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _mealCategoryForm, - fields: [ - TextFormField( - controller: _valueController, - decoration: const InputDecoration( - labelText: 'Name', + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _mealCategoryForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty name'; - } - return null; - }, - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ], + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, - onSave: handleSaveAction, + onAction: handleSaveAction, ), ); } diff --git a/lib/screens/meal/meal_category_list.dart b/lib/screens/meal/meal_category_list.dart index ead7fde..d4c8211 100644 --- a/lib/screens/meal/meal_category_list.dart +++ b/lib/screens/meal/meal_category_list.dart @@ -1,6 +1,5 @@ -import 'package:diameter/components/dialogs.dart'; -// import 'package:diameter/components/progress_indicator.dart'; -import 'package:diameter/config.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'; import 'package:flutter/material.dart'; @@ -18,7 +17,21 @@ class MealCategoryListScreen extends StatefulWidget { class _MealCategoryListScreenState extends State { List _mealCategories = []; - void refresh({String? message}) { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { setState(() { _mealCategories = MealCategory.getAll(); }); @@ -37,12 +50,12 @@ class _MealCategoryListScreenState extends State { void onDelete(MealCategory mealCategory) { MealCategory.remove(mealCategory.id); - refresh(message: 'Meal Category deleted'); + reload(message: 'Meal Category deleted'); } void handleDeleteAction(MealCategory mealCategory) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(mealCategory), message: 'Are you sure you want to delete this Meal Category?', @@ -52,12 +65,6 @@ class _MealCategoryListScreenState extends State { } } - @override - void initState() { - super.initState(); - refresh(); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -65,7 +72,7 @@ class _MealCategoryListScreenState extends State { title: const Text('Meal Categories'), actions: [ IconButton( - onPressed: refresh, + onPressed: reload, icon: const Icon(Icons.refresh), ), ], @@ -76,41 +83,48 @@ class _MealCategoryListScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: _mealCategories.isNotEmpty ? ListView.builder( - padding: const EdgeInsets.only(top: 10.0), - itemCount: _mealCategories.length, - itemBuilder: (context, index) { - final mealCategory = _mealCategories[index]; - - return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - MealCategoryDetailScreen( - mealCategory: mealCategory, - ), + child: _mealCategories.isNotEmpty ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(10.0), + itemCount: _mealCategories.length, + itemBuilder: (context, index) { + final mealCategory = _mealCategories[index]; + return Card( + child: ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MealCategoryDetailScreen( + id: mealCategory.id, + ), + ), + ).then((result) => reload(message: result?[0])); + }, + title: Text( + mealCategory.value.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, ), - ).then((message) => refresh(message: message)); - }, - title: Text(mealCategory.value), - subtitle: Text(mealCategory.notes ?? ''), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => - handleDeleteAction(mealCategory), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => + handleDeleteAction(mealCategory), + ), + ], ), - ], - ), - ); - }, + ), + ); + }, + ), ): const Center( child: Text('You have not created any Meal Categories yet!'), ), @@ -124,7 +138,7 @@ class _MealCategoryListScreenState extends State { MaterialPageRoute( builder: (context) => const MealCategoryDetailScreen(), ), - ).then((message) => refresh(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/screens/meal/meal_detail.dart b/lib/screens/meal/meal_detail.dart index c7ca8b3..83a5588 100644 --- a/lib/screens/meal/meal_detail.dart +++ b/lib/screens/meal/meal_detail.dart @@ -1,136 +1,191 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.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/accuracy.dart'; import 'package:diameter/models/meal.dart'; import 'package:diameter/models/meal_category.dart'; import 'package:diameter/models/meal_portion_type.dart'; import 'package:diameter/models/meal_source.dart'; +import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; -// import 'package:diameter/objectbox.g.dart'; -import 'package:diameter/settings.dart'; +import 'package:diameter/screens/accuracy_detail.dart'; +import 'package:diameter/screens/meal/meal_category_detail.dart'; +import 'package:diameter/screens/meal/meal_portion_type_detail.dart'; +import 'package:diameter/screens/meal/meal_source_detail.dart'; import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; class MealDetailScreen extends StatefulWidget { static const String routeName = '/meal'; + final int id; - final Meal? meal; - const MealDetailScreen({Key? key, this.meal}) : super(key: key); + const MealDetailScreen({Key? key, this.id = 0}) : super(key: key); @override _MealDetailScreenState createState() => _MealDetailScreenState(); } class _MealDetailScreenState extends State { + Meal? _meal; + + bool _isNew = true; + bool _isSaving = false; + bool _isExpanded = false; + bool _setManually = false; + final GlobalKey _mealForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); final _carbsRatioController = TextEditingController(text: ''); final _portionSizeController = TextEditingController(text: ''); final _carbsPerPortionController = TextEditingController(text: ''); - final _delayedBolusRateController = TextEditingController(text: ''); final _delayedBolusDurationController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); + double _delayedBolusPercentage = 0; + MealSource? _mealSource; MealCategory? _mealCategory; MealPortionType? _mealPortionType; Accuracy? _portionSizeAccuracy; Accuracy? _carbsRatioAccuracy; + final _mealSourceController = TextEditingController(text: ''); + final _mealCategoryController = TextEditingController(text: ''); + final _mealPortionTypeController = TextEditingController(text: ''); + final _portionSizeAccuracyController = TextEditingController(text: ''); + final _carbsRatioAccuracyController = TextEditingController(text: ''); + List _mealCategories = []; List _mealPortionTypes = []; List _mealSources = []; List _portionSizeAccuracies = []; List _carbsRatioAccuracies = []; - bool isSaving = false; - @override void initState() { super.initState(); + reload(); + _portionSizeAccuracies = Accuracy.getAllForPortionSize(); _carbsRatioAccuracies = Accuracy.getAllForCarbsRatio(); _mealCategories = MealCategory.getAll(); _mealPortionTypes = MealPortionType.getAll(); _mealSources = MealSource.getAll(); - if (widget.meal != null) { - _valueController.text = widget.meal!.value; - _carbsRatioController.text = (widget.meal!.carbsRatio ?? '').toString(); - _portionSizeController.text = (widget.meal!.portionSize ?? '').toString(); + if (_meal != null) { + _valueController.text = _meal!.value; + _carbsRatioController.text = (_meal!.carbsRatio ?? '').toString(); + _portionSizeController.text = (_meal!.portionSize ?? '').toString(); _carbsPerPortionController.text = - (widget.meal!.carbsPerPortion ?? '').toString(); - _delayedBolusRateController.text = - (widget.meal!.delayedBolusRate ?? '').toString(); + (_meal!.carbsPerPortion ?? '').toString(); + _delayedBolusPercentage = _meal!.delayedBolusPercentage ?? 0; _delayedBolusDurationController.text = - (widget.meal!.delayedBolusDuration ?? '').toString(); - _notesController.text = widget.meal!.notes ?? ''; + (_meal!.delayedBolusDuration ?? '').toString(); + _notesController.text = _meal!.notes ?? ''; - _mealSource = widget.meal!.mealSource.target; - _mealCategory = widget.meal!.mealCategory.target; - _mealPortionType = widget.meal!.mealPortionType.target; - _portionSizeAccuracy = widget.meal!.portionSizeAccuracy.target; - _carbsRatioAccuracy = widget.meal!.carbsRatioAccuracy.target; + _mealSource = _meal!.mealSource.target; + _mealSourceController.text = (_mealSource ?? '').toString(); + _mealCategory = _meal!.mealCategory.target; + _mealCategoryController.text = (_mealCategory ?? '').toString(); + _mealPortionType = _meal!.mealPortionType.target; + _mealPortionTypeController.text = (_mealPortionType ?? '').toString(); + _portionSizeAccuracy = _meal!.portionSizeAccuracy.target; + _portionSizeAccuracyController.text = + (_portionSizeAccuracy ?? '').toString(); + _carbsRatioAccuracy = _meal!.carbsRatioAccuracy.target; + _carbsRatioAccuracyController.text = + (_carbsRatioAccuracy ?? '').toString(); } } + @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(() { + _meal = Meal.get(widget.id); + }); + } + _isNew = _meal == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void updateCarbsRatioAccuracy(Accuracy? value) { + setState(() { + _carbsRatioAccuracy = value; + _carbsRatioAccuracyController.text = + (_carbsRatioAccuracy ?? '').toString(); + }); + } + + void updatePortionSizeAccuracy(Accuracy? value) { + setState(() { + _portionSizeAccuracy = value; + _portionSizeAccuracyController.text = + (_portionSizeAccuracy ?? '').toString(); + }); + } + + void updateMealCategory(MealCategory? value) { + setState(() { + _mealCategory = value; + _mealCategoryController.text = (_mealCategory ?? '').toString(); + }); + } + + void updateMealPortionType(MealPortionType? value) { + setState(() { + _mealPortionType = value; + _mealPortionTypeController.text = (_mealPortionType ?? '').toString(); + }); + } + void handleSaveAction() async { setState(() { - isSaving = true; + _isSaving = true; }); if (_mealForm.currentState!.validate()) { - bool isNew = widget.meal == null; - // isNew - // ? await Meal.save( - // value: _valueController.text, - // source: _mealSource, - // category: _mealCategory, - // portionType: _mealPortionType, - // carbsRatio: double.tryParse(_carbsRatioController.text), - // portionSize: double.tryParse(_portionSizeController.text), - // carbsPerPortion: double.tryParse(_carbsPerPortionController.text), - // // portionSizeAccuracy: _portionSizeAccuracy, - // // carbsRatioAccuracy: _carbsRatioAccuracy, - // portionSizeAccuracy: _portionSizeAccuracy?.id.toString(), - // carbsRatioAccuracy: _carbsRatioAccuracy?.id.toString(), - // delayedBolusDuration: - // int.tryParse(_delayedBolusDurationController.text), - // delayedBolusRate: - // double.tryParse(_delayedBolusRateController.text), - // notes: _notesController.text, - // ) - // : await Meal.update( - // widget.meal!.objectId!, - // value: _valueController.text, - // source: _mealSource, - // category: _mealCategory, - // portionType: _mealPortionType, - // carbsRatio: double.tryParse(_carbsRatioController.text), - // portionSize: double.tryParse(_portionSizeController.text), - // carbsPerPortion: double.tryParse(_carbsPerPortionController.text), - // // portionSizeAccuracy: _portionSizeAccuracy, - // // carbsRatioAccuracy: _carbsRatioAccuracy, - // portionSizeAccuracy: _portionSizeAccuracy?.id.toString(), - // carbsRatioAccuracy: _carbsRatioAccuracy?.id.toString(), - // delayedBolusDuration: - // int.tryParse(_delayedBolusDurationController.text), - // delayedBolusRate: - // double.tryParse(_delayedBolusRateController.text), - // notes: _notesController.text, - // ); Meal meal = Meal( - id: widget.meal?.id ?? 0, + id: widget.id, value: _valueController.text, carbsRatio: double.tryParse(_carbsRatioController.text), portionSize: double.tryParse(_portionSizeController.text), carbsPerPortion: double.tryParse(_carbsPerPortionController.text), delayedBolusDuration: int.tryParse(_delayedBolusDurationController.text), - delayedBolusRate: double.tryParse(_delayedBolusRateController.text), + delayedBolusPercentage: _delayedBolusPercentage, notes: _notesController.text, ); meal.mealSource.target = _mealSource; @@ -140,17 +195,16 @@ class _MealDetailScreenState extends State { meal.carbsRatioAccuracy.target = _carbsRatioAccuracy; Meal.put(meal); - Navigator.pop(context, '${isNew ? 'New' : ''} Meal Saved'); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Saved', meal]); } setState(() { - isSaving = false; + _isSaving = false; }); } void handleCancelAction() { - bool isNew = widget.meal == null; - if (showConfirmationDialogOnCancel && - ((isNew && + if (Settings.get().showConfirmationDialogOnCancel && + ((_isNew && (_valueController.text != '' || _mealSource != null || _mealCategory != null || @@ -162,29 +216,28 @@ class _MealDetailScreenState extends State { _portionSizeAccuracy != null || int.tryParse(_delayedBolusDurationController.text) != null || - double.tryParse(_delayedBolusRateController.text) != null || + _delayedBolusPercentage != 0 || _notesController.text != '')) || - (!isNew && - (_valueController.text != widget.meal!.value || - _mealSource != widget.meal!.mealSource.target || - _mealCategory != widget.meal!.mealCategory.target || - _mealPortionType != widget.meal!.mealPortionType.target || + (!_isNew && + (_valueController.text != _meal!.value || + _mealSource != _meal!.mealSource.target || + _mealCategory != _meal!.mealCategory.target || + _mealPortionType != _meal!.mealPortionType.target || double.tryParse(_carbsRatioController.text) != - widget.meal!.carbsRatio || + _meal!.carbsRatio || double.tryParse(_portionSizeController.text) != - widget.meal!.portionSize || + _meal!.portionSize || double.tryParse(_carbsPerPortionController.text) != - widget.meal!.carbsPerPortion || - _carbsRatioAccuracy != widget.meal!.carbsRatioAccuracy.target || - _portionSizeAccuracy != widget.meal!.portionSizeAccuracy.target || + _meal!.carbsPerPortion || + _carbsRatioAccuracy != _meal!.carbsRatioAccuracy.target || + _portionSizeAccuracy != _meal!.portionSizeAccuracy.target || int.tryParse(_delayedBolusDurationController.text) != - widget.meal!.delayedBolusDuration || - double.tryParse(_delayedBolusRateController.text) != - widget.meal!.delayedBolusRate || - _notesController.text != (widget.meal!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + _meal!.delayedBolusDuration || + _delayedBolusPercentage != _meal!.delayedBolusPercentage || + _notesController.text != (_meal!.notes ?? ''))))) { + DialogUtils.showCancelConfirmationDialog( context: context, - isNew: isNew, + isNew: _isNew, onSave: handleSaveAction, ); } else { @@ -192,299 +245,445 @@ class _MealDetailScreenState extends State { } } - Future onSelectMealSource(MealSource mealSource) async { + void onSelectMealSource(MealSource? mealSource) { setState(() { _mealSource = mealSource; + _mealSourceController.text = (_mealSource ?? '').toString(); + }); + if (mealSource != null) { if (mealSource.defaultCarbsRatioAccuracy.hasValue) { - _carbsRatioAccuracy = - mealSource.defaultCarbsRatioAccuracy.target; + updateCarbsRatioAccuracy(mealSource.defaultCarbsRatioAccuracy.target); } if (mealSource.defaultPortionSizeAccuracy.hasValue) { - _portionSizeAccuracy = - mealSource.defaultPortionSizeAccuracy.target; + updatePortionSizeAccuracy(mealSource.defaultPortionSizeAccuracy.target); } if (mealSource.defaultMealCategory.hasValue) { - _mealCategory = mealSource.defaultMealCategory.target; + updateMealCategory(mealSource.defaultMealCategory.target); } if (mealSource.defaultMealPortionType.hasValue) { - _mealPortionType = mealSource.defaultMealPortionType.target; + updateMealPortionType(mealSource.defaultMealPortionType.target); } - }); + } } void calculateThirdMeasurementOfPortionCarbsRelation( - {PortionCarbsParameter? parameterToBeCalculated}) { - double? carbsRatio; - double? portionSize; - double? carbsPerPortion; + {double? carbsRatioUpdate, + double? portionSizeUpdate, + double? carbsPerPortionUpdate}) { + if (!_setManually) { + double? carbsRatio = + carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text); + double? portionSize = + portionSizeUpdate ?? double.tryParse(_portionSizeController.text); + double? carbsPerPortion = carbsPerPortionUpdate ?? + double.tryParse(_carbsPerPortionController.text); - if (parameterToBeCalculated != PortionCarbsParameter.carbsRatio && - _carbsRatioController.text != '') { - carbsRatio = double.tryParse(_carbsRatioController.text); - } - if (parameterToBeCalculated != PortionCarbsParameter.portionSize && - _portionSizeController.text != '') { - portionSize = double.tryParse(_portionSizeController.text); - } - if (parameterToBeCalculated != PortionCarbsParameter.carbsPerPortion && - _carbsRatioController.text != '') { - carbsPerPortion = double.tryParse(_carbsPerPortionController.text); + int toCalculate = 0; + const calcCarbsRatio = 1; + const calcCarbsPerPortion = 2; + const calcPortionSize = 3; + + if (carbsRatioUpdate != null) { + if (portionSize != null && portionSize != 0) { + toCalculate = calcCarbsPerPortion; + } else if (carbsPerPortion != null && carbsPerPortion != 0) { + toCalculate = calcPortionSize; + } + } else if (portionSizeUpdate != null) { + if (carbsRatio != null && carbsRatio != 0) { + toCalculate = calcCarbsPerPortion; + } else if (carbsPerPortion != null && carbsPerPortion != 0) { + toCalculate = calcCarbsRatio; + } + } else if (carbsPerPortionUpdate != null) { + if (carbsRatio != null && carbsRatio != 0) { + toCalculate = calcPortionSize; + } else if (portionSize != null && portionSize != 0) { + toCalculate = calcCarbsRatio; + } + } else { + if (carbsRatio != null && carbsRatio != 0) { + if (portionSize != null && portionSize != 0) { + toCalculate = calcCarbsPerPortion; + } else if (carbsPerPortion != null && carbsPerPortion != 0) { + toCalculate = calcPortionSize; + } + } else if (portionSize != null && + portionSize != 0 && + carbsPerPortion != null && + carbsPerPortion != 0) { + toCalculate = calcCarbsRatio; + } } - if (carbsRatio != null && portionSize != null && carbsPerPortion == null) { setState(() { - _carbsPerPortionController.text = - Utils.calculateCarbsPerPortion(carbsRatio!, portionSize!) - .toString(); - }); - } - if (carbsRatio == null && portionSize != null && carbsPerPortion != null) { - setState(() { - _carbsRatioController.text = - Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!) - .toString(); - }); - } - if (carbsRatio != null && portionSize == null && carbsPerPortion != null) { - setState(() { - _portionSizeController.text = - Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!) - .toString(); + if (toCalculate == calcCarbsRatio) { + _carbsRatioController.text = + Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!) + .toString(); + } else if (toCalculate == calcCarbsPerPortion) { + _carbsPerPortionController.text = Utils.calculateCarbs( + carbsRatio!, portionSize!, + step: Settings.nutritionSteps) + .toString(); + } else if (toCalculate == calcPortionSize) { + _portionSizeController.text = Utils.calculatePortionSize( + carbsRatio!, carbsPerPortion!, + step: Settings.nutritionSteps) + .toString(); + } }); } } @override Widget build(BuildContext context) { - bool isNew = widget.meal == null; return Scaffold( appBar: AppBar( - title: Text(isNew ? 'New Meal' : widget.meal!.value), + title: Text(_isNew ? 'New Meal' : _meal!.value), ), drawer: const Navigation(currentLocation: MealDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _mealForm, - fields: [ - TextFormField( - controller: _valueController, - decoration: const InputDecoration( - labelText: 'Name', + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: [ + FormWrapper( + formState: _mealForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty name'; - } - return null; - }, - ), - StyledDropdownButton( - selectedItem: _mealSource, - label: 'Meal Source', - items: _mealSources, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - if (value != null) { - onSelectMealSource(value); - } - }, - ), - StyledDropdownButton( - selectedItem: _mealCategory, - label: 'Meal Category', - items: _mealCategories, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _mealCategory = value; - }); - }, - ), - StyledDropdownButton( - selectedItem: _mealPortionType, - label: 'Meal Portion Type', - items: _mealPortionTypes, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _mealPortionType = value; - }); - }, - ), - Row( - children: [ - Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Carbs ratio', - suffixText: '%', + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _mealSourceController, + selectedItem: _mealSource, + label: 'Meal Source', + items: _mealSources, + onChanged: onSelectMealSource, ), - controller: _carbsRatioController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) => - calculateThirdMeasurementOfPortionCarbsRelation(), ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.carbsRatio), - icon: const Icon(Icons.calculate), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Portion size', - suffixText: - nutritionMeasurement == NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == - NutritionMeasurement.ounces - ? 'oz' - : '', - alignLabelWithHint: true, + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _mealSource == null + ? const MealSourceDetailScreen() + : MealSourceDetailScreen(id: _mealSource!.id), + ), + ).then((result) { + onSelectMealSource(result?[1]); + reload(message: result?[0]); + }); + }, + icon: + Icon(_mealSource == null ? Icons.add : Icons.edit), + ), + ], + ), + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _mealPortionTypeController, + selectedItem: _mealPortionType, + label: 'Meal Portion Type', + items: _mealPortionTypes, + onChanged: updateMealPortionType, ), - controller: _portionSizeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) => - calculateThirdMeasurementOfPortionCarbsRelation(), ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.portionSize), - icon: const Icon(Icons.calculate), - ), - ], - ), - StyledDropdownButton( - selectedItem: _portionSizeAccuracy, - label: 'Portion Size Accuracy', - items: _portionSizeAccuracies, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _portionSizeAccuracy = value; - }); - }, - ), - // StyledFutureDropdownButton( - // selectedItem: _portionSizeAccuracy, - // label: 'Portion Size Accuracy', - // items: _portionSizeAccuracies, - // getItemValue: (item) => item.objectId, - // renderItem: (item) => Text(item.value), - // onChanged: (value) { - // setState(() { - // _portionSizeAccuracy = value; - // }); - // }, - // ), - Row( - children: [ - Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Carbs per portion', - suffixText: - nutritionMeasurement == NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == - NutritionMeasurement.ounces - ? 'oz' - : '', + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _mealPortionType == null + ? const MealPortionTypeDetailScreen() + : MealPortionTypeDetailScreen( + id: _mealPortionType!.id), + ), + ).then((result) { + updateMealPortionType(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon( + _mealPortionType == null ? Icons.add : Icons.edit), + ), + ], + ), + Row( + children: [ + Expanded( + child: NumberFormField( + label: 'Carbs ratio', + suffix: '%', + controller: _carbsRatioController, + showSteppers: false, + onChanged: (value) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation(carbsRatioUpdate: value); + }, ), - controller: _carbsPerPortionController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) => - calculateThirdMeasurementOfPortionCarbsRelation(), ), + const SizedBox(width: 10), + Expanded( + child: NumberFormField( + label: 'Portion size', + suffix: Settings.nutritionMeasurementSuffix, + controller: _portionSizeController, + showSteppers: false, + onChanged: (value) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation(portionSizeUpdate: value); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: NumberFormField( + label: 'Carbs per portion', + suffix: Settings.nutritionMeasurementSuffix, + controller: _carbsPerPortionController, + showSteppers: false, + onChanged: (value) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation(carbsPerPortionUpdate: value); + }, + ), + ), + ], + ), + Expanded( + child: BooleanFormField( + value: _setManually, + label: 'set carbs ratio manually', + onChanged: (value) { + setState(() { + _setManually = value; + calculateThirdMeasurementOfPortionCarbsRelation(); + }); + }, ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.carbsPerPortion), - icon: const Icon(Icons.calculate), + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', ), - ], - ), - StyledDropdownButton( - selectedItem: _carbsRatioAccuracy, - label: 'Carbs Ratio Accuracy', - items: _carbsRatioAccuracies, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _carbsRatioAccuracy = value; - }); - }, - ), - // StyledFutureDropdownButton( - // selectedItem: _carbsRatioAccuracy, - // label: 'Carbs Ratio Accuracy', - // items: _carbsRatioAccuracies, - // getItemValue: (item) => item.objectId, - // renderItem: (item) => Text(item.value), - // onChanged: (value) { - // setState(() { - // _carbsRatioAccuracy = value; - // }); - // }, - // ), - // TODO: display according to time format - TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus Duration', - suffixText: ' min', + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, ), - controller: _delayedBolusDurationController, - keyboardType: const TextInputType.numberWithOptions(), - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus Units', - suffixText: ' U', + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + children: [ + Text( + 'BOLUS DELAY', + style: Theme.of(context).textTheme.subtitle2, + ), + const Spacer(), + ], + ), ), - controller: _delayedBolusRateController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Duration', + suffixText: ' min', + ), + controller: _delayedBolusDurationController, + onChanged: (value) => setState(() {}), + keyboardType: const TextInputType.numberWithOptions(), + ), + ), + Expanded( + child: Slider( + label: + '${_delayedBolusPercentage.floor().toString()}%', + divisions: 100, + value: _delayedBolusPercentage, + min: 0, + max: 100, + onChanged: + _delayedBolusDurationController.text != '' + ? (value) { + setState(() { + _delayedBolusPercentage = value; + }); + } + : null), + ), + const Text('%', textScaleFactor: 1.5), + ], ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ], + const Divider(), + GestureDetector( + onTap: () => setState(() { + _isExpanded = !_isExpanded; + }), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + 'ADDITIONAL FIELDS', + style: Theme.of(context).textTheme.subtitle2, + ), + ), + Icon(_isExpanded + ? Icons.expand_less + : Icons.expand_more), + ], + ), + ), + Column( + children: _isExpanded + ? [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton< + MealCategory>( + controller: _mealCategoryController, + selectedItem: _mealCategory, + label: 'Meal Category', + items: _mealCategories, + onChanged: updateMealCategory, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _mealCategory == + null + ? const MealCategoryDetailScreen() + : MealCategoryDetailScreen( + id: _mealCategory!.id), + ), + ).then((result) { + updateMealCategory(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_mealCategory == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: + _portionSizeAccuracyController, + selectedItem: _portionSizeAccuracy, + label: 'Portion Size Accuracy', + items: _portionSizeAccuracies, + onChanged: updatePortionSizeAccuracy, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + _portionSizeAccuracy == null + ? const AccuracyDetailScreen() + : AccuracyDetailScreen( + id: _portionSizeAccuracy! + .id), + ), + ).then((result) { + updatePortionSizeAccuracy(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_portionSizeAccuracy == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _carbsRatioAccuracyController, + selectedItem: _carbsRatioAccuracy, + label: 'Carbs Ratio Accuracy', + items: _carbsRatioAccuracies, + onChanged: updateCarbsRatioAccuracy, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + _carbsRatioAccuracy == null + ? const AccuracyDetailScreen() + : AccuracyDetailScreen( + id: _carbsRatioAccuracy! + .id), + ), + ).then((result) { + updateCarbsRatioAccuracy(result?[1]); + reload(message: result?[0]); + }); + }, + icon: Icon(_carbsRatioAccuracy == null + ? Icons.add + : Icons.edit), + ), + ], + ), + ), + ] + : [], + ), + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, - onSave: isSaving ? null : handleSaveAction, + onAction: _isSaving ? null : handleSaveAction, ), ); } diff --git a/lib/screens/meal/meal_list.dart b/lib/screens/meal/meal_list.dart index d0f37b6..579b77b 100644 --- a/lib/screens/meal/meal_list.dart +++ b/lib/screens/meal/meal_list.dart @@ -1,7 +1,6 @@ -import 'package:diameter/components/dialogs.dart'; -// import 'package:diameter/components/progress_indicator.dart'; -import 'package:diameter/config.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'; import 'package:diameter/screens/meal/meal_detail.dart'; import 'package:flutter/material.dart'; @@ -18,7 +17,21 @@ class MealListScreen extends StatefulWidget { class _MealListScreenState extends State { List _meals = []; - void refresh({String? message}) { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { setState(() { _meals = Meal.getAll(); }); @@ -37,12 +50,12 @@ class _MealListScreenState extends State { void onDelete(Meal meal) { Meal.remove(meal.id); - refresh(message: 'Meal deleted'); + reload(message: 'Meal deleted'); } void handleDeleteAction(Meal meal) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(meal), message: 'Are you sure you want to delete this Meal?', @@ -52,53 +65,107 @@ class _MealListScreenState extends State { } } - @override - void initState() { - super.initState(); - refresh(); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Meals'), actions: [ - IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) ]), drawer: const Navigation(currentLocation: MealListScreen.routeName), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: _meals.isNotEmpty ? ListView.builder( - padding: const EdgeInsets.all(10.0), - itemCount: _meals.length, - itemBuilder: (context, index) { - final meal = _meals[index]; - - return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - MealDetailScreen(meal: meal), + child: _meals.isNotEmpty ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(10.0), + itemCount: _meals.length, + itemBuilder: (context, index) { + final meal = _meals[index]; + String portionType = meal.mealPortionType.hasValue ? ' per ${meal.mealPortionType.target!.value}' : ''; + return Card( + child: ListTile( + isThreeLine: true, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MealDetailScreen(id: meal.id), + ), + ).then((result) => reload(message: result?[0])); + }, + title: Text( + meal.value.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, ), - ).then((message) => refresh(message: message)); - }, - title: Text(meal.value), - subtitle: Text(meal.notes ?? ''), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () => handleDeleteAction(meal), - icon: const Icon(Icons.delete, - color: Colors.blue), - ) - ], - ), - ); - }, + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Column( + children: [ + Row( + children: [ + 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 + ), + ] + : [], + ), + ), + ], + ), + meal.notes != null && meal.notes!.trim() != '' ? Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Row( + children: [ + Expanded(child: Text(meal.notes ?? '')), + ], + ), + ) : Container(), + ], + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () => handleDeleteAction(meal), + icon: const Icon(Icons.delete, + color: Colors.blue), + ) + ], + ), + ), + ); + }, + ), ): const Center( child: Text('You have not created any Meals yet!'), ), @@ -112,7 +179,7 @@ class _MealListScreenState extends State { MaterialPageRoute( builder: (context) => const MealDetailScreen(), ), - ).then((message) => refresh(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/screens/meal/meal_portion_type_detail.dart b/lib/screens/meal/meal_portion_type_detail.dart index 1b6ef56..eb5ffb8 100644 --- a/lib/screens/meal/meal_portion_type_detail.dart +++ b/lib/screens/meal/meal_portion_type_detail.dart @@ -1,7 +1,7 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.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'; import 'package:diameter/models/meal_portion_type.dart'; @@ -9,10 +9,9 @@ import 'package:diameter/models/meal_portion_type.dart'; class MealPortionTypeDetailScreen extends StatefulWidget { static const String routeName = '/meal-portion-type'; - final MealPortionType? mealPortionType; + final int id; - const MealPortionTypeDetailScreen({Key? key, this.mealPortionType}) - : super(key: key); + const MealPortionTypeDetailScreen({Key? key, this.id = 0}) : super(key: key); @override _MealPortionTypeDetailScreenState createState() => @@ -21,53 +20,79 @@ class MealPortionTypeDetailScreen extends StatefulWidget { class _MealPortionTypeDetailScreenState extends State { + MealPortionType? _mealPortionType; + bool _isNew = true; + final GlobalKey _mealPortionTypeForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + final _valueController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); @override void initState() { super.initState(); - if (widget.mealPortionType != null) { - _valueController.text = widget.mealPortionType!.value; - _notesController.text = widget.mealPortionType!.notes ?? ''; + reload(); + + if (_mealPortionType != null) { + _valueController.text = _mealPortionType!.value; + _notesController.text = _mealPortionType!.notes ?? ''; } } + @override + void dispose() { + _scrollController.dispose(); + _valueController.dispose(); + _notesController.dispose(); + super.dispose(); + } + + void reload({String? message}) { + if (widget.id != 0) { + setState(() { + _mealPortionType = MealPortionType.get(widget.id); + }); + } + _isNew = _mealPortionType == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + void handleSaveAction() async { if (_mealPortionTypeForm.currentState!.validate()) { - bool isNew = widget.mealPortionType == null; - // isNew - // ? MealPortionType.save( - // value: _valueController.text, - // notes: _notesController.text, - // ) - // : MealPortionType.update( - // widget.mealPortionType!.objectId!, - // value: _valueController.text, - // notes: _notesController.text, - // ); - MealPortionType.put(MealPortionType( - id: widget.mealPortionType?.id ?? 0, + MealPortionType mealPortionType = MealPortionType( + id: _mealPortionType?.id ?? 0, value: _valueController.text, notes: _notesController.text, - )); - Navigator.pop(context, '${isNew ? 'New' : ''} Meal Portion Type saved'); + ); + MealPortionType.put(mealPortionType); + Navigator.pop(context, + ['${_isNew ? 'New' : ''} Meal Portion Type saved', mealPortionType]); } } void handleCancelAction() { - bool isNew = widget.mealPortionType == null; - if (showConfirmationDialogOnCancel && - ((isNew && + if (Settings.get().showConfirmationDialogOnCancel && + ((_isNew && (_valueController.text != '' || _notesController.text != '')) || - (!isNew && - (_valueController.text != widget.mealPortionType!.value || + (!_isNew && + (_valueController.text != _mealPortionType!.value || _notesController.text != - (widget.mealPortionType!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + (_mealPortionType!.notes ?? ''))))) { + DialogUtils.showCancelConfirmationDialog( context: context, - isNew: isNew, + isNew: _isNew, onSave: handleSaveAction, ); } else { @@ -77,49 +102,53 @@ class _MealPortionTypeDetailScreenState @override Widget build(BuildContext context) { - bool isNew = widget.mealPortionType == null; + bool isNew = _mealPortionType == null; return Scaffold( appBar: AppBar( - title: Text( - isNew ? 'New Meal Portion Type' : widget.mealPortionType!.value), + title: Text(isNew ? 'New Meal Portion Type' : _mealPortionType!.value), ), drawer: const Navigation( currentLocation: MealPortionTypeDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _mealPortionTypeForm, - fields: [ - TextFormField( - controller: _valueController, - decoration: const InputDecoration( - labelText: 'Name', + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _mealPortionTypeForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty name'; - } - return null; - }, - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, - ), - keyboardType: TextInputType.multiline, - ) - ], - ), - ], + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, + ) + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, - onSave: handleSaveAction, + onAction: handleSaveAction, ), ); } diff --git a/lib/screens/meal/meal_portion_type_list.dart b/lib/screens/meal/meal_portion_type_list.dart index c59a96e..7e453d5 100644 --- a/lib/screens/meal/meal_portion_type_list.dart +++ b/lib/screens/meal/meal_portion_type_list.dart @@ -1,6 +1,5 @@ -import 'package:diameter/components/dialogs.dart'; -// import 'package:diameter/components/progress_indicator.dart'; -import 'package:diameter/config.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'; import 'package:flutter/material.dart'; @@ -19,7 +18,21 @@ class MealPortionTypeListScreen extends StatefulWidget { class _MealPortionTypeListScreenState extends State { List _mealPortionTypes = []; - void refresh({String? message}) { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { setState(() { _mealPortionTypes = MealPortionType.getAll(); }); @@ -38,12 +51,12 @@ class _MealPortionTypeListScreenState extends State { void onDelete(MealPortionType mealPortionType) { MealPortionType.remove(mealPortionType.id); - refresh(message: 'Meal Portion Type deleted'); + reload(message: 'Meal Portion Type deleted'); } void handleDeleteAction(MealPortionType mealPortionType) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(mealPortionType), message: 'Are you sure you want to delete this Meal Portion Type?', @@ -52,20 +65,14 @@ class _MealPortionTypeListScreenState extends State { onDelete(mealPortionType); } } - - @override - void initState() { - super.initState(); - refresh(); - } - + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Meal Portion Types'), actions: [ - IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) ], ), drawer: const Navigation( @@ -74,43 +81,50 @@ class _MealPortionTypeListScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: _mealPortionTypes.isNotEmpty ? ListView.builder( - padding: const EdgeInsets.only(top: 10.0), - itemCount: _mealPortionTypes.length, - itemBuilder: (context, index) { - final mealPortionType = _mealPortionTypes[index]; - - return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - MealPortionTypeDetailScreen( - mealPortionType: mealPortionType, - ), + child: _mealPortionTypes.isNotEmpty ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(10.0), + itemCount: _mealPortionTypes.length, + itemBuilder: (context, index) { + final mealPortionType = _mealPortionTypes[index]; + return Card( + child: ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MealPortionTypeDetailScreen( + id: mealPortionType.id, + ), + ), + ).then( + (message) => reload(message: message)); + }, + title: Text( + mealPortionType.value.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, ), - ).then( - (message) => refresh(message: message)); - }, - title: Text(mealPortionType.value), - subtitle: Text(mealPortionType.notes ?? ''), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => - handleDeleteAction(mealPortionType), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => + handleDeleteAction(mealPortionType), + ), + ], ), - ], - ), - ); - }, - ) : const Center( + ), + ); + }, + ), + ) : const Center( child: Text('You have not created any Meal Portion Types yet!'), ), ), @@ -123,7 +137,7 @@ class _MealPortionTypeListScreenState extends State { MaterialPageRoute( builder: (context) => const MealPortionTypeDetailScreen(), ), - ).then((message) => refresh(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/screens/meal/meal_source_detail.dart b/lib/screens/meal/meal_source_detail.dart index 595286c..fb26892 100644 --- a/lib/screens/meal/meal_source_detail.dart +++ b/lib/screens/meal/meal_source_detail.dart @@ -1,130 +1,152 @@ import 'package:diameter/components/detail.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.dart'; -// import 'package:diameter/main.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'; import 'package:diameter/models/meal_source.dart'; +import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; -// import 'package:diameter/objectbox.g.dart'; +import 'package:diameter/screens/accuracy_detail.dart'; +import 'package:diameter/screens/meal/meal_category_detail.dart'; +import 'package:diameter/screens/meal/meal_portion_type_detail.dart'; import 'package:flutter/material.dart'; class MealSourceDetailScreen extends StatefulWidget { static const String routeName = '/meal-source'; - final MealSource? mealSource; + final int id; - const MealSourceDetailScreen({Key? key, this.mealSource}) : super(key: key); + const MealSourceDetailScreen({Key? key, this.id = 0}) : super(key: key); @override _MealSourceDetailScreenState createState() => _MealSourceDetailScreenState(); } class _MealSourceDetailScreenState extends State { + MealSource? _mealSource; + bool _isNew = true; + List _portionSizeAccuracies = []; List _carbsRatioAccuracies = []; List _mealCategories = []; List _mealPortionTypes = []; final GlobalKey _mealSourceForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); + Accuracy? _defaultCarbsRatioAccuracy; Accuracy? _defaultPortionSizeAccuracy; MealCategory? _defaultMealCategory; MealPortionType? _defaultMealPortionType; + final _defaultCarbsRatioAccuracyController = TextEditingController(text: ''); + final _defaultPortionSizeAccuracyController = TextEditingController(text: ''); + final _defaultMealCategoryController = TextEditingController(text: ''); + final _defaultMealPortionTypeController = TextEditingController(text: ''); @override void initState() { super.initState(); + reload(); + _portionSizeAccuracies = Accuracy.getAllForPortionSize(); _carbsRatioAccuracies = Accuracy.getAllForCarbsRatio(); _mealCategories = MealCategory.getAll(); _mealPortionTypes = MealPortionType.getAll(); - if (widget.mealSource != null) { - _valueController.text = widget.mealSource!.value; - _notesController.text = widget.mealSource!.notes ?? ''; + if (_mealSource != null) { + _valueController.text = _mealSource!.value; + _notesController.text = _mealSource!.notes ?? ''; _defaultPortionSizeAccuracy = - widget.mealSource!.defaultPortionSizeAccuracy.target; - _defaultCarbsRatioAccuracy = widget.mealSource!.defaultCarbsRatioAccuracy.target; + _mealSource!.defaultPortionSizeAccuracy.target; + _defaultPortionSizeAccuracyController.text = (_defaultPortionSizeAccuracy ?? '').toString(); + _defaultCarbsRatioAccuracy = + _mealSource!.defaultCarbsRatioAccuracy.target; + _defaultCarbsRatioAccuracyController.text = (_defaultCarbsRatioAccuracy ?? '').toString(); - _defaultMealCategory = widget.mealSource!.defaultMealCategory.target; - _defaultMealPortionType = - widget.mealSource!.defaultMealPortionType.target; + _defaultMealCategory = _mealSource!.defaultMealCategory.target; + _defaultMealCategoryController.text = (_defaultMealCategory ?? '').toString(); + _defaultMealPortionType = _mealSource!.defaultMealPortionType.target; + _defaultMealPortionTypeController.text = (_defaultMealPortionType ?? '').toString(); } } + @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(() { + _mealSource = MealSource.get(widget.id); + }); + } + _isNew = _mealSource == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + void handleSaveAction() async { - bool isNew = widget.mealSource == null; - if (_mealSourceForm.currentState!.validate()) { - // isNew - // ? await MealSource.save( - // value: _valueController.text, - // defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy?.id.toString(), - // defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy?.id.toString(), - // // defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy, - // // defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy, - // defaultMealCategory: _defaultMealCategory, - // defaultMealPortionType: _defaultMealPortionType, - // notes: _notesController.text, - // ) - // : await MealSource.update( - // widget.mealSource!.objectId!, - // value: _valueController.text, - // defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy?.id.toString(), - // defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy?.id.toString(), - // // defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy, - // // defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy, - // defaultMealCategory: _defaultMealCategory, - // defaultMealPortionType: _defaultMealPortionType, - // notes: _notesController.text, - // ); - MealSource mealSource = MealSource( - id: widget.mealSource?.id ?? 0, - value: _valueController.text, - notes: _notesController.text, - ); - mealSource.defaultCarbsRatioAccuracy.target = _defaultCarbsRatioAccuracy; - mealSource.defaultPortionSizeAccuracy.target = - _defaultPortionSizeAccuracy; - mealSource.defaultMealCategory.target = _defaultMealCategory; - mealSource.defaultMealPortionType.target = _defaultMealPortionType; - MealSource.put(mealSource); - Navigator.pop(context, '${isNew ? 'New' : ''} Meal Source saved'); - } + MealSource mealSource = MealSource( + id: widget.id, + value: _valueController.text, + notes: _notesController.text, + ); + mealSource.defaultCarbsRatioAccuracy.target = _defaultCarbsRatioAccuracy; + mealSource.defaultPortionSizeAccuracy.target = _defaultPortionSizeAccuracy; + mealSource.defaultMealCategory.target = _defaultMealCategory; + mealSource.defaultMealPortionType.target = _defaultMealPortionType; + MealSource.put(mealSource); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Source saved', mealSource]); } void handleCancelAction() { - bool isNew = widget.mealSource == null; - if (showConfirmationDialogOnCancel && - ((isNew && + if (Settings.get().showConfirmationDialogOnCancel && + ((_isNew && (_valueController.text != '' || _defaultCarbsRatioAccuracy != null || _defaultPortionSizeAccuracy != null || _defaultMealCategory != null || _defaultMealPortionType != null || _notesController.text != '')) || - (!isNew && - (_valueController.text != widget.mealSource!.value || + (!_isNew && + (_valueController.text != _mealSource!.value || _defaultCarbsRatioAccuracy != - widget.mealSource!.defaultCarbsRatioAccuracy.target || + _mealSource!.defaultCarbsRatioAccuracy.target || _defaultPortionSizeAccuracy != - widget.mealSource!.defaultPortionSizeAccuracy.target || + _mealSource!.defaultPortionSizeAccuracy.target || _defaultMealCategory != - widget.mealSource!.defaultMealCategory.target || + _mealSource!.defaultMealCategory.target || _defaultMealPortionType != - widget.mealSource!.defaultMealPortionType.target || - _notesController.text != - (widget.mealSource!.notes ?? ''))))) { - Dialogs.showCancelConfirmationDialog( + _mealSource!.defaultMealPortionType.target || + _notesController.text != (_mealSource!.notes ?? ''))))) { + DialogUtils.showCancelConfirmationDialog( context: context, - isNew: isNew, + isNew: _isNew, onSave: handleSaveAction, ); } else { @@ -134,118 +156,225 @@ class _MealSourceDetailScreenState extends State { @override Widget build(BuildContext context) { - bool isNew = widget.mealSource == null; return Scaffold( appBar: AppBar( - title: Text(isNew ? 'New Meal Source' : widget.mealSource!.value), + title: Text(_isNew ? 'New Meal Source' : _mealSource!.value), ), drawer: const Navigation(currentLocation: MealSourceDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _mealSourceForm, - fields: [ - TextFormField( - controller: _valueController, - decoration: const InputDecoration( - labelText: 'Name', + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _mealSourceForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty name'; - } - return null; - }, - ), - StyledDropdownButton( - selectedItem: _defaultCarbsRatioAccuracy, - label: 'Default Carbs Ratio Accuracy', - items: _carbsRatioAccuracies, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _defaultCarbsRatioAccuracy = value; - }); - }, - ), - StyledDropdownButton( - selectedItem: _defaultPortionSizeAccuracy, - label: 'Default Portion Size Accuracy', - items: _portionSizeAccuracies, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _defaultPortionSizeAccuracy = value; - }); - }, - ), - // StyledFutureDropdownButton( - // selectedItem: _defaultCarbsRatioAccuracy, - // label: 'Default Carbs Ratio Accuracy', - // items: _carbsRatioAccuracies, - // getItemValue: (item) => item.objectId, - // renderItem: (item) => Text(item.value), - // onChanged: (value) { - // setState(() { - // _defaultCarbsRatioAccuracy = value; - // }); - // }, - // ), - // StyledFutureDropdownButton( - // selectedItem: _defaultPortionSizeAccuracy, - // label: 'Default Portion Size Accuracy', - // items: _portionSizeAccuracies, - // getItemValue: (item) => item.objectId, - // renderItem: (item) => Text(item.value), - // onChanged: (value) { - // setState(() { - // _defaultPortionSizeAccuracy = value; - // }); - // }, - // ), - StyledDropdownButton( - selectedItem: _defaultMealCategory, - label: 'Default Meal Category', - items: _mealCategories, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _defaultMealCategory = value; - }); - }, - ), - StyledDropdownButton( - selectedItem: _defaultMealPortionType, - label: 'Default Meal Portion Type', - items: _mealPortionTypes, - // getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _defaultMealPortionType = value; - }); - }, - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + selectedItem: _defaultCarbsRatioAccuracy, + controller: _defaultCarbsRatioAccuracyController, + label: 'Default Carbs Ratio Accuracy', + items: _carbsRatioAccuracies, + onChanged: (value) { + setState(() { + _defaultCarbsRatioAccuracy = value; + _defaultCarbsRatioAccuracyController.text = + (_defaultCarbsRatioAccuracy ?? '').toString(); + }); + }, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + _defaultCarbsRatioAccuracy == null + ? const AccuracyDetailScreen() + : AccuracyDetailScreen( + id: _defaultCarbsRatioAccuracy!.id), + ), + ).then((result) { + setState(() { + _defaultCarbsRatioAccuracy = result?[1]; + _defaultCarbsRatioAccuracyController.text = + (_defaultCarbsRatioAccuracy ?? '').toString(); + }); + reload(message: result?[0]); + }); + }, + icon: Icon(_defaultCarbsRatioAccuracy == null + ? Icons.add + : Icons.edit), + ), + ], ), - keyboardType: TextInputType.multiline, - ) - ], - ), - ], + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + selectedItem: _defaultPortionSizeAccuracy, + controller: _defaultPortionSizeAccuracyController, + label: 'Default Portion Size Accuracy', + items: _portionSizeAccuracies, + onChanged: (value) { + setState(() { + _defaultPortionSizeAccuracy = value; + _defaultPortionSizeAccuracyController.text = + (_defaultPortionSizeAccuracy ?? '') + .toString(); + }); + }, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + _defaultPortionSizeAccuracy == null + ? const AccuracyDetailScreen() + : AccuracyDetailScreen( + id: _defaultPortionSizeAccuracy!.id), + ), + ).then((result) { + setState(() { + _defaultPortionSizeAccuracy = result?[1]; + _defaultPortionSizeAccuracyController.text = + (_defaultPortionSizeAccuracy ?? '') + .toString(); + }); + reload(message: result?[0]); + }); + }, + icon: Icon(_defaultPortionSizeAccuracy == null + ? Icons.add + : Icons.edit), + ), + ], + ), + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + selectedItem: _defaultMealCategory, + controller: _defaultMealCategoryController, + label: 'Default Meal Category', + items: _mealCategories, + onChanged: (value) { + setState(() { + _defaultMealCategory = value; + _defaultMealCategoryController.text = + (_defaultMealCategory ?? '').toString(); + }); + }, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _defaultMealCategory == null + ? const MealCategoryDetailScreen() + : MealCategoryDetailScreen( + id: _defaultMealCategory!.id), + ), + ).then((result) { + setState(() { + _defaultMealCategory = result?[1]; + _defaultMealCategoryController.text = + (_defaultMealCategory ?? '').toString(); + }); + reload(message: result?[0]); + }); + }, + icon: Icon(_defaultMealCategory == null + ? Icons.add + : Icons.edit), + ), + ], + ), + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + selectedItem: _defaultMealPortionType, + controller: _defaultMealPortionTypeController, + label: 'Default Meal Portion Type', + items: _mealPortionTypes, + onChanged: (value) { + setState(() { + _defaultMealPortionType = value; + _defaultMealPortionTypeController.text = + (_defaultMealPortionType ?? '').toString(); + }); + }, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + _defaultMealPortionType == null + ? const MealPortionTypeDetailScreen() + : MealPortionTypeDetailScreen( + id: _defaultMealPortionType!.id), + ), + ).then((result) { + setState(() { + _defaultMealPortionType = result?[1]; + _defaultMealPortionTypeController.text = + (_defaultMealPortionType ?? '').toString(); + }); + reload(message: result?[0]); + }); + }, + icon: Icon(_defaultMealPortionType == null + ? Icons.add + : Icons.edit), + ), + ], + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, + ) + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, - onSave: handleSaveAction, + onAction: handleSaveAction, ), ); } diff --git a/lib/screens/meal/meal_source_list.dart b/lib/screens/meal/meal_source_list.dart index c72ed19..479fcc9 100644 --- a/lib/screens/meal/meal_source_list.dart +++ b/lib/screens/meal/meal_source_list.dart @@ -1,7 +1,6 @@ -// import 'package:diameter/components/progress_indicator.dart'; -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/config.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'; import 'package:diameter/screens/meal/meal_source_detail.dart'; import 'package:flutter/material.dart'; @@ -18,7 +17,21 @@ class MealSourceListScreen extends StatefulWidget { class _MealSourceListScreenState extends State { List _mealSources = []; - void refresh({String? message}) { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + reload(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void reload({String? message}) { setState(() { _mealSources = MealSource.getAll(); }); @@ -37,12 +50,12 @@ class _MealSourceListScreenState extends State { void onDelete(MealSource mealSource) { MealSource.remove(mealSource.id); - refresh(message: 'Meal Source deleted'); + reload(message: 'Meal Source deleted'); } void handleDeleteAction(MealSource mealSource) async { - if (showConfirmationDialogOnDelete) { - Dialogs.showConfirmationDialog( + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( context: context, onConfirm: () => onDelete(mealSource), message: 'Are you sure you want to delete this Meal Source?', @@ -52,19 +65,13 @@ class _MealSourceListScreenState extends State { } } - @override - void initState() { - super.initState(); - refresh(); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Meal Sources'), actions: [ - IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) ], ), drawer: const Navigation(currentLocation: MealSourceListScreen.routeName), @@ -72,41 +79,49 @@ class _MealSourceListScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: _mealSources.isNotEmpty ? ListView.builder( - padding: const EdgeInsets.only(top: 10.0), - itemCount: _mealSources.length, - itemBuilder: (context, index) { - final mealSource = _mealSources[index]; - - return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MealSourceDetailScreen( - mealSource: mealSource, - ), + child: _mealSources.isNotEmpty ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(10.0), + itemCount: _mealSources.length, + itemBuilder: (context, index) { + final mealSource = _mealSources[index]; + + return Card( + child: ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MealSourceDetailScreen( + id: mealSource.id, + ), + ), + ).then((result) => reload(message: result?[0])); + }, + title: Text( + mealSource.value.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, ), - ).then((message) => refresh(message: message)); - }, - title: Text(mealSource.value), - subtitle: Text(mealSource.notes ?? ''), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () async { - handleDeleteAction(mealSource); - }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () async { + handleDeleteAction(mealSource); + }, + ), + ], ), - ], - ), - ); - } + ), + ); + } + ), ) : const Center( child: Text('You have not created any Meal Sources yet!'), ), @@ -120,7 +135,7 @@ class _MealSourceListScreenState extends State { MaterialPageRoute( builder: (context) => const MealSourceDetailScreen(), ), - ).then((message) => refresh(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/screens/recipe/recipe_detail.dart b/lib/screens/recipe/recipe_detail.dart new file mode 100644 index 0000000..6c08173 --- /dev/null +++ b/lib/screens/recipe/recipe_detail.dart @@ -0,0 +1,322 @@ +import 'package:diameter/components/detail.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'; +import 'package:diameter/models/settings.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/meal/meal_detail.dart'; +import 'package:flutter/material.dart'; + +class RecipeDetailScreen extends StatefulWidget { + static const String routeName = '/recipe'; + final int id; + + const RecipeDetailScreen({Key? key, this.id = 0}) : super(key: key); + + @override + _RecipeDetailScreenState createState() => _RecipeDetailScreenState(); +} + +class _RecipeDetailScreenState extends State { + Recipe? _recipe; + List _ingredients = []; + + bool _isNew = true; + bool _isSaving = false; + + final GlobalKey _recipeForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + final _nameController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + + double _servings = 1; + + final List _ingredientControllers = []; + + List _meals = []; + + @override + void initState() { + super.initState(); + + reload(); + + _meals = Meal.getAll(); + + if (_recipe != null) { + _nameController.text = _recipe!.name; + _servings = _recipe!.servings ?? 1; + _notesController.text = _recipe!.notes ?? ''; + + if (_ingredients.isNotEmpty) { + for (Ingredient ingredient in _ingredients) { + _ingredientControllers.add( + TextEditingController(text: ingredient.ingredient.target?.value)); + } + } + } + } + + void reload({String? message}) { + if (widget.id != 0) { + setState(() { + _recipe = Recipe.get(widget.id); + _ingredients = Ingredient.getAllForRecipe(widget.id); + }); + } + _isNew = _recipe == null; + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void onAddIngredient() { + final newIngredient = Ingredient(amount: 0); + setState(() { + newIngredient.recipe.target = _recipe; + _ingredients.add(newIngredient); + _ingredientControllers.add(TextEditingController(text: '')); + }); + } + + void handleSaveAction({bool close = false}) async { + setState(() { + _isSaving = true; + }); + if (_recipeForm.currentState!.validate()) { + Recipe recipe = Recipe( + id: widget.id, + name: _nameController.text, + servings: _servings, + notes: _notesController.text, + ); + Recipe.put(recipe); + List ingredients = _ingredients.map((ingredient) { + if (ingredient.id != 0 && + (!ingredient.ingredient.hasValue || ingredient.amount == 0)) { + ingredient.deleted = true; + } + return ingredient; + }).toList(); + ingredients.retainWhere((ingredient) { + return ingredient.id != 0 || + (ingredient.amount > 0 && ingredient.ingredient.hasValue); + }); + Ingredient.putMany(ingredients); + + if (close) { + Navigator.pop(context, ['${_isNew ? 'New' : ''} Recipe Saved', recipe]); + } else { + if (_isNew) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RecipeDetailScreen(id: recipe.id), + ), + ).then((result) => Navigator.pop(context, result)); + } else { + reload(message: 'Recipe saved'); + } + } + + setState(() { + _isSaving = false; + }); + } + } + + void handleCancelAction() { + if (Settings.get().showConfirmationDialogOnCancel && + ((_isNew && + (_nameController.text != '' || + _servings != 1 || + _notesController.text != '')) || + (!_isNew && + (_nameController.text != _recipe!.name || + _servings != _recipe!.servings || + _notesController.text != (_recipe!.notes ?? ''))))) { + DialogUtils.showCancelConfirmationDialog( + context: context, + isNew: _isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_isNew ? 'New Recipe' : _recipe!.name), + ), + drawer: const Navigation(currentLocation: RecipeDetailScreen.routeName), + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: [ + FormWrapper( + formState: _recipeForm, + fields: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty title'; + } + return null; + }, + ), + // 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, + decoration: const InputDecoration( + labelText: 'Notes', + ), + minLines: 2, + maxLines: 5, + ), + const Divider(), + GestureDetector( + onTap: onAddIngredient, + child: Row( + children: [ + Text( + 'INGREDIENTS', + style: Theme.of(context).textTheme.subtitle2, + ), + const Spacer(), + IconButton( + onPressed: onAddIngredient, + icon: const Icon(Icons.add), + ), + ], + ), + ), + ], + ), + !_isNew && _ingredients.isNotEmpty + ? ListBody( + children: _ingredients.map((item) { + final ingredient = item.ingredient.target; + final index = _ingredients.indexOf(item); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 5.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _ingredientControllers[index], + selectedItem: ingredient, + label: 'Meal Category', + items: _meals, + onChanged: (value) { + setState(() { + _ingredients[index] + .ingredient + .target = value; + _ingredientControllers[index].text = + value?.value ?? ''; + }); + }, + ), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ingredient == null + ? const MealDetailScreen() + : MealDetailScreen( + id: ingredient.id), + ), + ).then((result) { + _ingredients[index].ingredient.target = + result?[1]; + _ingredientControllers[index].text = + result?[1].value ?? ''; + reload(message: result?[0]); + }); + }, + icon: Icon(ingredient == null + ? Icons.add + : Icons.edit), + ), + ], + ), + // 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(); + // }); + // } + // }, + // ), + // ), + ], + ), + ); + }).toList(), + ) + : Center( + child: Text(_isNew + ? 'Save the Recipe in order to add ingredients!' + : 'You have not added any Ingredients yet!'), + ) + ], + ), + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onAction: _isSaving ? null : handleSaveAction, + onMiddleAction: _isSaving ? null : () => handleSaveAction(close: true), + ), + ); + } +} diff --git a/lib/screens/recipe/recipe_list.dart b/lib/screens/recipe/recipe_list.dart new file mode 100644 index 0000000..b4de28f --- /dev/null +++ b/lib/screens/recipe/recipe_list.dart @@ -0,0 +1,200 @@ +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'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/recipe/recipe_detail.dart'; +import 'package:flutter/material.dart'; + +class RecipeListScreen extends StatefulWidget { + static const String routeName = '/recipes'; + + const RecipeListScreen({Key? key}) : super(key: key); + + @override + _RecipeListScreenState createState() => _RecipeListScreenState(); +} + +class _RecipeListScreenState extends State { + List _recipes = []; + + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + reload(); + } + + void reload({String? message}) { + setState(() { + _recipes = Recipe.getAll(); + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void onDelete(Recipe recipe) { + Recipe.remove(recipe.id); + reload(message: 'Recipe deleted'); + } + + void handleDeleteAction(Recipe recipe) async { + if (Settings.get().showConfirmationDialogOnDelete) { + DialogUtils.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(recipe), + message: 'Are you sure you want to delete this Recipe?', + ); + } else { + onDelete(recipe); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Recipes'), actions: [ + IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) + ]), + drawer: const Navigation(currentLocation: RecipeListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: _recipes.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(10.0), + itemCount: _recipes.length, + itemBuilder: (context, index) { + final recipe = _recipes[index]; + final carbsRatio = + Ingredient.getCarbsRatioForRecipe(recipe.id); + final carbsPerPortion = Recipe.getCarbsPerPortion(recipe.id); + return Card( + child: ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + RecipeDetailScreen(id: recipe.id), + ), + ).then((result) => reload(message: result?[0])); + }, + title: Text( + recipe.name.toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + ), + subtitle: Padding( + padding: + const EdgeInsets.symmetric(vertical: 10.0), + child: Row( + children: [ + Column( + children: [ + Text(recipe.notes ?? ''), + ], + ), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: + ((carbsRatio ?? 0) > 0) + ? [ + Text(carbsRatio!.toString()), + const Text('% carbs', + textScaleFactor: 0.75), + ] + : [], + ), + ), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: + (recipe.servings != null) + ? [ + Text(recipe.servings! + .toStringAsPrecision(3)), + const Text('servings', + textScaleFactor: 0.75), + ] + : [], + ), + ), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: + ((carbsPerPortion ?? 0) > 0) + ? [ + Text(carbsPerPortion! + .toStringAsPrecision(3)), + Text( + '${Settings.nutritionMeasurementSuffix} carbs per serving', + textScaleFactor: 0.75), + ] + : [], + ), + ), + ], + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () => handleDeleteAction(recipe), + icon: const Icon(Icons.delete, + color: Colors.blue), + ) + ], + ), + ), + ); + }, + ), + ) + : const Center( + child: Text('You have not created any Recipes yet!'), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RecipeDetailScreen(), + ), + ).then((result) => reload(message: result?[0])); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/settings.dart b/lib/settings.dart index f17dd65..4c856da 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -1,87 +1,12 @@ -import 'package:diameter/components/dialogs.dart'; -import 'package:diameter/components/forms.dart'; -import 'package:diameter/config.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:shared_preferences/shared_preferences.dart'; - -enum GlucoseDisplayMode { activeOnly, bothForList, bothForDetail, both } - -enum GlucoseMeasurement { - mgPerDl, - mmolPerL, -} - -enum NutritionMeasurement { - grams, - ounces, - cups, -} - -class Settings { - static void loadSettingsIntoConfig() async { - nutritionMeasurement = await getNutritionMeasurement(); - glucoseMeasurement = await getGlucoseMeasurement(); - glucoseDisplayMode = await getGlucoseDisplayMode(); - } - - static Future getGlucoseDisplayMode() async { - final settings = await SharedPreferences.getInstance(); - int? index = settings.getInt('glucoseDisplayMode'); - return index != null && index < GlucoseDisplayMode.values.length - ? GlucoseDisplayMode.values[index] - : GlucoseDisplayMode.bothForList; - } - - static Future getGlucoseMeasurement() async { - final settings = await SharedPreferences.getInstance(); - int? index = settings.getInt('glucoseMeasurement'); - return index != null && index < GlucoseMeasurement.values.length - ? GlucoseMeasurement.values[index] - : GlucoseMeasurement.mgPerDl; - } - - static Future getNutritionMeasurement() async { - final settings = await SharedPreferences.getInstance(); - int? index = settings.getInt('nutritionMeasurement'); - return index != null && index < NutritionMeasurement.values.length - ? NutritionMeasurement.values[index] - : NutritionMeasurement.grams; - } - - static void setGlucoseDisplayMode( - GlucoseDisplayMode? glucoseDisplayMode) async { - final settings = await SharedPreferences.getInstance(); - if (glucoseDisplayMode != null) { - settings.setInt('glucoseDisplayMode', glucoseDisplayMode.index); - } - } - - static void setGlucoseMeasurement( - GlucoseMeasurement? glucoseMeasurement) async { - final settings = await SharedPreferences.getInstance(); - if (glucoseMeasurement != null) { - settings.setInt('preferredGlucoseMeasurement', glucoseMeasurement.index); - } - } - - static void setNutritionMeasurement( - NutritionMeasurement? nutritionMeasurement) async { - final settings = await SharedPreferences.getInstance(); - if (nutritionMeasurement != null) { - settings.setInt( - 'preferredNutritionMeasurement', nutritionMeasurement.index); - } - } - - static void resetAll() async { - final settings = await SharedPreferences.getInstance(); - - settings.remove('glucoseDisplayMode'); - settings.remove('preferredGlucoseMeasurement'); - settings.remove('preferredNutritionMeasurement'); - } -} +import 'package:intl/intl.dart'; class SettingsScreen extends StatefulWidget { static const String routeName = '/settings'; @@ -93,29 +18,156 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - final GlobalKey _settingsForm = GlobalKey(); + late Settings _settings; - void onReset() { - Settings.resetAll(); + final ScrollController _scrollController = ScrollController(); + + bool _measurementsIsExpanded = true; + bool _promptsIsExpanded = true; + bool _formatIsExpanded = true; + + final _nutritionMeasurementLabelController = TextEditingController(text: ''); + final _glucoseMeasurementLabelController = TextEditingController(text: ''); + final _dateFormatController = TextEditingController(text: ''); + final _longDateFormatController = TextEditingController(text: ''); + final _timeFormatController = TextEditingController(text: ''); + final _longTimeFormatController = TextEditingController(text: ''); + + final _insulinIncrementsController = TextEditingController(text: ''); + final _nutritionIncrementsController = TextEditingController(text: ''); + final _mmolPerLIncrementsController = TextEditingController(text: ''); + final _targetGlucoseMgPerDlController = TextEditingController(text: ''); + final _targetGlucoseMmolPerLController = TextEditingController(text: ''); + + late bool _onlyDisplayActiveGlucoseMeasurement; + late bool _displayBothGlucoseMeasurementsInDetailView; + late bool _displayBothGlucoseMeasurementsInListView; + + late bool _showConfirmationDialogOnCancel; + late bool _showConfirmationDialogOnDelete; + late bool _showConfirmationDialogOnStopEvent; + + @override + void initState() { + super.initState(); + _settings = Settings.get(); + _nutritionMeasurementLabelController.text = + nutritionMeasurementLabels[_settings.nutritionMeasurementIndex]; + _glucoseMeasurementLabelController.text = + glucoseMeasurementLabels[_settings.glucoseMeasurementIndex]; + _insulinIncrementsController.text = _settings.insulinIncrements.toString(); + _nutritionIncrementsController.text = + _settings.nutritionIncrements.toString(); + _mmolPerLIncrementsController.text = + _settings.mmolPerLIncrements.toString(); + _targetGlucoseMgPerDlController.text = + _settings.targetGlucoseMgPerDl.toInt().toString(); + _targetGlucoseMmolPerLController.text = + _settings.targetGlucoseMmolPerL.toString(); + _onlyDisplayActiveGlucoseMeasurement = _settings.glucoseDisplayModeIndex == + GlucoseDisplayMode.activeOnly.index; + _displayBothGlucoseMeasurementsInDetailView = + _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.both.index || + _settings.glucoseDisplayModeIndex == + GlucoseDisplayMode.bothForDetail.index; + _displayBothGlucoseMeasurementsInListView = + _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.both.index || + _settings.glucoseDisplayModeIndex == + GlucoseDisplayMode.bothForList.index; + _dateFormatController.text = _settings.dateFormat; + _longDateFormatController.text = _settings.longDateFormat ?? ''; + _timeFormatController.text = _settings.timeFormat; + _longTimeFormatController.text = _settings.longTimeFormat ?? ''; + _showConfirmationDialogOnCancel = _settings.showConfirmationDialogOnCancel; + _showConfirmationDialogOnDelete = _settings.showConfirmationDialogOnDelete; + _showConfirmationDialogOnStopEvent = + _settings.showConfirmationDialogOnStopEvent; + } + + @override + void dispose() { + _scrollController.dispose(); + _nutritionMeasurementLabelController.dispose(); + _glucoseMeasurementLabelController.dispose(); + _dateFormatController.dispose(); + _longDateFormatController.dispose(); + _timeFormatController.dispose(); + _longTimeFormatController.dispose(); + _insulinIncrementsController.dispose(); + _nutritionIncrementsController.dispose(); + _mmolPerLIncrementsController.dispose(); + _targetGlucoseMgPerDlController.dispose(); + _targetGlucoseMmolPerLController.dispose(); + super.dispose(); + } + + void reload({String? message}) { setState(() { - Settings.loadSettingsIntoConfig(); + _settings = Settings.get(); + }); + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } }); } + void saveSettings() { + Settings.put(Settings( + id: _settings.id, + nutritionMeasurementIndex: nutritionMeasurementLabels + .indexOf(_nutritionMeasurementLabelController.text), + glucoseMeasurementIndex: glucoseMeasurementLabels + .indexOf(_glucoseMeasurementLabelController.text), + glucoseDisplayModeIndex: _onlyDisplayActiveGlucoseMeasurement + ? GlucoseDisplayMode.activeOnly.index + : _displayBothGlucoseMeasurementsInDetailView && + _displayBothGlucoseMeasurementsInListView + ? GlucoseDisplayMode.both.index + : _displayBothGlucoseMeasurementsInDetailView + ? GlucoseDisplayMode.bothForDetail.index + : GlucoseDisplayMode.bothForList.index, + targetGlucoseMgPerDl: + int.tryParse(_targetGlucoseMgPerDlController.text) ?? _settings.targetGlucoseMgPerDl, + targetGlucoseMmolPerL: + double.tryParse(_targetGlucoseMmolPerLController.text) ?? _settings.targetGlucoseMmolPerL, + insulinIncrements: + double.tryParse(_insulinIncrementsController.text) ?? _settings.insulinIncrements, + nutritionIncrements: + double.tryParse(_nutritionIncrementsController.text) ?? _settings.nutritionIncrements, + mmolPerLIncrements: + double.tryParse(_mmolPerLIncrementsController.text) ?? _settings.mmolPerLIncrements, + dateFormat: _dateFormatController.text, + longDateFormat: _longDateFormatController.text, + timeFormat: _timeFormatController.text, + longTimeFormat: _longTimeFormatController.text, + showConfirmationDialogOnCancel: _showConfirmationDialogOnCancel, + showConfirmationDialogOnDelete: _showConfirmationDialogOnDelete, + showConfirmationDialogOnStopEvent: _showConfirmationDialogOnStopEvent, + )); + reload(message: 'Settings updated'); + } + + void onReset() { + Settings.reset(); + reload(message: 'Settings have been reset to default'); + } + void handleResetAction() async { - Dialogs.showConfirmationDialog( + DialogUtils.showConfirmationDialog( context: context, onConfirm: onReset, message: 'Are you sure you want to reset all settings?', ); } - @override - initState() { - super.initState(); - Settings.loadSettingsIntoConfig(); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -124,93 +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.stretch, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - StyledForm( - formState: _settingsForm, - fields: [ - StyledDropdownButton( - selectedItem: nutritionMeasurement, - label: 'Preferred Nutrition Measurement', - items: NutritionMeasurement.values, - renderItem: (item) => Text(item.toString().split('.')[1]), - onChanged: (value) { - if (value != null) { - Settings.setNutritionMeasurement(value); - setState(() { - nutritionMeasurement = value; - }); - } - }, + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: GestureDetector( + onTap: () => setState(() { + _measurementsIsExpanded = !_measurementsIsExpanded; + }), + child: Row( + children: [ + Expanded( + child: Text( + 'MEASUREMENTS', + style: Theme.of(context).textTheme.subtitle2, + ), + ), + Icon(_measurementsIsExpanded + ? Icons.expand_less + : Icons.expand_more), + ], ), - StyledDropdownButton( - selectedItem: glucoseMeasurement, - label: 'Preferred Glucose Measurement', - items: GlucoseMeasurement.values, - renderItem: (item) => Text(item.toString().split('.')[1]), - onChanged: (value) { - if (value != null) { - Settings.setGlucoseMeasurement(value); - setState(() { - glucoseMeasurement = value; - }); - } - }, + ), + ), + Column( + children: _measurementsIsExpanded + ? [ + AutoCompleteDropdownButton( + controller: _nutritionMeasurementLabelController, + selectedItem: _nutritionMeasurementLabelController.text, + label: 'Preferred Nutrition Measurement', + items: nutritionMeasurementLabels, + onChanged: (value) { + _nutritionMeasurementLabelController.text = + value ?? ''; + saveSettings(); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: AutoCompleteDropdownButton( + controller: _glucoseMeasurementLabelController, + selectedItem: _glucoseMeasurementLabelController.text, + label: 'Preferred Glucose Measurement', + items: glucoseMeasurementLabels, + onChanged: (value) { + _glucoseMeasurementLabelController.text = + value ?? ''; + saveSettings(); + }, + ), + ), + Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl + ? NumberFormField( + label: 'Target glucose', + suffix: 'mg/dl', + controller: _targetGlucoseMgPerDlController, + showSteppers: false, + onChanged: (_) async { + await Future.delayed( + const Duration(seconds: 1)); + if (Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl) { + final value = int.tryParse( + _targetGlucoseMgPerDlController.text); + _targetGlucoseMmolPerLController.text = Utils + .toStringMatchingTemplateFractionPrecision( + Utils.convertMgPerDlToMmolPerL(value ?? 0), + Settings.mmolPerLSteps); + await Future.delayed( + const Duration(seconds: 1)); + saveSettings(); + } + }, + ) + : NumberFormField( + label: 'Target glucose', + suffix: 'mmol/l', + controller: _targetGlucoseMmolPerLController, + showSteppers: false, + onChanged: (_) async { + await Future.delayed( + const Duration(seconds: 1)); + if (Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL) { + final value = double.tryParse( + _targetGlucoseMmolPerLController.text); + _targetGlucoseMgPerDlController.text = + Utils.convertMmolPerLToMgPerDl(value ?? 0) + .toString(); + saveSettings(); + } + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: NumberFormField( + controller: _insulinIncrementsController, + showSteppers: false, + label: 'Insulin increment', + onChanged: (value) { + _insulinIncrementsController.text = + (value ?? 0).toString(); + saveSettings(); + }), + ), + NumberFormField( + controller: _nutritionIncrementsController, + showSteppers: false, + label: 'Nutrition increment', + onChanged: (value) { + _nutritionIncrementsController.text = + (value ?? 0).toString(); + saveSettings(); + }), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: NumberFormField( + controller: _mmolPerLIncrementsController, + showSteppers: false, + label: 'Mmol/L increment', + onChanged: (value) { + _mmolPerLIncrementsController.text = + (value ?? 0).toString(); + saveSettings(); + }), + ), + BooleanFormField( + value: _onlyDisplayActiveGlucoseMeasurement, + label: 'only display active glucose measurement', + onChanged: (value) { + _onlyDisplayActiveGlucoseMeasurement = value; + saveSettings(); + }, + ), + BooleanFormField( + value: _displayBothGlucoseMeasurementsInDetailView, + enabled: !_onlyDisplayActiveGlucoseMeasurement, + label: + 'display both glucose measurements in detail view', + onChanged: (value) { + _displayBothGlucoseMeasurementsInDetailView = value; + saveSettings(); + }, + ), + BooleanFormField( + value: _displayBothGlucoseMeasurementsInListView, + enabled: !_onlyDisplayActiveGlucoseMeasurement, + label: 'display both glucose measurements in list view', + onChanged: (value) { + _displayBothGlucoseMeasurementsInListView = value; + saveSettings(); + }, + ), + ] + : [], + ), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: GestureDetector( + onTap: () => setState(() { + _promptsIsExpanded = !_promptsIsExpanded; + }), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + 'CONFIRMATION PROMPTS', + style: Theme.of(context).textTheme.subtitle2, + ), + ), + Icon(_promptsIsExpanded + ? Icons.expand_less + : Icons.expand_more), + ], ), - StyledBooleanFormField( - value: glucoseDisplayMode == GlucoseDisplayMode.activeOnly, - label: 'only display active glucose measurement', - onChanged: (_) { - GlucoseDisplayMode mode = - glucoseDisplayMode == GlucoseDisplayMode.activeOnly - ? GlucoseDisplayMode.both - : GlucoseDisplayMode.activeOnly; - Settings.setGlucoseDisplayMode(mode); - setState(() { - glucoseDisplayMode = mode; - }); - }, + ), + ), + 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), + ], ), - StyledBooleanFormField( - value: glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == GlucoseDisplayMode.bothForDetail, - enabled: glucoseDisplayMode != GlucoseDisplayMode.activeOnly, - label: 'display both glucose measurements in detail view', - onChanged: (_) { - GlucoseDisplayMode mode = glucoseDisplayMode == - GlucoseDisplayMode.both - ? GlucoseDisplayMode.bothForList - : glucoseDisplayMode == GlucoseDisplayMode.bothForList - ? GlucoseDisplayMode.both - : GlucoseDisplayMode.activeOnly; - Settings.setGlucoseDisplayMode(mode); - setState(() { - glucoseDisplayMode = mode; - }); - }, - ), - StyledBooleanFormField( - value: glucoseDisplayMode == GlucoseDisplayMode.both || - glucoseDisplayMode == GlucoseDisplayMode.bothForList, - enabled: glucoseDisplayMode != GlucoseDisplayMode.activeOnly, - label: 'display both glucose measurements in list view', - onChanged: (_) { - GlucoseDisplayMode mode = glucoseDisplayMode == - GlucoseDisplayMode.both - ? GlucoseDisplayMode.bothForDetail - : glucoseDisplayMode == GlucoseDisplayMode.bothForDetail - ? GlucoseDisplayMode.both - : GlucoseDisplayMode.activeOnly; - Settings.setGlucoseDisplayMode(mode); - setState(() { - glucoseDisplayMode = mode; - }); - }, - ), - // TODO: add fields for date and time formats - // TODO: add fields for glucose target - ], + ), + ), + 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 9023e30..d213d58 100644 --- a/lib/utils/date_time_utils.dart +++ b/lib/utils/date_time_utils.dart @@ -1,14 +1,17 @@ -import 'package:diameter/config.dart'; +import 'package:diameter/models/settings.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +final DateTime dummyDate = DateTime(2000); + class DateTimeUtils { static String displayDateTime(DateTime? date, {String fallback = ''}) { if (date == null) { return fallback; } DateTime localDate = date.toLocal(); - final DateFormat formatter = DateFormat('$dateFormat $timeFormat'); + final DateFormat formatter = + DateFormat('${Settings.get().dateFormat} ${Settings.get().timeFormat}'); return formatter.format(localDate); } @@ -17,7 +20,8 @@ class DateTimeUtils { return fallback; } DateTime localDate = date.toLocal(); - final DateFormat formatter = DateFormat(longDateFormat ?? dateFormat); + final DateFormat formatter = + DateFormat(Settings.get().longDateFormat ?? Settings.get().dateFormat); return formatter.format(localDate); } @@ -27,8 +31,9 @@ class DateTimeUtils { return fallback; } DateTime localDate = date.toLocal(); - final DateFormat formatter = DateFormat( - longFormat == true ? longTimeFormat ?? timeFormat : timeFormat); + final DateFormat formatter = DateFormat(longFormat == true + ? Settings.get().longTimeFormat ?? Settings.get().timeFormat + : Settings.get().timeFormat); return formatter.format(localDate); } @@ -37,8 +42,9 @@ class DateTimeUtils { if (time == null) { return fallback; } - final DateFormat formatter = DateFormat( - longFormat == true ? longTimeFormat ?? timeFormat : 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 1e5e6e4..be9e234 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,31 +1,67 @@ import 'dart:math'; class Utils { - static double roundToDecimalPlaces(double value, int places) { - double mod = pow(10.0, places).toDouble(); - return ((value * mod).round().toDouble() / mod); + // static double roundToDecimalPlaces(double value, int precision) { + // double mod = pow(10.0, precision).toDouble(); + // return ((value * mod).round().toDouble() / mod); + // } + + static double roundToMultipleOfBase(double value, double base) { + double result = value; + double remainder = value % base; + int precision = Utils.getFractionDigitsLength(base); + + if (remainder != 0) { + result = Utils.addDoublesWithPrecision(result, -remainder, precision); + if (remainder > base / 2) { + result = Utils.addDoublesWithPrecision(result, base, precision); + } + } + + return result; } - static double convertMgPerDlToMmolPerL(int mgPerDl) { - return Utils.roundToDecimalPlaces(mgPerDl * 0.0555, 2); + 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] == '0' ? 0 : fractionDigits[1].length; + } + + static String toStringMatchingTemplateFractionPrecision( + double value, double template) { + final precision = getFractionDigitsLength(template); + return value.toStringAsFixed(precision); + } + + static double convertMgPerDlToMmolPerL(int mgPerDl, {double step = 0.01}) { + return Utils.roundToMultipleOfBase(mgPerDl * 0.0555, step); } static int convertMmolPerLToMgPerDl(double mmolPerL) { return (mmolPerL * 18.018).round(); } - static double calculateCarbsPerPortion( - double carbsRatio, double portionSize) { - return Utils.roundToDecimalPlaces(carbsRatio * portionSize / 100, 2); + static double calculateCarbs(double carbsRatio, double portionSize, + {double step = 0.01}) { + return Utils.roundToMultipleOfBase(carbsRatio * portionSize / 100, step); } static double calculateCarbsRatio( - double carbsPerPortion, double portionSize) { - return Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2); + double carbsPerPortion, double portionSize, {double step = 0.01}) { + return portionSize > 0 + ? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / portionSize, step) + : 0; } static double calculatePortionSize( - double carbsRatio, double carbsPerPortion) { - return Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2); + double carbsRatio, double carbsPerPortion, {double step = 0.01}) { + return carbsRatio > 0 + ? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / carbsRatio, step) + : 0; } } diff --git a/objectbox/data.mdb b/objectbox/data.mdb new file mode 100644 index 0000000..c30d7a2 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..d2ddfb8 Binary files /dev/null and b/objectbox/lock.mdb differ diff --git a/pubspec.lock b/pubspec.lock index 3e177e8..7a848e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "30.0.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.7.0" + version: "2.8.0" args: dependency: transitive description: @@ -63,14 +63,14 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" build_runner_core: dependency: transitive description: @@ -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.0" - 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.0" - 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: @@ -203,7 +161,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" dart_style: 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.5.6" - dio: - dependency: transitive - description: - name: dio - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.1" 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: @@ -358,7 +283,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.3.0" + version: "4.4.0" lints: dependency: transitive 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,34 +333,27 @@ 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.3.0" objectbox: dependency: "direct main" description: name: objectbox url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - objectbox_flutter_libs: - dependency: "direct main" - description: - name: objectbox_flutter_libs - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" + version: "1.3.0" objectbox_generator: dependency: "direct dev" description: name: objectbox_generator url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" + objectbox_sync_flutter_libs: + dependency: "direct main" + description: + name: objectbox_sync_flutter_libs + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" package_config: dependency: transitive 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: @@ -512,21 +374,35 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.7" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.4" path_provider_platform_interface: dependency: transitive description: @@ -540,28 +416,14 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.dartlang.org" - source: hosted - version: "4.4.0" + version: "2.0.4" platform: dependency: transitive description: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: @@ -603,63 +465,7 @@ packages: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "1.1.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.8" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - 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.2" + version: "1.2.0" shelf: dependency: transitive description: @@ -685,7 +491,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.2.0" source_span: dependency: transitive description: @@ -693,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.0+4" - 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: @@ -735,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: @@ -755,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: @@ -770,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: @@ -804,7 +582,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.2.10" + version: "2.3.1" xdg_directories: dependency: transitive description: @@ -812,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 95d9cfd..b200920 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,17 +9,14 @@ 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_flutter_libs: any + objectbox_sync_flutter_libs: any dev_dependencies: flutter_test: @@ -31,7 +28,3 @@ dev_dependencies: flutter: uses-material-design: true - fonts: - - family: RobotoCondensed - fonts: - - asset: assets/fonts/RobotoCondensed-Regular.ttf 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 diff --git a/web/manifest.json b/web/manifest.json index 59cac49..5608aff 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "tide", - "short_name": "tide", + "name": "diameter", + "short_name": "diameter", "start_url": ".", "display": "standalone", "background_color": "#0175C2",