From 0dfcedff0b012116849ac68aff4c6badeafe974e Mon Sep 17 00:00:00 2001 From: spinel Date: Thu, 9 Dec 2021 06:14:55 +0100 Subject: [PATCH] ui improvements to most components --- TODO | 29 +- android/app/build.gradle | 2 +- lib/components/app_theme.dart | 40 +- lib/components/dropdown.dart | 93 ++- lib/components/forms.dart | 10 +- lib/config.dart | 1 + lib/main.dart | 76 +- lib/models/accuracy.dart | 1 + lib/models/basal.dart | 1 + lib/models/basal_profile.dart | 1 + lib/models/bolus.dart | 1 + lib/models/bolus_profile.dart | 1 + lib/models/glucose_target.dart | 1 + lib/models/log_bolus.dart | 1 + lib/models/log_entry.dart | 7 +- lib/models/log_event.dart | 1 + lib/models/log_event_type.dart | 1 + lib/models/log_meal.dart | 1 + lib/models/meal.dart | 1 + lib/models/meal_category.dart | 1 + lib/models/meal_portion_type.dart | 1 + lib/models/meal_source.dart | 1 + lib/models/settings.dart | 18 +- lib/objectbox-model.json | 30 +- lib/objectbox.g.dart | 67 +- lib/screens/accuracy_detail.dart | 134 ++-- lib/screens/accuracy_list.dart | 134 ++-- lib/screens/basal/basal_detail.dart | 133 ++-- lib/screens/basal/basal_list.dart | 98 ++- lib/screens/basal/basal_profile_detail.dart | 95 +-- lib/screens/basal/basal_profile_list.dart | 115 +-- lib/screens/bolus/bolus_detail.dart | 315 ++++---- lib/screens/bolus/bolus_list.dart | 158 ++-- lib/screens/bolus/bolus_profile_detail.dart | 98 +-- lib/screens/bolus/bolus_profile_list.dart | 87 ++- lib/screens/log/log.dart | 308 +++++--- .../log/log_entry/log_bolus_detail.dart | 708 ++++++++++-------- lib/screens/log/log_entry/log_bolus_list.dart | 84 ++- lib/screens/log/log_entry/log_entry.dart | 354 ++++----- .../log/log_entry/log_meal_detail.dart | 613 ++++++++++----- lib/screens/log/log_entry/log_meal_list.dart | 74 +- .../log/log_event/log_event_detail.dart | 507 +++++++------ lib/screens/log/log_event/log_event_list.dart | 169 +++-- .../log/log_event/log_event_type_detail.dart | 190 +++-- .../log/log_event/log_event_type_list.dart | 86 ++- lib/screens/meal/meal_category_detail.dart | 86 ++- lib/screens/meal/meal_category_list.dart | 77 +- lib/screens/meal/meal_detail.dart | 580 +++++++++----- lib/screens/meal/meal_list.dart | 141 ++-- .../meal/meal_portion_type_detail.dart | 89 ++- lib/screens/meal/meal_portion_type_list.dart | 81 +- lib/screens/meal/meal_source_detail.dart | 328 +++++--- lib/screens/meal/meal_source_list.dart | 78 +- lib/settings.dart | 74 +- pubspec.lock | 18 +- pubspec.yaml | 6 +- web/manifest.json | 4 +- 57 files changed, 3906 insertions(+), 2503 deletions(-) create mode 100644 lib/config.dart diff --git a/TODO b/TODO index d0ded7b..dd32359 100644 --- a/TODO +++ b/TODO @@ -1,35 +1,31 @@ BUGFIXES: General/Framework: - ☐ fix preloading of dropdown values (appear blank at first as of now) ☐ make sure 'null' isn't shown in text fields - Log Entry: - ☐ glucose target isn't displaed correctly anymore Basal/Bolus: ☐ "no element" error on creating basal/bolus rates when working from apk MAIN TASKS: Layout: ☐ make a styleguide (actively decide what components should look like) - ☐ make components rounder/nicer/closer to new material style + ☐ make components rounder/nicer/closer to new material style @started(21-12-08 02:17) General/Framework: ☐ show indicator and make all fields readonly if user somehow gets to a deleted record detail view - ☐ add functionality to delete dead records (meaning: set deleted flag and no relations) ☐ clean up controllers (dispose method of each stateful widget) - ☐ improve dropdown component - ☐ account for deleted/disabled elements in dropdowns - ☐ hide dropdown overlay on tapping anywhere else (especially menu) - ☐ add clear button to dropdown (or all text fields?) + ☐ account for deleted/disabled elements in dropdowns ☐ check through all detail forms and set required fields/according messages ☐ implement component for durations ☐ change placement of delete and floating button because its very easy to accidentally hit delete + ☐ hide details like accuracies etc when picking meals Basal/Bolus: ☐ add save and close and next buttons on rate creations + ☐ always calculate other glucose measurement from active one and make other one readonly Log Entry: ☐ add save and close button ☐ move on to newly created entry after saving - ☐ add option to specify trend for blood sugar ☐ recalculate bolus upon deactivating 'set manually' option - ☐ account for delayed percentage setting on meals + ☐ account for delayed percentage setting on choosing meals + ☐ give option to supply quantity + ☐ give option to pick meal from a different log entry (that doesn't have an associated bolus yet) Event Types: ☐ add colors as indicators for log entries (and later graphs in reports) Settings: @@ -39,18 +35,22 @@ MAIN TASKS: ☐ 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) FUTURE TASKS: General/Framework: + ☐ setup objectbox sync server ☐ add explanations to each section ☐ find a better way to work with multiple glucose measurements (or disable it?) ☐ evaluate if some fields should be readonly instead of completely hidden ☐ alternate languages ☐ log hba1c + ☐ add recipe calculator Reports: ☐ evaluate what type of reports there should be Log Overview: ☐ add pagination + ☐ add filters Log Entry: ☐ check if there is still an active bolus when suggesting glucose bolus Event Types: @@ -58,8 +58,15 @@ FUTURE TASKS: ☐ implement reminders as push notifications Settings: ☐ add option to hide extra customization options (ie. changing pre calculated values)? + ☐ option to switch theme Archive: + ✔ 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) diff --git a/android/app/build.gradle b/android/app/build.gradle index e8ef626..f609987 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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/lib/components/app_theme.dart b/lib/components/app_theme.dart index 5fb6e15..e46df56 100644 --- a/lib/components/app_theme.dart +++ b/lib/components/app_theme.dart @@ -6,18 +6,46 @@ class AppTheme { static ThemeData lightTheme = FlexColorScheme.light( surfaceStyle: FlexSurface.medium, - scheme: FlexScheme.mandyRed, - fontFamily: 'RobotoCondensed', + 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( + 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)); + backgroundColor: baseThemeData.primaryColor, + ), + ); } } diff --git a/lib/components/dropdown.dart b/lib/components/dropdown.dart index db775d8..cd858e6 100644 --- a/lib/components/dropdown.dart +++ b/lib/components/dropdown.dart @@ -7,6 +7,7 @@ class AutoCompleteDropdownButton extends StatefulWidget { final List items; final void Function(T? value) onChanged; final List Function(String? value)? applyQuery; + final TextEditingController controller; const AutoCompleteDropdownButton( {Key? key, @@ -14,7 +15,8 @@ class AutoCompleteDropdownButton extends StatefulWidget { required this.label, required this.items, required this.onChanged, - this.applyQuery}) + this.applyQuery, + required this.controller}) : super(key: key); @override @@ -24,10 +26,10 @@ class AutoCompleteDropdownButton extends StatefulWidget { class _AutoCompleteDropdownButtonState extends State> { - TextEditingController controller = TextEditingController(text: ''); late List options; late List suggestions; + final FocusNode focusNode = FocusNode(); final LayerLink layerLink = LayerLink(); OverlayEntry? entry; bool isOpen = false; @@ -35,25 +37,32 @@ class _AutoCompleteDropdownButtonState @override void initState() { super.initState(); + setState(() { - controller.text = widget.selectedItem == null - ? '' - : widget.selectedItem!.toString(); 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; @@ -79,16 +88,14 @@ class _AutoCompleteDropdownButtonState items.addAll(options .where((item) => !(widget.selectedItem != null && - item.toString() == - widget.selectedItem!.toString()) && + 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 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 && @@ -107,11 +114,16 @@ class _AutoCompleteDropdownButtonState displayAbove ? Alignment.bottomLeft : Alignment.topLeft, offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)), showWhenUnlinked: false, - child: Material( - elevation: 8, - child: SingleChildScrollView( - child: Column( - children: items, + child: Scrollbar( + controller: _scrollController, + isAlwaysShown: true, + child: Material( + elevation: 8, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: items, + ), ), ), ), @@ -139,13 +151,14 @@ class _AutoCompleteDropdownButtonState void hideOverlay() { entry?.remove(); - entry = null; - isOpen = false; + setState(() { + entry = null; + isOpen = false; + }); } - void handleChanged(item) { + void handleChanged(T? item) { widget.onChanged(item); - controller.text = item.toString(); hideOverlay(); } @@ -178,17 +191,37 @@ class _AutoCompleteDropdownButtonState @override Widget build(BuildContext context) { - return CompositedTransformTarget( - link: layerLink, - child: TextFormField( - onChanged: onChangeQuery, - onTap: toggleOverlay, - controller: controller, - decoration: InputDecoration( - labelText: widget.label, - suffixIcon: IconButton( - onPressed: toggleOverlay, - icon: const Icon(Icons.arrow_drop_down), + 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.dart b/lib/components/forms.dart index f8b2858..de9a804 100644 --- a/lib/components/forms.dart +++ b/lib/components/forms.dart @@ -23,11 +23,11 @@ class _FormWrapperState extends State { children: [ Column( children: widget.fields - ?.map((e) => Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: e)) - .toList() ?? - [], + ?.map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: e)) + .toList() ?? + [], ), Container( padding: const EdgeInsets.only(top: 10.0), diff --git a/lib/config.dart b/lib/config.dart new file mode 100644 index 0000000..094eda9 --- /dev/null +++ b/lib/config.dart @@ -0,0 +1 @@ +String secret = 'm4Gwehzgv18jZ5gCVUBZl5li3Z0FX2Yb'; diff --git a/lib/main.dart b/lib/main.dart index bbddeba..4eacab3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ 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'; @@ -23,43 +25,57 @@ import 'package:diameter/screens/accuracy_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(); objectBox = await ObjectBox.create(); + + Sync.isAvailable(); + SyncClient syncClient = Sync.client( + objectBox.store, + 'wss://127.0.0.1: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 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.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.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 39093f0..680e377 100644 --- a/lib/models/accuracy.dart +++ b/lib/models/accuracy.dart @@ -3,6 +3,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show Accuracy_; @Entity(uid: 291512798403320400) +@Sync() class Accuracy { static final Box box = objectBox.store.box(); diff --git a/lib/models/basal.dart b/lib/models/basal.dart index fd583fb..9ccd952 100644 --- a/lib/models/basal.dart +++ b/lib/models/basal.dart @@ -5,6 +5,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show Basal_, BasalProfile_; @Entity(uid: 1467758525778521891) +@Sync() class Basal { static final Box box = objectBox.store.box(); diff --git a/lib/models/basal_profile.dart b/lib/models/basal_profile.dart index 8886025..54a3df3 100644 --- a/lib/models/basal_profile.dart +++ b/lib/models/basal_profile.dart @@ -4,6 +4,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show BasalProfile_; @Entity(uid: 3613736032926903785) +@Sync() class BasalProfile { static final Box box = objectBox.store.box(); diff --git a/lib/models/bolus.dart b/lib/models/bolus.dart index 23f794e..ed043a0 100644 --- a/lib/models/bolus.dart +++ b/lib/models/bolus.dart @@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show Bolus_, BolusProfile_; @Entity(uid: 3417770529060202389) +@Sync() class Bolus { static final Box box = objectBox.store.box(); diff --git a/lib/models/bolus_profile.dart b/lib/models/bolus_profile.dart index 045cd9c..3e0408f 100644 --- a/lib/models/bolus_profile.dart +++ b/lib/models/bolus_profile.dart @@ -4,6 +4,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show BolusProfile_; @Entity(uid: 8812452529027052317) +@Sync() class BolusProfile { static final Box box = objectBox.store.box(); diff --git a/lib/models/glucose_target.dart b/lib/models/glucose_target.dart index 41a4831..890df51 100644 --- a/lib/models/glucose_target.dart +++ b/lib/models/glucose_target.dart @@ -5,6 +5,7 @@ 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(); diff --git a/lib/models/log_bolus.dart b/lib/models/log_bolus.dart index 1f6fd42..aa2e615 100644 --- a/lib/models/log_bolus.dart +++ b/lib/models/log_bolus.dart @@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show LogBolus_, LogEntry_; @Entity(uid: 8033487006694871160) +@Sync() class LogBolus { static final Box box = objectBox.store.box(); diff --git a/lib/models/log_entry.dart b/lib/models/log_entry.dart index 8127c9d..f8f2e4e 100644 --- a/lib/models/log_entry.dart +++ b/lib/models/log_entry.dart @@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show LogEntry_; @Entity(uid: 752131069307970560) +@Sync() class LogEntry { static final Box box = objectBox.store.box(); @@ -16,6 +17,7 @@ class LogEntry { DateTime time; int? mgPerDl; double? mmolPerL; + double? glucoseTrend; String? notes; // constructor @@ -25,6 +27,7 @@ class LogEntry { required this.time, this.mgPerDl, this.mmolPerL, + this.glucoseTrend, this.notes, }); @@ -43,8 +46,8 @@ class LogEntry { static bool hasUncorrectedGlucose(int id) { final entry = box.get(id); - if (((entry?.mgPerDl ?? 0) > Settings.targetMgPerDl() || - (entry?.mmolPerL ?? 0) > Settings.targetMmolPerL())) { + if (((entry?.mgPerDl ?? 0) > Settings.targetMgPerDl || + (entry?.mmolPerL ?? 0) > Settings.targetMmolPerL)) { return !LogBolus.glucoseBolusForEntryExists(id); } return false; diff --git a/lib/models/log_event.dart b/lib/models/log_event.dart index 4b87371..2caac05 100644 --- a/lib/models/log_event.dart +++ b/lib/models/log_event.dart @@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show LogEvent_, LogEventType_; @Entity(uid: 4303325892753185970) +@Sync() class LogEvent { static final Box box = objectBox.store.box(); diff --git a/lib/models/log_event_type.dart b/lib/models/log_event_type.dart index dbe0650..e1683fb 100644 --- a/lib/models/log_event_type.dart +++ b/lib/models/log_event_type.dart @@ -5,6 +5,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show LogEventType_; @Entity(uid: 8362795406595606110) +@Sync() class LogEventType { static final Box box = objectBox.store.box(); diff --git a/lib/models/log_meal.dart b/lib/models/log_meal.dart index 08cc523..3789134 100644 --- a/lib/models/log_meal.dart +++ b/lib/models/log_meal.dart @@ -9,6 +9,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show LogMeal_, LogEntry_; @Entity(uid: 411177866700467286) +@Sync() class LogMeal { static final Box box = objectBox.store.box(); diff --git a/lib/models/meal.dart b/lib/models/meal.dart index 27c949b..c168dc0 100644 --- a/lib/models/meal.dart +++ b/lib/models/meal.dart @@ -9,6 +9,7 @@ import 'package:objectbox/objectbox.dart'; enum PortionCarbsParameter { carbsRatio, portionSize, carbsPerPortion } @Entity(uid: 382130101578692012) +@Sync() class Meal { static final Box box = objectBox.store.box(); diff --git a/lib/models/meal_category.dart b/lib/models/meal_category.dart index 67f56ca..80a44a4 100644 --- a/lib/models/meal_category.dart +++ b/lib/models/meal_category.dart @@ -3,6 +3,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show MealCategory_; @Entity(uid: 3158200688796904913) +@Sync() class MealCategory { static final Box box = objectBox.store.box(); diff --git a/lib/models/meal_portion_type.dart b/lib/models/meal_portion_type.dart index 6e6a696..f337b5b 100644 --- a/lib/models/meal_portion_type.dart +++ b/lib/models/meal_portion_type.dart @@ -3,6 +3,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show MealPortionType_; @Entity(uid: 2111511899235985637) +@Sync() class MealPortionType { static final Box box = objectBox.store.box(); diff --git a/lib/models/meal_source.dart b/lib/models/meal_source.dart index 99e1706..df007b0 100644 --- a/lib/models/meal_source.dart +++ b/lib/models/meal_source.dart @@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:diameter/objectbox.g.dart' show MealSource_; @Entity(uid: 1283034494527412242) +@Sync() class MealSource { static final Box box = objectBox.store.box(); diff --git a/lib/models/settings.dart b/lib/models/settings.dart index cb0838a..7d56c16 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,4 +1,5 @@ import 'package:diameter/main.dart'; +import 'package:flutter/material.dart'; import 'package:objectbox/objectbox.dart'; enum GlucoseDisplayMode { activeOnly, bothForList, bothForDetail, both } @@ -39,14 +40,18 @@ List nutritionMeasurementLabels = [ ]; @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; String dateFormat; String? longDateFormat; @@ -57,8 +62,7 @@ class Settings { bool showConfirmationDialogOnDelete; bool showConfirmationDialogOnStopEvent; - int targetGlucoseMgPerDl; - double targetGlucoseMmolPerL; + bool useDarkTheme; // constructor Settings({ @@ -75,6 +79,7 @@ class Settings { this.showConfirmationDialogOnStopEvent = true, this.targetGlucoseMgPerDl = 100, this.targetGlucoseMmolPerL = 5.49, + this.useDarkTheme = false, }); // methods @@ -97,13 +102,16 @@ class Settings { static String get glucoseMeasurementSuffix => glucoseMeasurementSuffixes[get().glucoseMeasurementIndex]; - static int targetMgPerDl() => get().targetGlucoseMgPerDl; - static double targetMmolPerL() => get().targetGlucoseMmolPerL; + static int get targetMgPerDl => get().targetGlucoseMgPerDl; + static double get targetMmolPerL => get().targetGlucoseMmolPerL; + + 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()); + box.put(Settings(useDarkTheme: ThemeMode.system == ThemeMode.dark)); } } diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json index ba4f633..13e2d15 100644 --- a/lib/objectbox-model.json +++ b/lib/objectbox-model.json @@ -7,6 +7,7 @@ "id": "2:1467758525778521891", "lastPropertyId": "6:3409466778841164684", "name": "Basal", + "flags": 2, "properties": [ { "id": "1:4281816825522738642", @@ -49,6 +50,7 @@ "id": "3:3613736032926903785", "lastPropertyId": "5:8140071977687660397", "name": "BasalProfile", + "flags": 2, "properties": [ { "id": "1:353771983641472117", @@ -83,6 +85,7 @@ "id": "4:3417770529060202389", "lastPropertyId": "9:7440090146687096977", "name": "Bolus", + "flags": 2, "properties": [ { "id": "1:8141647919190345775", @@ -140,6 +143,7 @@ "id": "5:8812452529027052317", "lastPropertyId": "5:8082994824481464395", "name": "BolusProfile", + "flags": 2, "properties": [ { "id": "1:4233863196673391978", @@ -172,8 +176,9 @@ }, { "id": "6:752131069307970560", - "lastPropertyId": "9:1692732373071965573", + "lastPropertyId": "10:2505303363495348118", "name": "LogEntry", + "flags": 2, "properties": [ { "id": "1:5528657304180237933", @@ -205,6 +210,11 @@ "id": "9:1692732373071965573", "name": "deleted", "type": 1 + }, + { + "id": "10:2505303363495348118", + "name": "glucoseTrend", + "type": 8 } ], "relations": [] @@ -213,6 +223,7 @@ "id": "7:4303325892753185970", "lastPropertyId": "12:3041952167628926163", "name": "LogEvent", + "flags": 2, "properties": [ { "id": "1:6648501734758557663", @@ -281,6 +292,7 @@ "id": "8:8362795406595606110", "lastPropertyId": "8:1869014400856897151", "name": "LogEventType", + "flags": 2, "properties": [ { "id": "1:1430413826199774000", @@ -336,6 +348,7 @@ "id": "9:411177866700467286", "lastPropertyId": "17:7341439841011629937", "name": "LogMeal", + "flags": 2, "properties": [ { "id": "1:962999525294133158", @@ -441,6 +454,7 @@ "id": "10:382130101578692012", "lastPropertyId": "15:8283810711091063880", "name": "Meal", + "flags": 2, "properties": [ { "id": "1:612386612600420389", @@ -535,6 +549,7 @@ "id": "11:3158200688796904913", "lastPropertyId": "4:824435977543069541", "name": "MealCategory", + "flags": 2, "properties": [ { "id": "1:3678943122076184840", @@ -564,6 +579,7 @@ "id": "12:2111511899235985637", "lastPropertyId": "4:5680236937391945907", "name": "MealPortionType", + "flags": 2, "properties": [ { "id": "1:65428405312238271", @@ -593,6 +609,7 @@ "id": "13:1283034494527412242", "lastPropertyId": "8:4547899751779962180", "name": "MealSource", + "flags": 2, "properties": [ { "id": "1:7205380295259922130", @@ -654,6 +671,7 @@ "id": "14:8033487006694871160", "lastPropertyId": "18:7503231998671134983", "name": "LogBolus", + "flags": 2, "properties": [ { "id": "1:8254237730262024662", @@ -752,6 +770,7 @@ "id": "15:291512798403320400", "lastPropertyId": "7:6675647182186603076", "name": "Accuracy", + "flags": 2, "properties": [ { "id": "1:8405388350474524599", @@ -794,8 +813,9 @@ }, { "id": "16:3989341091218179227", - "lastPropertyId": "22:3595473653451456068", + "lastPropertyId": "23:3611447442844013652", "name": "Settings", + "flags": 2, "properties": [ { "id": "1:7803753645747063723", @@ -862,6 +882,11 @@ "id": "22:3595473653451456068", "name": "targetGlucoseMmolPerL", "type": 8 + }, + { + "id": "23:3611447442844013652", + "name": "useDarkTheme", + "type": 1 } ], "relations": [] @@ -870,6 +895,7 @@ "id": "17:5041265995704044399", "lastPropertyId": "7:1333487551279074696", "name": "GlucoseTarget", + "flags": 2, "properties": [ { "id": "1:4322960567133959537", diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index 42a1068..85ed8a2 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -7,7 +7,7 @@ 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'; @@ -33,7 +33,7 @@ final _entities = [ id: const IdUid(2, 1467758525778521891), name: 'Basal', lastPropertyId: const IdUid(6, 3409466778841164684), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 4281816825522738642), @@ -74,7 +74,7 @@ final _entities = [ id: const IdUid(3, 3613736032926903785), name: 'BasalProfile', lastPropertyId: const IdUid(5, 8140071977687660397), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 353771983641472117), @@ -108,7 +108,7 @@ final _entities = [ id: const IdUid(4, 3417770529060202389), name: 'Bolus', lastPropertyId: const IdUid(9, 7440090146687096977), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 8141647919190345775), @@ -164,7 +164,7 @@ final _entities = [ id: const IdUid(5, 8812452529027052317), name: 'BolusProfile', lastPropertyId: const IdUid(5, 8082994824481464395), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 4233863196673391978), @@ -197,8 +197,8 @@ final _entities = [ ModelEntity( id: const IdUid(6, 752131069307970560), name: 'LogEntry', - lastPropertyId: const IdUid(9, 1692732373071965573), - flags: 0, + lastPropertyId: const IdUid(10, 2505303363495348118), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 5528657304180237933), @@ -229,6 +229,11 @@ final _entities = [ id: const IdUid(9, 1692732373071965573), name: 'deleted', type: 1, + flags: 0), + ModelProperty( + id: const IdUid(10, 2505303363495348118), + name: 'glucoseTrend', + type: 8, flags: 0) ], relations: [], @@ -237,7 +242,7 @@ final _entities = [ id: const IdUid(7, 4303325892753185970), name: 'LogEvent', lastPropertyId: const IdUid(12, 3041952167628926163), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 6648501734758557663), @@ -302,7 +307,7 @@ final _entities = [ id: const IdUid(8, 8362795406595606110), name: 'LogEventType', lastPropertyId: const IdUid(8, 1869014400856897151), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 1430413826199774000), @@ -355,7 +360,7 @@ final _entities = [ id: const IdUid(9, 411177866700467286), name: 'LogMeal', lastPropertyId: const IdUid(17, 7341439841011629937), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 962999525294133158), @@ -453,7 +458,7 @@ final _entities = [ id: const IdUid(10, 382130101578692012), name: 'Meal', lastPropertyId: const IdUid(15, 8283810711091063880), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 612386612600420389), @@ -542,7 +547,7 @@ final _entities = [ id: const IdUid(11, 3158200688796904913), name: 'MealCategory', lastPropertyId: const IdUid(4, 824435977543069541), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 3678943122076184840), @@ -571,7 +576,7 @@ final _entities = [ id: const IdUid(12, 2111511899235985637), name: 'MealPortionType', lastPropertyId: const IdUid(4, 5680236937391945907), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 65428405312238271), @@ -600,7 +605,7 @@ final _entities = [ id: const IdUid(13, 1283034494527412242), name: 'MealSource', lastPropertyId: const IdUid(8, 4547899751779962180), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 7205380295259922130), @@ -657,7 +662,7 @@ final _entities = [ id: const IdUid(14, 8033487006694871160), name: 'LogBolus', lastPropertyId: const IdUid(18, 7503231998671134983), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 8254237730262024662), @@ -752,7 +757,7 @@ final _entities = [ id: const IdUid(15, 291512798403320400), name: 'Accuracy', lastPropertyId: const IdUid(7, 6675647182186603076), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 8405388350474524599), @@ -795,8 +800,8 @@ final _entities = [ ModelEntity( id: const IdUid(16, 3989341091218179227), name: 'Settings', - lastPropertyId: const IdUid(22, 3595473653451456068), - flags: 0, + lastPropertyId: const IdUid(23, 3611447442844013652), + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 7803753645747063723), @@ -862,6 +867,11 @@ final _entities = [ id: const IdUid(22, 3595473653451456068), name: 'targetGlucoseMmolPerL', type: 8, + flags: 0), + ModelProperty( + id: const IdUid(23, 3611447442844013652), + name: 'useDarkTheme', + type: 1, flags: 0) ], relations: [], @@ -870,7 +880,7 @@ final _entities = [ id: const IdUid(17, 5041265995704044399), name: 'GlucoseTarget', lastPropertyId: const IdUid(7, 1333487551279074696), - flags: 0, + flags: 2, properties: [ ModelProperty( id: const IdUid(1, 4322960567133959537), @@ -1143,13 +1153,14 @@ ModelDefinition getObjectBoxModel() { objectToFB: (LogEntry object, fb.Builder fbb) { final notesOffset = object.notes == null ? null : fbb.writeString(object.notes!); - fbb.startTable(10); + 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.addOffset(7, notesOffset); fbb.addBool(8, object.deleted); + fbb.addFloat64(9, object.glucoseTrend); fbb.finish(fbb.endTable()); return object.id; }, @@ -1167,6 +1178,8 @@ ModelDefinition getObjectBoxModel() { .vTableGetNullable(buffer, rootOffset, 8), mmolPerL: const fb.Float64Reader() .vTableGetNullable(buffer, rootOffset, 10), + glucoseTrend: const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 22), notes: const fb.StringReader() .vTableGetNullable(buffer, rootOffset, 18)); @@ -1694,7 +1707,7 @@ ModelDefinition getObjectBoxModel() { final longTimeFormatOffset = object.longTimeFormat == null ? null : fbb.writeString(object.longTimeFormat!); - fbb.startTable(23); + fbb.startTable(24); fbb.addInt64(0, object.id); fbb.addOffset(1, dateFormatOffset); fbb.addOffset(2, longDateFormatOffset); @@ -1708,6 +1721,7 @@ ModelDefinition getObjectBoxModel() { 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; }, @@ -1740,7 +1754,8 @@ ModelDefinition getObjectBoxModel() { targetGlucoseMgPerDl: const fb.Int64Reader().vTableGet(buffer, rootOffset, 44, 0), targetGlucoseMmolPerL: - const fb.Float64Reader().vTableGet(buffer, rootOffset, 46, 0)); + const fb.Float64Reader().vTableGet(buffer, rootOffset, 46, 0), + useDarkTheme: const fb.BoolReader().vTableGet(buffer, rootOffset, 48, false)); return object; }), @@ -1921,6 +1936,10 @@ class LogEntry_ { /// 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. @@ -2337,6 +2356,10 @@ class Settings_ { /// 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. diff --git a/lib/screens/accuracy_detail.dart b/lib/screens/accuracy_detail.dart index 1b25802..507c1d4 100644 --- a/lib/screens/accuracy_detail.dart +++ b/lib/screens/accuracy_detail.dart @@ -22,12 +22,13 @@ 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; + bool _forCarbsRatio = true; + bool _forPortionSize = true; @override void initState() { @@ -43,13 +44,25 @@ class _AccuracyDetailScreenState extends State { } } - void reload() { + 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 { @@ -67,7 +80,7 @@ class _AccuracyDetailScreenState extends State { Accuracy.box.put(accuracy); Accuracy.reorder( accuracy, int.tryParse(_confidenceRatingController.text)); - Navigator.pop(context, '${_isNew ? 'New' : ''} Accuracy saved'); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Accuracy saved', accuracy]); } setState(() { _isSaving = false; @@ -77,8 +90,8 @@ class _AccuracyDetailScreenState extends State { void handleCancelAction() { if (Settings.get().showConfirmationDialogOnCancel && (_isNew && - (_forCarbsRatio || - _forPortionSize || + (!_forCarbsRatio || + !_forPortionSize || _valueController.text != '' || int.tryParse(_confidenceRatingController.text) != null || _notesController.text != '')) || @@ -106,61 +119,66 @@ 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: [ - FormWrapper( - 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; - }, - ), - BooleanFormField( - value: _forCarbsRatio, - label: 'for carbs ratio', - onChanged: (value) { - setState(() { - _forCarbsRatio = value; - }); - }, - ), - BooleanFormField( - 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; + }); + }, ), - ), - ], - ), - ], + TextFormField( + controller: _confidenceRatingController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Confidence Rating', + ), + ), + TextFormField( + controller: _notesController, + keyboardType: TextInputType.multiline, + decoration: const InputDecoration( + labelText: 'Notes', + ), + minLines: 2, + maxLines: 5, + ), + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( diff --git a/lib/screens/accuracy_list.dart b/lib/screens/accuracy_list.dart index 9815916..5a841d8 100644 --- a/lib/screens/accuracy_list.dart +++ b/lib/screens/accuracy_list.dart @@ -16,6 +16,8 @@ class AccuracyListScreen extends StatefulWidget { class _AccuracyListScreenState extends State { List _accuracies = Accuracy.getAll(); + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -84,70 +86,78 @@ class _AccuracyListScreenState extends State { children: [ Expanded( child: _accuracies.isNotEmpty - ? ReorderableListView.builder( - padding: const EdgeInsets.only(top: 10.0), - itemCount: _accuracies.length, - onReorder: (oldIndex, newIndex) { - Accuracy.reorder(_accuracies[oldIndex], newIndex); - reload(); - }, - itemBuilder: (context, index) { - final accuracy = _accuracies[index]; - return ListTile( - key: Key(index.toString()), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AccuracyDetailScreen(id: accuracy.id), + ? 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, ), - ).then((message) => reload(message: message)); - }, - title: Text(accuracy.value), - 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), + leading: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.reorder), + ], ), - IconButton( - icon: Icon( - Icons.pie_chart, - color: accuracy.forCarbsRatio - ? Theme.of(context) - .toggleableActiveColor - : Theme.of(context).highlightColor, - ), - onPressed: () => - handleToggleForCarbsRatioAction( - accuracy), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + Icons.square_foot, + color: accuracy.forPortionSize + ? Theme.of(context) + .toggleableActiveColor + : Theme.of(context).highlightColor, + ), + onPressed: () => + handleToggleForPortionSizeAction( + accuracy), + ), + IconButton( + icon: Icon( + Icons.pie_chart, + color: accuracy.forCarbsRatio + ? Theme.of(context) + .toggleableActiveColor + : Theme.of(context).highlightColor, + ), + onPressed: () => + handleToggleForCarbsRatioAction( + accuracy), + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => + handleDeleteAction(accuracy), + ) + ], ), - const SizedBox(width: 24), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => - handleDeleteAction(accuracy), - ) - ], - ), - ); - }) + ), + ); + }), + ) : const Center( child: Text('You have not created any Accuracies yet!'), ), @@ -161,7 +171,7 @@ class _AccuracyListScreenState extends State { MaterialPageRoute( builder: (context) => const AccuracyDetailScreen(), ), - ).then((message) => reload(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 8ab4101..2d258f7 100644 --- a/lib/screens/basal/basal_detail.dart +++ b/lib/screens/basal/basal_detail.dart @@ -34,6 +34,7 @@ class _BasalDetailScreenState extends State { bool _isSaving = false; final GlobalKey _basalForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); @@ -64,13 +65,25 @@ class _BasalDetailScreenState extends State { updateEndTime(); } - void reload() { + 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() { @@ -141,7 +154,7 @@ class _BasalDetailScreenState extends State { ); basal.basalProfile.targetId = widget.basalProfileId; Basal.put(basal); - Navigator.pop(context, '${_isNew ? 'New' : ''} Basal Rate saved'); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Basal Rate saved', basal]); } }); } @@ -183,69 +196,75 @@ class _BasalDetailScreenState extends State { '${_isNew ? 'New' : 'Edit'} Basal Rate for ${BasalProfile.get(widget.basalProfileId)?.name}'), ), drawer: const Navigation(currentLocation: BasalDetailScreen.routeName), - body: 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: (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: (newStartTime) { + if (newStartTime != null) { + setState(() { + _startTime = newStartTime; + }); + updateStartTime(); + } + }, + ), + ), ), - ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5), + child: TimeOfDayFormField( + 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: (newEndTime) { - if (newEndTime != null) { - setState(() { - _endTime = newEndTime; - }); - updateEndTime(); - } - }, - ), + 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; + }, ), ], ), - 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, diff --git a/lib/screens/basal/basal_list.dart b/lib/screens/basal/basal_list.dart index f59394f..fda595a 100644 --- a/lib/screens/basal/basal_list.dart +++ b/lib/screens/basal/basal_list.dart @@ -23,6 +23,8 @@ class BasalListScreen extends StatefulWidget { } class _BasalListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + void reload({String? message}) { widget.reload(); @@ -48,7 +50,7 @@ class _BasalListScreenState extends State { id: basal.id, ), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onDelete(Basal basal) { @@ -108,44 +110,64 @@ class _BasalListScreenState extends State { @override Widget build(BuildContext context) { - return 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!'), ); diff --git a/lib/screens/basal/basal_profile_detail.dart b/lib/screens/basal/basal_profile_detail.dart index 797bc98..48d6d28 100644 --- a/lib/screens/basal/basal_profile_detail.dart +++ b/lib/screens/basal/basal_profile_detail.dart @@ -29,6 +29,7 @@ class _BasalProfileDetailScreenState extends State { bool _isNew = true; final GlobalKey _basalProfileForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); late FloatingActionButton addBasalButton; late IconButton refreshButton; @@ -214,7 +215,7 @@ class _BasalProfileDetailScreenState extends State { ); }, ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void handleSaveAction() async { @@ -223,13 +224,14 @@ class _BasalProfileDetailScreenState extends State { }); 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); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Basal Profile saved', basalProfile]); } setState(() { bottomNav = detailBottomRow; @@ -284,52 +286,59 @@ class _BasalProfileDetailScreenState extends State { renderTabButtons(tabController.index); }); List tabs = [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - 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, ), - ), - BooleanFormField( - 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 index 8c9de13..9729ab7 100644 --- a/lib/screens/basal/basal_profile_list.dart +++ b/lib/screens/basal/basal_profile_list.dart @@ -16,15 +16,15 @@ class BasalProfileListScreen extends StatefulWidget { } class _BasalProfileListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + late List _basalProfiles; Widget banner = Container(); - bool pickActiveProfileMode = false; final BasalProfile? _activeProfile = BasalProfile.getActive(DateTime.now()); void refresh({String? message}) { setState(() { - pickActiveProfileMode = false; _basalProfiles = BasalProfile.getAll(); }); updateBanner(); @@ -106,6 +106,7 @@ class _BasalProfileListScreenState extends State { setState(() { banner = MaterialBanner( content: AutoCompleteDropdownButton( + controller: TextEditingController(text: ''), items: _basalProfiles, label: 'Default Basal Profile', onChanged: onPickActive, @@ -119,7 +120,6 @@ class _BasalProfileListScreenState extends State { ), ], ); - pickActiveProfileMode = true; }); } @@ -130,7 +130,7 @@ class _BasalProfileListScreenState extends State { builder: (context) => BasalProfileDetailScreen(id: basalProfile?.id ?? 0, active: active), ), - ).then((message) => refresh(message: message)); + ).then((result) => refresh(message: result?[0])); } void onNew(bool active) { @@ -164,57 +164,68 @@ class _BasalProfileListScreenState extends State { banner, Expanded( child: _basalProfiles.isNotEmpty - ? ListView.builder( - 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' - : ''; - if (activeProfileText != '' && - (basalProfile.notes ?? '') != '') { - activeProfileText += '\n'; - } - return ListTile( - selected: basalProfile.active || - basalProfile.id == _activeProfile?.id, - onTap: () => onEdit(basalProfile), - title: Text(basalProfile.name), - subtitle: Row( - children: [ - Text( - '$activeProfileText${basalProfile.notes ?? ''}'), - Expanded( - child: Column( - children: dailyTotal > 0 - ? [ - Text(dailyTotal.toStringAsPrecision(3)), - const Text('U/day', textScaleFactor: 0.75), - ] - : [], + ? 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.delete, - color: Colors.blue, - ), - onPressed: () => handleDeleteAction(basalProfile), + 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!'), ), diff --git a/lib/screens/bolus/bolus_detail.dart b/lib/screens/bolus/bolus_detail.dart index ceaeca9..56a1a54 100644 --- a/lib/screens/bolus/bolus_detail.dart +++ b/lib/screens/bolus/bolus_detail.dart @@ -35,6 +35,7 @@ class _BolusDetailScreenState extends State { bool _isSaving = false; final GlobalKey _bolusForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); @@ -72,13 +73,25 @@ class _BolusDetailScreenState extends State { updateEndTime(); } - void reload() { + 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() { @@ -154,7 +167,7 @@ class _BolusDetailScreenState extends State { ); bolus.bolusProfile.targetId = widget.bolusProfileId; Bolus.put(bolus); - Navigator.pop(context, '${_isNew ? 'New' : ''} Bolus Rate saved'); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Bolus Rate saved', bolus]); } }); } @@ -232,162 +245,166 @@ 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: [ - 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: (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: (newStartTime) { + if (newStartTime != null) { + setState(() { + _startTime = newStartTime; + }); + updateStartTime(); + } + }, + ), ), ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 5), - child: TimeOfDayFormField( - 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: (newEndTime) { + if (newEndTime != null) { + setState(() { + _endTime = newEndTime; + }); + updateEndTime(); + } + }, + ), ), ), + ], + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Units', + suffixText: 'U', ), - ], - ), - 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; + }, ), - 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: Settings.nutritionMeasurementSuffix, + TextFormField( + decoration: InputDecoration( + labelText: 'per carbs', + suffixText: Settings.nutritionMeasurementSuffix, + ), + 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; + }, ), - 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: [ - Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl || - Settings.glucoseDisplayMode == GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'per mg/dl', - suffixText: 'mg/dl', + Row( + children: [ + Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl || + Settings.glucoseDisplayMode == GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'per mg/dl', + suffixText: 'mg/dl', + ), + controller: _mgPerDlController, + onChanged: (_) async { + await Future.delayed(const Duration(seconds: 1)); + convertBetweenMgPerDlAndMmolPerL( + calculateFrom: + GlucoseMeasurement.mgPerDl); + }, + keyboardType: + const TextInputType.numberWithOptions(), + validator: (value) { + if (value!.trim().isEmpty && + _mmolPerLController.text.trim().isEmpty) { + return 'How many mg/dl does the rate make up for?'; + } + return null; + }, ), - controller: _mgPerDlController, - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - convertBetweenMgPerDlAndMmolPerL( - calculateFrom: - GlucoseMeasurement.mgPerDl); - }, - keyboardType: - const TextInputType.numberWithOptions(), - validator: (value) { - if (value!.trim().isEmpty && - _mmolPerLController.text.trim().isEmpty) { - return 'How many mg/dl does the rate make up for?'; - } - return null; - }, - ), - ) - : Container(), - Settings.glucoseDisplayMode == GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? IconButton( - onPressed: () => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mmolPerL), - icon: const Icon(Icons.calculate), - ) - : Container(), - Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL || + ) + : Container(), + Settings.glucoseDisplayMode == GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? IconButton( + onPressed: () => convertBetweenMgPerDlAndMmolPerL( + calculateFrom: GlucoseMeasurement.mmolPerL), + icon: const Icon(Icons.calculate), + ) + : Container(), + Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL || + [GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode) + ? Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'per mmol/l', + suffixText: 'mmol/l', + ), + controller: _mmolPerLController, + onChanged: (_) async { + await Future.delayed(const Duration(seconds: 1)); + convertBetweenMgPerDlAndMmolPerL( + calculateFrom: + GlucoseMeasurement.mmolPerL); + }, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true), + validator: (value) { + if (value!.trim().isEmpty && + _mgPerDlController.text.trim().isEmpty) { + return 'How many mmol/l does rhe rate make up for?'; + } + return null; + }, + ), + ) + : Container(), [GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode) - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'per mmol/l', - suffixText: 'mmol/l', - ), - controller: _mmolPerLController, - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - convertBetweenMgPerDlAndMmolPerL( - calculateFrom: - GlucoseMeasurement.mmolPerL); - }, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true), - validator: (value) { - if (value!.trim().isEmpty && - _mgPerDlController.text.trim().isEmpty) { - return 'How many mmol/l does rhe rate make up for?'; - } - return null; - }, - ), - ) - : Container(), - [GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode) - ? IconButton( - onPressed: () => convertBetweenMgPerDlAndMmolPerL( - calculateFrom: GlucoseMeasurement.mgPerDl), - icon: const Icon(Icons.calculate), - ) - : Container(), - ], - ), - ], - ), - ], + ? IconButton( + onPressed: () => convertBetweenMgPerDlAndMmolPerL( + calculateFrom: GlucoseMeasurement.mgPerDl), + icon: const Icon(Icons.calculate), + ) + : Container(), + ], + ), + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( diff --git a/lib/screens/bolus/bolus_list.dart b/lib/screens/bolus/bolus_list.dart index 583f7c2..f73dacb 100644 --- a/lib/screens/bolus/bolus_list.dart +++ b/lib/screens/bolus/bolus_list.dart @@ -20,6 +20,8 @@ class BolusListScreen extends StatefulWidget { } class _BolusListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + void reload({String? message}) { widget.reload(); @@ -45,7 +47,7 @@ class _BolusListScreenState extends State { id: bolus.id, ), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onDelete(Bolus bolus) { @@ -104,80 +106,94 @@ class _BolusListScreenState extends State { @override Widget build(BuildContext context) { - return 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( - children: [ - error != null - ? Text(error, - style: const TextStyle(color: Colors.red)) - : const Text(''), - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - children: (bolus.units > 0 && bolus.carbs > 0) - ? [ - Text((bolus.carbs / bolus.units).toStringAsPrecision(2)), - const Text('carbs per U', - textScaleFactor: 0.75), - ] - : [], - ), + return widget.bolusRates.isNotEmpty ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + padding: const EdgeInsets.all(10.0), + controller: _scrollController, + shrinkWrap: true, + itemCount: widget.bolusRates.length, + itemBuilder: (context, index) { + final bolus = widget.bolusRates[index]; + final error = validateTimePeriod(index); + return Card( + child: Column( + children: [ + error != null + ? Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.warning, color: Theme.of(context).errorColor), + Text( + error, style: TextStyle(color: Theme.of(context).errorColor) + ), + ], ), - Expanded( - child: Column( - children: (bolus.units > 0 && bolus.carbs > 0) - ? [ - Text((bolus.units / bolus.carbs * 12).toStringAsPrecision(2)), - const Text('U per bread unit', - textScaleFactor: 0.75), - ] - : [], + ) : 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.mgPerDl ?? bolus.mmolPerL ?? 0) > 0) - ? [ - Text((((Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDl : bolus.mmolPerL)! / bolus.units)).toString()), - Text('${Settings.glucoseMeasurementSuffix} per unit', - 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), + ] + : [], + ), ), - ), - ]), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, + Expanded( + child: Column( + children: (bolus.units > 0 && (bolus.mgPerDl ?? bolus.mmolPerL ?? 0) > 0) + ? [ + Text((((Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDl : bolus.mmolPerL)! / 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!'), ); diff --git a/lib/screens/bolus/bolus_profile_detail.dart b/lib/screens/bolus/bolus_profile_detail.dart index e0e6d3b..fb69747 100644 --- a/lib/screens/bolus/bolus_profile_detail.dart +++ b/lib/screens/bolus/bolus_profile_detail.dart @@ -28,6 +28,7 @@ class _BolusProfileDetailScreenState extends State { bool _isNew = true; final GlobalKey _bolusProfileForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); late FloatingActionButton addBolusButton; late IconButton refreshButton; @@ -211,7 +212,7 @@ class _BolusProfileDetailScreenState extends State { ); }, ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void handleSaveAction() async { @@ -221,15 +222,15 @@ class _BolusProfileDetailScreenState extends State { 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); + Navigator.pop(context, + ['${_isNew ? 'New' : ''} Bolus Profile saved', bolusProfile]); } setState(() { @@ -286,45 +287,50 @@ class _BolusProfileDetailScreenState extends State { }); List tabs = [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - 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, - ), - BooleanFormField( - value: _active, - onChanged: (value) { - setState(() { - _active = value; - }); - }, - label: 'active', - ), - ], - ), - ], + BooleanFormField( + value: _active, + onChanged: (value) { + setState(() { + _active = value; + }); + }, + label: 'active', + ), + ], + ), + ], + ), ), ), ]; diff --git a/lib/screens/bolus/bolus_profile_list.dart b/lib/screens/bolus/bolus_profile_list.dart index 54ae351..504b246 100644 --- a/lib/screens/bolus/bolus_profile_list.dart +++ b/lib/screens/bolus/bolus_profile_list.dart @@ -15,15 +15,15 @@ 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()); void reload({String? message}) { setState(() { - pickActiveProfileMode = false; _bolusProfiles = BolusProfile.getAll(); }); @@ -99,7 +99,8 @@ class _BolusProfileListScreenState extends State { BolusProfile.setAllInactive; bolusProfile.active = true; BolusProfile.put(bolusProfile); - reload(message: '${bolusProfile.name} has been set as your active Profile'); + reload( + message: '${bolusProfile.name} has been set as your active Profile'); } } @@ -107,6 +108,7 @@ class _BolusProfileListScreenState extends State { setState(() { banner = MaterialBanner( content: AutoCompleteDropdownButton( + controller: TextEditingController(text: ''), items: _bolusProfiles, label: 'Default Basal Profile', onChanged: onPickActive, @@ -120,7 +122,6 @@ class _BolusProfileListScreenState extends State { ), ], ); - pickActiveProfileMode = true; }); } @@ -131,7 +132,7 @@ class _BolusProfileListScreenState extends State { builder: (context) => BolusProfileDetailScreen(id: bolusProfile?.id ?? 0, active: active), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onNew(bool active) { @@ -165,40 +166,50 @@ class _BolusProfileListScreenState extends State { banner, Expanded( child: _bolusProfiles.isNotEmpty - ? ListView.builder( - itemCount: _bolusProfiles.length, - itemBuilder: (context, index) { - final bolusProfile = _bolusProfiles[index]; - String activeProfileText = bolusProfile.active - ? 'Default Profile' - : bolusProfile.id == _activeProfile?.id - ? 'Current Active Profile' - : ''; - if (activeProfileText != '' && (bolusProfile.notes ?? '') != '') { - activeProfileText += '\n'; - } - return ListTile( - selected: bolusProfile.active || bolusProfile.id == _activeProfile?.id, - onTap: () => onEdit(bolusProfile), - title: Text( - bolusProfile.name, - ), - isThreeLine: activeProfileText != '' && (bolusProfile.notes ?? '') != '', - subtitle: Text('$activeProfileText${bolusProfile.notes ?? ''}'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => handleDeleteAction(bolusProfile), + ? 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.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/log.dart b/lib/screens/log/log.dart index 6041fe2..de6cc16 100644 --- a/lib/screens/log/log.dart +++ b/lib/screens/log/log.dart @@ -8,6 +8,7 @@ import 'package:diameter/navigation.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'; @@ -20,6 +21,8 @@ class LogScreen extends StatefulWidget { class _LogScreenState extends State { late Map> _logEntryDailyMap; + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -74,130 +77,189 @@ class _LogScreenState extends State { children: [ Expanded( child: _logEntryDailyMap.isNotEmpty - ? ListView.builder( - padding: const EdgeInsets.all(10.0), - shrinkWrap: true, - itemCount: _logEntryDailyMap.length, - itemBuilder: (context, dateIndex) { - List dateList = _logEntryDailyMap.keys.toList(); - final date = dateList[dateIndex]; - final entryList = _logEntryDailyMap[date]; - final tiles = []; - for (LogEntry logEntry in entryList!) { - 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)); - tiles.add(ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - LogEntryScreen(id: logEntry.id), + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(10.0), + shrinkWrap: true, + itemCount: _logEntryDailyMap.length, + itemBuilder: (context, dateIndex) { + List dateList = + _logEntryDailyMap.keys.toList(); + final date = dateList[dateIndex]; + final entryList = _logEntryDailyMap[date]; + final tiles = []; + for (LogEntry logEntry in entryList!) { + 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)); + tiles.add( + 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), + ] + : [], + ), + ), + ], ), - ).then((message) => reload(message: message)); - }, - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - DateTimeUtils.displayTime(logEntry.time), - ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => handleDeleteAction(logEntry), + icon: const Icon(Icons.delete, + color: Colors.blue), + ) + ], ), - Expanded( - child: Column( - children: logEntry.mgPerDl != null && - (Settings.glucoseMeasurement == - GlucoseMeasurement.mgPerDl || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode - .bothForList) - ? [ - Text(logEntry.mgPerDl.toString(), - style: glucoseStyle), - Text( - 'mg/dl', - style: glucoseStyle, - textScaleFactor: 0.75, - ), - ] - : [], - ), - ), - Expanded( - child: Column( - children: logEntry.mmolPerL != null && - (Settings.glucoseMeasurement == - GlucoseMeasurement.mmolPerL || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode - .bothForList) - ? [ - Text(logEntry.mmolPerL.toString(), style: glucoseStyle), - Text( - 'mmol/l', - style: glucoseStyle, - 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), - ) - ], - ), - )); - } - return ListBody( - children: [ - Text(DateTimeUtils.displayDate(date)) - ] + - tiles, - ); - }, + ), + )); + } + return ListBody( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + DateTimeUtils.displayDate(date).toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + ), + ) + ] + + tiles + + [const Divider()] + ); + }, + ), ) : const Center( child: Text('You have not created any Log Entries yet!'), @@ -212,7 +274,7 @@ class _LogScreenState extends State { MaterialPageRoute( builder: (context) => const LogEntryScreen(), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/screens/log/log_entry/log_bolus_detail.dart b/lib/screens/log/log_entry/log_bolus_detail.dart index 1e505ad..45037b7 100644 --- a/lib/screens/log/log_entry/log_bolus_detail.dart +++ b/lib/screens/log/log_entry/log_bolus_detail.dart @@ -10,6 +10,7 @@ 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'; @@ -48,6 +49,7 @@ class _LogBolusDetailScreenState extends State { bool _isSaving = false; final GlobalKey _logBolusForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _unitsController = TextEditingController(text: ''); final _carbsController = TextEditingController(text: ''); @@ -63,6 +65,8 @@ class _LogBolusDetailScreenState extends State { final _delayedUnitsController = TextEditingController(text: ''); final _immediateUnitsController = TextEditingController(text: ''); + final _mealController = TextEditingController(text: ''); + bool _setManually = false; BolusType _bolusType = BolusType.meal; LogMeal? _meal; @@ -78,13 +82,14 @@ class _LogBolusDetailScreenState extends State { _logEntry = LogEntry.get(widget.logEntryId); _logMeals = LogMeal.getAllForEntry(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; } @@ -96,7 +101,7 @@ class _LogBolusDetailScreenState extends State { : 0)) .toString(); _mgPerDlTargetController.text = - (_logBolus?.mgPerDlTarget ?? Settings.targetMgPerDl()).toString(); + (_logBolus?.mgPerDlTarget ?? Settings.targetMgPerDl).toString(); _mgPerDlCorrectionController.text = (_logBolus?.mgPerDlCorrection ?? max( (int.tryParse(_mgPerDlCurrentController.text) ?? 0) - @@ -109,7 +114,7 @@ class _LogBolusDetailScreenState extends State { : 0)) .toString(); _mmolPerLTargetController.text = - (_logBolus?.mmolPerLTarget ?? Settings.targetMmolPerL()).toString(); + (_logBolus?.mmolPerLTarget ?? Settings.targetMmolPerL).toString(); _mmolPerLCorrectionController.text = (_logBolus?.mmolPerLCorrection ?? max( (double.tryParse(_mmolPerLCurrentController.text) ?? 0) - @@ -131,13 +136,32 @@ class _LogBolusDetailScreenState extends State { updateDelayedRatio(); } - void reload() { + 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(); + }); } void updateDelayedRatio() { @@ -157,14 +181,14 @@ class _LogBolusDetailScreenState extends State { } } - void onSelectMeal(LogMeal meal) { - setState(() { - _meal = meal; - if (meal.carbsPerPortion != null) { + void onSelectMeal(LogMeal? meal) { + updateLogMeal(meal); + if (meal != null && meal.carbsPerPortion != null) { + setState(() { _carbsController.text = meal.carbsPerPortion.toString(); onChangeCarbs(); - } - }); + }); + } } void onChangeCarbs() { @@ -332,7 +356,7 @@ class _LogBolusDetailScreenState extends State { LogBolus.put(delayedBolus); } - Navigator.pop(context, '${_isNew ? 'New' : ''} Bolus Saved'); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Bolus Saved', logBolus, delayedBolus]); } setState(() { _isSaving = false; @@ -349,9 +373,9 @@ class _LogBolusDetailScreenState extends State { _mmolPerLCurrentController.text != (_logEntry?.mmolPerL.toString() ?? ''))) || _mgPerDlTargetController.text != - Settings.targetMgPerDl().toString() || + Settings.targetMgPerDl.toString() || _mmolPerLTargetController.text != - Settings.targetMmolPerL().toString() || + Settings.targetMmolPerL.toString() || _delayController.text != '' || _setManually || _notesController.text != '')) || @@ -391,321 +415,379 @@ class _LogBolusDetailScreenState extends State { title: Text(_isNew ? 'New Bolus' : 'Edit Bolus'), ), drawer: const Navigation(currentLocation: LogBolusDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - formState: _logBolusForm, - fields: [ - Row( - children: [ - Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Bolus Units', - suffixText: ' U', - ), - controller: _unitsController, - onChanged: (_) { - setState(() { - _setManually = true; - }); - updateDelayedRatio(); - }, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - ), - ), - 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; - }); - }, - ), - ), - ], - ), - Row( - children: [ - Expanded( - child: RadioListTile( - title: const Text('for glucose'), - groupValue: _bolusType, - value: BolusType.glucose, + body: Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormWrapper( + formState: _logBolusForm, + fields: [ + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Bolus Units', + suffixText: ' U', + ), + controller: _unitsController, onChanged: (_) { setState(() { - _bolusType = BolusType.glucose; + _setManually = true; }); - }), - ), - Expanded( - child: RadioListTile( - title: const Text('for meal'), - groupValue: _bolusType, - value: BolusType.meal, + updateDelayedRatio(); + }, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + ), + ), + Expanded( + child: BooleanFormField( + contentPadding: const EdgeInsets.only( + left: 10.0, right: 10.0, top: 10.0), + value: _setManually, + label: 'set manually', onChanged: (value) { setState(() { - _bolusType = BolusType.meal; + _setManually = value; }); - }), - ), - ], - ), - 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: TextFormField( - decoration: const InputDecoration( - labelText: 'Current', - suffixText: 'mg/dl', - ), - controller: _mgPerDlCurrentController, - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - onChangeGlucose( - calculateFrom: - GlucoseMeasurement.mgPerDl); - }, - keyboardType: const TextInputType - .numberWithOptions(), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Target', - suffixText: 'mg/dl', - ), - controller: _mgPerDlTargetController, - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - onChangeGlucose( - calculateFrom: - GlucoseMeasurement.mgPerDl); - }, - keyboardType: const TextInputType - .numberWithOptions(), - ), - ), - ), - Expanded( - child: Padding( - padding: - const EdgeInsets.only(left: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Correction', - suffixText: 'mg/dl', - ), - controller: - _mgPerDlCorrectionController, - readOnly: true, - ), - ), - ), - [GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode) - ? IconButton( - onPressed: () => onChangeGlucose( - calculateFrom: - GlucoseMeasurement - .mmolPerL), - icon: const Icon(Icons.calculate), - ) - : Container(), - ] - : [], - ), - Row( - children: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL || - [GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode) - ? [ - Expanded( - child: Padding( - padding: - const EdgeInsets.only(right: 5), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Current', - suffixText: 'mmol/l', - ), - controller: - _mmolPerLCurrentController, - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - onChangeGlucose( - calculateFrom: - GlucoseMeasurement.mmolPerL); - }, - keyboardType: const TextInputType - .numberWithOptions(), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Target', - suffixText: 'mmol/l', - ), - controller: _mmolPerLTargetController, - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - onChangeGlucose( - calculateFrom: - GlucoseMeasurement.mmolPerL); - }, - keyboardType: const TextInputType - .numberWithOptions(), - ), - ), - ), - Expanded( - child: Padding( - padding: - const EdgeInsets.only(left: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Correction', - suffixText: 'mmol/l', - ), - controller: - _mmolPerLCorrectionController, - readOnly: true, - ), - ), - ), - [GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode) - ? IconButton( - onPressed: () => onChangeGlucose( - calculateFrom: - GlucoseMeasurement.mgPerDl), - icon: const Icon(Icons.calculate), - ) - : Container(), - ] - : [], - ), - ] - : [ - AutoCompleteDropdownButton( - selectedItem: _meal, - label: 'Meal', - items: _logMeals, + }, + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: RadioListTile( + title: const Text('for glucose'), + groupValue: _bolusType, + value: BolusType.glucose, + onChanged: (_) { + setState(() { + _bolusType = BolusType.glucose; + }); + }), + ), + Expanded( + child: RadioListTile( + title: const Text('for meal'), + groupValue: _bolusType, + value: BolusType.meal, onChanged: (value) { - if (value != null) { - onSelectMeal(value); - } - }, - ), - TextFormField( - decoration: InputDecoration( - labelText: 'Carbs', - suffixText: Settings.nutritionMeasurementSuffix, - ), - controller: _carbsController, - onChanged: (_) => onChangeCarbs(), - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - ), - ], - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus Duration', - suffixText: ' min', + setState(() { + _bolusType = BolusType.meal; + }); + }), + ), + ], ), - controller: _delayController, - onChanged: (value) => setState(() {}), - keyboardType: const TextInputType.numberWithOptions(), - ), - (int.tryParse(_delayController.text) ?? 0) != 0 - ? Slider( - label: '${_delayPercentage.floor().toString()}%', - divisions: 100, - value: _delayPercentage, - min: 0, - max: 100, - onChanged: _delayController.text != '' - ? (value) { - setState(() { - _delayPercentage = value; - }); - updateDelayedRatio(); - } - : null, - ) - : Container(), - Row( - children: (int.tryParse(_delayController.text) ?? 0) != 0 - ? [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Immediate Bolus', - suffixText: ' U', + 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: TextFormField( + decoration: const InputDecoration( + labelText: 'Current', + suffixText: 'mg/dl', + ), + controller: + _mgPerDlCurrentController, + onChanged: (_) async { + await Future.delayed( + const Duration(seconds: 1)); + onChangeGlucose( + calculateFrom: + GlucoseMeasurement + .mgPerDl); + }, + keyboardType: const TextInputType + .numberWithOptions(), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Target', + suffixText: 'mg/dl', + ), + controller: + _mgPerDlTargetController, + onChanged: (_) async { + await Future.delayed( + const Duration(seconds: 1)); + onChangeGlucose( + calculateFrom: + GlucoseMeasurement + .mgPerDl); + }, + keyboardType: const TextInputType + .numberWithOptions(), + ), + ), + ), + Expanded( + child: Padding( + padding: + const EdgeInsets.only(left: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Correction', + suffixText: 'mg/dl', + ), + controller: + _mgPerDlCorrectionController, + readOnly: true, + ), + ), + ), + [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? IconButton( + onPressed: () => onChangeGlucose( + calculateFrom: + GlucoseMeasurement + .mmolPerL), + icon: const Icon(Icons.calculate), + ) + : Container(), + ] + : [], + ), + Row( + children: Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL || + [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? [ + Expanded( + child: Padding( + padding: + const EdgeInsets.only(right: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Current', + suffixText: 'mmol/l', + ), + controller: + _mmolPerLCurrentController, + onChanged: (_) async { + await Future.delayed( + const Duration(seconds: 1)); + onChangeGlucose( + calculateFrom: + GlucoseMeasurement + .mmolPerL); + }, + keyboardType: const TextInputType + .numberWithOptions(), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Target', + suffixText: 'mmol/l', + ), + controller: + _mmolPerLTargetController, + onChanged: (_) async { + await Future.delayed( + const Duration(seconds: 1)); + onChangeGlucose( + calculateFrom: + GlucoseMeasurement + .mmolPerL); + }, + keyboardType: const TextInputType + .numberWithOptions(), + ), + ), + ), + Expanded( + child: Padding( + padding: + const EdgeInsets.only(left: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Correction', + suffixText: 'mmol/l', + ), + controller: + _mmolPerLCorrectionController, + readOnly: true, + ), + ), + ), + [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? IconButton( + onPressed: () => onChangeGlucose( + calculateFrom: + GlucoseMeasurement + .mgPerDl), + icon: const Icon(Icons.calculate), + ) + : Container(), + ] + : [], + ), + ] + : [ + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _mealController, + selectedItem: _meal, + label: 'Meal', + items: _logMeals, + onChanged: onSelectMeal, + ), ), - controller: _immediateUnitsController, - readOnly: true, - enabled: (int.tryParse(_delayController.text) ?? - 0) != - 0, + 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: TextFormField( + decoration: InputDecoration( + labelText: 'Carbs', + suffixText: Settings.nutritionMeasurementSuffix, + ), + controller: _carbsController, + onChanged: (_) => onChangeCarbs(), + keyboardType: + const TextInputType.numberWithOptions( + decimal: true), ), ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 5.0), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus', - suffixText: ' U', + ], + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Delayed Bolus Duration', + suffixText: ' min', + ), + controller: _delayController, + onChanged: (value) => setState(() {}), + keyboardType: const TextInputType.numberWithOptions(), + ), + (int.tryParse(_delayController.text) ?? 0) != 0 + ? Slider( + label: '${_delayPercentage.floor().toString()}%', + divisions: 100, + value: _delayPercentage, + min: 0, + max: 100, + onChanged: _delayController.text != '' + ? (value) { + setState(() { + _delayPercentage = value; + }); + updateDelayedRatio(); + } + : null, + ) + : Container(), + Row( + children: (int.tryParse(_delayController.text) ?? 0) != 0 + ? [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Immediate Bolus', + suffixText: ' U', + ), + controller: _immediateUnitsController, + readOnly: true, + enabled: + (int.tryParse(_delayController.text) ?? + 0) != + 0, ), - controller: _delayedUnitsController, - readOnly: true, - enabled: (int.tryParse(_delayController.text) ?? - 0) != - 0, ), ), - ), - ] - : [], - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5.0), + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Delayed Bolus', + suffixText: ' U', + ), + controller: _delayedUnitsController, + readOnly: true, + enabled: + (int.tryParse(_delayController.text) ?? + 0) != + 0, + ), + ), + ), + ] + : [], ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ], + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + ), + keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, + ), + ], + ), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( diff --git a/lib/screens/log/log_entry/log_bolus_list.dart b/lib/screens/log/log_entry/log_bolus_list.dart index 36f3311..257e6c3 100644 --- a/lib/screens/log/log_entry/log_bolus_list.dart +++ b/lib/screens/log/log_entry/log_bolus_list.dart @@ -23,6 +23,8 @@ class LogBolusListScreen extends StatefulWidget { } class _LogBolusListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + void reload({String? message}) { widget.reload(); @@ -48,7 +50,7 @@ class _LogBolusListScreenState extends State { id: logBolus.id, ), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onDelete(LogBolus logBolus) { @@ -77,49 +79,55 @@ class _LogBolusListScreenState extends State { id: mealId, ), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } @override Widget build(BuildContext context) { return widget.logBoli.isNotEmpty - ? ListView.builder( - 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 ListTile( - onTap: () => handleEditAction(bolus), - title: Text(titleText), - 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), + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + 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), + 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 index 9b0e63d..a52a11e 100644 --- a/lib/screens/log/log_entry/log_entry.dart +++ b/lib/screens/log/log_entry/log_entry.dart @@ -13,6 +13,7 @@ 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'; @@ -28,12 +29,15 @@ class _LogEntryScreenState extends State { LogEntry? _logEntry; List _logMeals = []; List _logBoli = []; + bool _isNew = true; bool _isSaving = false; final GlobalKey logEntryForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); late DateTime _time; + double? _glucoseTrend; final _timeController = TextEditingController(text: ''); final _dateController = TextEditingController(text: ''); @@ -90,6 +94,7 @@ class _LogEntryScreenState extends State { _time = _logEntry!.time; _mgPerDlController.text = (_logEntry!.mgPerDl ?? '').toString(); _mmolPerLController.text = (_logEntry!.mmolPerL ?? '').toString(); + _glucoseTrend = _logEntry!.glucoseTrend; _notesController.text = _logEntry!.notes ?? ''; } else { _time = DateTime.now(); @@ -126,26 +131,21 @@ class _LogEntryScreenState extends State { _dateController.text = DateTimeUtils.displayDate(_time); } - void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { + void convertBetweenMgPerDlAndMmolPerL() { int? mgPerDl; double? mmolPerL; - if (calculateFrom != GlucoseMeasurement.mmolPerL && + if (Settings.glucoseMeasurement != 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) { + if (Settings.glucoseMeasurement != GlucoseMeasurement.mgPerDl && + _mmolPerLController.text != '') { + mmolPerL = double.tryParse(_mmolPerLController.text); setState(() { _mgPerDlController.text = Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString(); @@ -158,15 +158,17 @@ class _LogEntryScreenState extends State { _isSaving = true; }); if (logEntryForm.currentState!.validate()) { - LogEntry.put(LogEntry( + 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); Navigator.pushReplacementNamed(context, '/log', - arguments: '${_isNew ? 'New' : ''} Log Entry Saved'); + arguments: ['${_isNew ? 'New' : ''} Log Entry Saved', logEntry]); } setState(() { _isSaving = false; @@ -204,7 +206,7 @@ class _LogEntryScreenState extends State { return LogMealDetailScreen(logEntryId: _logEntry!.id); }, ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void handleAddNewBolus() async { @@ -215,7 +217,7 @@ class _LogEntryScreenState extends State { return LogBolusDetailScreen(logEntryId: _logEntry!.id); }, ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void renderTabButtons(index) { @@ -251,171 +253,173 @@ class _LogEntryScreenState extends State { renderTabButtons(tabController.index); }); List tabs = [ - SingleChildScrollView( - 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(); - } - }, + 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(); - } - }, + 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 || - [ - GlucoseDisplayMode.both, - GlucoseDisplayMode.bothForDetail - ].contains(Settings.glucoseDisplayMode) - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'mg/dl', - suffixText: 'mg/dl', - ), - controller: _mgPerDlController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - convertBetweenMgPerDlAndMmolPerL( - calculateFrom: - GlucoseMeasurement.mgPerDl); - }, - keyboardType: - const TextInputType.numberWithOptions(), - validator: (value) { - if (value!.trim().isEmpty && - _mmolPerLController.text - .trim() - .isEmpty) { - return 'How high is your blood sugar?'; - } - return null; - }, - ), - ) - : Container(), - Settings.glucoseDisplayMode == - GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? IconButton( - onPressed: () => - convertBetweenMgPerDlAndMmolPerL( - calculateFrom: - GlucoseMeasurement.mmolPerL), - icon: const Icon(Icons.calculate), - ) - : Container(), - Settings.glucoseMeasurement == - GlucoseMeasurement.mmolPerL || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'mmol/l', - suffixText: 'mmol/l', - ), - controller: _mmolPerLController, - onChanged: (_) async { - await Future.delayed( - const Duration(seconds: 1)); - convertBetweenMgPerDlAndMmolPerL( - calculateFrom: - GlucoseMeasurement.mmolPerL); - }, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true), - validator: (value) { - if (value!.trim().isEmpty && - _mgPerDlController.text - .trim() - .isEmpty) { - return 'How high is your blood sugar?'; - } - return null; - }, - ), - ) - : Container(), - Settings.glucoseDisplayMode == - GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.bothForDetail - ? IconButton( - onPressed: () => - convertBetweenMgPerDlAndMmolPerL( - calculateFrom: - GlucoseMeasurement.mgPerDl), - icon: const Icon(Icons.calculate), - ) - : Container(), - ], - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + ], ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ]), + Row( + children: [ + Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl || + [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'mg/dl', + suffixText: 'mg/dl', + ), + readOnly: Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL, + controller: _mgPerDlController, + onChanged: (_) async { + await Future.delayed( + const Duration(seconds: 1)); + convertBetweenMgPerDlAndMmolPerL(); + }, + keyboardType: const TextInputType + .numberWithOptions(), + validator: (value) { + if (value!.trim().isEmpty && + _mmolPerLController.text + .trim() + .isEmpty) { + return 'How high is your blood sugar?'; + } + return null; + }, + ), + ) + : Container(), + [ + GlucoseDisplayMode.both, + GlucoseDisplayMode.bothForDetail + ].contains(Settings.glucoseDisplayMode) + ? const SizedBox(width: 10.0) + : Container(), + Settings.glucoseMeasurement == + GlucoseMeasurement.mmolPerL || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'mmol/l', + suffixText: 'mmol/l', + ), + readOnly: Settings.glucoseMeasurement == + GlucoseMeasurement.mgPerDl, + controller: _mmolPerLController, + onChanged: (_) async { + await Future.delayed( + const Duration(seconds: 1)); + convertBetweenMgPerDlAndMmolPerL(); + }, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true), + validator: (value) { + if (value!.trim().isEmpty && + _mgPerDlController.text + .trim() + .isEmpty) { + return 'How high is your blood sugar?'; + } + return null; + }, + ), + ) + : 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, + ), + ], + ), + ]), + ), ), ]; diff --git a/lib/screens/log/log_entry/log_meal_detail.dart b/lib/screens/log/log_entry/log_meal_detail.dart index c023145..bbedeb5 100644 --- a/lib/screens/log/log_entry/log_meal_detail.dart +++ b/lib/screens/log/log_entry/log_meal_detail.dart @@ -10,6 +10,11 @@ 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'; @@ -30,8 +35,10 @@ class _LogMealDetailScreenState extends State { LogMeal? _logMeal; bool _isNew = true; bool _isSaving = false; + bool _isExpanded = false; final GlobalKey _logMealForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); final _carbsRatioController = TextEditingController(text: ''); @@ -46,6 +53,13 @@ class _LogMealDetailScreenState extends State { 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: ''); + List _meals = []; List _mealCategories = []; List _mealPortionTypes = []; @@ -72,22 +86,89 @@ class _LogMealDetailScreenState extends State { _carbsPerPortionController.text = (_logMeal!.carbsPerPortion ?? '').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(); } } - void reload() { + 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); + } + }); } - Future onSelectMeal(Meal meal) async { + 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; - _valueController.text = meal.value; + _valueController.text = _mealController.text = (_meal ?? '').toString(); + }); + + if (meal != null) { if (meal.carbsRatio != null) { _carbsRatioController.text = meal.carbsRatio.toString(); } @@ -98,21 +179,21 @@ class _LogMealDetailScreenState extends State { _carbsPerPortionController.text = meal.carbsPerPortion.toString(); } if (meal.mealSource.hasValue) { - _mealSource = meal.mealSource.target; + updateMealSource(meal.mealSource.target); } if (meal.mealCategory.hasValue) { - _mealCategory = meal.mealCategory.target; + updateMealCategory(meal.mealCategory.target); } if (meal.mealPortionType.hasValue) { - _mealPortionType = meal.mealPortionType.target; + updateMealPortionType(meal.mealPortionType.target); } if (meal.portionSizeAccuracy.hasValue) { - _portionSizeAccuracy = meal.portionSizeAccuracy.target; + updatePortionSizeAccuracy(meal.portionSizeAccuracy.target); } if (meal.carbsRatioAccuracy.hasValue) { - _carbsRatioAccuracy = meal.carbsRatioAccuracy.target; + updateCarbsRatioAccuracy(meal.carbsRatioAccuracy.target); } - }); + } } void handleSaveAction() async { @@ -136,7 +217,7 @@ class _LogMealDetailScreenState extends State { logMeal.carbsRatioAccuracy.target = _carbsRatioAccuracy; LogMeal.put(logMeal); - Navigator.pop(context, '${_isNew ? 'New' : ''} Meal Saved'); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Saved', logMeal]); } setState(() { _isSaving = false; @@ -233,178 +314,358 @@ class _LogMealDetailScreenState extends State { title: Text(_isNew ? 'New Meal' : _logMeal!.value), ), drawer: const Navigation(currentLocation: LogMealDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - formState: _logMealForm, - 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: _logMealForm, + 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; - }, - ), - AutoCompleteDropdownButton( - selectedItem: _meal, - label: 'Meal', - items: _meals, - onChanged: (value) { - if (value != null) { - onSelectMeal(value); - } - }, - ), - AutoCompleteDropdownButton( - selectedItem: _mealSource, - label: 'Meal Source', - items: _mealSources, - onChanged: (value) { - setState(() { - _mealSource = value; - }); - }, - ), - AutoCompleteDropdownButton( - selectedItem: _mealCategory, - label: 'Meal Category', - items: _mealCategories, - onChanged: (value) { - setState(() { - _mealCategory = value; - }); - }, - ), - AutoCompleteDropdownButton( - selectedItem: _mealPortionType, - label: 'Meal Portion Type', - items: _mealPortionTypes, - onChanged: (value) { - setState(() { - _mealPortionType = value; - }); - }, - ), - Row( - children: [ - Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Carbs ratio', - suffixText: '%', + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _mealController, + selectedItem: _meal, + label: 'Meal', + items: _meals, + onChanged: (value) { + if (value != null) { + onSelectMeal(value); + } + }, ), - controller: _carbsRatioController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - 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: - Settings.nutritionMeasurementSuffix, - alignLabelWithHint: true, - ), - controller: _portionSizeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); + 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), ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.portionSize), - icon: const Icon(Icons.calculate), - ), - ], - ), - AutoCompleteDropdownButton( - selectedItem: _portionSizeAccuracy, - label: 'Portion Size Accuracy', - items: _portionSizeAccuracies, - onChanged: (value) { - setState(() { - _portionSizeAccuracy = value; - }); - }, - ), - Row( - children: [ - Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Carbs per portion', - suffixText: - Settings.nutritionMeasurementSuffix, - ), - controller: _carbsPerPortionController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); - }, - ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.carbsPerPortion), - icon: const Icon(Icons.calculate), - ), - ], - ), - AutoCompleteDropdownButton( - selectedItem: _carbsRatioAccuracy, - label: 'Carbs Ratio Accuracy', - items: _carbsRatioAccuracies, - onChanged: (value) { - setState(() { - _carbsRatioAccuracy = value; - }); - }, - ), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + ], ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ], + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Carbs ratio', + suffixText: '%', + ), + controller: _carbsRatioController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) async { + await Future.delayed(const Duration(seconds: 1)); + 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: Settings.nutritionMeasurementSuffix, + ), + controller: _portionSizeController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation(); + }, + ), + ), + IconButton( + onPressed: () => + calculateThirdMeasurementOfPortionCarbsRelation( + parameterToBeCalculated: + PortionCarbsParameter.portionSize), + icon: const Icon(Icons.calculate), + ), + ], + ), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Carbs per portion', + suffixText: Settings.nutritionMeasurementSuffix, + ), + controller: _carbsPerPortionController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation(); + }, + ), + ), + IconButton( + onPressed: () => + calculateThirdMeasurementOfPortionCarbsRelation( + parameterToBeCalculated: + PortionCarbsParameter.carbsPerPortion), + icon: const Icon(Icons.calculate), + ), + ], + ), + 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( + 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( diff --git a/lib/screens/log/log_entry/log_meal_list.dart b/lib/screens/log/log_entry/log_meal_list.dart index bc48cd4..c562cb2 100644 --- a/lib/screens/log/log_entry/log_meal_list.dart +++ b/lib/screens/log/log_entry/log_meal_list.dart @@ -19,6 +19,8 @@ class LogMealListScreen extends StatefulWidget { } class _LogMealListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + void reload({String? message}) { widget.reload(); @@ -44,7 +46,7 @@ class _LogMealListScreenState extends State { id: meal.id, ), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onDelete(LogMeal logMeal) { @@ -67,40 +69,46 @@ class _LogMealListScreenState extends State { @override Widget build(BuildContext context) { return widget.logMeals.isNotEmpty - ? ListView.builder( - shrinkWrap: true, - itemCount: widget.logMeals.length, - itemBuilder: (context, index) { - final meal = widget.logMeals[index]; - return ListTile( - onTap: () => handleEditAction(meal), - title: Row( - children: [ - Expanded(child: Text(meal.value)), - Expanded( - child: Column( - children: ((meal.carbsPerPortion ?? 0) > 0) - ? [ - Text(meal.carbsPerPortion!.toStringAsPrecision(3)), - Text( - '${Settings.nutritionMeasurementSuffix} carbs', - textScaleFactor: 0.75), - ] - : [], - ), + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + 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)), + Expanded( + child: Column( + children: ((meal.carbsPerPortion ?? 0) > 0) + ? [ + Text(meal.carbsPerPortion!.toStringAsPrecision(3)), + Text( + '${Settings.nutritionMeasurementSuffix} carbs', + textScaleFactor: 0.75), + ] + : [], + ), + ), + ], + ), + trailing: IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, ), - ], - ), - trailing: IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, + onPressed: () => handleDeleteAction(meal), + ), ), - 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_event/log_event_detail.dart b/lib/screens/log/log_event/log_event_detail.dart index 629bc40..9bade50 100644 --- a/lib/screens/log/log_event/log_event_detail.dart +++ b/lib/screens/log/log_event/log_event_detail.dart @@ -8,6 +8,8 @@ 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'; @@ -35,6 +37,7 @@ class _LogEventDetailScreenState extends State { List _basalProfiles = []; final GlobalKey _logEventForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); late DateTime _time; DateTime? _endTime; @@ -49,9 +52,14 @@ class _LogEventDetailScreenState extends State { 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 = []; @@ -69,9 +77,16 @@ class _LogEventDetailScreenState extends State { (_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 { @@ -115,56 +130,78 @@ class _LogEventDetailScreenState extends State { _endDateController.text = DateTimeUtils.displayDate(_endTime); } - void onSelectEventType(LogEventType eventType) { + void updateBasalProfile(BasalProfile? value) { setState(() { - _eventType = eventType; - _hasEndTime = eventType.hasEndTime; - if (eventType.basalProfile.target != null) { - _basalProfile = eventType.basalProfile.target; - } - if (eventType.bolusProfile.target != null) { - _bolusProfile = eventType.bolusProfile.target; - } - if (eventType.defaultReminderDuration != null) { - _reminderDurationController.text = - eventType.defaultReminderDuration.toString(); - } + _basalProfile = value; + _basalProfileController.text = (_basalProfile ?? '').toString(); }); } - 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 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() { @@ -180,7 +217,7 @@ class _LogEventDetailScreenState extends State { event.basalProfile.target = _basalProfile; event.bolusProfile.target = _bolusProfile; LogEvent.put(event); - Navigator.pop(context, '${_isNew ? 'New' : ''} Event Saved'); + Navigator.pop(context, ['${_isNew ? 'New' : ''} Event Saved', event]); } void handleSaveAction() async { @@ -223,171 +260,221 @@ class _LogEventDetailScreenState extends State { title: Text(_isNew ? 'New Event' : 'Edit Event'), ), drawer: const Navigation(currentLocation: LogEventDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - formState: _logEventForm, - fields: [ - AutoCompleteDropdownButton( - selectedItem: _eventType, - label: 'Event Type', - items: _logEventTypes, - onChanged: (value) { - if (value != null) { - onSelectEventType(value); - } - }, - ), - 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(); - } - }, - ), - ), - ), - ], - ), - TextFormField( - controller: _reminderDurationController, - keyboardType: - const TextInputType.numberWithOptions(), - decoration: InputDecoration( - labelText: 'Default Reminder Duration', - suffixText: ' min', - enabled: _hasEndTime, - ), - ), - AutoCompleteDropdownButton( - selectedItem: _bolusProfile, - label: 'Bolus Profile', - items: _bolusProfiles, - onChanged: (value) { - setState(() { - _bolusProfile = value; - }); - }, - ), - AutoCompleteDropdownButton( - selectedItem: _basalProfile, - label: 'Basal Profile', - items: _basalProfiles, - onChanged: (value) { - setState(() { - _basalProfile = value; - }); - }, - ) - ] - : []), - TextFormField( - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, + 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, ), - keyboardType: TextInputType.multiline, - ), - ], - ), - ], + 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(); + } + }, + ), + ), + ), + ], + ), + 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), + ), + ], + ), + 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( diff --git a/lib/screens/log/log_event/log_event_list.dart b/lib/screens/log/log_event/log_event_list.dart index 48ed4e3..b460f8e 100644 --- a/lib/screens/log/log_event/log_event_list.dart +++ b/lib/screens/log/log_event/log_event_list.dart @@ -17,6 +17,7 @@ class LogEventListScreen extends StatefulWidget { class _LogEventListScreenState extends State { List _activeEvents = []; late Map> _logEventDailyMap; + final ScrollController _scrollController = ScrollController(); @override void initState() { @@ -51,7 +52,7 @@ class _LogEventListScreenState extends State { return const LogEventDetailScreen(); }, ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void handleEditAction(LogEvent event) { @@ -62,7 +63,7 @@ class _LogEventListScreenState extends State { id: event.id, ), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); } void onDelete(LogEvent logEvent) { @@ -116,92 +117,104 @@ class _LogEventListScreenState extends State { children: [ Expanded( child: _logEventDailyMap.isNotEmpty - ? ListView.builder( - shrinkWrap: true, - padding: const EdgeInsets.all(10.0), - itemCount: _logEventDailyMap.length, - itemBuilder: (context, dateIndex) { - List dateList = (_activeEvents.isNotEmpty - ? [null] - : []) + - _logEventDailyMap.keys.toList(); - final date = dateList[dateIndex]; - final eventList = date != null - ? _logEventDailyMap[date] - : _activeEvents; - final tiles = []; - if (eventList != null) { - for (LogEvent event in eventList) { - tiles.add(ListTile( - onTap: () { - handleEditAction(event); - }, - title: Row( - crossAxisAlignment: - CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Text(date == null + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + shrinkWrap: true, + padding: const EdgeInsets.all(10.0), + itemCount: _logEventDailyMap.length, + itemBuilder: (context, dateIndex) { + List dateList = (_activeEvents.isNotEmpty + ? [null] + : []) + + _logEventDailyMap.keys.toList(); + final date = dateList[dateIndex]; + final eventList = date != null + ? _logEventDailyMap[date] + : _activeEvents; + final tiles = []; + if (eventList != null) { + for (LogEvent event in eventList) { + tiles.add(Card( + child: ListTile( + onTap: () { + handleEditAction(event); + }, + title: Row( + crossAxisAlignment: + CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text(date == null ? DateTimeUtils .displayDateTime( event.time) : DateTimeUtils.displayTime( event.isEndEvent ? event.endTime - : event.time))), - const SizedBox(width: 24), - Expanded( - child: Text(event.title ?? - event.eventType.target?.value ?? - ''), - ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - event.hasEndTime && - event.endTime == null - ? IconButton( - icon: const Icon( - Icons.stop, - color: Colors.blue, + : event.time), ), - onPressed: () => - handleStopAction(event), - ) - : const SizedBox(width: 50), - IconButton( - icon: const Icon( - Icons.edit, - color: Colors.blue, - ), - onPressed: () => - handleEditAction(event), + const SizedBox(width: 24), + Expanded( + child: Text( + event.title ?? event.eventType.target?.value ?? '', + // style: Theme.of(context).textTheme.subtitle2, + ), + ), + ], ), - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => - handleDeleteAction(event), + 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), + ), + ], ), - ], - ), - )); + ), + )); + } } - } - return eventList != null && eventList.isNotEmpty ? ListBody( - children: [ - Text(DateTimeUtils.displayDate(date, - fallback: 'Active Events')) - ] + tiles, - - ) : Container(); - }, - ) + return eventList != null && eventList.isNotEmpty ? ListBody( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + DateTimeUtils.displayDate(date, fallback: 'Active Events').toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + ), + ), + ] + tiles + + [const Divider()], + ) : Container(); + }, + ), + ) : const Center( child: Text('There are no Events!'), ), diff --git a/lib/screens/log/log_event/log_event_type_detail.dart b/lib/screens/log/log_event/log_event_type_detail.dart index 4b6c7f9..98ea5b4 100644 --- a/lib/screens/log/log_event/log_event_type_detail.dart +++ b/lib/screens/log/log_event/log_event_type_detail.dart @@ -7,6 +7,8 @@ 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 { @@ -15,8 +17,7 @@ class EventTypeDetailScreen extends StatefulWidget { const EventTypeDetailScreen({Key? key, this.id = 0}) : super(key: key); @override - _EventTypeDetailScreenState createState() => - _EventTypeDetailScreenState(); + _EventTypeDetailScreenState createState() => _EventTypeDetailScreenState(); } class _EventTypeDetailScreenState extends State { @@ -28,13 +29,17 @@ class _EventTypeDetailScreenState extends State { List _basalProfiles = []; final GlobalKey _logEventTypeForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); final _defaultReminderDurationController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); + bool _hasEndTime = false; BolusProfile? _bolusProfile; BasalProfile? _basalProfile; + final _bolusProfileController = TextEditingController(text: ''); + final _basalProfileController = TextEditingController(text: ''); @override void initState() { @@ -52,17 +57,45 @@ class _EventTypeDetailScreenState extends State { _hasEndTime = _logEventType!.hasEndTime; _notesController.text = _logEventType!.notes ?? ''; _basalProfile = _logEventType!.basalProfile.target; + _basalProfileController.text = (_basalProfile ?? '').toString(); _bolusProfile = _logEventType!.bolusProfile.target; + _bolusProfileController.text = (_bolusProfile ?? '').toString(); } } - void reload() { + 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 { @@ -81,7 +114,8 @@ class _EventTypeDetailScreenState extends State { eventType.basalProfile.target = _basalProfile; eventType.bolusProfile.target = _bolusProfile; LogEventType.put(eventType); - Navigator.pop(context, '${_isNew ? 'New' : ''} Log Event Type Saved'); + Navigator.pop( + context, ['${_isNew ? 'New' : ''} Log Event Type Saved', eventType]); } setState(() { _isSaving = false; @@ -121,18 +155,18 @@ class _EventTypeDetailScreenState extends State { ), drawer: const Navigation(currentLocation: EventTypeDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - formState: _logEventTypeForm, - fields: [ + 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', - alignLabelWithHint: true, ), validator: (value) { if (value!.trim().isEmpty) { @@ -151,48 +185,110 @@ class _EventTypeDetailScreenState extends State { }, ), Column( - children: _hasEndTime ? [ - TextFormField( - controller: _defaultReminderDurationController, - keyboardType: const TextInputType.numberWithOptions(), - decoration: InputDecoration( - labelText: 'Default Reminder Duration', - suffixText: ' min', - enabled: _hasEndTime, - ), - ), - AutoCompleteDropdownButton( - selectedItem: _bolusProfile, - label: 'Bolus Profile', - items: _bolusProfiles, - onChanged: (value) { - setState(() { - _bolusProfile = value; - }); - }, - ), - AutoCompleteDropdownButton( - selectedItem: _basalProfile, - label: 'Basal Profile', - items: _basalProfiles, - onChanged: (value) { - setState(() { - _basalProfile = value; - }); - }, - ), - ] : []), + children: _hasEndTime + ? [ + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: TextFormField( + controller: _defaultReminderDurationController, + keyboardType: + const TextInputType.numberWithOptions(), + decoration: InputDecoration( + labelText: 'Default Reminder Duration', + suffixText: ' min', + enabled: _hasEndTime, + ), + ), + ), + 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]); + _bolusProfileController.text = + _bolusProfile.toString(); + }); + 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', - alignLabelWithHint: true, ), keyboardType: TextInputType.multiline, + minLines: 2, + maxLines: 5, ), - ] - ), - ], + ]), + ], + ), ), ), bottomNavigationBar: DetailBottomRow( diff --git a/lib/screens/log/log_event/log_event_type_list.dart b/lib/screens/log/log_event/log_event_type_list.dart index 9ad370a..85711b1 100644 --- a/lib/screens/log/log_event/log_event_type_list.dart +++ b/lib/screens/log/log_event/log_event_type_list.dart @@ -14,6 +14,8 @@ class LogEventTypeListScreen extends StatefulWidget { class _LogEventTypeListScreenState extends State { List _logEventTypes = []; + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -49,43 +51,53 @@ class _LogEventTypeListScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: _logEventTypes.isNotEmpty ? ListView.builder( - padding: const EdgeInsets.all(10.0), - itemCount: _logEventTypes.length, - itemBuilder: (context, index) { - final logEventType = _logEventTypes[index]; - return ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EventTypeDetailScreen( - id: logEventType.id), - ), - ).then((message) => reload(message: message)); - }, - title: Text(logEventType.value), - 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), - ) - ], + 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!'), ), - ); - }, - ) : const Center( - child: Text('You have not created any Log Event Types yet!'), - ), ), ], ), @@ -96,7 +108,7 @@ class _LogEventTypeListScreenState extends State { MaterialPageRoute( builder: (context) => const EventTypeDetailScreen(), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/screens/meal/meal_category_detail.dart b/lib/screens/meal/meal_category_detail.dart index 86c84a4..0708dac 100644 --- a/lib/screens/meal/meal_category_detail.dart +++ b/lib/screens/meal/meal_category_detail.dart @@ -22,6 +22,7 @@ class _MealCategoryDetailScreenState extends State { bool _isNew = true; final GlobalKey _mealCategoryForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); @@ -37,22 +38,38 @@ class _MealCategoryDetailScreenState extends State { } } - void reload() { + 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()) { - MealCategory.put(MealCategory( + MealCategory mealCategory = MealCategory( id: widget.id, value: _valueController.text, - notes: _notesController.text)); - Navigator.pop(context, '${_isNew ? 'New' : ''} Meal Category saved'); + notes: _notesController.text, + ); + MealCategory.put(mealCategory); + Navigator.pop(context, [ + '${_isNew ? 'New' : ''} Meal Category saved', mealCategory + ]); } } @@ -81,36 +98,41 @@ class _MealCategoryDetailScreenState extends State { ), drawer: const Navigation(currentLocation: MealCategoryDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - 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( diff --git a/lib/screens/meal/meal_category_list.dart b/lib/screens/meal/meal_category_list.dart index 40a960e..e5acda3 100644 --- a/lib/screens/meal/meal_category_list.dart +++ b/lib/screens/meal/meal_category_list.dart @@ -17,6 +17,8 @@ class MealCategoryListScreen extends StatefulWidget { class _MealCategoryListScreenState extends State { List _mealCategories = []; + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -75,41 +77,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( - id: mealCategory.id, - ), + 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) => reload(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!'), ), @@ -123,7 +132,7 @@ class _MealCategoryListScreenState extends State { MaterialPageRoute( builder: (context) => const MealCategoryDetailScreen(), ), - ).then((message) => reload(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 118284a..d612ed1 100644 --- a/lib/screens/meal/meal_detail.dart +++ b/lib/screens/meal/meal_detail.dart @@ -9,6 +9,10 @@ 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_portion_type_detail.dart'; +import 'package:diameter/screens/meal/meal_source_detail.dart'; import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; @@ -24,10 +28,13 @@ class MealDetailScreen extends StatefulWidget { class _MealDetailScreenState extends State { Meal? _meal; + bool _isNew = true; bool _isSaving = false; + bool _isExpanded = false; final GlobalKey _mealForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); final _carbsRatioController = TextEditingController(text: ''); @@ -44,6 +51,12 @@ class _MealDetailScreenState extends State { 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 = []; @@ -68,27 +81,75 @@ class _MealDetailScreenState extends State { _portionSizeController.text = (_meal!.portionSize ?? '').toString(); _carbsPerPortionController.text = (_meal!.carbsPerPortion ?? '').toString(); - _delayedBolusPercentage = - _meal!.delayedBolusPercentage ?? 0; + _delayedBolusPercentage = _meal!.delayedBolusPercentage ?? 0; _delayedBolusDurationController.text = (_meal!.delayedBolusDuration ?? '').toString(); _notesController.text = _meal!.notes ?? ''; _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(); } } - void reload() { + 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 { @@ -114,7 +175,7 @@ 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; @@ -152,8 +213,7 @@ class _MealDetailScreenState extends State { _portionSizeAccuracy != _meal!.portionSizeAccuracy.target || int.tryParse(_delayedBolusDurationController.text) != _meal!.delayedBolusDuration || - _delayedBolusPercentage != - _meal!.delayedBolusPercentage || + _delayedBolusPercentage != _meal!.delayedBolusPercentage || _notesController.text != (_meal!.notes ?? ''))))) { Dialogs.showCancelConfirmationDialog( context: context, @@ -165,40 +225,40 @@ class _MealDetailScreenState extends State { } } - Future onSelectMealSource(MealSource mealSource) async { + Future onSelectMealSource(MealSource? mealSource) async { 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}) { + {PortionCarbsParameter? changedParameter}) { double? carbsRatio; double? portionSize; double? carbsPerPortion; - if (parameterToBeCalculated != PortionCarbsParameter.carbsRatio && - _carbsRatioController.text != '') { + if (_carbsRatioController.text != '') { carbsRatio = double.tryParse(_carbsRatioController.text); } - if (parameterToBeCalculated != PortionCarbsParameter.portionSize && - _portionSizeController.text != '') { + if (_portionSizeController.text != '') { portionSize = double.tryParse(_portionSizeController.text); } - if (parameterToBeCalculated != PortionCarbsParameter.carbsPerPortion && - _carbsRatioController.text != '') { + if (_carbsRatioController.text != '') { carbsPerPortion = double.tryParse(_carbsPerPortionController.text); } @@ -232,59 +292,92 @@ class _MealDetailScreenState extends State { title: Text(_isNew ? 'New Meal' : _meal!.value), ), drawer: const Navigation(currentLocation: MealDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - 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; - }, - ), - AutoCompleteDropdownButton( - selectedItem: _mealSource, - label: 'Meal Source', - items: _mealSources, - onChanged: (value) { - if (value != null) { - onSelectMealSource(value); - } - }, - ), - AutoCompleteDropdownButton( - selectedItem: _mealCategory, - label: 'Meal Category', - items: _mealCategories, - onChanged: (value) { - setState(() { - _mealCategory = value; - }); - }, - ), - AutoCompleteDropdownButton( - selectedItem: _mealPortionType, - label: 'Meal Portion Type', - items: _mealPortionTypes, - onChanged: (value) { - setState(() { - _mealPortionType = value; - }); - }, - ), - Row( - children: [ - Expanded( - child: TextFormField( + Row( + children: [ + Expanded( + child: AutoCompleteDropdownButton( + controller: _mealSourceController, + selectedItem: _mealSource, + label: 'Meal Source', + items: _mealSources, + onChanged: onSelectMealSource, + ), + ), + 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, + ), + ), + 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: TextFormField( decoration: const InputDecoration( labelText: 'Carbs ratio', suffixText: '%', @@ -296,134 +389,239 @@ class _MealDetailScreenState extends State { await Future.delayed(const Duration(seconds: 1)); 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: Settings.nutritionMeasurementSuffix, - alignLabelWithHint: true, ), - controller: _portionSizeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); - }, ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.portionSize), - icon: const Icon(Icons.calculate), - ), - ], - ), - AutoCompleteDropdownButton( - selectedItem: _portionSizeAccuracy, - label: 'Portion Size Accuracy', - items: _portionSizeAccuracies, - onChanged: (value) { - setState(() { - _portionSizeAccuracy = value; - }); - }, - ), - Row( - children: [ - Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Carbs per portion', - suffixText: Settings.nutritionMeasurementSuffix, - ), - controller: _carbsPerPortionController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { - await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); - }, - ), - ), - IconButton( - onPressed: () => - calculateThirdMeasurementOfPortionCarbsRelation( - parameterToBeCalculated: - PortionCarbsParameter.carbsPerPortion), - icon: const Icon(Icons.calculate), - ), - ], - ), - AutoCompleteDropdownButton( - selectedItem: _carbsRatioAccuracy, - label: 'Carbs Ratio Accuracy', - items: _carbsRatioAccuracies, - onChanged: (value) { - setState(() { - _carbsRatioAccuracy = value; - }); - }, - ), - // ignore: todo - // TODO: display according to time format - TextFormField( - decoration: const InputDecoration( - labelText: 'Delayed Bolus Duration', - suffixText: ' min', - ), - controller: _delayedBolusDurationController, - keyboardType: const TextInputType.numberWithOptions(), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 5.0), - child: Row( - children: [ - const Text('Delayed Bolus Percentage:'), + const SizedBox(width: 10), Expanded( - child: Slider( - label: '${_delayedBolusPercentage.floor().toString()}%', - divisions: 100, - value: _delayedBolusPercentage, - min: 0, - max: 100, - onChanged: (value) { - setState(() { - _delayedBolusPercentage = value; - }); - } + child: TextFormField( + decoration: InputDecoration( + labelText: 'Portion size', + suffixText: Settings.nutritionMeasurementSuffix, + ), + controller: _portionSizeController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation(); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Carbs per portion', + suffixText: Settings.nutritionMeasurementSuffix, + ), + controller: _carbsPerPortionController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) async { + await Future.delayed(const Duration(seconds: 1)); + calculateThirdMeasurementOfPortionCarbsRelation(); + }, ), ), ], ), - ), - 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, - ), - ], - ), - ], + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + children: [ + Text( + 'BOLUS DELAY', + style: Theme.of(context).textTheme.subtitle2, + ), + const Spacer(), + ], + ), + ), + // ignore: todo + // TODO: display according to time format + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Duration', + suffixText: ' min', + ), + controller: _delayedBolusDurationController, + keyboardType: const TextInputType.numberWithOptions(), + ), + ), + Expanded( + child: Slider( + label: + '${_delayedBolusPercentage.floor().toString()}%', + divisions: 100, + value: _delayedBolusPercentage, + min: 0, + max: 100, + onChanged: (value) { + setState(() { + _delayedBolusPercentage = value; + }); + }), + ), + const Text('%', textScaleFactor: 1.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< + 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( diff --git a/lib/screens/meal/meal_list.dart b/lib/screens/meal/meal_list.dart index af192a8..45bfd31 100644 --- a/lib/screens/meal/meal_list.dart +++ b/lib/screens/meal/meal_list.dart @@ -17,6 +17,8 @@ class MealListScreen extends StatefulWidget { class _MealListScreenState extends State { List _meals = []; + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -68,70 +70,87 @@ class _MealListScreenState extends State { 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]; - String portionType = meal.mealPortionType.hasValue ? ' per ${meal.mealPortionType.target!.value}' : ''; - return ListTile( - isThreeLine: true, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - MealDetailScreen(id: meal.id), + 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) => reload(message: message)); - }, - title: Text(meal.value), - subtitle: Row( - children: [ - Column( + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Row( + children: [ + Column( + children: [ + Text(meal.mealSource.target?.value ?? ''), + Text(meal.notes ?? ''), + ], + ), + 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 + ), + ] + : [], + ), + ), + ], + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ - Text(meal.mealSource.target?.value ?? ''), - Text(meal.notes ?? ''), + IconButton( + onPressed: () => handleDeleteAction(meal), + icon: const Icon(Icons.delete, + color: Colors.blue), + ) ], ), - Expanded( - child: Column( - 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', - textScaleFactor: 0.75), - ] - : [], - ), - ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () => handleDeleteAction(meal), - icon: const Icon(Icons.delete, - color: Colors.blue), - ) - ], - ), - ); - }, + ), + ); + }, + ), ): const Center( child: Text('You have not created any Meals yet!'), ), @@ -145,7 +164,7 @@ class _MealListScreenState extends State { MaterialPageRoute( builder: (context) => const MealDetailScreen(), ), - ).then((message) => reload(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 14c0768..6b69054 100644 --- a/lib/screens/meal/meal_portion_type_detail.dart +++ b/lib/screens/meal/meal_portion_type_detail.dart @@ -24,6 +24,7 @@ class _MealPortionTypeDetailScreenState bool _isNew = true; final GlobalKey _mealPortionTypeForm = GlobalKey(); + final ScrollController _scrollController = ScrollController(); final _valueController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); @@ -39,23 +40,37 @@ class _MealPortionTypeDetailScreenState } } - void reload() { + 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()) { - MealPortionType.put(MealPortionType( + 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]); } } @@ -82,41 +97,45 @@ class _MealPortionTypeDetailScreenState bool isNew = _mealPortionType == null; return Scaffold( appBar: AppBar( - title: Text( - isNew ? 'New Meal Portion Type' : _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: [ - FormWrapper( - 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( diff --git a/lib/screens/meal/meal_portion_type_list.dart b/lib/screens/meal/meal_portion_type_list.dart index 81db16f..64c8463 100644 --- a/lib/screens/meal/meal_portion_type_list.dart +++ b/lib/screens/meal/meal_portion_type_list.dart @@ -18,6 +18,8 @@ class MealPortionTypeListScreen extends StatefulWidget { class _MealPortionTypeListScreenState extends State { List _mealPortionTypes = []; + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -73,43 +75,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( - id: mealPortionType.id, - ), + 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) => reload(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!'), ), ), @@ -122,7 +131,7 @@ class _MealPortionTypeListScreenState extends State { MaterialPageRoute( builder: (context) => const MealPortionTypeDetailScreen(), ), - ).then((message) => reload(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 cfe0f15..cc7c228 100644 --- a/lib/screens/meal/meal_source_detail.dart +++ b/lib/screens/meal/meal_source_detail.dart @@ -8,6 +8,9 @@ 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_portion_type_detail.dart'; import 'package:flutter/material.dart'; class MealSourceDetailScreen extends StatefulWidget { @@ -31,13 +34,19 @@ class _MealSourceDetailScreenState extends State { 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() { @@ -56,37 +65,51 @@ class _MealSourceDetailScreenState extends State { _defaultPortionSizeAccuracy = _mealSource!.defaultPortionSizeAccuracy.target; + _defaultPortionSizeAccuracyController.text = (_defaultPortionSizeAccuracy ?? '').toString(); _defaultCarbsRatioAccuracy = _mealSource!.defaultCarbsRatioAccuracy.target; + _defaultCarbsRatioAccuracyController.text = (_defaultCarbsRatioAccuracy ?? '').toString(); _defaultMealCategory = _mealSource!.defaultMealCategory.target; - _defaultMealPortionType = - _mealSource!.defaultMealPortionType.target; + _defaultMealCategoryController.text = (_defaultMealCategory ?? '').toString(); + _defaultMealPortionType = _mealSource!.defaultMealPortionType.target; + _defaultMealPortionTypeController.text = (_defaultMealPortionType ?? '').toString(); } } - void reload() { + 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 { 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'); + 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() { @@ -108,8 +131,7 @@ class _MealSourceDetailScreenState extends State { _mealSource!.defaultMealCategory.target || _defaultMealPortionType != _mealSource!.defaultMealPortionType.target || - _notesController.text != - (_mealSource!.notes ?? ''))))) { + _notesController.text != (_mealSource!.notes ?? ''))))) { Dialogs.showCancelConfirmationDialog( context: context, isNew: _isNew, @@ -128,76 +150,214 @@ class _MealSourceDetailScreenState extends State { ), drawer: const Navigation(currentLocation: MealSourceDetailScreen.routeName), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FormWrapper( - 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; - }, - ), - AutoCompleteDropdownButton( - selectedItem: _defaultCarbsRatioAccuracy, - label: 'Default Carbs Ratio Accuracy', - items: _carbsRatioAccuracies, - onChanged: (value) { - setState(() { - _defaultCarbsRatioAccuracy = value; - }); - }, - ), - AutoCompleteDropdownButton( - selectedItem: _defaultPortionSizeAccuracy, - label: 'Default Portion Size Accuracy', - items: _portionSizeAccuracies, - onChanged: (value) { - setState(() { - _defaultPortionSizeAccuracy = value; - }); - }, - ), - AutoCompleteDropdownButton( - selectedItem: _defaultMealCategory, - label: 'Default Meal Category', - items: _mealCategories, - onChanged: (value) { - setState(() { - _defaultMealCategory = value; - }); - }, - ), - AutoCompleteDropdownButton( - selectedItem: _defaultMealPortionType, - label: 'Default Meal Portion Type', - items: _mealPortionTypes, - 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( diff --git a/lib/screens/meal/meal_source_list.dart b/lib/screens/meal/meal_source_list.dart index bf4ece8..543d8b3 100644 --- a/lib/screens/meal/meal_source_list.dart +++ b/lib/screens/meal/meal_source_list.dart @@ -17,6 +17,8 @@ class MealSourceListScreen extends StatefulWidget { class _MealSourceListScreenState extends State { List _mealSources = []; + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); @@ -71,41 +73,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( - id: mealSource.id, - ), + 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) => reload(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!'), ), @@ -119,7 +129,7 @@ class _MealSourceListScreenState extends State { MaterialPageRoute( builder: (context) => const MealSourceDetailScreen(), ), - ).then((message) => reload(message: message)); + ).then((result) => reload(message: result?[0])); }, child: const Icon(Icons.add), ), diff --git a/lib/settings.dart b/lib/settings.dart index 3c58cde..36fd80f 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -17,8 +17,8 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { late Settings _settings; - late String _nutritionMeasurementLabel; - late String _glucoseMeasurementLabel; + final TextEditingController _nutritionMeasurementLabelController = TextEditingController(text: ''); + final TextEditingController _glucoseMeasurementLabelController = TextEditingController(text: ''); late bool _onlyDisplayActiveGlucoseMeasurement; late bool _displayBothGlucoseMeasurementsInDetailView; @@ -44,9 +44,9 @@ class _SettingsScreenState extends State { void initState() { super.initState(); _settings = Settings.get(); - _nutritionMeasurementLabel = + _nutritionMeasurementLabelController.text = nutritionMeasurementLabels[_settings.nutritionMeasurementIndex]; - _glucoseMeasurementLabel = + _glucoseMeasurementLabelController.text = glucoseMeasurementLabels[_settings.glucoseMeasurementIndex]; _onlyDisplayActiveGlucoseMeasurement = _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.activeOnly.index; _displayBothGlucoseMeasurementsInDetailView = @@ -93,9 +93,9 @@ class _SettingsScreenState extends State { Settings.put(Settings( id: _settings.id, nutritionMeasurementIndex: - nutritionMeasurementLabels.indexOf(_nutritionMeasurementLabel), + nutritionMeasurementLabels.indexOf(_nutritionMeasurementLabelController.text), glucoseMeasurementIndex: - glucoseMeasurementLabels.indexOf(_glucoseMeasurementLabel), + glucoseMeasurementLabels.indexOf(_glucoseMeasurementLabelController.text), glucoseDisplayModeIndex: _onlyDisplayActiveGlucoseMeasurement ? GlucoseDisplayMode.activeOnly.index : _displayBothGlucoseMeasurementsInDetailView && _displayBothGlucoseMeasurementsInListView @@ -157,27 +157,48 @@ class _SettingsScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + children: [ + Text( + 'MEASUREMENTS', + style: Theme.of(context).textTheme.subtitle2, + ), + const Spacer(), + ], + ), + ), AutoCompleteDropdownButton( - selectedItem: _nutritionMeasurementLabel, + controller: _nutritionMeasurementLabelController, + selectedItem: _nutritionMeasurementLabelController.text, label: 'Preferred Nutrition Measurement', items: nutritionMeasurementLabels, onChanged: (value) { if (value != null) { - _nutritionMeasurementLabel = value; + setState(() { + _nutritionMeasurementLabelController.text = value; + }); saveSettings(); } }, ), - AutoCompleteDropdownButton( - selectedItem: _glucoseMeasurementLabel, - label: 'Preferred Glucose Measurement', - items: glucoseMeasurementLabels, - onChanged: (value) { - if (value != null) { - _glucoseMeasurementLabel = 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) { + if (value != null) { + setState(() { + _glucoseMeasurementLabelController.text = value; + }); + saveSettings(); + } + }, + ), ), BooleanFormField( value: _onlyDisplayActiveGlucoseMeasurement, @@ -205,10 +226,19 @@ class _SettingsScreenState extends State { saveSettings(); }, ), - const Padding( - padding: EdgeInsets.only(top: 10.0), - child: Text('Confirmation prompts'), - ), + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + children: [ + Text( + 'CONFIRMATION PROMPTS', + style: Theme.of(context).textTheme.subtitle2, + ), + const Spacer(), + ], + ), + ), BooleanFormField( value: _showConfirmationDialogOnCancel, label: 'on cancelling edit or creation of a record if changes have already been made', diff --git a/pubspec.lock b/pubspec.lock index c41da0e..5616526 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -422,13 +422,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" - objectbox_flutter_libs: - dependency: "direct main" - description: - name: objectbox_flutter_libs - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" objectbox_generator: dependency: "direct dev" description: @@ -436,6 +429,13 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: @@ -610,7 +610,7 @@ packages: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" sembast: dependency: transitive description: @@ -720,7 +720,7 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+4" + version: "2.0.1" sqflite_common: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 95d9cfd..0da46cc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: 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 +31,3 @@ dev_dependencies: flutter: uses-material-design: true - fonts: - - family: RobotoCondensed - fonts: - - asset: assets/fonts/RobotoCondensed-Regular.ttf 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",