update meal screens to use number component; show log entries & events by day
This commit is contained in:
parent
09c5230caf
commit
361bd0b741
27
TODO
27
TODO
@ -1,30 +1,16 @@
|
|||||||
MAIN TASKS:
|
MAIN TASKS:
|
||||||
Components/Framework:
|
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
|
☐ come up with new concept for duration component
|
||||||
☐ update duration fields to use corresponding component
|
☐ update duration fields to use corresponding component
|
||||||
☐ log event type detail (reminder duration)
|
☐ log event type detail (reminder duration)
|
||||||
☐ log event detail (reminder duration)
|
☐ log event detail (reminder duration)
|
||||||
☐ meal (bolus delay)
|
☐ meal (bolus delay)
|
||||||
☐ log bolus (delay)
|
☐ log bolus (delay)
|
||||||
☐ put dropdowns first if they override name field
|
|
||||||
☐ set name properties as unique (and add checks to forms)
|
☐ set name properties as unique (and add checks to forms)
|
||||||
☐ check through all detail forms and set required fields/according messages
|
☐ 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
|
☐ change placement of delete and floating button because its very easy to accidentally hit delete
|
||||||
☐ implement deletion by swiping left on item instead?
|
☐ implement deletion by swiping left on item instead?
|
||||||
☐ check for changes before navigating as well (not just on cancel)
|
☐ 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:
|
Reports:
|
||||||
☐ evaluate what type of reports there should be
|
☐ evaluate what type of reports there should be
|
||||||
☐ try out graph/diagram components
|
☐ 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)
|
☐ add functionality to delete dead records (meaning: set deleted flag and no relations to undeleted records)
|
||||||
|
|
||||||
Archive:
|
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)
|
✔ 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)
|
✔ 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)
|
✔ basal detail (units) @done(22-01-21 18:14) @project(MAIN TASKS.Components/Framework)
|
||||||
|
@ -100,16 +100,8 @@ class _NumberFormFieldState extends State<NumberFormField> {
|
|||||||
onChanged: (input) async {
|
onChanged: (input) async {
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
double? value = double.tryParse(input);
|
double? value = double.tryParse(input);
|
||||||
if (value != null &&
|
if (widget.autoRoundToMultipleOfStep) {
|
||||||
widget.autoRoundToMultipleOfStep &&
|
value = value != null ? Utils.roundToMultipleOfBase(value, widget.step) : null;
|
||||||
(value % widget.step != 0)) {
|
|
||||||
double remainder = value % widget.step;
|
|
||||||
value =
|
|
||||||
Utils.addDoublesWithPrecision(value, -remainder, precision);
|
|
||||||
if (remainder > widget.step / 2) {
|
|
||||||
value = Utils.addDoublesWithPrecision(
|
|
||||||
value, widget.step, precision);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
widget.onChanged(value);
|
widget.onChanged(value);
|
||||||
},
|
},
|
||||||
|
@ -70,6 +70,16 @@ class LogEntry {
|
|||||||
return dateMap;
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return DateTimeUtils.displayDateTime(time);
|
return DateTimeUtils.displayDateTime(time);
|
||||||
|
@ -140,6 +140,54 @@ class LogEvent {
|
|||||||
return sortedDateMap;
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return eventType.target?.value ?? '';
|
return eventType.target?.value ?? '';
|
||||||
|
@ -19,25 +19,33 @@ class LogScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LogScreenState extends State<LogScreen> {
|
class _LogScreenState extends State<LogScreen> {
|
||||||
late Map<DateTime, List<LogEntry>> _logEntryDailyMap;
|
late List<LogEntry> _logEntries;
|
||||||
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
final TextEditingController _dateController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
late DateTime _date;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_date = DateTime.now();
|
||||||
|
_dateController.text = DateTimeUtils.displayDate(_date);
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
|
_dateController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reload({String? message}) {
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_logEntryDailyMap = LogEntry.getDailyEntryMap();
|
_logEntries = LogEntry.getAllForDate(_date);
|
||||||
});
|
});
|
||||||
setState(() {
|
setState(() {
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
@ -69,34 +77,80 @@ 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Log Entries'),
|
title: const Text('Log Entries'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => onChangeDate(DateTime.now()),
|
||||||
|
icon: const Icon(Icons.today)),
|
||||||
IconButton(onPressed: reload, icon: const Icon(Icons.refresh)),
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: LogScreen.routeName),
|
drawer: const Navigation(currentLocation: LogScreen.routeName),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: _logEntryDailyMap.isNotEmpty
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final newTime = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _date,
|
||||||
|
firstDate: DateTime(2000, 1, 1),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
onChangeDate(newTime);
|
||||||
|
},
|
||||||
|
child: Expanded(
|
||||||
|
child: Text(
|
||||||
|
DateTimeUtils.displayDate(_date).toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed:
|
||||||
|
_date.add(const Duration(days: 1)).isBefore(DateTime.now())
|
||||||
|
? () => onChangeDate(_date.add(const Duration(days: 1)))
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.arrow_forward),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _logEntries.isNotEmpty
|
||||||
? Scrollbar(
|
? Scrollbar(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: _logEntryDailyMap.length,
|
itemCount: _logEntries.length,
|
||||||
itemBuilder: (context, dateIndex) {
|
itemBuilder: (context, index) {
|
||||||
List<DateTime> dateList =
|
LogEntry logEntry = _logEntries[index];
|
||||||
_logEntryDailyMap.keys.toList();
|
|
||||||
final date = dateList[dateIndex];
|
|
||||||
final entryList = _logEntryDailyMap[date];
|
|
||||||
final tiles = <Widget>[];
|
|
||||||
for (LogEntry logEntry in entryList!) {
|
|
||||||
double bolus =
|
double bolus =
|
||||||
LogBolus.getTotalBolusForEntry(logEntry.id);
|
LogBolus.getTotalBolusForEntry(logEntry.id);
|
||||||
double carbs =
|
double carbs =
|
||||||
@ -105,8 +159,7 @@ class _LogScreenState extends State<LogScreen> {
|
|||||||
color: GlucoseTarget.getColorForGlucose(
|
color: GlucoseTarget.getColorForGlucose(
|
||||||
mgPerDl: logEntry.mgPerDl ?? 0,
|
mgPerDl: logEntry.mgPerDl ?? 0,
|
||||||
mmolPerL: logEntry.mmolPerL ?? 0));
|
mmolPerL: logEntry.mmolPerL ?? 0));
|
||||||
tiles.add(
|
return Card(
|
||||||
Card(
|
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@ -249,36 +302,25 @@ class _LogScreenState extends State<LogScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
|
||||||
}
|
|
||||||
return ListBody(
|
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(10.0),
|
|
||||||
child: Text(
|
|
||||||
DateTimeUtils.displayDate(date).toUpperCase(),
|
|
||||||
style: Theme.of(context).textTheme.subtitle2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
] +
|
|
||||||
tiles +
|
|
||||||
[const Divider()]
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Center(
|
: 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(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
final now = DateTime.now();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
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]));
|
).then((result) => reload(message: result?[0]));
|
||||||
},
|
},
|
||||||
|
@ -135,7 +135,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
|||||||
_bolusType = BolusType.glucose;
|
_bolusType = BolusType.glucose;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDelayedRatio();
|
calculateBolus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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) {
|
void onSelectMeal(LogMeal? meal) {
|
||||||
updateLogMeal(meal);
|
updateLogMeal(meal);
|
||||||
if (meal != null && meal.totalCarbs != null) {
|
if (meal != null && meal.totalCarbs != null) {
|
||||||
@ -282,25 +212,26 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
|||||||
|
|
||||||
void calculateBolus() {
|
void calculateBolus() {
|
||||||
if (_rate != null && !_setManually) {
|
if (_rate != null && !_setManually) {
|
||||||
double units = (double.tryParse(_carbsController.text) ?? 0) /
|
double? units;
|
||||||
|
|
||||||
|
if (_bolusType == BolusType.glucose) {
|
||||||
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl) {
|
||||||
|
units = (int.tryParse(_mgPerDlCorrectionController.text) ?? 0) /
|
||||||
|
(_rate!.mgPerDl ?? 1) /
|
||||||
|
_rate!.units;
|
||||||
|
}
|
||||||
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL) {
|
||||||
|
units = (int.tryParse(_mmolPerLCorrectionController.text) ?? 0) /
|
||||||
|
(_rate!.mmolPerL ?? 1) /
|
||||||
|
_rate!.units;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_bolusType == BolusType.meal) {
|
||||||
|
units = (double.tryParse(_carbsController.text) ?? 0) /
|
||||||
(_rate!.carbs / _rate!.units);
|
(_rate!.carbs / _rate!.units);
|
||||||
double remainder = units % Settings.insulinSteps;
|
|
||||||
int precision = Utils.getFractionDigitsLength(Settings.insulinSteps);
|
|
||||||
|
|
||||||
if (remainder != 0) {
|
|
||||||
units = Utils.addDoublesWithPrecision(units, -remainder, precision);
|
|
||||||
if (remainder > Settings.insulinSteps / 2) {
|
|
||||||
units = Utils.addDoublesWithPrecision(
|
|
||||||
units, Settings.insulinSteps, precision);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
updateDelayedRatio(totalUnitsUpdate: units);
|
||||||
_unitsController.text = units.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
updateDelayedRatio(
|
|
||||||
totalUnitsUpdate: double.tryParse(_unitsController.text));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,11 +270,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
|||||||
Utils.convertMgPerDlToMmolPerL(mgPerDlTarget ?? 0).toString();
|
Utils.convertMgPerDlToMmolPerL(mgPerDlTarget ?? 0).toString();
|
||||||
_mmolPerLCorrectionController.text =
|
_mmolPerLCorrectionController.text =
|
||||||
Utils.convertMgPerDlToMmolPerL(mgPerDlCorrection ?? 0).toString();
|
Utils.convertMgPerDlToMmolPerL(mgPerDlCorrection ?? 0).toString();
|
||||||
if (_rate != null && !_setManually) {
|
calculateBolus();
|
||||||
updateDelayedRatio(
|
|
||||||
totalUnitsUpdate: (mgPerDlCorrection ?? 0) /
|
|
||||||
((_rate!.mgPerDl ?? 0) / _rate!.units));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ((mmolPerLCurrent != null && mgPerDlCurrent == null) ||
|
if ((mmolPerLCurrent != null && mgPerDlCurrent == null) ||
|
||||||
@ -357,14 +284,92 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
|||||||
Utils.convertMmolPerLToMgPerDl(mmolPerLTarget ?? 0).toString();
|
Utils.convertMmolPerLToMgPerDl(mmolPerLTarget ?? 0).toString();
|
||||||
_mgPerDlCorrectionController.text =
|
_mgPerDlCorrectionController.text =
|
||||||
Utils.convertMmolPerLToMgPerDl(mmolPerLCorrection ?? 0).toString();
|
Utils.convertMmolPerLToMgPerDl(mmolPerLCorrection ?? 0).toString();
|
||||||
if (_rate != null && !_setManually) {
|
calculateBolus();
|
||||||
updateDelayedRatio(
|
});
|
||||||
totalUnitsUpdate: (mmolPerLCorrection ?? 0) /
|
}
|
||||||
((_rate!.mmolPerL ?? 0) / _rate!.units));
|
}
|
||||||
|
|
||||||
|
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 {
|
void handleSaveAction() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -560,7 +565,7 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
|||||||
onChanged: (_) {
|
onChanged: (_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_bolusType = BolusType.glucose;
|
_bolusType = BolusType.glucose;
|
||||||
onChangeGlucose();
|
calculateBolus();
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -741,7 +746,8 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
|||||||
controller: _carbsController,
|
controller: _carbsController,
|
||||||
step: Settings.nutritionSteps,
|
step: Settings.nutritionSteps,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_carbsController.text = (value ?? 0).toString();
|
_carbsController.text =
|
||||||
|
(value ?? 0).toString();
|
||||||
calculateBolus();
|
calculateBolus();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -768,11 +774,9 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
|||||||
value: _delayPercentage,
|
value: _delayPercentage,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
onChanged: _delayController.text != ''
|
onChanged: (value) {
|
||||||
? (value) {
|
|
||||||
updateDelayedRatio(percentageUpdate: value);
|
updateDelayedRatio(percentageUpdate: value);
|
||||||
}
|
},
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Text('%', textScaleFactor: 1.5),
|
const Text('%', textScaleFactor: 1.5),
|
||||||
@ -806,9 +810,10 @@ class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
|||||||
max: double.tryParse(_unitsController.text),
|
max: double.tryParse(_unitsController.text),
|
||||||
step: Settings.insulinSteps,
|
step: Settings.insulinSteps,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
onChanged: (value) => updateDelayedRatio(
|
onChanged: (value) => {
|
||||||
|
updateDelayedRatio(
|
||||||
delayedUnitsUpdate: value),
|
delayedUnitsUpdate: value),
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -21,8 +21,9 @@ 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';
|
||||||
final int id;
|
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
|
@override
|
||||||
_LogEntryScreenState createState() => _LogEntryScreenState();
|
_LogEntryScreenState createState() => _LogEntryScreenState();
|
||||||
@ -43,10 +44,8 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
|
|||||||
|
|
||||||
final _timeController = TextEditingController(text: '');
|
final _timeController = TextEditingController(text: '');
|
||||||
final _dateController = TextEditingController(text: '');
|
final _dateController = TextEditingController(text: '');
|
||||||
final _mgPerDlController =
|
final _mgPerDlController = TextEditingController(text: '');
|
||||||
TextEditingController(text: Settings.targetMgPerDl.toString());
|
final _mmolPerLController = TextEditingController(text: '');
|
||||||
final _mmolPerLController =
|
|
||||||
TextEditingController(text: Settings.targetMmolPerL.toString());
|
|
||||||
final _notesController = TextEditingController(text: '');
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
late FloatingActionButton addMealButton;
|
late FloatingActionButton addMealButton;
|
||||||
@ -108,7 +107,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
|
|||||||
_glucoseTrend = _logEntry!.glucoseTrend;
|
_glucoseTrend = _logEntry!.glucoseTrend;
|
||||||
_notesController.text = _logEntry!.notes ?? '';
|
_notesController.text = _logEntry!.notes ?? '';
|
||||||
} else {
|
} else {
|
||||||
_time = DateTime.now();
|
_time = widget.suggestedDate ?? DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTime();
|
updateTime();
|
||||||
@ -364,7 +363,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
|
|||||||
? 2
|
? 2
|
||||||
: 1,
|
: 1,
|
||||||
child: NumberFormField(
|
child: NumberFormField(
|
||||||
label: 'per mg/dl',
|
label: 'Blood Glucose',
|
||||||
suffix: 'mg/dl',
|
suffix: 'mg/dl',
|
||||||
readOnly: Settings.glucoseMeasurement ==
|
readOnly: Settings.glucoseMeasurement ==
|
||||||
GlucoseMeasurement.mmolPerL,
|
GlucoseMeasurement.mmolPerL,
|
||||||
@ -389,7 +388,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
|
|||||||
? 2
|
? 2
|
||||||
: 1,
|
: 1,
|
||||||
child: NumberFormField(
|
child: NumberFormField(
|
||||||
label: 'per mmol/l',
|
label: 'Blood Glucose',
|
||||||
suffix: 'mmol/l',
|
suffix: 'mmol/l',
|
||||||
readOnly: Settings.glucoseMeasurement ==
|
readOnly: Settings.glucoseMeasurement ==
|
||||||
GlucoseMeasurement.mgPerDl,
|
GlucoseMeasurement.mgPerDl,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
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/components/forms/number_form_field.dart';
|
||||||
import 'package:diameter/utils/dialog_utils.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
@ -37,10 +38,13 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
|
|||||||
bool _isNew = true;
|
bool _isNew = true;
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
bool _isExpanded = false;
|
bool _isExpanded = false;
|
||||||
|
bool _setManually = false;
|
||||||
|
|
||||||
final GlobalKey<FormState> _logMealForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _logMealForm = GlobalKey<FormState>();
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
double _amount = 1;
|
||||||
|
|
||||||
final _valueController = TextEditingController(text: '');
|
final _valueController = TextEditingController(text: '');
|
||||||
final _carbsRatioController = TextEditingController(text: '');
|
final _carbsRatioController = TextEditingController(text: '');
|
||||||
final _portionSizeController = TextEditingController(text: '');
|
final _portionSizeController = TextEditingController(text: '');
|
||||||
@ -271,74 +275,111 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateAmount(double? amount) {
|
void updateAmount(double? newAmount) {
|
||||||
double previousAmount;
|
if (newAmount != null) {
|
||||||
double newAmount;
|
|
||||||
double? portionSize;
|
|
||||||
double? carbsRatio;
|
|
||||||
|
|
||||||
previousAmount = double.tryParse(_amountController.text) ?? 1;
|
|
||||||
newAmount = amount?.toDouble() ?? 1;
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_amountController.text = newAmount.toString();
|
_amountController.text = Utils.getFractionDigitsLength(newAmount) == 0
|
||||||
|
? newAmount.toInt().toString()
|
||||||
|
: newAmount.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_carbsRatioController.text != '') {
|
double? portionSize;
|
||||||
carbsRatio = double.tryParse(_carbsRatioController.text);
|
double? basePortionSize;
|
||||||
}
|
|
||||||
if (_portionSizeController.text != '') {
|
if (_portionSizeController.text != '') {
|
||||||
portionSize = double.tryParse(_portionSizeController.text);
|
portionSize = double.tryParse(_portionSizeController.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (portionSize != null) {
|
if (portionSize != null && portionSize != 0) {
|
||||||
setState(() {
|
basePortionSize = portionSize / _amount;
|
||||||
portionSize = portionSize! / previousAmount * newAmount;
|
} else if (_meal != null) {
|
||||||
_portionSizeController.text = portionSize.toString();
|
basePortionSize = _meal!.portionSize;
|
||||||
});
|
|
||||||
if (carbsRatio != null) {
|
|
||||||
setState(() {
|
|
||||||
_totalCarbsController.text =
|
|
||||||
Utils.calculateCarbs(carbsRatio!, portionSize!).toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void calculateThirdMeasurementOfPortionCarbsRelation() {
|
if (basePortionSize != null) {
|
||||||
double? amount = double.tryParse(_amountController.text) ?? 1;
|
|
||||||
double? carbsRatio;
|
|
||||||
double? portionSize;
|
|
||||||
double? carbsPerPortion;
|
|
||||||
|
|
||||||
if (_carbsRatioController.text != '') {
|
|
||||||
carbsRatio = double.tryParse(_carbsRatioController.text);
|
|
||||||
}
|
|
||||||
if (_portionSizeController.text != '') {
|
|
||||||
portionSize = double.tryParse(_portionSizeController.text);
|
|
||||||
}
|
|
||||||
if (_totalCarbsController.text != '') {
|
|
||||||
carbsPerPortion = double.tryParse(_totalCarbsController.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(() {
|
setState(() {
|
||||||
|
portionSize = basePortionSize! * newAmount;
|
||||||
_portionSizeController.text =
|
_portionSizeController.text =
|
||||||
Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!)
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
.toString();
|
portionSize!, Settings.nutritionSteps);
|
||||||
|
});
|
||||||
|
calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
portionSizeUpdate: portionSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_amount = newAmount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
{double? carbsRatioUpdate,
|
||||||
|
double? portionSizeUpdate,
|
||||||
|
double? totalCarbsUpdate}) {
|
||||||
|
if (!_setManually) {
|
||||||
|
double? carbsRatio =
|
||||||
|
carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text);
|
||||||
|
double? portionSize =
|
||||||
|
portionSizeUpdate ?? double.tryParse(_portionSizeController.text);
|
||||||
|
double? totalCarbs =
|
||||||
|
totalCarbsUpdate ?? double.tryParse(_totalCarbsController.text);
|
||||||
|
|
||||||
|
int toCalculate = 0;
|
||||||
|
const calcCarbsRatio = 1;
|
||||||
|
const calcTotalCarbs = 2;
|
||||||
|
const calcPortionSize = 3;
|
||||||
|
|
||||||
|
if (carbsRatioUpdate != null) {
|
||||||
|
if (portionSize != null && portionSize != 0) {
|
||||||
|
toCalculate = calcTotalCarbs;
|
||||||
|
} else if (totalCarbs != null && totalCarbs != 0) {
|
||||||
|
toCalculate = calcPortionSize;
|
||||||
|
}
|
||||||
|
} else if (portionSizeUpdate != null) {
|
||||||
|
if (carbsRatio != null && carbsRatio != 0) {
|
||||||
|
toCalculate = calcTotalCarbs;
|
||||||
|
} else if (totalCarbs != null && totalCarbs != 0) {
|
||||||
|
toCalculate = calcCarbsRatio;
|
||||||
|
}
|
||||||
|
} else if (totalCarbsUpdate != null) {
|
||||||
|
if (carbsRatio != null && carbsRatio != 0) {
|
||||||
|
toCalculate = calcPortionSize;
|
||||||
|
} else if (portionSize != null && portionSize != 0) {
|
||||||
|
toCalculate = calcCarbsRatio;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (carbsRatio != null && carbsRatio != 0) {
|
||||||
|
if (portionSize != null && portionSize != 0) {
|
||||||
|
toCalculate = calcTotalCarbs;
|
||||||
|
} else if (totalCarbs != null && totalCarbs != 0) {
|
||||||
|
toCalculate = calcPortionSize;
|
||||||
|
}
|
||||||
|
} else if (portionSize != null &&
|
||||||
|
portionSize != 0 &&
|
||||||
|
totalCarbs != null &&
|
||||||
|
totalCarbs != 0) {
|
||||||
|
toCalculate = calcCarbsRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (toCalculate == calcCarbsRatio) {
|
||||||
|
_carbsRatioController.text =
|
||||||
|
Utils.calculateCarbsRatio(totalCarbs!, portionSize!).toString();
|
||||||
|
} else if (toCalculate == calcTotalCarbs) {
|
||||||
|
_totalCarbsController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
Utils.calculateCarbs(carbsRatio!, portionSize!,
|
||||||
|
step: Settings.nutritionSteps),
|
||||||
|
Settings.nutritionSteps);
|
||||||
|
} else if (toCalculate == calcPortionSize) {
|
||||||
|
_portionSizeController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
Utils.calculatePortionSize(carbsRatio!, totalCarbs!,
|
||||||
|
step: Settings.nutritionSteps),
|
||||||
|
Settings.nutritionSteps);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -360,18 +401,6 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
|
|||||||
FormWrapper(
|
FormWrapper(
|
||||||
formState: _logMealForm,
|
formState: _logMealForm,
|
||||||
fields: [
|
fields: [
|
||||||
TextFormField(
|
|
||||||
controller: _valueController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Name',
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty) {
|
|
||||||
return 'Empty name';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
flex: 10,
|
||||||
child: NumberFormField(
|
child: NumberFormField(
|
||||||
controller: _amountController,
|
controller: _amountController,
|
||||||
label: 'Amount',
|
label: 'Amount',
|
||||||
@ -411,86 +453,117 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
|
|||||||
onChanged: updateAmount,
|
onChanged: updateAmount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
onPressed: () => updateAmount(0.5),
|
onPressed: () => updateAmount(0.5),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: const [
|
children: const [
|
||||||
Text('1', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),),
|
Text(
|
||||||
|
'1',
|
||||||
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationThickness: 2),
|
||||||
|
),
|
||||||
Text('2'),
|
Text('2'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
onPressed: () => updateAmount(0.33),
|
onPressed: () => updateAmount(0.33),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: const [
|
children: const [
|
||||||
Text('1', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),),
|
Text(
|
||||||
|
'1',
|
||||||
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationThickness: 2),
|
||||||
|
),
|
||||||
Text('3'),
|
Text('3'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
onPressed: () => updateAmount(0.67),
|
onPressed: () => updateAmount(0.67),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: const [
|
children: const [
|
||||||
Text('2', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),),
|
Text(
|
||||||
|
'2',
|
||||||
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationThickness: 2),
|
||||||
|
),
|
||||||
Text('3'),
|
Text('3'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: NumberFormField(
|
||||||
decoration: InputDecoration(
|
label: 'Portion size',
|
||||||
labelText: 'Portion size',
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
suffixText: Settings.nutritionMeasurementSuffix,
|
|
||||||
),
|
|
||||||
controller: _portionSizeController,
|
controller: _portionSizeController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
showSteppers: false,
|
||||||
decimal: true),
|
autoRoundToMultipleOfStep: true,
|
||||||
onChanged: (_) async {
|
step: Settings.nutritionSteps,
|
||||||
|
onChanged: (value) async {
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation();
|
calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
portionSizeUpdate: value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: NumberFormField(
|
||||||
decoration: const InputDecoration(
|
label: 'Carbs ratio',
|
||||||
labelText: 'Carbs ratio',
|
suffix: '%',
|
||||||
suffixText: '%',
|
|
||||||
),
|
|
||||||
controller: _carbsRatioController,
|
controller: _carbsRatioController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
showSteppers: false,
|
||||||
decimal: true),
|
onChanged: (value) async {
|
||||||
onChanged: (_) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation();
|
calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
carbsRatioUpdate: value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: NumberFormField(
|
||||||
decoration: InputDecoration(
|
label: 'Total carbs',
|
||||||
labelText: 'Total carbs',
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
suffixText: Settings.nutritionMeasurementSuffix,
|
|
||||||
),
|
|
||||||
controller: _totalCarbsController,
|
controller: _totalCarbsController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
showSteppers: false,
|
||||||
decimal: true),
|
autoRoundToMultipleOfStep: true,
|
||||||
onChanged: (_) async {
|
step: Settings.nutritionSteps,
|
||||||
|
onChanged: (value) async {
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
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(
|
TextFormField(
|
||||||
controller: _notesController,
|
controller: _notesController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
|
@ -22,9 +22,10 @@ class LogEventDetailScreen extends StatefulWidget {
|
|||||||
final int logEntryId;
|
final int logEntryId;
|
||||||
final int endLogEntryId;
|
final int endLogEntryId;
|
||||||
final int id;
|
final int id;
|
||||||
|
final DateTime? suggestedDate;
|
||||||
|
|
||||||
const LogEventDetailScreen(
|
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);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -93,7 +94,7 @@ class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
|
|||||||
_time = _logEvent!.time;
|
_time = _logEvent!.time;
|
||||||
_endTime = _logEvent!.endTime;
|
_endTime = _logEvent!.endTime;
|
||||||
} else {
|
} else {
|
||||||
_time = DateTime.now();
|
_time = widget.suggestedDate ?? DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
_logEventTypes = LogEventType.getAll();
|
_logEventTypes = LogEventType.getAll();
|
||||||
|
@ -16,25 +16,36 @@ 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 List<LogEvent> _logEvents;
|
||||||
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
final TextEditingController _dateController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
late DateTime _date;
|
||||||
|
bool _showActive = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_date = DateTime.now();
|
||||||
|
_dateController.text = DateTimeUtils.displayDate(_date);
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
|
_dateController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reload({String? message}) {
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_activeEvents = LogEvent.getAllActiveForTime(DateTime.now());
|
_activeEvents = LogEvent.getAllActiveForTime(DateTime.now());
|
||||||
_logEventDailyMap = LogEvent.getDailyEntryMap();
|
_logEvents = LogEvent.getAllForDate(_date);
|
||||||
});
|
});
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -51,11 +62,14 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleAddNewEvent() async {
|
void handleAddNewEvent() async {
|
||||||
|
final now = DateTime.now();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) {
|
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]));
|
).then((result) => reload(message: result?[0]));
|
||||||
@ -108,63 +122,82 @@ 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Log Events'),
|
title: const Text('Log Events'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => onChangeDate(DateTime.now()),
|
||||||
|
icon: const Icon(Icons.today)),
|
||||||
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: LogEventListScreen.routeName),
|
drawer: const Navigation(currentLocation: LogEventListScreen.routeName),
|
||||||
body: Column(
|
body: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() {
|
||||||
|
_showActive = !_showActive;
|
||||||
|
}),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _logEventDailyMap.isNotEmpty
|
child: Text(
|
||||||
? Scrollbar(
|
'ACTIVE EVENTS',
|
||||||
controller: _scrollController,
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
child: ListView.builder(
|
textAlign: TextAlign.center,
|
||||||
controller: _scrollController,
|
),
|
||||||
|
),
|
||||||
|
Icon(_showActive
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
!_showActive ? Container() :
|
||||||
|
_activeEvents.isNotEmpty
|
||||||
|
? ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
itemCount: _logEventDailyMap.length,
|
itemCount: _activeEvents.length,
|
||||||
itemBuilder: (context, dateIndex) {
|
itemBuilder: (context, index) {
|
||||||
List<DateTime?> dateList = (_activeEvents.isNotEmpty
|
LogEvent event = _activeEvents[index];
|
||||||
? <DateTime?>[null]
|
return Card(
|
||||||
: <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(
|
child: ListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
handleEditAction(event);
|
handleEditAction(event);
|
||||||
},
|
},
|
||||||
title: Row(
|
title: Row(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
CrossAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(date == null
|
Text(
|
||||||
? DateTimeUtils
|
DateTimeUtils.displayDateTime(event.time),
|
||||||
.displayDateTime(
|
|
||||||
event.time)
|
|
||||||
: DateTimeUtils.displayTime(
|
|
||||||
event.isEndEvent
|
|
||||||
? event.endTime
|
|
||||||
: event.time),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
(event.title ?? event.eventType.target?.value ?? '').toUpperCase(),
|
(event.title ??
|
||||||
|
event.eventType.target?.value ??
|
||||||
|
'')
|
||||||
|
.toUpperCase(),
|
||||||
style: Theme.of(context).textTheme.subtitle2,
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -173,8 +206,122 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
|
|||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
event.hasEndTime &&
|
event.hasEndTime && event.endTime == null
|
||||||
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(
|
? IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.stop,
|
Icons.stop,
|
||||||
@ -189,40 +336,23 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
|
|||||||
Icons.edit,
|
Icons.edit,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
),
|
),
|
||||||
onPressed: () =>
|
onPressed: () => handleEditAction(event),
|
||||||
handleEditAction(event),
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.delete,
|
Icons.delete,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
),
|
),
|
||||||
onPressed: () =>
|
onPressed: () => handleDeleteAction(event),
|
||||||
handleDeleteAction(event),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
return eventList != null && eventList.isNotEmpty ? ListBody(
|
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(10.0),
|
|
||||||
child: Text(
|
|
||||||
DateTimeUtils.displayDate(date, fallback: 'Active Events').toUpperCase(),
|
|
||||||
style: Theme.of(context).textTheme.subtitle2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] + tiles +
|
|
||||||
[const Divider()],
|
|
||||||
) : Container();
|
|
||||||
},
|
},
|
||||||
),
|
))
|
||||||
)
|
|
||||||
: const Center(
|
: const Center(
|
||||||
child: Text('There are no Events!'),
|
child: Text('There are no Events for that date!'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
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/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
import 'package:diameter/components/forms/form_wrapper.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
@ -32,6 +34,7 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
|
|||||||
bool _isNew = true;
|
bool _isNew = true;
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
bool _isExpanded = false;
|
bool _isExpanded = false;
|
||||||
|
bool _setManually = false;
|
||||||
|
|
||||||
final GlobalKey<FormState> _mealForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _mealForm = GlobalKey<FormState>();
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
@ -242,7 +245,7 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onSelectMealSource(MealSource? mealSource) async {
|
void onSelectMealSource(MealSource? mealSource) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_mealSource = mealSource;
|
_mealSource = mealSource;
|
||||||
_mealSourceController.text = (_mealSource ?? '').toString();
|
_mealSourceController.text = (_mealSource ?? '').toString();
|
||||||
@ -263,40 +266,72 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void calculateThirdMeasurementOfPortionCarbsRelation() {
|
void calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
double? carbsRatio;
|
{double? carbsRatioUpdate,
|
||||||
double? portionSize;
|
double? portionSizeUpdate,
|
||||||
double? carbsPerPortion;
|
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 != '') {
|
int toCalculate = 0;
|
||||||
carbsRatio = double.tryParse(_carbsRatioController.text);
|
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;
|
||||||
}
|
}
|
||||||
if (_portionSizeController.text != '') {
|
} else if (portionSizeUpdate != null) {
|
||||||
portionSize = double.tryParse(_portionSizeController.text);
|
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 (_carbsRatioController.text != '') {
|
|
||||||
carbsPerPortion = double.tryParse(_carbsPerPortionController.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (carbsRatio != null && portionSize != null && carbsPerPortion == null) {
|
|
||||||
setState(() {
|
|
||||||
_carbsPerPortionController.text =
|
|
||||||
Utils.calculateCarbs(carbsRatio!, portionSize!)
|
|
||||||
.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (carbsRatio == null && portionSize != null && carbsPerPortion != null) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
if (toCalculate == calcCarbsRatio) {
|
||||||
_carbsRatioController.text =
|
_carbsRatioController.text =
|
||||||
Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!)
|
Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!)
|
||||||
.toString();
|
.toString();
|
||||||
});
|
} else if (toCalculate == calcCarbsPerPortion) {
|
||||||
}
|
_carbsPerPortionController.text = Utils.calculateCarbs(
|
||||||
if (carbsRatio != null && portionSize == null && carbsPerPortion != null) {
|
carbsRatio!, portionSize!,
|
||||||
setState(() {
|
step: Settings.nutritionSteps)
|
||||||
_portionSizeController.text =
|
|
||||||
Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!)
|
|
||||||
.toString();
|
.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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: NumberFormField(
|
||||||
decoration: const InputDecoration(
|
label: 'Carbs ratio',
|
||||||
labelText: 'Carbs ratio',
|
suffix: '%',
|
||||||
suffixText: '%',
|
|
||||||
),
|
|
||||||
controller: _carbsRatioController,
|
controller: _carbsRatioController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
showSteppers: false,
|
||||||
decimal: true),
|
onChanged: (value) async {
|
||||||
onChanged: (_) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation();
|
calculateThirdMeasurementOfPortionCarbsRelation(carbsRatioUpdate: value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: NumberFormField(
|
||||||
decoration: InputDecoration(
|
label: 'Portion size',
|
||||||
labelText: 'Portion size',
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
suffixText: Settings.nutritionMeasurementSuffix,
|
|
||||||
),
|
|
||||||
controller: _portionSizeController,
|
controller: _portionSizeController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
showSteppers: false,
|
||||||
decimal: true),
|
onChanged: (value) async {
|
||||||
onChanged: (_) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation();
|
calculateThirdMeasurementOfPortionCarbsRelation(portionSizeUpdate: value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: NumberFormField(
|
||||||
decoration: InputDecoration(
|
label: 'Carbs per portion',
|
||||||
labelText: 'Carbs per portion',
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
suffixText: Settings.nutritionMeasurementSuffix,
|
|
||||||
),
|
|
||||||
controller: _carbsPerPortionController,
|
controller: _carbsPerPortionController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
showSteppers: false,
|
||||||
decimal: true),
|
onChanged: (value) async {
|
||||||
onChanged: (_) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
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(
|
TextFormField(
|
||||||
controller: _notesController,
|
controller: _notesController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
@ -463,8 +501,6 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// ignore: todo
|
|
||||||
// TODO: display according to time format
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -486,13 +522,14 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
|
|||||||
value: _delayedBolusPercentage,
|
value: _delayedBolusPercentage,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
onChanged: _delayedBolusDurationController.text != ''
|
onChanged:
|
||||||
|
_delayedBolusDurationController.text != ''
|
||||||
? (value) {
|
? (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_delayedBolusPercentage = value;
|
_delayedBolusPercentage = value;
|
||||||
});
|
});
|
||||||
} : null
|
}
|
||||||
),
|
: null),
|
||||||
),
|
),
|
||||||
const Text('%', textScaleFactor: 1.5),
|
const Text('%', textScaleFactor: 1.5),
|
||||||
],
|
],
|
||||||
|
@ -1,9 +1,24 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
class Utils {
|
class Utils {
|
||||||
static double roundToDecimalPlaces(double value, int precision) {
|
// static double roundToDecimalPlaces(double value, int precision) {
|
||||||
double mod = pow(10.0, precision).toDouble();
|
// double mod = pow(10.0, precision).toDouble();
|
||||||
return ((value * mod).round().toDouble() / mod);
|
// 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) {
|
static double addDoublesWithPrecision(double a, double b, int precision) {
|
||||||
@ -14,7 +29,7 @@ class Utils {
|
|||||||
|
|
||||||
static int getFractionDigitsLength(double value) {
|
static int getFractionDigitsLength(double value) {
|
||||||
final fractionDigits = value.toString().split('.');
|
final fractionDigits = value.toString().split('.');
|
||||||
return fractionDigits[1].length;
|
return fractionDigits[1] == '0' ? 0 : fractionDigits[1].length;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String toStringMatchingTemplateFractionPrecision(
|
static String toStringMatchingTemplateFractionPrecision(
|
||||||
@ -23,29 +38,30 @@ class Utils {
|
|||||||
return value.toStringAsFixed(precision);
|
return value.toStringAsFixed(precision);
|
||||||
}
|
}
|
||||||
|
|
||||||
static double convertMgPerDlToMmolPerL(int mgPerDl) {
|
static double convertMgPerDlToMmolPerL(int mgPerDl, {double step = 0.01}) {
|
||||||
return Utils.roundToDecimalPlaces(mgPerDl * 0.0555, 2);
|
return Utils.roundToMultipleOfBase(mgPerDl * 0.0555, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int convertMmolPerLToMgPerDl(double mmolPerL) {
|
static int convertMmolPerLToMgPerDl(double mmolPerL) {
|
||||||
return (mmolPerL * 18.018).round();
|
return (mmolPerL * 18.018).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
static double calculateCarbs(double carbsRatio, double portionSize) {
|
static double calculateCarbs(double carbsRatio, double portionSize,
|
||||||
return Utils.roundToDecimalPlaces(carbsRatio * portionSize / 100, 2);
|
{double step = 0.01}) {
|
||||||
|
return Utils.roundToMultipleOfBase(carbsRatio * portionSize / 100, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
static double calculateCarbsRatio(
|
static double calculateCarbsRatio(
|
||||||
double carbsPerPortion, double portionSize) {
|
double carbsPerPortion, double portionSize, {double step = 0.01}) {
|
||||||
return portionSize > 0
|
return portionSize > 0
|
||||||
? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2)
|
? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / portionSize, step)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static double calculatePortionSize(
|
static double calculatePortionSize(
|
||||||
double carbsRatio, double carbsPerPortion) {
|
double carbsRatio, double carbsPerPortion, {double step = 0.01}) {
|
||||||
return carbsRatio > 0
|
return carbsRatio > 0
|
||||||
? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2)
|
? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / carbsRatio, step)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user