update meal screens to use number component; show log entries & events by day

This commit is contained in:
spinel 2022-01-24 05:43:55 +01:00
parent 09c5230caf
commit 361bd0b741
14 changed files with 976 additions and 624 deletions

27
TODO
View File

@ -1,30 +1,16 @@
MAIN TASKS:
Components/Framework:
☐ update number fields to use corresponding components
☐ meal detail (carbs ratio, portion size, carbs per portion)
☐ log meal detail (amount, carbs ratio, portion size, carbs per portion)
☐ add "set manually" switch (like in log bolus detail) wherever parameters can be calculated from others
☐ meal detail
☐ log meal detail
☐ come up with new concept for duration component
☐ update duration fields to use corresponding component
☐ log event type detail (reminder duration)
☐ log event detail (reminder duration)
☐ meal (bolus delay)
☐ log bolus (delay)
☐ put dropdowns first if they override name field
☐ set name properties as unique (and add checks to forms)
☐ check through all detail forms and set required fields/according messages
☐ change placement of delete and floating button because its very easy to accidentally hit delete
☐ implement deletion by swiping left on item instead?
☐ check for changes before navigating as well (not just on cancel)
Log Overview:
☐ only show current day
☐ add calendar field on top to navigate
☐ use currently selected day when adding a log entry
Event Types:
☐ add pagination
Reports:
☐ evaluate what type of reports there should be
☐ try out graph/diagram components
@ -73,6 +59,19 @@ FUTURE TASKS:
☐ add functionality to delete dead records (meaning: set deleted flag and no relations to undeleted records)
Archive:
✔ only show current day @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview)
✔ add calendar field on top to navigate @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview)
✔ use currently selected day when adding a log entry @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview)
✔ only show current day @done(22-01-24 05:39) @project(MAIN TASKS.Event Types)
✔ add calendar field on top to navigate @done(22-01-24 05:39) @project(MAIN TASKS.Event Types)
✔ use currently selected day when adding a log event @done(22-01-24 05:39) @project(MAIN TASKS.Event Types)
✔ update number fields to use corresponding components @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework)
✔ meal detail (carbs ratio, portion size, carbs per portion) @done(22-01-24 03:12) @project(MAIN TASKS.Components/Framework)
✔ log meal detail (amount, carbs ratio, portion size, carbs per portion) @done(22-01-24 03:12) @project(MAIN TASKS.Components/Framework)
✔ add "set manually" switch (like in log bolus detail) wherever parameters can be calculated from others @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework)
✔ meal detail @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework)
✔ log meal detail @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework)
✔ put dropdowns first if they override name field @done(22-01-24 03:17) @project(MAIN TASKS.Components/Framework)
✔ settings (target glucose, increments) @done(22-01-22 01:48) @project(MAIN TASKS.Components/Framework)
✔ accuracy detail (confidence rating) @done(22-01-21 16:51) @project(MAIN TASKS.Components/Framework)
✔ basal detail (units) @done(22-01-21 18:14) @project(MAIN TASKS.Components/Framework)

View File

@ -75,8 +75,8 @@ class _NumberFormFieldState extends State<NumberFormField> {
children: [
widget.showSteppers
? RepeatOnHoldButton(
onTap: onDecrease,
child: IconButton(
onTap: onDecrease,
child: IconButton(
onPressed: double.tryParse(widget.controller.text) != null &&
(double.parse(widget.controller.text) - widget.step >=
widget.min)
@ -84,7 +84,7 @@ class _NumberFormFieldState extends State<NumberFormField> {
: null,
icon: const Icon(Icons.remove),
),
)
)
: Container(),
Expanded(
child: TextFormField(
@ -100,16 +100,8 @@ class _NumberFormFieldState extends State<NumberFormField> {
onChanged: (input) async {
await Future.delayed(const Duration(seconds: 1));
double? value = double.tryParse(input);
if (value != null &&
widget.autoRoundToMultipleOfStep &&
(value % widget.step != 0)) {
double remainder = value % widget.step;
value =
Utils.addDoublesWithPrecision(value, -remainder, precision);
if (remainder > widget.step / 2) {
value = Utils.addDoublesWithPrecision(
value, widget.step, precision);
}
if (widget.autoRoundToMultipleOfStep) {
value = value != null ? Utils.roundToMultipleOfBase(value, widget.step) : null;
}
widget.onChanged(value);
},
@ -118,8 +110,8 @@ class _NumberFormFieldState extends State<NumberFormField> {
),
widget.showSteppers
? RepeatOnHoldButton(
onTap: onIncrease,
child: IconButton(
onTap: onIncrease,
child: IconButton(
onPressed: double.tryParse(widget.controller.text) != null &&
(widget.max == null ||
double.parse(widget.controller.text) +

View File

@ -70,6 +70,16 @@ class LogEntry {
return dateMap;
}
static List<LogEntry> getAllForDate(DateTime date) {
DateTime startOfDay = DateTime(date.year, date.month, date.day);
DateTime endOfDay = startOfDay.add(const Duration(days: 1));
QueryBuilder<LogEntry> builder = box.query(LogEntry_.deleted.equals(false))
..order(LogEntry_.time, flags: Order.descending);
return builder.build().find().where((entry) {
return (entry.time.compareTo(startOfDay) >= 0 && entry.time.isBefore(endOfDay));
}).toList();
}
@override
String toString() {
return DateTimeUtils.displayDateTime(time);

View File

@ -140,6 +140,54 @@ class LogEvent {
return sortedDateMap;
}
static List<LogEvent> getAllForDate(DateTime date) {
DateTime startOfDay = DateTime(date.year, date.month, date.day);
DateTime endOfDay = startOfDay.add(const Duration(days: 1));
List<LogEvent> events = [];
QueryBuilder<LogEvent> allByDate = box
.query(LogEvent_.deleted.equals(false))
..order(LogEvent_.time, flags: Order.descending);
List<LogEvent> startEvents = allByDate.build().find().where((event) {
return (event.time.compareTo(startOfDay) >= 0 &&
event.time.isBefore(endOfDay));
}).toList();
for (LogEvent event in startEvents) {
date = DateTime.utc(event.time.year, event.time.month, event.time.day);
LogEvent startEvent = event;
startEvent.title =
'${event.toString()} ${event.hasEndTime ? '(Start)' : ''}';
events.add(startEvent);
}
QueryBuilder<LogEvent> allByEndDate = box
.query(LogEvent_.deleted.equals(false).and(LogEvent_.endTime.notNull()))
..order(LogEvent_.endTime, flags: Order.descending);
List<LogEvent> endEvents = allByEndDate.build().find().where((event) {
return (event.endTime!.compareTo(startOfDay) >= 0 &&
event.endTime!.isBefore(endOfDay));
}).toList();
for (LogEvent event in endEvents) {
date = DateTime.utc(
event.endTime!.year, event.endTime!.month, event.endTime!.day);
LogEvent endEvent = event;
endEvent.isEndEvent = true;
endEvent.title = '${event.toString()} (End)';
events.add(endEvent);
}
events.sort((LogEvent a, LogEvent b) {
final dateA = a.isEndEvent ? a.endTime : a.time;
final dateB = b.isEndEvent ? b.endTime : b.time;
return -(dateA!.compareTo(dateB!));
});
return events;
}
@override
String toString() {
return eventType.target?.value ?? '';

View File

@ -19,25 +19,33 @@ class LogScreen extends StatefulWidget {
}
class _LogScreenState extends State<LogScreen> {
late Map<DateTime, List<LogEntry>> _logEntryDailyMap;
late List<LogEntry> _logEntries;
final ScrollController _scrollController = ScrollController();
final TextEditingController _dateController = TextEditingController(text: '');
late DateTime _date;
@override
void initState() {
super.initState();
_date = DateTime.now();
_dateController.text = DateTimeUtils.displayDate(_date);
reload();
}
@override
void dispose() {
_scrollController.dispose();
_dateController.dispose();
super.dispose();
}
void reload({String? message}) {
setState(() {
_logEntryDailyMap = LogEntry.getDailyEntryMap();
_logEntries = LogEntry.getAllForDate(_date);
});
setState(() {
if (message != null) {
@ -69,216 +77,250 @@ class _LogScreenState extends State<LogScreen> {
}
}
void onChangeDate(DateTime? date) {
if (date != null) {
setState(() {
_date = DateTime(date.year, date.month, date.day);
_dateController.text = DateTimeUtils.displayDate(date);
});
reload();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Log Entries'),
actions: <Widget>[
IconButton(
onPressed: () => onChangeDate(DateTime.now()),
icon: const Icon(Icons.today)),
IconButton(onPressed: reload, icon: const Icon(Icons.refresh)),
],
),
drawer: const Navigation(currentLocation: LogScreen.routeName),
body: Column(
children: [
Row(
children: [
IconButton(
onPressed: _date.isAtSameMomentAs(DateTime(2000, 1, 1))
? null
: () =>
onChangeDate(_date.subtract(const Duration(days: 1))),
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: GestureDetector(
onTap: () async {
final newTime = await showDatePicker(
context: context,
initialDate: _date,
firstDate: DateTime(2000, 1, 1),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
onChangeDate(newTime);
},
child: Expanded(
child: Text(
DateTimeUtils.displayDate(_date).toUpperCase(),
style: Theme.of(context).textTheme.subtitle2,
textAlign: TextAlign.center,
),
),
),
),
IconButton(
onPressed:
_date.add(const Duration(days: 1)).isBefore(DateTime.now())
? () => onChangeDate(_date.add(const Duration(days: 1)))
: null,
icon: const Icon(Icons.arrow_forward),
),
],
),
Expanded(
child: _logEntryDailyMap.isNotEmpty
child: _logEntries.isNotEmpty
? Scrollbar(
controller: _scrollController,
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(10.0),
shrinkWrap: true,
itemCount: _logEntryDailyMap.length,
itemBuilder: (context, dateIndex) {
List<DateTime> dateList =
_logEntryDailyMap.keys.toList();
final date = dateList[dateIndex];
final entryList = _logEntryDailyMap[date];
final tiles = <Widget>[];
for (LogEntry logEntry in entryList!) {
double bolus =
LogBolus.getTotalBolusForEntry(logEntry.id);
double carbs =
LogMeal.getTotalCarbsForEntry(logEntry.id);
TextStyle glucoseStyle = TextStyle(
color: GlucoseTarget.getColorForGlucose(
mgPerDl: logEntry.mgPerDl ?? 0,
mmolPerL: logEntry.mmolPerL ?? 0));
tiles.add(
Card(
child: ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
LogEntryScreen(id: logEntry.id),
),
).then((result) => reload(message: result?[0]));
},
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
DateTimeUtils.displayTime(logEntry.time),
),
),
Expanded(
child: Column(
children: logEntry.mgPerDl != null &&
(Settings.glucoseMeasurement ==
GlucoseMeasurement
.mgPerDl ||
Settings.glucoseDisplayMode ==
GlucoseDisplayMode.both ||
Settings.glucoseDisplayMode ==
GlucoseDisplayMode
.bothForList)
? [
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
logEntry.mgPerDl.toString(),
style: glucoseStyle),
logEntry.glucoseTrend != null
? Transform.rotate(
angle: logEntry
.glucoseTrend! *
math.pi /
180,
child: Icon(
Icons.arrow_upward,
color: glucoseStyle
.color,
size: 16.0,
),
)
: Container(),
],
),
const Text(
'mg/dl',
textScaleFactor: 0.75,
),
]
: [],
),
),
Expanded(
child: Column(
children: logEntry.mmolPerL != null &&
(Settings.glucoseMeasurement ==
GlucoseMeasurement
.mmolPerL ||
Settings.glucoseDisplayMode ==
GlucoseDisplayMode.both ||
Settings.glucoseDisplayMode ==
GlucoseDisplayMode
.bothForList)
? [
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
logEntry.mmolPerL
.toString(),
style: glucoseStyle),
logEntry.glucoseTrend != null
? Transform.rotate(
angle: logEntry
.glucoseTrend! *
math.pi /
180,
child: Icon(
Icons.arrow_upward,
color: glucoseStyle
.color,
size: 16.0,
),
)
: Container(),
],
),
const Text(
'mmol/l',
textScaleFactor: 0.75,
),
]
: [],
),
),
Expanded(
child: Column(
children: (bolus > 0)
? [
Text(bolus.toStringAsPrecision(3)),
const Text('U',
textScaleFactor: 0.75),
]
: [],
),
),
Expanded(
child: Column(
children: (carbs > 0)
? [
Text(carbs.toStringAsPrecision(3)),
Text(
'${Settings.nutritionMeasurementSuffix} carbs',
textScaleFactor: 0.75),
]
: [],
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => handleDeleteAction(logEntry),
icon: const Icon(Icons.delete,
color: Colors.blue),
)
],
),
),
));
}
return ListBody(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(10.0),
itemCount: _logEntries.length,
itemBuilder: (context, index) {
LogEntry logEntry = _logEntries[index];
double bolus =
LogBolus.getTotalBolusForEntry(logEntry.id);
double carbs =
LogMeal.getTotalCarbsForEntry(logEntry.id);
TextStyle glucoseStyle = TextStyle(
color: GlucoseTarget.getColorForGlucose(
mgPerDl: logEntry.mgPerDl ?? 0,
mmolPerL: logEntry.mmolPerL ?? 0));
return Card(
child: ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
LogEntryScreen(id: logEntry.id),
),
).then((result) => reload(message: result?[0]));
},
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
DateTimeUtils.displayDate(date).toUpperCase(),
style: Theme.of(context).textTheme.subtitle2,
DateTimeUtils.displayTime(logEntry.time),
),
),
Expanded(
child: Column(
children: logEntry.mgPerDl != null &&
(Settings.glucoseMeasurement ==
GlucoseMeasurement
.mgPerDl ||
Settings.glucoseDisplayMode ==
GlucoseDisplayMode.both ||
Settings.glucoseDisplayMode ==
GlucoseDisplayMode
.bothForList)
? [
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
logEntry.mgPerDl.toString(),
style: glucoseStyle),
logEntry.glucoseTrend != null
? Transform.rotate(
angle: logEntry
.glucoseTrend! *
math.pi /
180,
child: Icon(
Icons.arrow_upward,
color: glucoseStyle
.color,
size: 16.0,
),
)
: Container(),
],
),
const Text(
'mg/dl',
textScaleFactor: 0.75,
),
]
: [],
),
),
Expanded(
child: Column(
children: logEntry.mmolPerL != null &&
(Settings.glucoseMeasurement ==
GlucoseMeasurement
.mmolPerL ||
Settings.glucoseDisplayMode ==
GlucoseDisplayMode.both ||
Settings.glucoseDisplayMode ==
GlucoseDisplayMode
.bothForList)
? [
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
logEntry.mmolPerL
.toString(),
style: glucoseStyle),
logEntry.glucoseTrend != null
? Transform.rotate(
angle: logEntry
.glucoseTrend! *
math.pi /
180,
child: Icon(
Icons.arrow_upward,
color: glucoseStyle
.color,
size: 16.0,
),
)
: Container(),
],
),
const Text(
'mmol/l',
textScaleFactor: 0.75,
),
]
: [],
),
),
Expanded(
child: Column(
children: (bolus > 0)
? [
Text(bolus.toStringAsPrecision(3)),
const Text('U',
textScaleFactor: 0.75),
]
: [],
),
),
Expanded(
child: Column(
children: (carbs > 0)
? [
Text(carbs.toStringAsPrecision(3)),
Text(
'${Settings.nutritionMeasurementSuffix} carbs',
textScaleFactor: 0.75),
]
: [],
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => handleDeleteAction(logEntry),
icon: const Icon(Icons.delete,
color: Colors.blue),
)
] +
tiles +
[const Divider()]
],
),
),
);
},
),
)
: const Center(
child: Text('You have not created any Log Entries yet!'),
child: Text(
'You have not created any Log Entries for this date yet!'),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final now = DateTime.now();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LogEntryScreen(),
builder: (context) => LogEntryScreen(
suggestedDate: _date.isAtSameMomentAs(DateTime(now.year, now.month, now.day)) ? now : _date),
),
).then((result) => reload(message: result?[0]));
},

View File

@ -135,7 +135,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
_bolusType = BolusType.glucose;
}
updateDelayedRatio();
calculateBolus();
}
@override
@ -200,76 +200,6 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
}
}
void updateDelayedRatio(
{double? totalUnitsUpdate,
double? delayedUnitsUpdate,
double? immediateUnitsUpdate,
double? percentageUpdate}) {
int precision = Utils.getFractionDigitsLength(Settings.insulinSteps);
double? totalUnits =
totalUnitsUpdate ?? double.tryParse(_unitsController.text);
double? delayedUnits;
double? immediateUnits;
if (totalUnits == null) {
delayedUnits =
delayedUnitsUpdate ?? double.tryParse(_delayedUnitsController.text);
immediateUnits = immediateUnitsUpdate ??
double.tryParse(_immediateUnitsController.text);
if (percentageUpdate != null) {
if (delayedUnits != null) {
totalUnits = delayedUnits / percentageUpdate * 100;
} else if (immediateUnits != null) {
totalUnits = immediateUnits / percentageUpdate * 100;
}
} else if (delayedUnits != null && immediateUnits != null) {
totalUnits = Utils.addDoublesWithPrecision(
delayedUnits, immediateUnits, precision);
}
}
setState(() {
_unitsController.text = (totalUnits ?? 0).toString();
});
if (totalUnits != null) {
double percentage = percentageUpdate ?? _delayPercentage;
if (totalUnitsUpdate != null || percentageUpdate != null) {
delayedUnits = totalUnits * percentage / 100;
} else if (delayedUnitsUpdate != null) {
delayedUnits = delayedUnitsUpdate;
} else if (immediateUnitsUpdate != null) {
delayedUnits = totalUnits - immediateUnitsUpdate;
}
if (delayedUnits != null) {
double remainder = delayedUnits % Settings.insulinSteps;
int precision = Utils.getFractionDigitsLength(Settings.insulinSteps);
if (remainder != 0) {
delayedUnits = Utils.addDoublesWithPrecision(
delayedUnits, -remainder, precision);
if (remainder > Settings.insulinSteps / 2) {
delayedUnits = Utils.addDoublesWithPrecision(
delayedUnits, Settings.insulinSteps, precision);
}
}
setState(() {
_delayedUnitsController.text = delayedUnits.toString();
_immediateUnitsController.text = Utils.addDoublesWithPrecision(
totalUnits!, -delayedUnits!, precision)
.toString();
if (totalUnits != 0) {
_delayPercentage = delayedUnits * 100 / totalUnits;
}
});
}
}
}
void onSelectMeal(LogMeal? meal) {
updateLogMeal(meal);
if (meal != null && meal.totalCarbs != null) {
@ -282,25 +212,26 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
void calculateBolus() {
if (_rate != null && !_setManually) {
double units = (double.tryParse(_carbsController.text) ?? 0) /
(_rate!.carbs / _rate!.units);
double remainder = units % Settings.insulinSteps;
int precision = Utils.getFractionDigitsLength(Settings.insulinSteps);
double? units;
if (remainder != 0) {
units = Utils.addDoublesWithPrecision(units, -remainder, precision);
if (remainder > Settings.insulinSteps / 2) {
units = Utils.addDoublesWithPrecision(
units, Settings.insulinSteps, precision);
if (_bolusType == BolusType.glucose) {
if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl) {
units = (int.tryParse(_mgPerDlCorrectionController.text) ?? 0) /
(_rate!.mgPerDl ?? 1) /
_rate!.units;
}
if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL) {
units = (int.tryParse(_mmolPerLCorrectionController.text) ?? 0) /
(_rate!.mmolPerL ?? 1) /
_rate!.units;
}
}
if (_bolusType == BolusType.meal) {
units = (double.tryParse(_carbsController.text) ?? 0) /
(_rate!.carbs / _rate!.units);
}
setState(() {
_unitsController.text = units.toString();
});
updateDelayedRatio(
totalUnitsUpdate: double.tryParse(_unitsController.text));
updateDelayedRatio(totalUnitsUpdate: units);
}
}
@ -339,11 +270,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
Utils.convertMgPerDlToMmolPerL(mgPerDlTarget ?? 0).toString();
_mmolPerLCorrectionController.text =
Utils.convertMgPerDlToMmolPerL(mgPerDlCorrection ?? 0).toString();
if (_rate != null && !_setManually) {
updateDelayedRatio(
totalUnitsUpdate: (mgPerDlCorrection ?? 0) /
((_rate!.mgPerDl ?? 0) / _rate!.units));
}
calculateBolus();
});
}
if ((mmolPerLCurrent != null && mgPerDlCurrent == null) ||
@ -357,15 +284,93 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
Utils.convertMmolPerLToMgPerDl(mmolPerLTarget ?? 0).toString();
_mgPerDlCorrectionController.text =
Utils.convertMmolPerLToMgPerDl(mmolPerLCorrection ?? 0).toString();
if (_rate != null && !_setManually) {
updateDelayedRatio(
totalUnitsUpdate: (mmolPerLCorrection ?? 0) /
((_rate!.mmolPerL ?? 0) / _rate!.units));
}
calculateBolus();
});
}
}
void updateDelayedRatio({
double? totalUnitsUpdate,
double? delayedUnitsUpdate,
double? immediateUnitsUpdate,
double? percentageUpdate
}) {
int precision = Utils.getFractionDigitsLength(Settings.insulinSteps);
double? totalUnits;
double? delayedUnits;
double? immediateUnits;
if (totalUnitsUpdate != null) {
totalUnits = Utils.roundToMultipleOfBase(totalUnitsUpdate, Settings.insulinSteps);
} else if (double.tryParse(_unitsController.text) != null) {
totalUnits = Utils.roundToMultipleOfBase(double.tryParse(_unitsController.text)!, Settings.insulinSteps);
}
if (delayedUnitsUpdate != null) {
delayedUnits = Utils.roundToMultipleOfBase(delayedUnitsUpdate, Settings.insulinSteps);
} else if (double.tryParse(_delayedUnitsController.text) != null) {
delayedUnits = Utils.roundToMultipleOfBase(double.tryParse(_delayedUnitsController.text)!, Settings.insulinSteps);
}
if (immediateUnitsUpdate != null) {
immediateUnits = Utils.roundToMultipleOfBase(immediateUnitsUpdate, Settings.insulinSteps);
} else if (double.tryParse(_immediateUnitsController.text) != null) {
immediateUnits = Utils.roundToMultipleOfBase(double.tryParse(_immediateUnitsController.text)!, Settings.insulinSteps);
}
if (totalUnits == null) {
if (percentageUpdate != null) {
if (immediateUnits != null) {
totalUnits = immediateUnits / (100 - percentageUpdate) * 100;
} else if (delayedUnits != null) {
totalUnits = delayedUnits / percentageUpdate * 100;
}
} else if (delayedUnits != null && immediateUnits != null) {
totalUnits = Utils.addDoublesWithPrecision(
delayedUnits, immediateUnits, precision);
}
if (totalUnits != null) {
totalUnits =
Utils.roundToMultipleOfBase(totalUnits, Settings.insulinSteps);
}
}
setState(() {
_unitsController.text = Utils.toStringMatchingTemplateFractionPrecision(
totalUnits ?? 0, Settings.insulinSteps);
});
if (totalUnits != null) {
double percentage = percentageUpdate ?? _delayPercentage;
if (totalUnitsUpdate != null || percentageUpdate != null) {
immediateUnits = Utils.roundToMultipleOfBase(
totalUnits * (100 - percentage) / 100, Settings.insulinSteps);
} else if (delayedUnitsUpdate != null) {
immediateUnits = totalUnits - delayedUnits!;
}
if (immediateUnits != null) {
delayedUnits = Utils.addDoublesWithPrecision(
totalUnits, -immediateUnits, precision);
setState(() {
_immediateUnitsController.text =
Utils.toStringMatchingTemplateFractionPrecision(
immediateUnits!, Settings.insulinSteps);
_delayedUnitsController.text =
Utils.toStringMatchingTemplateFractionPrecision(
delayedUnits!, Settings.insulinSteps);
if (totalUnits != 0) {
_delayPercentage = delayedUnits / totalUnits! * 100;
}
});
}
}
}
void handleSaveAction() async {
setState(() {
_isSaving = true;
@ -560,7 +565,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
onChanged: (_) {
setState(() {
_bolusType = BolusType.glucose;
onChangeGlucose();
calculateBolus();
});
}),
),
@ -741,7 +746,8 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
controller: _carbsController,
step: Settings.nutritionSteps,
onChanged: (value) {
_carbsController.text = (value ?? 0).toString();
_carbsController.text =
(value ?? 0).toString();
calculateBolus();
},
),
@ -768,11 +774,9 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
value: _delayPercentage,
min: 0,
max: 100,
onChanged: _delayController.text != ''
? (value) {
updateDelayedRatio(percentageUpdate: value);
}
: null,
onChanged: (value) {
updateDelayedRatio(percentageUpdate: value);
},
),
),
const Text('%', textScaleFactor: 1.5),
@ -800,15 +804,16 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
child: Padding(
padding: const EdgeInsets.only(left: 5.0),
child: NumberFormField(
label: 'Delayed Bolus',
suffix: ' U',
controller: _delayedUnitsController,
max: double.tryParse(_unitsController.text),
step: Settings.insulinSteps,
readOnly: true,
onChanged: (value) => updateDelayedRatio(
delayedUnitsUpdate: value),
),
label: 'Delayed Bolus',
suffix: ' U',
controller: _delayedUnitsController,
max: double.tryParse(_unitsController.text),
step: Settings.insulinSteps,
readOnly: true,
onChanged: (value) => {
updateDelayedRatio(
delayedUnitsUpdate: value),
}),
),
),
]

View File

@ -21,8 +21,9 @@ import 'dart:math' as math;
class LogEntryScreen extends StatefulWidget {
static const String routeName = '/log-entry';
final int id;
final DateTime? suggestedDate;
const LogEntryScreen({Key? key, this.id = 0}) : super(key: key);
const LogEntryScreen({Key? key, this.id = 0, this.suggestedDate}) : super(key: key);
@override
_LogEntryScreenState createState() => _LogEntryScreenState();
@ -43,10 +44,8 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
final _timeController = TextEditingController(text: '');
final _dateController = TextEditingController(text: '');
final _mgPerDlController =
TextEditingController(text: Settings.targetMgPerDl.toString());
final _mmolPerLController =
TextEditingController(text: Settings.targetMmolPerL.toString());
final _mgPerDlController = TextEditingController(text: '');
final _mmolPerLController = TextEditingController(text: '');
final _notesController = TextEditingController(text: '');
late FloatingActionButton addMealButton;
@ -108,7 +107,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
_glucoseTrend = _logEntry!.glucoseTrend;
_notesController.text = _logEntry!.notes ?? '';
} else {
_time = DateTime.now();
_time = widget.suggestedDate ?? DateTime.now();
}
updateTime();
@ -364,7 +363,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
? 2
: 1,
child: NumberFormField(
label: 'per mg/dl',
label: 'Blood Glucose',
suffix: 'mg/dl',
readOnly: Settings.glucoseMeasurement ==
GlucoseMeasurement.mmolPerL,
@ -389,7 +388,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
? 2
: 1,
child: NumberFormField(
label: 'per mmol/l',
label: 'Blood Glucose',
suffix: 'mmol/l',
readOnly: Settings.glucoseMeasurement ==
GlucoseMeasurement.mgPerDl,

View File

@ -1,4 +1,5 @@
import 'package:diameter/components/detail.dart';
import 'package:diameter/components/forms/boolean_form_field.dart';
import 'package:diameter/components/forms/number_form_field.dart';
import 'package:diameter/utils/dialog_utils.dart';
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
@ -37,10 +38,13 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
bool _isNew = true;
bool _isSaving = false;
bool _isExpanded = false;
bool _setManually = false;
final GlobalKey<FormState> _logMealForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
double _amount = 1;
final _valueController = TextEditingController(text: '');
final _carbsRatioController = TextEditingController(text: '');
final _portionSizeController = TextEditingController(text: '');
@ -271,74 +275,111 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
}
}
void updateAmount(double? amount) {
double previousAmount;
double newAmount;
double? portionSize;
double? carbsRatio;
previousAmount = double.tryParse(_amountController.text) ?? 1;
newAmount = amount?.toDouble() ?? 1;
setState(() {
_amountController.text = newAmount.toString();
});
if (_carbsRatioController.text != '') {
carbsRatio = double.tryParse(_carbsRatioController.text);
}
if (_portionSizeController.text != '') {
portionSize = double.tryParse(_portionSizeController.text);
}
if (portionSize != null) {
void updateAmount(double? newAmount) {
if (newAmount != null) {
setState(() {
portionSize = portionSize! / previousAmount * newAmount;
_portionSizeController.text = portionSize.toString();
_amountController.text = Utils.getFractionDigitsLength(newAmount) == 0
? newAmount.toInt().toString()
: newAmount.toString();
});
if (carbsRatio != null) {
setState(() {
_totalCarbsController.text =
Utils.calculateCarbs(carbsRatio!, portionSize!).toString();
});
double? portionSize;
double? basePortionSize;
if (_portionSizeController.text != '') {
portionSize = double.tryParse(_portionSizeController.text);
}
if (portionSize != null && portionSize != 0) {
basePortionSize = portionSize / _amount;
} else if (_meal != null) {
basePortionSize = _meal!.portionSize;
}
if (basePortionSize != null) {
setState(() {
portionSize = basePortionSize! * newAmount;
_portionSizeController.text =
Utils.toStringMatchingTemplateFractionPrecision(
portionSize!, Settings.nutritionSteps);
});
calculateThirdMeasurementOfPortionCarbsRelation(
portionSizeUpdate: portionSize);
}
setState(() {
_amount = newAmount;
});
}
}
void calculateThirdMeasurementOfPortionCarbsRelation() {
double? amount = double.tryParse(_amountController.text) ?? 1;
double? carbsRatio;
double? portionSize;
double? carbsPerPortion;
void calculateThirdMeasurementOfPortionCarbsRelation(
{double? carbsRatioUpdate,
double? portionSizeUpdate,
double? totalCarbsUpdate}) {
if (!_setManually) {
double? carbsRatio =
carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text);
double? portionSize =
portionSizeUpdate ?? double.tryParse(_portionSizeController.text);
double? totalCarbs =
totalCarbsUpdate ?? double.tryParse(_totalCarbsController.text);
if (_carbsRatioController.text != '') {
carbsRatio = double.tryParse(_carbsRatioController.text);
}
if (_portionSizeController.text != '') {
portionSize = double.tryParse(_portionSizeController.text);
}
if (_totalCarbsController.text != '') {
carbsPerPortion = double.tryParse(_totalCarbsController.text);
}
int toCalculate = 0;
const calcCarbsRatio = 1;
const calcTotalCarbs = 2;
const calcPortionSize = 3;
if (carbsRatioUpdate != null) {
if (portionSize != null && portionSize != 0) {
toCalculate = calcTotalCarbs;
} else if (totalCarbs != null && totalCarbs != 0) {
toCalculate = calcPortionSize;
}
} else if (portionSizeUpdate != null) {
if (carbsRatio != null && carbsRatio != 0) {
toCalculate = calcTotalCarbs;
} else if (totalCarbs != null && totalCarbs != 0) {
toCalculate = calcCarbsRatio;
}
} else if (totalCarbsUpdate != null) {
if (carbsRatio != null && carbsRatio != 0) {
toCalculate = calcPortionSize;
} else if (portionSize != null && portionSize != 0) {
toCalculate = calcCarbsRatio;
}
} else {
if (carbsRatio != null && carbsRatio != 0) {
if (portionSize != null && portionSize != 0) {
toCalculate = calcTotalCarbs;
} else if (totalCarbs != null && totalCarbs != 0) {
toCalculate = calcPortionSize;
}
} else if (portionSize != null &&
portionSize != 0 &&
totalCarbs != null &&
totalCarbs != 0) {
toCalculate = calcCarbsRatio;
}
}
if (carbsRatio != null && portionSize != null && carbsPerPortion == null) {
setState(() {
_totalCarbsController.text =
Utils.calculateCarbs(carbsRatio!, portionSize! * amount).toString();
});
}
if (carbsRatio == null && portionSize != null && carbsPerPortion != null) {
setState(() {
_carbsRatioController.text =
Utils.calculateCarbsRatio(carbsPerPortion!, portionSize! * amount)
.toString();
});
}
if (carbsRatio != null && portionSize == null && carbsPerPortion != null) {
setState(() {
_portionSizeController.text =
Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!)
.toString();
if (toCalculate == calcCarbsRatio) {
_carbsRatioController.text =
Utils.calculateCarbsRatio(totalCarbs!, portionSize!).toString();
} else if (toCalculate == calcTotalCarbs) {
_totalCarbsController.text =
Utils.toStringMatchingTemplateFractionPrecision(
Utils.calculateCarbs(carbsRatio!, portionSize!,
step: Settings.nutritionSteps),
Settings.nutritionSteps);
} else if (toCalculate == calcPortionSize) {
_portionSizeController.text =
Utils.toStringMatchingTemplateFractionPrecision(
Utils.calculatePortionSize(carbsRatio!, totalCarbs!,
step: Settings.nutritionSteps),
Settings.nutritionSteps);
}
});
}
}
@ -360,18 +401,6 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
FormWrapper(
formState: _logMealForm,
fields: [
TextFormField(
controller: _valueController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty name';
}
return null;
},
),
Row(
children: [
Expanded(
@ -401,9 +430,22 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
),
],
),
TextFormField(
controller: _valueController,
decoration: const InputDecoration(
labelText: 'Name',
),
validator: (value) {
if (value!.trim().isEmpty) {
return 'Empty name';
}
return null;
},
),
Row(
children: [
Expanded(
flex: 10,
child: NumberFormField(
controller: _amountController,
label: 'Amount',
@ -411,31 +453,52 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
onChanged: updateAmount,
),
),
TextButton(
onPressed: () => updateAmount(0.5),
child: Column(
children: const [
Text('1', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),),
Text('2'),
],
Expanded(
child: TextButton(
onPressed: () => updateAmount(0.5),
child: Column(
children: const [
Text(
'1',
style: TextStyle(
decoration: TextDecoration.underline,
decorationThickness: 2),
),
Text('2'),
],
),
),
),
TextButton(
onPressed: () => updateAmount(0.33),
child: Column(
children: const [
Text('1', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),),
Text('3'),
],
Expanded(
child: TextButton(
onPressed: () => updateAmount(0.33),
child: Column(
children: const [
Text(
'1',
style: TextStyle(
decoration: TextDecoration.underline,
decorationThickness: 2),
),
Text('3'),
],
),
),
),
TextButton(
onPressed: () => updateAmount(0.67),
child: Column(
children: const [
Text('2', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),),
Text('3'),
],
Expanded(
child: TextButton(
onPressed: () => updateAmount(0.67),
child: Column(
children: const [
Text(
'2',
style: TextStyle(
decoration: TextDecoration.underline,
decorationThickness: 2),
),
Text('3'),
],
),
),
),
],
@ -443,54 +506,64 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
Row(
children: [
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'Portion size',
suffixText: Settings.nutritionMeasurementSuffix,
),
child: NumberFormField(
label: 'Portion size',
suffix: Settings.nutritionMeasurementSuffix,
controller: _portionSizeController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
onChanged: (_) async {
showSteppers: false,
autoRoundToMultipleOfStep: true,
step: Settings.nutritionSteps,
onChanged: (value) async {
await Future.delayed(const Duration(seconds: 1));
calculateThirdMeasurementOfPortionCarbsRelation();
calculateThirdMeasurementOfPortionCarbsRelation(
portionSizeUpdate: value);
},
),
),
const SizedBox(width: 10),
Expanded(
child: TextFormField(
decoration: const InputDecoration(
labelText: 'Carbs ratio',
suffixText: '%',
),
child: NumberFormField(
label: 'Carbs ratio',
suffix: '%',
controller: _carbsRatioController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
onChanged: (_) async {
showSteppers: false,
onChanged: (value) async {
await Future.delayed(const Duration(seconds: 1));
calculateThirdMeasurementOfPortionCarbsRelation();
calculateThirdMeasurementOfPortionCarbsRelation(
carbsRatioUpdate: value);
},
),
),
const SizedBox(width: 10),
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'Total carbs',
suffixText: Settings.nutritionMeasurementSuffix,
),
child: NumberFormField(
label: 'Total carbs',
suffix: Settings.nutritionMeasurementSuffix,
controller: _totalCarbsController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
onChanged: (_) async {
showSteppers: false,
autoRoundToMultipleOfStep: true,
step: Settings.nutritionSteps,
onChanged: (value) async {
await Future.delayed(const Duration(seconds: 1));
calculateThirdMeasurementOfPortionCarbsRelation();
calculateThirdMeasurementOfPortionCarbsRelation(
totalCarbsUpdate: value);
},
),
),
],
),
Expanded(
child: BooleanFormField(
value: _setManually,
label: 'set carbs ratio manually',
onChanged: (value) {
setState(() {
_setManually = value;
calculateThirdMeasurementOfPortionCarbsRelation();
});
},
),
),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(

View File

@ -22,9 +22,10 @@ class LogEventDetailScreen extends StatefulWidget {
final int logEntryId;
final int endLogEntryId;
final int id;
final DateTime? suggestedDate;
const LogEventDetailScreen(
{Key? key, this.logEntryId = 0, this.endLogEntryId = 0, this.id = 0})
{Key? key, this.logEntryId = 0, this.endLogEntryId = 0, this.id = 0, this.suggestedDate})
: super(key: key);
@override
@ -93,7 +94,7 @@ class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
_time = _logEvent!.time;
_endTime = _logEvent!.endTime;
} else {
_time = DateTime.now();
_time = widget.suggestedDate ?? DateTime.now();
}
_logEventTypes = LogEventType.getAll();

View File

@ -16,25 +16,36 @@ class LogEventListScreen extends StatefulWidget {
class _LogEventListScreenState extends State<LogEventListScreen> {
List<LogEvent> _activeEvents = [];
late Map<DateTime, List<LogEvent>> _logEventDailyMap;
late List<LogEvent> _logEvents;
final ScrollController _scrollController = ScrollController();
final TextEditingController _dateController = TextEditingController(text: '');
late DateTime _date;
bool _showActive = true;
@override
void initState() {
super.initState();
_date = DateTime.now();
_dateController.text = DateTimeUtils.displayDate(_date);
reload();
}
@override
void dispose() {
_scrollController.dispose();
_dateController.dispose();
super.dispose();
}
void reload({String? message}) {
setState(() {
_activeEvents = LogEvent.getAllActiveForTime(DateTime.now());
_logEventDailyMap = LogEvent.getDailyEntryMap();
_logEvents = LogEvent.getAllForDate(_date);
});
setState(() {
@ -51,11 +62,14 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
}
void handleAddNewEvent() async {
final now = DateTime.now();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const LogEventDetailScreen();
return LogEventDetailScreen(
suggestedDate: _date.isAtSameMomentAs(DateTime(now.year, now.month, now.day)) ? now : _date,
);
},
),
).then((result) => reload(message: result?[0]));
@ -108,12 +122,25 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
}
}
void onChangeDate(DateTime? date) {
if (date != null) {
setState(() {
_date = DateTime(date.year, date.month, date.day);
_dateController.text = DateTimeUtils.displayDate(date);
});
reload();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Log Events'),
actions: <Widget>[
IconButton(
onPressed: () => onChangeDate(DateTime.now()),
icon: const Icon(Icons.today)),
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
],
),
@ -121,108 +148,211 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: _logEventDailyMap.isNotEmpty
? Scrollbar(
controller: _scrollController,
child: ListView.builder(
controller: _scrollController,
shrinkWrap: true,
padding: const EdgeInsets.all(10.0),
itemCount: _logEventDailyMap.length,
itemBuilder: (context, dateIndex) {
List<DateTime?> dateList = (_activeEvents.isNotEmpty
? <DateTime?>[null]
: <DateTime?>[]) +
_logEventDailyMap.keys.toList();
final date = dateList[dateIndex];
final eventList = date != null
? _logEventDailyMap[date]
: _activeEvents;
final tiles = <Widget>[];
if (eventList != null) {
for (LogEvent event in eventList) {
tiles.add(Card(
child: ListTile(
onTap: () {
handleEditAction(event);
},
title: Row(
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(date == null
? DateTimeUtils
.displayDateTime(
event.time)
: DateTimeUtils.displayTime(
event.isEndEvent
? event.endTime
: event.time),
),
const SizedBox(width: 24),
Expanded(
child: Text(
(event.title ?? event.eventType.target?.value ?? '').toUpperCase(),
style: Theme.of(context).textTheme.subtitle2,
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
event.hasEndTime &&
event.endTime == null
? IconButton(
icon: const Icon(
Icons.stop,
color: Colors.blue,
),
onPressed: () =>
handleStopAction(event),
)
: const SizedBox(width: 50),
IconButton(
icon: const Icon(
Icons.edit,
color: Colors.blue,
),
onPressed: () =>
handleEditAction(event),
),
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
),
onPressed: () =>
handleDeleteAction(event),
),
],
),
),
));
}
}
return eventList != null && eventList.isNotEmpty ? ListBody(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(10.0),
GestureDetector(
onTap: () => setState(() {
_showActive = !_showActive;
}),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
'ACTIVE EVENTS',
style: Theme.of(context).textTheme.subtitle2,
textAlign: TextAlign.center,
),
),
Icon(_showActive
? Icons.expand_less
: Icons.expand_more),
],
),
),
),
!_showActive ? Container() :
_activeEvents.isNotEmpty
? ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.all(10.0),
itemCount: _activeEvents.length,
itemBuilder: (context, index) {
LogEvent event = _activeEvents[index];
return Card(
child: ListTile(
onTap: () {
handleEditAction(event);
},
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
DateTimeUtils.displayDateTime(event.time),
),
const SizedBox(width: 24),
Expanded(
child: Text(
DateTimeUtils.displayDate(date, fallback: 'Active Events').toUpperCase(),
(event.title ??
event.eventType.target?.value ??
'')
.toUpperCase(),
style: Theme.of(context).textTheme.subtitle2,
),
),
] + tiles +
[const Divider()],
) : Container();
},
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
event.hasEndTime && event.endTime == null
? IconButton(
icon: const Icon(
Icons.stop,
color: Colors.blue,
),
onPressed: () => handleStopAction(event),
)
: const SizedBox(width: 50),
IconButton(
icon: const Icon(
Icons.edit,
color: Colors.blue,
),
onPressed: () => handleEditAction(event),
),
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
),
onPressed: () => handleDeleteAction(event),
),
],
),
),
);
},
)
: const Center(
child: Text('There are no Active Events!'),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(),
),
Row(
children: [
IconButton(
onPressed: _date.isAtSameMomentAs(DateTime(2000, 1, 1))
? null
: () =>
onChangeDate(_date.subtract(const Duration(days: 1))),
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: GestureDetector(
onTap: () async {
final newTime = await showDatePicker(
context: context,
initialDate: _date,
firstDate: DateTime(2000, 1, 1),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
onChangeDate(newTime);
},
child: Expanded(
child: Text(
DateTimeUtils.displayDate(_date).toUpperCase(),
style: Theme.of(context).textTheme.subtitle2,
textAlign: TextAlign.center,
),
),
),
),
IconButton(
onPressed:
_date.add(const Duration(days: 1)).isBefore(DateTime.now())
? () => onChangeDate(_date.add(const Duration(days: 1)))
: null,
icon: const Icon(Icons.arrow_forward),
),
],
),
Expanded(
child: _logEvents.isNotEmpty
? Scrollbar(
controller: _scrollController,
child: ListView.builder(
controller: _scrollController,
shrinkWrap: true,
padding: const EdgeInsets.all(10.0),
itemCount: _logEvents.length,
itemBuilder: (context, index) {
LogEvent event = _logEvents[index];
return Card(
child: ListTile(
onTap: () {
handleEditAction(event);
},
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
DateTimeUtils.displayTime(event.isEndEvent
? event.endTime
: event.time),
),
const SizedBox(width: 24),
Expanded(
child: Text(
(event.title ??
event.eventType.target?.value ??
'')
.toUpperCase(),
style:
Theme.of(context).textTheme.subtitle2,
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
event.hasEndTime && event.endTime == null
? IconButton(
icon: const Icon(
Icons.stop,
color: Colors.blue,
),
onPressed: () =>
handleStopAction(event),
)
: const SizedBox(width: 50),
IconButton(
icon: const Icon(
Icons.edit,
color: Colors.blue,
),
onPressed: () => handleEditAction(event),
),
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
),
onPressed: () => handleDeleteAction(event),
),
],
),
),
);
},
))
: const Center(
child: Text('There are no Events!'),
child: Text('There are no Events for that date!'),
),
),
],

View File

@ -1,4 +1,6 @@
import 'package:diameter/components/detail.dart';
import 'package:diameter/components/forms/boolean_form_field.dart';
import 'package:diameter/components/forms/number_form_field.dart';
import 'package:diameter/utils/dialog_utils.dart';
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
import 'package:diameter/components/forms/form_wrapper.dart';
@ -32,6 +34,7 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
bool _isNew = true;
bool _isSaving = false;
bool _isExpanded = false;
bool _setManually = false;
final GlobalKey<FormState> _mealForm = GlobalKey<FormState>();
final ScrollController _scrollController = ScrollController();
@ -242,7 +245,7 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
}
}
Future<void> onSelectMealSource(MealSource? mealSource) async {
void onSelectMealSource(MealSource? mealSource) {
setState(() {
_mealSource = mealSource;
_mealSourceController.text = (_mealSource ?? '').toString();
@ -263,40 +266,72 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
}
}
void calculateThirdMeasurementOfPortionCarbsRelation() {
double? carbsRatio;
double? portionSize;
double? carbsPerPortion;
void calculateThirdMeasurementOfPortionCarbsRelation(
{double? carbsRatioUpdate,
double? portionSizeUpdate,
double? carbsPerPortionUpdate}) {
if (!_setManually) {
double? carbsRatio =
carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text);
double? portionSize =
portionSizeUpdate ?? double.tryParse(_portionSizeController.text);
double? carbsPerPortion = carbsPerPortionUpdate ??
double.tryParse(_carbsPerPortionController.text);
if (_carbsRatioController.text != '') {
carbsRatio = double.tryParse(_carbsRatioController.text);
}
if (_portionSizeController.text != '') {
portionSize = double.tryParse(_portionSizeController.text);
}
if (_carbsRatioController.text != '') {
carbsPerPortion = double.tryParse(_carbsPerPortionController.text);
int toCalculate = 0;
const calcCarbsRatio = 1;
const calcCarbsPerPortion = 2;
const calcPortionSize = 3;
if (carbsRatioUpdate != null) {
if (portionSize != null && portionSize != 0) {
toCalculate = calcCarbsPerPortion;
} else if (carbsPerPortion != null && carbsPerPortion != 0) {
toCalculate = calcPortionSize;
}
} else if (portionSizeUpdate != null) {
if (carbsRatio != null && carbsRatio != 0) {
toCalculate = calcCarbsPerPortion;
} else if (carbsPerPortion != null && carbsPerPortion != 0) {
toCalculate = calcCarbsRatio;
}
} else if (carbsPerPortionUpdate != null) {
if (carbsRatio != null && carbsRatio != 0) {
toCalculate = calcPortionSize;
} else if (portionSize != null && portionSize != 0) {
toCalculate = calcCarbsRatio;
}
} else {
if (carbsRatio != null && carbsRatio != 0) {
if (portionSize != null && portionSize != 0) {
toCalculate = calcCarbsPerPortion;
} else if (carbsPerPortion != null && carbsPerPortion != 0) {
toCalculate = calcPortionSize;
}
} else if (portionSize != null &&
portionSize != 0 &&
carbsPerPortion != null &&
carbsPerPortion != 0) {
toCalculate = calcCarbsRatio;
}
}
if (carbsRatio != null && portionSize != null && carbsPerPortion == null) {
setState(() {
_carbsPerPortionController.text =
Utils.calculateCarbs(carbsRatio!, portionSize!)
.toString();
});
}
if (carbsRatio == null && portionSize != null && carbsPerPortion != null) {
setState(() {
_carbsRatioController.text =
Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!)
.toString();
});
}
if (carbsRatio != null && portionSize == null && carbsPerPortion != null) {
setState(() {
_portionSizeController.text =
Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!)
.toString();
if (toCalculate == calcCarbsRatio) {
_carbsRatioController.text =
Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!)
.toString();
} else if (toCalculate == calcCarbsPerPortion) {
_carbsPerPortionController.text = Utils.calculateCarbs(
carbsRatio!, portionSize!,
step: Settings.nutritionSteps)
.toString();
} else if (toCalculate == calcPortionSize) {
_portionSizeController.text = Utils.calculatePortionSize(
carbsRatio!, carbsPerPortion!,
step: Settings.nutritionSteps)
.toString();
}
});
}
}
@ -393,54 +428,57 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
Row(
children: [
Expanded(
child: TextFormField(
decoration: const InputDecoration(
labelText: 'Carbs ratio',
suffixText: '%',
),
child: NumberFormField(
label: 'Carbs ratio',
suffix: '%',
controller: _carbsRatioController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
onChanged: (_) async {
showSteppers: false,
onChanged: (value) async {
await Future.delayed(const Duration(seconds: 1));
calculateThirdMeasurementOfPortionCarbsRelation();
calculateThirdMeasurementOfPortionCarbsRelation(carbsRatioUpdate: value);
},
),
),
const SizedBox(width: 10),
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'Portion size',
suffixText: Settings.nutritionMeasurementSuffix,
),
child: NumberFormField(
label: 'Portion size',
suffix: Settings.nutritionMeasurementSuffix,
controller: _portionSizeController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
onChanged: (_) async {
showSteppers: false,
onChanged: (value) async {
await Future.delayed(const Duration(seconds: 1));
calculateThirdMeasurementOfPortionCarbsRelation();
calculateThirdMeasurementOfPortionCarbsRelation(portionSizeUpdate: value);
},
),
),
const SizedBox(width: 10),
Expanded(
child: TextFormField(
decoration: InputDecoration(
labelText: 'Carbs per portion',
suffixText: Settings.nutritionMeasurementSuffix,
),
child: NumberFormField(
label: 'Carbs per portion',
suffix: Settings.nutritionMeasurementSuffix,
controller: _carbsPerPortionController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
onChanged: (_) async {
showSteppers: false,
onChanged: (value) async {
await Future.delayed(const Duration(seconds: 1));
calculateThirdMeasurementOfPortionCarbsRelation();
calculateThirdMeasurementOfPortionCarbsRelation(carbsPerPortionUpdate: value);
},
),
),
],
),
Expanded(
child: BooleanFormField(
value: _setManually,
label: 'set carbs ratio manually',
onChanged: (value) {
setState(() {
_setManually = value;
calculateThirdMeasurementOfPortionCarbsRelation();
});
},
),
),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(
@ -463,8 +501,6 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
],
),
),
// ignore: todo
// TODO: display according to time format
Row(
children: [
Expanded(
@ -486,13 +522,14 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
value: _delayedBolusPercentage,
min: 0,
max: 100,
onChanged: _delayedBolusDurationController.text != ''
? (value) {
setState(() {
_delayedBolusPercentage = value;
});
} : null
),
onChanged:
_delayedBolusDurationController.text != ''
? (value) {
setState(() {
_delayedBolusPercentage = value;
});
}
: null),
),
const Text('%', textScaleFactor: 1.5),
],

View File

@ -1,9 +1,24 @@
import 'dart:math';
class Utils {
static double roundToDecimalPlaces(double value, int precision) {
double mod = pow(10.0, precision).toDouble();
return ((value * mod).round().toDouble() / mod);
// static double roundToDecimalPlaces(double value, int precision) {
// double mod = pow(10.0, precision).toDouble();
// return ((value * mod).round().toDouble() / mod);
// }
static double roundToMultipleOfBase(double value, double base) {
double result = value;
double remainder = value % base;
int precision = Utils.getFractionDigitsLength(base);
if (remainder != 0) {
result = Utils.addDoublesWithPrecision(result, -remainder, precision);
if (remainder > base / 2) {
result = Utils.addDoublesWithPrecision(result, base, precision);
}
}
return result;
}
static double addDoublesWithPrecision(double a, double b, int precision) {
@ -14,7 +29,7 @@ class Utils {
static int getFractionDigitsLength(double value) {
final fractionDigits = value.toString().split('.');
return fractionDigits[1].length;
return fractionDigits[1] == '0' ? 0 : fractionDigits[1].length;
}
static String toStringMatchingTemplateFractionPrecision(
@ -23,29 +38,30 @@ class Utils {
return value.toStringAsFixed(precision);
}
static double convertMgPerDlToMmolPerL(int mgPerDl) {
return Utils.roundToDecimalPlaces(mgPerDl * 0.0555, 2);
static double convertMgPerDlToMmolPerL(int mgPerDl, {double step = 0.01}) {
return Utils.roundToMultipleOfBase(mgPerDl * 0.0555, step);
}
static int convertMmolPerLToMgPerDl(double mmolPerL) {
return (mmolPerL * 18.018).round();
}
static double calculateCarbs(double carbsRatio, double portionSize) {
return Utils.roundToDecimalPlaces(carbsRatio * portionSize / 100, 2);
static double calculateCarbs(double carbsRatio, double portionSize,
{double step = 0.01}) {
return Utils.roundToMultipleOfBase(carbsRatio * portionSize / 100, step);
}
static double calculateCarbsRatio(
double carbsPerPortion, double portionSize) {
double carbsPerPortion, double portionSize, {double step = 0.01}) {
return portionSize > 0
? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2)
? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / portionSize, step)
: 0;
}
static double calculatePortionSize(
double carbsRatio, double carbsPerPortion) {
double carbsRatio, double carbsPerPortion, {double step = 0.01}) {
return carbsRatio > 0
? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2)
? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / carbsRatio, step)
: 0;
}
}

Binary file not shown.

Binary file not shown.