ui improvements to most components

This commit is contained in:
spinel 2021-12-09 06:14:55 +01:00
parent 6c987009dc
commit 0dfcedff0b
57 changed files with 3906 additions and 2503 deletions

29
TODO
View File

@ -1,35 +1,31 @@
BUGFIXES: BUGFIXES:
General/Framework: General/Framework:
☐ fix preloading of dropdown values (appear blank at first as of now)
☐ make sure 'null' isn't shown in text fields ☐ make sure 'null' isn't shown in text fields
Log Entry:
☐ glucose target isn't displaed correctly anymore
Basal/Bolus: Basal/Bolus:
☐ "no element" error on creating basal/bolus rates when working from apk ☐ "no element" error on creating basal/bolus rates when working from apk
MAIN TASKS: MAIN TASKS:
Layout: Layout:
☐ make a styleguide (actively decide what components should look like) ☐ 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: General/Framework:
☐ show indicator and make all fields readonly if user somehow gets to a deleted record detail view ☐ 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) ☐ clean up controllers (dispose method of each stateful widget)
☐ improve dropdown component ☐ account for deleted/disabled elements in dropdowns
☐ 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?)
☐ check through all detail forms and set required fields/according messages ☐ check through all detail forms and set required fields/according messages
☐ implement component for durations ☐ implement component for durations
☐ change placement of delete and floating button because its very easy to accidentally hit delete ☐ 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: Basal/Bolus:
☐ add save and close and next buttons on rate creations ☐ 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: Log Entry:
☐ add save and close button ☐ add save and close button
☐ move on to newly created entry after saving ☐ move on to newly created entry after saving
☐ add option to specify trend for blood sugar
☐ recalculate bolus upon deactivating 'set manually' option ☐ 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: Event Types:
☐ add colors as indicators for log entries (and later graphs in reports) ☐ add colors as indicators for log entries (and later graphs in reports)
Settings: Settings:
@ -39,18 +35,22 @@ MAIN TASKS:
☐ add field for active insulin duration ☐ add field for active insulin duration
☐ add setting for carb units/bread units ☐ add setting for carb units/bread units
☐ add option to switch 'save' and 'save & close' buttons ☐ 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: FUTURE TASKS:
General/Framework: General/Framework:
☐ setup objectbox sync server
☐ add explanations to each section ☐ add explanations to each section
☐ find a better way to work with multiple glucose measurements (or disable it?) ☐ find a better way to work with multiple glucose measurements (or disable it?)
☐ evaluate if some fields should be readonly instead of completely hidden ☐ evaluate if some fields should be readonly instead of completely hidden
☐ alternate languages ☐ alternate languages
☐ log hba1c ☐ log hba1c
☐ add recipe calculator
Reports: Reports:
☐ evaluate what type of reports there should be ☐ evaluate what type of reports there should be
Log Overview: Log Overview:
☐ add pagination ☐ add pagination
☐ add filters
Log Entry: Log Entry:
☐ check if there is still an active bolus when suggesting glucose bolus ☐ check if there is still an active bolus when suggesting glucose bolus
Event Types: Event Types:
@ -58,8 +58,15 @@ FUTURE TASKS:
☐ implement reminders as push notifications ☐ implement reminders as push notifications
Settings: Settings:
☐ add option to hide extra customization options (ie. changing pre calculated values)? ☐ add option to hide extra customization options (ie. changing pre calculated values)?
☐ option to switch theme
Archive: 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) ✔ 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) ✔ 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) ✔ add delay to auto conversions @done(21-12-06 20:25) @project(MAIN TASKS.General/Framework)

View File

@ -44,7 +44,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.diameter" applicationId "com.example.diameter"
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName

View File

@ -6,18 +6,46 @@ class AppTheme {
static ThemeData lightTheme = FlexColorScheme.light( static ThemeData lightTheme = FlexColorScheme.light(
surfaceStyle: FlexSurface.medium, surfaceStyle: FlexSurface.medium,
scheme: FlexScheme.mandyRed, scheme: FlexScheme.aquaBlue,
fontFamily: 'RobotoCondensed', fontFamily: 'Roboto',
).toTheme; ).toTheme;
static ThemeData darkTheme = FlexColorScheme.light( static ThemeData darkTheme = FlexColorScheme.dark(
scheme: FlexScheme.mandyRed, scheme: FlexScheme.aquaBlue,
fontFamily: 'RobotoCondensed', fontFamily: 'Roboto',
).toTheme; ).toTheme;
static ThemeData makeTheme(ThemeData baseThemeData) { static ThemeData makeTheme(ThemeData baseThemeData) {
return baseThemeData.copyWith( 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( bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: baseThemeData.primaryColor)); backgroundColor: baseThemeData.primaryColor,
),
);
} }
} }

View File

@ -7,6 +7,7 @@ class AutoCompleteDropdownButton<T> extends StatefulWidget {
final List<T> items; final List<T> items;
final void Function(T? value) onChanged; final void Function(T? value) onChanged;
final List<T> Function(String? value)? applyQuery; final List<T> Function(String? value)? applyQuery;
final TextEditingController controller;
const AutoCompleteDropdownButton( const AutoCompleteDropdownButton(
{Key? key, {Key? key,
@ -14,7 +15,8 @@ class AutoCompleteDropdownButton<T> extends StatefulWidget {
required this.label, required this.label,
required this.items, required this.items,
required this.onChanged, required this.onChanged,
this.applyQuery}) this.applyQuery,
required this.controller})
: super(key: key); : super(key: key);
@override @override
@ -24,10 +26,10 @@ class AutoCompleteDropdownButton<T> extends StatefulWidget {
class _AutoCompleteDropdownButtonState<T> class _AutoCompleteDropdownButtonState<T>
extends State<AutoCompleteDropdownButton<T>> { extends State<AutoCompleteDropdownButton<T>> {
TextEditingController controller = TextEditingController(text: '');
late List<T> options; late List<T> options;
late List<T> suggestions; late List<T> suggestions;
final FocusNode focusNode = FocusNode();
final LayerLink layerLink = LayerLink(); final LayerLink layerLink = LayerLink();
OverlayEntry? entry; OverlayEntry? entry;
bool isOpen = false; bool isOpen = false;
@ -35,25 +37,32 @@ class _AutoCompleteDropdownButtonState<T>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
setState(() { setState(() {
controller.text = widget.selectedItem == null
? ''
: widget.selectedItem!.toString();
options = widget.items; options = widget.items;
suggestions = []; suggestions = [];
}); });
} }
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
void toggleOverlay() { void toggleOverlay() {
isOpen ? hideOverlay() : showOverlay(); isOpen ? hideOverlay() : showOverlay();
} }
void showOverlay() { void showOverlay() {
hideOverlay(); hideOverlay();
focusNode.requestFocus();
List<Widget> items = []; List<Widget> items = [];
Divider? divider; Divider? divider;
ScrollController _scrollController = ScrollController();
final overlay = Overlay.of(context)!; final overlay = Overlay.of(context)!;
final renderBox = context.findRenderObject() as RenderBox; final renderBox = context.findRenderObject() as RenderBox;
@ -79,16 +88,14 @@ class _AutoCompleteDropdownButtonState<T>
items.addAll(options items.addAll(options
.where((item) => .where((item) =>
!(widget.selectedItem != null && !(widget.selectedItem != null &&
item.toString() == item.toString() == widget.selectedItem!.toString()) &&
widget.selectedItem!.toString()) &&
!suggestions.contains(item)) !suggestions.contains(item))
.map((item) => buildListTile(item)) .map((item) => buildListTile(item))
.toList()); .toList());
final screenHeight = MediaQuery.of(context).size.height; final screenHeight = MediaQuery.of(context).size.height;
final neededHeight = final neededHeight = renderBox.size.height * (items.length - 1) +
renderBox.size.height * (items.length - 1) + (divider?.height ?? (divider?.height ?? renderBox.size.height);
renderBox.size.height);
final availableHeight = screenHeight - final availableHeight = screenHeight -
(renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height); (renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height);
bool displayAbove = neededHeight > availableHeight && bool displayAbove = neededHeight > availableHeight &&
@ -107,11 +114,16 @@ class _AutoCompleteDropdownButtonState<T>
displayAbove ? Alignment.bottomLeft : Alignment.topLeft, displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)), offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)),
showWhenUnlinked: false, showWhenUnlinked: false,
child: Material( child: Scrollbar(
elevation: 8, controller: _scrollController,
child: SingleChildScrollView( isAlwaysShown: true,
child: Column( child: Material(
children: items, elevation: 8,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: items,
),
), ),
), ),
), ),
@ -139,13 +151,14 @@ class _AutoCompleteDropdownButtonState<T>
void hideOverlay() { void hideOverlay() {
entry?.remove(); entry?.remove();
entry = null; setState(() {
isOpen = false; entry = null;
isOpen = false;
});
} }
void handleChanged(item) { void handleChanged(T? item) {
widget.onChanged(item); widget.onChanged(item);
controller.text = item.toString();
hideOverlay(); hideOverlay();
} }
@ -178,17 +191,37 @@ class _AutoCompleteDropdownButtonState<T>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CompositedTransformTarget( return Focus(
link: layerLink, focusNode: focusNode,
child: TextFormField( onFocusChange: (isFocused) {
onChanged: onChangeQuery, if (!isFocused) {
onTap: toggleOverlay, hideOverlay();
controller: controller, }
decoration: InputDecoration( },
labelText: widget.label, child: CompositedTransformTarget(
suffixIcon: IconButton( link: layerLink,
onPressed: toggleOverlay, child: TextFormField(
icon: const Icon(Icons.arrow_drop_down), 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),
),
],
),
), ),
), ),
), ),

View File

@ -23,11 +23,11 @@ class _FormWrapperState extends State<FormWrapper> {
children: [ children: [
Column( Column(
children: widget.fields children: widget.fields
?.map((e) => Padding( ?.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0), padding: const EdgeInsets.symmetric(vertical: 5.0),
child: e)) child: e))
.toList() ?? .toList() ??
[], [],
), ),
Container( Container(
padding: const EdgeInsets.only(top: 10.0), padding: const EdgeInsets.only(top: 10.0),

1
lib/config.dart Normal file
View File

@ -0,0 +1 @@
String secret = 'm4Gwehzgv18jZ5gCVUBZl5li3Z0FX2Yb';

View File

@ -1,4 +1,6 @@
import 'package:diameter/components/app_theme.dart'; 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/object_box.dart';
import 'package:diameter/screens/accuracy_detail.dart'; import 'package:diameter/screens/accuracy_detail.dart';
import 'package:diameter/screens/basal/basal_profile_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/basal/basal_profile_list.dart';
import 'package:diameter/screens/bolus/bolus_profile_list.dart'; import 'package:diameter/screens/bolus/bolus_profile_list.dart';
import 'package:diameter/navigation.dart'; import 'package:diameter/navigation.dart';
import 'package:objectbox/objectbox.dart';
late ObjectBox objectBox; late ObjectBox objectBox;
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
objectBox = await ObjectBox.create(); 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( runApp(
MaterialApp( GestureDetector(
theme: AppTheme.makeTheme(AppTheme.lightTheme), onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
darkTheme: AppTheme.makeTheme(AppTheme.darkTheme), child: MaterialApp(
themeMode: ThemeMode.system, theme: AppTheme.makeTheme(AppTheme.lightTheme),
initialRoute: '/', darkTheme: AppTheme.makeTheme(AppTheme.darkTheme),
routes: { themeMode: Settings.themeMode,
'/': (context) => const LogScreen(), initialRoute: '/',
Routes.log: (context) => const LogScreen(), routes: {
Routes.logEntry: (context) => const LogEntryScreen(), '/': (context) => const LogScreen(),
Routes.logEvent: (context) => const LogEventDetailScreen(), Routes.log: (context) => const LogScreen(),
Routes.logEventTypes: (context) => const LogEventTypeListScreen(), Routes.logEntry: (context) => const LogEntryScreen(),
Routes.logEventType: (context) => const EventTypeDetailScreen(), Routes.logEvent: (context) => const LogEventDetailScreen(),
Routes.events: (context) => const LogEventListScreen(), Routes.logEventTypes: (context) => const LogEventTypeListScreen(),
Routes.accuracies: (context) => const AccuracyListScreen(), Routes.logEventType: (context) => const EventTypeDetailScreen(),
Routes.accuracy: (context) => const AccuracyDetailScreen(), Routes.events: (context) => const LogEventListScreen(),
Routes.meals: (context) => const MealListScreen(), Routes.accuracies: (context) => const AccuracyListScreen(),
Routes.meal: (context) => const MealDetailScreen(), Routes.accuracy: (context) => const AccuracyDetailScreen(),
Routes.mealCategories: (context) => const MealCategoryListScreen(), Routes.meals: (context) => const MealListScreen(),
Routes.mealCategory: (context) => const MealCategoryDetailScreen(), Routes.meal: (context) => const MealDetailScreen(),
Routes.mealPortionTypes: (context) => const MealPortionTypeListScreen(), Routes.mealCategories: (context) => const MealCategoryListScreen(),
Routes.mealPortionType: (context) => Routes.mealCategory: (context) => const MealCategoryDetailScreen(),
const MealPortionTypeDetailScreen(), Routes.mealPortionTypes: (context) =>
Routes.mealSources: (context) => const MealSourceListScreen(), const MealPortionTypeListScreen(),
Routes.mealSource: (context) => const MealSourceDetailScreen(), Routes.mealPortionType: (context) =>
Routes.bolusProfiles: (context) => const BolusProfileListScreen(), const MealPortionTypeDetailScreen(),
Routes.bolusProfile: (context) => const BolusProfileDetailScreen(), Routes.mealSources: (context) => const MealSourceListScreen(),
Routes.basalProfiles: (context) => const BasalProfileListScreen(), Routes.mealSource: (context) => const MealSourceDetailScreen(),
Routes.basalProfile: (context) => const BasalProfileDetailScreen(), Routes.bolusProfiles: (context) => const BolusProfileListScreen(),
Routes.settings: (context) => const SettingsScreen(), Routes.bolusProfile: (context) => const BolusProfileDetailScreen(),
}, Routes.basalProfiles: (context) => const BasalProfileListScreen(),
Routes.basalProfile: (context) => const BasalProfileDetailScreen(),
Routes.settings: (context) => const SettingsScreen(),
},
),
), ),
); );
} }

View File

@ -3,6 +3,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show Accuracy_; import 'package:diameter/objectbox.g.dart' show Accuracy_;
@Entity(uid: 291512798403320400) @Entity(uid: 291512798403320400)
@Sync()
class Accuracy { class Accuracy {
static final Box<Accuracy> box = objectBox.store.box<Accuracy>(); static final Box<Accuracy> box = objectBox.store.box<Accuracy>();

View File

@ -5,6 +5,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show Basal_, BasalProfile_; import 'package:diameter/objectbox.g.dart' show Basal_, BasalProfile_;
@Entity(uid: 1467758525778521891) @Entity(uid: 1467758525778521891)
@Sync()
class Basal { class Basal {
static final Box<Basal> box = objectBox.store.box<Basal>(); static final Box<Basal> box = objectBox.store.box<Basal>();

View File

@ -4,6 +4,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show BasalProfile_; import 'package:diameter/objectbox.g.dart' show BasalProfile_;
@Entity(uid: 3613736032926903785) @Entity(uid: 3613736032926903785)
@Sync()
class BasalProfile { class BasalProfile {
static final Box<BasalProfile> box = objectBox.store.box<BasalProfile>(); static final Box<BasalProfile> box = objectBox.store.box<BasalProfile>();

View File

@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show Bolus_, BolusProfile_; import 'package:diameter/objectbox.g.dart' show Bolus_, BolusProfile_;
@Entity(uid: 3417770529060202389) @Entity(uid: 3417770529060202389)
@Sync()
class Bolus { class Bolus {
static final Box<Bolus> box = objectBox.store.box<Bolus>(); static final Box<Bolus> box = objectBox.store.box<Bolus>();

View File

@ -4,6 +4,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show BolusProfile_; import 'package:diameter/objectbox.g.dart' show BolusProfile_;
@Entity(uid: 8812452529027052317) @Entity(uid: 8812452529027052317)
@Sync()
class BolusProfile { class BolusProfile {
static final Box<BolusProfile> box = objectBox.store.box<BolusProfile>(); static final Box<BolusProfile> box = objectBox.store.box<BolusProfile>();

View File

@ -5,6 +5,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show GlucoseTarget_; import 'package:diameter/objectbox.g.dart' show GlucoseTarget_;
@Entity(uid: 5041265995704044399) @Entity(uid: 5041265995704044399)
@Sync()
class GlucoseTarget { class GlucoseTarget {
static final Box<GlucoseTarget> box = objectBox.store.box<GlucoseTarget>(); static final Box<GlucoseTarget> box = objectBox.store.box<GlucoseTarget>();

View File

@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show LogBolus_, LogEntry_; import 'package:diameter/objectbox.g.dart' show LogBolus_, LogEntry_;
@Entity(uid: 8033487006694871160) @Entity(uid: 8033487006694871160)
@Sync()
class LogBolus { class LogBolus {
static final Box<LogBolus> box = objectBox.store.box<LogBolus>(); static final Box<LogBolus> box = objectBox.store.box<LogBolus>();

View File

@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show LogEntry_; import 'package:diameter/objectbox.g.dart' show LogEntry_;
@Entity(uid: 752131069307970560) @Entity(uid: 752131069307970560)
@Sync()
class LogEntry { class LogEntry {
static final Box<LogEntry> box = objectBox.store.box<LogEntry>(); static final Box<LogEntry> box = objectBox.store.box<LogEntry>();
@ -16,6 +17,7 @@ class LogEntry {
DateTime time; DateTime time;
int? mgPerDl; int? mgPerDl;
double? mmolPerL; double? mmolPerL;
double? glucoseTrend;
String? notes; String? notes;
// constructor // constructor
@ -25,6 +27,7 @@ class LogEntry {
required this.time, required this.time,
this.mgPerDl, this.mgPerDl,
this.mmolPerL, this.mmolPerL,
this.glucoseTrend,
this.notes, this.notes,
}); });
@ -43,8 +46,8 @@ class LogEntry {
static bool hasUncorrectedGlucose(int id) { static bool hasUncorrectedGlucose(int id) {
final entry = box.get(id); final entry = box.get(id);
if (((entry?.mgPerDl ?? 0) > Settings.targetMgPerDl() || if (((entry?.mgPerDl ?? 0) > Settings.targetMgPerDl ||
(entry?.mmolPerL ?? 0) > Settings.targetMmolPerL())) { (entry?.mmolPerL ?? 0) > Settings.targetMmolPerL)) {
return !LogBolus.glucoseBolusForEntryExists(id); return !LogBolus.glucoseBolusForEntryExists(id);
} }
return false; return false;

View File

@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show LogEvent_, LogEventType_; import 'package:diameter/objectbox.g.dart' show LogEvent_, LogEventType_;
@Entity(uid: 4303325892753185970) @Entity(uid: 4303325892753185970)
@Sync()
class LogEvent { class LogEvent {
static final Box<LogEvent> box = objectBox.store.box<LogEvent>(); static final Box<LogEvent> box = objectBox.store.box<LogEvent>();

View File

@ -5,6 +5,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show LogEventType_; import 'package:diameter/objectbox.g.dart' show LogEventType_;
@Entity(uid: 8362795406595606110) @Entity(uid: 8362795406595606110)
@Sync()
class LogEventType { class LogEventType {
static final Box<LogEventType> box = objectBox.store.box<LogEventType>(); static final Box<LogEventType> box = objectBox.store.box<LogEventType>();

View File

@ -9,6 +9,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show LogMeal_, LogEntry_; import 'package:diameter/objectbox.g.dart' show LogMeal_, LogEntry_;
@Entity(uid: 411177866700467286) @Entity(uid: 411177866700467286)
@Sync()
class LogMeal { class LogMeal {
static final Box<LogMeal> box = objectBox.store.box<LogMeal>(); static final Box<LogMeal> box = objectBox.store.box<LogMeal>();

View File

@ -9,6 +9,7 @@ import 'package:objectbox/objectbox.dart';
enum PortionCarbsParameter { carbsRatio, portionSize, carbsPerPortion } enum PortionCarbsParameter { carbsRatio, portionSize, carbsPerPortion }
@Entity(uid: 382130101578692012) @Entity(uid: 382130101578692012)
@Sync()
class Meal { class Meal {
static final Box<Meal> box = objectBox.store.box<Meal>(); static final Box<Meal> box = objectBox.store.box<Meal>();

View File

@ -3,6 +3,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show MealCategory_; import 'package:diameter/objectbox.g.dart' show MealCategory_;
@Entity(uid: 3158200688796904913) @Entity(uid: 3158200688796904913)
@Sync()
class MealCategory { class MealCategory {
static final Box<MealCategory> box = objectBox.store.box<MealCategory>(); static final Box<MealCategory> box = objectBox.store.box<MealCategory>();

View File

@ -3,6 +3,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show MealPortionType_; import 'package:diameter/objectbox.g.dart' show MealPortionType_;
@Entity(uid: 2111511899235985637) @Entity(uid: 2111511899235985637)
@Sync()
class MealPortionType { class MealPortionType {
static final Box<MealPortionType> box = objectBox.store.box<MealPortionType>(); static final Box<MealPortionType> box = objectBox.store.box<MealPortionType>();

View File

@ -6,6 +6,7 @@ import 'package:objectbox/objectbox.dart';
import 'package:diameter/objectbox.g.dart' show MealSource_; import 'package:diameter/objectbox.g.dart' show MealSource_;
@Entity(uid: 1283034494527412242) @Entity(uid: 1283034494527412242)
@Sync()
class MealSource { class MealSource {
static final Box<MealSource> box = objectBox.store.box<MealSource>(); static final Box<MealSource> box = objectBox.store.box<MealSource>();

View File

@ -1,4 +1,5 @@
import 'package:diameter/main.dart'; import 'package:diameter/main.dart';
import 'package:flutter/material.dart';
import 'package:objectbox/objectbox.dart'; import 'package:objectbox/objectbox.dart';
enum GlucoseDisplayMode { activeOnly, bothForList, bothForDetail, both } enum GlucoseDisplayMode { activeOnly, bothForList, bothForDetail, both }
@ -39,14 +40,18 @@ List<String> nutritionMeasurementLabels = [
]; ];
@Entity(uid: 3989341091218179227) @Entity(uid: 3989341091218179227)
@Sync()
class Settings { class Settings {
static final Box<Settings> box = objectBox.store.box<Settings>(); static final Box<Settings> box = objectBox.store.box<Settings>();
// properties // properties
int id; int id;
int nutritionMeasurementIndex; int nutritionMeasurementIndex;
int glucoseDisplayModeIndex; int glucoseDisplayModeIndex;
int glucoseMeasurementIndex; int glucoseMeasurementIndex;
int targetGlucoseMgPerDl;
double targetGlucoseMmolPerL;
String dateFormat; String dateFormat;
String? longDateFormat; String? longDateFormat;
@ -57,8 +62,7 @@ class Settings {
bool showConfirmationDialogOnDelete; bool showConfirmationDialogOnDelete;
bool showConfirmationDialogOnStopEvent; bool showConfirmationDialogOnStopEvent;
int targetGlucoseMgPerDl; bool useDarkTheme;
double targetGlucoseMmolPerL;
// constructor // constructor
Settings({ Settings({
@ -75,6 +79,7 @@ class Settings {
this.showConfirmationDialogOnStopEvent = true, this.showConfirmationDialogOnStopEvent = true,
this.targetGlucoseMgPerDl = 100, this.targetGlucoseMgPerDl = 100,
this.targetGlucoseMmolPerL = 5.49, this.targetGlucoseMmolPerL = 5.49,
this.useDarkTheme = false,
}); });
// methods // methods
@ -97,13 +102,16 @@ class Settings {
static String get glucoseMeasurementSuffix => static String get glucoseMeasurementSuffix =>
glucoseMeasurementSuffixes[get().glucoseMeasurementIndex]; glucoseMeasurementSuffixes[get().glucoseMeasurementIndex];
static int targetMgPerDl() => get().targetGlucoseMgPerDl; static int get targetMgPerDl => get().targetGlucoseMgPerDl;
static double targetMmolPerL() => get().targetGlucoseMmolPerL; 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 put(Settings settings) => box.put(settings);
static void reset() { static void reset() {
box.removeAll(); box.removeAll();
box.put(Settings()); box.put(Settings(useDarkTheme: ThemeMode.system == ThemeMode.dark));
} }
} }

View File

@ -7,6 +7,7 @@
"id": "2:1467758525778521891", "id": "2:1467758525778521891",
"lastPropertyId": "6:3409466778841164684", "lastPropertyId": "6:3409466778841164684",
"name": "Basal", "name": "Basal",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:4281816825522738642", "id": "1:4281816825522738642",
@ -49,6 +50,7 @@
"id": "3:3613736032926903785", "id": "3:3613736032926903785",
"lastPropertyId": "5:8140071977687660397", "lastPropertyId": "5:8140071977687660397",
"name": "BasalProfile", "name": "BasalProfile",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:353771983641472117", "id": "1:353771983641472117",
@ -83,6 +85,7 @@
"id": "4:3417770529060202389", "id": "4:3417770529060202389",
"lastPropertyId": "9:7440090146687096977", "lastPropertyId": "9:7440090146687096977",
"name": "Bolus", "name": "Bolus",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:8141647919190345775", "id": "1:8141647919190345775",
@ -140,6 +143,7 @@
"id": "5:8812452529027052317", "id": "5:8812452529027052317",
"lastPropertyId": "5:8082994824481464395", "lastPropertyId": "5:8082994824481464395",
"name": "BolusProfile", "name": "BolusProfile",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:4233863196673391978", "id": "1:4233863196673391978",
@ -172,8 +176,9 @@
}, },
{ {
"id": "6:752131069307970560", "id": "6:752131069307970560",
"lastPropertyId": "9:1692732373071965573", "lastPropertyId": "10:2505303363495348118",
"name": "LogEntry", "name": "LogEntry",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:5528657304180237933", "id": "1:5528657304180237933",
@ -205,6 +210,11 @@
"id": "9:1692732373071965573", "id": "9:1692732373071965573",
"name": "deleted", "name": "deleted",
"type": 1 "type": 1
},
{
"id": "10:2505303363495348118",
"name": "glucoseTrend",
"type": 8
} }
], ],
"relations": [] "relations": []
@ -213,6 +223,7 @@
"id": "7:4303325892753185970", "id": "7:4303325892753185970",
"lastPropertyId": "12:3041952167628926163", "lastPropertyId": "12:3041952167628926163",
"name": "LogEvent", "name": "LogEvent",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:6648501734758557663", "id": "1:6648501734758557663",
@ -281,6 +292,7 @@
"id": "8:8362795406595606110", "id": "8:8362795406595606110",
"lastPropertyId": "8:1869014400856897151", "lastPropertyId": "8:1869014400856897151",
"name": "LogEventType", "name": "LogEventType",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:1430413826199774000", "id": "1:1430413826199774000",
@ -336,6 +348,7 @@
"id": "9:411177866700467286", "id": "9:411177866700467286",
"lastPropertyId": "17:7341439841011629937", "lastPropertyId": "17:7341439841011629937",
"name": "LogMeal", "name": "LogMeal",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:962999525294133158", "id": "1:962999525294133158",
@ -441,6 +454,7 @@
"id": "10:382130101578692012", "id": "10:382130101578692012",
"lastPropertyId": "15:8283810711091063880", "lastPropertyId": "15:8283810711091063880",
"name": "Meal", "name": "Meal",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:612386612600420389", "id": "1:612386612600420389",
@ -535,6 +549,7 @@
"id": "11:3158200688796904913", "id": "11:3158200688796904913",
"lastPropertyId": "4:824435977543069541", "lastPropertyId": "4:824435977543069541",
"name": "MealCategory", "name": "MealCategory",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:3678943122076184840", "id": "1:3678943122076184840",
@ -564,6 +579,7 @@
"id": "12:2111511899235985637", "id": "12:2111511899235985637",
"lastPropertyId": "4:5680236937391945907", "lastPropertyId": "4:5680236937391945907",
"name": "MealPortionType", "name": "MealPortionType",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:65428405312238271", "id": "1:65428405312238271",
@ -593,6 +609,7 @@
"id": "13:1283034494527412242", "id": "13:1283034494527412242",
"lastPropertyId": "8:4547899751779962180", "lastPropertyId": "8:4547899751779962180",
"name": "MealSource", "name": "MealSource",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:7205380295259922130", "id": "1:7205380295259922130",
@ -654,6 +671,7 @@
"id": "14:8033487006694871160", "id": "14:8033487006694871160",
"lastPropertyId": "18:7503231998671134983", "lastPropertyId": "18:7503231998671134983",
"name": "LogBolus", "name": "LogBolus",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:8254237730262024662", "id": "1:8254237730262024662",
@ -752,6 +770,7 @@
"id": "15:291512798403320400", "id": "15:291512798403320400",
"lastPropertyId": "7:6675647182186603076", "lastPropertyId": "7:6675647182186603076",
"name": "Accuracy", "name": "Accuracy",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:8405388350474524599", "id": "1:8405388350474524599",
@ -794,8 +813,9 @@
}, },
{ {
"id": "16:3989341091218179227", "id": "16:3989341091218179227",
"lastPropertyId": "22:3595473653451456068", "lastPropertyId": "23:3611447442844013652",
"name": "Settings", "name": "Settings",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:7803753645747063723", "id": "1:7803753645747063723",
@ -862,6 +882,11 @@
"id": "22:3595473653451456068", "id": "22:3595473653451456068",
"name": "targetGlucoseMmolPerL", "name": "targetGlucoseMmolPerL",
"type": 8 "type": 8
},
{
"id": "23:3611447442844013652",
"name": "useDarkTheme",
"type": 1
} }
], ],
"relations": [] "relations": []
@ -870,6 +895,7 @@
"id": "17:5041265995704044399", "id": "17:5041265995704044399",
"lastPropertyId": "7:1333487551279074696", "lastPropertyId": "7:1333487551279074696",
"name": "GlucoseTarget", "name": "GlucoseTarget",
"flags": 2,
"properties": [ "properties": [
{ {
"id": "1:4322960567133959537", "id": "1:4322960567133959537",

View File

@ -7,7 +7,7 @@ import 'dart:typed_data';
import 'package:objectbox/flatbuffers/flat_buffers.dart' as fb; import 'package:objectbox/flatbuffers/flat_buffers.dart' as fb;
import 'package:objectbox/internal.dart'; // generated code can access "internal" functionality import 'package:objectbox/internal.dart'; // generated code can access "internal" functionality
import 'package:objectbox/objectbox.dart'; 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/accuracy.dart';
import 'models/basal.dart'; import 'models/basal.dart';
@ -33,7 +33,7 @@ final _entities = <ModelEntity>[
id: const IdUid(2, 1467758525778521891), id: const IdUid(2, 1467758525778521891),
name: 'Basal', name: 'Basal',
lastPropertyId: const IdUid(6, 3409466778841164684), lastPropertyId: const IdUid(6, 3409466778841164684),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 4281816825522738642), id: const IdUid(1, 4281816825522738642),
@ -74,7 +74,7 @@ final _entities = <ModelEntity>[
id: const IdUid(3, 3613736032926903785), id: const IdUid(3, 3613736032926903785),
name: 'BasalProfile', name: 'BasalProfile',
lastPropertyId: const IdUid(5, 8140071977687660397), lastPropertyId: const IdUid(5, 8140071977687660397),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 353771983641472117), id: const IdUid(1, 353771983641472117),
@ -108,7 +108,7 @@ final _entities = <ModelEntity>[
id: const IdUid(4, 3417770529060202389), id: const IdUid(4, 3417770529060202389),
name: 'Bolus', name: 'Bolus',
lastPropertyId: const IdUid(9, 7440090146687096977), lastPropertyId: const IdUid(9, 7440090146687096977),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 8141647919190345775), id: const IdUid(1, 8141647919190345775),
@ -164,7 +164,7 @@ final _entities = <ModelEntity>[
id: const IdUid(5, 8812452529027052317), id: const IdUid(5, 8812452529027052317),
name: 'BolusProfile', name: 'BolusProfile',
lastPropertyId: const IdUid(5, 8082994824481464395), lastPropertyId: const IdUid(5, 8082994824481464395),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 4233863196673391978), id: const IdUid(1, 4233863196673391978),
@ -197,8 +197,8 @@ final _entities = <ModelEntity>[
ModelEntity( ModelEntity(
id: const IdUid(6, 752131069307970560), id: const IdUid(6, 752131069307970560),
name: 'LogEntry', name: 'LogEntry',
lastPropertyId: const IdUid(9, 1692732373071965573), lastPropertyId: const IdUid(10, 2505303363495348118),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 5528657304180237933), id: const IdUid(1, 5528657304180237933),
@ -229,6 +229,11 @@ final _entities = <ModelEntity>[
id: const IdUid(9, 1692732373071965573), id: const IdUid(9, 1692732373071965573),
name: 'deleted', name: 'deleted',
type: 1, type: 1,
flags: 0),
ModelProperty(
id: const IdUid(10, 2505303363495348118),
name: 'glucoseTrend',
type: 8,
flags: 0) flags: 0)
], ],
relations: <ModelRelation>[], relations: <ModelRelation>[],
@ -237,7 +242,7 @@ final _entities = <ModelEntity>[
id: const IdUid(7, 4303325892753185970), id: const IdUid(7, 4303325892753185970),
name: 'LogEvent', name: 'LogEvent',
lastPropertyId: const IdUid(12, 3041952167628926163), lastPropertyId: const IdUid(12, 3041952167628926163),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 6648501734758557663), id: const IdUid(1, 6648501734758557663),
@ -302,7 +307,7 @@ final _entities = <ModelEntity>[
id: const IdUid(8, 8362795406595606110), id: const IdUid(8, 8362795406595606110),
name: 'LogEventType', name: 'LogEventType',
lastPropertyId: const IdUid(8, 1869014400856897151), lastPropertyId: const IdUid(8, 1869014400856897151),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 1430413826199774000), id: const IdUid(1, 1430413826199774000),
@ -355,7 +360,7 @@ final _entities = <ModelEntity>[
id: const IdUid(9, 411177866700467286), id: const IdUid(9, 411177866700467286),
name: 'LogMeal', name: 'LogMeal',
lastPropertyId: const IdUid(17, 7341439841011629937), lastPropertyId: const IdUid(17, 7341439841011629937),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 962999525294133158), id: const IdUid(1, 962999525294133158),
@ -453,7 +458,7 @@ final _entities = <ModelEntity>[
id: const IdUid(10, 382130101578692012), id: const IdUid(10, 382130101578692012),
name: 'Meal', name: 'Meal',
lastPropertyId: const IdUid(15, 8283810711091063880), lastPropertyId: const IdUid(15, 8283810711091063880),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 612386612600420389), id: const IdUid(1, 612386612600420389),
@ -542,7 +547,7 @@ final _entities = <ModelEntity>[
id: const IdUid(11, 3158200688796904913), id: const IdUid(11, 3158200688796904913),
name: 'MealCategory', name: 'MealCategory',
lastPropertyId: const IdUid(4, 824435977543069541), lastPropertyId: const IdUid(4, 824435977543069541),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 3678943122076184840), id: const IdUid(1, 3678943122076184840),
@ -571,7 +576,7 @@ final _entities = <ModelEntity>[
id: const IdUid(12, 2111511899235985637), id: const IdUid(12, 2111511899235985637),
name: 'MealPortionType', name: 'MealPortionType',
lastPropertyId: const IdUid(4, 5680236937391945907), lastPropertyId: const IdUid(4, 5680236937391945907),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 65428405312238271), id: const IdUid(1, 65428405312238271),
@ -600,7 +605,7 @@ final _entities = <ModelEntity>[
id: const IdUid(13, 1283034494527412242), id: const IdUid(13, 1283034494527412242),
name: 'MealSource', name: 'MealSource',
lastPropertyId: const IdUid(8, 4547899751779962180), lastPropertyId: const IdUid(8, 4547899751779962180),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 7205380295259922130), id: const IdUid(1, 7205380295259922130),
@ -657,7 +662,7 @@ final _entities = <ModelEntity>[
id: const IdUid(14, 8033487006694871160), id: const IdUid(14, 8033487006694871160),
name: 'LogBolus', name: 'LogBolus',
lastPropertyId: const IdUid(18, 7503231998671134983), lastPropertyId: const IdUid(18, 7503231998671134983),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 8254237730262024662), id: const IdUid(1, 8254237730262024662),
@ -752,7 +757,7 @@ final _entities = <ModelEntity>[
id: const IdUid(15, 291512798403320400), id: const IdUid(15, 291512798403320400),
name: 'Accuracy', name: 'Accuracy',
lastPropertyId: const IdUid(7, 6675647182186603076), lastPropertyId: const IdUid(7, 6675647182186603076),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 8405388350474524599), id: const IdUid(1, 8405388350474524599),
@ -795,8 +800,8 @@ final _entities = <ModelEntity>[
ModelEntity( ModelEntity(
id: const IdUid(16, 3989341091218179227), id: const IdUid(16, 3989341091218179227),
name: 'Settings', name: 'Settings',
lastPropertyId: const IdUid(22, 3595473653451456068), lastPropertyId: const IdUid(23, 3611447442844013652),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 7803753645747063723), id: const IdUid(1, 7803753645747063723),
@ -862,6 +867,11 @@ final _entities = <ModelEntity>[
id: const IdUid(22, 3595473653451456068), id: const IdUid(22, 3595473653451456068),
name: 'targetGlucoseMmolPerL', name: 'targetGlucoseMmolPerL',
type: 8, type: 8,
flags: 0),
ModelProperty(
id: const IdUid(23, 3611447442844013652),
name: 'useDarkTheme',
type: 1,
flags: 0) flags: 0)
], ],
relations: <ModelRelation>[], relations: <ModelRelation>[],
@ -870,7 +880,7 @@ final _entities = <ModelEntity>[
id: const IdUid(17, 5041265995704044399), id: const IdUid(17, 5041265995704044399),
name: 'GlucoseTarget', name: 'GlucoseTarget',
lastPropertyId: const IdUid(7, 1333487551279074696), lastPropertyId: const IdUid(7, 1333487551279074696),
flags: 0, flags: 2,
properties: <ModelProperty>[ properties: <ModelProperty>[
ModelProperty( ModelProperty(
id: const IdUid(1, 4322960567133959537), id: const IdUid(1, 4322960567133959537),
@ -1143,13 +1153,14 @@ ModelDefinition getObjectBoxModel() {
objectToFB: (LogEntry object, fb.Builder fbb) { objectToFB: (LogEntry object, fb.Builder fbb) {
final notesOffset = final notesOffset =
object.notes == null ? null : fbb.writeString(object.notes!); object.notes == null ? null : fbb.writeString(object.notes!);
fbb.startTable(10); fbb.startTable(11);
fbb.addInt64(0, object.id); fbb.addInt64(0, object.id);
fbb.addInt64(1, object.time.millisecondsSinceEpoch); fbb.addInt64(1, object.time.millisecondsSinceEpoch);
fbb.addInt64(2, object.mgPerDl); fbb.addInt64(2, object.mgPerDl);
fbb.addFloat64(3, object.mmolPerL); fbb.addFloat64(3, object.mmolPerL);
fbb.addOffset(7, notesOffset); fbb.addOffset(7, notesOffset);
fbb.addBool(8, object.deleted); fbb.addBool(8, object.deleted);
fbb.addFloat64(9, object.glucoseTrend);
fbb.finish(fbb.endTable()); fbb.finish(fbb.endTable());
return object.id; return object.id;
}, },
@ -1167,6 +1178,8 @@ ModelDefinition getObjectBoxModel() {
.vTableGetNullable(buffer, rootOffset, 8), .vTableGetNullable(buffer, rootOffset, 8),
mmolPerL: const fb.Float64Reader() mmolPerL: const fb.Float64Reader()
.vTableGetNullable(buffer, rootOffset, 10), .vTableGetNullable(buffer, rootOffset, 10),
glucoseTrend: const fb.Float64Reader()
.vTableGetNullable(buffer, rootOffset, 22),
notes: const fb.StringReader() notes: const fb.StringReader()
.vTableGetNullable(buffer, rootOffset, 18)); .vTableGetNullable(buffer, rootOffset, 18));
@ -1694,7 +1707,7 @@ ModelDefinition getObjectBoxModel() {
final longTimeFormatOffset = object.longTimeFormat == null final longTimeFormatOffset = object.longTimeFormat == null
? null ? null
: fbb.writeString(object.longTimeFormat!); : fbb.writeString(object.longTimeFormat!);
fbb.startTable(23); fbb.startTable(24);
fbb.addInt64(0, object.id); fbb.addInt64(0, object.id);
fbb.addOffset(1, dateFormatOffset); fbb.addOffset(1, dateFormatOffset);
fbb.addOffset(2, longDateFormatOffset); fbb.addOffset(2, longDateFormatOffset);
@ -1708,6 +1721,7 @@ ModelDefinition getObjectBoxModel() {
fbb.addInt64(19, object.glucoseMeasurementIndex); fbb.addInt64(19, object.glucoseMeasurementIndex);
fbb.addInt64(20, object.targetGlucoseMgPerDl); fbb.addInt64(20, object.targetGlucoseMgPerDl);
fbb.addFloat64(21, object.targetGlucoseMmolPerL); fbb.addFloat64(21, object.targetGlucoseMmolPerL);
fbb.addBool(22, object.useDarkTheme);
fbb.finish(fbb.endTable()); fbb.finish(fbb.endTable());
return object.id; return object.id;
}, },
@ -1740,7 +1754,8 @@ ModelDefinition getObjectBoxModel() {
targetGlucoseMgPerDl: targetGlucoseMgPerDl:
const fb.Int64Reader().vTableGet(buffer, rootOffset, 44, 0), const fb.Int64Reader().vTableGet(buffer, rootOffset, 44, 0),
targetGlucoseMmolPerL: 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; return object;
}), }),
@ -1921,6 +1936,10 @@ class LogEntry_ {
/// see [LogEntry.deleted] /// see [LogEntry.deleted]
static final deleted = static final deleted =
QueryBooleanProperty<LogEntry>(_entities[4].properties[5]); QueryBooleanProperty<LogEntry>(_entities[4].properties[5]);
/// see [LogEntry.glucoseTrend]
static final glucoseTrend =
QueryDoubleProperty<LogEntry>(_entities[4].properties[6]);
} }
/// [LogEvent] entity fields to define ObjectBox queries. /// [LogEvent] entity fields to define ObjectBox queries.
@ -2337,6 +2356,10 @@ class Settings_ {
/// see [Settings.targetGlucoseMmolPerL] /// see [Settings.targetGlucoseMmolPerL]
static final targetGlucoseMmolPerL = static final targetGlucoseMmolPerL =
QueryDoubleProperty<Settings>(_entities[14].properties[12]); QueryDoubleProperty<Settings>(_entities[14].properties[12]);
/// see [Settings.useDarkTheme]
static final useDarkTheme =
QueryBooleanProperty<Settings>(_entities[14].properties[13]);
} }
/// [GlucoseTarget] entity fields to define ObjectBox queries. /// [GlucoseTarget] entity fields to define ObjectBox queries.

View File

@ -22,12 +22,13 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
bool _isSaving = false; bool _isSaving = false;
final GlobalKey<FormState> _accuracyForm = GlobalKey<FormState>(); final GlobalKey<FormState> _accuracyForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
final _valueController = TextEditingController(text: ''); final _valueController = TextEditingController(text: '');
final _confidenceRatingController = TextEditingController(text: ''); final _confidenceRatingController = TextEditingController(text: '');
final _notesController = TextEditingController(text: ''); final _notesController = TextEditingController(text: '');
bool _forCarbsRatio = false; bool _forCarbsRatio = true;
bool _forPortionSize = false; bool _forPortionSize = true;
@override @override
void initState() { void initState() {
@ -43,13 +44,25 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
} }
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_accuracy = Accuracy.get(widget.id); _accuracy = Accuracy.get(widget.id);
}); });
} }
_isNew = _accuracy == null; _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 { void handleSaveAction() async {
@ -67,7 +80,7 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
Accuracy.box.put(accuracy); Accuracy.box.put(accuracy);
Accuracy.reorder( Accuracy.reorder(
accuracy, int.tryParse(_confidenceRatingController.text)); accuracy, int.tryParse(_confidenceRatingController.text));
Navigator.pop(context, '${_isNew ? 'New' : ''} Accuracy saved'); Navigator.pop(context, ['${_isNew ? 'New' : ''} Accuracy saved', accuracy]);
} }
setState(() { setState(() {
_isSaving = false; _isSaving = false;
@ -77,8 +90,8 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
void handleCancelAction() { void handleCancelAction() {
if (Settings.get().showConfirmationDialogOnCancel && if (Settings.get().showConfirmationDialogOnCancel &&
(_isNew && (_isNew &&
(_forCarbsRatio || (!_forCarbsRatio ||
_forPortionSize || !_forPortionSize ||
_valueController.text != '' || _valueController.text != '' ||
int.tryParse(_confidenceRatingController.text) != null || int.tryParse(_confidenceRatingController.text) != null ||
_notesController.text != '')) || _notesController.text != '')) ||
@ -106,61 +119,66 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
title: Text(_isNew ? 'New Accuracy' : _accuracy!.value), title: Text(_isNew ? 'New Accuracy' : _accuracy!.value),
), ),
drawer: const Navigation(currentLocation: AccuracyDetailScreen.routeName), drawer: const Navigation(currentLocation: AccuracyDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _accuracyForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
TextFormField( FormWrapper(
controller: _valueController, formState: _accuracyForm,
decoration: const InputDecoration( fields: [
labelText: 'Name', TextFormField(
controller: _valueController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty name';
}
return null;
},
), ),
validator: (value) { BooleanFormField(
if (value!.trim().isEmpty) { value: _forCarbsRatio,
return 'Empty name'; label: 'for carbs ratio',
} onChanged: (value) {
return null; setState(() {
}, _forCarbsRatio = value;
), });
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(
TextFormField( value: _forPortionSize,
controller: _notesController, label: 'for portion size',
keyboardType: TextInputType.multiline, onChanged: (value) {
decoration: const InputDecoration( setState(() {
labelText: 'Notes', _forPortionSize = value;
alignLabelWithHint: true, });
},
), ),
), 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( bottomNavigationBar: DetailBottomRow(

View File

@ -16,6 +16,8 @@ class AccuracyListScreen extends StatefulWidget {
class _AccuracyListScreenState extends State<AccuracyListScreen> { class _AccuracyListScreenState extends State<AccuracyListScreen> {
List<Accuracy> _accuracies = Accuracy.getAll(); List<Accuracy> _accuracies = Accuracy.getAll();
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -84,70 +86,78 @@ class _AccuracyListScreenState extends State<AccuracyListScreen> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: _accuracies.isNotEmpty child: _accuracies.isNotEmpty
? ReorderableListView.builder( ? Scrollbar(
padding: const EdgeInsets.only(top: 10.0), controller: _scrollController,
itemCount: _accuracies.length, child: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) { padding: const EdgeInsets.all(10.0),
Accuracy.reorder(_accuracies[oldIndex], newIndex); scrollController: _scrollController,
reload(); itemCount: _accuracies.length,
}, onReorder: (oldIndex, newIndex) {
itemBuilder: (context, index) { Accuracy.reorder(_accuracies[oldIndex], newIndex);
final accuracy = _accuracies[index]; reload();
return ListTile( },
key: Key(index.toString()), itemBuilder: (context, index) {
onTap: () { final accuracy = _accuracies[index];
Navigator.push( return Card(
context, key: Key(accuracy.id.toString()),
MaterialPageRoute( child: ListTile(
builder: (context) => onTap: () {
AccuracyDetailScreen(id: accuracy.id), 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)); leading: Row(
}, mainAxisSize: MainAxisSize.min,
title: Text(accuracy.value), children: const [
leading: Row( Icon(Icons.reorder),
mainAxisSize: MainAxisSize.min, ],
children: const [
Icon(Icons.reorder),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
Icons.square_foot,
color: accuracy.forPortionSize
? Theme.of(context)
.toggleableActiveColor
: Theme.of(context).highlightColor,
),
onPressed: () =>
handleToggleForPortionSizeAction(
accuracy),
), ),
IconButton( trailing: Row(
icon: Icon( mainAxisSize: MainAxisSize.min,
Icons.pie_chart, children: [
color: accuracy.forCarbsRatio IconButton(
? Theme.of(context) icon: Icon(
.toggleableActiveColor Icons.square_foot,
: Theme.of(context).highlightColor, color: accuracy.forPortionSize
), ? Theme.of(context)
onPressed: () => .toggleableActiveColor
handleToggleForCarbsRatioAction( : Theme.of(context).highlightColor,
accuracy), ),
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( : const Center(
child: Text('You have not created any Accuracies yet!'), child: Text('You have not created any Accuracies yet!'),
), ),
@ -161,7 +171,7 @@ class _AccuracyListScreenState extends State<AccuracyListScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const AccuracyDetailScreen(), builder: (context) => const AccuracyDetailScreen(),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),

View File

@ -34,6 +34,7 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
bool _isSaving = false; bool _isSaving = false;
final GlobalKey<FormState> _basalForm = GlobalKey<FormState>(); final GlobalKey<FormState> _basalForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0);
TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0);
@ -64,13 +65,25 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
updateEndTime(); updateEndTime();
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_basal = Basal.get(widget.id); _basal = Basal.get(widget.id);
}); });
} }
_isNew = _basal == null; _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() { void updateStartTime() {
@ -141,7 +154,7 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
); );
basal.basalProfile.targetId = widget.basalProfileId; basal.basalProfile.targetId = widget.basalProfileId;
Basal.put(basal); 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<BasalDetailScreen> {
'${_isNew ? 'New' : 'Edit'} Basal Rate for ${BasalProfile.get(widget.basalProfileId)?.name}'), '${_isNew ? 'New' : 'Edit'} Basal Rate for ${BasalProfile.get(widget.basalProfileId)?.name}'),
), ),
drawer: const Navigation(currentLocation: BasalDetailScreen.routeName), drawer: const Navigation(currentLocation: BasalDetailScreen.routeName),
body: Column( body: Scrollbar(
children: [ controller: _scrollController,
FormWrapper( child: SingleChildScrollView(
formState: _basalForm, controller: _scrollController,
fields: [ child: Column(
Row( children: [
children: [ FormWrapper(
Expanded( formState: _basalForm,
child: Padding( fields: [
padding: const EdgeInsets.only(right: 5), Row(
child: TimeOfDayFormField( children: [
label: 'Start Time', Expanded(
controller: _startTimeController, child: Padding(
time: _startTime, padding: const EdgeInsets.only(right: 5),
onChanged: (newStartTime) { child: TimeOfDayFormField(
if (newStartTime != null) { label: 'Start Time',
setState(() { controller: _startTimeController,
_startTime = newStartTime; time: _startTime,
}); onChanged: (newStartTime) {
updateStartTime(); 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( TextFormField(
child: Padding( controller: _unitsController,
padding: const EdgeInsets.only(left: 5), keyboardType:
child: TimeOfDayFormField( const TextInputType.numberWithOptions(decimal: true),
label: 'End Time', decoration: const InputDecoration(
controller: _endTimeController, labelText: 'Units',
time: _endTime, suffixText: 'U',
onChanged: (newEndTime) {
if (newEndTime != null) {
setState(() {
_endTime = newEndTime;
});
updateEndTime();
}
},
),
), ),
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( bottomNavigationBar: DetailBottomRow(
onCancel: handleCancelAction, onCancel: handleCancelAction,

View File

@ -23,6 +23,8 @@ class BasalListScreen extends StatefulWidget {
} }
class _BasalListScreenState extends State<BasalListScreen> { class _BasalListScreenState extends State<BasalListScreen> {
final ScrollController _scrollController = ScrollController();
void reload({String? message}) { void reload({String? message}) {
widget.reload(); widget.reload();
@ -48,7 +50,7 @@ class _BasalListScreenState extends State<BasalListScreen> {
id: basal.id, id: basal.id,
), ),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void onDelete(Basal basal) { void onDelete(Basal basal) {
@ -108,44 +110,64 @@ class _BasalListScreenState extends State<BasalListScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.basalRates.isNotEmpty ? ListView.builder( return widget.basalRates.isNotEmpty ? Scrollbar(
shrinkWrap: true, controller: _scrollController,
itemCount: widget.basalRates.length, child: ListView.builder(
itemBuilder: (context, index) { padding: const EdgeInsets.all(10.0),
final basal = widget.basalRates[index]; controller: _scrollController,
final error = validateTimePeriod(index); shrinkWrap: true,
return ListTile( itemCount: widget.basalRates.length,
tileColor: error != null ? Colors.red.shade100 : null, itemBuilder: (context, index) {
onTap: () { final basal = widget.basalRates[index];
handleEditAction(basal); final error = validateTimePeriod(index);
}, return Card(
title: Row( child: Column(
mainAxisSize: MainAxisSize.max, children: [
children: [ error != null
Expanded( ? Padding(
child: Text( padding: const EdgeInsets.all(5.0),
'${DateTimeUtils.displayTime(basal.startTime)} - ${DateTimeUtils.displayTime(basal.endTime)}')), child: Row(
const Spacer(), mainAxisAlignment: MainAxisAlignment.center,
Expanded(child: Text('${basal.units} U')), children: [
], Icon(Icons.warning, color: Theme.of(context).errorColor),
), Text(
subtitle: error != null error, style: TextStyle(color: Theme.of(context).errorColor)
? Text(error, style: const TextStyle(color: Colors.red)) ),
: Container(), ],
trailing: Row( ),
mainAxisSize: MainAxisSize.min, ) : Container(),
children: [ ListTile(
IconButton( onTap: () {
icon: const Icon( handleEditAction(basal);
Icons.delete, },
color: Colors.blue, 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( ) : const Center(
child: Text('You have not created any Basal Rates yet!'), child: Text('You have not created any Basal Rates yet!'),
); );

View File

@ -29,6 +29,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
bool _isNew = true; bool _isNew = true;
final GlobalKey<FormState> _basalProfileForm = GlobalKey<FormState>(); final GlobalKey<FormState> _basalProfileForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
late FloatingActionButton addBasalButton; late FloatingActionButton addBasalButton;
late IconButton refreshButton; late IconButton refreshButton;
@ -214,7 +215,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
); );
}, },
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void handleSaveAction() async { void handleSaveAction() async {
@ -223,13 +224,14 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
}); });
if (_basalProfileForm.currentState!.validate()) { if (_basalProfileForm.currentState!.validate()) {
await checkActiveProfiles(); await checkActiveProfiles();
BasalProfile.put(BasalProfile( BasalProfile basalProfile = BasalProfile(
id: widget.id, id: widget.id,
name: _nameController.text, name: _nameController.text,
active: _active, active: _active,
notes: _notesController.text, notes: _notesController.text,
)); );
Navigator.pop(context, '${_isNew ? 'New' : ''} Basal Profile saved'); BasalProfile.put(basalProfile);
Navigator.pop(context, ['${_isNew ? 'New' : ''} Basal Profile saved', basalProfile]);
} }
setState(() { setState(() {
bottomNav = detailBottomRow; bottomNav = detailBottomRow;
@ -284,52 +286,59 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
renderTabButtons(tabController.index); renderTabButtons(tabController.index);
}); });
List<Widget> tabs = [ List<Widget> tabs = [
SingleChildScrollView( Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: [ controller: _scrollController,
FormWrapper( child: Column(
formState: _basalProfileForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: [
TextFormField( FormWrapper(
controller: _nameController, formState: _basalProfileForm,
decoration: const InputDecoration( fields: [
labelText: 'Name', TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty title';
}
return null;
},
), ),
validator: (value) { TextFormField(
if (value!.trim().isEmpty) { keyboardType: TextInputType.multiline,
return 'Empty title'; controller: _notesController,
} decoration: const InputDecoration(
return null; labelText: 'Notes',
}, ),
), minLines: 2,
TextFormField( maxLines: 5,
keyboardType: TextInputType.multiline,
controller: _notesController,
decoration: const InputDecoration(
labelText: 'Notes',
suffixText: '',
alignLabelWithHint: true,
), ),
), BooleanFormField(
BooleanFormField( value: _active,
value: _active, onChanged: (value) {
onChanged: (value) { setState(() {
setState(() { _active = value;
_active = value; });
}); },
}, label: 'active',
label: 'active', ),
), ],
], ),
), ],
], ),
), ),
), ),
]; ];
if (!_isNew) { if (!_isNew) {
tabs.add(BasalListScreen(basalProfile: _basalProfile!, basalRates: _basalRates, reload: reload)); tabs.add(BasalListScreen(
basalProfile: _basalProfile!,
basalRates: _basalRates,
reload: reload));
} }
return Scaffold( return Scaffold(

View File

@ -16,15 +16,15 @@ class BasalProfileListScreen extends StatefulWidget {
} }
class _BasalProfileListScreenState extends State<BasalProfileListScreen> { class _BasalProfileListScreenState extends State<BasalProfileListScreen> {
final ScrollController _scrollController = ScrollController();
late List<BasalProfile> _basalProfiles; late List<BasalProfile> _basalProfiles;
Widget banner = Container(); Widget banner = Container();
bool pickActiveProfileMode = false;
final BasalProfile? _activeProfile = BasalProfile.getActive(DateTime.now()); final BasalProfile? _activeProfile = BasalProfile.getActive(DateTime.now());
void refresh({String? message}) { void refresh({String? message}) {
setState(() { setState(() {
pickActiveProfileMode = false;
_basalProfiles = BasalProfile.getAll(); _basalProfiles = BasalProfile.getAll();
}); });
updateBanner(); updateBanner();
@ -106,6 +106,7 @@ class _BasalProfileListScreenState extends State<BasalProfileListScreen> {
setState(() { setState(() {
banner = MaterialBanner( banner = MaterialBanner(
content: AutoCompleteDropdownButton( content: AutoCompleteDropdownButton(
controller: TextEditingController(text: ''),
items: _basalProfiles, items: _basalProfiles,
label: 'Default Basal Profile', label: 'Default Basal Profile',
onChanged: onPickActive, onChanged: onPickActive,
@ -119,7 +120,6 @@ class _BasalProfileListScreenState extends State<BasalProfileListScreen> {
), ),
], ],
); );
pickActiveProfileMode = true;
}); });
} }
@ -130,7 +130,7 @@ class _BasalProfileListScreenState extends State<BasalProfileListScreen> {
builder: (context) => builder: (context) =>
BasalProfileDetailScreen(id: basalProfile?.id ?? 0, active: active), BasalProfileDetailScreen(id: basalProfile?.id ?? 0, active: active),
), ),
).then((message) => refresh(message: message)); ).then((result) => refresh(message: result?[0]));
} }
void onNew(bool active) { void onNew(bool active) {
@ -164,57 +164,68 @@ class _BasalProfileListScreenState extends State<BasalProfileListScreen> {
banner, banner,
Expanded( Expanded(
child: _basalProfiles.isNotEmpty child: _basalProfiles.isNotEmpty
? ListView.builder( ? Scrollbar(
itemCount: _basalProfiles.length, controller: _scrollController,
itemBuilder: (context, index) { child: ListView.builder(
final basalProfile = _basalProfiles[index]; padding: const EdgeInsets.all(10.0),
double dailyTotal = controller: _scrollController,
Basal.getDailyTotalForProfile(basalProfile.id); itemCount: _basalProfiles.length,
String activeProfileText = basalProfile.active itemBuilder: (context, index) {
? 'Default Profile' final basalProfile = _basalProfiles[index];
: basalProfile.id == _activeProfile?.id double dailyTotal =
? 'Current Active Profile' Basal.getDailyTotalForProfile(basalProfile.id);
: ''; String activeProfileText = basalProfile.active
if (activeProfileText != '' && ? ' (Default Profile)'
(basalProfile.notes ?? '') != '') { : basalProfile.id == _activeProfile?.id
activeProfileText += '\n'; ? ' (Current Active Profile)'
} : '';
return ListTile( return Card(
selected: basalProfile.active || child: ListTile(
basalProfile.id == _activeProfile?.id, isThreeLine: true,
onTap: () => onEdit(basalProfile), selected: basalProfile.active ||
title: Text(basalProfile.name), basalProfile.id == _activeProfile?.id,
subtitle: Row( onTap: () => onEdit(basalProfile),
children: [ title: Text(
Text( basalProfile.name.toUpperCase() + activeProfileText,
'$activeProfileText${basalProfile.notes ?? ''}'), style: Theme.of(context).textTheme.subtitle2,
Expanded( ),
child: Column( subtitle: Padding(
children: dailyTotal > 0 padding: const EdgeInsets.only(top: 10.0),
? [ child: Row(
Text(dailyTotal.toStringAsPrecision(3)), children: [
const Text('U/day', textScaleFactor: 0.75), 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,
trailing: Row( children: [
mainAxisSize: MainAxisSize.min, IconButton(
children: [ icon: const Icon(
IconButton( Icons.delete,
icon: const Icon( color: Colors.blue,
Icons.delete, ),
color: Colors.blue, onPressed: () => handleDeleteAction(basalProfile),
), ),
onPressed: () => handleDeleteAction(basalProfile), ],
), ),
], ),
), );
); },
}, ),
) )
: const Center( : const Center(
child: Text('You have not created any Basal Profiles yet!'), child: Text('You have not created any Basal Profiles yet!'),
), ),

View File

@ -35,6 +35,7 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
bool _isSaving = false; bool _isSaving = false;
final GlobalKey<FormState> _bolusForm = GlobalKey<FormState>(); final GlobalKey<FormState> _bolusForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0);
TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0);
@ -72,13 +73,25 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
updateEndTime(); updateEndTime();
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_bolus = Bolus.get(widget.id); _bolus = Bolus.get(widget.id);
}); });
} }
_isNew = _bolus == null; _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() { void updateStartTime() {
@ -154,7 +167,7 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
); );
bolus.bolusProfile.targetId = widget.bolusProfileId; bolus.bolusProfile.targetId = widget.bolusProfileId;
Bolus.put(bolus); 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<BolusDetailScreen> {
'${_isNew ? 'New' : 'Edit'} Bolus Rate for ${BolusProfile.get(widget.bolusProfileId)?.name}'), '${_isNew ? 'New' : 'Edit'} Bolus Rate for ${BolusProfile.get(widget.bolusProfileId)?.name}'),
), ),
drawer: const Navigation(currentLocation: BolusDetailScreen.routeName), drawer: const Navigation(currentLocation: BolusDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
children: [ child: SingleChildScrollView(
FormWrapper( controller: _scrollController,
formState: _bolusForm, child: Column(
fields: [ children: [
Row( FormWrapper(
children: [ formState: _bolusForm,
Expanded( fields: [
child: Padding( Row(
padding: const EdgeInsets.only(right: 5), children: [
child: TimeOfDayFormField( Expanded(
label: 'Start Time', child: Padding(
controller: _startTimeController, padding: const EdgeInsets.only(right: 5),
time: _startTime, child: TimeOfDayFormField(
onChanged: (newStartTime) { label: 'Start Time',
if (newStartTime != null) { controller: _startTimeController,
setState(() { time: _startTime,
_startTime = newStartTime; onChanged: (newStartTime) {
}); if (newStartTime != null) {
updateStartTime(); setState(() {
} _startTime = newStartTime;
}, });
updateStartTime();
}
},
),
), ),
), ),
), Expanded(
Expanded( child: Padding(
child: Padding( padding: const EdgeInsets.only(left: 5),
padding: const EdgeInsets.only(left: 5), child: TimeOfDayFormField(
child: TimeOfDayFormField( label: 'End Time',
label: 'End Time', controller: _endTimeController,
controller: _endTimeController, time: _endTime,
time: _endTime, onChanged: (newEndTime) {
onChanged: (newEndTime) { if (newEndTime != null) {
if (newEndTime != null) { setState(() {
setState(() { _endTime = newEndTime;
_endTime = newEndTime; });
}); updateEndTime();
updateEndTime(); }
} },
}, ),
), ),
), ),
],
),
TextFormField(
decoration: const InputDecoration(
labelText: 'Units',
suffixText: 'U',
), ),
], controller: _unitsController,
), keyboardType:
TextFormField( const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration( validator: (value) {
labelText: 'Units', if (value!.trim().isEmpty) {
suffixText: 'U', return 'Empty amount of units';
}
return null;
},
), ),
controller: _unitsController, TextFormField(
keyboardType: decoration: InputDecoration(
const TextInputType.numberWithOptions(decimal: true), labelText: 'per carbs',
validator: (value) { suffixText: Settings.nutritionMeasurementSuffix,
if (value!.trim().isEmpty) { ),
return 'Empty amount of units'; controller: _carbsController,
} keyboardType:
return null; const TextInputType.numberWithOptions(decimal: true),
}, validator: (value) {
), if (value!.trim().isEmpty) {
TextFormField( return 'How many carbs does the rate make up for?';
decoration: InputDecoration( }
labelText: 'per carbs', return null;
suffixText: Settings.nutritionMeasurementSuffix, },
), ),
controller: _carbsController, Row(
keyboardType: children: [
const TextInputType.numberWithOptions(decimal: true), Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ||
validator: (value) { Settings.glucoseDisplayMode == GlucoseDisplayMode.both ||
if (value!.trim().isEmpty) { Settings.glucoseDisplayMode ==
return 'How many carbs does the rate make up for?'; GlucoseDisplayMode.bothForDetail
} ? Expanded(
return null; child: TextFormField(
}, decoration: const InputDecoration(
), labelText: 'per mg/dl',
Row( suffixText: 'mg/dl',
children: [ ),
Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl || controller: _mgPerDlController,
Settings.glucoseDisplayMode == GlucoseDisplayMode.both || onChanged: (_) async {
Settings.glucoseDisplayMode == await Future.delayed(const Duration(seconds: 1));
GlucoseDisplayMode.bothForDetail convertBetweenMgPerDlAndMmolPerL(
? Expanded( calculateFrom:
child: TextFormField( GlucoseMeasurement.mgPerDl);
decoration: const InputDecoration( },
labelText: 'per mg/dl', keyboardType:
suffixText: 'mg/dl', 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 { : Container(),
await Future.delayed(const Duration(seconds: 1)); Settings.glucoseDisplayMode == GlucoseDisplayMode.both ||
convertBetweenMgPerDlAndMmolPerL( Settings.glucoseDisplayMode ==
calculateFrom: GlucoseDisplayMode.bothForDetail
GlucoseMeasurement.mgPerDl); ? IconButton(
}, onPressed: () => convertBetweenMgPerDlAndMmolPerL(
keyboardType: calculateFrom: GlucoseMeasurement.mmolPerL),
const TextInputType.numberWithOptions(), icon: const Icon(Icons.calculate),
validator: (value) { )
if (value!.trim().isEmpty && : Container(),
_mmolPerLController.text.trim().isEmpty) { Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL ||
return 'How many mg/dl does the rate make up for?'; [GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode)
} ? Expanded(
return null; child: TextFormField(
}, decoration: const InputDecoration(
), labelText: 'per mmol/l',
) suffixText: 'mmol/l',
: Container(), ),
Settings.glucoseDisplayMode == GlucoseDisplayMode.both || controller: _mmolPerLController,
Settings.glucoseDisplayMode == onChanged: (_) async {
GlucoseDisplayMode.bothForDetail await Future.delayed(const Duration(seconds: 1));
? IconButton( convertBetweenMgPerDlAndMmolPerL(
onPressed: () => convertBetweenMgPerDlAndMmolPerL( calculateFrom:
calculateFrom: GlucoseMeasurement.mmolPerL), GlucoseMeasurement.mmolPerL);
icon: const Icon(Icons.calculate), },
) keyboardType:
: Container(), const TextInputType.numberWithOptions(
Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL || 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) [GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode)
? Expanded( ? IconButton(
child: TextFormField( onPressed: () => convertBetweenMgPerDlAndMmolPerL(
decoration: const InputDecoration( calculateFrom: GlucoseMeasurement.mgPerDl),
labelText: 'per mmol/l', icon: const Icon(Icons.calculate),
suffixText: 'mmol/l', )
), : Container(),
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(),
],
),
],
),
],
), ),
), ),
bottomNavigationBar: DetailBottomRow( bottomNavigationBar: DetailBottomRow(

View File

@ -20,6 +20,8 @@ class BolusListScreen extends StatefulWidget {
} }
class _BolusListScreenState extends State<BolusListScreen> { class _BolusListScreenState extends State<BolusListScreen> {
final ScrollController _scrollController = ScrollController();
void reload({String? message}) { void reload({String? message}) {
widget.reload(); widget.reload();
@ -45,7 +47,7 @@ class _BolusListScreenState extends State<BolusListScreen> {
id: bolus.id, id: bolus.id,
), ),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void onDelete(Bolus bolus) { void onDelete(Bolus bolus) {
@ -104,80 +106,94 @@ class _BolusListScreenState extends State<BolusListScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.bolusRates.isNotEmpty ? ListView.builder( return widget.bolusRates.isNotEmpty ? Scrollbar(
shrinkWrap: true, controller: _scrollController,
itemCount: widget.bolusRates.length, child: ListView.builder(
itemBuilder: (context, index) { padding: const EdgeInsets.all(10.0),
final bolus = widget.bolusRates[index]; controller: _scrollController,
final error = validateTimePeriod(index); shrinkWrap: true,
return ListTile( itemCount: widget.bolusRates.length,
isThreeLine: true, itemBuilder: (context, index) {
tileColor: error != null ? Colors.red.shade100 : null, final bolus = widget.bolusRates[index];
onTap: () { final error = validateTimePeriod(index);
handleEditAction(bolus); return Card(
}, child: Column(
title: Text( children: [
'${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}'), error != null
subtitle: Column( ? Padding(
children: [ padding: const EdgeInsets.all(5.0),
error != null child: Row(
? Text(error, mainAxisAlignment: MainAxisAlignment.center,
style: const TextStyle(color: Colors.red)) children: [
: const Text(''), Icon(Icons.warning, color: Theme.of(context).errorColor),
Row( Text(
mainAxisSize: MainAxisSize.max, error, style: TextStyle(color: Theme.of(context).errorColor)
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),
]
: [],
),
), ),
Expanded( ) : Container(),
child: Column( ListTile(
children: (bolus.units > 0 && bolus.carbs > 0) onTap: () {
? [ handleEditAction(bolus);
Text((bolus.units / bolus.carbs * 12).toStringAsPrecision(2)), },
const Text('U per bread unit', isThreeLine: true,
textScaleFactor: 0.75), 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(
Expanded( child: Column(
child: Column( children: (bolus.units > 0 && bolus.carbs > 0)
children: (bolus.units > 0 && (bolus.mgPerDl ?? bolus.mmolPerL ?? 0) > 0) ? [
? [ Text((bolus.units / bolus.carbs * 12).toStringAsPrecision(2)),
Text((((Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDl : bolus.mmolPerL)! / bolus.units)).toString()), const Text('U per bread unit',
Text('${Settings.glucoseMeasurementSuffix} per unit', textAlign: TextAlign.center, textScaleFactor: 0.75),
textScaleFactor: 0.75), ]
] : [],
: [], ),
), ),
), Expanded(
]), child: Column(
], children: (bolus.units > 0 && (bolus.mgPerDl ?? bolus.mmolPerL ?? 0) > 0)
), ? [
trailing: Row( Text((((Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDl : bolus.mmolPerL)! / bolus.units)).toString()),
mainAxisSize: MainAxisSize.min, Text('${Settings.glucoseMeasurementSuffix} per unit',
children: [ textAlign: TextAlign.center, textScaleFactor: 0.75),
IconButton( ]
icon: const Icon( : [],
Icons.delete, ),
color: Colors.blue, ),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
),
onPressed: () => handleDeleteAction(bolus),
),
],
),
), ),
onPressed: () => handleDeleteAction(bolus), ],
), ),
], );
), },
); ),
},
) : const Center( ) : const Center(
child: Text('You have not created any Bolus Rates yet!'), child: Text('You have not created any Bolus Rates yet!'),
); );

View File

@ -28,6 +28,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
bool _isNew = true; bool _isNew = true;
final GlobalKey<FormState> _bolusProfileForm = GlobalKey<FormState>(); final GlobalKey<FormState> _bolusProfileForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
late FloatingActionButton addBolusButton; late FloatingActionButton addBolusButton;
late IconButton refreshButton; late IconButton refreshButton;
@ -211,7 +212,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
); );
}, },
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void handleSaveAction() async { void handleSaveAction() async {
@ -221,15 +222,15 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
if (_bolusProfileForm.currentState!.validate()) { if (_bolusProfileForm.currentState!.validate()) {
await checkActiveProfiles(); await checkActiveProfiles();
BolusProfile.put( BolusProfile bolusProfile = BolusProfile(
BolusProfile( id: widget.id,
id: widget.id, name: _nameController.text,
name: _nameController.text, active: _active,
active: _active, notes: _notesController.text,
notes: _notesController.text,
),
); );
Navigator.pop(context, '${_isNew ? 'New' : ''} Bolus Profile saved'); BolusProfile.put(bolusProfile);
Navigator.pop(context,
['${_isNew ? 'New' : ''} Bolus Profile saved', bolusProfile]);
} }
setState(() { setState(() {
@ -286,45 +287,50 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
}); });
List<Widget> tabs = [ List<Widget> tabs = [
SingleChildScrollView( Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: [ controller: _scrollController,
FormWrapper( child: Column(
formState: _bolusProfileForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: [
TextFormField( FormWrapper(
controller: _nameController, formState: _bolusProfileForm,
decoration: const InputDecoration( fields: [
labelText: 'Name', TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty title';
}
return null;
},
), ),
validator: (value) { TextFormField(
if (value!.trim().isEmpty) { decoration: const InputDecoration(
return 'Empty title'; labelText: 'Notes',
} ),
return null; controller: _notesController,
}, keyboardType: TextInputType.multiline,
), minLines: 2,
TextFormField( maxLines: 5,
decoration: const InputDecoration(
labelText: 'Notes',
alignLabelWithHint: true,
), ),
controller: _notesController, BooleanFormField(
keyboardType: TextInputType.multiline, value: _active,
), onChanged: (value) {
BooleanFormField( setState(() {
value: _active, _active = value;
onChanged: (value) { });
setState(() { },
_active = value; label: 'active',
}); ),
}, ],
label: 'active', ),
), ],
], ),
),
],
), ),
), ),
]; ];

View File

@ -15,15 +15,15 @@ class BolusProfileListScreen extends StatefulWidget {
} }
class _BolusProfileListScreenState extends State<BolusProfileListScreen> { class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
final ScrollController _scrollController = ScrollController();
List<BolusProfile> _bolusProfiles = []; List<BolusProfile> _bolusProfiles = [];
Widget banner = Container(); Widget banner = Container();
bool pickActiveProfileMode = false;
final BolusProfile? _activeProfile = BolusProfile.getActive(DateTime.now()); final BolusProfile? _activeProfile = BolusProfile.getActive(DateTime.now());
void reload({String? message}) { void reload({String? message}) {
setState(() { setState(() {
pickActiveProfileMode = false;
_bolusProfiles = BolusProfile.getAll(); _bolusProfiles = BolusProfile.getAll();
}); });
@ -99,7 +99,8 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
BolusProfile.setAllInactive; BolusProfile.setAllInactive;
bolusProfile.active = true; bolusProfile.active = true;
BolusProfile.put(bolusProfile); 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<BolusProfileListScreen> {
setState(() { setState(() {
banner = MaterialBanner( banner = MaterialBanner(
content: AutoCompleteDropdownButton( content: AutoCompleteDropdownButton(
controller: TextEditingController(text: ''),
items: _bolusProfiles, items: _bolusProfiles,
label: 'Default Basal Profile', label: 'Default Basal Profile',
onChanged: onPickActive, onChanged: onPickActive,
@ -120,7 +122,6 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
), ),
], ],
); );
pickActiveProfileMode = true;
}); });
} }
@ -131,7 +132,7 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
builder: (context) => builder: (context) =>
BolusProfileDetailScreen(id: bolusProfile?.id ?? 0, active: active), BolusProfileDetailScreen(id: bolusProfile?.id ?? 0, active: active),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void onNew(bool active) { void onNew(bool active) {
@ -165,40 +166,50 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
banner, banner,
Expanded( Expanded(
child: _bolusProfiles.isNotEmpty child: _bolusProfiles.isNotEmpty
? ListView.builder( ? Scrollbar(
itemCount: _bolusProfiles.length, controller: _scrollController,
itemBuilder: (context, index) { child: ListView.builder(
final bolusProfile = _bolusProfiles[index]; padding: const EdgeInsets.all(10.0),
String activeProfileText = bolusProfile.active controller: _scrollController,
? 'Default Profile' itemCount: _bolusProfiles.length,
: bolusProfile.id == _activeProfile?.id itemBuilder: (context, index) {
? 'Current Active Profile' final bolusProfile = _bolusProfiles[index];
: ''; String activeProfileText = bolusProfile.active
if (activeProfileText != '' && (bolusProfile.notes ?? '') != '') { ? ' (Default Profile)'
activeProfileText += '\n'; : bolusProfile.id == _activeProfile?.id
} ? ' (Current Active Profile)'
return ListTile( : '';
selected: bolusProfile.active || bolusProfile.id == _activeProfile?.id, return Card(
onTap: () => onEdit(bolusProfile), child: ListTile(
title: Text( selected: bolusProfile.active ||
bolusProfile.name, bolusProfile.id == _activeProfile?.id,
), onTap: () => onEdit(bolusProfile),
isThreeLine: activeProfileText != '' && (bolusProfile.notes ?? '') != '', title: Text(
subtitle: Text('$activeProfileText${bolusProfile.notes ?? ''}'), bolusProfile.name.toUpperCase() +
trailing: Row( activeProfileText,
mainAxisSize: MainAxisSize.min, style: Theme.of(context).textTheme.subtitle2,
children: [
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
),
onPressed: () => handleDeleteAction(bolusProfile),
), ),
], 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( : const Center(
child: Text('You have not created any Bolus Profiles yet!'), child: Text('You have not created any Bolus Profiles yet!'),

View File

@ -8,6 +8,7 @@ import 'package:diameter/navigation.dart';
import 'package:diameter/screens/log/log_entry/log_entry.dart'; import 'package:diameter/screens/log/log_entry/log_entry.dart';
import 'package:diameter/utils/date_time_utils.dart'; import 'package:diameter/utils/date_time_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:math' as math;
class LogScreen extends StatefulWidget { class LogScreen extends StatefulWidget {
static const String routeName = '/log'; static const String routeName = '/log';
@ -20,6 +21,8 @@ class LogScreen extends StatefulWidget {
class _LogScreenState extends State<LogScreen> { class _LogScreenState extends State<LogScreen> {
late Map<DateTime, List<LogEntry>> _logEntryDailyMap; late Map<DateTime, List<LogEntry>> _logEntryDailyMap;
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -74,130 +77,189 @@ class _LogScreenState extends State<LogScreen> {
children: [ children: [
Expanded( Expanded(
child: _logEntryDailyMap.isNotEmpty child: _logEntryDailyMap.isNotEmpty
? ListView.builder( ? Scrollbar(
padding: const EdgeInsets.all(10.0), controller: _scrollController,
shrinkWrap: true, child: ListView.builder(
itemCount: _logEntryDailyMap.length, controller: _scrollController,
itemBuilder: (context, dateIndex) { padding: const EdgeInsets.all(10.0),
List<DateTime> dateList = _logEntryDailyMap.keys.toList(); shrinkWrap: true,
final date = dateList[dateIndex]; itemCount: _logEntryDailyMap.length,
final entryList = _logEntryDailyMap[date]; itemBuilder: (context, dateIndex) {
final tiles = <Widget>[]; List<DateTime> dateList =
for (LogEntry logEntry in entryList!) { _logEntryDailyMap.keys.toList();
double bolus = final date = dateList[dateIndex];
LogBolus.getTotalBolusForEntry(logEntry.id); final entryList = _logEntryDailyMap[date];
double carbs = final tiles = <Widget>[];
LogMeal.getTotalCarbsForEntry(logEntry.id); for (LogEntry logEntry in entryList!) {
TextStyle glucoseStyle = TextStyle( double bolus =
color: GlucoseTarget.getColorForGlucose( LogBolus.getTotalBolusForEntry(logEntry.id);
mgPerDl: logEntry.mgPerDl ?? 0, double carbs =
mmolPerL: logEntry.mmolPerL ?? 0)); LogMeal.getTotalCarbsForEntry(logEntry.id);
tiles.add(ListTile( TextStyle glucoseStyle = TextStyle(
onTap: () { color: GlucoseTarget.getColorForGlucose(
Navigator.push( mgPerDl: logEntry.mgPerDl ?? 0,
context, mmolPerL: logEntry.mmolPerL ?? 0));
MaterialPageRoute( tiles.add(
builder: (context) => Card(
LogEntryScreen(id: logEntry.id), 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)); trailing: Row(
}, mainAxisSize: MainAxisSize.min,
title: Row( children: [
mainAxisAlignment: MainAxisAlignment.start, IconButton(
crossAxisAlignment: CrossAxisAlignment.start, onPressed: () => handleDeleteAction(logEntry),
children: [ icon: const Icon(Icons.delete,
Expanded( color: Colors.blue),
child: Text( )
DateTimeUtils.displayTime(logEntry.time), ],
),
), ),
Expanded( ),
child: Column( ));
children: logEntry.mgPerDl != null && }
(Settings.glucoseMeasurement == return ListBody(
GlucoseMeasurement.mgPerDl || children: <Widget>[
Settings.glucoseDisplayMode == Padding(
GlucoseDisplayMode.both || padding: const EdgeInsets.all(10.0),
Settings.glucoseDisplayMode == child: Text(
GlucoseDisplayMode DateTimeUtils.displayDate(date).toUpperCase(),
.bothForList) style: Theme.of(context).textTheme.subtitle2,
? [ ),
Text(logEntry.mgPerDl.toString(), )
style: glucoseStyle), ] +
Text( tiles +
'mg/dl', [const Divider()]
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: <Widget>[
Text(DateTimeUtils.displayDate(date))
] +
tiles,
);
},
) )
: const Center( : const Center(
child: Text('You have not created any Log Entries yet!'), child: Text('You have not created any Log Entries yet!'),
@ -212,7 +274,7 @@ class _LogScreenState extends State<LogScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const LogEntryScreen(), builder: (context) => const LogEntryScreen(),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),

View File

@ -10,6 +10,7 @@ import 'package:diameter/models/log_entry.dart';
import 'package:diameter/models/log_meal.dart'; import 'package:diameter/models/log_meal.dart';
import 'package:diameter/models/settings.dart'; import 'package:diameter/models/settings.dart';
import 'package:diameter/navigation.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:diameter/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -48,6 +49,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
bool _isSaving = false; bool _isSaving = false;
final GlobalKey<FormState> _logBolusForm = GlobalKey<FormState>(); final GlobalKey<FormState> _logBolusForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
final _unitsController = TextEditingController(text: ''); final _unitsController = TextEditingController(text: '');
final _carbsController = TextEditingController(text: ''); final _carbsController = TextEditingController(text: '');
@ -63,6 +65,8 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
final _delayedUnitsController = TextEditingController(text: ''); final _delayedUnitsController = TextEditingController(text: '');
final _immediateUnitsController = TextEditingController(text: ''); final _immediateUnitsController = TextEditingController(text: '');
final _mealController = TextEditingController(text: '');
bool _setManually = false; bool _setManually = false;
BolusType _bolusType = BolusType.meal; BolusType _bolusType = BolusType.meal;
LogMeal? _meal; LogMeal? _meal;
@ -85,6 +89,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
_notesController.text = _logBolus!.notes ?? ''; _notesController.text = _logBolus!.notes ?? '';
_setManually = _logBolus!.setManually; _setManually = _logBolus!.setManually;
_meal = _logBolus!.meal.target; _meal = _logBolus!.meal.target;
_mealController.text = (_meal ?? '').toString();
_rate = _logBolus!.rate.target; _rate = _logBolus!.rate.target;
} }
@ -96,7 +101,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
: 0)) : 0))
.toString(); .toString();
_mgPerDlTargetController.text = _mgPerDlTargetController.text =
(_logBolus?.mgPerDlTarget ?? Settings.targetMgPerDl()).toString(); (_logBolus?.mgPerDlTarget ?? Settings.targetMgPerDl).toString();
_mgPerDlCorrectionController.text = (_logBolus?.mgPerDlCorrection ?? _mgPerDlCorrectionController.text = (_logBolus?.mgPerDlCorrection ??
max( max(
(int.tryParse(_mgPerDlCurrentController.text) ?? 0) - (int.tryParse(_mgPerDlCurrentController.text) ?? 0) -
@ -109,7 +114,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
: 0)) : 0))
.toString(); .toString();
_mmolPerLTargetController.text = _mmolPerLTargetController.text =
(_logBolus?.mmolPerLTarget ?? Settings.targetMmolPerL()).toString(); (_logBolus?.mmolPerLTarget ?? Settings.targetMmolPerL).toString();
_mmolPerLCorrectionController.text = (_logBolus?.mmolPerLCorrection ?? _mmolPerLCorrectionController.text = (_logBolus?.mmolPerLCorrection ??
max( max(
(double.tryParse(_mmolPerLCurrentController.text) ?? 0) - (double.tryParse(_mmolPerLCurrentController.text) ?? 0) -
@ -131,13 +136,32 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
updateDelayedRatio(); updateDelayedRatio();
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_logBolus = LogBolus.get(widget.id); _logBolus = LogBolus.get(widget.id);
}); });
} }
_isNew = _logBolus == null; _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() { void updateDelayedRatio() {
@ -157,14 +181,14 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
} }
} }
void onSelectMeal(LogMeal meal) { void onSelectMeal(LogMeal? meal) {
setState(() { updateLogMeal(meal);
_meal = meal; if (meal != null && meal.carbsPerPortion != null) {
if (meal.carbsPerPortion != null) { setState(() {
_carbsController.text = meal.carbsPerPortion.toString(); _carbsController.text = meal.carbsPerPortion.toString();
onChangeCarbs(); onChangeCarbs();
} });
}); }
} }
void onChangeCarbs() { void onChangeCarbs() {
@ -332,7 +356,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
LogBolus.put(delayedBolus); LogBolus.put(delayedBolus);
} }
Navigator.pop(context, '${_isNew ? 'New' : ''} Bolus Saved'); Navigator.pop(context, ['${_isNew ? 'New' : ''} Bolus Saved', logBolus, delayedBolus]);
} }
setState(() { setState(() {
_isSaving = false; _isSaving = false;
@ -349,9 +373,9 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
_mmolPerLCurrentController.text != _mmolPerLCurrentController.text !=
(_logEntry?.mmolPerL.toString() ?? ''))) || (_logEntry?.mmolPerL.toString() ?? ''))) ||
_mgPerDlTargetController.text != _mgPerDlTargetController.text !=
Settings.targetMgPerDl().toString() || Settings.targetMgPerDl.toString() ||
_mmolPerLTargetController.text != _mmolPerLTargetController.text !=
Settings.targetMmolPerL().toString() || Settings.targetMmolPerL.toString() ||
_delayController.text != '' || _delayController.text != '' ||
_setManually || _setManually ||
_notesController.text != '')) || _notesController.text != '')) ||
@ -391,321 +415,379 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
title: Text(_isNew ? 'New Bolus' : 'Edit Bolus'), title: Text(_isNew ? 'New Bolus' : 'Edit Bolus'),
), ),
drawer: const Navigation(currentLocation: LogBolusDetailScreen.routeName), drawer: const Navigation(currentLocation: LogBolusDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _logBolusForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
Row( FormWrapper(
children: [ formState: _logBolusForm,
Expanded( fields: [
child: TextFormField( Row(
decoration: const InputDecoration( children: [
labelText: 'Bolus Units', Expanded(
suffixText: ' U', child: TextFormField(
), decoration: const InputDecoration(
controller: _unitsController, labelText: 'Bolus Units',
onChanged: (_) { suffixText: ' U',
setState(() { ),
_setManually = true; controller: _unitsController,
});
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,
onChanged: (_) { onChanged: (_) {
setState(() { setState(() {
_bolusType = BolusType.glucose; _setManually = true;
}); });
}), updateDelayedRatio();
), },
Expanded( keyboardType: const TextInputType.numberWithOptions(
child: RadioListTile( decimal: true),
title: const Text('for meal'), ),
groupValue: _bolusType, ),
value: BolusType.meal, Expanded(
child: BooleanFormField(
contentPadding: const EdgeInsets.only(
left: 10.0, right: 10.0, top: 10.0),
value: _setManually,
label: 'set manually',
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_bolusType = BolusType.meal; _setManually = value;
}); });
}), },
), ),
], ),
), ],
Column( ),
children: _bolusType == BolusType.glucose Row(
? [ children: [
Row( Expanded(
children: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl || child: RadioListTile(
[GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail].contains(Settings.glucoseDisplayMode) title: const Text('for glucose'),
? [ groupValue: _bolusType,
Expanded( value: BolusType.glucose,
child: Padding( onChanged: (_) {
padding: setState(() {
const EdgeInsets.only(right: 5.0), _bolusType = BolusType.glucose;
child: TextFormField( });
decoration: const InputDecoration( }),
labelText: 'Current', ),
suffixText: 'mg/dl', Expanded(
), child: RadioListTile(
controller: _mgPerDlCurrentController, title: const Text('for meal'),
onChanged: (_) async { groupValue: _bolusType,
await Future.delayed(const Duration(seconds: 1)); value: BolusType.meal,
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<LogMeal>(
selectedItem: _meal,
label: 'Meal',
items: _logMeals,
onChanged: (value) { onChanged: (value) {
if (value != null) { setState(() {
onSelectMeal(value); _bolusType = BolusType.meal;
} });
}, }),
), ),
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',
), ),
controller: _delayController, Column(
onChanged: (value) => setState(() {}), children: _bolusType == BolusType.glucose
keyboardType: const TextInputType.numberWithOptions(), ? [
), Row(
(int.tryParse(_delayController.text) ?? 0) != 0 children: Settings.glucoseMeasurement ==
? Slider( GlucoseMeasurement.mgPerDl ||
label: '${_delayPercentage.floor().toString()}%', [
divisions: 100, GlucoseDisplayMode.both,
value: _delayPercentage, GlucoseDisplayMode.bothForDetail
min: 0, ].contains(Settings.glucoseDisplayMode)
max: 100, ? [
onChanged: _delayController.text != '' Expanded(
? (value) { child: Padding(
setState(() { padding:
_delayPercentage = value; const EdgeInsets.only(right: 5.0),
}); child: TextFormField(
updateDelayedRatio(); decoration: const InputDecoration(
} labelText: 'Current',
: null, suffixText: 'mg/dl',
) ),
: Container(), controller:
Row( _mgPerDlCurrentController,
children: (int.tryParse(_delayController.text) ?? 0) != 0 onChanged: (_) async {
? [ await Future.delayed(
Expanded( const Duration(seconds: 1));
child: Padding( onChangeGlucose(
padding: const EdgeInsets.only(right: 5.0), calculateFrom:
child: TextFormField( GlucoseMeasurement
decoration: const InputDecoration( .mgPerDl);
labelText: 'Immediate Bolus', },
suffixText: ' U', 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<LogMeal>(
controller: _mealController,
selectedItem: _meal,
label: 'Meal',
items: _logMeals,
onChanged: onSelectMeal,
),
), ),
controller: _immediateUnitsController, IconButton(
readOnly: true, onPressed: () {
enabled: (int.tryParse(_delayController.text) ?? Navigator.push(
0) != context,
0, 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( TextFormField(
padding: const EdgeInsets.only(left: 5.0), decoration: const InputDecoration(
child: TextFormField( labelText: 'Delayed Bolus Duration',
decoration: const InputDecoration( suffixText: ' min',
labelText: 'Delayed Bolus', ),
suffixText: ' U', 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,
), ),
), ),
), Expanded(
] child: Padding(
: [], padding: const EdgeInsets.only(left: 5.0),
), child: TextFormField(
TextFormField( decoration: const InputDecoration(
controller: _notesController, labelText: 'Delayed Bolus',
decoration: const InputDecoration( suffixText: ' U',
labelText: 'Notes', ),
alignLabelWithHint: true, 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( bottomNavigationBar: DetailBottomRow(

View File

@ -23,6 +23,8 @@ class LogBolusListScreen extends StatefulWidget {
} }
class _LogBolusListScreenState extends State<LogBolusListScreen> { class _LogBolusListScreenState extends State<LogBolusListScreen> {
final ScrollController _scrollController = ScrollController();
void reload({String? message}) { void reload({String? message}) {
widget.reload(); widget.reload();
@ -48,7 +50,7 @@ class _LogBolusListScreenState extends State<LogBolusListScreen> {
id: logBolus.id, id: logBolus.id,
), ),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void onDelete(LogBolus logBolus) { void onDelete(LogBolus logBolus) {
@ -77,49 +79,55 @@ class _LogBolusListScreenState extends State<LogBolusListScreen> {
id: mealId, id: mealId,
), ),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.logBoli.isNotEmpty return widget.logBoli.isNotEmpty
? ListView.builder( ? Scrollbar(
shrinkWrap: true, controller: _scrollController,
itemCount: widget.logBoli.length, child: ListView.builder(
itemBuilder: (context, index) { controller: _scrollController,
final bolus = widget.logBoli[index]; shrinkWrap: true,
String titleText = '${bolus.units} U ${(bolus.delay ?? 0) != 0 itemCount: widget.logBoli.length,
? ' (delayed by ${bolus.delay} min)' itemBuilder: (context, index) {
: ''}'; final bolus = widget.logBoli[index];
return ListTile( String titleText = '${bolus.units} U ${(bolus.delay ?? 0) != 0
onTap: () => handleEditAction(bolus), ? ' (delayed by ${bolus.delay} min)'
title: Text(titleText), : ''}';
subtitle: Text(bolus.carbs != null ? return Card(
'for ${bolus.meal.target.toString()} (${bolus.carbs}${Settings.nutritionMeasurementSuffix} carbs)' child: ListTile(
: 'to correct ${Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDlCorrection : bolus.mmolPerLCorrection} ${Settings.glucoseMeasurementSuffix}'), onTap: () => handleEditAction(bolus),
trailing: Row( title: Text(titleText),
mainAxisSize: MainAxisSize.min, subtitle: Text(bolus.carbs != null ?
children: [ 'for ${bolus.meal.target.toString()} (${bolus.carbs}${Settings.nutritionMeasurementSuffix} carbs)'
bolus.meal.target != null : 'to correct ${Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDlCorrection : bolus.mmolPerLCorrection} ${Settings.glucoseMeasurementSuffix}'),
? IconButton( trailing: Row(
icon: const Icon(Icons.restaurant), mainAxisSize: MainAxisSize.min,
onPressed: () => children: [
handleEditMealAction(bolus.meal.targetId), bolus.meal.target != null
) ? IconButton(
: Container(), icon: const Icon(Icons.restaurant),
const SizedBox(width: 24), onPressed: () =>
IconButton( handleEditMealAction(bolus.meal.targetId),
icon: const Icon( )
Icons.delete, : Container(),
color: Colors.blue, const SizedBox(width: 24),
), IconButton(
onPressed: () => handleDeleteAction(bolus), icon: const Icon(
Icons.delete,
color: Colors.blue,
),
onPressed: () => handleDeleteAction(bolus),
),
],
), ),
], ),
), );
); },
}, ),
) )
: const Center( : const Center(
child: Text( child: Text(
'You have not added any Boli to this Log Entry yet!'), 'You have not added any Boli to this Log Entry yet!'),

View File

@ -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/date_time_utils.dart';
import 'package:diameter/utils/utils.dart'; import 'package:diameter/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:math' as math;
class LogEntryScreen extends StatefulWidget { class LogEntryScreen extends StatefulWidget {
static const String routeName = '/log-entry'; static const String routeName = '/log-entry';
@ -28,12 +29,15 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
LogEntry? _logEntry; LogEntry? _logEntry;
List<LogMeal> _logMeals = []; List<LogMeal> _logMeals = [];
List<LogBolus> _logBoli = []; List<LogBolus> _logBoli = [];
bool _isNew = true; bool _isNew = true;
bool _isSaving = false; bool _isSaving = false;
final GlobalKey<FormState> logEntryForm = GlobalKey<FormState>(); final GlobalKey<FormState> logEntryForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
late DateTime _time; late DateTime _time;
double? _glucoseTrend;
final _timeController = TextEditingController(text: ''); final _timeController = TextEditingController(text: '');
final _dateController = TextEditingController(text: ''); final _dateController = TextEditingController(text: '');
@ -90,6 +94,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
_time = _logEntry!.time; _time = _logEntry!.time;
_mgPerDlController.text = (_logEntry!.mgPerDl ?? '').toString(); _mgPerDlController.text = (_logEntry!.mgPerDl ?? '').toString();
_mmolPerLController.text = (_logEntry!.mmolPerL ?? '').toString(); _mmolPerLController.text = (_logEntry!.mmolPerL ?? '').toString();
_glucoseTrend = _logEntry!.glucoseTrend;
_notesController.text = _logEntry!.notes ?? ''; _notesController.text = _logEntry!.notes ?? '';
} else { } else {
_time = DateTime.now(); _time = DateTime.now();
@ -126,26 +131,21 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
_dateController.text = DateTimeUtils.displayDate(_time); _dateController.text = DateTimeUtils.displayDate(_time);
} }
void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { void convertBetweenMgPerDlAndMmolPerL() {
int? mgPerDl; int? mgPerDl;
double? mmolPerL; double? mmolPerL;
if (calculateFrom != GlucoseMeasurement.mmolPerL && if (Settings.glucoseMeasurement != GlucoseMeasurement.mmolPerL &&
_mgPerDlController.text != '') { _mgPerDlController.text != '') {
mgPerDl = int.tryParse(_mgPerDlController.text); mgPerDl = int.tryParse(_mgPerDlController.text);
}
if (calculateFrom != GlucoseMeasurement.mgPerDl &&
_mmolPerLController.text != '') {
mmolPerL = double.tryParse(_mmolPerLController.text);
}
if (mgPerDl != null && mmolPerL == null) {
setState(() { setState(() {
_mmolPerLController.text = _mmolPerLController.text =
Utils.convertMgPerDlToMmolPerL(mgPerDl!).toString(); Utils.convertMgPerDlToMmolPerL(mgPerDl!).toString();
}); });
} }
if (mmolPerL != null && mgPerDl == null) { if (Settings.glucoseMeasurement != GlucoseMeasurement.mgPerDl &&
_mmolPerLController.text != '') {
mmolPerL = double.tryParse(_mmolPerLController.text);
setState(() { setState(() {
_mgPerDlController.text = _mgPerDlController.text =
Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString(); Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString();
@ -158,15 +158,17 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
_isSaving = true; _isSaving = true;
}); });
if (logEntryForm.currentState!.validate()) { if (logEntryForm.currentState!.validate()) {
LogEntry.put(LogEntry( LogEntry logEntry = LogEntry(
id: widget.id, id: widget.id,
time: _time, time: _time,
mgPerDl: int.tryParse(_mgPerDlController.text), mgPerDl: int.tryParse(_mgPerDlController.text),
mmolPerL: double.tryParse(_mmolPerLController.text), mmolPerL: double.tryParse(_mmolPerLController.text),
glucoseTrend: _glucoseTrend,
notes: _notesController.text, notes: _notesController.text,
)); );
LogEntry.put(logEntry);
Navigator.pushReplacementNamed(context, '/log', Navigator.pushReplacementNamed(context, '/log',
arguments: '${_isNew ? 'New' : ''} Log Entry Saved'); arguments: ['${_isNew ? 'New' : ''} Log Entry Saved', logEntry]);
} }
setState(() { setState(() {
_isSaving = false; _isSaving = false;
@ -204,7 +206,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
return LogMealDetailScreen(logEntryId: _logEntry!.id); return LogMealDetailScreen(logEntryId: _logEntry!.id);
}, },
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void handleAddNewBolus() async { void handleAddNewBolus() async {
@ -215,7 +217,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
return LogBolusDetailScreen(logEntryId: _logEntry!.id); return LogBolusDetailScreen(logEntryId: _logEntry!.id);
}, },
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void renderTabButtons(index) { void renderTabButtons(index) {
@ -251,171 +253,173 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
renderTabButtons(tabController.index); renderTabButtons(tabController.index);
}); });
List<Widget> tabs = [ List<Widget> tabs = [
SingleChildScrollView( Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: logEntryForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
Row( FormWrapper(
children: [ formState: logEntryForm,
Expanded( fields: [
child: Padding( Row(
padding: const EdgeInsets.only(right: 5), children: [
child: DateTimeFormField( Expanded(
date: _time, child: Padding(
label: 'Date', padding: const EdgeInsets.only(right: 5),
controller: _dateController, child: DateTimeFormField(
onChanged: (newTime) { date: _time,
if (newTime != null) { label: 'Date',
setState(() { controller: _dateController,
_time = DateTime( onChanged: (newTime) {
newTime.year, if (newTime != null) {
newTime.month, setState(() {
newTime.day, _time = DateTime(
_time.hour, newTime.year,
_time.minute); newTime.month,
}); newTime.day,
updateTime(); _time.hour,
} _time.minute);
}, });
updateTime();
}
},
),
), ),
), ),
), Expanded(
Expanded( child: Padding(
child: Padding( padding: const EdgeInsets.only(left: 5),
padding: const EdgeInsets.only(left: 5), child: TimeOfDayFormField(
child: TimeOfDayFormField( time: TimeOfDay.fromDateTime(_time),
time: TimeOfDay.fromDateTime(_time), label: 'Time',
label: 'Time', controller: _timeController,
controller: _timeController, onChanged: (newTime) {
onChanged: (newTime) { if (newTime != null) {
if (newTime != null) { setState(() {
setState(() { _time = DateTime(
_time = DateTime( _time.year,
_time.year, _time.month,
_time.month, _time.day,
_time.day, newTime.hour,
newTime.hour, newTime.minute);
newTime.minute); });
}); updateTime();
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,
),
],
),
]),
),
), ),
]; ];

View File

@ -10,6 +10,11 @@ import 'package:diameter/models/meal_portion_type.dart';
import 'package:diameter/models/meal_source.dart'; import 'package:diameter/models/meal_source.dart';
import 'package:diameter/models/settings.dart'; import 'package:diameter/models/settings.dart';
import 'package:diameter/navigation.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:diameter/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -30,8 +35,10 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
LogMeal? _logMeal; LogMeal? _logMeal;
bool _isNew = true; bool _isNew = true;
bool _isSaving = false; bool _isSaving = false;
bool _isExpanded = false;
final GlobalKey<FormState> _logMealForm = GlobalKey<FormState>(); final GlobalKey<FormState> _logMealForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
final _valueController = TextEditingController(text: ''); final _valueController = TextEditingController(text: '');
final _carbsRatioController = TextEditingController(text: ''); final _carbsRatioController = TextEditingController(text: '');
@ -46,6 +53,13 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
Accuracy? _portionSizeAccuracy; Accuracy? _portionSizeAccuracy;
Accuracy? _carbsRatioAccuracy; 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<Meal> _meals = []; List<Meal> _meals = [];
List<MealCategory> _mealCategories = []; List<MealCategory> _mealCategories = [];
List<MealPortionType> _mealPortionTypes = []; List<MealPortionType> _mealPortionTypes = [];
@ -72,22 +86,89 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
_carbsPerPortionController.text = _carbsPerPortionController.text =
(_logMeal!.carbsPerPortion ?? '').toString(); (_logMeal!.carbsPerPortion ?? '').toString();
_notesController.text = _logMeal!.notes ?? ''; _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) { if (widget.id != 0) {
setState(() { setState(() {
_logMeal = LogMeal.get(widget.id); _logMeal = LogMeal.get(widget.id);
}); });
} }
_isNew = _logMeal == null; _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<void> 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<void> onSelectMeal(Meal? meal) async {
setState(() { setState(() {
_meal = meal; _meal = meal;
_valueController.text = meal.value; _valueController.text = _mealController.text = (_meal ?? '').toString();
});
if (meal != null) {
if (meal.carbsRatio != null) { if (meal.carbsRatio != null) {
_carbsRatioController.text = meal.carbsRatio.toString(); _carbsRatioController.text = meal.carbsRatio.toString();
} }
@ -98,21 +179,21 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
_carbsPerPortionController.text = meal.carbsPerPortion.toString(); _carbsPerPortionController.text = meal.carbsPerPortion.toString();
} }
if (meal.mealSource.hasValue) { if (meal.mealSource.hasValue) {
_mealSource = meal.mealSource.target; updateMealSource(meal.mealSource.target);
} }
if (meal.mealCategory.hasValue) { if (meal.mealCategory.hasValue) {
_mealCategory = meal.mealCategory.target; updateMealCategory(meal.mealCategory.target);
} }
if (meal.mealPortionType.hasValue) { if (meal.mealPortionType.hasValue) {
_mealPortionType = meal.mealPortionType.target; updateMealPortionType(meal.mealPortionType.target);
} }
if (meal.portionSizeAccuracy.hasValue) { if (meal.portionSizeAccuracy.hasValue) {
_portionSizeAccuracy = meal.portionSizeAccuracy.target; updatePortionSizeAccuracy(meal.portionSizeAccuracy.target);
} }
if (meal.carbsRatioAccuracy.hasValue) { if (meal.carbsRatioAccuracy.hasValue) {
_carbsRatioAccuracy = meal.carbsRatioAccuracy.target; updateCarbsRatioAccuracy(meal.carbsRatioAccuracy.target);
} }
}); }
} }
void handleSaveAction() async { void handleSaveAction() async {
@ -136,7 +217,7 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
logMeal.carbsRatioAccuracy.target = _carbsRatioAccuracy; logMeal.carbsRatioAccuracy.target = _carbsRatioAccuracy;
LogMeal.put(logMeal); LogMeal.put(logMeal);
Navigator.pop(context, '${_isNew ? 'New' : ''} Meal Saved'); Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Saved', logMeal]);
} }
setState(() { setState(() {
_isSaving = false; _isSaving = false;
@ -233,178 +314,358 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
title: Text(_isNew ? 'New Meal' : _logMeal!.value), title: Text(_isNew ? 'New Meal' : _logMeal!.value),
), ),
drawer: const Navigation(currentLocation: LogMealDetailScreen.routeName), drawer: const Navigation(currentLocation: LogMealDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _logMealForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
TextFormField( FormWrapper(
controller: _valueController, formState: _logMealForm,
decoration: const InputDecoration( fields: [
labelText: 'Name', TextFormField(
controller: _valueController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty name';
}
return null;
},
), ),
validator: (value) { Row(
if (value!.trim().isEmpty) { children: [
return 'Empty name'; Expanded(
} child: AutoCompleteDropdownButton<Meal>(
return null; controller: _mealController,
}, selectedItem: _meal,
), label: 'Meal',
AutoCompleteDropdownButton<Meal>( items: _meals,
selectedItem: _meal, onChanged: (value) {
label: 'Meal', if (value != null) {
items: _meals, onSelectMeal(value);
onChanged: (value) { }
if (value != null) { },
onSelectMeal(value);
}
},
),
AutoCompleteDropdownButton<MealSource>(
selectedItem: _mealSource,
label: 'Meal Source',
items: _mealSources,
onChanged: (value) {
setState(() {
_mealSource = value;
});
},
),
AutoCompleteDropdownButton<MealCategory>(
selectedItem: _mealCategory,
label: 'Meal Category',
items: _mealCategories,
onChanged: (value) {
setState(() {
_mealCategory = value;
});
},
),
AutoCompleteDropdownButton<MealPortionType>(
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: '%',
), ),
controller: _carbsRatioController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
onChanged: (_) async {
await Future.delayed(const Duration(seconds: 1));
calculateThirdMeasurementOfPortionCarbsRelation();
},
), ),
), IconButton(
IconButton( onPressed: () {
onPressed: () => Navigator.push(
calculateThirdMeasurementOfPortionCarbsRelation( context,
parameterToBeCalculated: MaterialPageRoute(
PortionCarbsParameter.carbsRatio), builder: (context) => _meal == null
icon: const Icon(Icons.calculate), ? const MealDetailScreen()
), : MealDetailScreen(id: _meal!.id),
], ),
), ).then((result) {
Row( onSelectMeal(result?[1]);
mainAxisSize: MainAxisSize.min, reload(message: result?[0]);
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();
}, },
icon: Icon(_meal == null ? Icons.add : Icons.edit),
), ),
), ],
IconButton(
onPressed: () =>
calculateThirdMeasurementOfPortionCarbsRelation(
parameterToBeCalculated:
PortionCarbsParameter.portionSize),
icon: const Icon(Icons.calculate),
),
],
),
AutoCompleteDropdownButton<Accuracy>(
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<Accuracy>(
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<MealSource>(
controller: _mealSourceController,
selectedItem: _mealSource,
label: 'Meal Source',
items: _mealSources,
onChanged: updateMealSource,
),
),
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
_mealSource == null
? const MealSourceDetailScreen()
: MealSourceDetailScreen(
id: _mealSource!.id),
),
).then((result) {
updateMealSource(result?[1]);
reload(message: result?[0]);
});
},
icon: Icon(_mealSource == null
? Icons.add
: Icons.edit),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0),
child: Row(
children: [
Expanded(
child:
AutoCompleteDropdownButton<MealCategory>(
controller: _mealCategoryController,
selectedItem: _mealCategory,
label: 'Meal Category',
items: _mealCategories,
onChanged: updateMealCategory,
),
),
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _mealCategory ==
null
? const MealCategoryDetailScreen()
: MealCategoryDetailScreen(
id: _mealCategory!.id),
),
).then((result) {
updateMealCategory(result?[1]);
reload(message: result?[0]);
});
},
icon: Icon(_mealCategory == null
? Icons.add
: Icons.edit),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0),
child: Row(
children: [
Expanded(
child: AutoCompleteDropdownButton<
MealPortionType>(
controller: _mealPortionTypeController,
selectedItem: _mealPortionType,
label: 'Meal Portion Type',
items: _mealPortionTypes,
onChanged: updateMealPortionType,
),
),
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _mealPortionType ==
null
? const MealPortionTypeDetailScreen()
: MealPortionTypeDetailScreen(
id: _mealPortionType!.id),
),
).then((result) {
updateMealPortionType(result?[1]);
reload(message: result?[0]);
});
},
icon: Icon(_mealPortionType == null
? Icons.add
: Icons.edit),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0),
child: Row(
children: [
Expanded(
child: AutoCompleteDropdownButton<Accuracy>(
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<Accuracy>(
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( bottomNavigationBar: DetailBottomRow(

View File

@ -19,6 +19,8 @@ class LogMealListScreen extends StatefulWidget {
} }
class _LogMealListScreenState extends State<LogMealListScreen> { class _LogMealListScreenState extends State<LogMealListScreen> {
final ScrollController _scrollController = ScrollController();
void reload({String? message}) { void reload({String? message}) {
widget.reload(); widget.reload();
@ -44,7 +46,7 @@ class _LogMealListScreenState extends State<LogMealListScreen> {
id: meal.id, id: meal.id,
), ),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void onDelete(LogMeal logMeal) { void onDelete(LogMeal logMeal) {
@ -67,40 +69,46 @@ class _LogMealListScreenState extends State<LogMealListScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.logMeals.isNotEmpty return widget.logMeals.isNotEmpty
? ListView.builder( ? Scrollbar(
shrinkWrap: true, controller: _scrollController,
itemCount: widget.logMeals.length, child: ListView.builder(
itemBuilder: (context, index) { controller: _scrollController,
final meal = widget.logMeals[index]; shrinkWrap: true,
return ListTile( itemCount: widget.logMeals.length,
onTap: () => handleEditAction(meal), itemBuilder: (context, index) {
title: Row( final meal = widget.logMeals[index];
children: [ return Card(
Expanded(child: Text(meal.value)), child: ListTile(
Expanded( onTap: () => handleEditAction(meal),
child: Column( title: Row(
children: ((meal.carbsPerPortion ?? 0) > 0) children: [
? [ Expanded(child: Text(meal.value)),
Text(meal.carbsPerPortion!.toStringAsPrecision(3)), Expanded(
Text( child: Column(
'${Settings.nutritionMeasurementSuffix} carbs', children: ((meal.carbsPerPortion ?? 0) > 0)
textScaleFactor: 0.75), ? [
] Text(meal.carbsPerPortion!.toStringAsPrecision(3)),
: [], Text(
), '${Settings.nutritionMeasurementSuffix} carbs',
textScaleFactor: 0.75),
]
: [],
),
),
],
),
trailing: IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
), ),
], onPressed: () => handleDeleteAction(meal),
), ),
trailing: IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
), ),
onPressed: () => handleDeleteAction(meal), );
), },
); ),
}, )
)
: const Center( : const Center(
child: Text( child: Text(
'You have not added any Meals to this Log Entry yet!'), 'You have not added any Meals to this Log Entry yet!'),

View File

@ -8,6 +8,8 @@ import 'package:diameter/models/log_event.dart';
import 'package:diameter/models/log_event_type.dart'; import 'package:diameter/models/log_event_type.dart';
import 'package:diameter/models/settings.dart'; import 'package:diameter/models/settings.dart';
import 'package:diameter/navigation.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:diameter/utils/date_time_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -35,6 +37,7 @@ class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
List<BasalProfile> _basalProfiles = []; List<BasalProfile> _basalProfiles = [];
final GlobalKey<FormState> _logEventForm = GlobalKey<FormState>(); final GlobalKey<FormState> _logEventForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
late DateTime _time; late DateTime _time;
DateTime? _endTime; DateTime? _endTime;
@ -49,9 +52,14 @@ class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
final _notesController = TextEditingController(text: ''); final _notesController = TextEditingController(text: '');
LogEventType? _eventType; LogEventType? _eventType;
final _eventTypeController = TextEditingController(text: '');
bool _hasEndTime = false; bool _hasEndTime = false;
BolusProfile? _bolusProfile; BolusProfile? _bolusProfile;
BasalProfile? _basalProfile; BasalProfile? _basalProfile;
final _bolusProfileController = TextEditingController(text: '');
final _basalProfileController = TextEditingController(text: '');
List<LogEventType> _logEventTypes = []; List<LogEventType> _logEventTypes = [];
@ -69,9 +77,16 @@ class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
(_logEvent!.reminderDuration ?? '').toString(); (_logEvent!.reminderDuration ?? '').toString();
_hasEndTime = _logEvent!.hasEndTime; _hasEndTime = _logEvent!.hasEndTime;
_notesController.text = _logEvent!.notes ?? ''; _notesController.text = _logEvent!.notes ?? '';
_eventType = _logEvent!.eventType.target; _eventType = _logEvent!.eventType.target;
_eventTypeController.text = (_eventType ?? '').toString();
_basalProfile = _logEvent!.basalProfile.target; _basalProfile = _logEvent!.basalProfile.target;
_basalProfileController.text = (_basalProfile ?? '').toString();
_bolusProfile = _logEvent!.bolusProfile.target; _bolusProfile = _logEvent!.bolusProfile.target;
_bolusProfileController.text = (_bolusProfile ?? '').toString();
_time = _logEvent!.time; _time = _logEvent!.time;
_endTime = _logEvent!.endTime; _endTime = _logEvent!.endTime;
} else { } else {
@ -115,56 +130,78 @@ class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
_endDateController.text = DateTimeUtils.displayDate(_endTime); _endDateController.text = DateTimeUtils.displayDate(_endTime);
} }
void onSelectEventType(LogEventType eventType) { void updateBasalProfile(BasalProfile? value) {
setState(() { setState(() {
_eventType = eventType; _basalProfile = value;
_hasEndTime = eventType.hasEndTime; _basalProfileController.text = (_basalProfile ?? '').toString();
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();
}
}); });
} }
Future<void> checkIfActiveEventOfTypeExistsBeforeSaving() async { void updateBolusProfile(BolusProfile? value) {
if (_eventType != null && LogEvent.eventTypeExistsForTime(_eventType!.id, _time)) { setState(() {
await showDialog( _bolusProfile = value;
context: context, _bolusProfileController.text = (_bolusProfile ?? '').toString();
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?'), void onSelectEventType(LogEventType? eventType) {
actions: [ setState(() {
TextButton( _eventType = eventType;
onPressed: () => Navigator.pop(context, 'DISCARD'), _eventTypeController.text = (_eventType ?? '').toString();
child: const Text('DISCARD'), });
),
TextButton( if (eventType != null) {
onPressed: () => Navigator.pop(context, 'EDIT'), setState(() {
child: const Text('KEEP EDITING'), _hasEndTime = eventType.hasEndTime;
), if (eventType.defaultReminderDuration != null) {
ElevatedButton( _reminderDurationController.text =
onPressed: () => Navigator.pop(context, 'SAVE'), eventType.defaultReminderDuration.toString();
child: const Text('SAVE'), }
) });
],
); if (eventType.basalProfile.target != null) {
}).then((value) async { updateBasalProfile(eventType.basalProfile.target);
if (value == 'DISCARD') {
Navigator.pop(context);
} else if (value == 'SAVE') {
onSave();
}
});
} else {
onSave();
} }
if (eventType.bolusProfile.target != null) {
updateBolusProfile(eventType.bolusProfile.target);
}
}
}
Future<void> 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() { void onSave() {
@ -180,7 +217,7 @@ class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
event.basalProfile.target = _basalProfile; event.basalProfile.target = _basalProfile;
event.bolusProfile.target = _bolusProfile; event.bolusProfile.target = _bolusProfile;
LogEvent.put(event); LogEvent.put(event);
Navigator.pop(context, '${_isNew ? 'New' : ''} Event Saved'); Navigator.pop(context, ['${_isNew ? 'New' : ''} Event Saved', event]);
} }
void handleSaveAction() async { void handleSaveAction() async {
@ -223,171 +260,221 @@ class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
title: Text(_isNew ? 'New Event' : 'Edit Event'), title: Text(_isNew ? 'New Event' : 'Edit Event'),
), ),
drawer: const Navigation(currentLocation: LogEventDetailScreen.routeName), drawer: const Navigation(currentLocation: LogEventDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _logEventForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
AutoCompleteDropdownButton<LogEventType>( FormWrapper(
selectedItem: _eventType, formState: _logEventForm,
label: 'Event Type', fields: [
items: _logEventTypes, AutoCompleteDropdownButton<LogEventType>(
onChanged: (value) { controller: _eventTypeController,
if (value != null) { selectedItem: _eventType,
onSelectEventType(value); label: 'Event Type',
} items: _logEventTypes,
}, onChanged: onSelectEventType,
),
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 5),
child: DateTimeFormField(
date: _time,
label: _hasEndTime ? 'Start Date' : 'Date',
controller: _dateController,
onChanged: (newTime) {
if (newTime != null) {
setState(() {
_time = DateTime(newTime.year, newTime.month,
newTime.day, _time.hour, _time.minute);
});
updateTime();
}
},
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 5),
child: TimeOfDayFormField(
time: TimeOfDay.fromDateTime(_time),
label: _hasEndTime ? 'Start Time' : 'Time',
controller: _timeController,
onChanged: (newTime) {
if (newTime != null) {
setState(() {
_time = DateTime(_time.year, _time.month,
_time.day, newTime.hour, newTime.minute);
});
updateTime();
}
},
),
),
),
],
),
BooleanFormField(
value: _hasEndTime,
onChanged: (value) {
setState(() {
_hasEndTime = value;
});
},
label: 'has end time',
),
Column(
children: _hasEndTime
? [
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 5),
child: DateTimeFormField(
date: _endTime ?? now,
label: 'End Date',
controller: _endDateController,
onChanged: (newTime) {
if (newTime != null) {
setState(() {
_endTime = DateTime(
newTime.year,
newTime.month,
newTime.day,
_endTime?.hour ?? 0,
_endTime?.minute ?? 0);
});
updateEndTime();
}
},
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 5),
child: TimeOfDayFormField(
time: TimeOfDay.fromDateTime(
_endTime ?? now),
label: 'End Time',
controller: _endTimeController,
onChanged: (newTime) {
if (newTime != null) {
setState(() {
_endTime = DateTime(
_endTime?.year ?? now.year,
_endTime?.month ?? now.month,
_endTime?.day ?? now.day,
newTime.hour,
newTime.minute);
});
updateEndTime();
}
},
),
),
),
],
),
TextFormField(
controller: _reminderDurationController,
keyboardType:
const TextInputType.numberWithOptions(),
decoration: InputDecoration(
labelText: 'Default Reminder Duration',
suffixText: ' min',
enabled: _hasEndTime,
),
),
AutoCompleteDropdownButton<BolusProfile>(
selectedItem: _bolusProfile,
label: 'Bolus Profile',
items: _bolusProfiles,
onChanged: (value) {
setState(() {
_bolusProfile = value;
});
},
),
AutoCompleteDropdownButton<BasalProfile>(
selectedItem: _basalProfile,
label: 'Basal Profile',
items: _basalProfiles,
onChanged: (value) {
setState(() {
_basalProfile = value;
});
},
)
]
: []),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(
labelText: 'Notes',
alignLabelWithHint: true,
), ),
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( bottomNavigationBar: DetailBottomRow(

View File

@ -17,6 +17,7 @@ class LogEventListScreen extends StatefulWidget {
class _LogEventListScreenState extends State<LogEventListScreen> { class _LogEventListScreenState extends State<LogEventListScreen> {
List<LogEvent> _activeEvents = []; List<LogEvent> _activeEvents = [];
late Map<DateTime, List<LogEvent>> _logEventDailyMap; late Map<DateTime, List<LogEvent>> _logEventDailyMap;
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
@ -51,7 +52,7 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
return const LogEventDetailScreen(); return const LogEventDetailScreen();
}, },
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void handleEditAction(LogEvent event) { void handleEditAction(LogEvent event) {
@ -62,7 +63,7 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
id: event.id, id: event.id,
), ),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
} }
void onDelete(LogEvent logEvent) { void onDelete(LogEvent logEvent) {
@ -116,92 +117,104 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
children: [ children: [
Expanded( Expanded(
child: _logEventDailyMap.isNotEmpty child: _logEventDailyMap.isNotEmpty
? ListView.builder( ? Scrollbar(
shrinkWrap: true, controller: _scrollController,
padding: const EdgeInsets.all(10.0), child: ListView.builder(
itemCount: _logEventDailyMap.length, controller: _scrollController,
itemBuilder: (context, dateIndex) { shrinkWrap: true,
List<DateTime?> dateList = (_activeEvents.isNotEmpty padding: const EdgeInsets.all(10.0),
? <DateTime?>[null] itemCount: _logEventDailyMap.length,
: <DateTime?>[]) + itemBuilder: (context, dateIndex) {
_logEventDailyMap.keys.toList(); List<DateTime?> dateList = (_activeEvents.isNotEmpty
final date = dateList[dateIndex]; ? <DateTime?>[null]
final eventList = date != null : <DateTime?>[]) +
? _logEventDailyMap[date] _logEventDailyMap.keys.toList();
: _activeEvents; final date = dateList[dateIndex];
final tiles = <Widget>[]; final eventList = date != null
if (eventList != null) { ? _logEventDailyMap[date]
for (LogEvent event in eventList) { : _activeEvents;
tiles.add(ListTile( final tiles = <Widget>[];
onTap: () { if (eventList != null) {
handleEditAction(event); for (LogEvent event in eventList) {
}, tiles.add(Card(
title: Row( child: ListTile(
crossAxisAlignment: onTap: () {
CrossAxisAlignment.center, handleEditAction(event);
mainAxisSize: MainAxisSize.max, },
children: [ title: Row(
Expanded( crossAxisAlignment:
child: Text(date == null CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(date == null
? DateTimeUtils ? DateTimeUtils
.displayDateTime( .displayDateTime(
event.time) event.time)
: DateTimeUtils.displayTime( : DateTimeUtils.displayTime(
event.isEndEvent event.isEndEvent
? event.endTime ? event.endTime
: event.time))), : 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,
), ),
onPressed: () => const SizedBox(width: 24),
handleStopAction(event), Expanded(
) child: Text(
: const SizedBox(width: 50), event.title ?? event.eventType.target?.value ?? '',
IconButton( // style: Theme.of(context).textTheme.subtitle2,
icon: const Icon( ),
Icons.edit, ),
color: Colors.blue, ],
),
onPressed: () =>
handleEditAction(event),
), ),
IconButton( trailing: Row(
icon: const Icon( mainAxisSize: MainAxisSize.min,
Icons.delete, children: [
color: Colors.blue, event.hasEndTime &&
), event.endTime == null
onPressed: () => ? IconButton(
handleDeleteAction(event), 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(
return eventList != null && eventList.isNotEmpty ? ListBody( children: <Widget>[
children: <Widget>[ Padding(
Text(DateTimeUtils.displayDate(date, padding: const EdgeInsets.all(10.0),
fallback: 'Active Events')) child: Text(
] + tiles, DateTimeUtils.displayDate(date, fallback: 'Active Events').toUpperCase(),
style: Theme.of(context).textTheme.subtitle2,
) : Container(); ),
}, ),
) ] + tiles +
[const Divider()],
) : Container();
},
),
)
: const Center( : const Center(
child: Text('There are no Events!'), child: Text('There are no Events!'),
), ),

View File

@ -7,6 +7,8 @@ import 'package:diameter/models/bolus_profile.dart';
import 'package:diameter/models/log_event_type.dart'; import 'package:diameter/models/log_event_type.dart';
import 'package:diameter/models/settings.dart'; import 'package:diameter/models/settings.dart';
import 'package:diameter/navigation.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'; import 'package:flutter/material.dart';
class EventTypeDetailScreen extends StatefulWidget { class EventTypeDetailScreen extends StatefulWidget {
@ -15,8 +17,7 @@ class EventTypeDetailScreen extends StatefulWidget {
const EventTypeDetailScreen({Key? key, this.id = 0}) : super(key: key); const EventTypeDetailScreen({Key? key, this.id = 0}) : super(key: key);
@override @override
_EventTypeDetailScreenState createState() => _EventTypeDetailScreenState createState() => _EventTypeDetailScreenState();
_EventTypeDetailScreenState();
} }
class _EventTypeDetailScreenState extends State<EventTypeDetailScreen> { class _EventTypeDetailScreenState extends State<EventTypeDetailScreen> {
@ -28,13 +29,17 @@ class _EventTypeDetailScreenState extends State<EventTypeDetailScreen> {
List<BasalProfile> _basalProfiles = []; List<BasalProfile> _basalProfiles = [];
final GlobalKey<FormState> _logEventTypeForm = GlobalKey<FormState>(); final GlobalKey<FormState> _logEventTypeForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
final _valueController = TextEditingController(text: ''); final _valueController = TextEditingController(text: '');
final _defaultReminderDurationController = TextEditingController(text: ''); final _defaultReminderDurationController = TextEditingController(text: '');
final _notesController = TextEditingController(text: ''); final _notesController = TextEditingController(text: '');
bool _hasEndTime = false; bool _hasEndTime = false;
BolusProfile? _bolusProfile; BolusProfile? _bolusProfile;
BasalProfile? _basalProfile; BasalProfile? _basalProfile;
final _bolusProfileController = TextEditingController(text: '');
final _basalProfileController = TextEditingController(text: '');
@override @override
void initState() { void initState() {
@ -52,17 +57,45 @@ class _EventTypeDetailScreenState extends State<EventTypeDetailScreen> {
_hasEndTime = _logEventType!.hasEndTime; _hasEndTime = _logEventType!.hasEndTime;
_notesController.text = _logEventType!.notes ?? ''; _notesController.text = _logEventType!.notes ?? '';
_basalProfile = _logEventType!.basalProfile.target; _basalProfile = _logEventType!.basalProfile.target;
_basalProfileController.text = (_basalProfile ?? '').toString();
_bolusProfile = _logEventType!.bolusProfile.target; _bolusProfile = _logEventType!.bolusProfile.target;
_bolusProfileController.text = (_bolusProfile ?? '').toString();
} }
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_logEventType = LogEventType.get(widget.id); _logEventType = LogEventType.get(widget.id);
}); });
} }
_isNew = _logEventType == null; _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 { void handleSaveAction() async {
@ -81,7 +114,8 @@ class _EventTypeDetailScreenState extends State<EventTypeDetailScreen> {
eventType.basalProfile.target = _basalProfile; eventType.basalProfile.target = _basalProfile;
eventType.bolusProfile.target = _bolusProfile; eventType.bolusProfile.target = _bolusProfile;
LogEventType.put(eventType); LogEventType.put(eventType);
Navigator.pop(context, '${_isNew ? 'New' : ''} Log Event Type Saved'); Navigator.pop(
context, ['${_isNew ? 'New' : ''} Log Event Type Saved', eventType]);
} }
setState(() { setState(() {
_isSaving = false; _isSaving = false;
@ -121,18 +155,18 @@ class _EventTypeDetailScreenState extends State<EventTypeDetailScreen> {
), ),
drawer: drawer:
const Navigation(currentLocation: EventTypeDetailScreen.routeName), const Navigation(currentLocation: EventTypeDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _logEventTypeForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
FormWrapper(formState: _logEventTypeForm, fields: [
TextFormField( TextFormField(
controller: _valueController, controller: _valueController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Name', labelText: 'Name',
alignLabelWithHint: true,
), ),
validator: (value) { validator: (value) {
if (value!.trim().isEmpty) { if (value!.trim().isEmpty) {
@ -151,48 +185,110 @@ class _EventTypeDetailScreenState extends State<EventTypeDetailScreen> {
}, },
), ),
Column( Column(
children: _hasEndTime ? [ children: _hasEndTime
TextFormField( ? [
controller: _defaultReminderDurationController, Padding(
keyboardType: const TextInputType.numberWithOptions(), padding: const EdgeInsets.only(bottom: 10.0),
decoration: InputDecoration( child: TextFormField(
labelText: 'Default Reminder Duration', controller: _defaultReminderDurationController,
suffixText: ' min', keyboardType:
enabled: _hasEndTime, const TextInputType.numberWithOptions(),
), decoration: InputDecoration(
), labelText: 'Default Reminder Duration',
AutoCompleteDropdownButton<BolusProfile>( suffixText: ' min',
selectedItem: _bolusProfile, enabled: _hasEndTime,
label: 'Bolus Profile', ),
items: _bolusProfiles, ),
onChanged: (value) { ),
setState(() { Padding(
_bolusProfile = value; padding: const EdgeInsets.only(bottom: 10.0),
}); child: Row(
}, children: [
), Expanded(
AutoCompleteDropdownButton<BasalProfile>( child: AutoCompleteDropdownButton<
selectedItem: _basalProfile, BolusProfile>(
label: 'Basal Profile', selectedItem: _bolusProfile,
items: _basalProfiles, controller: _bolusProfileController,
onChanged: (value) { label: 'Bolus Profile',
setState(() { items: _bolusProfiles,
_basalProfile = value; 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<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( TextFormField(
controller: _notesController, controller: _notesController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Notes', labelText: 'Notes',
alignLabelWithHint: true,
), ),
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
minLines: 2,
maxLines: 5,
), ),
] ]),
), ],
], ),
), ),
), ),
bottomNavigationBar: DetailBottomRow( bottomNavigationBar: DetailBottomRow(

View File

@ -14,6 +14,8 @@ class LogEventTypeListScreen extends StatefulWidget {
class _LogEventTypeListScreenState extends State<LogEventTypeListScreen> { class _LogEventTypeListScreenState extends State<LogEventTypeListScreen> {
List<LogEventType> _logEventTypes = []; List<LogEventType> _logEventTypes = [];
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -49,43 +51,53 @@ class _LogEventTypeListScreenState extends State<LogEventTypeListScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: _logEventTypes.isNotEmpty ? ListView.builder( child: _logEventTypes.isNotEmpty
padding: const EdgeInsets.all(10.0), ? Scrollbar(
itemCount: _logEventTypes.length, controller: _scrollController,
itemBuilder: (context, index) { child: ListView.builder(
final logEventType = _logEventTypes[index]; controller: _scrollController,
return ListTile( padding: const EdgeInsets.all(10.0),
onTap: () { itemCount: _logEventTypes.length,
Navigator.push( itemBuilder: (context, index) {
context, final logEventType = _logEventTypes[index];
MaterialPageRoute( return Card(
builder: (context) => child: ListTile(
EventTypeDetailScreen( onTap: () {
id: logEventType.id), Navigator.push(
), context,
).then((message) => reload(message: message)); MaterialPageRoute(
}, builder: (context) =>
title: Text(logEventType.value), EventTypeDetailScreen(id: logEventType.id),
subtitle: Text(logEventType.notes ?? ''), ),
trailing: Row( ).then((result) => reload(message: result?[0]));
mainAxisSize: MainAxisSize.min, },
children: [ title: Text(
IconButton( logEventType.value.toUpperCase(),
onPressed: () async { style: Theme.of(context).textTheme.subtitle2,
LogEventType.remove(logEventType.id); ),
reload( subtitle: Text(logEventType.notes ?? ''),
message: 'Log Event Type deleted'); trailing: Row(
}, mainAxisSize: MainAxisSize.min,
icon: const Icon(Icons.delete, children: [
color: Colors.blue), 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<LogEventTypeListScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const EventTypeDetailScreen(), builder: (context) => const EventTypeDetailScreen(),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),

View File

@ -22,6 +22,7 @@ class _MealCategoryDetailScreenState extends State<MealCategoryDetailScreen> {
bool _isNew = true; bool _isNew = true;
final GlobalKey<FormState> _mealCategoryForm = GlobalKey<FormState>(); final GlobalKey<FormState> _mealCategoryForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
final _valueController = TextEditingController(text: ''); final _valueController = TextEditingController(text: '');
final _notesController = TextEditingController(text: ''); final _notesController = TextEditingController(text: '');
@ -37,22 +38,38 @@ class _MealCategoryDetailScreenState extends State<MealCategoryDetailScreen> {
} }
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_mealCategory = MealCategory.get(widget.id); _mealCategory = MealCategory.get(widget.id);
}); });
} }
_isNew = _mealCategory == null; _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 { void handleSaveAction() async {
if (_mealCategoryForm.currentState!.validate()) { if (_mealCategoryForm.currentState!.validate()) {
MealCategory.put(MealCategory( MealCategory mealCategory = MealCategory(
id: widget.id, id: widget.id,
value: _valueController.text, value: _valueController.text,
notes: _notesController.text)); notes: _notesController.text,
Navigator.pop(context, '${_isNew ? 'New' : ''} Meal Category saved'); );
MealCategory.put(mealCategory);
Navigator.pop(context, [
'${_isNew ? 'New' : ''} Meal Category saved', mealCategory
]);
} }
} }
@ -81,36 +98,41 @@ class _MealCategoryDetailScreenState extends State<MealCategoryDetailScreen> {
), ),
drawer: drawer:
const Navigation(currentLocation: MealCategoryDetailScreen.routeName), const Navigation(currentLocation: MealCategoryDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _mealCategoryForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
TextFormField( FormWrapper(
controller: _valueController, formState: _mealCategoryForm,
decoration: const InputDecoration( fields: [
labelText: 'Name', TextFormField(
controller: _valueController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty name';
}
return null;
},
), ),
validator: (value) { TextFormField(
if (value!.trim().isEmpty) { controller: _notesController,
return 'Empty name'; decoration: const InputDecoration(
} labelText: 'Notes',
return null; ),
}, keyboardType: TextInputType.multiline,
), minLines: 2,
TextFormField( maxLines: 5,
controller: _notesController,
decoration: const InputDecoration(
labelText: 'Notes',
alignLabelWithHint: true,
), ),
keyboardType: TextInputType.multiline, ],
), ),
], ],
), ),
],
), ),
), ),
bottomNavigationBar: DetailBottomRow( bottomNavigationBar: DetailBottomRow(

View File

@ -17,6 +17,8 @@ class MealCategoryListScreen extends StatefulWidget {
class _MealCategoryListScreenState extends State<MealCategoryListScreen> { class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
List<MealCategory> _mealCategories = []; List<MealCategory> _mealCategories = [];
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -75,41 +77,48 @@ class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: _mealCategories.isNotEmpty ? ListView.builder( child: _mealCategories.isNotEmpty ? Scrollbar(
padding: const EdgeInsets.only(top: 10.0), controller: _scrollController,
itemCount: _mealCategories.length, child: ListView.builder(
itemBuilder: (context, index) { controller: _scrollController,
final mealCategory = _mealCategories[index]; padding: const EdgeInsets.all(10.0),
itemCount: _mealCategories.length,
return ListTile( itemBuilder: (context, index) {
onTap: () { final mealCategory = _mealCategories[index];
Navigator.push( return Card(
context, child: ListTile(
MaterialPageRoute( onTap: () {
builder: (context) => Navigator.push(
MealCategoryDetailScreen( context,
id: mealCategory.id, 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)); trailing: Row(
}, mainAxisSize: MainAxisSize.min,
title: Text(mealCategory.value), children: [
subtitle: Text(mealCategory.notes ?? ''), IconButton(
trailing: Row( icon: const Icon(
mainAxisSize: MainAxisSize.min, Icons.delete,
children: [ color: Colors.blue,
IconButton( ),
icon: const Icon( onPressed: () =>
Icons.delete, handleDeleteAction(mealCategory),
color: Colors.blue, ),
), ],
onPressed: () =>
handleDeleteAction(mealCategory),
), ),
], ),
), );
); },
}, ),
): const Center( ): const Center(
child: Text('You have not created any Meal Categories yet!'), child: Text('You have not created any Meal Categories yet!'),
), ),
@ -123,7 +132,7 @@ class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const MealCategoryDetailScreen(), builder: (context) => const MealCategoryDetailScreen(),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),

View File

@ -9,6 +9,10 @@ import 'package:diameter/models/meal_portion_type.dart';
import 'package:diameter/models/meal_source.dart'; import 'package:diameter/models/meal_source.dart';
import 'package:diameter/models/settings.dart'; import 'package:diameter/models/settings.dart';
import 'package:diameter/navigation.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:diameter/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -24,10 +28,13 @@ class MealDetailScreen extends StatefulWidget {
class _MealDetailScreenState extends State<MealDetailScreen> { class _MealDetailScreenState extends State<MealDetailScreen> {
Meal? _meal; Meal? _meal;
bool _isNew = true; bool _isNew = true;
bool _isSaving = false; bool _isSaving = false;
bool _isExpanded = false;
final GlobalKey<FormState> _mealForm = GlobalKey<FormState>(); final GlobalKey<FormState> _mealForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
final _valueController = TextEditingController(text: ''); final _valueController = TextEditingController(text: '');
final _carbsRatioController = TextEditingController(text: ''); final _carbsRatioController = TextEditingController(text: '');
@ -44,6 +51,12 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
Accuracy? _portionSizeAccuracy; Accuracy? _portionSizeAccuracy;
Accuracy? _carbsRatioAccuracy; Accuracy? _carbsRatioAccuracy;
final _mealSourceController = TextEditingController(text: '');
final _mealCategoryController = TextEditingController(text: '');
final _mealPortionTypeController = TextEditingController(text: '');
final _portionSizeAccuracyController = TextEditingController(text: '');
final _carbsRatioAccuracyController = TextEditingController(text: '');
List<MealCategory> _mealCategories = []; List<MealCategory> _mealCategories = [];
List<MealPortionType> _mealPortionTypes = []; List<MealPortionType> _mealPortionTypes = [];
List<MealSource> _mealSources = []; List<MealSource> _mealSources = [];
@ -68,27 +81,75 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
_portionSizeController.text = (_meal!.portionSize ?? '').toString(); _portionSizeController.text = (_meal!.portionSize ?? '').toString();
_carbsPerPortionController.text = _carbsPerPortionController.text =
(_meal!.carbsPerPortion ?? '').toString(); (_meal!.carbsPerPortion ?? '').toString();
_delayedBolusPercentage = _delayedBolusPercentage = _meal!.delayedBolusPercentage ?? 0;
_meal!.delayedBolusPercentage ?? 0;
_delayedBolusDurationController.text = _delayedBolusDurationController.text =
(_meal!.delayedBolusDuration ?? '').toString(); (_meal!.delayedBolusDuration ?? '').toString();
_notesController.text = _meal!.notes ?? ''; _notesController.text = _meal!.notes ?? '';
_mealSource = _meal!.mealSource.target; _mealSource = _meal!.mealSource.target;
_mealSourceController.text = (_mealSource ?? '').toString();
_mealCategory = _meal!.mealCategory.target; _mealCategory = _meal!.mealCategory.target;
_mealCategoryController.text = (_mealCategory ?? '').toString();
_mealPortionType = _meal!.mealPortionType.target; _mealPortionType = _meal!.mealPortionType.target;
_mealPortionTypeController.text = (_mealPortionType ?? '').toString();
_portionSizeAccuracy = _meal!.portionSizeAccuracy.target; _portionSizeAccuracy = _meal!.portionSizeAccuracy.target;
_portionSizeAccuracyController.text =
(_portionSizeAccuracy ?? '').toString();
_carbsRatioAccuracy = _meal!.carbsRatioAccuracy.target; _carbsRatioAccuracy = _meal!.carbsRatioAccuracy.target;
_carbsRatioAccuracyController.text =
(_carbsRatioAccuracy ?? '').toString();
} }
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_meal = Meal.get(widget.id); _meal = Meal.get(widget.id);
}); });
} }
_isNew = _meal == null; _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 { void handleSaveAction() async {
@ -114,7 +175,7 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
meal.carbsRatioAccuracy.target = _carbsRatioAccuracy; meal.carbsRatioAccuracy.target = _carbsRatioAccuracy;
Meal.put(meal); Meal.put(meal);
Navigator.pop(context, '${_isNew ? 'New' : ''} Meal Saved'); Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Saved', meal]);
} }
setState(() { setState(() {
_isSaving = false; _isSaving = false;
@ -152,8 +213,7 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
_portionSizeAccuracy != _meal!.portionSizeAccuracy.target || _portionSizeAccuracy != _meal!.portionSizeAccuracy.target ||
int.tryParse(_delayedBolusDurationController.text) != int.tryParse(_delayedBolusDurationController.text) !=
_meal!.delayedBolusDuration || _meal!.delayedBolusDuration ||
_delayedBolusPercentage != _delayedBolusPercentage != _meal!.delayedBolusPercentage ||
_meal!.delayedBolusPercentage ||
_notesController.text != (_meal!.notes ?? ''))))) { _notesController.text != (_meal!.notes ?? ''))))) {
Dialogs.showCancelConfirmationDialog( Dialogs.showCancelConfirmationDialog(
context: context, context: context,
@ -165,40 +225,40 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
} }
} }
Future<void> onSelectMealSource(MealSource mealSource) async { Future<void> onSelectMealSource(MealSource? mealSource) async {
setState(() { setState(() {
_mealSource = mealSource; _mealSource = mealSource;
_mealSourceController.text = _mealSource.toString();
});
if (mealSource != null) {
if (mealSource.defaultCarbsRatioAccuracy.hasValue) { if (mealSource.defaultCarbsRatioAccuracy.hasValue) {
_carbsRatioAccuracy = mealSource.defaultCarbsRatioAccuracy.target; updateCarbsRatioAccuracy(mealSource.defaultCarbsRatioAccuracy.target);
} }
if (mealSource.defaultPortionSizeAccuracy.hasValue) { if (mealSource.defaultPortionSizeAccuracy.hasValue) {
_portionSizeAccuracy = mealSource.defaultPortionSizeAccuracy.target; updatePortionSizeAccuracy(mealSource.defaultPortionSizeAccuracy.target);
} }
if (mealSource.defaultMealCategory.hasValue) { if (mealSource.defaultMealCategory.hasValue) {
_mealCategory = mealSource.defaultMealCategory.target; updateMealCategory(mealSource.defaultMealCategory.target);
} }
if (mealSource.defaultMealPortionType.hasValue) { if (mealSource.defaultMealPortionType.hasValue) {
_mealPortionType = mealSource.defaultMealPortionType.target; updateMealPortionType(mealSource.defaultMealPortionType.target);
} }
}); }
} }
void calculateThirdMeasurementOfPortionCarbsRelation( void calculateThirdMeasurementOfPortionCarbsRelation(
{PortionCarbsParameter? parameterToBeCalculated}) { {PortionCarbsParameter? changedParameter}) {
double? carbsRatio; double? carbsRatio;
double? portionSize; double? portionSize;
double? carbsPerPortion; double? carbsPerPortion;
if (parameterToBeCalculated != PortionCarbsParameter.carbsRatio && if (_carbsRatioController.text != '') {
_carbsRatioController.text != '') {
carbsRatio = double.tryParse(_carbsRatioController.text); carbsRatio = double.tryParse(_carbsRatioController.text);
} }
if (parameterToBeCalculated != PortionCarbsParameter.portionSize && if (_portionSizeController.text != '') {
_portionSizeController.text != '') {
portionSize = double.tryParse(_portionSizeController.text); portionSize = double.tryParse(_portionSizeController.text);
} }
if (parameterToBeCalculated != PortionCarbsParameter.carbsPerPortion && if (_carbsRatioController.text != '') {
_carbsRatioController.text != '') {
carbsPerPortion = double.tryParse(_carbsPerPortionController.text); carbsPerPortion = double.tryParse(_carbsPerPortionController.text);
} }
@ -232,59 +292,92 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
title: Text(_isNew ? 'New Meal' : _meal!.value), title: Text(_isNew ? 'New Meal' : _meal!.value),
), ),
drawer: const Navigation(currentLocation: MealDetailScreen.routeName), drawer: const Navigation(currentLocation: MealDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _mealForm, children: <Widget>[
fields: [ FormWrapper(
TextFormField( formState: _mealForm,
controller: _valueController, fields: [
decoration: const InputDecoration( TextFormField(
labelText: 'Name', controller: _valueController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty name';
}
return null;
},
), ),
validator: (value) { Row(
if (value!.trim().isEmpty) { children: [
return 'Empty name'; Expanded(
} child: AutoCompleteDropdownButton<MealSource>(
return null; controller: _mealSourceController,
}, selectedItem: _mealSource,
), label: 'Meal Source',
AutoCompleteDropdownButton<MealSource>( items: _mealSources,
selectedItem: _mealSource, onChanged: onSelectMealSource,
label: 'Meal Source', ),
items: _mealSources, ),
onChanged: (value) { IconButton(
if (value != null) { onPressed: () {
onSelectMealSource(value); Navigator.push(
} context,
}, MaterialPageRoute(
), builder: (context) => _mealSource == null
AutoCompleteDropdownButton<MealCategory>( ? const MealSourceDetailScreen()
selectedItem: _mealCategory, : MealSourceDetailScreen(id: _mealSource!.id),
label: 'Meal Category', ),
items: _mealCategories, ).then((result) {
onChanged: (value) { onSelectMealSource(result?[1]);
setState(() { reload(message: result?[0]);
_mealCategory = value; });
}); },
}, icon:
), Icon(_mealSource == null ? Icons.add : Icons.edit),
AutoCompleteDropdownButton<MealPortionType>( ),
selectedItem: _mealPortionType, ],
label: 'Meal Portion Type', ),
items: _mealPortionTypes, Row(
onChanged: (value) { children: [
setState(() { Expanded(
_mealPortionType = value; child: AutoCompleteDropdownButton<MealPortionType>(
}); controller: _mealPortionTypeController,
}, selectedItem: _mealPortionType,
), label: 'Meal Portion Type',
Row( items: _mealPortionTypes,
children: [ onChanged: updateMealPortionType,
Expanded( ),
child: TextFormField( ),
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( decoration: const InputDecoration(
labelText: 'Carbs ratio', labelText: 'Carbs ratio',
suffixText: '%', suffixText: '%',
@ -296,134 +389,239 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
calculateThirdMeasurementOfPortionCarbsRelation(); 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();
},
), ),
), const SizedBox(width: 10),
IconButton(
onPressed: () =>
calculateThirdMeasurementOfPortionCarbsRelation(
parameterToBeCalculated:
PortionCarbsParameter.portionSize),
icon: const Icon(Icons.calculate),
),
],
),
AutoCompleteDropdownButton<Accuracy>(
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<Accuracy>(
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:'),
Expanded( Expanded(
child: Slider( child: TextFormField(
label: '${_delayedBolusPercentage.floor().toString()}%', decoration: InputDecoration(
divisions: 100, labelText: 'Portion size',
value: _delayedBolusPercentage, suffixText: Settings.nutritionMeasurementSuffix,
min: 0, ),
max: 100, controller: _portionSizeController,
onChanged: (value) { keyboardType: const TextInputType.numberWithOptions(
setState(() { decimal: true),
_delayedBolusPercentage = value; 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(
TextFormField( controller: _notesController,
controller: _notesController, decoration: const InputDecoration(
decoration: const InputDecoration( labelText: 'Notes',
labelText: 'Notes', ),
alignLabelWithHint: true, 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<Accuracy>(
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<Accuracy>(
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( bottomNavigationBar: DetailBottomRow(

View File

@ -17,6 +17,8 @@ class MealListScreen extends StatefulWidget {
class _MealListScreenState extends State<MealListScreen> { class _MealListScreenState extends State<MealListScreen> {
List<Meal> _meals = []; List<Meal> _meals = [];
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -68,70 +70,87 @@ class _MealListScreenState extends State<MealListScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: _meals.isNotEmpty ? ListView.builder( child: _meals.isNotEmpty ? Scrollbar(
padding: const EdgeInsets.all(10.0), controller: _scrollController,
itemCount: _meals.length, child: ListView.builder(
itemBuilder: (context, index) { controller: _scrollController,
final meal = _meals[index]; padding: const EdgeInsets.all(10.0),
String portionType = meal.mealPortionType.hasValue ? ' per ${meal.mealPortionType.target!.value}' : ''; itemCount: _meals.length,
return ListTile( itemBuilder: (context, index) {
isThreeLine: true, final meal = _meals[index];
onTap: () { String portionType = meal.mealPortionType.hasValue ? ' per ${meal.mealPortionType.target!.value}' : '';
Navigator.push( return Card(
context, child: ListTile(
MaterialPageRoute( isThreeLine: true,
builder: (context) => onTap: () {
MealDetailScreen(id: meal.id), 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)); subtitle: Padding(
}, padding: const EdgeInsets.symmetric(vertical: 10.0),
title: Text(meal.value), child: Row(
subtitle: Row( children: [
children: [ Column(
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: [ children: [
Text(meal.mealSource.target?.value ?? ''), IconButton(
Text(meal.notes ?? ''), 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( ): const Center(
child: Text('You have not created any Meals yet!'), child: Text('You have not created any Meals yet!'),
), ),
@ -145,7 +164,7 @@ class _MealListScreenState extends State<MealListScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const MealDetailScreen(), builder: (context) => const MealDetailScreen(),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),

View File

@ -24,6 +24,7 @@ class _MealPortionTypeDetailScreenState
bool _isNew = true; bool _isNew = true;
final GlobalKey<FormState> _mealPortionTypeForm = GlobalKey<FormState>(); final GlobalKey<FormState> _mealPortionTypeForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
final _valueController = TextEditingController(text: ''); final _valueController = TextEditingController(text: '');
final _notesController = TextEditingController(text: ''); final _notesController = TextEditingController(text: '');
@ -39,23 +40,37 @@ class _MealPortionTypeDetailScreenState
} }
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_mealPortionType = MealPortionType.get(widget.id); _mealPortionType = MealPortionType.get(widget.id);
}); });
} }
_isNew = _mealPortionType == null; _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 { void handleSaveAction() async {
if (_mealPortionTypeForm.currentState!.validate()) { if (_mealPortionTypeForm.currentState!.validate()) {
MealPortionType.put(MealPortionType( MealPortionType mealPortionType = MealPortionType(
id: _mealPortionType?.id ?? 0, id: _mealPortionType?.id ?? 0,
value: _valueController.text, value: _valueController.text,
notes: _notesController.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; bool isNew = _mealPortionType == null;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(isNew ? 'New Meal Portion Type' : _mealPortionType!.value),
isNew ? 'New Meal Portion Type' : _mealPortionType!.value),
), ),
drawer: const Navigation( drawer: const Navigation(
currentLocation: MealPortionTypeDetailScreen.routeName), currentLocation: MealPortionTypeDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _mealPortionTypeForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
TextFormField( FormWrapper(
controller: _valueController, formState: _mealPortionTypeForm,
decoration: const InputDecoration( fields: [
labelText: 'Name', TextFormField(
controller: _valueController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty name';
}
return null;
},
), ),
validator: (value) { TextFormField(
if (value!.trim().isEmpty) { controller: _notesController,
return 'Empty name'; decoration: const InputDecoration(
} labelText: 'Notes',
return null; ),
}, keyboardType: TextInputType.multiline,
), minLines: 2,
TextFormField( maxLines: 5,
controller: _notesController, )
decoration: const InputDecoration( ],
labelText: 'Notes', ),
alignLabelWithHint: true, ],
), ),
keyboardType: TextInputType.multiline,
)
],
),
],
), ),
), ),
bottomNavigationBar: DetailBottomRow( bottomNavigationBar: DetailBottomRow(

View File

@ -18,6 +18,8 @@ class MealPortionTypeListScreen extends StatefulWidget {
class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> { class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
List<MealPortionType> _mealPortionTypes = []; List<MealPortionType> _mealPortionTypes = [];
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -73,43 +75,50 @@ class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: _mealPortionTypes.isNotEmpty ? ListView.builder( child: _mealPortionTypes.isNotEmpty ? Scrollbar(
padding: const EdgeInsets.only(top: 10.0), controller: _scrollController,
itemCount: _mealPortionTypes.length, child: ListView.builder(
itemBuilder: (context, index) { controller: _scrollController,
final mealPortionType = _mealPortionTypes[index]; padding: const EdgeInsets.all(10.0),
itemCount: _mealPortionTypes.length,
return ListTile( itemBuilder: (context, index) {
onTap: () { final mealPortionType = _mealPortionTypes[index];
Navigator.push( return Card(
context, child: ListTile(
MaterialPageRoute( onTap: () {
builder: (context) => Navigator.push(
MealPortionTypeDetailScreen( context,
id: mealPortionType.id, MaterialPageRoute(
), builder: (context) =>
MealPortionTypeDetailScreen(
id: mealPortionType.id,
),
),
).then(
(message) => reload(message: message));
},
title: Text(
mealPortionType.value.toUpperCase(),
style: Theme.of(context).textTheme.subtitle2,
), ),
).then( trailing: Row(
(message) => reload(message: message)); mainAxisSize: MainAxisSize.min,
}, children: [
title: Text(mealPortionType.value), IconButton(
subtitle: Text(mealPortionType.notes ?? ''), icon: const Icon(
trailing: Row( Icons.delete,
mainAxisSize: MainAxisSize.min, color: Colors.blue,
children: [ ),
IconButton( onPressed: () =>
icon: const Icon( handleDeleteAction(mealPortionType),
Icons.delete, ),
color: Colors.blue, ],
),
onPressed: () =>
handleDeleteAction(mealPortionType),
), ),
], ),
), );
); },
}, ),
) : const Center( ) : const Center(
child: Text('You have not created any Meal Portion Types yet!'), child: Text('You have not created any Meal Portion Types yet!'),
), ),
), ),
@ -122,7 +131,7 @@ class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const MealPortionTypeDetailScreen(), builder: (context) => const MealPortionTypeDetailScreen(),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),

View File

@ -8,6 +8,9 @@ import 'package:diameter/models/meal_portion_type.dart';
import 'package:diameter/models/meal_source.dart'; import 'package:diameter/models/meal_source.dart';
import 'package:diameter/models/settings.dart'; import 'package:diameter/models/settings.dart';
import 'package:diameter/navigation.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'; import 'package:flutter/material.dart';
class MealSourceDetailScreen extends StatefulWidget { class MealSourceDetailScreen extends StatefulWidget {
@ -31,13 +34,19 @@ class _MealSourceDetailScreenState extends State<MealSourceDetailScreen> {
List<MealPortionType> _mealPortionTypes = []; List<MealPortionType> _mealPortionTypes = [];
final GlobalKey<FormState> _mealSourceForm = GlobalKey<FormState>(); final GlobalKey<FormState> _mealSourceForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
final _valueController = TextEditingController(text: ''); final _valueController = TextEditingController(text: '');
final _notesController = TextEditingController(text: ''); final _notesController = TextEditingController(text: '');
Accuracy? _defaultCarbsRatioAccuracy; Accuracy? _defaultCarbsRatioAccuracy;
Accuracy? _defaultPortionSizeAccuracy; Accuracy? _defaultPortionSizeAccuracy;
MealCategory? _defaultMealCategory; MealCategory? _defaultMealCategory;
MealPortionType? _defaultMealPortionType; MealPortionType? _defaultMealPortionType;
final _defaultCarbsRatioAccuracyController = TextEditingController(text: '');
final _defaultPortionSizeAccuracyController = TextEditingController(text: '');
final _defaultMealCategoryController = TextEditingController(text: '');
final _defaultMealPortionTypeController = TextEditingController(text: '');
@override @override
void initState() { void initState() {
@ -56,37 +65,51 @@ class _MealSourceDetailScreenState extends State<MealSourceDetailScreen> {
_defaultPortionSizeAccuracy = _defaultPortionSizeAccuracy =
_mealSource!.defaultPortionSizeAccuracy.target; _mealSource!.defaultPortionSizeAccuracy.target;
_defaultPortionSizeAccuracyController.text = (_defaultPortionSizeAccuracy ?? '').toString();
_defaultCarbsRatioAccuracy = _defaultCarbsRatioAccuracy =
_mealSource!.defaultCarbsRatioAccuracy.target; _mealSource!.defaultCarbsRatioAccuracy.target;
_defaultCarbsRatioAccuracyController.text = (_defaultCarbsRatioAccuracy ?? '').toString();
_defaultMealCategory = _mealSource!.defaultMealCategory.target; _defaultMealCategory = _mealSource!.defaultMealCategory.target;
_defaultMealPortionType = _defaultMealCategoryController.text = (_defaultMealCategory ?? '').toString();
_mealSource!.defaultMealPortionType.target; _defaultMealPortionType = _mealSource!.defaultMealPortionType.target;
_defaultMealPortionTypeController.text = (_defaultMealPortionType ?? '').toString();
} }
} }
void reload() { void reload({String? message}) {
if (widget.id != 0) { if (widget.id != 0) {
setState(() { setState(() {
_mealSource = MealSource.get(widget.id); _mealSource = MealSource.get(widget.id);
}); });
} }
_isNew = _mealSource == null; _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 { void handleSaveAction() async {
MealSource mealSource = MealSource( MealSource mealSource = MealSource(
id: widget.id, id: widget.id,
value: _valueController.text, value: _valueController.text,
notes: _notesController.text, notes: _notesController.text,
); );
mealSource.defaultCarbsRatioAccuracy.target = _defaultCarbsRatioAccuracy; mealSource.defaultCarbsRatioAccuracy.target = _defaultCarbsRatioAccuracy;
mealSource.defaultPortionSizeAccuracy.target = mealSource.defaultPortionSizeAccuracy.target = _defaultPortionSizeAccuracy;
_defaultPortionSizeAccuracy; mealSource.defaultMealCategory.target = _defaultMealCategory;
mealSource.defaultMealCategory.target = _defaultMealCategory; mealSource.defaultMealPortionType.target = _defaultMealPortionType;
mealSource.defaultMealPortionType.target = _defaultMealPortionType; MealSource.put(mealSource);
MealSource.put(mealSource); Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Source saved', mealSource]);
Navigator.pop(context, '${_isNew ? 'New' : ''} Meal Source saved');
} }
void handleCancelAction() { void handleCancelAction() {
@ -108,8 +131,7 @@ class _MealSourceDetailScreenState extends State<MealSourceDetailScreen> {
_mealSource!.defaultMealCategory.target || _mealSource!.defaultMealCategory.target ||
_defaultMealPortionType != _defaultMealPortionType !=
_mealSource!.defaultMealPortionType.target || _mealSource!.defaultMealPortionType.target ||
_notesController.text != _notesController.text != (_mealSource!.notes ?? ''))))) {
(_mealSource!.notes ?? ''))))) {
Dialogs.showCancelConfirmationDialog( Dialogs.showCancelConfirmationDialog(
context: context, context: context,
isNew: _isNew, isNew: _isNew,
@ -128,76 +150,214 @@ class _MealSourceDetailScreenState extends State<MealSourceDetailScreen> {
), ),
drawer: drawer:
const Navigation(currentLocation: MealSourceDetailScreen.routeName), const Navigation(currentLocation: MealSourceDetailScreen.routeName),
body: SingleChildScrollView( body: Scrollbar(
child: Column( controller: _scrollController,
crossAxisAlignment: CrossAxisAlignment.stretch, child: SingleChildScrollView(
children: <Widget>[ controller: _scrollController,
FormWrapper( child: Column(
formState: _mealSourceForm, crossAxisAlignment: CrossAxisAlignment.stretch,
fields: [ children: <Widget>[
TextFormField( FormWrapper(
controller: _valueController, formState: _mealSourceForm,
decoration: const InputDecoration( fields: [
labelText: 'Name', TextFormField(
controller: _valueController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty name';
}
return null;
},
), ),
validator: (value) { Row(
if (value!.trim().isEmpty) { children: [
return 'Empty name'; Expanded(
} child: AutoCompleteDropdownButton<Accuracy>(
return null; selectedItem: _defaultCarbsRatioAccuracy,
}, controller: _defaultCarbsRatioAccuracyController,
), label: 'Default Carbs Ratio Accuracy',
AutoCompleteDropdownButton<Accuracy>( items: _carbsRatioAccuracies,
selectedItem: _defaultCarbsRatioAccuracy, onChanged: (value) {
label: 'Default Carbs Ratio Accuracy', setState(() {
items: _carbsRatioAccuracies, _defaultCarbsRatioAccuracy = value;
onChanged: (value) { _defaultCarbsRatioAccuracyController.text =
setState(() { (_defaultCarbsRatioAccuracy ?? '').toString();
_defaultCarbsRatioAccuracy = value; });
}); },
}, ),
), ),
AutoCompleteDropdownButton<Accuracy>( IconButton(
selectedItem: _defaultPortionSizeAccuracy, onPressed: () {
label: 'Default Portion Size Accuracy', Navigator.push(
items: _portionSizeAccuracies, context,
onChanged: (value) { MaterialPageRoute(
setState(() { builder: (context) =>
_defaultPortionSizeAccuracy = value; _defaultCarbsRatioAccuracy == null
}); ? const AccuracyDetailScreen()
}, : AccuracyDetailScreen(
), id: _defaultCarbsRatioAccuracy!.id),
AutoCompleteDropdownButton<MealCategory>( ),
selectedItem: _defaultMealCategory, ).then((result) {
label: 'Default Meal Category', setState(() {
items: _mealCategories, _defaultCarbsRatioAccuracy = result?[1];
onChanged: (value) { _defaultCarbsRatioAccuracyController.text =
setState(() { (_defaultCarbsRatioAccuracy ?? '').toString();
_defaultMealCategory = value; });
}); reload(message: result?[0]);
}, });
), },
AutoCompleteDropdownButton<MealPortionType>( icon: Icon(_defaultCarbsRatioAccuracy == null
selectedItem: _defaultMealPortionType, ? Icons.add
label: 'Default Meal Portion Type', : Icons.edit),
items: _mealPortionTypes, ),
onChanged: (value) { ],
setState(() {
_defaultMealPortionType = value;
});
},
),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(
labelText: 'Notes',
alignLabelWithHint: true,
), ),
keyboardType: TextInputType.multiline, Row(
) children: [
], Expanded(
), child: AutoCompleteDropdownButton<Accuracy>(
], 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<MealCategory>(
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<MealPortionType>(
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( bottomNavigationBar: DetailBottomRow(

View File

@ -17,6 +17,8 @@ class MealSourceListScreen extends StatefulWidget {
class _MealSourceListScreenState extends State<MealSourceListScreen> { class _MealSourceListScreenState extends State<MealSourceListScreen> {
List<MealSource> _mealSources = []; List<MealSource> _mealSources = [];
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -71,41 +73,49 @@ class _MealSourceListScreenState extends State<MealSourceListScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: _mealSources.isNotEmpty ? ListView.builder( child: _mealSources.isNotEmpty ? Scrollbar(
padding: const EdgeInsets.only(top: 10.0), controller: _scrollController,
itemCount: _mealSources.length, child: ListView.builder(
itemBuilder: (context, index) { controller: _scrollController,
final mealSource = _mealSources[index]; padding: const EdgeInsets.all(10.0),
itemCount: _mealSources.length,
itemBuilder: (context, index) {
final mealSource = _mealSources[index];
return ListTile( return Card(
onTap: () { child: ListTile(
Navigator.push( onTap: () {
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => MealSourceDetailScreen( MaterialPageRoute(
id: mealSource.id, 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)); trailing: Row(
}, mainAxisSize: MainAxisSize.min,
title: Text(mealSource.value), children: [
subtitle: Text(mealSource.notes ?? ''), IconButton(
trailing: Row( icon: const Icon(
mainAxisSize: MainAxisSize.min, Icons.delete,
children: [ color: Colors.blue,
IconButton( ),
icon: const Icon( onPressed: () async {
Icons.delete, handleDeleteAction(mealSource);
color: Colors.blue, },
), ),
onPressed: () async { ],
handleDeleteAction(mealSource);
},
), ),
], ),
), );
); }
} ),
) : const Center( ) : const Center(
child: Text('You have not created any Meal Sources yet!'), child: Text('You have not created any Meal Sources yet!'),
), ),
@ -119,7 +129,7 @@ class _MealSourceListScreenState extends State<MealSourceListScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const MealSourceDetailScreen(), builder: (context) => const MealSourceDetailScreen(),
), ),
).then((message) => reload(message: message)); ).then((result) => reload(message: result?[0]));
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),

View File

@ -17,8 +17,8 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
late Settings _settings; late Settings _settings;
late String _nutritionMeasurementLabel; final TextEditingController _nutritionMeasurementLabelController = TextEditingController(text: '');
late String _glucoseMeasurementLabel; final TextEditingController _glucoseMeasurementLabelController = TextEditingController(text: '');
late bool _onlyDisplayActiveGlucoseMeasurement; late bool _onlyDisplayActiveGlucoseMeasurement;
late bool _displayBothGlucoseMeasurementsInDetailView; late bool _displayBothGlucoseMeasurementsInDetailView;
@ -44,9 +44,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_settings = Settings.get(); _settings = Settings.get();
_nutritionMeasurementLabel = _nutritionMeasurementLabelController.text =
nutritionMeasurementLabels[_settings.nutritionMeasurementIndex]; nutritionMeasurementLabels[_settings.nutritionMeasurementIndex];
_glucoseMeasurementLabel = _glucoseMeasurementLabelController.text =
glucoseMeasurementLabels[_settings.glucoseMeasurementIndex]; glucoseMeasurementLabels[_settings.glucoseMeasurementIndex];
_onlyDisplayActiveGlucoseMeasurement = _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.activeOnly.index; _onlyDisplayActiveGlucoseMeasurement = _settings.glucoseDisplayModeIndex == GlucoseDisplayMode.activeOnly.index;
_displayBothGlucoseMeasurementsInDetailView = _displayBothGlucoseMeasurementsInDetailView =
@ -93,9 +93,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
Settings.put(Settings( Settings.put(Settings(
id: _settings.id, id: _settings.id,
nutritionMeasurementIndex: nutritionMeasurementIndex:
nutritionMeasurementLabels.indexOf(_nutritionMeasurementLabel), nutritionMeasurementLabels.indexOf(_nutritionMeasurementLabelController.text),
glucoseMeasurementIndex: glucoseMeasurementIndex:
glucoseMeasurementLabels.indexOf(_glucoseMeasurementLabel), glucoseMeasurementLabels.indexOf(_glucoseMeasurementLabelController.text),
glucoseDisplayModeIndex: _onlyDisplayActiveGlucoseMeasurement glucoseDisplayModeIndex: _onlyDisplayActiveGlucoseMeasurement
? GlucoseDisplayMode.activeOnly.index ? GlucoseDisplayMode.activeOnly.index
: _displayBothGlucoseMeasurementsInDetailView && _displayBothGlucoseMeasurementsInListView : _displayBothGlucoseMeasurementsInDetailView && _displayBothGlucoseMeasurementsInListView
@ -157,27 +157,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Row(
children: [
Text(
'MEASUREMENTS',
style: Theme.of(context).textTheme.subtitle2,
),
const Spacer(),
],
),
),
AutoCompleteDropdownButton<String>( AutoCompleteDropdownButton<String>(
selectedItem: _nutritionMeasurementLabel, controller: _nutritionMeasurementLabelController,
selectedItem: _nutritionMeasurementLabelController.text,
label: 'Preferred Nutrition Measurement', label: 'Preferred Nutrition Measurement',
items: nutritionMeasurementLabels, items: nutritionMeasurementLabels,
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
_nutritionMeasurementLabel = value; setState(() {
_nutritionMeasurementLabelController.text = value;
});
saveSettings(); saveSettings();
} }
}, },
), ),
AutoCompleteDropdownButton<String>( Padding(
selectedItem: _glucoseMeasurementLabel, padding: const EdgeInsets.symmetric(vertical: 10.0),
label: 'Preferred Glucose Measurement', child: AutoCompleteDropdownButton<String>(
items: glucoseMeasurementLabels, controller: _glucoseMeasurementLabelController,
onChanged: (value) { selectedItem: _glucoseMeasurementLabelController.text,
if (value != null) { label: 'Preferred Glucose Measurement',
_glucoseMeasurementLabel = value; items: glucoseMeasurementLabels,
saveSettings(); onChanged: (value) {
} if (value != null) {
}, setState(() {
_glucoseMeasurementLabelController.text = value;
});
saveSettings();
}
},
),
), ),
BooleanFormField( BooleanFormField(
value: _onlyDisplayActiveGlucoseMeasurement, value: _onlyDisplayActiveGlucoseMeasurement,
@ -205,10 +226,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
saveSettings(); saveSettings();
}, },
), ),
const Padding( const Divider(),
padding: EdgeInsets.only(top: 10.0), Padding(
child: Text('Confirmation prompts'), padding: const EdgeInsets.only(bottom: 10.0),
), child: Row(
children: [
Text(
'CONFIRMATION PROMPTS',
style: Theme.of(context).textTheme.subtitle2,
),
const Spacer(),
],
),
),
BooleanFormField( BooleanFormField(
value: _showConfirmationDialogOnCancel, value: _showConfirmationDialogOnCancel,
label: 'on cancelling edit or creation of a record if changes have already been made', label: 'on cancelling edit or creation of a record if changes have already been made',

View File

@ -422,13 +422,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" 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: objectbox_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -436,6 +429,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
objectbox_sync_flutter_libs:
dependency: "direct main"
description:
name: objectbox_sync_flutter_libs
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@ -610,7 +610,7 @@ packages:
name: pubspec_parse name: pubspec_parse
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.2.0"
sembast: sembast:
dependency: transitive dependency: transitive
description: description:
@ -720,7 +720,7 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0+4" version: "2.0.1"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:

View File

@ -19,7 +19,7 @@ dependencies:
shared_preferences: ^2.0.8 shared_preferences: ^2.0.8
intl: ^0.17.0 intl: ^0.17.0
objectbox: ^1.2.0 objectbox: ^1.2.0
objectbox_flutter_libs: any objectbox_sync_flutter_libs: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -31,7 +31,3 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
fonts:
- family: RobotoCondensed
fonts:
- asset: assets/fonts/RobotoCondensed-Regular.ttf

View File

@ -1,6 +1,6 @@
{ {
"name": "tide", "name": "diameter",
"short_name": "tide", "short_name": "diameter",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#0175C2", "background_color": "#0175C2",