various updates
This commit is contained in:
parent
f7d727e070
commit
f590a134b5
155
TODO
155
TODO
@ -1,7 +1,150 @@
|
|||||||
|
MAIN TASKS:
|
||||||
|
Components/Framework:
|
||||||
|
☐ come up with new concept for duration component
|
||||||
|
☐ update duration fields to use corresponding component
|
||||||
|
☐ log event type detail (reminder duration)
|
||||||
|
☐ log event detail (reminder duration)
|
||||||
|
☐ meal (bolus delay)
|
||||||
|
☐ log bolus (delay)
|
||||||
|
☐ set name properties as unique (and add checks to forms)
|
||||||
|
☐ check through all detail forms and set required fields/according messages
|
||||||
|
☐ change placement of delete and floating button because its very easy to accidentally hit delete
|
||||||
|
☐ implement deletion by swiping left on item instead?
|
||||||
|
☐ check for changes before navigating as well (not just on cancel)
|
||||||
|
Reports:
|
||||||
|
☐ evaluate what type of reports there should be
|
||||||
|
☐ try out graph/diagram components
|
||||||
|
|
||||||
Todo:
|
FUTURE TASKS:
|
||||||
☐ add active/deleted flag to all data models
|
Features:
|
||||||
☐ account for deleted/disabled elements in dropdowns
|
☐ app icon
|
||||||
☐ place dropdown items right below their input
|
☐ desktop version
|
||||||
✔ use local database instead of back4app @done(21-11-07 18:53)
|
☐ add explanations to each section
|
||||||
|
☐ alternate languages
|
||||||
|
☐ log hba1c
|
||||||
|
☐ indicate nested creation process (creating from dropdown etc)
|
||||||
|
☐ enable restoring data from sync
|
||||||
|
☐ indicate read only fields
|
||||||
|
Components/Framework:
|
||||||
|
☐ show indicator and make all fields readonly if user somehow gets to a deleted record detail view
|
||||||
|
☐ dropdown tweaks
|
||||||
|
☐ edit item -> cancel: shouldn't clear dropdwon
|
||||||
|
☐ keep focus on textfield when typing
|
||||||
|
☐ account for deleted/disabled elements
|
||||||
|
Accuracy:
|
||||||
|
☐ same icons in detail as in overview to indicate what's what
|
||||||
|
Recipe:
|
||||||
|
☐ update to use correct components, init/dispose etc
|
||||||
|
☐ change the entire concept of ingredients
|
||||||
|
☐ add functionality to create a meal from a recipe
|
||||||
|
Reports:
|
||||||
|
☐ meal tweaking
|
||||||
|
☐ bolus tweaking
|
||||||
|
☐ basal test
|
||||||
|
☐ daily graph (showing glucose curve, events, boli and meals)
|
||||||
|
Log Overview:
|
||||||
|
☐ add filters
|
||||||
|
Log Entry:
|
||||||
|
☐ check if there is still an active bolus when suggesting glucose bolus
|
||||||
|
Event Types:
|
||||||
|
☐ add colors as indicators for log entries (and later graphs in reports)
|
||||||
|
☐ implement reminders as push notifications
|
||||||
|
Settings:
|
||||||
|
☐ add option to hide extra customization options (ie. changing pre calculated values)?
|
||||||
|
☐ option to switch theme
|
||||||
|
☐ add fields for glucose target tiers (as map of cutoff glucose and colors)
|
||||||
|
☐ add field for active insulin duration
|
||||||
|
☐ add setting for carb units/bread units
|
||||||
|
☐ add option to switch 'save' and 'save & close' buttons
|
||||||
|
☐ add functionality to delete dead records (meaning: set deleted flag and no relations to undeleted records)
|
||||||
|
|
||||||
|
Archive:
|
||||||
|
✔ only show current day @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview)
|
||||||
|
✔ add calendar field on top to navigate @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview)
|
||||||
|
✔ use currently selected day when adding a log entry @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview)
|
||||||
|
✔ only show current day @done(22-01-24 05:39) @project(MAIN TASKS.Event Types)
|
||||||
|
✔ add calendar field on top to navigate @done(22-01-24 05:39) @project(MAIN TASKS.Event Types)
|
||||||
|
✔ use currently selected day when adding a log event @done(22-01-24 05:39) @project(MAIN TASKS.Event Types)
|
||||||
|
✔ update number fields to use corresponding components @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ meal detail (carbs ratio, portion size, carbs per portion) @done(22-01-24 03:12) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ log meal detail (amount, carbs ratio, portion size, carbs per portion) @done(22-01-24 03:12) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ add "set manually" switch (like in log bolus detail) wherever parameters can be calculated from others @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ meal detail @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ log meal detail @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ put dropdowns first if they override name field @done(22-01-24 03:17) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ settings (target glucose, increments) @done(22-01-22 01:48) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ accuracy detail (confidence rating) @done(22-01-21 16:51) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ basal detail (units) @done(22-01-21 18:14) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ bolus detail (units, per carbs, per glucose) @done(22-01-21 20:35) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ log entry (glucose) @done(22-01-22 15:13) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ log bolus detail (units, current, target, correction, carbs) @done(22-01-22 22:59) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ add dispose methods everywhere and clean up controllers @done(22-01-21 17:55) @project(MAIN TASKS.Components/Framework)
|
||||||
|
✔ fix spacing @done(22-01-21 17:20) @project(MAIN TASKS.Event Types)
|
||||||
|
✔ calculation log meal carbs @done(22-01-08 22:21) @project(BUG FIXES.Log Entry)
|
||||||
|
✔ implement component for durations @done(22-01-08 19:00) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ make glucose optional @done(22-01-08 19:00) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ add setting for decimal places/unit steps @done(22-01-08 22:18) @project(MAIN TASKS.Settings)
|
||||||
|
✔ add fields for preferred date and time formats @done(22-01-07 21:06) @project(MAIN TASKS.Settings)
|
||||||
|
✔ add field for glucose target @done(22-01-08 19:00) @project(MAIN TASKS.Settings)
|
||||||
|
✔ setup objectbox sync server @done(21-12-22 15:21) @project(FUTURE TASKS.General/Framework)
|
||||||
|
✔ recipe list screen @done(21-12-11 22:01) @project(MAIN TASKS.Recipe)
|
||||||
|
✔ recipe detail screen @done(21-12-11 22:01) @project(MAIN TASKS.Recipe)
|
||||||
|
✔ add model for recipe @done(21-12-11 02:23) @project(MAIN TASKS.Recipe)
|
||||||
|
✔ add model for ingredient (relation betweeen recipe and meal) @done(21-12-11 02:23) @project(MAIN TASKS.Recipe)
|
||||||
|
✔ give option to specify quantity @done(21-12-11 01:28) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ give option to pick meal from a different log entry (that doesn't have an associated bolus yet and within certain time span) @done(21-12-11 02:22) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ find a better way to work with multiple glucose measurements @done(21-12-11 02:23) @project(FUTURE TASKS.General/Framework)
|
||||||
|
✔ make components rounder/nicer/closer to new material style @done(21-12-10 04:10) @project(MAIN TASKS.Layout)
|
||||||
|
✔ make sure 'null' isn't shown in text fields @done(21-12-10 04:23) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ hide details like accuracies etc when picking meals @done(21-12-10 06:12) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ add save and close and next buttons on rate creations @done(21-12-10 06:12) @project(MAIN TASKS.Basal/Bolus)
|
||||||
|
✔ always calculate other glucose measurement from active one and make other one readonly @done(21-12-10 04:33) @project(MAIN TASKS.Basal/Bolus)
|
||||||
|
✔ add save and close button @done(21-12-10 06:11) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ move on to newly created entry after saving @done(21-12-10 06:11) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ recalculate bolus upon deactivating 'set manually' option @done(21-12-10 06:18) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ account for delayed percentage setting on choosing meals @done(21-12-10 06:39) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ fix preloading of dropdown values (appear blank at first as of now) @done(21-12-09 05:31) @project(BUGFIXES.General/Framework)
|
||||||
|
✔ glucose target isn't displayed correctly anymore @done(21-12-09 05:31) @project(BUGFIXES.Log Entry)
|
||||||
|
✔ hide dropdown overlay on tapping anywhere else (especially menu) @done(21-12-07 21:04) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ add clear button to dropdown @done(21-12-07 21:21) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ add option to specify trend for blood sugar @done(21-12-07 14:20) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ always calculate other glucose measurement from active one and make other one readonly @done(21-12-07 14:33) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ scrollbars in rate overview not showing @done(21-12-06 20:01) @project(BUGFIXES.Basal/Bolus)
|
||||||
|
✔ order category lists (meals, meal sources,...) alphabetically @done(21-12-06 20:34) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ add delay to auto conversions @done(21-12-06 20:25) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ show daily Basal sum in overview @done(21-12-06 21:09) @project(MAIN TASKS.Basal/Bolus)
|
||||||
|
✔ show KI and stuff for Bolus in overview @done(21-12-06 21:44) @project(MAIN TASKS.Basal/Bolus)
|
||||||
|
✔ apply target color settings to glucose @done(21-12-06 22:57) @project(MAIN TASKS.Log Overview)
|
||||||
|
✔ improve log meal list display @done(21-12-06 20:25) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ change delayed bolus rate to percentage @done(21-12-06 20:47) @project(MAIN TASKS.Meal)
|
||||||
|
✔ add meal source, carbs and portion size as subtitle in list @done(21-12-06 22:01) @project(MAIN TASKS.Meal)
|
||||||
|
✔ add option to hide warning dialogs on cancel, delete or event stop @done(21-12-05 19:18) @project(FUTURE TASKS.Settings)
|
||||||
|
✔ fix settings saving @done(21-12-05 19:08) @project(MAIN TASKS.Settings)
|
||||||
|
✔ add objectbox settings class and use instead of shared preferences @done(21-12-05 00:41) @project(MAIN TASKS.Settings)
|
||||||
|
✔ provide percentage functionality for delayed bolus @done(21-12-04 21:39) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ create two bolus entries accordingly @done(21-12-04 22:12) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ replace active profile picking mode with simple dropdown @done(21-12-04 20:10) @project(MAIN TASKS.Basal/Bolus)
|
||||||
|
✔ indicate both the default rate and the currently active one (according to event) @done(21-12-04 20:10) @project(MAIN TASKS.Basal/Bolus)
|
||||||
|
✔ get rid of excessive cancellation warnings @done(21-12-04 19:09) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ give a warning if event of same type is already running @done(21-12-04 18:50) @project(MAIN TASKS.Events)
|
||||||
|
✔ implement reordering @started(21-12-03 23:12) @done(21-12-04 17:01) @lasted(17h49m38s) @project(MAIN TASKS.Accuracies)
|
||||||
|
✔ show event start AND end times in list @done(21-12-03 22:04) @project(MAIN TASKS.Events)
|
||||||
|
✔ separate events from log entries @done(21-12-01 23:37) @project(MAIN TASKS.Events)
|
||||||
|
✔ show total bolus and carbs per entry @done(21-12-01 19:50) @project(MAIN TASKS.Log Overview)
|
||||||
|
✔ display boli correctly @done(21-11-30 04:14) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ replace meal and glucose boli with logbolus entities @done(21-11-30 03:56) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ adjust/debug active events view @done(21-11-26 22:54) @project(MAIN TASKS.Log Overview)
|
||||||
|
✔ show all active events, not just those assigned to the entry @done(21-11-26 22:12) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ add active events view (as main menu item) @done(21-11-26 21:28) @project(MAIN TASKS.Log Overview)
|
||||||
|
✔ add option to change bolus/basal profile for event duration @done(21-11-26 21:13) @project(MAIN TASKS.Event Types)
|
||||||
|
✔ add deleted flag to all data models @done(21-11-26 18:56) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ adjust remove and fetch methods accordingly @done(21-11-26 20:52) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ implement tostring methods for all models @done(21-11-26 20:52) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ fix logmeals/logboli/logevents @done(21-11-25 17:10) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ add tab for bolus overview @done(21-11-24 22:05) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ calculate bolus suggestions according to active profile @done(21-11-24 22:05) @project(MAIN TASKS.Log Entry)
|
||||||
|
✔ place dropdown items right below their input @done(21-11-23 20:33) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ add autocomplete function to dropdowns @done(21-11-23 20:33) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ use local database instead of back4app @done(21-11-07 18:53) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ use ids instead of passing entities around where possible @done(21-11-10 00:06) @project(MAIN TASKS.General/Framework)
|
||||||
|
✔ add time picker for entry date/time @done(21-11-10 00:06) @project(MAIN TASKS.Log Entry)
|
||||||
|
@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
|
|||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 31
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -44,7 +44,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.example.diameter"
|
applicationId "com.example.diameter"
|
||||||
minSdkVersion 16
|
minSdkVersion 21
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.50'
|
ext.kotlin_version = '1.6.10'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -5,19 +5,47 @@ class AppTheme {
|
|||||||
AppTheme._();
|
AppTheme._();
|
||||||
|
|
||||||
static ThemeData lightTheme = FlexColorScheme.light(
|
static ThemeData lightTheme = FlexColorScheme.light(
|
||||||
scheme: FlexScheme.mandyRed,
|
surfaceStyle: FlexSurface.medium,
|
||||||
fontFamily: 'RobotoCondensed',
|
scheme: FlexScheme.aquaBlue,
|
||||||
|
fontFamily: 'Roboto',
|
||||||
).toTheme;
|
).toTheme;
|
||||||
|
|
||||||
static ThemeData darkTheme = FlexColorScheme.light(
|
static ThemeData darkTheme = FlexColorScheme.dark(
|
||||||
scheme: FlexScheme.mandyRed,
|
scheme: FlexScheme.aquaBlue,
|
||||||
fontFamily: 'RobotoCondensed',
|
fontFamily: 'Roboto',
|
||||||
).toTheme;
|
).toTheme;
|
||||||
|
|
||||||
static ThemeData makeTheme(ThemeData baseThemeData) {
|
static ThemeData makeTheme(ThemeData baseThemeData) {
|
||||||
return baseThemeData.copyWith(
|
return baseThemeData.copyWith(
|
||||||
visualDensity: VisualDensity.compact,
|
cardTheme: baseThemeData.cardTheme.copyWith(
|
||||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
color: baseThemeData.bottomAppBarColor,
|
||||||
backgroundColor: baseThemeData.primaryColor));
|
elevation: 1,
|
||||||
|
margin: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
scrollbarTheme: baseThemeData.scrollbarTheme.copyWith(
|
||||||
|
isAlwaysShown: true,
|
||||||
|
),
|
||||||
|
textTheme: baseThemeData.textTheme.copyWith(
|
||||||
|
subtitle2: TextStyle(
|
||||||
|
color: baseThemeData.primaryColor,
|
||||||
|
letterSpacing: 2.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: baseThemeData.inputDecorationTheme.copyWith(
|
||||||
|
fillColor: baseThemeData.textSelectionTheme.selectionColor,
|
||||||
|
border: const UnderlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(8.0),
|
||||||
|
topRight: Radius.circular(8.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||||
|
backgroundColor: baseThemeData.primaryColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
abstract class DataTableContent {
|
|
||||||
bool selected = false;
|
|
||||||
List<DataCell> asDataTableCells(List<Widget> actions) => [];
|
|
||||||
static List<DataColumn> asDataTableColumns() => [];
|
|
||||||
}
|
|
||||||
|
|
||||||
class DataTableSourceBuilder extends DataTableSource {
|
|
||||||
final List<DataTableContent> data;
|
|
||||||
final BuildContext context;
|
|
||||||
|
|
||||||
DataTableSourceBuilder(this.context, this.data);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isRowCountApproximate => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get rowCount => data.length;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get selectedRowCount {
|
|
||||||
int count = 0;
|
|
||||||
for (var element in data) {
|
|
||||||
if (element.selected) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DataRow? getRow(int index) {
|
|
||||||
assert(index >= 0);
|
|
||||||
if (index >= data.length) return null;
|
|
||||||
final rowData = data[index];
|
|
||||||
return DataRow.byIndex(
|
|
||||||
index: index,
|
|
||||||
selected: rowData.selected,
|
|
||||||
cells: rowData.asDataTableCells([]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,10 +2,22 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class DetailBottomRow extends StatefulWidget {
|
class DetailBottomRow extends StatefulWidget {
|
||||||
final void Function()? onCancel;
|
final void Function()? onCancel;
|
||||||
final void Function()? onSave;
|
final void Function()? onAction;
|
||||||
|
final void Function()? onMiddleAction;
|
||||||
|
final String actionText;
|
||||||
|
final String middleActionText;
|
||||||
|
final IconData actionIcon;
|
||||||
|
final IconData middleActionIcon;
|
||||||
|
|
||||||
const DetailBottomRow(
|
const DetailBottomRow(
|
||||||
{Key? key, required this.onCancel, required this.onSave})
|
{Key? key,
|
||||||
|
required this.onCancel,
|
||||||
|
required this.onAction,
|
||||||
|
this.onMiddleAction,
|
||||||
|
this.actionText = 'SAVE',
|
||||||
|
this.actionIcon = Icons.save,
|
||||||
|
this.middleActionText = 'SAVE & CLOSE',
|
||||||
|
this.middleActionIcon = Icons.done})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -19,6 +31,7 @@ class _DetailBottomRowState<T> extends State<DetailBottomRow> {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: widget.onCancel,
|
onPressed: widget.onCancel,
|
||||||
@ -28,14 +41,23 @@ class _DetailBottomRowState<T> extends State<DetailBottomRow> {
|
|||||||
),
|
),
|
||||||
label: const Text('CANCEL'),
|
label: const Text('CANCEL'),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
widget.onMiddleAction != null
|
||||||
|
? ElevatedButton.icon(
|
||||||
|
onPressed: widget.onMiddleAction,
|
||||||
|
icon: Icon(
|
||||||
|
widget.middleActionIcon,
|
||||||
|
size: 18.0,
|
||||||
|
),
|
||||||
|
label: Text(widget.middleActionText),
|
||||||
|
)
|
||||||
|
: const Spacer(),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: widget.onSave,
|
onPressed: widget.onAction,
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.save,
|
widget.actionIcon,
|
||||||
size: 18.0,
|
size: 18.0,
|
||||||
),
|
),
|
||||||
label: const Text('SAVE'),
|
label: Text(widget.actionText),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,222 +0,0 @@
|
|||||||
import 'package:diameter/components/progress_indicator.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class StyledForm extends StatefulWidget {
|
|
||||||
final List<Widget>? fields;
|
|
||||||
final List<Widget>? buttons;
|
|
||||||
final GlobalKey<FormState>? formState;
|
|
||||||
|
|
||||||
const StyledForm({Key? key, this.formState, this.fields, this.buttons})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_StyledFormState createState() => _StyledFormState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StyledFormState extends State<StyledForm> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(10.0),
|
|
||||||
child: Form(
|
|
||||||
key: widget.formState,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: widget.fields
|
|
||||||
?.map((e) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
|
||||||
child: e))
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: widget.buttons ?? [],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StyledBooleanFormField extends StatefulWidget {
|
|
||||||
final bool value;
|
|
||||||
final String label;
|
|
||||||
final void Function(bool) onChanged;
|
|
||||||
final bool? enabled;
|
|
||||||
|
|
||||||
const StyledBooleanFormField(
|
|
||||||
{Key? key,
|
|
||||||
required this.value,
|
|
||||||
required this.label,
|
|
||||||
required this.onChanged,
|
|
||||||
this.enabled})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_StyledBooleanFormFieldState createState() => _StyledBooleanFormFieldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StyledBooleanFormFieldState extends State<StyledBooleanFormField> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FormField<bool>(builder: (context) {
|
|
||||||
return ListTile(
|
|
||||||
onTap: () => widget.onChanged(!widget.value),
|
|
||||||
trailing: Switch(
|
|
||||||
value: widget.value,
|
|
||||||
onChanged: widget.onChanged,
|
|
||||||
),
|
|
||||||
title: Text(widget.label),
|
|
||||||
enabled: widget.enabled ?? true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StyledTimeOfDayFormField extends StatefulWidget {
|
|
||||||
final TimeOfDay time;
|
|
||||||
final TextEditingController controller;
|
|
||||||
final String label;
|
|
||||||
final void Function(TimeOfDay?) onChanged;
|
|
||||||
|
|
||||||
const StyledTimeOfDayFormField(
|
|
||||||
{Key? key,
|
|
||||||
required this.time,
|
|
||||||
required this.controller,
|
|
||||||
required this.label,
|
|
||||||
required this.onChanged})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_StyledTimeOfDayFormFieldState createState() =>
|
|
||||||
_StyledTimeOfDayFormFieldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StyledTimeOfDayFormFieldState extends State<StyledTimeOfDayFormField> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TextFormField(
|
|
||||||
readOnly: true,
|
|
||||||
controller: widget.controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: widget.label,
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
final newTime = await showTimePicker(
|
|
||||||
context: context,
|
|
||||||
initialTime: widget.time,
|
|
||||||
);
|
|
||||||
widget.onChanged(newTime);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StyledDropdownButton<T> extends StatefulWidget {
|
|
||||||
final String label;
|
|
||||||
final T? selectedItem;
|
|
||||||
final List<T> items;
|
|
||||||
final Widget Function(T item) renderItem;
|
|
||||||
final void Function(T? value) onChanged;
|
|
||||||
|
|
||||||
const StyledDropdownButton(
|
|
||||||
{Key? key,
|
|
||||||
this.selectedItem,
|
|
||||||
required this.label,
|
|
||||||
required this.items,
|
|
||||||
required this.renderItem,
|
|
||||||
required this.onChanged})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_StyledDropdownButtonState<T> createState() => _StyledDropdownButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StyledDropdownButtonState<T> extends State<StyledDropdownButton<T>> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return DropdownButtonFormField<T>(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: widget.label,
|
|
||||||
),
|
|
||||||
value: widget.selectedItem,
|
|
||||||
onChanged: widget.onChanged,
|
|
||||||
items: widget.items
|
|
||||||
.map((item) => DropdownMenuItem<T>(
|
|
||||||
value: item,
|
|
||||||
child: widget.renderItem(item),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StyledFutureDropdownButton<T> extends StatefulWidget {
|
|
||||||
final String label;
|
|
||||||
final String? selectedItem;
|
|
||||||
final Future<List<T>> items;
|
|
||||||
final String? Function(T item) getItemValue;
|
|
||||||
final Widget Function(T item) renderItem;
|
|
||||||
final void Function(String? value) onChanged;
|
|
||||||
|
|
||||||
const StyledFutureDropdownButton(
|
|
||||||
{Key? key,
|
|
||||||
this.selectedItem,
|
|
||||||
required this.label,
|
|
||||||
required this.items,
|
|
||||||
required this.getItemValue,
|
|
||||||
required this.renderItem,
|
|
||||||
required this.onChanged})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_StyledFutureDropdownButtonState<T> createState() =>
|
|
||||||
_StyledFutureDropdownButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StyledFutureDropdownButtonState<T>
|
|
||||||
extends State<StyledFutureDropdownButton<T>> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FutureBuilder<List<T>>(
|
|
||||||
future: widget.items,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
return ViewWithProgressIndicator(
|
|
||||||
snapshot: snapshot,
|
|
||||||
padding: const EdgeInsets.all(10.0),
|
|
||||||
progressIndicatorSize: 44,
|
|
||||||
child: snapshot.data == null || snapshot.data!.isEmpty
|
|
||||||
? Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: const [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(10.0),
|
|
||||||
child: Text('No Meal Sources'),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: DropdownButtonFormField<String>(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: widget.label,
|
|
||||||
),
|
|
||||||
value: widget.selectedItem,
|
|
||||||
onChanged: widget.onChanged,
|
|
||||||
items: snapshot.data!
|
|
||||||
.map((item) => DropdownMenuItem<String>(
|
|
||||||
value: widget.getItemValue(item),
|
|
||||||
child: widget.renderItem(item),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
230
lib/components/forms/auto_complete_dropdown_button.dart
Normal file
230
lib/components/forms/auto_complete_dropdown_button.dart
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AutoCompleteDropdownButton<T> extends StatefulWidget {
|
||||||
|
final String label;
|
||||||
|
final T? selectedItem;
|
||||||
|
final List<T> items;
|
||||||
|
final void Function(T? value) onChanged;
|
||||||
|
final List<T> Function(String? value)? applyQuery;
|
||||||
|
final TextEditingController controller;
|
||||||
|
|
||||||
|
const AutoCompleteDropdownButton(
|
||||||
|
{Key? key,
|
||||||
|
this.selectedItem,
|
||||||
|
required this.label,
|
||||||
|
required this.items,
|
||||||
|
required this.onChanged,
|
||||||
|
this.applyQuery,
|
||||||
|
required this.controller})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AutoCompleteDropdownButtonState<T> createState() =>
|
||||||
|
_AutoCompleteDropdownButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AutoCompleteDropdownButtonState<T>
|
||||||
|
extends State<AutoCompleteDropdownButton<T>> {
|
||||||
|
late List<T> options;
|
||||||
|
late List<T> suggestions;
|
||||||
|
|
||||||
|
final FocusNode focusNode = FocusNode();
|
||||||
|
final LayerLink layerLink = LayerLink();
|
||||||
|
OverlayEntry? entry;
|
||||||
|
bool isOpen = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
options = widget.items;
|
||||||
|
suggestions = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleOverlay() {
|
||||||
|
isOpen ? hideOverlay() : showOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showOverlay() {
|
||||||
|
hideOverlay();
|
||||||
|
focusNode.requestFocus();
|
||||||
|
|
||||||
|
List<Widget> items = [];
|
||||||
|
Divider? divider;
|
||||||
|
|
||||||
|
ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
final overlay = Overlay.of(context)!;
|
||||||
|
final renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
|
||||||
|
if (widget.selectedItem != null) {
|
||||||
|
items.add(buildListTile(widget.selectedItem!));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggestions.isNotEmpty) {
|
||||||
|
items.addAll(suggestions
|
||||||
|
.where((item) => item != widget.selectedItem)
|
||||||
|
.map((item) => buildListTile(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((widget.selectedItem != null || suggestions.isNotEmpty) &&
|
||||||
|
(options.length -
|
||||||
|
suggestions.length -
|
||||||
|
(widget.selectedItem == null ? 0 : 1) >
|
||||||
|
0)) {
|
||||||
|
divider = const Divider(height: 10);
|
||||||
|
items.add(divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.addAll(options
|
||||||
|
.where((item) =>
|
||||||
|
!(widget.selectedItem != null &&
|
||||||
|
item.toString() == widget.selectedItem!.toString()) &&
|
||||||
|
!suggestions.contains(item))
|
||||||
|
.map((item) => buildListTile(item))
|
||||||
|
.toList());
|
||||||
|
|
||||||
|
final screenHeight = MediaQuery.of(context).size.height;
|
||||||
|
final neededHeight = renderBox.size.height * (items.length - 1) +
|
||||||
|
(divider?.height ?? renderBox.size.height);
|
||||||
|
final availableHeight = screenHeight -
|
||||||
|
(renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height);
|
||||||
|
bool displayAbove = neededHeight > availableHeight &&
|
||||||
|
screenHeight - availableHeight > availableHeight;
|
||||||
|
final height = min(neededHeight,
|
||||||
|
max(availableHeight, screenHeight - availableHeight - 55) - 55);
|
||||||
|
|
||||||
|
entry = OverlayEntry(
|
||||||
|
builder: (context) => Positioned(
|
||||||
|
width: renderBox.size.width,
|
||||||
|
height: height,
|
||||||
|
child: CompositedTransformFollower(
|
||||||
|
link: layerLink,
|
||||||
|
targetAnchor: displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
|
||||||
|
followerAnchor:
|
||||||
|
displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
|
||||||
|
offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)),
|
||||||
|
showWhenUnlinked: false,
|
||||||
|
child: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
isAlwaysShown: true,
|
||||||
|
child: Material(
|
||||||
|
elevation: 8,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Column(
|
||||||
|
children: items,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
overlay.insert(entry!);
|
||||||
|
isOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTile buildListTile(T item) {
|
||||||
|
return ListTile(
|
||||||
|
onTap: () => handleChanged(item),
|
||||||
|
selected: item == widget.selectedItem,
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(item.toString()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideOverlay() {
|
||||||
|
entry?.remove();
|
||||||
|
setState(() {
|
||||||
|
entry = null;
|
||||||
|
isOpen = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleChanged(T? item) {
|
||||||
|
widget.onChanged(item);
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChangeQuery(String value) {
|
||||||
|
if (value.trim() == '' ||
|
||||||
|
(widget.selectedItem != null &&
|
||||||
|
value == widget.selectedItem!.toString())) {
|
||||||
|
setState(() {
|
||||||
|
suggestions = [];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (widget.applyQuery == null) {
|
||||||
|
setState(() {
|
||||||
|
suggestions = widget.items.where((item) {
|
||||||
|
String itemString = item.toString().toLowerCase();
|
||||||
|
String valueLowercase = value.toLowerCase();
|
||||||
|
return itemString.contains(valueLowercase);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
suggestions = widget.applyQuery!(value)
|
||||||
|
.where((item) => item != widget.selectedItem)
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Focus(
|
||||||
|
focusNode: focusNode,
|
||||||
|
onFocusChange: (isFocused) {
|
||||||
|
if (!isFocused) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: CompositedTransformTarget(
|
||||||
|
link: layerLink,
|
||||||
|
child: TextFormField(
|
||||||
|
onChanged: onChangeQuery,
|
||||||
|
onTap: toggleOverlay,
|
||||||
|
controller: widget.controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.label,
|
||||||
|
suffixIcon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
widget.selectedItem != null
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () => handleChanged(null),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
iconSize: 20.0,
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: toggleOverlay,
|
||||||
|
icon: const Icon(Icons.arrow_drop_down),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
lib/components/forms/boolean_form_field.dart
Normal file
39
lib/components/forms/boolean_form_field.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class BooleanFormField extends StatefulWidget {
|
||||||
|
final bool value;
|
||||||
|
final String label;
|
||||||
|
final void Function(bool) onChanged;
|
||||||
|
final bool? enabled;
|
||||||
|
final EdgeInsets? contentPadding;
|
||||||
|
|
||||||
|
const BooleanFormField(
|
||||||
|
{Key? key,
|
||||||
|
required this.value,
|
||||||
|
required this.label,
|
||||||
|
required this.onChanged,
|
||||||
|
this.enabled,
|
||||||
|
this.contentPadding})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_BooleanFormFieldState createState() => _BooleanFormFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BooleanFormFieldState extends State<BooleanFormField> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FormField<bool>(builder: (state) {
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: widget.contentPadding,
|
||||||
|
onTap: () => widget.onChanged(!widget.value),
|
||||||
|
trailing: Switch(
|
||||||
|
value: widget.value,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
),
|
||||||
|
title: Text(widget.label),
|
||||||
|
enabled: widget.enabled ?? true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
47
lib/components/forms/date_time_form_field.dart
Normal file
47
lib/components/forms/date_time_form_field.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DateTimeFormField extends StatefulWidget {
|
||||||
|
final DateTime date;
|
||||||
|
final DateTime? minDate;
|
||||||
|
final DateTime? maxDate;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String label;
|
||||||
|
final void Function(DateTime?) onChanged;
|
||||||
|
|
||||||
|
const DateTimeFormField(
|
||||||
|
{Key? key,
|
||||||
|
required this.date,
|
||||||
|
this.minDate,
|
||||||
|
this.maxDate,
|
||||||
|
required this.controller,
|
||||||
|
required this.label,
|
||||||
|
required this.onChanged})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_DateTimeFormFieldState createState() => _DateTimeFormFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateTimeFormFieldState extends State<DateTimeFormField> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
controller: widget.controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.label,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final newTime = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: widget.date,
|
||||||
|
firstDate: widget.minDate ?? DateTime(2000, 1, 1),
|
||||||
|
lastDate:
|
||||||
|
widget.maxDate ?? DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
widget.onChanged(newTime);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
123
lib/components/forms/duration_form_field.dart
Normal file
123
lib/components/forms/duration_form_field.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DurationFormField extends StatefulWidget {
|
||||||
|
final String label;
|
||||||
|
final int minutes;
|
||||||
|
final void Function(int?) onChanged;
|
||||||
|
final bool showSteppers;
|
||||||
|
final bool readOnly;
|
||||||
|
final int min;
|
||||||
|
final int? max;
|
||||||
|
final int step;
|
||||||
|
|
||||||
|
const DurationFormField(
|
||||||
|
{Key? key,
|
||||||
|
required this.label,
|
||||||
|
this.minutes = 0,
|
||||||
|
required this.onChanged,
|
||||||
|
this.showSteppers = false,
|
||||||
|
this.readOnly = false,
|
||||||
|
this.min = 0,
|
||||||
|
this.max,
|
||||||
|
this.step = 5})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_DurationFormFieldState createState() => _DurationFormFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DurationFormFieldState extends State<DurationFormField> {
|
||||||
|
late Duration duration;
|
||||||
|
final TextEditingController controller = TextEditingController(text: '');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
updateDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDuration() {
|
||||||
|
duration = Duration(minutes: widget.minutes);
|
||||||
|
|
||||||
|
int days = duration.inDays;
|
||||||
|
int hours = duration.inHours - days * 24;
|
||||||
|
int minutes = duration.inMinutes - hours * 60;
|
||||||
|
int seconds = duration.inSeconds - minutes * 60;
|
||||||
|
|
||||||
|
String daysString = days > 9 ? '$days d' : days > 0 ? '0$days d' : '00 d';
|
||||||
|
String hoursString = hours > 9 ? ' $hours h' : hours > 0 ? ' 0$hours h' : ' 00 h';
|
||||||
|
String minutesString = minutes > 9 ? ' $minutes m' : minutes > 0 ? ' 0$minutes m' : ' 00 m';
|
||||||
|
String secondsString = seconds > 9 ? ' $seconds s' : seconds > 0 ? ' 0$seconds s' : ' 00 s';
|
||||||
|
controller.text = '$daysString $hoursString $minutesString $secondsString'.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleChange(String value) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
int days = int.tryParse(value.split(' d')[0]) ?? 0;
|
||||||
|
int hours = int.tryParse(value.split('d')[1].split(' h')[0]) ?? 0;
|
||||||
|
int minutes = int.tryParse(value.split('h')[1].split(' m')[0]) ?? 0;
|
||||||
|
int seconds = int.tryParse(value.split('m')[1].split(' s')[0]) ?? 0;
|
||||||
|
int updatedMinutes =
|
||||||
|
Duration(days: days, hours: hours, minutes: minutes, seconds: seconds)
|
||||||
|
.inMinutes;
|
||||||
|
|
||||||
|
widget.onChanged(updatedMinutes);
|
||||||
|
setState(() {
|
||||||
|
updateDuration();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onIncrease() {
|
||||||
|
if (widget.max == null || widget.minutes + widget.step <= widget.max!) {
|
||||||
|
int value = widget.minutes + widget.step;
|
||||||
|
widget.onChanged(value);
|
||||||
|
setState(() {
|
||||||
|
updateDuration();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDecrease() {
|
||||||
|
if (widget.minutes - widget.step >= widget.min) {
|
||||||
|
int value = widget.minutes - widget.step;
|
||||||
|
widget.onChanged(value);
|
||||||
|
setState(() {
|
||||||
|
updateDuration();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
widget.showSteppers
|
||||||
|
? IconButton(
|
||||||
|
onPressed: onDecrease,
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.label,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.numberWithOptions(
|
||||||
|
decimal: true, signed: widget.min.isNegative),
|
||||||
|
onChanged: handleChange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
widget.showSteppers
|
||||||
|
? IconButton(
|
||||||
|
onPressed: onIncrease,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
45
lib/components/forms/form_wrapper.dart
Normal file
45
lib/components/forms/form_wrapper.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FormWrapper extends StatefulWidget {
|
||||||
|
final List<Widget>? fields;
|
||||||
|
final List<Widget>? buttons;
|
||||||
|
final GlobalKey<FormState>? formState;
|
||||||
|
|
||||||
|
const FormWrapper({Key? key, this.formState, this.fields, this.buttons})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_FormWrapperState createState() => _FormWrapperState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FormWrapperState extends State<FormWrapper> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
child: Form(
|
||||||
|
key: widget.formState,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: widget.fields
|
||||||
|
?.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
child: e))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: widget.buttons ?? [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
129
lib/components/forms/number_form_field.dart
Normal file
129
lib/components/forms/number_form_field.dart
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import 'package:diameter/components/repeat_on_hold_button.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NumberFormField extends StatefulWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final double min;
|
||||||
|
final double? max;
|
||||||
|
final double step;
|
||||||
|
final String label;
|
||||||
|
final String? suffix;
|
||||||
|
final void Function(double?) onChanged;
|
||||||
|
final bool readOnly;
|
||||||
|
final bool showSteppers;
|
||||||
|
final bool autoRoundToMultipleOfStep;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
|
||||||
|
const NumberFormField({
|
||||||
|
Key? key,
|
||||||
|
required this.controller,
|
||||||
|
required this.label,
|
||||||
|
required this.onChanged,
|
||||||
|
this.suffix,
|
||||||
|
this.min = 0,
|
||||||
|
this.max,
|
||||||
|
this.step = 1,
|
||||||
|
this.readOnly = false,
|
||||||
|
this.showSteppers = true,
|
||||||
|
this.autoRoundToMultipleOfStep = false,
|
||||||
|
this.validator,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_NumberFormFieldState createState() => _NumberFormFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NumberFormFieldState extends State<NumberFormField> {
|
||||||
|
int precision = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
precision = Utils.getFractionDigitsLength(widget.step) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onIncrease() {
|
||||||
|
double? currentValue = double.tryParse(widget.controller.text);
|
||||||
|
|
||||||
|
if (currentValue != null &&
|
||||||
|
(widget.max == null || currentValue + widget.step <= widget.max!)) {
|
||||||
|
widget.onChanged(
|
||||||
|
Utils.addDoublesWithPrecision(currentValue, widget.step, precision));
|
||||||
|
setState(() {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onDecrease() {
|
||||||
|
double? currentValue = double.tryParse(widget.controller.text);
|
||||||
|
|
||||||
|
if (currentValue != null && (currentValue - widget.step >= widget.min)) {
|
||||||
|
widget.onChanged(
|
||||||
|
Utils.addDoublesWithPrecision(currentValue, -widget.step, precision));
|
||||||
|
setState(() {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
widget.showSteppers
|
||||||
|
? RepeatOnHoldButton(
|
||||||
|
onTap: onDecrease,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: double.tryParse(widget.controller.text) != null &&
|
||||||
|
(double.parse(widget.controller.text) - widget.step >=
|
||||||
|
widget.min)
|
||||||
|
? onDecrease
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
readOnly: widget.readOnly,
|
||||||
|
controller: widget.controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.label,
|
||||||
|
suffixText: widget.suffix,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.numberWithOptions(
|
||||||
|
decimal: widget.step > 0 && widget.step < 1,
|
||||||
|
signed: widget.min.isNegative),
|
||||||
|
onChanged: (input) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
double? value = double.tryParse(input);
|
||||||
|
if (widget.autoRoundToMultipleOfStep) {
|
||||||
|
value = value != null ? Utils.roundToMultipleOfBase(value, widget.step) : null;
|
||||||
|
}
|
||||||
|
widget.onChanged(value);
|
||||||
|
},
|
||||||
|
validator: widget.validator,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
widget.showSteppers
|
||||||
|
? RepeatOnHoldButton(
|
||||||
|
onTap: onIncrease,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: double.tryParse(widget.controller.text) != null &&
|
||||||
|
(widget.max == null ||
|
||||||
|
double.parse(widget.controller.text) +
|
||||||
|
widget.step <=
|
||||||
|
widget.max!)
|
||||||
|
? onIncrease
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
40
lib/components/forms/time_of_day_form_field.dart
Normal file
40
lib/components/forms/time_of_day_form_field.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TimeOfDayFormField extends StatefulWidget {
|
||||||
|
final TimeOfDay time;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String label;
|
||||||
|
final void Function(TimeOfDay?) onChanged;
|
||||||
|
|
||||||
|
const TimeOfDayFormField(
|
||||||
|
{Key? key,
|
||||||
|
required this.time,
|
||||||
|
required this.controller,
|
||||||
|
required this.label,
|
||||||
|
required this.onChanged})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TimeOfDayFormFieldState createState() => _TimeOfDayFormFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeOfDayFormFieldState extends State<TimeOfDayFormField> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
controller: widget.controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.label,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final newTime = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: widget.time,
|
||||||
|
);
|
||||||
|
widget.onChanged(newTime);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,65 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
// import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class ViewWithProgressIndicator extends StatefulWidget {
|
|
||||||
final AsyncSnapshot snapshot;
|
|
||||||
final Widget child;
|
|
||||||
final double progressIndicatorSize;
|
|
||||||
final EdgeInsets padding;
|
|
||||||
|
|
||||||
const ViewWithProgressIndicator(
|
|
||||||
{Key? key,
|
|
||||||
required this.snapshot,
|
|
||||||
required this.child,
|
|
||||||
this.progressIndicatorSize = 100,
|
|
||||||
this.padding = const EdgeInsets.all(0)})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ViewWithProgressIndicatorState createState() =>
|
|
||||||
_ViewWithProgressIndicatorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ViewWithProgressIndicatorState extends State<ViewWithProgressIndicator> {
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
switch (widget.snapshot.connectionState) {
|
|
||||||
case ConnectionState.none:
|
|
||||||
case ConnectionState.waiting:
|
|
||||||
return Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
padding: widget.padding,
|
|
||||||
child: Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: widget.progressIndicatorSize,
|
|
||||||
height: widget.progressIndicatorSize,
|
|
||||||
child: FutureBuilder(
|
|
||||||
future: Future.delayed(const Duration(seconds: 1)),
|
|
||||||
builder: (context, wait) {
|
|
||||||
if (wait.connectionState != ConnectionState.waiting) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
}
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
if (widget.snapshot.hasError) {
|
|
||||||
return Center(
|
|
||||||
child: Text(widget.snapshot.error.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!widget.snapshot.hasData) {
|
|
||||||
return const Center(
|
|
||||||
child: Text("No data"),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return widget.child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
74
lib/components/repeat_on_hold_button.dart
Normal file
74
lib/components/repeat_on_hold_button.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RepeatOnHoldButton extends StatefulWidget {
|
||||||
|
/// Function to be called on tap and on long press.
|
||||||
|
/// Return [false] to signify that the loop should be broken after execution.
|
||||||
|
final bool? Function() onTap;
|
||||||
|
|
||||||
|
/// Specifies whether repetition speeds up when the user keeps holding the button.
|
||||||
|
final bool increaseSpeed;
|
||||||
|
|
||||||
|
/// Specifies how many ms should pass before action is repeated.
|
||||||
|
final int initialRepetitionIntervalMs;
|
||||||
|
|
||||||
|
/// Specifies by how much the interval between actions should be divided after [speedUpAfterTimes] times.
|
||||||
|
final int speedUpFactor;
|
||||||
|
|
||||||
|
/// Specifies how many times [onTap] will be called before increasing the speed.
|
||||||
|
final int speedUpAfterTimes;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const RepeatOnHoldButton({
|
||||||
|
Key? key,
|
||||||
|
required this.onTap,
|
||||||
|
this.increaseSpeed = true,
|
||||||
|
this.initialRepetitionIntervalMs = 250,
|
||||||
|
this.speedUpFactor = 2,
|
||||||
|
this.speedUpAfterTimes = 5,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RepeatOnHoldButtonState createState() => _RepeatOnHoldButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RepeatOnHoldButtonState extends State<RepeatOnHoldButton> {
|
||||||
|
bool _isHeld = false;
|
||||||
|
|
||||||
|
void onLongPress() async {
|
||||||
|
setState(() {
|
||||||
|
_isHeld = true;
|
||||||
|
});
|
||||||
|
int holdCycle = 0;
|
||||||
|
int speed = widget.initialRepetitionIntervalMs;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
final result = widget.onTap() ?? true;
|
||||||
|
if (!_isHeld || !result) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
holdCycle++;
|
||||||
|
if (speed > 1 && holdCycle % widget.speedUpAfterTimes == 0) {
|
||||||
|
speed = max(1, (speed ~/ widget.speedUpFactor));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.delayed(
|
||||||
|
Duration(
|
||||||
|
milliseconds: speed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
onLongPressEnd: (_) => _isHeld = false,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +1 @@
|
|||||||
import 'package:diameter/settings.dart';
|
String secret = 'm4Gwehzgv18jZ5gCVUBZl5li3Z0FX2Yb';
|
||||||
|
|
||||||
const keyApplicationId = 'DFfD2aeppmqQnVmox02kUZhYOUc7vAtGfunAP7hn';
|
|
||||||
const keyClientKey = '0ROGEVQP0Id21EMEqK05wJP3nBDuOW5DM5Cpzdt3';
|
|
||||||
const keyParseServerUrl = 'https://parseapi.back4app.com';
|
|
||||||
|
|
||||||
// settings
|
|
||||||
NutritionMeasurement nutritionMeasurement = NutritionMeasurement.grams;
|
|
||||||
GlucoseMeasurement glucoseMeasurement = GlucoseMeasurement.mgPerDl;
|
|
||||||
GlucoseDisplayMode glucoseDisplayMode = GlucoseDisplayMode.bothForList;
|
|
||||||
|
|
||||||
DateTime dummyDate = DateTime(2000);
|
|
||||||
String dateFormat = 'MM/dd/yy';
|
|
||||||
String? longDateFormat = 'MMMM dd, yyyy';
|
|
||||||
String timeFormat = 'HH:mm';
|
|
||||||
String? longTimeFormat = 'HH:mm:ss';
|
|
||||||
|
|
||||||
bool showConfirmationDialogOnCancel = true;
|
|
||||||
bool showConfirmationDialogOnDelete = true;
|
|
||||||
bool showConfirmationDialogOnStopEvent = true;
|
|
||||||
|
|
||||||
int lowGlucoseMgPerDl = 80;
|
|
||||||
int moderateGlucoseMgPerDl = 140;
|
|
||||||
int highGlucoseMgPerDl = 240;
|
|
||||||
|
|
||||||
double lowGlucoseMmolPerL = 4.44;
|
|
||||||
double moderateGlucoseMmolPerL = 7.77;
|
|
||||||
double highGlucoseMmolPerDl = 13.32;
|
|
||||||
|
103
lib/main.dart
103
lib/main.dart
@ -1,13 +1,16 @@
|
|||||||
import 'package:diameter/components/app_theme.dart';
|
import 'package:diameter/components/app_theme.dart';
|
||||||
|
import 'package:diameter/config.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/object_box.dart';
|
import 'package:diameter/object_box.dart';
|
||||||
import 'package:diameter/screens/accuracy_detail.dart';
|
import 'package:diameter/screens/accuracy_detail.dart';
|
||||||
import 'package:diameter/screens/basal/basal_profile_detail.dart';
|
import 'package:diameter/screens/basal/basal_profile_detail.dart';
|
||||||
import 'package:diameter/screens/bolus/bolus_profile_detail.dart';
|
import 'package:diameter/screens/bolus/bolus_profile_detail.dart';
|
||||||
import 'package:diameter/screens/log/log.dart';
|
import 'package:diameter/screens/log/log.dart';
|
||||||
import 'package:diameter/screens/log/log_entry.dart';
|
import 'package:diameter/screens/log/log_entry/log_entry.dart';
|
||||||
import 'package:diameter/screens/log/log_event_detail.dart';
|
import 'package:diameter/screens/log/log_event/log_event_detail.dart';
|
||||||
import 'package:diameter/screens/log/log_event_type_detail.dart';
|
import 'package:diameter/screens/log/log_event/log_event_list.dart';
|
||||||
import 'package:diameter/screens/log/log_event_type_list.dart';
|
import 'package:diameter/screens/log/log_event/log_event_type_detail.dart';
|
||||||
|
import 'package:diameter/screens/log/log_event/log_event_type_list.dart';
|
||||||
import 'package:diameter/screens/meal/meal_category_detail.dart';
|
import 'package:diameter/screens/meal/meal_category_detail.dart';
|
||||||
import 'package:diameter/screens/meal/meal_category_list.dart';
|
import 'package:diameter/screens/meal/meal_category_list.dart';
|
||||||
import 'package:diameter/screens/meal/meal_detail.dart';
|
import 'package:diameter/screens/meal/meal_detail.dart';
|
||||||
@ -16,61 +19,67 @@ import 'package:diameter/screens/meal/meal_portion_type_detail.dart';
|
|||||||
import 'package:diameter/screens/meal/meal_portion_type_list.dart';
|
import 'package:diameter/screens/meal/meal_portion_type_list.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_list.dart';
|
import 'package:diameter/screens/meal/meal_source_list.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_detail.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_list.dart';
|
||||||
import 'package:diameter/settings.dart';
|
import 'package:diameter/settings.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:parse_server_sdk_flutter/parse_server_sdk.dart';
|
|
||||||
import 'package:diameter/screens/accuracy_list.dart';
|
import 'package:diameter/screens/accuracy_list.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/screens/basal/basal_profile_list.dart';
|
||||||
import 'package:diameter/screens/basal/basal_profiles_list.dart';
|
|
||||||
import 'package:diameter/screens/bolus/bolus_profile_list.dart';
|
import 'package:diameter/screens/bolus/bolus_profile_list.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
|
||||||
late ObjectBox objectBox;
|
late ObjectBox objectBox;
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await Parse().initialize(
|
|
||||||
keyApplicationId,
|
|
||||||
keyParseServerUrl,
|
|
||||||
clientKey: keyClientKey,
|
|
||||||
debug: true,
|
|
||||||
coreStore: await CoreStoreSharedPrefsImp.getInstance(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Settings.loadSettingsIntoConfig();
|
|
||||||
|
|
||||||
objectBox = await ObjectBox.create();
|
objectBox = await ObjectBox.create();
|
||||||
|
|
||||||
|
Sync.isAvailable();
|
||||||
|
SyncClient syncClient = Sync.client(
|
||||||
|
objectBox.store,
|
||||||
|
'ws://192.168.1.184:9999',
|
||||||
|
SyncCredentials.sharedSecretString(secret)
|
||||||
|
);
|
||||||
|
syncClient.start();
|
||||||
|
syncClient.requestUpdates(subscribeForFuturePushes: false);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MaterialApp(
|
GestureDetector(
|
||||||
theme: AppTheme.makeTheme(AppTheme.lightTheme),
|
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
darkTheme: AppTheme.makeTheme(AppTheme.darkTheme),
|
child: MaterialApp(
|
||||||
themeMode: ThemeMode.system,
|
theme: AppTheme.makeTheme(AppTheme.lightTheme),
|
||||||
initialRoute: '/',
|
darkTheme: AppTheme.makeTheme(AppTheme.darkTheme),
|
||||||
routes: {
|
themeMode: Settings.themeMode,
|
||||||
'/': (context) => const LogScreen(),
|
initialRoute: '/',
|
||||||
Routes.log: (context) => const LogScreen(),
|
routes: {
|
||||||
Routes.logEntry: (context) => const LogEntryScreen(),
|
'/': (context) => const LogScreen(),
|
||||||
Routes.logEvent: (context) => const LogEventDetailScreen(),
|
Routes.log: (context) => const LogScreen(),
|
||||||
Routes.logEventTypes: (context) => const LogEventTypeListScreen(),
|
Routes.logEntry: (context) => const LogEntryScreen(),
|
||||||
Routes.logEventType: (context) => const LogEventTypeDetailScreen(),
|
Routes.logEvent: (context) => const LogEventDetailScreen(),
|
||||||
Routes.accuracies: (context) => const AccuracyListScreen(),
|
Routes.logEventTypes: (context) => const LogEventTypeListScreen(),
|
||||||
Routes.accuracy: (context) => const AccuracyDetailScreen(),
|
Routes.logEventType: (context) => const EventTypeDetailScreen(),
|
||||||
Routes.meals: (context) => const MealListScreen(),
|
Routes.events: (context) => const LogEventListScreen(),
|
||||||
Routes.meal: (context) => const MealDetailScreen(),
|
Routes.accuracies: (context) => const AccuracyListScreen(),
|
||||||
Routes.mealCategories: (context) => const MealCategoryListScreen(),
|
Routes.accuracy: (context) => const AccuracyDetailScreen(),
|
||||||
Routes.mealCategory: (context) => const MealCategoryDetailScreen(),
|
Routes.meals: (context) => const MealListScreen(),
|
||||||
Routes.mealPortionTypes: (context) => const MealPortionTypeListScreen(),
|
Routes.meal: (context) => const MealDetailScreen(),
|
||||||
Routes.mealPortionType: (context) =>
|
Routes.recipes: (context) => const RecipeListScreen(),
|
||||||
const MealPortionTypeDetailScreen(),
|
Routes.recipe: (context) => const RecipeDetailScreen(),
|
||||||
Routes.mealSources: (context) => const MealSourceListScreen(),
|
Routes.mealCategories: (context) => const MealCategoryListScreen(),
|
||||||
Routes.mealSource: (context) => const MealSourceDetailScreen(),
|
Routes.mealCategory: (context) => const MealCategoryDetailScreen(),
|
||||||
Routes.bolusProfiles: (context) => const BolusProfileListScreen(),
|
Routes.mealPortionTypes: (context) =>
|
||||||
Routes.bolusProfile: (context) => const BolusProfileDetailScreen(),
|
const MealPortionTypeListScreen(),
|
||||||
Routes.basalProfiles: (context) => const BasalProfileListScreen(),
|
Routes.mealPortionType: (context) =>
|
||||||
Routes.basalProfile: (context) => const BasalProfileDetailScreen(),
|
const MealPortionTypeDetailScreen(),
|
||||||
Routes.settings: (context) => const SettingsScreen(),
|
Routes.mealSources: (context) => const MealSourceListScreen(),
|
||||||
},
|
Routes.mealSource: (context) => const MealSourceDetailScreen(),
|
||||||
|
Routes.bolusProfiles: (context) => const BolusProfileListScreen(),
|
||||||
|
Routes.bolusProfile: (context) => const BolusProfileDetailScreen(),
|
||||||
|
Routes.basalProfiles: (context) => const BasalProfileListScreen(),
|
||||||
|
Routes.basalProfile: (context) => const BasalProfileDetailScreen(),
|
||||||
|
Routes.settings: (context) => const SettingsScreen(),
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:diameter/objectbox.g.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show Accuracy_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 291512798403320400)
|
||||||
|
@Sync()
|
||||||
class Accuracy {
|
class Accuracy {
|
||||||
static final Box<Accuracy> box = objectBox.store.box<Accuracy>();
|
static final Box<Accuracy> box = objectBox.store.box<Accuracy>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String value;
|
String value;
|
||||||
bool forCarbsRatio;
|
bool forCarbsRatio;
|
||||||
bool forPortionSize;
|
bool forPortionSize;
|
||||||
int? confidenceRating;
|
int? confidenceRating;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
|
// constructor
|
||||||
Accuracy({
|
Accuracy({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.value = '',
|
this.value = '',
|
||||||
this.forCarbsRatio = false,
|
this.forCarbsRatio = false,
|
||||||
this.forPortionSize = false,
|
this.forPortionSize = false,
|
||||||
@ -21,25 +27,51 @@ class Accuracy {
|
|||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static Accuracy? get(int id) => box.get(id);
|
static Accuracy? get(int id) => box.get(id);
|
||||||
|
static void put(Accuracy accuracy) => box.put(accuracy);
|
||||||
|
|
||||||
static List<Accuracy> getAll() {
|
static List<Accuracy> getAll() {
|
||||||
QueryBuilder<Accuracy> all = box.query()..order(Accuracy_.confidenceRating);
|
QueryBuilder<Accuracy> all = box.query(Accuracy_.deleted.equals(false))
|
||||||
|
..order(Accuracy_.confidenceRating);
|
||||||
return all.build().find();
|
return all.build().find();
|
||||||
}
|
}
|
||||||
static void put(Accuracy accuracy) => box.put(accuracy);
|
|
||||||
static void remove(int id) => box.remove(id);
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static List<Accuracy> getAllForPortionSize() {
|
static List<Accuracy> getAllForPortionSize() {
|
||||||
QueryBuilder<Accuracy> allForPortionSize = box
|
QueryBuilder<Accuracy> allForPortionSize = box.query(
|
||||||
.query(Accuracy_.forPortionSize.equals(true))
|
Accuracy_.forPortionSize.equals(true) & Accuracy_.deleted.equals(false))
|
||||||
..order(Accuracy_.confidenceRating);
|
..order(Accuracy_.confidenceRating);
|
||||||
return allForPortionSize.build().find();
|
return allForPortionSize.build().find();
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Accuracy> getAllForCarbsRatio() {
|
static List<Accuracy> getAllForCarbsRatio() {
|
||||||
QueryBuilder<Accuracy> allForCarbsRatio = box
|
QueryBuilder<Accuracy> allForCarbsRatio = box.query(
|
||||||
.query(Accuracy_.forCarbsRatio.equals(true))
|
Accuracy_.forCarbsRatio.equals(true) & Accuracy_.deleted.equals(false))
|
||||||
..order(Accuracy_.confidenceRating);
|
..order(Accuracy_.confidenceRating);
|
||||||
return allForCarbsRatio.build().find();
|
return allForCarbsRatio.build().find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void reorder(Accuracy accuracy, int? newPosition) {
|
||||||
|
QueryBuilder<Accuracy> all = box.query(Accuracy_.deleted.equals(false).and(Accuracy_.id.notEquals(accuracy.id)))
|
||||||
|
..order(Accuracy_.confidenceRating);
|
||||||
|
List<Accuracy> accuracies = all.build().find();
|
||||||
|
newPosition == null || newPosition >= accuracies.length ? accuracies.add(accuracy) : accuracies.insert(newPosition, accuracy);
|
||||||
|
box.putMany(accuracies.map((item) {
|
||||||
|
item.confidenceRating = accuracies.indexOf(item);
|
||||||
|
return item;
|
||||||
|
}).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,75 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:diameter/models/basal_profile.dart';
|
import 'package:diameter/models/basal_profile.dart';
|
||||||
import 'package:diameter/objectbox.g.dart';
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show Basal_, BasalProfile_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 1467758525778521891)
|
||||||
|
@Sync()
|
||||||
class Basal {
|
class Basal {
|
||||||
static final Box<Basal> box = objectBox.store.box<Basal>();
|
static final Box<Basal> box = objectBox.store.box<Basal>();
|
||||||
|
|
||||||
int id;
|
|
||||||
|
|
||||||
|
// properties
|
||||||
|
int id;
|
||||||
|
bool deleted;
|
||||||
@Property(type: PropertyType.date)
|
@Property(type: PropertyType.date)
|
||||||
DateTime startTime;
|
DateTime startTime;
|
||||||
|
|
||||||
@Property(type: PropertyType.date)
|
@Property(type: PropertyType.date)
|
||||||
DateTime endTime;
|
DateTime endTime;
|
||||||
|
|
||||||
double units;
|
double units;
|
||||||
|
|
||||||
|
// relations
|
||||||
final basalProfile = ToOne<BasalProfile>();
|
final basalProfile = ToOne<BasalProfile>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
Basal({
|
Basal({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
required this.startTime,
|
required this.startTime,
|
||||||
required this.endTime,
|
required this.endTime,
|
||||||
this.units = 0,
|
this.units = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static Basal? get(int id) => box.get(id);
|
static Basal? get(int id) => box.get(id);
|
||||||
static void put(Basal basal) => box.put(basal);
|
static void put(Basal basal) => box.put(basal);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static List<Basal> getAllForProfile(int id) {
|
static List<Basal> getAllForProfile(int id) {
|
||||||
QueryBuilder<Basal> builder = box.query()..order(Basal_.startTime);
|
QueryBuilder<Basal> builder = box.query(Basal_.deleted.equals(false))
|
||||||
|
..order(Basal_.startTime);
|
||||||
builder.link(Basal_.basalProfile, BasalProfile_.id.equals(id));
|
builder.link(Basal_.basalProfile, BasalProfile_.id.equals(id));
|
||||||
return builder.build().find();
|
return builder.build().find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double getDailyTotalForProfile(int id) {
|
||||||
|
double sum = 0.0;
|
||||||
|
|
||||||
|
QueryBuilder<Basal> builder = box.query(Basal_.deleted.equals(false));
|
||||||
|
builder.link(Basal_.basalProfile, BasalProfile_.id.equals(id));
|
||||||
|
|
||||||
|
List<Basal> basalRates = builder.build().find();
|
||||||
|
for (Basal basal in basalRates) {
|
||||||
|
double rateDuration =
|
||||||
|
basal.endTime.difference(basal.startTime).inMinutes / 60;
|
||||||
|
if (rateDuration < 0) {
|
||||||
|
rateDuration += 24;
|
||||||
|
}
|
||||||
|
sum += basal.units * rateDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return DateTimeUtils.displayTime(startTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,87 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:diameter/objectbox.g.dart';
|
import 'package:diameter/models/log_event.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show BasalProfile_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 3613736032926903785)
|
||||||
|
@Sync()
|
||||||
class BasalProfile {
|
class BasalProfile {
|
||||||
static final Box<BasalProfile> box = objectBox.store.box<BasalProfile>();
|
static final Box<BasalProfile> box = objectBox.store.box<BasalProfile>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String name;
|
String name;
|
||||||
bool active;
|
bool active;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
|
// constructor
|
||||||
BasalProfile({
|
BasalProfile({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.name = '',
|
this.name = '',
|
||||||
this.active = false,
|
this.active = false,
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static BasalProfile? get(int id) => box.get(id);
|
static BasalProfile? get(int id) => box.get(id);
|
||||||
static List<BasalProfile> getAll() => box.getAll();
|
|
||||||
static void put(BasalProfile basalProfile) => box.put(basalProfile);
|
static void put(BasalProfile basalProfile) => box.put(basalProfile);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static List<BasalProfile> getAll() {
|
||||||
|
QueryBuilder<BasalProfile> all = box.query(BasalProfile_.deleted.equals(false))
|
||||||
|
..order(BasalProfile_.name);
|
||||||
|
return all.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int activeCount() {
|
static int activeCount() {
|
||||||
Query<BasalProfile> query =
|
Query<BasalProfile> query = box
|
||||||
box.query(BasalProfile_.active.equals(true)).build();
|
.query(BasalProfile_.active.equals(true) & BasalProfile_.deleted.equals(false)).build();
|
||||||
return query.find().length;
|
return query.find().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setAllInactive() {
|
static void setAllInactive() {
|
||||||
box.putMany(box.getAll().map((element) {
|
box.putMany(box.getAll().map((item) {
|
||||||
element.active = false;
|
item.active = false;
|
||||||
return element;
|
return item;
|
||||||
}).toList());
|
}).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static BasalProfile? getActive(DateTime? dateTime) {
|
||||||
|
if (dateTime != null) {
|
||||||
|
List<LogEvent> activeEvents = LogEvent.getAllActiveForTime(dateTime)
|
||||||
|
.where((event) => event.basalProfile.target != null).toList();
|
||||||
|
if (activeEvents.length > 1) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
activeEvents =
|
||||||
|
activeEvents.where((item) => !activeEvents.any((other) =>
|
||||||
|
item.time.isBefore(other.time) || (item.endTime ?? now).isAfter(other.endTime ?? now)
|
||||||
|
)).toList();
|
||||||
|
}
|
||||||
|
if (activeEvents.length == 1) {
|
||||||
|
return activeEvents.single.basalProfile.target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Query<BasalProfile> query = box
|
||||||
|
.query(BasalProfile_.active
|
||||||
|
.equals(true)
|
||||||
|
.and(BasalProfile_.deleted.equals(false)))
|
||||||
|
.build();
|
||||||
|
final result = query.find();
|
||||||
|
return result.length != 1 ? null : result.single;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:diameter/models/bolus_profile.dart';
|
import 'package:diameter/models/bolus_profile.dart';
|
||||||
import 'package:diameter/objectbox.g.dart';
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show Bolus_, BolusProfile_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 3417770529060202389)
|
||||||
|
@Sync()
|
||||||
class Bolus {
|
class Bolus {
|
||||||
static final Box<Bolus> box = objectBox.store.box<Bolus>();
|
static final Box<Bolus> box = objectBox.store.box<Bolus>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
@Property(type: PropertyType.date)
|
@Property(type: PropertyType.date)
|
||||||
DateTime startTime;
|
DateTime startTime;
|
||||||
@Property(type: PropertyType.date)
|
@Property(type: PropertyType.date)
|
||||||
@ -16,10 +22,13 @@ class Bolus {
|
|||||||
int? mgPerDl;
|
int? mgPerDl;
|
||||||
double? mmolPerL;
|
double? mmolPerL;
|
||||||
|
|
||||||
|
// relations
|
||||||
final bolusProfile = ToOne<BolusProfile>();
|
final bolusProfile = ToOne<BolusProfile>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
Bolus({
|
Bolus({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
required this.startTime,
|
required this.startTime,
|
||||||
required this.endTime,
|
required this.endTime,
|
||||||
this.units = 0,
|
this.units = 0,
|
||||||
@ -28,13 +37,48 @@ class Bolus {
|
|||||||
this.mmolPerL,
|
this.mmolPerL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static Bolus? get(int id) => box.get(id);
|
static Bolus? get(int id) => box.get(id);
|
||||||
static void put(Bolus bolus) => box.put(bolus);
|
static void put(Bolus bolus) => box.put(bolus);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
|
||||||
static List<Bolus> getAllForProfile(int id) {
|
static List<Bolus> getAllForProfile(int id) {
|
||||||
QueryBuilder<Bolus> builder = box.query()..order(Bolus_.startTime);
|
QueryBuilder<Bolus> builder = box.query(Bolus_.deleted.equals(false))
|
||||||
|
..order(Bolus_.startTime);
|
||||||
builder.link(Bolus_.bolusProfile, BolusProfile_.id.equals(id));
|
builder.link(Bolus_.bolusProfile, BolusProfile_.id.equals(id));
|
||||||
return builder.build().find();
|
return builder.build().find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Bolus? getRateForTime(DateTime? dateTime) {
|
||||||
|
if (dateTime != null) {
|
||||||
|
final bolusProfile = BolusProfile.getActive(dateTime);
|
||||||
|
final time = DateTimeUtils.convertTimeOfDayToDateTime(
|
||||||
|
TimeOfDay.fromDateTime(dateTime));
|
||||||
|
if (bolusProfile != null) {
|
||||||
|
final rates = Bolus.getAllForProfile(bolusProfile.id);
|
||||||
|
final result = rates.where((rate) {
|
||||||
|
DateTime endTime = rate.endTime == dummyDate
|
||||||
|
? rate.endTime.add(const Duration(days: 1))
|
||||||
|
: rate.endTime;
|
||||||
|
return (time.isAfter(rate.startTime) ||
|
||||||
|
time.isAtSameMomentAs(rate.startTime)) &&
|
||||||
|
time.isBefore(endTime);
|
||||||
|
});
|
||||||
|
return result.length != 1 ? null : result.single;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return DateTimeUtils.displayTime(startTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,53 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:diameter/objectbox.g.dart';
|
import 'package:diameter/models/log_event.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show BolusProfile_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 8812452529027052317)
|
||||||
|
@Sync()
|
||||||
class BolusProfile {
|
class BolusProfile {
|
||||||
static final Box<BolusProfile> box = objectBox.store.box<BolusProfile>();
|
static final Box<BolusProfile> box = objectBox.store.box<BolusProfile>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String name;
|
String name;
|
||||||
bool active;
|
bool active;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
|
// constructor
|
||||||
BolusProfile({
|
BolusProfile({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.name = '',
|
this.name = '',
|
||||||
this.active = false,
|
this.active = false,
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static BolusProfile? get(int id) => box.get(id);
|
static BolusProfile? get(int id) => box.get(id);
|
||||||
static List<BolusProfile> getAll() => box.getAll();
|
|
||||||
static void put(BolusProfile bolusProfile) => box.put(bolusProfile);
|
static void put(BolusProfile bolusProfile) => box.put(bolusProfile);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static List<BolusProfile> getAll() {
|
||||||
|
QueryBuilder<BolusProfile> all =
|
||||||
|
box.query(BolusProfile_.deleted.equals(false))..order(BolusProfile_.name);
|
||||||
|
return all.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int activeCount() {
|
static int activeCount() {
|
||||||
Query<BolusProfile> query =
|
Query<BolusProfile> query = box
|
||||||
box.query(BolusProfile_.active.equals(true)).build();
|
.query(BolusProfile_.active
|
||||||
|
.equals(true)
|
||||||
|
.and(BolusProfile_.deleted.equals(false)))
|
||||||
|
.build();
|
||||||
return query.find().length;
|
return query.find().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,4 +57,34 @@ class BolusProfile {
|
|||||||
return element;
|
return element;
|
||||||
}).toList());
|
}).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static BolusProfile? getActive(DateTime? dateTime) {
|
||||||
|
if (dateTime != null) {
|
||||||
|
List<LogEvent> activeEvents = LogEvent.getAllActiveForTime(dateTime)
|
||||||
|
.where((event) => event.bolusProfile.target != null).toList();
|
||||||
|
if (activeEvents.length > 1) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
activeEvents =
|
||||||
|
activeEvents.where((item) => !activeEvents.any((other) =>
|
||||||
|
item.time.isBefore(other.time) || (item.endTime ?? now).isAfter(other.endTime ?? now)
|
||||||
|
)).toList();
|
||||||
|
}
|
||||||
|
if (activeEvents.length == 1) {
|
||||||
|
return activeEvents.single.bolusProfile.target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Query<BolusProfile> query = box
|
||||||
|
.query(BolusProfile_.active
|
||||||
|
.equals(true)
|
||||||
|
.and(BolusProfile_.deleted.equals(false)))
|
||||||
|
.build();
|
||||||
|
final result = query.find();
|
||||||
|
return result.length != 1 ? null : result.single;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
122
lib/models/glucose_target.dart
Normal file
122
lib/models/glucose_target.dart
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import 'package:diameter/main.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show GlucoseTarget_;
|
||||||
|
|
||||||
|
@Entity(uid: 5041265995704044399)
|
||||||
|
@Sync()
|
||||||
|
class GlucoseTarget {
|
||||||
|
static final Box<GlucoseTarget> box = objectBox.store.box<GlucoseTarget>();
|
||||||
|
|
||||||
|
// properties
|
||||||
|
int id;
|
||||||
|
bool deleted;
|
||||||
|
int fromMgPerDL;
|
||||||
|
int toMgPerDl;
|
||||||
|
double fromMmolPerL;
|
||||||
|
double toMmolPerL;
|
||||||
|
int color;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
GlucoseTarget({
|
||||||
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
|
required this.fromMgPerDL,
|
||||||
|
required this.toMgPerDl,
|
||||||
|
required this.fromMmolPerL,
|
||||||
|
required this.toMmolPerL,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
|
static GlucoseTarget? get(int id) => box.get(id);
|
||||||
|
|
||||||
|
// methods
|
||||||
|
static List<GlucoseTarget> getAll() {
|
||||||
|
if (box.getAll().isEmpty) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
return box.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color getColorForGlucose({int mgPerDl = 0, double mmolPerL = 0}) {
|
||||||
|
if (box.getAll().isEmpty) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
Condition<GlucoseTarget> condition;
|
||||||
|
if (mgPerDl > 0 &&
|
||||||
|
(mmolPerL == 0 ||
|
||||||
|
Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl)) {
|
||||||
|
condition = GlucoseTarget_.fromMgPerDL.lessOrEqual(mgPerDl).and(GlucoseTarget_.toMgPerDl.greaterOrEqual(mgPerDl));
|
||||||
|
} else if (mmolPerL > 0 &&
|
||||||
|
(mgPerDl == 0 ||
|
||||||
|
Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL)) {
|
||||||
|
condition = GlucoseTarget_.fromMmolPerL.lessOrEqual(mmolPerL).and(GlucoseTarget_.toMmolPerL.greaterOrEqual(mmolPerL));
|
||||||
|
} else {
|
||||||
|
return Colors.black;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GlucoseTarget> result = box
|
||||||
|
.query(GlucoseTarget_.deleted.equals(false) & condition)
|
||||||
|
.build()
|
||||||
|
.find();
|
||||||
|
|
||||||
|
if (result.length != 1) {
|
||||||
|
return Colors.black;
|
||||||
|
}
|
||||||
|
return Color(result.single.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void put(GlucoseTarget glucoseTarget) => box.put(glucoseTarget);
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reset() {
|
||||||
|
box.removeAll();
|
||||||
|
List<GlucoseTarget> defaultTargets = [
|
||||||
|
GlucoseTarget(
|
||||||
|
fromMgPerDL: 0,
|
||||||
|
toMgPerDl: 69,
|
||||||
|
fromMmolPerL: 0,
|
||||||
|
toMmolPerL: 3.83,
|
||||||
|
color: Colors.red.value,
|
||||||
|
),
|
||||||
|
GlucoseTarget(
|
||||||
|
fromMgPerDL: 70,
|
||||||
|
toMgPerDl: 99,
|
||||||
|
fromMmolPerL: 3.84,
|
||||||
|
toMmolPerL: 5.48,
|
||||||
|
color: Colors.orange.value,
|
||||||
|
),
|
||||||
|
GlucoseTarget(
|
||||||
|
fromMgPerDL: 100,
|
||||||
|
toMgPerDl: 140,
|
||||||
|
fromMmolPerL: 5.49,
|
||||||
|
toMmolPerL: 7.77,
|
||||||
|
color: Colors.green.value,
|
||||||
|
),
|
||||||
|
GlucoseTarget(
|
||||||
|
fromMgPerDL: 141,
|
||||||
|
toMgPerDl: 240,
|
||||||
|
fromMmolPerL: 7.78,
|
||||||
|
toMmolPerL: 13.32,
|
||||||
|
color: Colors.orange.value,
|
||||||
|
),
|
||||||
|
GlucoseTarget(
|
||||||
|
fromMgPerDL: 241,
|
||||||
|
toMgPerDl: 999,
|
||||||
|
fromMmolPerL: 13.33,
|
||||||
|
toMmolPerL: 55.44,
|
||||||
|
color: Colors.deepOrange.value,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
box.putMany(defaultTargets);
|
||||||
|
}
|
||||||
|
}
|
79
lib/models/ingredient.dart
Normal file
79
lib/models/ingredient.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import 'package:diameter/main.dart';
|
||||||
|
import 'package:diameter/models/meal.dart';
|
||||||
|
import 'package:diameter/models/recipe.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show Ingredient_, Recipe_;
|
||||||
|
|
||||||
|
@Entity(uid: 6950311793136068892)
|
||||||
|
@Sync()
|
||||||
|
class Ingredient {
|
||||||
|
static final Box<Ingredient> box = objectBox.store.box<Ingredient>();
|
||||||
|
|
||||||
|
// properties
|
||||||
|
int id;
|
||||||
|
bool deleted;
|
||||||
|
double amount;
|
||||||
|
|
||||||
|
// relations
|
||||||
|
final recipe = ToOne<Recipe>();
|
||||||
|
final ingredient = ToOne<Meal>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
Ingredient({
|
||||||
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
|
required this.amount,
|
||||||
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
|
static Ingredient? get(int id) => box.get(id);
|
||||||
|
static void put(Ingredient ingredient) => box.put(ingredient);
|
||||||
|
static void putMany(List<Ingredient> ingredients) => box.putMany(ingredients);
|
||||||
|
|
||||||
|
static List<Ingredient> getAllForRecipe(int id) {
|
||||||
|
QueryBuilder<Ingredient> builder =
|
||||||
|
box.query(Ingredient_.deleted.equals(false));
|
||||||
|
builder.link(Ingredient_.recipe, Recipe_.id.equals(id));
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static double? getCarbsRatioForRecipe(int id) {
|
||||||
|
double carbsSum = 0.0;
|
||||||
|
double totalWeight = 0.0;
|
||||||
|
|
||||||
|
List<Ingredient> ingredients = getAllForRecipe(id);
|
||||||
|
|
||||||
|
for (Ingredient ingredient in ingredients) {
|
||||||
|
if ((ingredient.ingredient.target?.carbsRatio ?? 0) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
totalWeight += ingredient.amount;
|
||||||
|
carbsSum +=
|
||||||
|
Utils.calculateCarbs(ingredient.ingredient.target!.carbsRatio!, ingredient.amount);
|
||||||
|
}
|
||||||
|
return totalWeight > 0
|
||||||
|
? Utils.calculateCarbsRatio(carbsSum, totalWeight)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double? getTotalWeightForRecipe(int id) {
|
||||||
|
double totalWeight = 0.0;
|
||||||
|
|
||||||
|
List<Ingredient> ingredients = getAllForRecipe(id);
|
||||||
|
|
||||||
|
for (Ingredient ingredient in ingredients) {
|
||||||
|
if (ingredient.ingredient.target?.carbsRatio == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
totalWeight += ingredient.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return ingredient.target?.value ?? '';
|
||||||
|
}
|
||||||
|
}
|
93
lib/models/log_bolus.dart
Normal file
93
lib/models/log_bolus.dart
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import 'package:diameter/main.dart';
|
||||||
|
import 'package:diameter/models/bolus.dart';
|
||||||
|
import 'package:diameter/models/log_entry.dart';
|
||||||
|
import 'package:diameter/models/log_meal.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show LogBolus_, LogEntry_, LogMeal_;
|
||||||
|
|
||||||
|
@Entity(uid: 8033487006694871160)
|
||||||
|
@Sync()
|
||||||
|
class LogBolus {
|
||||||
|
static final Box<LogBolus> box = objectBox.store.box<LogBolus>();
|
||||||
|
|
||||||
|
// properties
|
||||||
|
int id;
|
||||||
|
bool deleted;
|
||||||
|
double units;
|
||||||
|
double? carbs;
|
||||||
|
int? delay;
|
||||||
|
int? mgPerDlCurrent;
|
||||||
|
int? mgPerDlTarget;
|
||||||
|
int? mgPerDlCorrection;
|
||||||
|
double? mmolPerLCurrent;
|
||||||
|
double? mmolPerLTarget;
|
||||||
|
double? mmolPerLCorrection;
|
||||||
|
bool setManually;
|
||||||
|
String? notes;
|
||||||
|
|
||||||
|
// relations
|
||||||
|
final logEntry = ToOne<LogEntry>();
|
||||||
|
final rate = ToOne<Bolus>();
|
||||||
|
final meal = ToOne<LogMeal>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
LogBolus({
|
||||||
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
|
this.units = 0,
|
||||||
|
this.carbs,
|
||||||
|
this.delay,
|
||||||
|
this.mgPerDlCurrent,
|
||||||
|
this.mgPerDlTarget,
|
||||||
|
this.mgPerDlCorrection,
|
||||||
|
this.mmolPerLCurrent,
|
||||||
|
this.mmolPerLTarget,
|
||||||
|
this.mmolPerLCorrection,
|
||||||
|
this.setManually = false,
|
||||||
|
this.notes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
|
static LogBolus? get(int id) => box.get(id);
|
||||||
|
static void put(LogBolus logBolus) => box.put(logBolus);
|
||||||
|
|
||||||
|
static List<LogBolus> getAllForEntry(int id) {
|
||||||
|
QueryBuilder<LogBolus> builder = box.query(LogBolus_.deleted.equals(false));
|
||||||
|
builder.link(LogBolus_.logEntry, LogEntry_.id.equals(id));
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static double getTotalBolusForEntry(int id) {
|
||||||
|
QueryBuilder<LogBolus> builder = box.query(LogBolus_.deleted.equals(false));
|
||||||
|
builder.link(LogBolus_.logEntry, LogEntry_.id.equals(id));
|
||||||
|
return builder.build().property(LogBolus_.units).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool glucoseBolusForEntryExists(int id) {
|
||||||
|
QueryBuilder<LogBolus> builder = box.query(LogBolus_.deleted
|
||||||
|
.equals(false)
|
||||||
|
.and(LogBolus_.mgPerDlCorrection.notNull()));
|
||||||
|
builder.link(LogBolus_.meal, LogMeal_.id.equals(id));
|
||||||
|
return builder.build().find().isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool bolusForMealExists(int id) {
|
||||||
|
QueryBuilder<LogBolus> builder = box.query(LogBolus_.deleted
|
||||||
|
.equals(false));
|
||||||
|
builder.link(LogBolus_.meal, LogMeal_.id.equals(id));
|
||||||
|
return builder.build().find().isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return units.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +1,64 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:diameter/models/log_event.dart';
|
import 'package:diameter/models/log_bolus.dart';
|
||||||
import 'package:diameter/models/log_meal.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/objectbox.g.dart';
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show LogEntry_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 752131069307970560)
|
||||||
|
@Sync()
|
||||||
class LogEntry {
|
class LogEntry {
|
||||||
static final Box<LogEntry> box = objectBox.store.box<LogEntry>();
|
static final Box<LogEntry> box = objectBox.store.box<LogEntry>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
@Property(type: PropertyType.date)
|
@Property(type: PropertyType.date)
|
||||||
DateTime time;
|
DateTime time;
|
||||||
|
|
||||||
int? mgPerDl;
|
int? mgPerDl;
|
||||||
double? mmolPerL;
|
double? mmolPerL;
|
||||||
double? bolusGlucose;
|
double? glucoseTrend;
|
||||||
int? delayedBolusDuration;
|
|
||||||
double? delayedBolusRate;
|
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
@Backlink('logEntry')
|
// constructor
|
||||||
final events = ToMany<LogEvent>();
|
|
||||||
|
|
||||||
@Backlink('endLogEntry')
|
|
||||||
final endedEvents = ToMany<LogEvent>();
|
|
||||||
|
|
||||||
@Backlink('logEntry')
|
|
||||||
final meals = ToMany<LogMeal>();
|
|
||||||
|
|
||||||
LogEntry({
|
LogEntry({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
required this.time,
|
required this.time,
|
||||||
this.mgPerDl,
|
this.mgPerDl,
|
||||||
this.mmolPerL,
|
this.mmolPerL,
|
||||||
this.bolusGlucose,
|
this.glucoseTrend,
|
||||||
this.delayedBolusDuration,
|
|
||||||
this.delayedBolusRate,
|
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
static LogEntry? get(int id) => box.get(id);
|
// methods
|
||||||
|
static LogEntry? get(int id) => id == 0 ? null : box.get(id);
|
||||||
static List<LogEntry> getAll() => box.getAll();
|
static List<LogEntry> getAll() => box.getAll();
|
||||||
static void put(LogEntry logEntry) => box.put(logEntry);
|
static void put(LogEntry logEntry) => box.put(logEntry);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hasUncorrectedGlucose(int id) {
|
||||||
|
final entry = box.get(id);
|
||||||
|
if (((entry?.mgPerDl ?? 0) > Settings.targetMgPerDl ||
|
||||||
|
(entry?.mmolPerL ?? 0) > Settings.targetMmolPerL)) {
|
||||||
|
return !LogBolus.glucoseBolusForEntryExists(id);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static Map<DateTime, List<LogEntry>> getDailyEntryMap() {
|
static Map<DateTime, List<LogEntry>> getDailyEntryMap() {
|
||||||
Map<DateTime, List<LogEntry>> dateMap = <DateTime, List<LogEntry>>{};
|
Map<DateTime, List<LogEntry>> dateMap = <DateTime, List<LogEntry>>{};
|
||||||
|
|
||||||
QueryBuilder<LogEntry> allByDate = box.query()..order(LogEntry_.time, flags: Order.descending);
|
QueryBuilder<LogEntry> allByDate = box
|
||||||
|
.query(LogEntry_.deleted.equals(false))
|
||||||
|
..order(LogEntry_.time, flags: Order.descending);
|
||||||
List<LogEntry> entries = allByDate.build().find();
|
List<LogEntry> entries = allByDate.build().find();
|
||||||
DateTime? date;
|
DateTime? date;
|
||||||
|
|
||||||
@ -59,4 +69,19 @@ 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
|
||||||
|
String toString() {
|
||||||
|
return DateTimeUtils.displayDateTime(time);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,195 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:diameter/models/log_entry.dart';
|
import 'package:diameter/models/basal_profile.dart';
|
||||||
|
import 'package:diameter/models/bolus_profile.dart';
|
||||||
import 'package:diameter/models/log_event_type.dart';
|
import 'package:diameter/models/log_event_type.dart';
|
||||||
import 'package:diameter/objectbox.g.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show LogEvent_, LogEventType_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 4303325892753185970)
|
||||||
|
@Sync()
|
||||||
class LogEvent {
|
class LogEvent {
|
||||||
static final Box<LogEvent> box = objectBox.store.box<LogEvent>();
|
static final Box<LogEvent> box = objectBox.store.box<LogEvent>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
@Property(type: PropertyType.date)
|
@Property(type: PropertyType.date)
|
||||||
DateTime time;
|
DateTime time;
|
||||||
|
|
||||||
@Property(type: PropertyType.date)
|
@Property(type: PropertyType.date)
|
||||||
DateTime? endTime;
|
DateTime? endTime;
|
||||||
|
|
||||||
bool hasEndTime;
|
bool hasEndTime;
|
||||||
|
int? reminderDuration;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
final logEntry = ToOne<LogEntry>();
|
@Transient()
|
||||||
final endLogEntry = ToOne<LogEntry>();
|
String? title;
|
||||||
final eventType = ToOne<LogEventType>();
|
@Transient()
|
||||||
|
bool isEndEvent = false;
|
||||||
|
|
||||||
|
// relations
|
||||||
|
final eventType = ToOne<LogEventType>();
|
||||||
|
final bolusProfile = ToOne<BolusProfile>();
|
||||||
|
final basalProfile = ToOne<BasalProfile>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
LogEvent({
|
LogEvent({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
required this.time,
|
required this.time,
|
||||||
this.endTime,
|
this.endTime,
|
||||||
this.hasEndTime = false,
|
this.hasEndTime = false,
|
||||||
|
this.reminderDuration,
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static LogEvent? get(int id) => box.get(id);
|
static LogEvent? get(int id) => box.get(id);
|
||||||
static List<LogEvent> getAll() => box.getAll();
|
|
||||||
static void put(LogEvent logEvent) => box.put(logEvent);
|
static void put(LogEvent logEvent) => box.put(logEvent);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static List<LogEvent> getAllOngoing() {
|
static List<LogEvent> getAllOngoing() {
|
||||||
QueryBuilder<LogEvent> query =
|
QueryBuilder<LogEvent> query = box.query(LogEvent_.hasEndTime.equals(true) &
|
||||||
box.query(LogEvent_.hasEndTime.equals(true) & LogEvent_.endTime.isNull())..order(LogEvent_.time);
|
LogEvent_.endTime.isNull() &
|
||||||
|
LogEvent_.deleted.equals(false))
|
||||||
|
..order(LogEvent_.time);
|
||||||
return query.build().find();
|
return query.build().find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<LogEvent> getAllActiveForTime(DateTime? dateTime) {
|
||||||
|
if (dateTime != null) {
|
||||||
|
QueryBuilder<LogEvent> builder = box.query(
|
||||||
|
LogEvent_.hasEndTime.equals(true) & LogEvent_.deleted.equals(false))
|
||||||
|
..order(LogEvent_.time, flags: Order.descending);
|
||||||
|
final eventsWithEndTime = builder.build().find();
|
||||||
|
return eventsWithEndTime.where((event) {
|
||||||
|
return (!dateTime.isBefore(event.time)) &&
|
||||||
|
!dateTime.isAfter(event.endTime ?? DateTime.now());
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool eventTypeExistsForTime(int id, DateTime? dateTime) {
|
||||||
|
QueryBuilder<LogEvent> builder = box.query(
|
||||||
|
LogEvent_.hasEndTime.equals(true) & LogEvent_.deleted.equals(false))
|
||||||
|
..order(LogEvent_.time, flags: Order.descending);
|
||||||
|
builder.link(LogEvent_.eventType, LogEventType_.id.equals(id));
|
||||||
|
final eventsWithEndTime = builder.build().find();
|
||||||
|
if (dateTime != null) {
|
||||||
|
return eventsWithEndTime.where((event) {
|
||||||
|
return (!dateTime.isBefore(event.time)) &&
|
||||||
|
!dateTime.isAfter(event.endTime ?? DateTime.now());
|
||||||
|
}).isNotEmpty;
|
||||||
|
}
|
||||||
|
return eventsWithEndTime.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<DateTime, List<LogEvent>> getDailyEntryMap() {
|
||||||
|
Map<DateTime, List<LogEvent>> dateMap = <DateTime, List<LogEvent>>{};
|
||||||
|
Map<DateTime, List<LogEvent>> sortedDateMap = <DateTime, List<LogEvent>>{};
|
||||||
|
|
||||||
|
QueryBuilder<LogEvent> allByDate = box
|
||||||
|
.query(LogEvent_.deleted.equals(false))
|
||||||
|
..order(LogEvent_.time, flags: Order.descending);
|
||||||
|
List<LogEvent> events = allByDate.build().find();
|
||||||
|
|
||||||
|
DateTime? date;
|
||||||
|
|
||||||
|
for (LogEvent event in events) {
|
||||||
|
date = DateTime.utc(event.time.year, event.time.month, event.time.day);
|
||||||
|
LogEvent startEvent = event;
|
||||||
|
startEvent.title =
|
||||||
|
'${event.toString()} ${event.hasEndTime ? '(Start)' : ''}';
|
||||||
|
dateMap.putIfAbsent(date, () => <LogEvent>[]).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();
|
||||||
|
|
||||||
|
for (LogEvent event in endEvents) {
|
||||||
|
date = DateTime.utc(
|
||||||
|
event.endTime!.year, event.endTime!.month, event.endTime!.day);
|
||||||
|
LogEvent endEvent = event;
|
||||||
|
endEvent.isEndEvent = true;
|
||||||
|
endEvent.title = '${event.toString()} (End)';
|
||||||
|
dateMap.putIfAbsent(date, () => <LogEvent>[]).add(endEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final dates = dateMap.keys.toList();
|
||||||
|
dates.sort();
|
||||||
|
for (DateTime date in dates.reversed) {
|
||||||
|
dateMap[date]!.sort((LogEvent a, LogEvent b) {
|
||||||
|
final dateA = a.isEndEvent ? a.endTime : a.time;
|
||||||
|
final dateB = b.isEndEvent ? b.endTime : b.time;
|
||||||
|
return -(dateA!.compareTo(dateB!));
|
||||||
|
});
|
||||||
|
sortedDateMap
|
||||||
|
.putIfAbsent(date, () => <LogEvent>[])
|
||||||
|
.addAll(dateMap[date]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,55 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
|
import 'package:diameter/models/basal_profile.dart';
|
||||||
|
import 'package:diameter/models/bolus_profile.dart';
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show LogEventType_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 8362795406595606110)
|
||||||
|
@Sync()
|
||||||
class LogEventType {
|
class LogEventType {
|
||||||
static final Box<LogEventType> box = objectBox.store.box<LogEventType>();
|
static final Box<LogEventType> box = objectBox.store.box<LogEventType>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String value;
|
String value;
|
||||||
bool hasEndTime;
|
bool hasEndTime;
|
||||||
int? defaultReminderDuration;
|
int? defaultReminderDuration;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
|
// constructor
|
||||||
LogEventType({
|
LogEventType({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.value = '',
|
this.value = '',
|
||||||
this.hasEndTime = false,
|
this.hasEndTime = false,
|
||||||
this.defaultReminderDuration,
|
this.defaultReminderDuration,
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// relations
|
||||||
|
final bolusProfile = ToOne<BolusProfile>();
|
||||||
|
final basalProfile = ToOne<BasalProfile>();
|
||||||
|
|
||||||
|
// methods
|
||||||
static LogEventType? get(int id) => box.get(id);
|
static LogEventType? get(int id) => box.get(id);
|
||||||
static List<LogEventType> getAll() => box.getAll();
|
|
||||||
static void put(LogEventType logEventType) => box.put(logEventType);
|
static void put(LogEventType logEventType) => box.put(logEventType);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static List<LogEventType> getAll() {
|
||||||
|
QueryBuilder<LogEventType> builder = box.query(LogEventType_.deleted.equals(false))..order(LogEventType_.value);
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
|
import 'package:diameter/models/log_bolus.dart';
|
||||||
import 'package:diameter/models/log_entry.dart';
|
import 'package:diameter/models/log_entry.dart';
|
||||||
import 'package:diameter/models/meal.dart';
|
import 'package:diameter/models/meal.dart';
|
||||||
import 'package:diameter/models/meal_category.dart';
|
import 'package:diameter/models/meal_category.dart';
|
||||||
@ -6,21 +7,25 @@ import 'package:diameter/models/meal_portion_type.dart';
|
|||||||
import 'package:diameter/models/meal_source.dart';
|
import 'package:diameter/models/meal_source.dart';
|
||||||
import 'package:diameter/models/accuracy.dart';
|
import 'package:diameter/models/accuracy.dart';
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show LogMeal_, LogEntry_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 411177866700467286)
|
||||||
|
@Sync()
|
||||||
class LogMeal {
|
class LogMeal {
|
||||||
static final Box<LogMeal> box = objectBox.store.box<LogMeal>();
|
static final Box<LogMeal> box = objectBox.store.box<LogMeal>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String value;
|
String value;
|
||||||
double? carbsRatio;
|
double? carbsRatio;
|
||||||
double? portionSize;
|
double? portionSize;
|
||||||
double? carbsPerPortion;
|
double? totalCarbs;
|
||||||
double? bolus;
|
|
||||||
int? delayedBolusDuration;
|
|
||||||
double? delayedBolusRate;
|
|
||||||
String? notes;
|
String? notes;
|
||||||
|
double? bolus;
|
||||||
|
double amount;
|
||||||
|
|
||||||
|
// relations
|
||||||
final logEntry = ToOne<LogEntry>();
|
final logEntry = ToOne<LogEntry>();
|
||||||
final meal = ToOne<Meal>();
|
final meal = ToOne<Meal>();
|
||||||
final mealSource = ToOne<MealSource>();
|
final mealSource = ToOne<MealSource>();
|
||||||
@ -29,20 +34,56 @@ class LogMeal {
|
|||||||
final portionSizeAccuracy = ToOne<Accuracy>();
|
final portionSizeAccuracy = ToOne<Accuracy>();
|
||||||
final carbsRatioAccuracy = ToOne<Accuracy>();
|
final carbsRatioAccuracy = ToOne<Accuracy>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
LogMeal({
|
LogMeal({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.value = '',
|
this.value = '',
|
||||||
|
this.amount = 1,
|
||||||
this.carbsRatio,
|
this.carbsRatio,
|
||||||
this.portionSize,
|
this.portionSize,
|
||||||
this.carbsPerPortion,
|
this.totalCarbs,
|
||||||
this.bolus,
|
|
||||||
this.delayedBolusDuration,
|
|
||||||
this.delayedBolusRate,
|
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static LogMeal? get(int id) => box.get(id);
|
static LogMeal? get(int id) => box.get(id);
|
||||||
static List<LogMeal> getAll() => box.getAll();
|
|
||||||
static void put(LogMeal logMeal) => box.put(logMeal);
|
static void put(LogMeal logMeal) => box.put(logMeal);
|
||||||
static void remove(int id) => box.remove(id);
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<LogMeal> getAllForEntry(int id) {
|
||||||
|
QueryBuilder<LogMeal> builder = box.query(LogMeal_.deleted.equals(false));
|
||||||
|
builder.link(LogMeal_.logEntry, LogEntry_.id.equals(id));
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<LogMeal> getRecentWithoutBolus(int id) {
|
||||||
|
final dateTime = LogEntry.get(id)?.time ?? DateTime.now();
|
||||||
|
QueryBuilder<LogMeal> builder = box.query(LogMeal_.deleted.equals(false));
|
||||||
|
builder.link(LogMeal_.logEntry);
|
||||||
|
List<LogMeal> results = builder.build().find();
|
||||||
|
results.retainWhere((logMeal) {
|
||||||
|
final entryTime = logMeal.logEntry.target!.time;
|
||||||
|
return entryTime.isAfter(dateTime.subtract(const Duration(hours: 12))) &&
|
||||||
|
entryTime.isBefore(dateTime.add(const Duration(hours: 12))) && !LogBolus.bolusForMealExists(logMeal.id);
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double getTotalCarbsForEntry(int id) {
|
||||||
|
QueryBuilder<LogMeal> builder = box.query(LogMeal_.deleted.equals(false));
|
||||||
|
builder.link(LogMeal_.logEntry, LogEntry_.id.equals(id));
|
||||||
|
return builder.build().property(LogMeal_.totalCarbs).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,43 +3,66 @@ import 'package:diameter/models/accuracy.dart';
|
|||||||
import 'package:diameter/models/meal_category.dart';
|
import 'package:diameter/models/meal_category.dart';
|
||||||
import 'package:diameter/models/meal_portion_type.dart';
|
import 'package:diameter/models/meal_portion_type.dart';
|
||||||
import 'package:diameter/models/meal_source.dart';
|
import 'package:diameter/models/meal_source.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show Meal_;
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
|
||||||
enum PortionCarbsParameter { carbsRatio, portionSize, carbsPerPortion }
|
enum PortionCarbsParameter { carbsRatio, portionSize, carbsPerPortion }
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 382130101578692012)
|
||||||
|
@Sync()
|
||||||
class Meal {
|
class Meal {
|
||||||
static final Box<Meal> box = objectBox.store.box<Meal>();
|
static final Box<Meal> box = objectBox.store.box<Meal>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String value;
|
String value;
|
||||||
double? carbsRatio;
|
double? carbsRatio;
|
||||||
double? portionSize;
|
double? portionSize;
|
||||||
double? carbsPerPortion;
|
double? carbsPerPortion;
|
||||||
int? delayedBolusDuration;
|
int? delayedBolusDuration;
|
||||||
double? delayedBolusRate;
|
double? delayedBolusPercentage;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
|
// relations
|
||||||
final mealSource = ToOne<MealSource>();
|
final mealSource = ToOne<MealSource>();
|
||||||
final mealCategory = ToOne<MealCategory>();
|
final mealCategory = ToOne<MealCategory>();
|
||||||
final mealPortionType = ToOne<MealPortionType>();
|
final mealPortionType = ToOne<MealPortionType>();
|
||||||
final portionSizeAccuracy = ToOne<Accuracy>();
|
final portionSizeAccuracy = ToOne<Accuracy>();
|
||||||
final carbsRatioAccuracy = ToOne<Accuracy>();
|
final carbsRatioAccuracy = ToOne<Accuracy>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
Meal({
|
Meal({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.value = '',
|
this.value = '',
|
||||||
this.carbsRatio,
|
this.carbsRatio,
|
||||||
this.portionSize,
|
this.portionSize,
|
||||||
this.carbsPerPortion,
|
this.carbsPerPortion,
|
||||||
this.delayedBolusDuration,
|
this.delayedBolusDuration,
|
||||||
this.delayedBolusRate,
|
this.delayedBolusPercentage,
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static Meal? get(int id) => box.get(id);
|
static Meal? get(int id) => box.get(id);
|
||||||
static List<Meal> getAll() => box.getAll();
|
|
||||||
static void put(Meal meal) => box.put(meal);
|
static void put(Meal meal) => box.put(meal);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static List<Meal> getAll() {
|
||||||
|
QueryBuilder<Meal> builder = box.query(Meal_.deleted.equals(false))..order(Meal_.value);
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,45 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show MealCategory_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 3158200688796904913)
|
||||||
|
@Sync()
|
||||||
class MealCategory {
|
class MealCategory {
|
||||||
static final Box<MealCategory> box = objectBox.store.box<MealCategory>();
|
static final Box<MealCategory> box = objectBox.store.box<MealCategory>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String value;
|
String value;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
|
// constructor
|
||||||
MealCategory({
|
MealCategory({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.value = '',
|
this.value = '',
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static MealCategory? get(int id) => box.get(id);
|
static MealCategory? get(int id) => box.get(id);
|
||||||
static List<MealCategory> getAll() => box.getAll();
|
|
||||||
static void put(MealCategory mealCategory) => box.put(mealCategory);
|
static void put(MealCategory mealCategory) => box.put(mealCategory);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static List<MealCategory> getAll() {
|
||||||
|
QueryBuilder<MealCategory> builder = box.query(MealCategory_.deleted.equals(false))..order(MealCategory_.value);
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,45 @@
|
|||||||
import 'package:diameter/main.dart';
|
import 'package:diameter/main.dart';
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show MealPortionType_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 2111511899235985637)
|
||||||
|
@Sync()
|
||||||
class MealPortionType {
|
class MealPortionType {
|
||||||
static final Box<MealPortionType> box = objectBox.store.box<MealPortionType>();
|
static final Box<MealPortionType> box = objectBox.store.box<MealPortionType>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String value;
|
String value;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
|
// constructor
|
||||||
MealPortionType({
|
MealPortionType({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.value = '',
|
this.value = '',
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static MealPortionType? get(int id) => box.get(id);
|
static MealPortionType? get(int id) => box.get(id);
|
||||||
static List<MealPortionType> getAll() => box.getAll();
|
|
||||||
static void put(MealPortionType mealPortionType) => box.put(mealPortionType);
|
static void put(MealPortionType mealPortionType) => box.put(mealPortionType);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static List<MealPortionType> getAll() {
|
||||||
|
QueryBuilder<MealPortionType> builder = box.query(MealPortionType_.deleted.equals(false))..order(MealPortionType_.value);
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,28 +3,52 @@ import 'package:diameter/models/accuracy.dart';
|
|||||||
import 'package:diameter/models/meal_category.dart';
|
import 'package:diameter/models/meal_category.dart';
|
||||||
import 'package:diameter/models/meal_portion_type.dart';
|
import 'package:diameter/models/meal_portion_type.dart';
|
||||||
import 'package:objectbox/objectbox.dart';
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show MealSource_;
|
||||||
|
|
||||||
@Entity()
|
@Entity(uid: 1283034494527412242)
|
||||||
|
@Sync()
|
||||||
class MealSource {
|
class MealSource {
|
||||||
static final Box<MealSource> box = objectBox.store.box<MealSource>();
|
static final Box<MealSource> box = objectBox.store.box<MealSource>();
|
||||||
|
|
||||||
|
// properties
|
||||||
int id;
|
int id;
|
||||||
|
bool deleted;
|
||||||
String value;
|
String value;
|
||||||
String? notes;
|
String? notes;
|
||||||
|
|
||||||
|
// relations
|
||||||
final defaultMealCategory = ToOne<MealCategory>();
|
final defaultMealCategory = ToOne<MealCategory>();
|
||||||
final defaultMealPortionType = ToOne<MealPortionType>();
|
final defaultMealPortionType = ToOne<MealPortionType>();
|
||||||
final defaultCarbsRatioAccuracy = ToOne<Accuracy>();
|
final defaultCarbsRatioAccuracy = ToOne<Accuracy>();
|
||||||
final defaultPortionSizeAccuracy = ToOne<Accuracy>();
|
final defaultPortionSizeAccuracy = ToOne<Accuracy>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
MealSource({
|
MealSource({
|
||||||
this.id = 0,
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
this.value = '',
|
this.value = '',
|
||||||
this.notes,
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
static MealSource? get(int id) => box.get(id);
|
static MealSource? get(int id) => box.get(id);
|
||||||
static List<MealSource> getAll() => box.getAll();
|
|
||||||
static void put(MealSource mealSource) => box.put(mealSource);
|
static void put(MealSource mealSource) => box.put(mealSource);
|
||||||
static void remove(int id) => box.remove(id);
|
|
||||||
|
static List<MealSource> getAll() {
|
||||||
|
QueryBuilder<MealSource> builder = box.query(MealSource_.deleted.equals(false))..order(MealSource_.value);
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
66
lib/models/recipe.dart
Normal file
66
lib/models/recipe.dart
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import 'package:diameter/main.dart';
|
||||||
|
import 'package:diameter/models/ingredient.dart';
|
||||||
|
import 'package:diameter/models/meal.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
import 'package:diameter/objectbox.g.dart' show Recipe_;
|
||||||
|
|
||||||
|
@Entity(uid: 6497942314956341514)
|
||||||
|
@Sync()
|
||||||
|
class Recipe {
|
||||||
|
static final Box<Recipe> box = objectBox.store.box<Recipe>();
|
||||||
|
|
||||||
|
// properties
|
||||||
|
int id;
|
||||||
|
bool deleted;
|
||||||
|
String name;
|
||||||
|
double? servings;
|
||||||
|
String? notes;
|
||||||
|
|
||||||
|
// relations
|
||||||
|
final portion = ToOne<Meal>();
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
Recipe({
|
||||||
|
this.id = 0,
|
||||||
|
this.deleted = false,
|
||||||
|
this.name = '',
|
||||||
|
this.servings,
|
||||||
|
this.notes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
|
static Recipe? get(int id) => box.get(id);
|
||||||
|
static void put(Recipe recipe) => box.put(recipe);
|
||||||
|
|
||||||
|
static List<Recipe> getAll() {
|
||||||
|
QueryBuilder<Recipe> builder = box.query(Recipe_.deleted.equals(false))
|
||||||
|
..order(Recipe_.name);
|
||||||
|
return builder.build().find();
|
||||||
|
}
|
||||||
|
|
||||||
|
static double? getCarbsPerPortion(int id) {
|
||||||
|
final servings = Recipe.get(id)?.servings;
|
||||||
|
final totalWeight = Ingredient.getTotalWeightForRecipe(id);
|
||||||
|
final carbsRatio = Ingredient.getCarbsRatioForRecipe(id);
|
||||||
|
|
||||||
|
if (servings != null && totalWeight != null && carbsRatio != null) {
|
||||||
|
final portionSize = totalWeight / servings;
|
||||||
|
return Utils.calculateCarbs(carbsRatio, portionSize);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove(int id) {
|
||||||
|
final item = box.get(id);
|
||||||
|
if (item != null) {
|
||||||
|
item.deleted = true;
|
||||||
|
box.put(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
130
lib/models/settings.dart
Normal file
130
lib/models/settings.dart
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import 'package:diameter/main.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:objectbox/objectbox.dart';
|
||||||
|
|
||||||
|
enum GlucoseDisplayMode { activeOnly, bothForList, bothForDetail, both }
|
||||||
|
List<String> glucoseDisplayModeLabels = [
|
||||||
|
'activeOnly',
|
||||||
|
'bothForList',
|
||||||
|
'bothForDetail',
|
||||||
|
'both',
|
||||||
|
];
|
||||||
|
|
||||||
|
enum GlucoseMeasurement {
|
||||||
|
mgPerDl,
|
||||||
|
mmolPerL,
|
||||||
|
}
|
||||||
|
List<String> glucoseMeasurementSuffixes = [
|
||||||
|
'mg/dl',
|
||||||
|
'mmol/l',
|
||||||
|
];
|
||||||
|
List<String> glucoseMeasurementLabels = [
|
||||||
|
'mgPerDl',
|
||||||
|
'mmolPerL',
|
||||||
|
];
|
||||||
|
|
||||||
|
enum NutritionMeasurement {
|
||||||
|
grams,
|
||||||
|
ounces,
|
||||||
|
lbs,
|
||||||
|
}
|
||||||
|
List<String> nutritionMeasurementSuffixes = [
|
||||||
|
'g',
|
||||||
|
'oz',
|
||||||
|
'lbs',
|
||||||
|
];
|
||||||
|
List<String> nutritionMeasurementLabels = [
|
||||||
|
'grams',
|
||||||
|
'ounces',
|
||||||
|
'lbs',
|
||||||
|
];
|
||||||
|
|
||||||
|
@Entity(uid: 3989341091218179227)
|
||||||
|
@Sync()
|
||||||
|
class Settings {
|
||||||
|
static final Box<Settings> box = objectBox.store.box<Settings>();
|
||||||
|
|
||||||
|
// properties
|
||||||
|
int id;
|
||||||
|
|
||||||
|
int nutritionMeasurementIndex;
|
||||||
|
int glucoseDisplayModeIndex;
|
||||||
|
int glucoseMeasurementIndex;
|
||||||
|
int targetGlucoseMgPerDl;
|
||||||
|
double targetGlucoseMmolPerL;
|
||||||
|
|
||||||
|
double insulinIncrements;
|
||||||
|
double nutritionIncrements;
|
||||||
|
double mmolPerLIncrements;
|
||||||
|
double amountIncrements;
|
||||||
|
|
||||||
|
String dateFormat;
|
||||||
|
String? longDateFormat;
|
||||||
|
String timeFormat;
|
||||||
|
String? longTimeFormat;
|
||||||
|
|
||||||
|
bool showConfirmationDialogOnCancel;
|
||||||
|
bool showConfirmationDialogOnDelete;
|
||||||
|
bool showConfirmationDialogOnStopEvent;
|
||||||
|
|
||||||
|
bool useDarkTheme;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
Settings({
|
||||||
|
this.id = 0,
|
||||||
|
this.nutritionMeasurementIndex = 0,
|
||||||
|
this.glucoseDisplayModeIndex = 0,
|
||||||
|
this.glucoseMeasurementIndex = 0,
|
||||||
|
this.insulinIncrements = 0.05,
|
||||||
|
this.nutritionIncrements = 0.01,
|
||||||
|
this.mmolPerLIncrements = 0.1,
|
||||||
|
this.amountIncrements = 0.05,
|
||||||
|
this.dateFormat = 'MM/dd/yy',
|
||||||
|
this.longDateFormat = 'MMMM dd, yyyy',
|
||||||
|
this.timeFormat = 'HH:mm',
|
||||||
|
this.longTimeFormat = 'HH:mm:ss',
|
||||||
|
this.showConfirmationDialogOnCancel = true,
|
||||||
|
this.showConfirmationDialogOnDelete = true,
|
||||||
|
this.showConfirmationDialogOnStopEvent = true,
|
||||||
|
this.targetGlucoseMgPerDl = 100,
|
||||||
|
this.targetGlucoseMmolPerL = 5.5,
|
||||||
|
this.useDarkTheme = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// methods
|
||||||
|
static Settings get() {
|
||||||
|
if (box.getAll().length != 1) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
return box.getAll().single;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NutritionMeasurement get nutritionMeasurement =>
|
||||||
|
NutritionMeasurement.values[get().nutritionMeasurementIndex];
|
||||||
|
static GlucoseMeasurement get glucoseMeasurement =>
|
||||||
|
GlucoseMeasurement.values[get().glucoseMeasurementIndex];
|
||||||
|
static GlucoseDisplayMode get glucoseDisplayMode =>
|
||||||
|
GlucoseDisplayMode.values[get().glucoseDisplayModeIndex];
|
||||||
|
|
||||||
|
static String get nutritionMeasurementSuffix =>
|
||||||
|
nutritionMeasurementSuffixes[get().nutritionMeasurementIndex];
|
||||||
|
static String get glucoseMeasurementSuffix =>
|
||||||
|
glucoseMeasurementSuffixes[get().glucoseMeasurementIndex];
|
||||||
|
|
||||||
|
static int get targetMgPerDl => get().targetGlucoseMgPerDl;
|
||||||
|
static double get targetMmolPerL => get().targetGlucoseMmolPerL;
|
||||||
|
|
||||||
|
static double get insulinSteps => get().insulinIncrements;
|
||||||
|
static double get nutritionSteps => get().nutritionIncrements;
|
||||||
|
static double get mmolPerLSteps => get().mmolPerLIncrements;
|
||||||
|
|
||||||
|
static ThemeMode get themeMode =>
|
||||||
|
get().useDarkTheme ? ThemeMode.dark : ThemeMode.light;
|
||||||
|
|
||||||
|
static void put(Settings settings) => box.put(settings);
|
||||||
|
|
||||||
|
static void reset() {
|
||||||
|
box.removeAll();
|
||||||
|
box.put(Settings(useDarkTheme: ThemeMode.system == ThemeMode.dark));
|
||||||
|
}
|
||||||
|
}
|
@ -2,16 +2,17 @@ import 'package:diameter/screens/accuracy_detail.dart';
|
|||||||
import 'package:diameter/screens/accuracy_list.dart';
|
import 'package:diameter/screens/accuracy_list.dart';
|
||||||
import 'package:diameter/screens/basal/basal_detail.dart';
|
import 'package:diameter/screens/basal/basal_detail.dart';
|
||||||
import 'package:diameter/screens/basal/basal_profile_detail.dart';
|
import 'package:diameter/screens/basal/basal_profile_detail.dart';
|
||||||
import 'package:diameter/screens/basal/basal_profiles_list.dart';
|
import 'package:diameter/screens/basal/basal_profile_list.dart';
|
||||||
import 'package:diameter/screens/bolus/bolus_detail.dart';
|
import 'package:diameter/screens/bolus/bolus_detail.dart';
|
||||||
import 'package:diameter/screens/bolus/bolus_profile_detail.dart';
|
import 'package:diameter/screens/bolus/bolus_profile_detail.dart';
|
||||||
import 'package:diameter/screens/bolus/bolus_profile_list.dart';
|
import 'package:diameter/screens/bolus/bolus_profile_list.dart';
|
||||||
import 'package:diameter/screens/log/log.dart';
|
import 'package:diameter/screens/log/log.dart';
|
||||||
import 'package:diameter/screens/log/log_entry.dart';
|
import 'package:diameter/screens/log/log_entry/log_entry.dart';
|
||||||
import 'package:diameter/screens/log/log_event_detail.dart';
|
import 'package:diameter/screens/log/log_event/log_event_detail.dart';
|
||||||
import 'package:diameter/screens/log/log_event_type_detail.dart';
|
import 'package:diameter/screens/log/log_event/log_event_list.dart';
|
||||||
import 'package:diameter/screens/log/log_event_type_list.dart';
|
import 'package:diameter/screens/log/log_event/log_event_type_detail.dart';
|
||||||
import 'package:diameter/screens/log/log_meal_detail.dart';
|
import 'package:diameter/screens/log/log_event/log_event_type_list.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_meal_detail.dart';
|
||||||
import 'package:diameter/screens/meal/meal_category_detail.dart';
|
import 'package:diameter/screens/meal/meal_category_detail.dart';
|
||||||
import 'package:diameter/screens/meal/meal_category_list.dart';
|
import 'package:diameter/screens/meal/meal_category_list.dart';
|
||||||
import 'package:diameter/screens/meal/meal_detail.dart';
|
import 'package:diameter/screens/meal/meal_detail.dart';
|
||||||
@ -20,6 +21,8 @@ import 'package:diameter/screens/meal/meal_portion_type_detail.dart';
|
|||||||
import 'package:diameter/screens/meal/meal_portion_type_list.dart';
|
import 'package:diameter/screens/meal/meal_portion_type_list.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_list.dart';
|
import 'package:diameter/screens/meal/meal_source_list.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_detail.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_list.dart';
|
||||||
import 'package:diameter/settings.dart';
|
import 'package:diameter/settings.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -39,9 +42,14 @@ class Routes {
|
|||||||
static const String logEvent = LogEventDetailScreen.routeName;
|
static const String logEvent = LogEventDetailScreen.routeName;
|
||||||
static const String logMeal = LogMealDetailScreen.routeName;
|
static const String logMeal = LogMealDetailScreen.routeName;
|
||||||
static const List<String> logEntryRoutes = [logEntry, logEvent, logMeal];
|
static const List<String> logEntryRoutes = [logEntry, logEvent, logMeal];
|
||||||
static const String logEventType = LogEventTypeDetailScreen.routeName;
|
static const String logEventType = EventTypeDetailScreen.routeName;
|
||||||
static const String logEventTypes = LogEventTypeListScreen.routeName;
|
static const String logEventTypes = LogEventTypeListScreen.routeName;
|
||||||
static const List<String> logEventTypeRoutes = [logEventType, logEventTypes];
|
static const List<String> logEventTypeRoutes = [logEventType, logEventTypes];
|
||||||
|
static const String events = LogEventListScreen.routeName;
|
||||||
|
|
||||||
|
static const String recipe = RecipeDetailScreen.routeName;
|
||||||
|
static const String recipes = RecipeListScreen.routeName;
|
||||||
|
static const List<String> recipeRoutes = [recipe, recipes];
|
||||||
|
|
||||||
static const String meal = MealDetailScreen.routeName;
|
static const String meal = MealDetailScreen.routeName;
|
||||||
static const String meals = MealListScreen.routeName;
|
static const String meals = MealListScreen.routeName;
|
||||||
@ -98,16 +106,24 @@ class _NavigationState extends State<Navigation> {
|
|||||||
selected: widget.currentLocation == Routes.log,
|
selected: widget.currentLocation == Routes.log,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Log Entry'),
|
title: const Text('Log Events'),
|
||||||
leading: const Icon(Icons.description),
|
leading: const Icon(Icons.event),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
selectDestination(Routes.logEntry);
|
selectDestination(Routes.events);
|
||||||
},
|
},
|
||||||
selected: Routes.logEntryRoutes.contains(widget.currentLocation),
|
selected: widget.currentLocation == Routes.events,
|
||||||
),
|
),
|
||||||
|
// ListTile(
|
||||||
|
// title: const Text('Recipes'),
|
||||||
|
// leading: const Icon(Icons.local_dining),
|
||||||
|
// onTap: () {
|
||||||
|
// selectDestination(Routes.recipes);
|
||||||
|
// },
|
||||||
|
// selected: Routes.recipeRoutes.contains(widget.currentLocation),
|
||||||
|
// ),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Meals'),
|
title: const Text('Meals'),
|
||||||
leading: const Icon(Icons.restaurant),
|
leading: const Icon(Icons.dinner_dining),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
selectDestination(Routes.meals);
|
selectDestination(Routes.meals);
|
||||||
},
|
},
|
||||||
|
@ -3,49 +3,11 @@
|
|||||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
|
||||||
"id": "1:3095978685310268382",
|
|
||||||
"lastPropertyId": "6:5471636804765937328",
|
|
||||||
"name": "Accuracy",
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"id": "1:3455702077061719523",
|
|
||||||
"name": "id",
|
|
||||||
"type": 6,
|
|
||||||
"flags": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2:1048198814030724077",
|
|
||||||
"name": "value",
|
|
||||||
"type": 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3:9003780003858349085",
|
|
||||||
"name": "forCarbsRatio",
|
|
||||||
"type": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4:5421422436108145565",
|
|
||||||
"name": "forPortionSize",
|
|
||||||
"type": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5:7741631874181070179",
|
|
||||||
"name": "confidenceRating",
|
|
||||||
"type": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "6:5471636804765937328",
|
|
||||||
"name": "notes",
|
|
||||||
"type": 9
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relations": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "2:1467758525778521891",
|
"id": "2:1467758525778521891",
|
||||||
"lastPropertyId": "5:3908367275335317130",
|
"lastPropertyId": "6:3409466778841164684",
|
||||||
"name": "Basal",
|
"name": "Basal",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:4281816825522738642",
|
"id": "1:4281816825522738642",
|
||||||
@ -75,14 +37,20 @@
|
|||||||
"flags": 520,
|
"flags": 520,
|
||||||
"indexId": "1:8279975749291974737",
|
"indexId": "1:8279975749291974737",
|
||||||
"relationTarget": "BasalProfile"
|
"relationTarget": "BasalProfile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6:3409466778841164684",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "3:3613736032926903785",
|
"id": "3:3613736032926903785",
|
||||||
"lastPropertyId": "4:6719547342639071472",
|
"lastPropertyId": "5:8140071977687660397",
|
||||||
"name": "BasalProfile",
|
"name": "BasalProfile",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:353771983641472117",
|
"id": "1:353771983641472117",
|
||||||
@ -104,14 +72,20 @@
|
|||||||
"id": "4:6719547342639071472",
|
"id": "4:6719547342639071472",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
"type": 9
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5:8140071977687660397",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "4:3417770529060202389",
|
"id": "4:3417770529060202389",
|
||||||
"lastPropertyId": "8:7679622918986671917",
|
"lastPropertyId": "9:7440090146687096977",
|
||||||
"name": "Bolus",
|
"name": "Bolus",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:8141647919190345775",
|
"id": "1:8141647919190345775",
|
||||||
@ -156,14 +130,20 @@
|
|||||||
"flags": 520,
|
"flags": 520,
|
||||||
"indexId": "2:1936045997906240691",
|
"indexId": "2:1936045997906240691",
|
||||||
"relationTarget": "BolusProfile"
|
"relationTarget": "BolusProfile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9:7440090146687096977",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5:8812452529027052317",
|
"id": "5:8812452529027052317",
|
||||||
"lastPropertyId": "4:3030493484602726372",
|
"lastPropertyId": "5:8082994824481464395",
|
||||||
"name": "BolusProfile",
|
"name": "BolusProfile",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:4233863196673391978",
|
"id": "1:4233863196673391978",
|
||||||
@ -185,14 +165,20 @@
|
|||||||
"id": "4:3030493484602726372",
|
"id": "4:3030493484602726372",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
"type": 9
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5:8082994824481464395",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6:752131069307970560",
|
"id": "6:752131069307970560",
|
||||||
"lastPropertyId": "8:6492273995038150006",
|
"lastPropertyId": "10:2505303363495348118",
|
||||||
"name": "LogEntry",
|
"name": "LogEntry",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:5528657304180237933",
|
"id": "1:5528657304180237933",
|
||||||
@ -215,33 +201,29 @@
|
|||||||
"name": "mmolPerL",
|
"name": "mmolPerL",
|
||||||
"type": 8
|
"type": 8
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "5:3678829169126156351",
|
|
||||||
"name": "bolusGlucose",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "6:1568597071506264632",
|
|
||||||
"name": "delayedBolusDuration",
|
|
||||||
"type": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7:8795268969829293398",
|
|
||||||
"name": "delayedBolusRate",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "8:6492273995038150006",
|
"id": "8:6492273995038150006",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
"type": 9
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9:1692732373071965573",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10:2505303363495348118",
|
||||||
|
"name": "glucoseTrend",
|
||||||
|
"type": 8
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7:4303325892753185970",
|
"id": "7:4303325892753185970",
|
||||||
"lastPropertyId": "8:2514297323717317184",
|
"lastPropertyId": "12:3041952167628926163",
|
||||||
"name": "LogEvent",
|
"name": "LogEvent",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:6648501734758557663",
|
"id": "1:6648501734758557663",
|
||||||
@ -269,22 +251,6 @@
|
|||||||
"name": "notes",
|
"name": "notes",
|
||||||
"type": 9
|
"type": 9
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "6:7838546213550447420",
|
|
||||||
"name": "logEntryId",
|
|
||||||
"type": 11,
|
|
||||||
"flags": 520,
|
|
||||||
"indexId": "3:3670661188280692002",
|
|
||||||
"relationTarget": "LogEntry"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7:8031421171668506924",
|
|
||||||
"name": "endLogEntryId",
|
|
||||||
"type": 11,
|
|
||||||
"flags": 520,
|
|
||||||
"indexId": "4:7379712902406481832",
|
|
||||||
"relationTarget": "LogEntry"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "8:2514297323717317184",
|
"id": "8:2514297323717317184",
|
||||||
"name": "eventTypeId",
|
"name": "eventTypeId",
|
||||||
@ -292,14 +258,41 @@
|
|||||||
"flags": 520,
|
"flags": 520,
|
||||||
"indexId": "5:1417691902662024007",
|
"indexId": "5:1417691902662024007",
|
||||||
"relationTarget": "LogEventType"
|
"relationTarget": "LogEventType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9:8477413048577624801",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10:987218091728524211",
|
||||||
|
"name": "bolusProfileId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "25:2500612771974500993",
|
||||||
|
"relationTarget": "BolusProfile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "11:2013538196800336796",
|
||||||
|
"name": "basalProfileId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "26:4562998391990896273",
|
||||||
|
"relationTarget": "BasalProfile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12:3041952167628926163",
|
||||||
|
"name": "reminderDuration",
|
||||||
|
"type": 6
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "8:8362795406595606110",
|
"id": "8:8362795406595606110",
|
||||||
"lastPropertyId": "5:7361377572496986196",
|
"lastPropertyId": "8:1869014400856897151",
|
||||||
"name": "LogEventType",
|
"name": "LogEventType",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:1430413826199774000",
|
"id": "1:1430413826199774000",
|
||||||
@ -326,14 +319,36 @@
|
|||||||
"id": "5:7361377572496986196",
|
"id": "5:7361377572496986196",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
"type": 9
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6:5428344494256722438",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7:9194648252717310397",
|
||||||
|
"name": "bolusProfileId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "27:758221514459743282",
|
||||||
|
"relationTarget": "BolusProfile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8:1869014400856897151",
|
||||||
|
"name": "basalProfileId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "28:4563029809754152081",
|
||||||
|
"relationTarget": "BasalProfile"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9:411177866700467286",
|
"id": "9:411177866700467286",
|
||||||
"lastPropertyId": "16:7121997990741934484",
|
"lastPropertyId": "19:8965198821438347033",
|
||||||
"name": "LogMeal",
|
"name": "LogMeal",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:962999525294133158",
|
"id": "1:962999525294133158",
|
||||||
@ -356,26 +371,11 @@
|
|||||||
"name": "portionSize",
|
"name": "portionSize",
|
||||||
"type": 8
|
"type": 8
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "5:2215708755581938580",
|
|
||||||
"name": "carbsPerPortion",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "6:8074052538574863399",
|
"id": "6:8074052538574863399",
|
||||||
"name": "bolus",
|
"name": "bolus",
|
||||||
"type": 8
|
"type": 8
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "7:3247926313599127440",
|
|
||||||
"name": "delayedBolusDuration",
|
|
||||||
"type": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "8:8789440370359282572",
|
|
||||||
"name": "delayedBolusRate",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "9:1920579694098037947",
|
"id": "9:1920579694098037947",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
@ -436,14 +436,30 @@
|
|||||||
"flags": 520,
|
"flags": 520,
|
||||||
"indexId": "12:35287836658362611",
|
"indexId": "12:35287836658362611",
|
||||||
"relationTarget": "Accuracy"
|
"relationTarget": "Accuracy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "17:7341439841011629937",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "18:7405129785654054238",
|
||||||
|
"name": "amount",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "19:8965198821438347033",
|
||||||
|
"name": "totalCarbs",
|
||||||
|
"type": 8
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "10:382130101578692012",
|
"id": "10:382130101578692012",
|
||||||
"lastPropertyId": "13:4890778480468380841",
|
"lastPropertyId": "15:8283810711091063880",
|
||||||
"name": "Meal",
|
"name": "Meal",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:612386612600420389",
|
"id": "1:612386612600420389",
|
||||||
@ -476,11 +492,6 @@
|
|||||||
"name": "delayedBolusDuration",
|
"name": "delayedBolusDuration",
|
||||||
"type": 6
|
"type": 6
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "7:2172890064639236018",
|
|
||||||
"name": "delayedBolusRate",
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "8:6111684052388229887",
|
"id": "8:6111684052388229887",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
@ -525,14 +536,25 @@
|
|||||||
"flags": 520,
|
"flags": 520,
|
||||||
"indexId": "17:9108886538013386415",
|
"indexId": "17:9108886538013386415",
|
||||||
"relationTarget": "Accuracy"
|
"relationTarget": "Accuracy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "14:3567196286623536415",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "15:8283810711091063880",
|
||||||
|
"name": "delayedBolusPercentage",
|
||||||
|
"type": 8
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "11:3158200688796904913",
|
"id": "11:3158200688796904913",
|
||||||
"lastPropertyId": "3:3543757971350345683",
|
"lastPropertyId": "4:824435977543069541",
|
||||||
"name": "MealCategory",
|
"name": "MealCategory",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:3678943122076184840",
|
"id": "1:3678943122076184840",
|
||||||
@ -549,14 +571,20 @@
|
|||||||
"id": "3:3543757971350345683",
|
"id": "3:3543757971350345683",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
"type": 9
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4:824435977543069541",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "12:2111511899235985637",
|
"id": "12:2111511899235985637",
|
||||||
"lastPropertyId": "3:1950852666001613408",
|
"lastPropertyId": "4:5680236937391945907",
|
||||||
"name": "MealPortionType",
|
"name": "MealPortionType",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:65428405312238271",
|
"id": "1:65428405312238271",
|
||||||
@ -573,14 +601,20 @@
|
|||||||
"id": "3:1950852666001613408",
|
"id": "3:1950852666001613408",
|
||||||
"name": "notes",
|
"name": "notes",
|
||||||
"type": 9
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4:5680236937391945907",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "13:1283034494527412242",
|
"id": "13:1283034494527412242",
|
||||||
"lastPropertyId": "7:5852853174931678667",
|
"lastPropertyId": "8:4547899751779962180",
|
||||||
"name": "MealSource",
|
"name": "MealSource",
|
||||||
|
"flags": 2,
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "1:7205380295259922130",
|
"id": "1:7205380295259922130",
|
||||||
@ -629,20 +663,415 @@
|
|||||||
"flags": 520,
|
"flags": 520,
|
||||||
"indexId": "21:1931330716440762729",
|
"indexId": "21:1931330716440762729",
|
||||||
"relationTarget": "Accuracy"
|
"relationTarget": "Accuracy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8:4547899751779962180",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "14:8033487006694871160",
|
||||||
|
"lastPropertyId": "18:7503231998671134983",
|
||||||
|
"name": "LogBolus",
|
||||||
|
"flags": 2,
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "1:8254237730262024662",
|
||||||
|
"name": "id",
|
||||||
|
"type": 6,
|
||||||
|
"flags": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2:7669701519569266656",
|
||||||
|
"name": "units",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3:1967840431906109999",
|
||||||
|
"name": "carbs",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4:5520321978435312625",
|
||||||
|
"name": "delay",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7:3065420032567707091",
|
||||||
|
"name": "setManually",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8:2967613978873295525",
|
||||||
|
"name": "notes",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9:5454965717985089938",
|
||||||
|
"name": "logEntryId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "22:5852072074740543047",
|
||||||
|
"relationTarget": "LogEntry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10:4105009806564072037",
|
||||||
|
"name": "rateId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "23:1594553054621930876",
|
||||||
|
"relationTarget": "Bolus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "11:4818762109001810295",
|
||||||
|
"name": "mealId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "24:4224983816051843140",
|
||||||
|
"relationTarget": "LogMeal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12:4765038304548427459",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "13:2530431967957143684",
|
||||||
|
"name": "mgPerDlCurrent",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "14:5210229118898251877",
|
||||||
|
"name": "mgPerDlTarget",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "15:657840864788362204",
|
||||||
|
"name": "mgPerDlCorrection",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "16:3999403624434995450",
|
||||||
|
"name": "mmolPerLCurrent",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "17:2852253735546692099",
|
||||||
|
"name": "mmolPerLTarget",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "18:7503231998671134983",
|
||||||
|
"name": "mmolPerLCorrection",
|
||||||
|
"type": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "15:291512798403320400",
|
||||||
|
"lastPropertyId": "7:6675647182186603076",
|
||||||
|
"name": "Accuracy",
|
||||||
|
"flags": 2,
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "1:8405388350474524599",
|
||||||
|
"name": "id",
|
||||||
|
"type": 6,
|
||||||
|
"flags": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2:1919049381880760479",
|
||||||
|
"name": "value",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3:7181081526218678274",
|
||||||
|
"name": "forCarbsRatio",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4:3576006369067328383",
|
||||||
|
"name": "forPortionSize",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5:7027546512578846894",
|
||||||
|
"name": "confidenceRating",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6:6625101003527710274",
|
||||||
|
"name": "notes",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7:6675647182186603076",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "16:3989341091218179227",
|
||||||
|
"lastPropertyId": "23:3611447442844013652",
|
||||||
|
"name": "Settings",
|
||||||
|
"flags": 2,
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "1:7803753645747063723",
|
||||||
|
"name": "id",
|
||||||
|
"type": 6,
|
||||||
|
"flags": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2:4703380985530623101",
|
||||||
|
"name": "dateFormat",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3:2983395924801005937",
|
||||||
|
"name": "longDateFormat",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4:2579032794029389590",
|
||||||
|
"name": "timeFormat",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5:3970690908108519507",
|
||||||
|
"name": "longTimeFormat",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6:349893175332801783",
|
||||||
|
"name": "showConfirmationDialogOnCancel",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7:4049915860178079910",
|
||||||
|
"name": "showConfirmationDialogOnDelete",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8:3088241443557186512",
|
||||||
|
"name": "showConfirmationDialogOnStopEvent",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "18:1203593429961092769",
|
||||||
|
"name": "nutritionMeasurementIndex",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "19:8895176254912253797",
|
||||||
|
"name": "glucoseDisplayModeIndex",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "20:6560414475711071975",
|
||||||
|
"name": "glucoseMeasurementIndex",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "21:7934134105044248002",
|
||||||
|
"name": "targetGlucoseMgPerDl",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "22:3595473653451456068",
|
||||||
|
"name": "targetGlucoseMmolPerL",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "23:3611447442844013652",
|
||||||
|
"name": "useDarkTheme",
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "17:5041265995704044399",
|
||||||
|
"lastPropertyId": "7:1333487551279074696",
|
||||||
|
"name": "GlucoseTarget",
|
||||||
|
"flags": 2,
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "1:4322960567133959537",
|
||||||
|
"name": "id",
|
||||||
|
"type": 6,
|
||||||
|
"flags": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2:7533461804561299987",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3:4949963248761074916",
|
||||||
|
"name": "fromMgPerDL",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4:8685380695305799464",
|
||||||
|
"name": "toMgPerDl",
|
||||||
|
"type": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5:2925449628924807050",
|
||||||
|
"name": "fromMmolPerL",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6:3244873743284485064",
|
||||||
|
"name": "toMmolPerL",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7:1333487551279074696",
|
||||||
|
"name": "color",
|
||||||
|
"type": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "18:6497942314956341514",
|
||||||
|
"lastPropertyId": "11:8488657312300528492",
|
||||||
|
"name": "Recipe",
|
||||||
|
"flags": 2,
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "1:6426741154282018946",
|
||||||
|
"name": "id",
|
||||||
|
"type": 6,
|
||||||
|
"flags": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2:1167304402395485629",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3:1244733840071626966",
|
||||||
|
"name": "name",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9:8593446427752839266",
|
||||||
|
"name": "notes",
|
||||||
|
"type": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10:4370359747396560337",
|
||||||
|
"name": "portionId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "29:5110151182694376118",
|
||||||
|
"relationTarget": "Meal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "11:8488657312300528492",
|
||||||
|
"name": "servings",
|
||||||
|
"type": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relations": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "19:6950311793136068892",
|
||||||
|
"lastPropertyId": "5:6495065881132428893",
|
||||||
|
"name": "Ingredient",
|
||||||
|
"flags": 2,
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "1:7766569281758551418",
|
||||||
|
"name": "id",
|
||||||
|
"type": 6,
|
||||||
|
"flags": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2:3830559702655088692",
|
||||||
|
"name": "deleted",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3:602057803225843875",
|
||||||
|
"name": "amount",
|
||||||
|
"type": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4:26686399245586953",
|
||||||
|
"name": "recipeId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "30:5492781242713788590",
|
||||||
|
"relationTarget": "Recipe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5:6495065881132428893",
|
||||||
|
"name": "ingredientId",
|
||||||
|
"type": 11,
|
||||||
|
"flags": 520,
|
||||||
|
"indexId": "31:3277019237664417023",
|
||||||
|
"relationTarget": "Meal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"relations": []
|
"relations": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lastEntityId": "13:1283034494527412242",
|
"lastEntityId": "19:6950311793136068892",
|
||||||
"lastIndexId": "21:1931330716440762729",
|
"lastIndexId": "31:3277019237664417023",
|
||||||
"lastRelationId": "0:0",
|
"lastRelationId": "0:0",
|
||||||
"lastSequenceId": "0:0",
|
"lastSequenceId": "0:0",
|
||||||
"modelVersion": 5,
|
"modelVersion": 5,
|
||||||
"modelVersionParserMinimum": 5,
|
"modelVersionParserMinimum": 5,
|
||||||
"retiredEntityUids": [],
|
"retiredEntityUids": [
|
||||||
"retiredIndexUids": [],
|
3095978685310268382
|
||||||
"retiredPropertyUids": [],
|
],
|
||||||
|
"retiredIndexUids": [
|
||||||
|
3670661188280692002,
|
||||||
|
7379712902406481832
|
||||||
|
],
|
||||||
|
"retiredPropertyUids": [
|
||||||
|
3455702077061719523,
|
||||||
|
1048198814030724077,
|
||||||
|
9003780003858349085,
|
||||||
|
5421422436108145565,
|
||||||
|
7741631874181070179,
|
||||||
|
5471636804765937328,
|
||||||
|
6855574218883169324,
|
||||||
|
5313708456544000157,
|
||||||
|
3678829169126156351,
|
||||||
|
1568597071506264632,
|
||||||
|
8795268969829293398,
|
||||||
|
3247926313599127440,
|
||||||
|
8789440370359282572,
|
||||||
|
7838546213550447420,
|
||||||
|
8031421171668506924,
|
||||||
|
1614362036318874174,
|
||||||
|
1675040259141389754,
|
||||||
|
7518219134349037920,
|
||||||
|
2172890064639236018,
|
||||||
|
310032577683835406,
|
||||||
|
5588897884422150510,
|
||||||
|
7638848982383620744,
|
||||||
|
3282706593658092097,
|
||||||
|
596980591281311896,
|
||||||
|
3633551763915044903,
|
||||||
|
2215708755581938580,
|
||||||
|
241621230513128588,
|
||||||
|
4678123663117222609,
|
||||||
|
780211923138281722,
|
||||||
|
763575433624979013,
|
||||||
|
1225271130099322691
|
||||||
|
],
|
||||||
"retiredRelationUids": [],
|
"retiredRelationUids": [],
|
||||||
"version": 1
|
"version": 1
|
||||||
}
|
}
|
1724
lib/objectbox.g.dart
1724
lib/objectbox.g.dart
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,11 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
import 'package:diameter/components/detail.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/components/forms/boolean_form_field.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/components/forms/number_form_field.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
import 'package:diameter/models/accuracy.dart';
|
import 'package:diameter/models/accuracy.dart';
|
||||||
|
|
||||||
class AccuracyDetailScreen extends StatefulWidget {
|
class AccuracyDetailScreen extends StatefulWidget {
|
||||||
@ -22,12 +24,14 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
|
|||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
|
|
||||||
final GlobalKey<FormState> _accuracyForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _accuracyForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
final _valueController = TextEditingController(text: '');
|
final _valueController = TextEditingController(text: '');
|
||||||
final _confidenceRatingController = TextEditingController(text: '');
|
|
||||||
final _notesController = TextEditingController(text: '');
|
final _notesController = TextEditingController(text: '');
|
||||||
bool _forCarbsRatio = false;
|
final _confidenceRatingController =
|
||||||
bool _forPortionSize = false;
|
TextEditingController(text: Accuracy.getAll().length.toString());
|
||||||
|
bool _forCarbsRatio = true;
|
||||||
|
bool _forPortionSize = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -38,18 +42,39 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
|
|||||||
_forCarbsRatio = _accuracy!.forCarbsRatio;
|
_forCarbsRatio = _accuracy!.forCarbsRatio;
|
||||||
_forPortionSize = _accuracy!.forPortionSize;
|
_forPortionSize = _accuracy!.forPortionSize;
|
||||||
_confidenceRatingController.text =
|
_confidenceRatingController.text =
|
||||||
(_accuracy!.confidenceRating ?? '').toString();
|
(_accuracy!.confidenceRating ?? Accuracy.getAll().length).toString();
|
||||||
_notesController.text = _accuracy!.notes ?? '';
|
_notesController.text = _accuracy!.notes ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void reload() {
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_valueController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
_confidenceRatingController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
if (widget.id != 0) {
|
if (widget.id != 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_accuracy = Accuracy.get(widget.id);
|
_accuracy = Accuracy.get(widget.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_isNew = _accuracy == null;
|
_isNew = _accuracy == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleSaveAction() async {
|
void handleSaveAction() async {
|
||||||
@ -57,15 +82,18 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
|
|||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
});
|
});
|
||||||
if (_accuracyForm.currentState!.validate()) {
|
if (_accuracyForm.currentState!.validate()) {
|
||||||
Accuracy.box.put(Accuracy(
|
Accuracy accuracy = Accuracy(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
value: _valueController.text,
|
value: _valueController.text,
|
||||||
forCarbsRatio: _forCarbsRatio,
|
forCarbsRatio: _forCarbsRatio,
|
||||||
forPortionSize: _forPortionSize,
|
forPortionSize: _forPortionSize,
|
||||||
confidenceRating: int.tryParse(_confidenceRatingController.text),
|
|
||||||
notes: _notesController.text,
|
notes: _notesController.text,
|
||||||
));
|
);
|
||||||
Navigator.pop(context, '${_isNew ? 'New' : ''} Accuracy saved');
|
Accuracy.put(accuracy);
|
||||||
|
Accuracy.reorder(
|
||||||
|
accuracy, int.tryParse(_confidenceRatingController.text));
|
||||||
|
Navigator.pop(
|
||||||
|
context, ['${_isNew ? 'New' : ''} Accuracy saved', accuracy]);
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = false;
|
_isSaving = false;
|
||||||
@ -73,21 +101,22 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleCancelAction() {
|
void handleCancelAction() {
|
||||||
if (showConfirmationDialogOnCancel &&
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
(_isNew &&
|
(_isNew &&
|
||||||
(_forCarbsRatio ||
|
(!_forCarbsRatio ||
|
||||||
_forPortionSize ||
|
!_forPortionSize ||
|
||||||
_valueController.text != '' ||
|
_valueController.text != '' ||
|
||||||
int.tryParse(_confidenceRatingController.text) != null ||
|
int.tryParse(_confidenceRatingController.text) !=
|
||||||
_notesController.text != '')) ||
|
Accuracy.getAll().length ||
|
||||||
(!_isNew &&
|
_notesController.text != '')) ||
|
||||||
(_forCarbsRatio != _accuracy!.forCarbsRatio ||
|
(!_isNew &&
|
||||||
_forPortionSize != _accuracy!.forPortionSize ||
|
(_forCarbsRatio != _accuracy!.forCarbsRatio ||
|
||||||
_accuracy!.value != _valueController.text ||
|
_forPortionSize != _accuracy!.forPortionSize ||
|
||||||
int.tryParse(_confidenceRatingController.text) !=
|
_accuracy!.value != _valueController.text ||
|
||||||
_accuracy!.confidenceRating ||
|
int.tryParse(_confidenceRatingController.text) !=
|
||||||
(_accuracy!.notes ?? '') != _notesController.text))) {
|
_accuracy!.confidenceRating ||
|
||||||
Dialogs.showCancelConfirmationDialog(
|
(_accuracy!.notes ?? '') != _notesController.text))) {
|
||||||
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
isNew: _isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
@ -104,66 +133,74 @@ class _AccuracyDetailScreenState extends State<AccuracyDetailScreen> {
|
|||||||
title: Text(_isNew ? 'New Accuracy' : _accuracy!.value),
|
title: Text(_isNew ? 'New Accuracy' : _accuracy!.value),
|
||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: AccuracyDetailScreen.routeName),
|
drawer: const Navigation(currentLocation: AccuracyDetailScreen.routeName),
|
||||||
body: SingleChildScrollView(
|
body: Scrollbar(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: SingleChildScrollView(
|
||||||
children: <Widget>[
|
controller: _scrollController,
|
||||||
StyledForm(
|
child: Column(
|
||||||
formState: _accuracyForm,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
fields: [
|
children: <Widget>[
|
||||||
TextFormField(
|
FormWrapper(
|
||||||
controller: _valueController,
|
formState: _accuracyForm,
|
||||||
decoration: const InputDecoration(
|
fields: [
|
||||||
labelText: 'Name',
|
TextFormField(
|
||||||
|
controller: _valueController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
validator: (value) {
|
BooleanFormField(
|
||||||
if (value!.trim().isEmpty) {
|
value: _forCarbsRatio,
|
||||||
return 'Empty name';
|
label: 'for carbs ratio',
|
||||||
}
|
onChanged: (value) {
|
||||||
return null;
|
setState(() {
|
||||||
},
|
_forCarbsRatio = value;
|
||||||
),
|
});
|
||||||
StyledBooleanFormField(
|
},
|
||||||
value: _forCarbsRatio,
|
|
||||||
label: 'for carbs ratio',
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_forCarbsRatio = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StyledBooleanFormField(
|
|
||||||
value: _forPortionSize,
|
|
||||||
label: 'for portion size',
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_forPortionSize = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: _confidenceRatingController,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Confidence Rating',
|
|
||||||
),
|
),
|
||||||
),
|
BooleanFormField(
|
||||||
TextFormField(
|
value: _forPortionSize,
|
||||||
controller: _notesController,
|
label: 'for portion size',
|
||||||
keyboardType: TextInputType.multiline,
|
onChanged: (value) {
|
||||||
decoration: const InputDecoration(
|
setState(() {
|
||||||
labelText: 'Notes',
|
_forPortionSize = value;
|
||||||
alignLabelWithHint: true,
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
NumberFormField(
|
||||||
],
|
controller: _confidenceRatingController,
|
||||||
),
|
label: 'Confidence Rating',
|
||||||
],
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_confidenceRatingController.text =
|
||||||
|
(value ?? 0).toInt().toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: DetailBottomRow(
|
bottomNavigationBar: DetailBottomRow(
|
||||||
onCancel: handleCancelAction,
|
onCancel: handleCancelAction,
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
onAction: _isSaving ? null : handleSaveAction,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/screens/accuracy_detail.dart';
|
import 'package:diameter/screens/accuracy_detail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -16,13 +16,21 @@ class AccuracyListScreen extends StatefulWidget {
|
|||||||
class _AccuracyListScreenState extends State<AccuracyListScreen> {
|
class _AccuracyListScreenState extends State<AccuracyListScreen> {
|
||||||
List<Accuracy> _accuracies = Accuracy.getAll();
|
List<Accuracy> _accuracies = Accuracy.getAll();
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
refresh();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh({String? message}) {
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_accuracies = Accuracy.getAll();
|
_accuracies = Accuracy.getAll();
|
||||||
});
|
});
|
||||||
@ -42,12 +50,12 @@ class _AccuracyListScreenState extends State<AccuracyListScreen> {
|
|||||||
|
|
||||||
void onDelete(Accuracy accuracy) {
|
void onDelete(Accuracy accuracy) {
|
||||||
Accuracy.remove(accuracy.id);
|
Accuracy.remove(accuracy.id);
|
||||||
refresh();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(Accuracy accuracy) async {
|
void handleDeleteAction(Accuracy accuracy) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(accuracy),
|
onConfirm: () => onDelete(accuracy),
|
||||||
message: 'Are you sure you want to delete this Accuracy?',
|
message: 'Are you sure you want to delete this Accuracy?',
|
||||||
@ -60,13 +68,13 @@ class _AccuracyListScreenState extends State<AccuracyListScreen> {
|
|||||||
void handleToggleForPortionSizeAction(Accuracy accuracy) async {
|
void handleToggleForPortionSizeAction(Accuracy accuracy) async {
|
||||||
accuracy.forPortionSize = !accuracy.forPortionSize;
|
accuracy.forPortionSize = !accuracy.forPortionSize;
|
||||||
Accuracy.put(accuracy);
|
Accuracy.put(accuracy);
|
||||||
refresh();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleToggleForCarbsRatioAction(Accuracy accuracy) async {
|
void handleToggleForCarbsRatioAction(Accuracy accuracy) async {
|
||||||
accuracy.forCarbsRatio = !accuracy.forCarbsRatio;
|
accuracy.forCarbsRatio = !accuracy.forCarbsRatio;
|
||||||
Accuracy.put(accuracy);
|
Accuracy.put(accuracy);
|
||||||
refresh();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -75,7 +83,7 @@ class _AccuracyListScreenState extends State<AccuracyListScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Accuracies'),
|
title: const Text('Accuracies'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(onPressed: refresh, icon: const Icon(Icons.refresh))
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: AccuracyListScreen.routeName),
|
drawer: const Navigation(currentLocation: AccuracyListScreen.routeName),
|
||||||
@ -83,67 +91,83 @@ class _AccuracyListScreenState extends State<AccuracyListScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _accuracies.isNotEmpty ? ListView.builder(
|
child: _accuracies.isNotEmpty
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
? Scrollbar(
|
||||||
itemCount: _accuracies.length,
|
controller: _scrollController,
|
||||||
itemBuilder: (context, index) {
|
child: ReorderableListView.builder(
|
||||||
final accuracy = _accuracies[index];
|
padding: const EdgeInsets.all(10.0),
|
||||||
return ListTile(
|
scrollController: _scrollController,
|
||||||
onTap: () {
|
itemCount: _accuracies.length,
|
||||||
Navigator.push(
|
onReorder: (oldIndex, newIndex) {
|
||||||
context,
|
Accuracy.reorder(_accuracies[oldIndex], newIndex);
|
||||||
MaterialPageRoute(
|
reload();
|
||||||
builder: (context) =>
|
|
||||||
AccuracyDetailScreen(id: accuracy.id),
|
|
||||||
),
|
|
||||||
).then((message) => refresh(message: message));
|
|
||||||
},
|
|
||||||
title: Text(accuracy.value),
|
|
||||||
leading: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.reorder),
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: implement reordering
|
|
||||||
},
|
},
|
||||||
),
|
itemBuilder: (context, index) {
|
||||||
],
|
final accuracy = _accuracies[index];
|
||||||
|
return Card(
|
||||||
|
key: Key(accuracy.id.toString()),
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
AccuracyDetailScreen(id: accuracy.id),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
accuracy.value.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
leading: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.reorder),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.square_foot,
|
||||||
|
color: accuracy.forPortionSize
|
||||||
|
? Theme.of(context)
|
||||||
|
.toggleableActiveColor
|
||||||
|
: Theme.of(context).highlightColor,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
handleToggleForPortionSizeAction(
|
||||||
|
accuracy),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.pie_chart,
|
||||||
|
color: accuracy.forCarbsRatio
|
||||||
|
? Theme.of(context)
|
||||||
|
.toggleableActiveColor
|
||||||
|
: Theme.of(context).highlightColor,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
handleToggleForCarbsRatioAction(
|
||||||
|
accuracy),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () =>
|
||||||
|
handleDeleteAction(accuracy),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text('You have not created any Accuracies yet!'),
|
||||||
),
|
),
|
||||||
trailing: Row(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.square_foot,
|
|
||||||
color: accuracy.forPortionSize
|
|
||||||
? Theme.of(context).toggleableActiveColor
|
|
||||||
: Theme.of(context).highlightColor,
|
|
||||||
),
|
|
||||||
onPressed: () => handleToggleForPortionSizeAction(accuracy),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.pie_chart,
|
|
||||||
color: accuracy.forCarbsRatio
|
|
||||||
? Theme.of(context).toggleableActiveColor
|
|
||||||
: Theme.of(context).highlightColor,
|
|
||||||
),
|
|
||||||
onPressed: () => handleToggleForCarbsRatioAction(accuracy),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 24),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: () => handleDeleteAction(accuracy),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
) : const Center(
|
|
||||||
child: Text('You have not created any Accuracies yet!'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
@ -153,7 +177,7 @@ class _AccuracyListScreenState extends State<AccuracyListScreen> {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const AccuracyDetailScreen(),
|
builder: (context) => const AccuracyDetailScreen(),
|
||||||
),
|
),
|
||||||
).then((message) => refresh(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
import 'package:diameter/components/detail.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/components/forms/number_form_field.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/components/forms/time_of_day_form_field.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/utils/date_time_utils.dart';
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
import 'package:diameter/models/basal.dart';
|
import 'package:diameter/models/basal.dart';
|
||||||
import 'package:diameter/models/basal_profile.dart';
|
import 'package:diameter/models/basal_profile.dart';
|
||||||
|
|
||||||
@ -32,15 +35,18 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
|
|||||||
Basal? _basal;
|
Basal? _basal;
|
||||||
bool _isNew = true;
|
bool _isNew = true;
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
|
bool _isFinalRate = true;
|
||||||
|
|
||||||
final GlobalKey<FormState> _basalForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _basalForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0);
|
TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0);
|
TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
|
|
||||||
final _startTimeController = TextEditingController(text: '');
|
final _startTimeController = TextEditingController(text: '');
|
||||||
final _endTimeController = TextEditingController(text: '');
|
final _endTimeController = TextEditingController(text: '');
|
||||||
final _unitsController = TextEditingController(text: '');
|
final _unitsController =
|
||||||
|
TextEditingController(text: 0.toStringAsPrecision(3));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -57,36 +63,67 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
|
|||||||
if (_basal != null) {
|
if (_basal != null) {
|
||||||
_startTime = TimeOfDay.fromDateTime(_basal!.startTime);
|
_startTime = TimeOfDay.fromDateTime(_basal!.startTime);
|
||||||
_endTime = TimeOfDay.fromDateTime(_basal!.endTime);
|
_endTime = TimeOfDay.fromDateTime(_basal!.endTime);
|
||||||
_unitsController.text = _basal!.units.toString();
|
_unitsController.text = _basal!.units.toStringAsPrecision(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStartTime();
|
_startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime);
|
||||||
updateEndTime();
|
_endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
void reload() {
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_startTimeController.dispose();
|
||||||
|
_endTimeController.dispose();
|
||||||
|
_unitsController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
if (widget.id != 0) {
|
if (widget.id != 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_basal = Basal.get(widget.id);
|
_basal = Basal.get(widget.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_isNew = _basal == null;
|
_isNew = _basal == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateStartTime() {
|
void updateStartTime(TimeOfDay? value) {
|
||||||
_startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime);
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_startTime = value;
|
||||||
|
_startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateEndTime() {
|
void updateEndTime(TimeOfDay? value) {
|
||||||
_endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime);
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_endTime = value;
|
||||||
|
_endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime);
|
||||||
|
_isFinalRate = widget.suggestedEndTime == null ||
|
||||||
|
_endTime == widget.suggestedEndTime!;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> validateTimePeriod() async {
|
Future<String?> validateTimePeriod() async {
|
||||||
String? error;
|
String? error;
|
||||||
List<Basal> basalRates = Basal.getAllForProfile(widget.basalProfileId);
|
List<Basal> basalRates = Basal.getAllForProfile(widget.basalProfileId);
|
||||||
|
|
||||||
// TODO use a query for the following checks instead?
|
|
||||||
// check for duplicates
|
|
||||||
if (basalRates
|
if (basalRates
|
||||||
.where((other) =>
|
.where((other) =>
|
||||||
(widget.id != other.id) &&
|
(widget.id != other.id) &&
|
||||||
@ -128,7 +165,7 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleSaveAction() async {
|
void handleSaveAction({bool next = true}) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
});
|
});
|
||||||
@ -143,7 +180,30 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
|
|||||||
);
|
);
|
||||||
basal.basalProfile.targetId = widget.basalProfileId;
|
basal.basalProfile.targetId = widget.basalProfileId;
|
||||||
Basal.put(basal);
|
Basal.put(basal);
|
||||||
Navigator.pop(context, '${_isNew ? 'New' : ''} Basal Rate saved');
|
|
||||||
|
if (next) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return BasalDetailScreen(
|
||||||
|
basalProfileId: widget.basalProfileId,
|
||||||
|
suggestedStartTime: _endTime,
|
||||||
|
suggestedEndTime: widget.suggestedEndTime,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
Navigator.pop(
|
||||||
|
context,
|
||||||
|
['New Basal Rate${result[1] != null ? 's' : ''} saved', basal] +
|
||||||
|
[result[1]],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Navigator.pop(
|
||||||
|
context, ['${_isNew ? 'New' : ''} Basal Rate saved', basal]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -153,21 +213,20 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleCancelAction() {
|
void handleCancelAction() {
|
||||||
if (showConfirmationDialogOnCancel &&
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
((_isNew &&
|
((_isNew &&
|
||||||
(_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) ||
|
(_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) ||
|
||||||
_endTime.hour != (widget.suggestedEndTime?.hour ?? 0) ||
|
_endTime.hour != (widget.suggestedEndTime?.hour ?? 0) ||
|
||||||
_startTime.minute !=
|
_startTime.minute !=
|
||||||
(widget.suggestedStartTime?.minute ?? 0) ||
|
(widget.suggestedStartTime?.minute ?? 0) ||
|
||||||
_endTime.minute != (widget.suggestedEndTime?.minute ?? 0) ||
|
_endTime.minute != (widget.suggestedEndTime?.minute ?? 0) ||
|
||||||
double.tryParse(_unitsController.text) != null)) ||
|
double.tryParse(_unitsController.text) != 0)) ||
|
||||||
(!_isNew &&
|
(!_isNew &&
|
||||||
(TimeOfDay.fromDateTime(_basal!.startTime) !=
|
(TimeOfDay.fromDateTime(_basal!.startTime) != _startTime ||
|
||||||
_startTime ||
|
|
||||||
TimeOfDay.fromDateTime(_basal!.endTime) != _endTime ||
|
TimeOfDay.fromDateTime(_basal!.endTime) != _endTime ||
|
||||||
(double.tryParse(_unitsController.text) ?? 0) !=
|
double.tryParse(_unitsController.text) !=
|
||||||
_basal!.units)))) {
|
_basal!.units)))) {
|
||||||
Dialogs.showCancelConfirmationDialog(
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
isNew: _isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
@ -185,73 +244,69 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
|
|||||||
'${_isNew ? 'New' : 'Edit'} Basal Rate for ${BasalProfile.get(widget.basalProfileId)?.name}'),
|
'${_isNew ? 'New' : 'Edit'} Basal Rate for ${BasalProfile.get(widget.basalProfileId)?.name}'),
|
||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: BasalDetailScreen.routeName),
|
drawer: const Navigation(currentLocation: BasalDetailScreen.routeName),
|
||||||
body: Column(
|
body: Scrollbar(
|
||||||
children: [
|
controller: _scrollController,
|
||||||
StyledForm(
|
child: SingleChildScrollView(
|
||||||
formState: _basalForm,
|
controller: _scrollController,
|
||||||
fields: [
|
child: Column(
|
||||||
Row(
|
children: [
|
||||||
children: [
|
FormWrapper(
|
||||||
Expanded(
|
formState: _basalForm,
|
||||||
child: Padding(
|
fields: [
|
||||||
padding: const EdgeInsets.only(right: 5),
|
Row(
|
||||||
child: StyledTimeOfDayFormField(
|
children: [
|
||||||
label: 'Start Time',
|
Expanded(
|
||||||
controller: _startTimeController,
|
child: Padding(
|
||||||
time: _startTime,
|
padding: const EdgeInsets.only(right: 5),
|
||||||
onChanged: (newStartTime) {
|
child: TimeOfDayFormField(
|
||||||
if (newStartTime != null) {
|
label: 'Start Time',
|
||||||
setState(() {
|
controller: _startTimeController,
|
||||||
_startTime = newStartTime;
|
time: _startTime,
|
||||||
});
|
onChanged: updateStartTime,
|
||||||
updateStartTime();
|
),
|
||||||
}
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
),
|
child: Padding(
|
||||||
Expanded(
|
padding: const EdgeInsets.only(left: 5),
|
||||||
child: Padding(
|
child: TimeOfDayFormField(
|
||||||
padding: const EdgeInsets.only(left: 5),
|
label: 'End Time',
|
||||||
child: StyledTimeOfDayFormField(
|
controller: _endTimeController,
|
||||||
label: 'End Time',
|
time: _endTime,
|
||||||
controller: _endTimeController,
|
onChanged: updateEndTime,
|
||||||
time: _endTime,
|
),
|
||||||
onChanged: (newEndTime) {
|
),
|
||||||
if (newEndTime != null) {
|
|
||||||
setState(() {
|
|
||||||
_endTime = newEndTime;
|
|
||||||
});
|
|
||||||
updateEndTime();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
|
NumberFormField(
|
||||||
|
controller: _unitsController,
|
||||||
|
label: 'Units',
|
||||||
|
suffix: 'U',
|
||||||
|
autoRoundToMultipleOfStep: true,
|
||||||
|
step: Settings.insulinSteps,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
_unitsController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
value, Settings.insulinSteps);
|
||||||
|
}
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
TextFormField(
|
|
||||||
controller: _unitsController,
|
|
||||||
keyboardType:
|
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Units',
|
|
||||||
suffixText: 'U',
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty) {
|
|
||||||
return 'Empty amount of units';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: DetailBottomRow(
|
bottomNavigationBar: DetailBottomRow(
|
||||||
onCancel: handleCancelAction,
|
onCancel: handleCancelAction,
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
onAction:
|
||||||
|
_isSaving ? null : () => handleSaveAction(next: !_isFinalRate),
|
||||||
|
onMiddleAction: _isSaving || _isFinalRate
|
||||||
|
? null
|
||||||
|
: () => handleSaveAction(next: false),
|
||||||
|
actionText: _isFinalRate ? 'SAVE & CLOSE' : 'NEXT',
|
||||||
|
middleActionText: 'SAVE & CLOSE',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/utils/date_time_utils.dart';
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/models/basal.dart';
|
import 'package:diameter/models/basal.dart';
|
||||||
@ -23,6 +23,14 @@ class BasalListScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BasalListScreenState extends State<BasalListScreen> {
|
class _BasalListScreenState extends State<BasalListScreen> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void reload({String? message}) {
|
void reload({String? message}) {
|
||||||
widget.reload();
|
widget.reload();
|
||||||
|
|
||||||
@ -48,7 +56,7 @@ class _BasalListScreenState extends State<BasalListScreen> {
|
|||||||
id: basal.id,
|
id: basal.id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((message) => reload(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDelete(Basal basal) {
|
void onDelete(Basal basal) {
|
||||||
@ -57,8 +65,8 @@ class _BasalListScreenState extends State<BasalListScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(Basal basal) async {
|
void handleDeleteAction(Basal basal) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(basal),
|
onConfirm: () => onDelete(basal),
|
||||||
message: 'Are you sure you want to delete this Basal Rate?',
|
message: 'Are you sure you want to delete this Basal Rate?',
|
||||||
@ -72,7 +80,6 @@ class _BasalListScreenState extends State<BasalListScreen> {
|
|||||||
List<Basal> basalRates = widget.basalRates;
|
List<Basal> basalRates = widget.basalRates;
|
||||||
Basal basal = basalRates[index];
|
Basal basal = basalRates[index];
|
||||||
|
|
||||||
// TODO: use queries for all this
|
|
||||||
// check for gaps
|
// check for gaps
|
||||||
if (index == 0 &&
|
if (index == 0 &&
|
||||||
(basal.startTime.hour != 0 || basal.startTime.minute != 0)) {
|
(basal.startTime.hour != 0 || basal.startTime.minute != 0)) {
|
||||||
@ -105,56 +112,76 @@ class _BasalListScreenState extends State<BasalListScreen> {
|
|||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
return 'This rate\'s time period overlaps with another one';
|
return 'This rate\'s time period overlaps with another one';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return widget.basalRates.isNotEmpty
|
||||||
child: Column(
|
? Scrollbar(
|
||||||
children: [
|
controller: _scrollController,
|
||||||
widget.basalRates.isNotEmpty ? ListView.builder(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
padding: const EdgeInsets.all(10.0),
|
||||||
itemCount: widget.basalRates.length,
|
controller: _scrollController,
|
||||||
itemBuilder: (context, index) {
|
shrinkWrap: true,
|
||||||
final basal = widget.basalRates[index];
|
itemCount: widget.basalRates.length,
|
||||||
final error = validateTimePeriod(index);
|
itemBuilder: (context, index) {
|
||||||
return ListTile(
|
final basal = widget.basalRates[index];
|
||||||
tileColor: error != null ? Colors.red.shade100 : null,
|
final error = validateTimePeriod(index);
|
||||||
onTap: () {
|
return Card(
|
||||||
handleEditAction(basal);
|
child: Column(
|
||||||
},
|
children: [
|
||||||
title: Row(
|
error != null
|
||||||
mainAxisSize: MainAxisSize.max,
|
? Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(5.0),
|
||||||
Expanded(
|
child: Row(
|
||||||
child: Text(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
'${DateTimeUtils.displayTime(basal.startTime)} - ${DateTimeUtils.displayTime(basal.endTime)}')),
|
children: [
|
||||||
const Spacer(),
|
Icon(Icons.warning,
|
||||||
Expanded(child: Text('${basal.units} U')),
|
color: Theme.of(context).errorColor),
|
||||||
],
|
Text(error,
|
||||||
),
|
style: TextStyle(
|
||||||
subtitle: error != null
|
color: Theme.of(context).errorColor)),
|
||||||
? Text(error, style: const TextStyle(color: Colors.red))
|
],
|
||||||
: Container(),
|
),
|
||||||
trailing: Row(
|
)
|
||||||
mainAxisSize: MainAxisSize.min,
|
: Container(),
|
||||||
children: [
|
ListTile(
|
||||||
IconButton(
|
onTap: () {
|
||||||
icon: const Icon(
|
handleEditAction(basal);
|
||||||
Icons.delete,
|
},
|
||||||
color: Colors.blue,
|
title: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${DateTimeUtils.displayTime(basal.startTime)} - ${DateTimeUtils.displayTime(basal.endTime)}')),
|
||||||
|
const Spacer(),
|
||||||
|
Expanded(child: Text('${basal.units} U')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleDeleteAction(basal),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => handleDeleteAction(basal),
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
) : const Center(
|
|
||||||
child: Text('You have not created any Basal Rates yet!'),
|
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
),
|
: const Center(
|
||||||
);
|
child: Text('You have not created any Basal Rates yet!'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
import 'package:diameter/components/detail.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/components/forms/boolean_form_field.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/models/basal.dart';
|
import 'package:diameter/models/basal.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/screens/basal/basal_detail.dart';
|
import 'package:diameter/screens/basal/basal_detail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
import 'package:diameter/models/basal_profile.dart';
|
import 'package:diameter/models/basal_profile.dart';
|
||||||
import 'package:diameter/screens/basal/basal_list.dart';
|
import 'package:diameter/screens/basal/basal_list.dart';
|
||||||
|
|
||||||
@ -27,14 +28,15 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
|||||||
BasalProfile? _basalProfile;
|
BasalProfile? _basalProfile;
|
||||||
List<Basal> _basalRates = [];
|
List<Basal> _basalRates = [];
|
||||||
bool _isNew = true;
|
bool _isNew = true;
|
||||||
bool _isSaving = false;
|
|
||||||
|
|
||||||
final GlobalKey<FormState> _basalProfileForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _basalProfileForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
late FloatingActionButton addBasalButton;
|
late FloatingActionButton addBasalButton;
|
||||||
late IconButton refreshButton;
|
late IconButton refreshButton;
|
||||||
late IconButton closeButton;
|
late IconButton closeButton;
|
||||||
late DetailBottomRow detailBottomRow;
|
late DetailBottomRow detailBottomRow;
|
||||||
|
late DetailBottomRow detailBottomRowWhileSaving;
|
||||||
|
|
||||||
FloatingActionButton? actionButton;
|
FloatingActionButton? actionButton;
|
||||||
List<Widget> appBarActions = [];
|
List<Widget> appBarActions = [];
|
||||||
@ -77,7 +79,13 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
|||||||
|
|
||||||
detailBottomRow = DetailBottomRow(
|
detailBottomRow = DetailBottomRow(
|
||||||
onCancel: handleCancelAction,
|
onCancel: handleCancelAction,
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
onAction: handleSaveAction,
|
||||||
|
onMiddleAction: () => handleSaveAction(close: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
detailBottomRowWhileSaving = DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
actionButton = null;
|
actionButton = null;
|
||||||
@ -85,6 +93,14 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
|||||||
bottomNav = detailBottomRow;
|
bottomNav = detailBottomRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_nameController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void reload({String? message}) {
|
void reload({String? message}) {
|
||||||
if (widget.id != 0) {
|
if (widget.id != 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -146,7 +162,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
|||||||
});
|
});
|
||||||
} else if (!_active &&
|
} else if (!_active &&
|
||||||
((_isNew && _activeCount == 0) ||
|
((_isNew && _activeCount == 0) ||
|
||||||
(_activeCount == 1 && _basalProfile!.active))) {
|
(!_isNew && _activeCount == 1 && _basalProfile!.active))) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
@ -178,25 +194,30 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
|||||||
TimeOfDay? suggestedStartTime;
|
TimeOfDay? suggestedStartTime;
|
||||||
TimeOfDay? suggestedEndTime;
|
TimeOfDay? suggestedEndTime;
|
||||||
|
|
||||||
_basalRates.asMap().forEach((index, basal) {
|
if (_basalRates.isEmpty) {
|
||||||
if (suggestedStartTime == null && suggestedEndTime == null) {
|
suggestedStartTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
if (index == 0 &&
|
suggestedEndTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
(basal.startTime.hour != 0 || basal.startTime.minute != 0)) {
|
} else {
|
||||||
suggestedStartTime = const TimeOfDay(hour: 0, minute: 0);
|
_basalRates.asMap().forEach((index, basal) {
|
||||||
suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime);
|
if (suggestedStartTime == null && suggestedEndTime == null) {
|
||||||
} else if ((index == _basalRates.length - 1) &&
|
if (index == 0 &&
|
||||||
(basal.endTime.hour != 0 || basal.endTime.minute != 0)) {
|
(basal.startTime.hour != 0 || basal.startTime.minute != 0)) {
|
||||||
suggestedStartTime = TimeOfDay.fromDateTime(basal.endTime);
|
suggestedStartTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
suggestedEndTime = const TimeOfDay(hour: 0, minute: 0);
|
|
||||||
} else if (index != 0) {
|
|
||||||
var lastEndTime = _basalRates[index - 1].endTime;
|
|
||||||
if (basal.startTime.isAfter(lastEndTime)) {
|
|
||||||
suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime);
|
|
||||||
suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime);
|
suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime);
|
||||||
|
} else if ((index == _basalRates.length - 1) &&
|
||||||
|
(basal.endTime.hour != 0 || basal.endTime.minute != 0)) {
|
||||||
|
suggestedStartTime = TimeOfDay.fromDateTime(basal.endTime);
|
||||||
|
suggestedEndTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
|
} else if (index != 0) {
|
||||||
|
var lastEndTime = _basalRates[index - 1].endTime;
|
||||||
|
if (basal.startTime.isAfter(lastEndTime)) {
|
||||||
|
suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime);
|
||||||
|
suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@ -209,30 +230,47 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).then((message) => reload(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleSaveAction() async {
|
void handleSaveAction({bool close = false}) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = true;
|
bottomNav = detailBottomRowWhileSaving;
|
||||||
});
|
});
|
||||||
if (_basalProfileForm.currentState!.validate()) {
|
if (_basalProfileForm.currentState!.validate()) {
|
||||||
await checkActiveProfiles();
|
await checkActiveProfiles();
|
||||||
BasalProfile.put(BasalProfile(
|
BasalProfile basalProfile = BasalProfile(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
name: _nameController.text,
|
name: _nameController.text,
|
||||||
active: _active,
|
active: _active,
|
||||||
notes: _notesController.text,
|
notes: _notesController.text,
|
||||||
));
|
);
|
||||||
Navigator.pop(context, '${_isNew ? 'New' : ''} Basal Profile saved');
|
BasalProfile.put(basalProfile);
|
||||||
|
|
||||||
|
if (close) {
|
||||||
|
Navigator.pop(context,
|
||||||
|
['${_isNew ? 'New' : ''} Basal Profile saved', basalProfile]);
|
||||||
|
} else {
|
||||||
|
if (_isNew) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
BasalProfileDetailScreen(id: basalProfile.id),
|
||||||
|
),
|
||||||
|
).then((result) => Navigator.pop(context, result));
|
||||||
|
} else {
|
||||||
|
reload(message: 'Basal Profile saved');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = false;
|
bottomNav = detailBottomRow;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleCancelAction() {
|
void handleCancelAction() {
|
||||||
if (showConfirmationDialogOnCancel &&
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
(_isNew &&
|
(_isNew &&
|
||||||
(_active != widget.active ||
|
(_active != widget.active ||
|
||||||
_nameController.text != '' ||
|
_nameController.text != '' ||
|
||||||
@ -241,7 +279,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
|||||||
(_basalProfile!.active != _active ||
|
(_basalProfile!.active != _active ||
|
||||||
_basalProfile!.name != _nameController.text ||
|
_basalProfile!.name != _nameController.text ||
|
||||||
(_basalProfile!.notes ?? '') != _notesController.text))) {
|
(_basalProfile!.notes ?? '') != _notesController.text))) {
|
||||||
Dialogs.showCancelConfirmationDialog(
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
isNew: _isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
@ -279,52 +317,59 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
|||||||
renderTabButtons(tabController.index);
|
renderTabButtons(tabController.index);
|
||||||
});
|
});
|
||||||
List<Widget> tabs = [
|
List<Widget> tabs = [
|
||||||
SingleChildScrollView(
|
Scrollbar(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: SingleChildScrollView(
|
||||||
children: [
|
controller: _scrollController,
|
||||||
StyledForm(
|
child: Column(
|
||||||
formState: _basalProfileForm,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
fields: [
|
children: [
|
||||||
TextFormField(
|
FormWrapper(
|
||||||
controller: _nameController,
|
formState: _basalProfileForm,
|
||||||
decoration: const InputDecoration(
|
fields: [
|
||||||
labelText: 'Name',
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
validator: (value) {
|
TextFormField(
|
||||||
if (value!.trim().isEmpty) {
|
keyboardType: TextInputType.multiline,
|
||||||
return 'Empty title';
|
controller: _notesController,
|
||||||
}
|
decoration: const InputDecoration(
|
||||||
return null;
|
labelText: 'Notes',
|
||||||
},
|
),
|
||||||
),
|
minLines: 2,
|
||||||
TextFormField(
|
maxLines: 5,
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
controller: _notesController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Notes',
|
|
||||||
suffixText: '',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
),
|
||||||
),
|
BooleanFormField(
|
||||||
StyledBooleanFormField(
|
value: _active,
|
||||||
value: _active,
|
onChanged: (value) {
|
||||||
onChanged: (value) {
|
setState(() {
|
||||||
setState(() {
|
_active = value;
|
||||||
_active = value;
|
});
|
||||||
});
|
},
|
||||||
},
|
label: 'active',
|
||||||
label: 'active',
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!_isNew) {
|
if (!_isNew) {
|
||||||
tabs.add(BasalListScreen(basalProfile: _basalProfile!, basalRates: _basalRates, reload: reload));
|
tabs.add(BasalListScreen(
|
||||||
|
basalProfile: _basalProfile!,
|
||||||
|
basalRates: _basalRates,
|
||||||
|
reload: reload));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
278
lib/screens/basal/basal_profile_list.dart
Normal file
278
lib/screens/basal/basal_profile_list.dart
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
|
import 'package:diameter/models/basal.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:diameter/models/basal_profile.dart';
|
||||||
|
import 'package:diameter/screens/basal/basal_profile_detail.dart';
|
||||||
|
|
||||||
|
class BasalProfileListScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/basal-profiles';
|
||||||
|
const BasalProfileListScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_BasalProfileListScreenState createState() => _BasalProfileListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BasalProfileListScreenState extends State<BasalProfileListScreen> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
late List<BasalProfile> _basalProfiles;
|
||||||
|
Widget banner = Container();
|
||||||
|
|
||||||
|
final BasalProfile? _activeProfile = BasalProfile.getActive(DateTime.now());
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
setState(() {
|
||||||
|
_basalProfiles = BasalProfile.getAll();
|
||||||
|
});
|
||||||
|
updateBanner();
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateBanner() {
|
||||||
|
int activeProfileCount = BasalProfile.activeCount();
|
||||||
|
setState(() {
|
||||||
|
banner = activeProfileCount != 1
|
||||||
|
? MaterialBanner(
|
||||||
|
content: Text(activeProfileCount == 0
|
||||||
|
? 'You currently do not have an active Basal Profile.'
|
||||||
|
: 'More than one active Basal Profile has been found.'),
|
||||||
|
leading: const CircleAvatar(child: Icon(Icons.warning)),
|
||||||
|
forceActionsBelow: true,
|
||||||
|
actions: activeProfileCount == 0
|
||||||
|
? [
|
||||||
|
_basalProfiles.isNotEmpty
|
||||||
|
? TextButton(
|
||||||
|
child: const Text('ACTIVATE A PROFILE'),
|
||||||
|
onPressed: handlePickActiveProfileAction,
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
TextButton(
|
||||||
|
child: const Text('CREATE A NEW PROFILE'),
|
||||||
|
onPressed: () => onNew(true),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('PICK A PROFILE'),
|
||||||
|
onPressed: handlePickActiveProfileAction,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Container();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDuplicateAction(BasalProfile basalProfile) async {
|
||||||
|
final copy = BasalProfile(
|
||||||
|
active: false,
|
||||||
|
name: 'Copy of ${basalProfile.name}',
|
||||||
|
);
|
||||||
|
BasalProfile.put(copy);
|
||||||
|
|
||||||
|
final rates = Basal.getAllForProfile(basalProfile.id);
|
||||||
|
for (Basal rate in rates) {
|
||||||
|
final basal = Basal(
|
||||||
|
endTime: rate.endTime,
|
||||||
|
startTime: rate.startTime,
|
||||||
|
units: rate.units,
|
||||||
|
);
|
||||||
|
basal.basalProfile.target = copy;
|
||||||
|
Basal.put(basal);
|
||||||
|
}
|
||||||
|
|
||||||
|
reload(message: 'Added copy of ${basalProfile.name}');
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDelete(BasalProfile basalProfile) {
|
||||||
|
BasalProfile.remove(basalProfile.id);
|
||||||
|
reload(message: 'Basal Profile deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeleteAction(BasalProfile basalProfile) async {
|
||||||
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
|
DialogUtils.showConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
onConfirm: () => onDelete(basalProfile),
|
||||||
|
message: 'Are you sure you want to delete this Basal Profile?',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onDelete(basalProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPickActive(BasalProfile? basalProfile) {
|
||||||
|
if (basalProfile != null) {
|
||||||
|
BasalProfile.setAllInactive;
|
||||||
|
basalProfile.active = true;
|
||||||
|
BasalProfile.put(basalProfile);
|
||||||
|
reload(
|
||||||
|
message: '${basalProfile.name} has been set as your active Profile');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handlePickActiveProfileAction() {
|
||||||
|
setState(() {
|
||||||
|
banner = MaterialBanner(
|
||||||
|
content: AutoCompleteDropdownButton(
|
||||||
|
controller: TextEditingController(text: ''),
|
||||||
|
items: _basalProfiles,
|
||||||
|
label: 'Default Basal Profile',
|
||||||
|
onChanged: onPickActive,
|
||||||
|
),
|
||||||
|
leading: const CircleAvatar(child: Icon(Icons.info)),
|
||||||
|
forceActionsBelow: true,
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('CREATE A NEW PROFILE INSTEAD'),
|
||||||
|
onPressed: () => onNew(true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void showDetailScreen({BasalProfile? basalProfile, bool active = false}) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
BasalProfileDetailScreen(id: basalProfile?.id ?? 0, active: active),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void onNew(bool active) {
|
||||||
|
showDetailScreen(active: active);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEdit(BasalProfile basalProfile) {
|
||||||
|
showDetailScreen(basalProfile: basalProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Basal Profiles'),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
drawer:
|
||||||
|
const Navigation(currentLocation: BasalProfileListScreen.routeName),
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
banner,
|
||||||
|
Expanded(
|
||||||
|
child: _basalProfiles.isNotEmpty
|
||||||
|
? Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
controller: _scrollController,
|
||||||
|
itemCount: _basalProfiles.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final basalProfile = _basalProfiles[index];
|
||||||
|
double dailyTotal =
|
||||||
|
Basal.getDailyTotalForProfile(basalProfile.id);
|
||||||
|
String activeProfileText = basalProfile.active
|
||||||
|
? ' (Default Profile)'
|
||||||
|
: basalProfile.id == _activeProfile?.id
|
||||||
|
? ' (Current Active Profile)'
|
||||||
|
: '';
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
isThreeLine: true,
|
||||||
|
selected: basalProfile.active ||
|
||||||
|
basalProfile.id == _activeProfile?.id,
|
||||||
|
onTap: () => onEdit(basalProfile),
|
||||||
|
title: Text(
|
||||||
|
basalProfile.name.toUpperCase() +
|
||||||
|
activeProfileText,
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(basalProfile.notes ?? ''),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: dailyTotal > 0
|
||||||
|
? [
|
||||||
|
Text(dailyTotal
|
||||||
|
.toStringAsPrecision(3)),
|
||||||
|
const Text('U/day',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.copy,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
handleDuplicateAction(basalProfile),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
handleDeleteAction(basalProfile),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text('You have not created any Basal Profiles yet!'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () => onNew(false),
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,203 +0,0 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/navigation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
// import 'package:diameter/components/progress_indicator.dart';
|
|
||||||
import 'package:diameter/models/basal_profile.dart';
|
|
||||||
import 'package:diameter/screens/basal/basal_profile_detail.dart';
|
|
||||||
|
|
||||||
class BasalProfileListScreen extends StatefulWidget {
|
|
||||||
static const String routeName = '/basal-profiles';
|
|
||||||
const BasalProfileListScreen({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_BasalProfileListScreenState createState() => _BasalProfileListScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BasalProfileListScreenState extends State<BasalProfileListScreen> {
|
|
||||||
late List<BasalProfile> _basalProfiles;
|
|
||||||
Widget banner = Container();
|
|
||||||
bool pickActiveProfileMode = false;
|
|
||||||
|
|
||||||
void refresh({String? message}) {
|
|
||||||
setState(() {
|
|
||||||
pickActiveProfileMode = false;
|
|
||||||
_basalProfiles = BasalProfile.getAll();
|
|
||||||
});
|
|
||||||
// _basalProfiles.then((list) =>
|
|
||||||
updateBanner();
|
|
||||||
setState(() {
|
|
||||||
if (message != null) {
|
|
||||||
var snackBar = SnackBar(
|
|
||||||
content: Text(message),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
..removeCurrentSnackBar()
|
|
||||||
..showSnackBar(snackBar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateBanner() {
|
|
||||||
int activeProfileCount = BasalProfile.activeCount();
|
|
||||||
setState(() {
|
|
||||||
banner = activeProfileCount != 1
|
|
||||||
? MaterialBanner(
|
|
||||||
content: Text(activeProfileCount == 0
|
|
||||||
? 'You currently do not have an active Basal Profile.'
|
|
||||||
: 'More than one active Basal Profile has been found.'),
|
|
||||||
leading: const CircleAvatar(child: Icon(Icons.warning)),
|
|
||||||
forceActionsBelow: true,
|
|
||||||
actions: activeProfileCount == 0
|
|
||||||
? [
|
|
||||||
_basalProfiles.isNotEmpty
|
|
||||||
? TextButton(
|
|
||||||
child: const Text('ACTIVATE A PROFILE'),
|
|
||||||
onPressed: handlePickActiveProfileAction,
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
TextButton(
|
|
||||||
child: const Text('CREATE A NEW PROFILE'),
|
|
||||||
onPressed: () => onNew(true),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
TextButton(
|
|
||||||
child: const Text('PICK A PROFILE'),
|
|
||||||
onPressed: handlePickActiveProfileAction,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Container();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDelete(BasalProfile basalProfile) {
|
|
||||||
BasalProfile.remove(basalProfile.id);
|
|
||||||
refresh(message: 'Basal Profile deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleDeleteAction(BasalProfile basalProfile) async {
|
|
||||||
if (showConfirmationDialogOnDelete) {
|
|
||||||
Dialogs.showConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
onConfirm: () => onDelete(basalProfile),
|
|
||||||
message: 'Are you sure you want to delete this Basal Profile?',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
onDelete(basalProfile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPickActive(BasalProfile basalProfile) {
|
|
||||||
BasalProfile.setAllInactive;
|
|
||||||
basalProfile.active = true;
|
|
||||||
BasalProfile.put(basalProfile);
|
|
||||||
// (exception: basalProfile.objectId!).then((_) =>
|
|
||||||
refresh(message: '${basalProfile.name} has been set as your active Profile');
|
|
||||||
}
|
|
||||||
|
|
||||||
void handlePickActiveProfileAction() {
|
|
||||||
setState(() {
|
|
||||||
banner = MaterialBanner(
|
|
||||||
content: const Text('Click one of the profiles to active it.'),
|
|
||||||
leading: const CircleAvatar(child: Icon(Icons.info)),
|
|
||||||
forceActionsBelow: true,
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
child: const Text('CREATE A NEW PROFILE INSTEAD'),
|
|
||||||
onPressed: () => onNew(true),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
pickActiveProfileMode = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void showDetailScreen({BasalProfile? basalProfile, bool active = false}) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => BasalProfileDetailScreen(
|
|
||||||
id: basalProfile?.id ?? 0, active: active),
|
|
||||||
),
|
|
||||||
).then((message) => refresh(message: message));
|
|
||||||
}
|
|
||||||
|
|
||||||
void onNew(bool active) {
|
|
||||||
showDetailScreen(active: active);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onEdit(BasalProfile basalProfile) {
|
|
||||||
showDetailScreen(basalProfile: basalProfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Basal Profiles'),
|
|
||||||
actions: <Widget>[
|
|
||||||
IconButton(onPressed: refresh, icon: const Icon(Icons.refresh))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
drawer:
|
|
||||||
const Navigation(currentLocation: BasalProfileListScreen.routeName),
|
|
||||||
body: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
banner,
|
|
||||||
Expanded(
|
|
||||||
child: _basalProfiles.isNotEmpty ? ListView.builder(
|
|
||||||
itemCount: _basalProfiles.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final basalProfile = _basalProfiles[index];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
tileColor: basalProfile.active
|
|
||||||
? Colors.green.shade100
|
|
||||||
: null,
|
|
||||||
onTap: () {
|
|
||||||
pickActiveProfileMode
|
|
||||||
? onPickActive(basalProfile)
|
|
||||||
: onEdit(basalProfile);
|
|
||||||
},
|
|
||||||
title: Text(
|
|
||||||
basalProfile.name,
|
|
||||||
),
|
|
||||||
subtitle: Text(basalProfile.notes!),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
onPressed: () =>
|
|
||||||
handleDeleteAction(basalProfile),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
) : const Center(
|
|
||||||
child: Text('You have not created any Basal Profiles yet!'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () => onNew(false),
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,13 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
import 'package:diameter/components/detail.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/components/forms/number_form_field.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/components/forms/time_of_day_form_field.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/settings.dart';
|
|
||||||
import 'package:diameter/utils/date_time_utils.dart';
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
import 'package:diameter/utils/utils.dart';
|
import 'package:diameter/utils/utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
import 'package:diameter/models/bolus.dart';
|
import 'package:diameter/models/bolus.dart';
|
||||||
import 'package:diameter/models/bolus_profile.dart';
|
import 'package:diameter/models/bolus_profile.dart';
|
||||||
|
|
||||||
@ -34,18 +35,20 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
|||||||
Bolus? _bolus;
|
Bolus? _bolus;
|
||||||
bool _isNew = true;
|
bool _isNew = true;
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
|
bool _isFinalRate = true;
|
||||||
|
|
||||||
final GlobalKey<FormState> _bolusForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _bolusForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0);
|
TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0);
|
TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
|
|
||||||
final _startTimeController = TextEditingController(text: '');
|
final _startTimeController = TextEditingController(text: '');
|
||||||
final _endTimeController = TextEditingController(text: '');
|
final _endTimeController = TextEditingController(text: '');
|
||||||
final _unitsController = TextEditingController(text: '');
|
final _unitsController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.insulinSteps));
|
||||||
final _carbsController = TextEditingController(text: '');
|
final _carbsController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.nutritionSteps));
|
||||||
final _mgPerDlController = TextEditingController(text: '');
|
final _mgPerDlController = TextEditingController(text: '0');
|
||||||
final _mmolPerLController = TextEditingController(text: '');
|
final _mmolPerLController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.mmolPerLSteps));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -61,41 +64,75 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
|||||||
|
|
||||||
if (_bolus != null) {
|
if (_bolus != null) {
|
||||||
_startTime = TimeOfDay.fromDateTime(_bolus!.startTime);
|
_startTime = TimeOfDay.fromDateTime(_bolus!.startTime);
|
||||||
|
|
||||||
_endTime = TimeOfDay.fromDateTime(_bolus!.endTime);
|
_endTime = TimeOfDay.fromDateTime(_bolus!.endTime);
|
||||||
|
|
||||||
_unitsController.text = _bolus!.units.toString();
|
_unitsController.text = _bolus!.units.toString();
|
||||||
_carbsController.text = _bolus!.carbs.toString();
|
_carbsController.text = _bolus!.carbs.toString();
|
||||||
_mgPerDlController.text = _bolus!.mgPerDl.toString();
|
_mgPerDlController.text = (_bolus!.mgPerDl ?? '').toString();
|
||||||
_mmolPerLController.text = _bolus!.mmolPerL.toString();
|
_mmolPerLController.text = (_bolus!.mmolPerL ?? '').toString();
|
||||||
}
|
}
|
||||||
|
_startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime);
|
||||||
updateStartTime();
|
_endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime);
|
||||||
updateEndTime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void reload() {
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_startTimeController.dispose();
|
||||||
|
_endTimeController.dispose();
|
||||||
|
_unitsController.dispose();
|
||||||
|
_carbsController.dispose();
|
||||||
|
_mgPerDlController.dispose();
|
||||||
|
_mmolPerLController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
if (widget.id != 0) {
|
if (widget.id != 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_bolus = Bolus.get(widget.id);
|
_bolus = Bolus.get(widget.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_isNew = _bolus == null;
|
_isNew = _bolus == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateStartTime() {
|
void updateStartTime(TimeOfDay? value) {
|
||||||
_startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime);
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_startTime = value;
|
||||||
|
_startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateEndTime() {
|
void updateEndTime(TimeOfDay? value) {
|
||||||
_endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime);
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_endTime = value;
|
||||||
|
_endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime);
|
||||||
|
_isFinalRate = widget.suggestedEndTime == null ||
|
||||||
|
_endTime == widget.suggestedEndTime!;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> validateTimePeriod() async {
|
Future<String?> validateTimePeriod() async {
|
||||||
String? error;
|
String? error;
|
||||||
List<Bolus> bolusRates = Bolus.getAllForProfile(widget.bolusProfileId);
|
List<Bolus> bolusRates = Bolus.getAllForProfile(widget.bolusProfileId);
|
||||||
// BolusProfile.get(widget.bolusProfileId)?.bolusRates ?? [];
|
|
||||||
|
|
||||||
// TODO use a query for the following checks instead?
|
|
||||||
// check for duplicates
|
// check for duplicates
|
||||||
if (bolusRates
|
if (bolusRates
|
||||||
.where((other) =>
|
.where((other) =>
|
||||||
@ -138,7 +175,7 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleSaveAction() async {
|
void handleSaveAction({bool next = true}) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
});
|
});
|
||||||
@ -153,11 +190,34 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
|||||||
units: double.tryParse(_unitsController.text) ?? 0,
|
units: double.tryParse(_unitsController.text) ?? 0,
|
||||||
carbs: double.tryParse(_carbsController.text) ?? 0,
|
carbs: double.tryParse(_carbsController.text) ?? 0,
|
||||||
mgPerDl: int.tryParse(_mgPerDlController.text),
|
mgPerDl: int.tryParse(_mgPerDlController.text),
|
||||||
mmolPerL: double.parse(_mmolPerLController.text),
|
mmolPerL: double.tryParse(_mmolPerLController.text),
|
||||||
);
|
);
|
||||||
bolus.bolusProfile.targetId = widget.bolusProfileId;
|
bolus.bolusProfile.targetId = widget.bolusProfileId;
|
||||||
Bolus.put(bolus);
|
Bolus.put(bolus);
|
||||||
Navigator.pop(context, '${_isNew ? 'New' : ''} Bolus Rate saved');
|
|
||||||
|
if (next) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return BolusDetailScreen(
|
||||||
|
bolusProfileId: widget.bolusProfileId,
|
||||||
|
suggestedStartTime: _endTime,
|
||||||
|
suggestedEndTime: widget.suggestedEndTime,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
Navigator.pop(
|
||||||
|
context,
|
||||||
|
['New Bolus Rate${result[1] != null ? 's' : ''} saved', bolus] +
|
||||||
|
[result[1]],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Navigator.pop(
|
||||||
|
context, ['${_isNew ? 'New' : ''} Bolus Rate saved', bolus]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -168,7 +228,7 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleCancelAction() {
|
void handleCancelAction() {
|
||||||
if (showConfirmationDialogOnCancel &&
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
((_isNew &&
|
((_isNew &&
|
||||||
(_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) ||
|
(_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) ||
|
||||||
_endTime.hour != (widget.suggestedEndTime?.hour ?? 0) ||
|
_endTime.hour != (widget.suggestedEndTime?.hour ?? 0) ||
|
||||||
@ -190,7 +250,7 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
|||||||
_bolus!.mgPerDl ||
|
_bolus!.mgPerDl ||
|
||||||
(double.tryParse(_mmolPerLController.text) ?? 0) !=
|
(double.tryParse(_mmolPerLController.text) ?? 0) !=
|
||||||
_bolus!.mmolPerL)))) {
|
_bolus!.mmolPerL)))) {
|
||||||
Dialogs.showCancelConfirmationDialog(
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
isNew: _isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
@ -200,30 +260,26 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) {
|
void convertBetweenMgPerDlAndMmolPerL(double? value) async {
|
||||||
int? mgPerDl;
|
if (value != null) {
|
||||||
double? mmolPerL;
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl &&
|
||||||
|
_mgPerDlController.text != '') {
|
||||||
if (calculateFrom != GlucoseMeasurement.mmolPerL &&
|
_mgPerDlController.text = value.toInt().toString();
|
||||||
_mgPerDlController.text != '') {
|
setState(() {
|
||||||
mgPerDl = int.tryParse(_mgPerDlController.text);
|
_mmolPerLController.text =
|
||||||
}
|
Utils.convertMgPerDlToMmolPerL(value.toInt()).toString();
|
||||||
if (calculateFrom != GlucoseMeasurement.mgPerDl &&
|
});
|
||||||
_mmolPerLController.text != '') {
|
}
|
||||||
mmolPerL = double.tryParse(_mmolPerLController.text);
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL &&
|
||||||
}
|
_mmolPerLController.text != '') {
|
||||||
|
|
||||||
if (mgPerDl != null && mmolPerL == null) {
|
|
||||||
setState(() {
|
|
||||||
_mmolPerLController.text =
|
_mmolPerLController.text =
|
||||||
Utils.convertMgPerDlToMmolPerL(mgPerDl!).toString();
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
});
|
value, Settings.mmolPerLSteps);
|
||||||
}
|
setState(() {
|
||||||
if (mmolPerL != null && mgPerDl == null) {
|
_mgPerDlController.text =
|
||||||
setState(() {
|
Utils.convertMmolPerLToMgPerDl(value.toDouble()).toString();
|
||||||
_mgPerDlController.text =
|
});
|
||||||
Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString();
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,172 +291,127 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
|||||||
'${_isNew ? 'New' : 'Edit'} Bolus Rate for ${BolusProfile.get(widget.bolusProfileId)?.name}'),
|
'${_isNew ? 'New' : 'Edit'} Bolus Rate for ${BolusProfile.get(widget.bolusProfileId)?.name}'),
|
||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: BolusDetailScreen.routeName),
|
drawer: const Navigation(currentLocation: BolusDetailScreen.routeName),
|
||||||
body: SingleChildScrollView(
|
body: Scrollbar(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
children: [
|
child: SingleChildScrollView(
|
||||||
StyledForm(
|
controller: _scrollController,
|
||||||
formState: _bolusForm,
|
child: Column(
|
||||||
fields: [
|
children: [
|
||||||
Row(
|
FormWrapper(
|
||||||
children: [
|
formState: _bolusForm,
|
||||||
Expanded(
|
fields: [
|
||||||
child: Padding(
|
Row(
|
||||||
padding: const EdgeInsets.only(right: 5),
|
children: [
|
||||||
child: StyledTimeOfDayFormField(
|
Expanded(
|
||||||
label: 'Start Time',
|
child: Padding(
|
||||||
controller: _startTimeController,
|
padding: const EdgeInsets.only(right: 5),
|
||||||
time: _startTime,
|
child: TimeOfDayFormField(
|
||||||
onChanged: (newStartTime) {
|
label: 'Start Time',
|
||||||
if (newStartTime != null) {
|
controller: _startTimeController,
|
||||||
setState(() {
|
time: _startTime,
|
||||||
_startTime = newStartTime;
|
onChanged: updateStartTime,
|
||||||
});
|
),
|
||||||
updateStartTime();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.only(left: 5),
|
||||||
padding: const EdgeInsets.only(left: 5),
|
child: TimeOfDayFormField(
|
||||||
child: StyledTimeOfDayFormField(
|
label: 'End Time',
|
||||||
label: 'End Time',
|
controller: _endTimeController,
|
||||||
controller: _endTimeController,
|
time: _endTime,
|
||||||
time: _endTime,
|
onChanged: updateEndTime,
|
||||||
onChanged: (newEndTime) {
|
),
|
||||||
if (newEndTime != null) {
|
|
||||||
setState(() {
|
|
||||||
_endTime = newEndTime;
|
|
||||||
});
|
|
||||||
updateEndTime();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Units',
|
|
||||||
suffixText: 'U',
|
|
||||||
),
|
),
|
||||||
controller: _unitsController,
|
NumberFormField(
|
||||||
keyboardType:
|
controller: _unitsController,
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
label: 'Units',
|
||||||
validator: (value) {
|
suffix: 'U',
|
||||||
if (value!.trim().isEmpty) {
|
autoRoundToMultipleOfStep: true,
|
||||||
return 'Empty amount of units';
|
step: Settings.insulinSteps,
|
||||||
}
|
onChanged: (value) {
|
||||||
return null;
|
if (value != null) {
|
||||||
},
|
_unitsController.text =
|
||||||
),
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
TextFormField(
|
value, Settings.insulinSteps);
|
||||||
decoration: InputDecoration(
|
}
|
||||||
labelText: 'per carbs',
|
},
|
||||||
suffixText: nutritionMeasurement ==
|
|
||||||
NutritionMeasurement.grams
|
|
||||||
? 'g'
|
|
||||||
: nutritionMeasurement == NutritionMeasurement.ounces
|
|
||||||
? 'oz'
|
|
||||||
: '',
|
|
||||||
),
|
),
|
||||||
controller: _carbsController,
|
NumberFormField(
|
||||||
keyboardType:
|
controller: _carbsController,
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
label: 'per carbs',
|
||||||
validator: (value) {
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
if (value!.trim().isEmpty) {
|
autoRoundToMultipleOfStep: true,
|
||||||
return 'How many carbs does the rate make up for?';
|
step: Settings.nutritionSteps,
|
||||||
}
|
onChanged: (value) {
|
||||||
return null;
|
if (value != null) {
|
||||||
},
|
_carbsController.text =
|
||||||
),
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
Row(
|
value, Settings.nutritionSteps);
|
||||||
children: [
|
}
|
||||||
glucoseMeasurement == GlucoseMeasurement.mgPerDl ||
|
},
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.both ||
|
),
|
||||||
glucoseDisplayMode ==
|
Row(
|
||||||
GlucoseDisplayMode.bothForDetail
|
children: [
|
||||||
? Expanded(
|
Settings.glucoseMeasurement ==
|
||||||
child: TextFormField(
|
GlucoseMeasurement.mgPerDl ||
|
||||||
decoration: const InputDecoration(
|
Settings.glucoseDisplayMode ==
|
||||||
labelText: 'per mg/dl',
|
GlucoseDisplayMode.both ||
|
||||||
suffixText: 'mg/dl',
|
Settings.glucoseDisplayMode ==
|
||||||
|
GlucoseDisplayMode.bothForDetail
|
||||||
|
? Expanded(
|
||||||
|
flex: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? 2 : 1,
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'per mg/dl',
|
||||||
|
suffix: 'mg/dl',
|
||||||
|
readOnly: Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mmolPerL,
|
||||||
|
showSteppers: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl,
|
||||||
|
controller: _mgPerDlController,
|
||||||
|
onChanged: convertBetweenMgPerDlAndMmolPerL,
|
||||||
),
|
),
|
||||||
controller: _mgPerDlController,
|
)
|
||||||
onChanged: (_) =>
|
: Container(),
|
||||||
convertBetweenMgPerDlAndMmolPerL(
|
Settings.glucoseMeasurement ==
|
||||||
calculateFrom:
|
GlucoseMeasurement.mmolPerL ||
|
||||||
GlucoseMeasurement.mgPerDl),
|
[
|
||||||
keyboardType:
|
GlucoseDisplayMode.both,
|
||||||
const TextInputType.numberWithOptions(),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty &&
|
|
||||||
_mmolPerLController.text.trim().isEmpty) {
|
|
||||||
return 'How many mg/dl does the rate make up for?';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.both ||
|
|
||||||
glucoseDisplayMode ==
|
|
||||||
GlucoseDisplayMode.bothForDetail
|
GlucoseDisplayMode.bothForDetail
|
||||||
? IconButton(
|
].contains(Settings.glucoseDisplayMode)
|
||||||
onPressed: () => convertBetweenMgPerDlAndMmolPerL(
|
? Expanded(
|
||||||
calculateFrom: GlucoseMeasurement.mmolPerL),
|
flex: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL ? 2 : 1,
|
||||||
icon: const Icon(Icons.calculate),
|
child: NumberFormField(
|
||||||
)
|
label: 'per mmol/l',
|
||||||
: Container(),
|
suffix: 'mmol/l',
|
||||||
glucoseMeasurement == GlucoseMeasurement.mmolPerL ||
|
readOnly: Settings.glucoseMeasurement ==
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.both ||
|
GlucoseMeasurement.mgPerDl,
|
||||||
glucoseDisplayMode ==
|
showSteppers: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL,
|
||||||
GlucoseDisplayMode.bothForDetail
|
controller: _mmolPerLController,
|
||||||
? Expanded(
|
step: Settings.mmolPerLSteps,
|
||||||
child: TextFormField(
|
onChanged: convertBetweenMgPerDlAndMmolPerL,
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'per mmol/l',
|
|
||||||
suffixText: 'mmol/l',
|
|
||||||
),
|
),
|
||||||
controller: _mmolPerLController,
|
)
|
||||||
onChanged: (_) =>
|
: Container(),
|
||||||
convertBetweenMgPerDlAndMmolPerL(
|
],
|
||||||
calculateFrom:
|
),
|
||||||
GlucoseMeasurement.mmolPerL),
|
],
|
||||||
keyboardType:
|
),
|
||||||
const TextInputType.numberWithOptions(
|
],
|
||||||
decimal: true),
|
),
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty &&
|
|
||||||
_mgPerDlController.text.trim().isEmpty) {
|
|
||||||
return 'How many mmol/l does rhe rate make up for?';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.both ||
|
|
||||||
glucoseDisplayMode ==
|
|
||||||
GlucoseDisplayMode.bothForDetail
|
|
||||||
? IconButton(
|
|
||||||
onPressed: () => convertBetweenMgPerDlAndMmolPerL(
|
|
||||||
calculateFrom: GlucoseMeasurement.mgPerDl),
|
|
||||||
icon: const Icon(Icons.calculate),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: DetailBottomRow(
|
bottomNavigationBar: DetailBottomRow(
|
||||||
onCancel: handleCancelAction,
|
onCancel: handleCancelAction,
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
onAction:
|
||||||
|
_isSaving ? null : () => handleSaveAction(next: !_isFinalRate),
|
||||||
|
onMiddleAction: _isSaving || _isFinalRate
|
||||||
|
? null
|
||||||
|
: () => handleSaveAction(next: false),
|
||||||
|
actionText: _isFinalRate ? 'SAVE & CLOSE' : 'NEXT',
|
||||||
|
middleActionText: 'SAVE & CLOSE',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/settings.dart';
|
|
||||||
import 'package:diameter/utils/date_time_utils.dart';
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/models/bolus.dart';
|
import 'package:diameter/models/bolus.dart';
|
||||||
@ -13,7 +12,10 @@ class BolusListScreen extends StatefulWidget {
|
|||||||
final Function() reload;
|
final Function() reload;
|
||||||
|
|
||||||
const BolusListScreen(
|
const BolusListScreen(
|
||||||
{Key? key, required this.bolusProfile, this.bolusRates = const [], required this.reload})
|
{Key? key,
|
||||||
|
required this.bolusProfile,
|
||||||
|
this.bolusRates = const [],
|
||||||
|
required this.reload})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -21,6 +23,14 @@ class BolusListScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BolusListScreenState extends State<BolusListScreen> {
|
class _BolusListScreenState extends State<BolusListScreen> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void reload({String? message}) {
|
void reload({String? message}) {
|
||||||
widget.reload();
|
widget.reload();
|
||||||
|
|
||||||
@ -46,7 +56,7 @@ class _BolusListScreenState extends State<BolusListScreen> {
|
|||||||
id: bolus.id,
|
id: bolus.id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((message) => reload(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDelete(Bolus bolus) {
|
void onDelete(Bolus bolus) {
|
||||||
@ -55,8 +65,8 @@ class _BolusListScreenState extends State<BolusListScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(Bolus bolus) async {
|
void handleDeleteAction(Bolus bolus) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(bolus),
|
onConfirm: () => onDelete(bolus),
|
||||||
message: 'Are you sure you want to delete this Bolus Rate?',
|
message: 'Are you sure you want to delete this Bolus Rate?',
|
||||||
@ -70,8 +80,6 @@ class _BolusListScreenState extends State<BolusListScreen> {
|
|||||||
List<Bolus> bolusRates = widget.bolusRates;
|
List<Bolus> bolusRates = widget.bolusRates;
|
||||||
Bolus bolus = bolusRates[index];
|
Bolus bolus = bolusRates[index];
|
||||||
|
|
||||||
// TODO: use queries for all this
|
|
||||||
// check for gaps
|
|
||||||
if (index == 0 &&
|
if (index == 0 &&
|
||||||
(bolus.startTime.toLocal().hour != 0 || bolus.startTime.minute != 0)) {
|
(bolus.startTime.toLocal().hour != 0 || bolus.startTime.minute != 0)) {
|
||||||
return 'First Bolus of the day needs to start at 00:00';
|
return 'First Bolus of the day needs to start at 00:00';
|
||||||
@ -103,58 +111,122 @@ class _BolusListScreenState extends State<BolusListScreen> {
|
|||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
return 'This rate\'s time period overlaps with another one';
|
return 'This rate\'s time period overlaps with another one';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return widget.bolusRates.isNotEmpty
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
? Scrollbar(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
children: [
|
child: ListView.builder(
|
||||||
widget.bolusRates.isNotEmpty ? ListView.builder(
|
padding: const EdgeInsets.all(10.0),
|
||||||
shrinkWrap: true,
|
controller: _scrollController,
|
||||||
itemCount: widget.bolusRates.length,
|
shrinkWrap: true,
|
||||||
itemBuilder: (context, index) {
|
itemCount: widget.bolusRates.length,
|
||||||
final bolus = widget.bolusRates[index];
|
itemBuilder: (context, index) {
|
||||||
final error = validateTimePeriod(index);
|
final bolus = widget.bolusRates[index];
|
||||||
return ListTile(
|
final error = validateTimePeriod(index);
|
||||||
isThreeLine: true,
|
return Card(
|
||||||
tileColor: error != null ? Colors.red.shade100 : null,
|
child: Column(
|
||||||
onTap: () {
|
|
||||||
handleEditAction(bolus);
|
|
||||||
},
|
|
||||||
title: Text(
|
|
||||||
'${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}'),
|
|
||||||
subtitle: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
'${bolus.units} U per ${bolus.carbs}${nutritionMeasurement == NutritionMeasurement.grams ? ' g' : ' oz'} carbs/${glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDl : bolus.mmolPerL} ${glucoseMeasurement == GlucoseMeasurement.mgPerDl ? 'mg/dl' : 'mmol/l'}'),
|
|
||||||
error != null
|
error != null
|
||||||
? Text(error,
|
? Padding(
|
||||||
style: const TextStyle(color: Colors.red))
|
padding: const EdgeInsets.all(5.0),
|
||||||
: const Text('')
|
child: Row(
|
||||||
]),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
trailing: Row(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
Icon(Icons.warning,
|
||||||
children: [
|
color: Theme.of(context).errorColor),
|
||||||
IconButton(
|
Text(error,
|
||||||
icon: const Icon(
|
style: TextStyle(
|
||||||
Icons.delete,
|
color: Theme.of(context).errorColor)),
|
||||||
color: Colors.blue,
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
ListTile(
|
||||||
|
onTap: () {
|
||||||
|
handleEditAction(bolus);
|
||||||
|
},
|
||||||
|
isThreeLine: true,
|
||||||
|
title: Text(
|
||||||
|
'${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}'),
|
||||||
|
subtitle: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: (bolus.units > 0 && bolus.carbs > 0)
|
||||||
|
? [
|
||||||
|
Text((bolus.carbs / bolus.units)
|
||||||
|
.toStringAsPrecision(2)),
|
||||||
|
Text(
|
||||||
|
'${Settings.nutritionMeasurementSuffix} carbs per U',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: (bolus.units > 0 && bolus.carbs > 0)
|
||||||
|
? [
|
||||||
|
Text((bolus.units / bolus.carbs * 12)
|
||||||
|
.toStringAsPrecision(2)),
|
||||||
|
const Text('U per bread unit',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: (bolus.units > 0 &&
|
||||||
|
(bolus.mgPerDl ?? bolus.mmolPerL ?? 0) >
|
||||||
|
0)
|
||||||
|
? [
|
||||||
|
Text((((Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement
|
||||||
|
.mgPerDl
|
||||||
|
? bolus.mgPerDl
|
||||||
|
: bolus.mmolPerL ?? 0)! /
|
||||||
|
bolus.units))
|
||||||
|
.toString()),
|
||||||
|
Text(
|
||||||
|
'${Settings.glucoseMeasurementSuffix} per unit',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleDeleteAction(bolus),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => handleDeleteAction(bolus),
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
) : const Center(
|
|
||||||
child: Text('You have not created any Bolus Rates yet!'),
|
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
),
|
: const Center(
|
||||||
);
|
child: Text('You have not created any Bolus Rates yet!'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
import 'package:diameter/components/detail.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/components/forms/boolean_form_field.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/models/bolus.dart';
|
import 'package:diameter/models/bolus.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/screens/bolus/bolus_detail.dart';
|
import 'package:diameter/screens/bolus/bolus_detail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
import 'package:diameter/models/bolus_profile.dart';
|
import 'package:diameter/models/bolus_profile.dart';
|
||||||
import 'package:diameter/screens/bolus/bolus_list.dart';
|
import 'package:diameter/screens/bolus/bolus_list.dart';
|
||||||
|
|
||||||
@ -26,14 +27,15 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
|||||||
BolusProfile? _bolusProfile;
|
BolusProfile? _bolusProfile;
|
||||||
List<Bolus> _bolusRates = [];
|
List<Bolus> _bolusRates = [];
|
||||||
bool _isNew = true;
|
bool _isNew = true;
|
||||||
bool _isSaving = false;
|
|
||||||
|
|
||||||
final GlobalKey<FormState> _bolusProfileForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _bolusProfileForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
late FloatingActionButton addBolusButton;
|
late FloatingActionButton addBolusButton;
|
||||||
late IconButton refreshButton;
|
late IconButton refreshButton;
|
||||||
late IconButton closeButton;
|
late IconButton closeButton;
|
||||||
late DetailBottomRow detailBottomRow;
|
late DetailBottomRow detailBottomRow;
|
||||||
|
late DetailBottomRow detailBottomRowWhileSaving;
|
||||||
|
|
||||||
FloatingActionButton? actionButton;
|
FloatingActionButton? actionButton;
|
||||||
List<Widget> appBarActions = [];
|
List<Widget> appBarActions = [];
|
||||||
@ -73,10 +75,15 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
|||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: fix (saving button doesnt get disabled)
|
|
||||||
detailBottomRow = DetailBottomRow(
|
detailBottomRow = DetailBottomRow(
|
||||||
onCancel: handleCancelAction,
|
onCancel: handleCancelAction,
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
onAction: handleSaveAction,
|
||||||
|
onMiddleAction: () => handleSaveAction(close: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
detailBottomRowWhileSaving = DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
actionButton = null;
|
actionButton = null;
|
||||||
@ -84,6 +91,14 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
|||||||
bottomNav = detailBottomRow;
|
bottomNav = detailBottomRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_nameController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void reload({String? message}) {
|
void reload({String? message}) {
|
||||||
if (widget.id != 0) {
|
if (widget.id != 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -144,7 +159,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
|||||||
});
|
});
|
||||||
} else if (!_active &&
|
} else if (!_active &&
|
||||||
((_isNew && _activeCount == 0) ||
|
((_isNew && _activeCount == 0) ||
|
||||||
(_activeCount == 1 && _bolusProfile!.active))) {
|
(!_isNew && _activeCount == 1 && _bolusProfile!.active))) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
@ -176,25 +191,30 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
|||||||
TimeOfDay? suggestedStartTime;
|
TimeOfDay? suggestedStartTime;
|
||||||
TimeOfDay? suggestedEndTime;
|
TimeOfDay? suggestedEndTime;
|
||||||
|
|
||||||
_bolusRates.asMap().forEach((index, bolus) {
|
if (_bolusRates.isEmpty) {
|
||||||
if (suggestedStartTime == null && suggestedEndTime == null) {
|
suggestedStartTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
if (index == 0 &&
|
suggestedEndTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
(bolus.startTime.hour != 0 || bolus.startTime.minute != 0)) {
|
} else {
|
||||||
suggestedStartTime = const TimeOfDay(hour: 0, minute: 0);
|
_bolusRates.asMap().forEach((index, bolus) {
|
||||||
suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime);
|
if (suggestedStartTime == null && suggestedEndTime == null) {
|
||||||
} else if ((index == _bolusRates.length - 1) &&
|
if (index == 0 &&
|
||||||
(bolus.endTime.hour != 0 || bolus.endTime.minute != 0)) {
|
(bolus.startTime.hour != 0 || bolus.startTime.minute != 0)) {
|
||||||
suggestedStartTime = TimeOfDay.fromDateTime(bolus.endTime);
|
suggestedStartTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
suggestedEndTime = const TimeOfDay(hour: 0, minute: 0);
|
|
||||||
} else if (index != 0) {
|
|
||||||
var lastEndTime = _bolusRates[index - 1].endTime;
|
|
||||||
if (bolus.startTime.isAfter(lastEndTime)) {
|
|
||||||
suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime);
|
|
||||||
suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime);
|
suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime);
|
||||||
|
} else if ((index == _bolusRates.length - 1) &&
|
||||||
|
(bolus.endTime.hour != 0 || bolus.endTime.minute != 0)) {
|
||||||
|
suggestedStartTime = TimeOfDay.fromDateTime(bolus.endTime);
|
||||||
|
suggestedEndTime = const TimeOfDay(hour: 0, minute: 0);
|
||||||
|
} else if (index != 0) {
|
||||||
|
var lastEndTime = _bolusRates[index - 1].endTime;
|
||||||
|
if (bolus.startTime.isAfter(lastEndTime)) {
|
||||||
|
suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime);
|
||||||
|
suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@ -207,34 +227,49 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).then((message) => reload(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleSaveAction() async {
|
void handleSaveAction({bool close = false}) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = true;
|
bottomNav = detailBottomRowWhileSaving;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_bolusProfileForm.currentState!.validate()) {
|
if (_bolusProfileForm.currentState!.validate()) {
|
||||||
await checkActiveProfiles();
|
await checkActiveProfiles();
|
||||||
BolusProfile.put(
|
BolusProfile bolusProfile = BolusProfile(
|
||||||
BolusProfile(
|
id: widget.id,
|
||||||
id: widget.id,
|
name: _nameController.text,
|
||||||
name: _nameController.text,
|
active: _active,
|
||||||
active: _active,
|
notes: _notesController.text,
|
||||||
notes: _notesController.text,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
Navigator.pop(context, '${_isNew ? 'New' : ''} Bolus Profile saved');
|
BolusProfile.put(bolusProfile);
|
||||||
|
|
||||||
|
if (close) {
|
||||||
|
Navigator.pop(context,
|
||||||
|
['${_isNew ? 'New' : ''} Bolus Profile saved', bolusProfile]);
|
||||||
|
} else {
|
||||||
|
if (_isNew) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
BolusProfileDetailScreen(id: bolusProfile.id),
|
||||||
|
),
|
||||||
|
).then((result) => Navigator.pop(context, result));
|
||||||
|
} else {
|
||||||
|
reload(message: 'Bolus Profile saved');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = false;
|
bottomNav = detailBottomRow;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleCancelAction() {
|
void handleCancelAction() {
|
||||||
if (showConfirmationDialogOnCancel &&
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
(_isNew &&
|
(_isNew &&
|
||||||
(_active != widget.active ||
|
(_active != widget.active ||
|
||||||
_nameController.text != '' ||
|
_nameController.text != '' ||
|
||||||
@ -243,7 +278,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
|||||||
(_bolusProfile!.active != _active ||
|
(_bolusProfile!.active != _active ||
|
||||||
_bolusProfile!.name != _nameController.text ||
|
_bolusProfile!.name != _nameController.text ||
|
||||||
(_bolusProfile!.notes ?? '') != _notesController.text))) {
|
(_bolusProfile!.notes ?? '') != _notesController.text))) {
|
||||||
Dialogs.showCancelConfirmationDialog(
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
isNew: _isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
@ -282,52 +317,59 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
List<Widget> tabs = [
|
List<Widget> tabs = [
|
||||||
SingleChildScrollView(
|
Scrollbar(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: SingleChildScrollView(
|
||||||
children: [
|
controller: _scrollController,
|
||||||
StyledForm(
|
child: Column(
|
||||||
formState: _bolusProfileForm,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
fields: [
|
children: [
|
||||||
TextFormField(
|
FormWrapper(
|
||||||
controller: _nameController,
|
formState: _bolusProfileForm,
|
||||||
decoration: const InputDecoration(
|
fields: [
|
||||||
labelText: 'Name',
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
validator: (value) {
|
TextFormField(
|
||||||
if (value!.trim().isEmpty) {
|
decoration: const InputDecoration(
|
||||||
return 'Empty title';
|
labelText: 'Notes',
|
||||||
}
|
),
|
||||||
return null;
|
controller: _notesController,
|
||||||
},
|
keyboardType: TextInputType.multiline,
|
||||||
),
|
minLines: 2,
|
||||||
TextFormField(
|
maxLines: 5,
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Notes',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
),
|
||||||
controller: _notesController,
|
BooleanFormField(
|
||||||
keyboardType: TextInputType.multiline,
|
value: _active,
|
||||||
),
|
onChanged: (value) {
|
||||||
StyledBooleanFormField(
|
setState(() {
|
||||||
value: _active,
|
_active = value;
|
||||||
onChanged: (value) {
|
});
|
||||||
setState(() {
|
},
|
||||||
_active = value;
|
label: 'active',
|
||||||
});
|
),
|
||||||
},
|
],
|
||||||
label: 'active',
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!_isNew) {
|
if (!_isNew) {
|
||||||
tabs.add(
|
tabs.add(BolusListScreen(
|
||||||
BolusListScreen(bolusProfile: _bolusProfile!, bolusRates: _bolusRates, reload: reload));
|
bolusProfile: _bolusProfile!,
|
||||||
|
bolusRates: _bolusRates,
|
||||||
|
reload: reload));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
|
import 'package:diameter/models/bolus.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/models/bolus_profile.dart';
|
import 'package:diameter/models/bolus_profile.dart';
|
||||||
@ -14,18 +16,32 @@ class BolusProfileListScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
|
class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
List<BolusProfile> _bolusProfiles = [];
|
List<BolusProfile> _bolusProfiles = [];
|
||||||
Widget banner = Container();
|
Widget banner = Container();
|
||||||
bool pickActiveProfileMode = false;
|
|
||||||
|
final BolusProfile? _activeProfile = BolusProfile.getActive(DateTime.now());
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void reload({String? message}) {
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
pickActiveProfileMode = false;
|
|
||||||
_bolusProfiles = BolusProfile.getAll();
|
_bolusProfiles = BolusProfile.getAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
updateBanner();
|
updateBanner();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
var snackBar = SnackBar(
|
var snackBar = SnackBar(
|
||||||
@ -74,14 +90,38 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handleDuplicateAction(BolusProfile bolusProfile) async {
|
||||||
|
final copy = BolusProfile(
|
||||||
|
active: false,
|
||||||
|
name: 'Copy of ${bolusProfile.name}',
|
||||||
|
);
|
||||||
|
BolusProfile.put(copy);
|
||||||
|
|
||||||
|
final rates = Bolus.getAllForProfile(bolusProfile.id);
|
||||||
|
for (Bolus rate in rates) {
|
||||||
|
final bolus = Bolus(
|
||||||
|
endTime: rate.endTime,
|
||||||
|
startTime: rate.startTime,
|
||||||
|
units: rate.units,
|
||||||
|
carbs: rate.carbs,
|
||||||
|
mgPerDl: rate.mgPerDl,
|
||||||
|
mmolPerL: rate.mmolPerL,
|
||||||
|
);
|
||||||
|
bolus.bolusProfile.target = copy;
|
||||||
|
Bolus.put(bolus);
|
||||||
|
}
|
||||||
|
|
||||||
|
reload(message: 'Added copy of ${bolusProfile.name}');
|
||||||
|
}
|
||||||
|
|
||||||
void onDelete(BolusProfile bolusProfile) {
|
void onDelete(BolusProfile bolusProfile) {
|
||||||
BolusProfile.remove(bolusProfile.id);
|
BolusProfile.remove(bolusProfile.id);
|
||||||
reload(message: 'Bolus Profile deleted');
|
reload(message: 'Bolus Profile deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(BolusProfile bolusProfile) async {
|
void handleDeleteAction(BolusProfile bolusProfile) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(bolusProfile),
|
onConfirm: () => onDelete(bolusProfile),
|
||||||
message: 'Are you sure you want to delete this Bolus Profile?',
|
message: 'Are you sure you want to delete this Bolus Profile?',
|
||||||
@ -91,18 +131,25 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onPickActive(BolusProfile bolusProfile) {
|
void onPickActive(BolusProfile? bolusProfile) {
|
||||||
BolusProfile.setAllInactive;
|
if (bolusProfile != null) {
|
||||||
bolusProfile.active = true;
|
BolusProfile.setAllInactive;
|
||||||
BolusProfile.put(bolusProfile);
|
bolusProfile.active = true;
|
||||||
reload(
|
BolusProfile.put(bolusProfile);
|
||||||
message: '${bolusProfile.name} has been set as your active Profile');
|
reload(
|
||||||
|
message: '${bolusProfile.name} has been set as your active Profile');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handlePickActiveProfileAction() {
|
void handlePickActiveProfileAction() {
|
||||||
setState(() {
|
setState(() {
|
||||||
banner = MaterialBanner(
|
banner = MaterialBanner(
|
||||||
content: const Text('Click one of the profiles to active it.'),
|
content: AutoCompleteDropdownButton(
|
||||||
|
controller: TextEditingController(text: ''),
|
||||||
|
items: _bolusProfiles,
|
||||||
|
label: 'Default Basal Profile',
|
||||||
|
onChanged: onPickActive,
|
||||||
|
),
|
||||||
leading: const CircleAvatar(child: Icon(Icons.info)),
|
leading: const CircleAvatar(child: Icon(Icons.info)),
|
||||||
forceActionsBelow: true,
|
forceActionsBelow: true,
|
||||||
actions: [
|
actions: [
|
||||||
@ -112,7 +159,6 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
pickActiveProfileMode = true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,10 +166,10 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BolusProfileDetailScreen(
|
builder: (context) =>
|
||||||
id: bolusProfile?.id ?? 0, active: active),
|
BolusProfileDetailScreen(id: bolusProfile?.id ?? 0, active: active),
|
||||||
),
|
),
|
||||||
).then((message) => reload(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onNew(bool active) {
|
void onNew(bool active) {
|
||||||
@ -134,12 +180,6 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
|
|||||||
showDetailScreen(bolusProfile: bolusProfile);
|
showDetailScreen(bolusProfile: bolusProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -156,42 +196,63 @@ class _BolusProfileListScreenState extends State<BolusProfileListScreen> {
|
|||||||
children: [
|
children: [
|
||||||
banner,
|
banner,
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _bolusProfiles.isNotEmpty ? ListView.builder(
|
child: _bolusProfiles.isNotEmpty
|
||||||
itemCount: _bolusProfiles.length,
|
? Scrollbar(
|
||||||
itemBuilder: (context, index) {
|
controller: _scrollController,
|
||||||
final bolusProfile = _bolusProfiles[index];
|
child: ListView.builder(
|
||||||
return ListTile(
|
padding: const EdgeInsets.all(10.0),
|
||||||
tileColor: bolusProfile.active
|
controller: _scrollController,
|
||||||
? Colors.green.shade100
|
itemCount: _bolusProfiles.length,
|
||||||
: null,
|
itemBuilder: (context, index) {
|
||||||
onTap: () {
|
final bolusProfile = _bolusProfiles[index];
|
||||||
// TODO: make pick active profile visually distinct
|
String activeProfileText = bolusProfile.active
|
||||||
pickActiveProfileMode
|
? ' (Default Profile)'
|
||||||
? onPickActive(bolusProfile)
|
: bolusProfile.id == _activeProfile?.id
|
||||||
: onEdit(bolusProfile);
|
? ' (Current Active Profile)'
|
||||||
},
|
: '';
|
||||||
title: Text(
|
return Card(
|
||||||
bolusProfile.name,
|
child: ListTile(
|
||||||
|
selected: bolusProfile.active ||
|
||||||
|
bolusProfile.id == _activeProfile?.id,
|
||||||
|
onTap: () => onEdit(bolusProfile),
|
||||||
|
title: Text(
|
||||||
|
bolusProfile.name.toUpperCase() +
|
||||||
|
activeProfileText,
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: Text(bolusProfile.notes ?? ''),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.copy,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
handleDuplicateAction(bolusProfile),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
handleDeleteAction(bolusProfile),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text('You have not created any Bolus Profiles yet!'),
|
||||||
),
|
),
|
||||||
subtitle: Text(bolusProfile.notes!),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
onPressed: () =>
|
|
||||||
handleDeleteAction(bolusProfile),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
) : const Center(
|
|
||||||
child: Text('You have not created any Bolus Profiles yet!'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,166 +0,0 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/log_entry.dart';
|
|
||||||
import 'package:diameter/models/log_event.dart';
|
|
||||||
// import 'package:diameter/models/log_event_type.dart';
|
|
||||||
import 'package:diameter/screens/log/log_event_detail.dart';
|
|
||||||
import 'package:diameter/utils/date_time_utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
// import 'package:diameter/components/progress_indicator.dart';
|
|
||||||
|
|
||||||
class ActiveLogEventListScreen extends StatefulWidget {
|
|
||||||
static const String routeName = '/active-log-events';
|
|
||||||
|
|
||||||
final LogEntry? endLogEntry;
|
|
||||||
final Function()? onSetEndTime;
|
|
||||||
|
|
||||||
const ActiveLogEventListScreen(
|
|
||||||
{Key? key, this.endLogEntry, this.onSetEndTime})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ActiveLogEventListScreenState createState() =>
|
|
||||||
_ActiveLogEventListScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ActiveLogEventListScreenState extends State<ActiveLogEventListScreen> {
|
|
||||||
List<LogEvent> _activeLogEvents = [];
|
|
||||||
|
|
||||||
void refresh({String? message}) {
|
|
||||||
setState(() {
|
|
||||||
_activeLogEvents = LogEvent.getAllOngoing();
|
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
if (message != null) {
|
|
||||||
var snackBar = SnackBar(
|
|
||||||
content: Text(message),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
..removeCurrentSnackBar()
|
|
||||||
..showSnackBar(snackBar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void onStop(LogEvent event) async {
|
|
||||||
event.endTime = DateTime.now();
|
|
||||||
event.endLogEntry.target =
|
|
||||||
widget.endLogEntry ?? LogEntry(time: DateTime.now());
|
|
||||||
LogEvent.put(event);
|
|
||||||
refresh();
|
|
||||||
if (widget.onSetEndTime != null) {
|
|
||||||
widget.onSetEndTime!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleStopAction(LogEvent event) async {
|
|
||||||
if (showConfirmationDialogOnStopEvent) {
|
|
||||||
Dialogs.showConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
onConfirm: () => onStop(event),
|
|
||||||
message: 'Are you sure you want to end this Event?',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
onStop(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDelete(LogEvent event) {
|
|
||||||
LogEvent.remove(event.id);
|
|
||||||
refresh(message: 'Event deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleDeleteAction(LogEvent event) async {
|
|
||||||
if (showConfirmationDialogOnDelete) {
|
|
||||||
Dialogs.showConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
onConfirm: () => onDelete(event),
|
|
||||||
message: 'Are you sure you want to delete this Event?',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
onDelete(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// TODO: make action button instead of appbar
|
|
||||||
AppBar(
|
|
||||||
title: const Text('Active Events'),
|
|
||||||
primary: false,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => LogEventDetailScreen(
|
|
||||||
logEntry: widget.endLogEntry!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).then((message) => refresh(message: message));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(icon: const Icon(Icons.refresh), onPressed: refresh),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
_activeLogEvents.isNotEmpty ?
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: _activeLogEvents.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final event = _activeLogEvents[index];
|
|
||||||
return ListTile(
|
|
||||||
title: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(event.eventType.target?.value ?? ''),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'${DateTimeUtils.displayDateTime(event.time)}${event.hasEndTime ? ' - ${DateTimeUtils.displayDateTime(event.endTime)}' : ''}'),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
onPressed: () => handleStopAction(event),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
onPressed: () => handleDeleteAction(event),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
) : const Center(
|
|
||||||
child: Text('There are no currently ongoing events!'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,14 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/models/glucose_target.dart';
|
||||||
|
import 'package:diameter/models/log_bolus.dart';
|
||||||
import 'package:diameter/models/log_entry.dart';
|
import 'package:diameter/models/log_entry.dart';
|
||||||
|
import 'package:diameter/models/log_meal.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/screens/log/log_entry.dart';
|
import 'package:diameter/screens/log/log_entry/log_entry.dart';
|
||||||
import 'package:diameter/utils/date_time_utils.dart';
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
class LogScreen extends StatefulWidget {
|
class LogScreen extends StatefulWidget {
|
||||||
static const String routeName = '/log';
|
static const String routeName = '/log';
|
||||||
@ -15,11 +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;
|
||||||
|
|
||||||
void refresh({String? message}) {
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
final TextEditingController _dateController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
late DateTime _date;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_date = DateTime.now();
|
||||||
|
_dateController.text = DateTimeUtils.displayDate(_date);
|
||||||
|
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_dateController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_logEntryDailyMap = LogEntry.getDailyEntryMap();
|
_logEntries = LogEntry.getAllForDate(_date);
|
||||||
});
|
});
|
||||||
setState(() {
|
setState(() {
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
@ -36,12 +62,12 @@ class _LogScreenState extends State<LogScreen> {
|
|||||||
|
|
||||||
void onDelete(LogEntry logEntry) {
|
void onDelete(LogEntry logEntry) {
|
||||||
LogEntry.remove(logEntry.id);
|
LogEntry.remove(logEntry.id);
|
||||||
refresh(message: 'Log Entry deleted');
|
reload(message: 'Log Entry deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(LogEntry logEntry) async {
|
void handleDeleteAction(LogEntry logEntry) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(logEntry),
|
onConfirm: () => onDelete(logEntry),
|
||||||
message: 'Are you sure you want to delete this Log Entry?',
|
message: 'Are you sure you want to delete this Log Entry?',
|
||||||
@ -51,10 +77,14 @@ class _LogScreenState extends State<LogScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void onChangeDate(DateTime? date) {
|
||||||
void initState() {
|
if (date != null) {
|
||||||
super.initState();
|
setState(() {
|
||||||
refresh();
|
_date = DateTime(date.year, date.month, date.day);
|
||||||
|
_dateController.text = DateTimeUtils.displayDate(date);
|
||||||
|
});
|
||||||
|
reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -64,92 +94,235 @@ class _LogScreenState extends State<LogScreen> {
|
|||||||
title: const Text('Log Entries'),
|
title: const Text('Log Entries'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: refresh,
|
onPressed: () => onChangeDate(DateTime.now()),
|
||||||
icon: const Icon(Icons.refresh)
|
icon: const Icon(Icons.today)),
|
||||||
),
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: LogScreen.routeName),
|
drawer: const Navigation(currentLocation: LogScreen.routeName),
|
||||||
body: Column(
|
body: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: <Widget>[
|
Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: SingleChildScrollView(
|
IconButton(
|
||||||
child: _logEntryDailyMap.isNotEmpty ? ListView.builder(
|
onPressed: _date.isAtSameMomentAs(DateTime(2000, 1, 1))
|
||||||
shrinkWrap: true,
|
? null
|
||||||
padding: const EdgeInsets.all(10.0),
|
: () =>
|
||||||
itemCount: _logEntryDailyMap.length,
|
onChangeDate(_date.subtract(const Duration(days: 1))),
|
||||||
itemBuilder: (context, dateIndex) {
|
icon: const Icon(Icons.arrow_back),
|
||||||
List<DateTime> dateList = _logEntryDailyMap.keys.toList();
|
|
||||||
final date = dateList[dateIndex];
|
|
||||||
final entryList = _logEntryDailyMap[date];
|
|
||||||
return ListBody(
|
|
||||||
children: [
|
|
||||||
Text(DateTimeUtils.displayDate(date)),
|
|
||||||
entryList != null && entryList.isNotEmpty
|
|
||||||
? ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: entryList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final logEntry = entryList[index];
|
|
||||||
return ListTile(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
LogEntryScreen(
|
|
||||||
id: logEntry.id),
|
|
||||||
),
|
|
||||||
).then((message) => refresh(
|
|
||||||
message: message));
|
|
||||||
},
|
|
||||||
title: Text(
|
|
||||||
DateTimeUtils.displayTime(
|
|
||||||
logEntry.time)),
|
|
||||||
// TODO: add additional fields (event icons...)
|
|
||||||
// TODO: display glucose in colors according to target settings
|
|
||||||
subtitle: Text(logEntry
|
|
||||||
.mgPerDl !=
|
|
||||||
null
|
|
||||||
? '${logEntry.mgPerDl.toString()} mg/dl'
|
|
||||||
: ''),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize:
|
|
||||||
MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () =>
|
|
||||||
handleDeleteAction(
|
|
||||||
logEntry),
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.blue),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
) : Container(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
) : const Center(
|
|
||||||
child: Text('You have not created any Log Entries yet!'),
|
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final newTime = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _date,
|
||||||
|
firstDate: DateTime(2000, 1, 1),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
onChangeDate(newTime);
|
||||||
|
},
|
||||||
|
child: Expanded(
|
||||||
|
child: Text(
|
||||||
|
DateTimeUtils.displayDate(_date).toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed:
|
||||||
|
_date.add(const Duration(days: 1)).isBefore(DateTime.now())
|
||||||
|
? () => onChangeDate(_date.add(const Duration(days: 1)))
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.arrow_forward),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _logEntries.isNotEmpty
|
||||||
|
? Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: _logEntries.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
LogEntry logEntry = _logEntries[index];
|
||||||
|
double bolus =
|
||||||
|
LogBolus.getTotalBolusForEntry(logEntry.id);
|
||||||
|
double carbs =
|
||||||
|
LogMeal.getTotalCarbsForEntry(logEntry.id);
|
||||||
|
TextStyle glucoseStyle = TextStyle(
|
||||||
|
color: GlucoseTarget.getColorForGlucose(
|
||||||
|
mgPerDl: logEntry.mgPerDl ?? 0,
|
||||||
|
mmolPerL: logEntry.mmolPerL ?? 0));
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
LogEntryScreen(id: logEntry.id),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
DateTimeUtils.displayTime(logEntry.time),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: logEntry.mgPerDl != null &&
|
||||||
|
(Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement
|
||||||
|
.mgPerDl ||
|
||||||
|
Settings.glucoseDisplayMode ==
|
||||||
|
GlucoseDisplayMode.both ||
|
||||||
|
Settings.glucoseDisplayMode ==
|
||||||
|
GlucoseDisplayMode
|
||||||
|
.bothForList)
|
||||||
|
? [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
logEntry.mgPerDl.toString(),
|
||||||
|
style: glucoseStyle),
|
||||||
|
logEntry.glucoseTrend != null
|
||||||
|
? Transform.rotate(
|
||||||
|
angle: logEntry
|
||||||
|
.glucoseTrend! *
|
||||||
|
math.pi /
|
||||||
|
180,
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_upward,
|
||||||
|
color: glucoseStyle
|
||||||
|
.color,
|
||||||
|
size: 16.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'mg/dl',
|
||||||
|
textScaleFactor: 0.75,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: logEntry.mmolPerL != null &&
|
||||||
|
(Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement
|
||||||
|
.mmolPerL ||
|
||||||
|
Settings.glucoseDisplayMode ==
|
||||||
|
GlucoseDisplayMode.both ||
|
||||||
|
Settings.glucoseDisplayMode ==
|
||||||
|
GlucoseDisplayMode
|
||||||
|
.bothForList)
|
||||||
|
? [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
logEntry.mmolPerL
|
||||||
|
.toString(),
|
||||||
|
style: glucoseStyle),
|
||||||
|
logEntry.glucoseTrend != null
|
||||||
|
? Transform.rotate(
|
||||||
|
angle: logEntry
|
||||||
|
.glucoseTrend! *
|
||||||
|
math.pi /
|
||||||
|
180,
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_upward,
|
||||||
|
color: glucoseStyle
|
||||||
|
.color,
|
||||||
|
size: 16.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'mmol/l',
|
||||||
|
textScaleFactor: 0.75,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: (bolus > 0)
|
||||||
|
? [
|
||||||
|
Text(bolus.toStringAsPrecision(3)),
|
||||||
|
const Text('U',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: (carbs > 0)
|
||||||
|
? [
|
||||||
|
Text(carbs.toStringAsPrecision(3)),
|
||||||
|
Text(
|
||||||
|
'${Settings.nutritionMeasurementSuffix} carbs',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => handleDeleteAction(logEntry),
|
||||||
|
icon: const Icon(Icons.delete,
|
||||||
|
color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text(
|
||||||
|
'You have not created any Log Entries for this date yet!'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// TODO: add button for active events
|
|
||||||
floatingActionButton: FloatingActionButton(
|
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((message) => refresh(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
|
@ -1,281 +0,0 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
|
||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/log_entry.dart';
|
|
||||||
import 'package:diameter/navigation.dart';
|
|
||||||
import 'package:diameter/screens/log/log_entry_form.dart';
|
|
||||||
import 'package:diameter/screens/log/log_event_detail.dart';
|
|
||||||
import 'package:diameter/screens/log/log_event_list.dart';
|
|
||||||
import 'package:diameter/screens/log/log_meal_detail.dart';
|
|
||||||
import 'package:diameter/screens/log/log_meal_list.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LogEntryScreen extends StatefulWidget {
|
|
||||||
static const String routeName = '/log-entry';
|
|
||||||
final int id;
|
|
||||||
|
|
||||||
const LogEntryScreen({Key? key, this.id = 0}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LogEntryScreenState createState() => _LogEntryScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogEntryScreenState extends State<LogEntryScreen> {
|
|
||||||
LogEntry? _logEntry;
|
|
||||||
bool _isNew = true;
|
|
||||||
bool _isSaving = false;
|
|
||||||
|
|
||||||
final GlobalKey<FormState> logEntryForm = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
late FloatingActionButton addMealButton;
|
|
||||||
late FloatingActionButton addEventButton;
|
|
||||||
late IconButton refreshButton;
|
|
||||||
late IconButton closeButton;
|
|
||||||
late DetailBottomRow detailBottomRow;
|
|
||||||
|
|
||||||
FloatingActionButton? actionButton;
|
|
||||||
List<Widget> appBarActions = [];
|
|
||||||
DetailBottomRow? bottomNav;
|
|
||||||
|
|
||||||
final formDataControllers = <String, TextEditingController>{
|
|
||||||
'time': TextEditingController(text: ''),
|
|
||||||
'mgPerDl': TextEditingController(text: ''),
|
|
||||||
'mmolPerL': TextEditingController(text: ''),
|
|
||||||
'bolusGlucose': TextEditingController(text: ''),
|
|
||||||
'delayedBolusRate': TextEditingController(text: ''),
|
|
||||||
'delayedBolusDuration': TextEditingController(text: ''),
|
|
||||||
'notes': TextEditingController(text: ''),
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
reload();
|
|
||||||
|
|
||||||
if (_logEntry != null) {
|
|
||||||
formDataControllers['time']!.text = _logEntry!.time.toString();
|
|
||||||
formDataControllers['mgPerDl']!.text =
|
|
||||||
(_logEntry!.mgPerDl ?? '').toString();
|
|
||||||
formDataControllers['mmolPerL']!.text =
|
|
||||||
(_logEntry!.mmolPerL ?? '').toString();
|
|
||||||
formDataControllers['bolusGlucose']!.text =
|
|
||||||
(_logEntry!.bolusGlucose ?? '').toString();
|
|
||||||
formDataControllers['delayedBolusRate']!.text =
|
|
||||||
(_logEntry!.delayedBolusRate ?? '').toString();
|
|
||||||
formDataControllers['delayedBolusDuration']!.text =
|
|
||||||
(_logEntry!.delayedBolusDuration ?? '').toString();
|
|
||||||
formDataControllers['notes']!.text = _logEntry!.notes ?? '';
|
|
||||||
} else {
|
|
||||||
formDataControllers['time']!.text = DateTime.now().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
addMealButton = FloatingActionButton(
|
|
||||||
onPressed: handleAddNewMeal,
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
);
|
|
||||||
|
|
||||||
addEventButton = FloatingActionButton(
|
|
||||||
onPressed: handleAddNewEvent,
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
);
|
|
||||||
|
|
||||||
refreshButton = IconButton(
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
onPressed: reload,
|
|
||||||
);
|
|
||||||
|
|
||||||
closeButton = IconButton(
|
|
||||||
onPressed: handleCancelAction,
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
);
|
|
||||||
|
|
||||||
detailBottomRow = DetailBottomRow(
|
|
||||||
onCancel: handleCancelAction,
|
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
|
||||||
);
|
|
||||||
|
|
||||||
actionButton = null;
|
|
||||||
appBarActions = [closeButton];
|
|
||||||
bottomNav = detailBottomRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
void reload({String? message}) {
|
|
||||||
if (widget.id != 0) {
|
|
||||||
setState(() {
|
|
||||||
_logEntry = LogEntry.get(widget.id);
|
|
||||||
});
|
|
||||||
_isNew = _logEntry == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
if (message != null) {
|
|
||||||
var snackBar = SnackBar(
|
|
||||||
content: Text(message),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
..removeCurrentSnackBar()
|
|
||||||
..showSnackBar(snackBar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleAddNewMeal() async {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return LogMealDetailScreen(logEntry: _logEntry!);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).then((message) => reload(message: message));
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleAddNewEvent() async {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return LogEventDetailScreen(logEntry: _logEntry!);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).then((message) => reload(message: message));
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleSaveAction() async {
|
|
||||||
setState(() {
|
|
||||||
_isSaving = true;
|
|
||||||
});
|
|
||||||
if (logEntryForm.currentState!.validate()) {
|
|
||||||
LogEntry.put(LogEntry(
|
|
||||||
id: widget.id,
|
|
||||||
time: DateTime.parse(formDataControllers['time']!.text),
|
|
||||||
mgPerDl: int.tryParse(formDataControllers['mgPerDl']!.text),
|
|
||||||
mmolPerL: double.tryParse(formDataControllers['mmolPerL']!.text),
|
|
||||||
bolusGlucose:
|
|
||||||
double.tryParse(formDataControllers['delayedBolusRate']!.text),
|
|
||||||
delayedBolusDuration:
|
|
||||||
int.tryParse(formDataControllers['delayedBolusDuration']!.text),
|
|
||||||
delayedBolusRate:
|
|
||||||
double.tryParse(formDataControllers['delayedBolusRate']!.text),
|
|
||||||
notes: formDataControllers['notes']!.text,
|
|
||||||
));
|
|
||||||
Navigator.pushReplacementNamed(context, '/log',
|
|
||||||
arguments: '${_isNew ? 'New' : ''} Log Entry Saved');
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_isSaving = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleCancelAction() {
|
|
||||||
if (showConfirmationDialogOnCancel &&
|
|
||||||
((_isNew &&
|
|
||||||
(int.tryParse(formDataControllers['mgPerDl']?.text ?? '') !=
|
|
||||||
null ||
|
|
||||||
double.tryParse(formDataControllers['mmolPerL']?.text ?? '') !=
|
|
||||||
null ||
|
|
||||||
double.tryParse(formDataControllers['bolusGlucose']?.text ?? '') !=
|
|
||||||
null ||
|
|
||||||
int.tryParse(formDataControllers['delayedBolusDuration']?.text ?? '') !=
|
|
||||||
null ||
|
|
||||||
double.tryParse(formDataControllers['delayedBolusRate']?.text ?? '') !=
|
|
||||||
null ||
|
|
||||||
formDataControllers['notes']?.text != '')) ||
|
|
||||||
(!_isNew &&
|
|
||||||
(int.tryParse(formDataControllers['mgPerDl']?.text ?? '') !=
|
|
||||||
_logEntry!.mgPerDl ||
|
|
||||||
double.tryParse(formDataControllers['mmolPerL']?.text ?? '') !=
|
|
||||||
_logEntry!.mmolPerL ||
|
|
||||||
double.tryParse(formDataControllers['bolusGlucose']?.text ?? '') !=
|
|
||||||
_logEntry!.bolusGlucose ||
|
|
||||||
int.tryParse(
|
|
||||||
formDataControllers['delayedBolusDuration']?.text ??
|
|
||||||
'') !=
|
|
||||||
_logEntry!.delayedBolusDuration ||
|
|
||||||
double.tryParse(formDataControllers['delayedBolusRate']?.text ?? '') !=
|
|
||||||
_logEntry!.delayedBolusRate ||
|
|
||||||
formDataControllers['notes']?.text !=
|
|
||||||
(_logEntry!.notes ?? ''))))) {
|
|
||||||
Dialogs.showCancelConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
isNew: _isNew,
|
|
||||||
onSave: handleSaveAction,
|
|
||||||
onDiscard: (context) => Navigator.pushReplacementNamed(context, '/log'),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Navigator.pushReplacementNamed(context, '/log',
|
|
||||||
arguments: '${_isNew ? 'New' : ''} Log Entry Saved');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void renderTabButtons(index) {
|
|
||||||
if (_logEntry != null) {
|
|
||||||
setState(() {
|
|
||||||
switch (index) {
|
|
||||||
case 1:
|
|
||||||
actionButton = addMealButton;
|
|
||||||
appBarActions = [refreshButton, closeButton];
|
|
||||||
bottomNav = null;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
actionButton = addEventButton;
|
|
||||||
appBarActions = [refreshButton, closeButton];
|
|
||||||
bottomNav = null;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
actionButton = null;
|
|
||||||
appBarActions = [closeButton];
|
|
||||||
bottomNav = detailBottomRow;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return DefaultTabController(
|
|
||||||
length: _isNew ? 1 : 3,
|
|
||||||
child: Builder(builder: (BuildContext context) {
|
|
||||||
final TabController tabController = DefaultTabController.of(context)!;
|
|
||||||
tabController.addListener(() {
|
|
||||||
renderTabButtons(tabController.index);
|
|
||||||
});
|
|
||||||
List<Widget> tabs = [
|
|
||||||
LogEntryForm(
|
|
||||||
formState: logEntryForm, controllers: formDataControllers),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!_isNew) {
|
|
||||||
tabs.add(LogMealListScreen(logEntry: _logEntry!, reload: reload));
|
|
||||||
tabs.add(LogEventListScreen(logEntry: _logEntry!, reload: reload));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(_isNew ? 'New Log Entry' : 'Edit Log Entry'),
|
|
||||||
bottom: _isNew
|
|
||||||
? PreferredSize(child: Container(), preferredSize: Size.zero)
|
|
||||||
: const TabBar(
|
|
||||||
tabs: [
|
|
||||||
Tab(text: 'GENERAL'),
|
|
||||||
Tab(text: 'MEALS'),
|
|
||||||
Tab(text: 'EVENTS'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: appBarActions,
|
|
||||||
),
|
|
||||||
drawer: const Navigation(currentLocation: LogEntryScreen.routeName),
|
|
||||||
body: TabBarView(
|
|
||||||
children: tabs,
|
|
||||||
),
|
|
||||||
bottomNavigationBar: bottomNav,
|
|
||||||
floatingActionButton: actionButton,
|
|
||||||
floatingActionButtonLocation:
|
|
||||||
FloatingActionButtonLocation.endFloat,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
843
lib/screens/log/log_entry/log_bolus_detail.dart
Normal file
843
lib/screens/log/log_entry/log_bolus_detail.dart
Normal file
@ -0,0 +1,843 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:diameter/components/detail.dart';
|
||||||
|
import 'package:diameter/components/forms/boolean_form_field.dart';
|
||||||
|
import 'package:diameter/components/forms/number_form_field.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
|
import 'package:diameter/models/bolus.dart';
|
||||||
|
import 'package:diameter/models/log_bolus.dart';
|
||||||
|
import 'package:diameter/models/log_entry.dart';
|
||||||
|
import 'package:diameter/models/log_meal.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_meal_detail.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
enum BolusType {
|
||||||
|
meal,
|
||||||
|
glucose,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GlucoseParameter {
|
||||||
|
mgdlCurrent,
|
||||||
|
mgdlTarget,
|
||||||
|
mgdlCorrection,
|
||||||
|
mmolCurrent,
|
||||||
|
mmolTarget,
|
||||||
|
mmolCorrection,
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogBolusDetailScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/log-bolus';
|
||||||
|
|
||||||
|
final int logEntryId;
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const LogBolusDetailScreen({Key? key, this.logEntryId = 0, this.id = 0})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogBolusDetailScreenState createState() => _LogBolusDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogBolusDetailScreenState extends State<LogBolusDetailScreen> {
|
||||||
|
LogEntry? _logEntry;
|
||||||
|
LogBolus? _logBolus;
|
||||||
|
|
||||||
|
bool _isNew = true;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
final GlobalKey<FormState> _logBolusForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
final _unitsController = TextEditingController(text: '');
|
||||||
|
final _carbsController = TextEditingController(text: '');
|
||||||
|
final _mgPerDlCurrentController = TextEditingController(text: '');
|
||||||
|
final _mgPerDlTargetController = TextEditingController(text: '');
|
||||||
|
final _mgPerDlCorrectionController = TextEditingController(text: '');
|
||||||
|
final _mmolPerLCurrentController = TextEditingController(text: '');
|
||||||
|
final _mmolPerLTargetController = TextEditingController(text: '');
|
||||||
|
final _mmolPerLCorrectionController = TextEditingController(text: '');
|
||||||
|
final _delayController = TextEditingController(text: '');
|
||||||
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
final _delayedUnitsController = TextEditingController(text: '');
|
||||||
|
final _immediateUnitsController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
final _mealController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
bool _setManually = false;
|
||||||
|
BolusType _bolusType = BolusType.meal;
|
||||||
|
LogMeal? _meal;
|
||||||
|
Bolus? _rate;
|
||||||
|
double _delayPercentage = 0;
|
||||||
|
|
||||||
|
List<LogMeal> _logMeals = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
|
||||||
|
_logEntry = LogEntry.get(widget.logEntryId);
|
||||||
|
_logMeals = LogMeal.getRecentWithoutBolus(widget.logEntryId);
|
||||||
|
|
||||||
|
if (widget.id != 0) {
|
||||||
|
_carbsController.text = (_logBolus!.carbs ?? '').toString();
|
||||||
|
_delayController.text = (_logBolus!.delay ?? '').toString();
|
||||||
|
_notesController.text = _logBolus!.notes ?? '';
|
||||||
|
_setManually = _logBolus!.setManually;
|
||||||
|
_meal = _logBolus!.meal.target;
|
||||||
|
_mealController.text = (_meal ?? '').toString();
|
||||||
|
_rate = _logBolus!.rate.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rate ??= Bolus.getRateForTime(_logEntry?.time);
|
||||||
|
|
||||||
|
_mgPerDlCurrentController.text = (_logBolus?.mgPerDlCurrent ??
|
||||||
|
(LogEntry.hasUncorrectedGlucose(widget.logEntryId)
|
||||||
|
? _logEntry?.mgPerDl ?? 0
|
||||||
|
: 0))
|
||||||
|
.toString();
|
||||||
|
_mgPerDlTargetController.text =
|
||||||
|
(_logBolus?.mgPerDlTarget ?? Settings.targetMgPerDl).toString();
|
||||||
|
_mgPerDlCorrectionController.text = (_logBolus?.mgPerDlCorrection ??
|
||||||
|
max(
|
||||||
|
(int.tryParse(_mgPerDlCurrentController.text) ?? 0) -
|
||||||
|
(int.tryParse(_mgPerDlTargetController.text) ?? 0),
|
||||||
|
0))
|
||||||
|
.toString();
|
||||||
|
_mmolPerLCurrentController.text = (_logBolus?.mmolPerLCurrent ??
|
||||||
|
(LogEntry.hasUncorrectedGlucose(widget.logEntryId)
|
||||||
|
? _logEntry?.mmolPerL ?? 0
|
||||||
|
: 0))
|
||||||
|
.toString();
|
||||||
|
_mmolPerLTargetController.text =
|
||||||
|
(_logBolus?.mmolPerLTarget ?? Settings.targetMmolPerL).toString();
|
||||||
|
_mmolPerLCorrectionController.text = (_logBolus?.mmolPerLCorrection ??
|
||||||
|
max(
|
||||||
|
(double.tryParse(_mmolPerLCurrentController.text) ?? 0) -
|
||||||
|
(double.tryParse(_mmolPerLTargetController.text) ?? 0),
|
||||||
|
0))
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
_unitsController.text = (_logBolus?.units ??
|
||||||
|
(_rate != null && !_setManually
|
||||||
|
? ((int.tryParse(_mgPerDlCorrectionController.text) ?? 0) /
|
||||||
|
((_rate!.mgPerDl ?? 0) / _rate!.units))
|
||||||
|
: 0))
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
if (widget.id == 0 && LogEntry.hasUncorrectedGlucose(widget.logEntryId)) {
|
||||||
|
_bolusType = BolusType.glucose;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBolus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_unitsController.dispose();
|
||||||
|
_carbsController.dispose();
|
||||||
|
_mgPerDlCurrentController.dispose();
|
||||||
|
_mgPerDlTargetController.dispose();
|
||||||
|
_mgPerDlCorrectionController.dispose();
|
||||||
|
_mmolPerLCurrentController.dispose();
|
||||||
|
_mmolPerLTargetController.dispose();
|
||||||
|
_mmolPerLCorrectionController.dispose();
|
||||||
|
_delayController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
_delayedUnitsController.dispose();
|
||||||
|
_immediateUnitsController.dispose();
|
||||||
|
_mealController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_logBolus = LogBolus.get(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _logBolus == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateLogMeal(LogMeal? value) {
|
||||||
|
setState(() {
|
||||||
|
_meal = value;
|
||||||
|
_mealController.text = (_meal ?? '').toString();
|
||||||
|
});
|
||||||
|
if (_meal != null) {
|
||||||
|
if (_meal!.totalCarbs != null) {
|
||||||
|
_carbsController.text = (_meal!.totalCarbs).toString();
|
||||||
|
}
|
||||||
|
if (_meal!.meal.hasValue) {
|
||||||
|
if (_meal!.meal.target!.delayedBolusDuration != null) {
|
||||||
|
_delayController.text =
|
||||||
|
(_meal!.meal.target?.delayedBolusDuration).toString();
|
||||||
|
}
|
||||||
|
if (_meal!.meal.target!.delayedBolusDuration != null) {
|
||||||
|
_delayPercentage = _meal!.meal.target!.delayedBolusPercentage!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
calculateBolus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSelectMeal(LogMeal? meal) {
|
||||||
|
updateLogMeal(meal);
|
||||||
|
if (meal != null && meal.totalCarbs != null) {
|
||||||
|
setState(() {
|
||||||
|
_carbsController.text = meal.totalCarbs.toString();
|
||||||
|
calculateBolus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void calculateBolus() {
|
||||||
|
if (_rate != null && !_setManually) {
|
||||||
|
double? units;
|
||||||
|
|
||||||
|
if (_bolusType == BolusType.glucose) {
|
||||||
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl) {
|
||||||
|
units = (int.tryParse(_mgPerDlCorrectionController.text) ?? 0) /
|
||||||
|
(_rate!.mgPerDl ?? 1) /
|
||||||
|
_rate!.units;
|
||||||
|
}
|
||||||
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL) {
|
||||||
|
units = (int.tryParse(_mmolPerLCorrectionController.text) ?? 0) /
|
||||||
|
(_rate!.mmolPerL ?? 1) /
|
||||||
|
_rate!.units;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_bolusType == BolusType.meal) {
|
||||||
|
units = (double.tryParse(_carbsController.text) ?? 0) /
|
||||||
|
(_rate!.carbs / _rate!.units);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDelayedRatio(totalUnitsUpdate: units);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChangeGlucose() {
|
||||||
|
int? mgPerDlCurrent;
|
||||||
|
int? mgPerDlTarget;
|
||||||
|
int? mgPerDlCorrection;
|
||||||
|
|
||||||
|
double? mmolPerLCurrent;
|
||||||
|
double? mmolPerLTarget;
|
||||||
|
double? mmolPerLCorrection;
|
||||||
|
|
||||||
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl &&
|
||||||
|
_mgPerDlCurrentController.text != '' &&
|
||||||
|
_mgPerDlTargetController.text != '') {
|
||||||
|
mgPerDlCurrent = int.tryParse(_mgPerDlCurrentController.text);
|
||||||
|
mgPerDlTarget = int.tryParse(_mgPerDlTargetController.text);
|
||||||
|
mgPerDlCorrection = max((mgPerDlCurrent ?? 0) - (mgPerDlTarget ?? 0), 0);
|
||||||
|
}
|
||||||
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL &&
|
||||||
|
_mmolPerLCurrentController.text != '') {
|
||||||
|
mmolPerLCurrent = double.tryParse(_mmolPerLCurrentController.text);
|
||||||
|
mmolPerLTarget = double.tryParse(_mmolPerLTargetController.text);
|
||||||
|
mmolPerLCorrection =
|
||||||
|
max((mmolPerLCurrent ?? 0) - (mmolPerLTarget ?? 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mgPerDlCurrent != null && mmolPerLCurrent == null) ||
|
||||||
|
(mgPerDlTarget != null && mmolPerLTarget == null) ||
|
||||||
|
(mgPerDlCorrection != null && mmolPerLCorrection == null)) {
|
||||||
|
setState(() {
|
||||||
|
_mgPerDlCorrectionController.text = (mgPerDlCorrection ?? 0).toString();
|
||||||
|
_mmolPerLCurrentController.text =
|
||||||
|
Utils.convertMgPerDlToMmolPerL(mgPerDlCurrent ?? 0).toString();
|
||||||
|
_mmolPerLTargetController.text =
|
||||||
|
Utils.convertMgPerDlToMmolPerL(mgPerDlTarget ?? 0).toString();
|
||||||
|
_mmolPerLCorrectionController.text =
|
||||||
|
Utils.convertMgPerDlToMmolPerL(mgPerDlCorrection ?? 0).toString();
|
||||||
|
calculateBolus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ((mmolPerLCurrent != null && mgPerDlCurrent == null) ||
|
||||||
|
(mmolPerLTarget != null && mgPerDlTarget == null) ||
|
||||||
|
(mmolPerLCorrection != null && mgPerDlCorrection == null)) {
|
||||||
|
setState(() {
|
||||||
|
_mmolPerLCurrentController.text = (mmolPerLCorrection ?? 0).toString();
|
||||||
|
_mgPerDlCurrentController.text =
|
||||||
|
Utils.convertMmolPerLToMgPerDl(mmolPerLCurrent ?? 0).toString();
|
||||||
|
_mgPerDlTargetController.text =
|
||||||
|
Utils.convertMmolPerLToMgPerDl(mmolPerLTarget ?? 0).toString();
|
||||||
|
_mgPerDlCorrectionController.text =
|
||||||
|
Utils.convertMmolPerLToMgPerDl(mmolPerLCorrection ?? 0).toString();
|
||||||
|
calculateBolus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDelayedRatio({
|
||||||
|
double? totalUnitsUpdate,
|
||||||
|
double? delayedUnitsUpdate,
|
||||||
|
double? immediateUnitsUpdate,
|
||||||
|
double? percentageUpdate
|
||||||
|
}) {
|
||||||
|
int precision = Utils.getFractionDigitsLength(Settings.insulinSteps);
|
||||||
|
|
||||||
|
double? totalUnits;
|
||||||
|
double? delayedUnits;
|
||||||
|
double? immediateUnits;
|
||||||
|
|
||||||
|
if (totalUnitsUpdate != null) {
|
||||||
|
totalUnits = Utils.roundToMultipleOfBase(totalUnitsUpdate, Settings.insulinSteps);
|
||||||
|
} else if (double.tryParse(_unitsController.text) != null) {
|
||||||
|
totalUnits = Utils.roundToMultipleOfBase(double.tryParse(_unitsController.text)!, Settings.insulinSteps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayedUnitsUpdate != null) {
|
||||||
|
delayedUnits = Utils.roundToMultipleOfBase(delayedUnitsUpdate, Settings.insulinSteps);
|
||||||
|
} else if (double.tryParse(_delayedUnitsController.text) != null) {
|
||||||
|
delayedUnits = Utils.roundToMultipleOfBase(double.tryParse(_delayedUnitsController.text)!, Settings.insulinSteps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (immediateUnitsUpdate != null) {
|
||||||
|
immediateUnits = Utils.roundToMultipleOfBase(immediateUnitsUpdate, Settings.insulinSteps);
|
||||||
|
} else if (double.tryParse(_immediateUnitsController.text) != null) {
|
||||||
|
immediateUnits = Utils.roundToMultipleOfBase(double.tryParse(_immediateUnitsController.text)!, Settings.insulinSteps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalUnits == null) {
|
||||||
|
if (percentageUpdate != null) {
|
||||||
|
if (immediateUnits != null) {
|
||||||
|
totalUnits = immediateUnits / (100 - percentageUpdate) * 100;
|
||||||
|
} else if (delayedUnits != null) {
|
||||||
|
totalUnits = delayedUnits / percentageUpdate * 100;
|
||||||
|
}
|
||||||
|
} else if (delayedUnits != null && immediateUnits != null) {
|
||||||
|
totalUnits = Utils.addDoublesWithPrecision(
|
||||||
|
delayedUnits, immediateUnits, precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalUnits != null) {
|
||||||
|
totalUnits =
|
||||||
|
Utils.roundToMultipleOfBase(totalUnits, Settings.insulinSteps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_unitsController.text = Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
totalUnits ?? 0, Settings.insulinSteps);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalUnits != null) {
|
||||||
|
double percentage = percentageUpdate ?? _delayPercentage;
|
||||||
|
|
||||||
|
if (totalUnitsUpdate != null || percentageUpdate != null) {
|
||||||
|
immediateUnits = Utils.roundToMultipleOfBase(
|
||||||
|
totalUnits * (100 - percentage) / 100, Settings.insulinSteps);
|
||||||
|
} else if (delayedUnitsUpdate != null) {
|
||||||
|
immediateUnits = totalUnits - delayedUnits!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (immediateUnits != null) {
|
||||||
|
delayedUnits = Utils.addDoublesWithPrecision(
|
||||||
|
totalUnits, -immediateUnits, precision);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_immediateUnitsController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
immediateUnits!, Settings.insulinSteps);
|
||||||
|
_delayedUnitsController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
delayedUnits!, Settings.insulinSteps);
|
||||||
|
if (totalUnits != 0) {
|
||||||
|
_delayPercentage = delayedUnits / totalUnits! * 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSaveAction() async {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
if (_logBolusForm.currentState!.validate()) {
|
||||||
|
LogBolus logBolus;
|
||||||
|
LogBolus? delayedBolus;
|
||||||
|
|
||||||
|
if ((int.tryParse(_delayController.text) ?? 0) != 0 &&
|
||||||
|
_delayPercentage != 0 &&
|
||||||
|
_delayPercentage != 100) {
|
||||||
|
logBolus = LogBolus(
|
||||||
|
id: widget.id,
|
||||||
|
units: double.tryParse(_immediateUnitsController.text) ?? 0,
|
||||||
|
setManually: _setManually,
|
||||||
|
notes: _notesController.text,
|
||||||
|
);
|
||||||
|
delayedBolus = LogBolus(
|
||||||
|
delay: int.tryParse(_delayController.text),
|
||||||
|
units: double.tryParse(_delayedUnitsController.text) ?? 0,
|
||||||
|
setManually: _setManually,
|
||||||
|
notes: _notesController.text,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logBolus = LogBolus(
|
||||||
|
id: widget.id,
|
||||||
|
units: double.tryParse(_unitsController.text) ?? 0,
|
||||||
|
delay: _delayPercentage == 100
|
||||||
|
? int.tryParse(_delayController.text)
|
||||||
|
: null,
|
||||||
|
setManually: _setManually,
|
||||||
|
notes: _notesController.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_bolusType == BolusType.meal) {
|
||||||
|
logBolus.carbs = double.tryParse(_carbsController.text);
|
||||||
|
if (delayedBolus != null) {
|
||||||
|
delayedBolus.carbs = double.tryParse(_carbsController.text);
|
||||||
|
}
|
||||||
|
logBolus.mgPerDlCurrent = null;
|
||||||
|
logBolus.mmolPerLCurrent = null;
|
||||||
|
} else {
|
||||||
|
logBolus.carbs = null;
|
||||||
|
logBolus.mgPerDlCurrent = int.tryParse(_mgPerDlCurrentController.text);
|
||||||
|
logBolus.mmolPerLCurrent =
|
||||||
|
double.tryParse(_mmolPerLCurrentController.text);
|
||||||
|
logBolus.mgPerDlTarget = int.tryParse(_mgPerDlTargetController.text);
|
||||||
|
logBolus.mmolPerLTarget =
|
||||||
|
double.tryParse(_mmolPerLTargetController.text);
|
||||||
|
logBolus.mgPerDlCorrection =
|
||||||
|
int.tryParse(_mgPerDlCorrectionController.text);
|
||||||
|
logBolus.mmolPerLCorrection =
|
||||||
|
double.tryParse(_mmolPerLCorrectionController.text);
|
||||||
|
if (delayedBolus != null) {
|
||||||
|
delayedBolus.mgPerDlCurrent =
|
||||||
|
int.tryParse(_mgPerDlCurrentController.text);
|
||||||
|
delayedBolus.mmolPerLCurrent =
|
||||||
|
double.tryParse(_mmolPerLCurrentController.text);
|
||||||
|
delayedBolus.mgPerDlTarget =
|
||||||
|
int.tryParse(_mgPerDlTargetController.text);
|
||||||
|
delayedBolus.mmolPerLTarget =
|
||||||
|
double.tryParse(_mmolPerLTargetController.text);
|
||||||
|
delayedBolus.mgPerDlCorrection =
|
||||||
|
int.tryParse(_mgPerDlCorrectionController.text);
|
||||||
|
delayedBolus.mmolPerLCorrection =
|
||||||
|
double.tryParse(_mmolPerLCorrectionController.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logBolus.logEntry.target = _logEntry;
|
||||||
|
logBolus.meal.target = _meal;
|
||||||
|
logBolus.rate.target = _rate;
|
||||||
|
LogBolus.put(logBolus);
|
||||||
|
|
||||||
|
if (delayedBolus != null) {
|
||||||
|
delayedBolus.logEntry.target = _logEntry;
|
||||||
|
delayedBolus.meal.target = _meal;
|
||||||
|
delayedBolus.rate.target = _rate;
|
||||||
|
LogBolus.put(delayedBolus);
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.pop(context,
|
||||||
|
['${_isNew ? 'New' : ''} Bolus Saved', logBolus, delayedBolus]);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCancelAction() {
|
||||||
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
|
((_isNew &&
|
||||||
|
(_carbsController.text != '' ||
|
||||||
|
(_bolusType == BolusType.glucose &&
|
||||||
|
(_mgPerDlCurrentController.text !=
|
||||||
|
(_logEntry?.mgPerDl.toString() ?? '') ||
|
||||||
|
_mmolPerLCurrentController.text !=
|
||||||
|
(_logEntry?.mmolPerL.toString() ?? ''))) ||
|
||||||
|
_mgPerDlTargetController.text !=
|
||||||
|
Settings.targetMgPerDl.toString() ||
|
||||||
|
_mmolPerLTargetController.text !=
|
||||||
|
Settings.targetMmolPerL.toString() ||
|
||||||
|
_delayController.text != '' ||
|
||||||
|
_setManually ||
|
||||||
|
_notesController.text != '')) ||
|
||||||
|
(!_isNew &&
|
||||||
|
(double.tryParse(_unitsController.text) != _logBolus!.units ||
|
||||||
|
double.tryParse(_carbsController.text) !=
|
||||||
|
_logBolus!.carbs ||
|
||||||
|
int.tryParse(_mgPerDlCurrentController.text) !=
|
||||||
|
_logBolus!.mgPerDlCurrent ||
|
||||||
|
int.tryParse(_mgPerDlTargetController.text) !=
|
||||||
|
_logBolus!.mgPerDlTarget ||
|
||||||
|
int.tryParse(_mgPerDlCorrectionController.text) !=
|
||||||
|
_logBolus!.mgPerDlCorrection ||
|
||||||
|
double.tryParse(_mmolPerLCurrentController.text) !=
|
||||||
|
_logBolus!.mmolPerLCurrent ||
|
||||||
|
double.tryParse(_mmolPerLTargetController.text) !=
|
||||||
|
_logBolus!.mmolPerLTarget ||
|
||||||
|
double.tryParse(_mmolPerLCorrectionController.text) !=
|
||||||
|
_logBolus!.mmolPerLCorrection ||
|
||||||
|
int.tryParse(_delayController.text) != _logBolus!.delay ||
|
||||||
|
_setManually != _logBolus!.setManually ||
|
||||||
|
_notesController.text != (_logBolus!.notes ?? ''))))) {
|
||||||
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
isNew: _isNew,
|
||||||
|
onSave: handleSaveAction,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_isNew ? 'New Bolus' : 'Edit Bolus'),
|
||||||
|
),
|
||||||
|
drawer: const Navigation(currentLocation: LogBolusDetailScreen.routeName),
|
||||||
|
body: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
FormWrapper(
|
||||||
|
formState: _logBolusForm,
|
||||||
|
fields: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Bolus Units',
|
||||||
|
suffix: ' U',
|
||||||
|
controller: _unitsController,
|
||||||
|
step: Settings.insulinSteps,
|
||||||
|
autoRoundToMultipleOfStep: true,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_setManually = true;
|
||||||
|
});
|
||||||
|
updateDelayedRatio(totalUnitsUpdate: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: BooleanFormField(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 10.0, right: 10.0, top: 10.0),
|
||||||
|
value: _setManually,
|
||||||
|
label: 'set manually',
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_setManually = value;
|
||||||
|
calculateBolus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: RadioListTile(
|
||||||
|
title: const Text('for glucose'),
|
||||||
|
groupValue: _bolusType,
|
||||||
|
value: BolusType.glucose,
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
_bolusType = BolusType.glucose;
|
||||||
|
calculateBolus();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: RadioListTile(
|
||||||
|
title: const Text('for meal'),
|
||||||
|
groupValue: _bolusType,
|
||||||
|
value: BolusType.meal,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_bolusType = BolusType.meal;
|
||||||
|
calculateBolus();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: _bolusType == BolusType.glucose
|
||||||
|
? [
|
||||||
|
Row(
|
||||||
|
children: Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mgPerDl ||
|
||||||
|
[
|
||||||
|
GlucoseDisplayMode.both,
|
||||||
|
GlucoseDisplayMode.bothForDetail
|
||||||
|
].contains(Settings.glucoseDisplayMode)
|
||||||
|
? [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(right: 5.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Current',
|
||||||
|
suffix: 'mg/dl',
|
||||||
|
controller:
|
||||||
|
_mgPerDlCurrentController,
|
||||||
|
onChanged: (_) => onChangeGlucose(),
|
||||||
|
showSteppers: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 5.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Target',
|
||||||
|
suffix: 'mg/dl',
|
||||||
|
controller:
|
||||||
|
_mgPerDlTargetController,
|
||||||
|
onChanged: (_) => onChangeGlucose(),
|
||||||
|
showSteppers: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(left: 5.0),
|
||||||
|
child: TextFormField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Correction',
|
||||||
|
suffixText: 'mg/dl',
|
||||||
|
),
|
||||||
|
controller:
|
||||||
|
_mgPerDlCorrectionController,
|
||||||
|
readOnly: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: [
|
||||||
|
GlucoseDisplayMode.both,
|
||||||
|
GlucoseDisplayMode.bothForDetail
|
||||||
|
].contains(Settings.glucoseDisplayMode)
|
||||||
|
? 10.0
|
||||||
|
: 0.0),
|
||||||
|
child: Row(
|
||||||
|
children: Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mmolPerL ||
|
||||||
|
[
|
||||||
|
GlucoseDisplayMode.both,
|
||||||
|
GlucoseDisplayMode.bothForDetail
|
||||||
|
].contains(Settings.glucoseDisplayMode)
|
||||||
|
? [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 5.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Current',
|
||||||
|
suffix: 'mmol/l',
|
||||||
|
controller:
|
||||||
|
_mmolPerLCurrentController,
|
||||||
|
onChanged: (_) =>
|
||||||
|
onChangeGlucose(),
|
||||||
|
showSteppers: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 5.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Target',
|
||||||
|
suffix: 'mmol/l',
|
||||||
|
controller:
|
||||||
|
_mmolPerLTargetController,
|
||||||
|
onChanged: (_) =>
|
||||||
|
onChangeGlucose(),
|
||||||
|
showSteppers: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 5.0),
|
||||||
|
child: TextFormField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Correction',
|
||||||
|
suffixText: 'mmol/l',
|
||||||
|
),
|
||||||
|
controller:
|
||||||
|
_mmolPerLCorrectionController,
|
||||||
|
readOnly: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<LogMeal>(
|
||||||
|
controller: _mealController,
|
||||||
|
selectedItem: _meal,
|
||||||
|
label: 'Meal',
|
||||||
|
items: _logMeals,
|
||||||
|
onChanged: onSelectMeal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _meal == null
|
||||||
|
? const LogMealDetailScreen()
|
||||||
|
: LogMealDetailScreen(
|
||||||
|
id: _meal!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updateLogMeal(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
_meal == null ? Icons.add : Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Carbs',
|
||||||
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
|
controller: _carbsController,
|
||||||
|
step: Settings.nutritionSteps,
|
||||||
|
onChanged: (value) {
|
||||||
|
_carbsController.text =
|
||||||
|
(value ?? 0).toString();
|
||||||
|
calculateBolus();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Delayed Bolus Duration',
|
||||||
|
suffixText: ' min',
|
||||||
|
),
|
||||||
|
controller: _delayController,
|
||||||
|
onChanged: (value) => setState(() {}),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Slider(
|
||||||
|
label: '${_delayPercentage.floor().toString()}%',
|
||||||
|
divisions: 100,
|
||||||
|
value: _delayPercentage,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
onChanged: (value) {
|
||||||
|
updateDelayedRatio(percentageUpdate: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text('%', textScaleFactor: 1.5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: (int.tryParse(_delayController.text) ?? 0) != 0
|
||||||
|
? [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Immediate Bolus',
|
||||||
|
suffix: ' U',
|
||||||
|
controller: _immediateUnitsController,
|
||||||
|
max: double.tryParse(_unitsController.text),
|
||||||
|
step: Settings.insulinSteps,
|
||||||
|
readOnly: true,
|
||||||
|
onChanged: (value) => updateDelayedRatio(
|
||||||
|
immediateUnitsUpdate: value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Delayed Bolus',
|
||||||
|
suffix: ' U',
|
||||||
|
controller: _delayedUnitsController,
|
||||||
|
max: double.tryParse(_unitsController.text),
|
||||||
|
step: Settings.insulinSteps,
|
||||||
|
readOnly: true,
|
||||||
|
onChanged: (value) => {
|
||||||
|
updateDelayedRatio(
|
||||||
|
delayedUnitsUpdate: value),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: _isSaving ? null : handleSaveAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
146
lib/screens/log/log_entry/log_bolus_list.dart
Normal file
146
lib/screens/log/log_entry/log_bolus_list.dart
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/models/log_bolus.dart';
|
||||||
|
import 'package:diameter/models/log_entry.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_bolus_detail.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_meal_detail.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LogBolusListScreen extends StatefulWidget {
|
||||||
|
final LogEntry logEntry;
|
||||||
|
final List<LogBolus> logBoli;
|
||||||
|
final Function() reload;
|
||||||
|
|
||||||
|
const LogBolusListScreen(
|
||||||
|
{Key? key,
|
||||||
|
required this.logEntry,
|
||||||
|
this.logBoli = const [],
|
||||||
|
required this.reload})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogBolusListScreenState createState() => _LogBolusListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogBolusListScreenState extends State<LogBolusListScreen> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
widget.reload();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleEditAction(LogBolus logBolus) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LogBolusDetailScreen(
|
||||||
|
logEntryId: widget.logEntry.id,
|
||||||
|
id: logBolus.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDelete(LogBolus logBolus) {
|
||||||
|
LogBolus.remove(logBolus.id);
|
||||||
|
reload(message: 'Bolus deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeleteAction(LogBolus logBolus) async {
|
||||||
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
|
DialogUtils.showConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
onConfirm: () => onDelete(logBolus),
|
||||||
|
message: 'Are you sure you want to delete this Bolus?',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onDelete(logBolus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleEditMealAction(int mealId) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LogMealDetailScreen(
|
||||||
|
logEntryId: widget.logEntry.id,
|
||||||
|
id: mealId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return widget.logBoli.isNotEmpty
|
||||||
|
? Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
controller: _scrollController,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: widget.logBoli.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final bolus = widget.logBoli[index];
|
||||||
|
String titleText = '${bolus.units} U ${(bolus.delay ?? 0) != 0
|
||||||
|
? ' (delayed by ${bolus.delay} min)'
|
||||||
|
: ''}';
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () => handleEditAction(bolus),
|
||||||
|
title: Text(
|
||||||
|
titleText.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
subtitle: Text(bolus.carbs != null ?
|
||||||
|
'for ${(bolus.meal.target ?? '').toString()} (${bolus.carbs}${Settings.nutritionMeasurementSuffix} carbs)'
|
||||||
|
: 'to correct ${Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDlCorrection : bolus.mmolPerLCorrection} ${Settings.glucoseMeasurementSuffix}'),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
bolus.meal.target != null
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.restaurant),
|
||||||
|
onPressed: () =>
|
||||||
|
handleEditMealAction(bolus.meal.targetId),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleDeleteAction(bolus),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text(
|
||||||
|
'You have not added any Boli to this Log Entry yet!'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
470
lib/screens/log/log_entry/log_entry.dart
Normal file
470
lib/screens/log/log_entry/log_entry.dart
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
import 'package:diameter/components/detail.dart';
|
||||||
|
import 'package:diameter/components/forms/date_time_form_field.dart';
|
||||||
|
import 'package:diameter/components/forms/number_form_field.dart';
|
||||||
|
import 'package:diameter/components/forms/time_of_day_form_field.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
|
import 'package:diameter/models/log_bolus.dart';
|
||||||
|
import 'package:diameter/models/log_entry.dart';
|
||||||
|
import 'package:diameter/models/log_meal.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_bolus_detail.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_bolus_list.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_meal_detail.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_meal_list.dart';
|
||||||
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
class LogEntryScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/log-entry';
|
||||||
|
final int id;
|
||||||
|
final DateTime? suggestedDate;
|
||||||
|
|
||||||
|
const LogEntryScreen({Key? key, this.id = 0, this.suggestedDate}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogEntryScreenState createState() => _LogEntryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogEntryScreenState extends State<LogEntryScreen> {
|
||||||
|
LogEntry? _logEntry;
|
||||||
|
List<LogMeal> _logMeals = [];
|
||||||
|
List<LogBolus> _logBoli = [];
|
||||||
|
|
||||||
|
bool _isNew = true;
|
||||||
|
|
||||||
|
final GlobalKey<FormState> logEntryForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
late DateTime _time;
|
||||||
|
double? _glucoseTrend;
|
||||||
|
|
||||||
|
final _timeController = TextEditingController(text: '');
|
||||||
|
final _dateController = TextEditingController(text: '');
|
||||||
|
final _mgPerDlController = TextEditingController(text: '');
|
||||||
|
final _mmolPerLController = TextEditingController(text: '');
|
||||||
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
late FloatingActionButton addMealButton;
|
||||||
|
late FloatingActionButton addBolusButton;
|
||||||
|
late IconButton refreshButton;
|
||||||
|
late IconButton closeButton;
|
||||||
|
late DetailBottomRow detailBottomRow;
|
||||||
|
late DetailBottomRow detailBottomRowWhileSaving;
|
||||||
|
|
||||||
|
FloatingActionButton? actionButton;
|
||||||
|
List<Widget> appBarActions = [];
|
||||||
|
DetailBottomRow? bottomNav;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
reload();
|
||||||
|
|
||||||
|
addMealButton = FloatingActionButton(
|
||||||
|
onPressed: handleAddNewMeal,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
);
|
||||||
|
|
||||||
|
addBolusButton = FloatingActionButton(
|
||||||
|
onPressed: handleAddNewBolus,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
);
|
||||||
|
|
||||||
|
refreshButton = IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: reload,
|
||||||
|
);
|
||||||
|
|
||||||
|
closeButton = IconButton(
|
||||||
|
onPressed: handleCancelAction,
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
);
|
||||||
|
|
||||||
|
detailBottomRow = DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: handleSaveAction,
|
||||||
|
onMiddleAction: () => handleSaveAction(close: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
detailBottomRowWhileSaving = DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
actionButton = null;
|
||||||
|
appBarActions = [closeButton];
|
||||||
|
bottomNav = detailBottomRow;
|
||||||
|
|
||||||
|
if (_logEntry != null) {
|
||||||
|
_time = _logEntry!.time;
|
||||||
|
_mgPerDlController.text = (_logEntry!.mgPerDl ?? '').toString();
|
||||||
|
_mmolPerLController.text = (_logEntry!.mmolPerL ?? '').toString();
|
||||||
|
_glucoseTrend = _logEntry!.glucoseTrend;
|
||||||
|
_notesController.text = _logEntry!.notes ?? '';
|
||||||
|
} else {
|
||||||
|
_time = widget.suggestedDate ?? DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_timeController.dispose();
|
||||||
|
_dateController.dispose();
|
||||||
|
_mgPerDlController.dispose();
|
||||||
|
_mmolPerLController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_logEntry = LogEntry.get(widget.id);
|
||||||
|
_logMeals = LogMeal.getAllForEntry(widget.id);
|
||||||
|
_logBoli = LogBolus.getAllForEntry(widget.id);
|
||||||
|
});
|
||||||
|
_isNew = _logEntry == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTime() {
|
||||||
|
_timeController.text = DateTimeUtils.displayTime(_time);
|
||||||
|
_dateController.text = DateTimeUtils.displayDate(_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void convertBetweenMgPerDlAndMmolPerL(double? value) async {
|
||||||
|
if (value != null) {
|
||||||
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl &&
|
||||||
|
_mgPerDlController.text != '') {
|
||||||
|
_mgPerDlController.text = value.toInt().toString();
|
||||||
|
setState(() {
|
||||||
|
_mmolPerLController.text =
|
||||||
|
Utils.convertMgPerDlToMmolPerL(value.toInt()).toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL &&
|
||||||
|
_mmolPerLController.text != '') {
|
||||||
|
_mmolPerLController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
value, Settings.mmolPerLSteps);
|
||||||
|
setState(() {
|
||||||
|
_mgPerDlController.text =
|
||||||
|
Utils.convertMmolPerLToMgPerDl(value.toDouble()).toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSaveAction({bool close = false}) async {
|
||||||
|
setState(() {
|
||||||
|
bottomNav = detailBottomRowWhileSaving;
|
||||||
|
});
|
||||||
|
if (logEntryForm.currentState!.validate()) {
|
||||||
|
LogEntry logEntry = LogEntry(
|
||||||
|
id: widget.id,
|
||||||
|
time: _time,
|
||||||
|
mgPerDl: int.tryParse(_mgPerDlController.text),
|
||||||
|
mmolPerL: double.tryParse(_mmolPerLController.text),
|
||||||
|
glucoseTrend: _glucoseTrend,
|
||||||
|
notes: _notesController.text,
|
||||||
|
);
|
||||||
|
LogEntry.put(logEntry);
|
||||||
|
|
||||||
|
if (close) {
|
||||||
|
Navigator.pop(
|
||||||
|
context, ['${_isNew ? 'New' : ''} Log Entry Saved', logEntry]);
|
||||||
|
} else {
|
||||||
|
if (_isNew) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LogEntryScreen(id: logEntry.id),
|
||||||
|
),
|
||||||
|
).then((result) => Navigator.pop(context, result));
|
||||||
|
} else {
|
||||||
|
reload(message: 'Log Entry Saved');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
bottomNav = detailBottomRow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCancelAction() {
|
||||||
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
|
((_isNew &&
|
||||||
|
(int.tryParse(_mgPerDlController.text) != null ||
|
||||||
|
double.tryParse(_mmolPerLController.text) != null ||
|
||||||
|
_notesController.text != '')) ||
|
||||||
|
(!_isNew &&
|
||||||
|
(int.tryParse(_mgPerDlController.text) != _logEntry!.mgPerDl ||
|
||||||
|
double.tryParse(_mmolPerLController.text) !=
|
||||||
|
_logEntry!.mmolPerL ||
|
||||||
|
_notesController.text != (_logEntry!.notes ?? ''))))) {
|
||||||
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
isNew: _isNew,
|
||||||
|
onSave: handleSaveAction,
|
||||||
|
onDiscard: (context) => Navigator.pop(context),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleAddNewMeal() async {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return LogMealDetailScreen(logEntryId: _logEntry!.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleAddNewBolus() async {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return LogBolusDetailScreen(logEntryId: _logEntry!.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderTabButtons(index) {
|
||||||
|
if (_logEntry != null) {
|
||||||
|
setState(() {
|
||||||
|
switch (index) {
|
||||||
|
case 1:
|
||||||
|
actionButton = addMealButton;
|
||||||
|
appBarActions = [refreshButton, closeButton];
|
||||||
|
bottomNav = null;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
actionButton = addBolusButton;
|
||||||
|
appBarActions = [refreshButton, closeButton];
|
||||||
|
bottomNav = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
actionButton = null;
|
||||||
|
appBarActions = [closeButton];
|
||||||
|
bottomNav = detailBottomRow;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DefaultTabController(
|
||||||
|
length: _isNew ? 1 : 3,
|
||||||
|
child: Builder(builder: (BuildContext context) {
|
||||||
|
final TabController tabController = DefaultTabController.of(context)!;
|
||||||
|
tabController.addListener(() {
|
||||||
|
renderTabButtons(tabController.index);
|
||||||
|
});
|
||||||
|
List<Widget> tabs = [
|
||||||
|
Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
FormWrapper(
|
||||||
|
formState: logEntryForm,
|
||||||
|
fields: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: DateTimeFormField(
|
||||||
|
date: _time,
|
||||||
|
label: 'Date',
|
||||||
|
controller: _dateController,
|
||||||
|
onChanged: (newTime) {
|
||||||
|
if (newTime != null) {
|
||||||
|
setState(() {
|
||||||
|
_time = DateTime(
|
||||||
|
newTime.year,
|
||||||
|
newTime.month,
|
||||||
|
newTime.day,
|
||||||
|
_time.hour,
|
||||||
|
_time.minute);
|
||||||
|
});
|
||||||
|
updateTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: TimeOfDayFormField(
|
||||||
|
time: TimeOfDay.fromDateTime(_time),
|
||||||
|
label: 'Time',
|
||||||
|
controller: _timeController,
|
||||||
|
onChanged: (newTime) {
|
||||||
|
if (newTime != null) {
|
||||||
|
setState(() {
|
||||||
|
_time = DateTime(
|
||||||
|
_time.year,
|
||||||
|
_time.month,
|
||||||
|
_time.day,
|
||||||
|
newTime.hour,
|
||||||
|
newTime.minute);
|
||||||
|
});
|
||||||
|
updateTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mgPerDl ||
|
||||||
|
Settings.glucoseDisplayMode ==
|
||||||
|
GlucoseDisplayMode.both ||
|
||||||
|
Settings.glucoseDisplayMode ==
|
||||||
|
GlucoseDisplayMode.bothForDetail
|
||||||
|
? Expanded(
|
||||||
|
flex: Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mgPerDl
|
||||||
|
? 2
|
||||||
|
: 1,
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Blood Glucose',
|
||||||
|
suffix: 'mg/dl',
|
||||||
|
readOnly: Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mmolPerL,
|
||||||
|
showSteppers:
|
||||||
|
Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mgPerDl,
|
||||||
|
controller: _mgPerDlController,
|
||||||
|
onChanged:
|
||||||
|
convertBetweenMgPerDlAndMmolPerL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mmolPerL ||
|
||||||
|
[
|
||||||
|
GlucoseDisplayMode.both,
|
||||||
|
GlucoseDisplayMode.bothForDetail
|
||||||
|
].contains(Settings.glucoseDisplayMode)
|
||||||
|
? Expanded(
|
||||||
|
flex: Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mmolPerL
|
||||||
|
? 2
|
||||||
|
: 1,
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Blood Glucose',
|
||||||
|
suffix: 'mmol/l',
|
||||||
|
readOnly: Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mgPerDl,
|
||||||
|
showSteppers:
|
||||||
|
Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mmolPerL,
|
||||||
|
controller: _mmolPerLController,
|
||||||
|
step: Settings.mmolPerLSteps,
|
||||||
|
onChanged:
|
||||||
|
convertBetweenMgPerDlAndMmolPerL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
Transform.rotate(
|
||||||
|
angle: (_glucoseTrend ?? 90) * math.pi / 180,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_glucoseTrend = (_glucoseTrend ?? -45) + 45;
|
||||||
|
if (_glucoseTrend! > 180) {
|
||||||
|
_glucoseTrend = null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
icon: Icon(Icons.arrow_upward,
|
||||||
|
color: _glucoseTrend != null
|
||||||
|
? Theme.of(context).iconTheme.color
|
||||||
|
: Theme.of(context).disabledColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!_isNew) {
|
||||||
|
tabs.add(LogMealListScreen(
|
||||||
|
logEntry: _logEntry!, logMeals: _logMeals, reload: reload));
|
||||||
|
tabs.add(LogBolusListScreen(
|
||||||
|
logEntry: _logEntry!, logBoli: _logBoli, reload: reload));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_isNew ? 'New Log Entry' : 'Edit Log Entry'),
|
||||||
|
bottom: _isNew
|
||||||
|
? PreferredSize(child: Container(), preferredSize: Size.zero)
|
||||||
|
: const TabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(text: 'GENERAL'),
|
||||||
|
Tab(text: 'MEALS'),
|
||||||
|
Tab(text: 'BOLI'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: appBarActions,
|
||||||
|
),
|
||||||
|
drawer: const Navigation(currentLocation: LogEntryScreen.routeName),
|
||||||
|
body: TabBarView(
|
||||||
|
children: tabs,
|
||||||
|
),
|
||||||
|
bottomNavigationBar: bottomNav,
|
||||||
|
floatingActionButton: actionButton,
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
804
lib/screens/log/log_entry/log_meal_detail.dart
Normal file
804
lib/screens/log/log_entry/log_meal_detail.dart
Normal file
@ -0,0 +1,804 @@
|
|||||||
|
import 'package:diameter/components/detail.dart';
|
||||||
|
import 'package:diameter/components/forms/boolean_form_field.dart';
|
||||||
|
import 'package:diameter/components/forms/number_form_field.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
|
import 'package:diameter/models/accuracy.dart';
|
||||||
|
import 'package:diameter/models/log_meal.dart';
|
||||||
|
import 'package:diameter/models/meal.dart';
|
||||||
|
import 'package:diameter/models/meal_category.dart';
|
||||||
|
import 'package:diameter/models/meal_portion_type.dart';
|
||||||
|
import 'package:diameter/models/meal_source.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/accuracy_detail.dart';
|
||||||
|
import 'package:diameter/screens/meal/meal_category_detail.dart';
|
||||||
|
import 'package:diameter/screens/meal/meal_detail.dart';
|
||||||
|
import 'package:diameter/screens/meal/meal_portion_type_detail.dart';
|
||||||
|
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LogMealDetailScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/log-meal';
|
||||||
|
|
||||||
|
final int logEntryId;
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const LogMealDetailScreen({Key? key, this.logEntryId = 0, this.id = 0})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogMealDetailScreenState createState() => _LogMealDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
|
||||||
|
LogMeal? _logMeal;
|
||||||
|
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: '');
|
||||||
|
final _totalCarbsController = TextEditingController(text: '');
|
||||||
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
Meal? _meal;
|
||||||
|
MealSource? _mealSource;
|
||||||
|
MealCategory? _mealCategory;
|
||||||
|
MealPortionType? _mealPortionType;
|
||||||
|
Accuracy? _portionSizeAccuracy;
|
||||||
|
Accuracy? _carbsRatioAccuracy;
|
||||||
|
|
||||||
|
final _mealController = TextEditingController(text: '');
|
||||||
|
final _mealSourceController = TextEditingController(text: '');
|
||||||
|
final _mealCategoryController = TextEditingController(text: '');
|
||||||
|
final _mealPortionTypeController = TextEditingController(text: '');
|
||||||
|
final _portionSizeAccuracyController = TextEditingController(text: '');
|
||||||
|
final _carbsRatioAccuracyController = TextEditingController(text: '');
|
||||||
|
final _amountController = TextEditingController(text: '1');
|
||||||
|
|
||||||
|
List<Meal> _meals = [];
|
||||||
|
List<MealCategory> _mealCategories = [];
|
||||||
|
List<MealPortionType> _mealPortionTypes = [];
|
||||||
|
List<MealSource> _mealSources = [];
|
||||||
|
List<Accuracy> _portionSizeAccuracies = [];
|
||||||
|
List<Accuracy> _carbsRatioAccuracies = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
|
||||||
|
_portionSizeAccuracies = Accuracy.getAllForPortionSize();
|
||||||
|
_carbsRatioAccuracies = Accuracy.getAllForCarbsRatio();
|
||||||
|
_meals = Meal.getAll();
|
||||||
|
_mealCategories = MealCategory.getAll();
|
||||||
|
_mealPortionTypes = MealPortionType.getAll();
|
||||||
|
_mealSources = MealSource.getAll();
|
||||||
|
|
||||||
|
if (widget.id != 0) {
|
||||||
|
_valueController.text = _logMeal!.value;
|
||||||
|
_carbsRatioController.text = (_logMeal!.carbsRatio ?? '').toString();
|
||||||
|
_portionSizeController.text = (_logMeal!.portionSize ?? '').toString();
|
||||||
|
_totalCarbsController.text = (_logMeal!.totalCarbs ?? '').toString();
|
||||||
|
_amountController.text = (_logMeal!.amount).toString();
|
||||||
|
_notesController.text = _logMeal!.notes ?? '';
|
||||||
|
_meal = _logMeal!.meal.target;
|
||||||
|
_mealController.text = (_meal ?? '').toString();
|
||||||
|
_mealSource = _logMeal!.mealSource.target;
|
||||||
|
_mealSourceController.text = (_mealSource ?? '').toString();
|
||||||
|
_mealCategory = _logMeal!.mealCategory.target;
|
||||||
|
_mealCategoryController.text = (_mealCategory ?? '').toString();
|
||||||
|
_mealPortionType = _logMeal!.mealPortionType.target;
|
||||||
|
_mealPortionTypeController.text = (_mealPortionType ?? '').toString();
|
||||||
|
_portionSizeAccuracy = _logMeal!.portionSizeAccuracy.target;
|
||||||
|
_portionSizeAccuracyController.text =
|
||||||
|
(_portionSizeAccuracy ?? '').toString();
|
||||||
|
_carbsRatioAccuracy = _logMeal!.carbsRatioAccuracy.target;
|
||||||
|
_carbsRatioAccuracyController.text =
|
||||||
|
(_carbsRatioAccuracy ?? '').toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_valueController.dispose();
|
||||||
|
_carbsRatioController.dispose();
|
||||||
|
_portionSizeController.dispose();
|
||||||
|
_totalCarbsController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
_mealController.dispose();
|
||||||
|
_mealSourceController.dispose();
|
||||||
|
_mealCategoryController.dispose();
|
||||||
|
_mealPortionTypeController.dispose();
|
||||||
|
_portionSizeAccuracyController.dispose();
|
||||||
|
_carbsRatioAccuracyController.dispose();
|
||||||
|
_amountController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_logMeal = LogMeal.get(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _logMeal == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCarbsRatioAccuracy(Accuracy? value) {
|
||||||
|
setState(() {
|
||||||
|
_carbsRatioAccuracy = value;
|
||||||
|
_carbsRatioAccuracyController.text =
|
||||||
|
(_carbsRatioAccuracy ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePortionSizeAccuracy(Accuracy? value) {
|
||||||
|
setState(() {
|
||||||
|
_portionSizeAccuracy = value;
|
||||||
|
_portionSizeAccuracyController.text =
|
||||||
|
(_portionSizeAccuracy ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMealCategory(MealCategory? value) {
|
||||||
|
setState(() {
|
||||||
|
_mealCategory = value;
|
||||||
|
_mealCategoryController.text = (_mealCategory ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMealPortionType(MealPortionType? value) {
|
||||||
|
setState(() {
|
||||||
|
_mealPortionType = value;
|
||||||
|
_mealPortionTypeController.text = (_mealPortionType ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMealSource(MealSource? value) {
|
||||||
|
setState(() {
|
||||||
|
_mealSource = value;
|
||||||
|
_mealSourceController.text = (_mealSource ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onSelectMeal(Meal? meal) async {
|
||||||
|
setState(() {
|
||||||
|
_meal = meal;
|
||||||
|
_mealController.text = (_meal ?? '').toString();
|
||||||
|
_valueController.text = _mealController.text;
|
||||||
|
_carbsRatioController.text = (meal?.carbsRatio ?? '').toString();
|
||||||
|
_amountController.text = '1';
|
||||||
|
_portionSizeController.text = (meal?.portionSize ?? '').toString();
|
||||||
|
_totalCarbsController.text = (meal?.carbsPerPortion ?? '').toString();
|
||||||
|
});
|
||||||
|
updateMealSource(meal?.mealSource.target);
|
||||||
|
updateMealCategory(meal?.mealCategory.target);
|
||||||
|
updateMealPortionType(meal?.mealPortionType.target);
|
||||||
|
updatePortionSizeAccuracy(meal?.portionSizeAccuracy.target);
|
||||||
|
updateCarbsRatioAccuracy(meal?.carbsRatioAccuracy.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSaveAction() async {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
if (_logMealForm.currentState!.validate()) {
|
||||||
|
LogMeal logMeal = LogMeal(
|
||||||
|
id: widget.id,
|
||||||
|
value: _valueController.text,
|
||||||
|
carbsRatio: double.tryParse(_carbsRatioController.text),
|
||||||
|
portionSize: double.tryParse(_portionSizeController.text),
|
||||||
|
totalCarbs: double.tryParse(_totalCarbsController.text),
|
||||||
|
amount: double.parse(_amountController.text),
|
||||||
|
);
|
||||||
|
logMeal.logEntry.targetId = widget.logEntryId;
|
||||||
|
logMeal.meal.target = _meal;
|
||||||
|
logMeal.mealSource.target = _mealSource;
|
||||||
|
logMeal.mealCategory.target = _mealCategory;
|
||||||
|
logMeal.mealPortionType.target = _mealPortionType;
|
||||||
|
logMeal.portionSizeAccuracy.target = _portionSizeAccuracy;
|
||||||
|
logMeal.carbsRatioAccuracy.target = _carbsRatioAccuracy;
|
||||||
|
|
||||||
|
LogMeal.put(logMeal);
|
||||||
|
Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Saved', logMeal]);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCancelAction() {
|
||||||
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
|
((_isNew &&
|
||||||
|
(_valueController.text != '' ||
|
||||||
|
_meal != null ||
|
||||||
|
_mealSource != null ||
|
||||||
|
_mealCategory != null ||
|
||||||
|
_mealPortionType != null ||
|
||||||
|
double.tryParse(_amountController.text) != 1 ||
|
||||||
|
double.tryParse(_carbsRatioController.text) != null ||
|
||||||
|
double.tryParse(_portionSizeController.text) != null ||
|
||||||
|
double.tryParse(_totalCarbsController.text) != null ||
|
||||||
|
_carbsRatioAccuracy != null ||
|
||||||
|
_portionSizeAccuracy != null ||
|
||||||
|
_notesController.text != '')) ||
|
||||||
|
(!_isNew &&
|
||||||
|
(_valueController.text != _logMeal!.value ||
|
||||||
|
_meal != _logMeal!.meal.target ||
|
||||||
|
_mealSource != _logMeal!.mealSource.target ||
|
||||||
|
_mealCategory != _logMeal!.mealCategory.target ||
|
||||||
|
_mealPortionType != _logMeal!.mealPortionType.target ||
|
||||||
|
double.tryParse(_amountController.text) !=
|
||||||
|
_logMeal!.amount ||
|
||||||
|
double.tryParse(_carbsRatioController.text) !=
|
||||||
|
_logMeal!.carbsRatio ||
|
||||||
|
double.tryParse(_portionSizeController.text) !=
|
||||||
|
_logMeal!.portionSize ||
|
||||||
|
double.tryParse(_totalCarbsController.text) !=
|
||||||
|
_logMeal!.totalCarbs ||
|
||||||
|
_carbsRatioAccuracy !=
|
||||||
|
_logMeal!.carbsRatioAccuracy.target ||
|
||||||
|
_portionSizeAccuracy !=
|
||||||
|
_logMeal!.portionSizeAccuracy.target ||
|
||||||
|
_notesController.text != (_logMeal!.notes ?? ''))))) {
|
||||||
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
isNew: _isNew,
|
||||||
|
onSave: handleSaveAction,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateAmount(double? newAmount) {
|
||||||
|
if (newAmount != null) {
|
||||||
|
setState(() {
|
||||||
|
_amountController.text = Utils.getFractionDigitsLength(newAmount) == 0
|
||||||
|
? newAmount.toInt().toString()
|
||||||
|
: newAmount.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
double? portionSize;
|
||||||
|
double? basePortionSize;
|
||||||
|
|
||||||
|
if (_portionSizeController.text != '') {
|
||||||
|
portionSize = double.tryParse(_portionSizeController.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portionSize != null && portionSize != 0) {
|
||||||
|
basePortionSize = portionSize / _amount;
|
||||||
|
} else if (_meal != null) {
|
||||||
|
basePortionSize = _meal!.portionSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basePortionSize != null) {
|
||||||
|
setState(() {
|
||||||
|
portionSize = basePortionSize! * newAmount;
|
||||||
|
_portionSizeController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
portionSize!, Settings.nutritionSteps);
|
||||||
|
});
|
||||||
|
calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
portionSizeUpdate: portionSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_amount = newAmount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
{double? carbsRatioUpdate,
|
||||||
|
double? portionSizeUpdate,
|
||||||
|
double? totalCarbsUpdate}) {
|
||||||
|
if (!_setManually) {
|
||||||
|
double? carbsRatio =
|
||||||
|
carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text);
|
||||||
|
double? portionSize =
|
||||||
|
portionSizeUpdate ?? double.tryParse(_portionSizeController.text);
|
||||||
|
double? totalCarbs =
|
||||||
|
totalCarbsUpdate ?? double.tryParse(_totalCarbsController.text);
|
||||||
|
|
||||||
|
int toCalculate = 0;
|
||||||
|
const calcCarbsRatio = 1;
|
||||||
|
const calcTotalCarbs = 2;
|
||||||
|
const calcPortionSize = 3;
|
||||||
|
|
||||||
|
if (carbsRatioUpdate != null) {
|
||||||
|
if (portionSize != null && portionSize != 0) {
|
||||||
|
toCalculate = calcTotalCarbs;
|
||||||
|
} else if (totalCarbs != null && totalCarbs != 0) {
|
||||||
|
toCalculate = calcPortionSize;
|
||||||
|
}
|
||||||
|
} else if (portionSizeUpdate != null) {
|
||||||
|
if (carbsRatio != null && carbsRatio != 0) {
|
||||||
|
toCalculate = calcTotalCarbs;
|
||||||
|
} else if (totalCarbs != null && totalCarbs != 0) {
|
||||||
|
toCalculate = calcCarbsRatio;
|
||||||
|
}
|
||||||
|
} else if (totalCarbsUpdate != null) {
|
||||||
|
if (carbsRatio != null && carbsRatio != 0) {
|
||||||
|
toCalculate = calcPortionSize;
|
||||||
|
} else if (portionSize != null && portionSize != 0) {
|
||||||
|
toCalculate = calcCarbsRatio;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (carbsRatio != null && carbsRatio != 0) {
|
||||||
|
if (portionSize != null && portionSize != 0) {
|
||||||
|
toCalculate = calcTotalCarbs;
|
||||||
|
} else if (totalCarbs != null && totalCarbs != 0) {
|
||||||
|
toCalculate = calcPortionSize;
|
||||||
|
}
|
||||||
|
} else if (portionSize != null &&
|
||||||
|
portionSize != 0 &&
|
||||||
|
totalCarbs != null &&
|
||||||
|
totalCarbs != 0) {
|
||||||
|
toCalculate = calcCarbsRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (toCalculate == calcCarbsRatio) {
|
||||||
|
_carbsRatioController.text =
|
||||||
|
Utils.calculateCarbsRatio(totalCarbs!, portionSize!).toString();
|
||||||
|
} else if (toCalculate == calcTotalCarbs) {
|
||||||
|
_totalCarbsController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
Utils.calculateCarbs(carbsRatio!, portionSize!,
|
||||||
|
step: Settings.nutritionSteps),
|
||||||
|
Settings.nutritionSteps);
|
||||||
|
} else if (toCalculate == calcPortionSize) {
|
||||||
|
_portionSizeController.text =
|
||||||
|
Utils.toStringMatchingTemplateFractionPrecision(
|
||||||
|
Utils.calculatePortionSize(carbsRatio!, totalCarbs!,
|
||||||
|
step: Settings.nutritionSteps),
|
||||||
|
Settings.nutritionSteps);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_isNew ? 'New Meal for Log Entry' : _logMeal!.value),
|
||||||
|
),
|
||||||
|
drawer: const Navigation(currentLocation: LogMealDetailScreen.routeName),
|
||||||
|
body: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
FormWrapper(
|
||||||
|
formState: _logMealForm,
|
||||||
|
fields: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<Meal>(
|
||||||
|
controller: _mealController,
|
||||||
|
selectedItem: _meal,
|
||||||
|
label: 'Meal',
|
||||||
|
items: _meals,
|
||||||
|
onChanged: onSelectMeal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _meal == null
|
||||||
|
? const MealDetailScreen()
|
||||||
|
: MealDetailScreen(id: _meal!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
onSelectMeal(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_meal == null ? Icons.add : Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _valueController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 10,
|
||||||
|
child: NumberFormField(
|
||||||
|
controller: _amountController,
|
||||||
|
label: 'Amount',
|
||||||
|
suffix: _mealPortionType?.value,
|
||||||
|
onChanged: updateAmount,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => updateAmount(0.5),
|
||||||
|
child: Column(
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
'1',
|
||||||
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationThickness: 2),
|
||||||
|
),
|
||||||
|
Text('2'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => updateAmount(0.33),
|
||||||
|
child: Column(
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
'1',
|
||||||
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationThickness: 2),
|
||||||
|
),
|
||||||
|
Text('3'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => updateAmount(0.67),
|
||||||
|
child: Column(
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
'2',
|
||||||
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationThickness: 2),
|
||||||
|
),
|
||||||
|
Text('3'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Portion size',
|
||||||
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
|
controller: _portionSizeController,
|
||||||
|
showSteppers: false,
|
||||||
|
autoRoundToMultipleOfStep: true,
|
||||||
|
step: Settings.nutritionSteps,
|
||||||
|
onChanged: (value) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
portionSizeUpdate: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Carbs ratio',
|
||||||
|
suffix: '%',
|
||||||
|
controller: _carbsRatioController,
|
||||||
|
showSteppers: false,
|
||||||
|
onChanged: (value) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
carbsRatioUpdate: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: NumberFormField(
|
||||||
|
label: 'Total carbs',
|
||||||
|
suffix: Settings.nutritionMeasurementSuffix,
|
||||||
|
controller: _totalCarbsController,
|
||||||
|
showSteppers: false,
|
||||||
|
autoRoundToMultipleOfStep: true,
|
||||||
|
step: Settings.nutritionSteps,
|
||||||
|
onChanged: (value) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
calculateThirdMeasurementOfPortionCarbsRelation(
|
||||||
|
totalCarbsUpdate: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: BooleanFormField(
|
||||||
|
value: _setManually,
|
||||||
|
label: 'set carbs ratio manually',
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_setManually = value;
|
||||||
|
calculateThirdMeasurementOfPortionCarbsRelation();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() {
|
||||||
|
_isExpanded = !_isExpanded;
|
||||||
|
}),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'ADDITIONAL FIELDS',
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Icon(_isExpanded
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: _isExpanded
|
||||||
|
? [
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
AutoCompleteDropdownButton<MealSource>(
|
||||||
|
controller: _mealSourceController,
|
||||||
|
selectedItem: _mealSource,
|
||||||
|
label: 'Meal Source',
|
||||||
|
items: _mealSources,
|
||||||
|
onChanged: updateMealSource,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _mealSource ==
|
||||||
|
null
|
||||||
|
? const MealSourceDetailScreen()
|
||||||
|
: MealSourceDetailScreen(
|
||||||
|
id: _mealSource!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updateMealSource(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_mealSource == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<
|
||||||
|
MealCategory>(
|
||||||
|
controller: _mealCategoryController,
|
||||||
|
selectedItem: _mealCategory,
|
||||||
|
label: 'Meal Category',
|
||||||
|
items: _mealCategories,
|
||||||
|
onChanged: updateMealCategory,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _mealCategory ==
|
||||||
|
null
|
||||||
|
? const MealCategoryDetailScreen()
|
||||||
|
: MealCategoryDetailScreen(
|
||||||
|
id: _mealCategory!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updateMealCategory(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_mealCategory == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<
|
||||||
|
MealPortionType>(
|
||||||
|
controller: _mealPortionTypeController,
|
||||||
|
selectedItem: _mealPortionType,
|
||||||
|
label: 'Meal Portion Type',
|
||||||
|
items: _mealPortionTypes,
|
||||||
|
onChanged: updateMealPortionType,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _mealPortionType ==
|
||||||
|
null
|
||||||
|
? const MealPortionTypeDetailScreen()
|
||||||
|
: MealPortionTypeDetailScreen(
|
||||||
|
id: _mealPortionType!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updateMealPortionType(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_mealPortionType == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<Accuracy>(
|
||||||
|
controller:
|
||||||
|
_portionSizeAccuracyController,
|
||||||
|
selectedItem: _portionSizeAccuracy,
|
||||||
|
label: 'Portion Size Accuracy',
|
||||||
|
items: _portionSizeAccuracies,
|
||||||
|
onChanged: updatePortionSizeAccuracy,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
_portionSizeAccuracy == null
|
||||||
|
? const AccuracyDetailScreen()
|
||||||
|
: AccuracyDetailScreen(
|
||||||
|
id: _portionSizeAccuracy!
|
||||||
|
.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updatePortionSizeAccuracy(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_portionSizeAccuracy == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<Accuracy>(
|
||||||
|
controller: _carbsRatioAccuracyController,
|
||||||
|
selectedItem: _carbsRatioAccuracy,
|
||||||
|
label: 'Carbs Ratio Accuracy',
|
||||||
|
items: _carbsRatioAccuracies,
|
||||||
|
onChanged: updateCarbsRatioAccuracy,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
_carbsRatioAccuracy == null
|
||||||
|
? const AccuracyDetailScreen()
|
||||||
|
: AccuracyDetailScreen(
|
||||||
|
id: _carbsRatioAccuracy!
|
||||||
|
.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updateCarbsRatioAccuracy(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_carbsRatioAccuracy == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: _isSaving ? null : handleSaveAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
127
lib/screens/log/log_entry/log_meal_list.dart
Normal file
127
lib/screens/log/log_entry/log_meal_list.dart
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/models/log_entry.dart';
|
||||||
|
import 'package:diameter/models/log_meal.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/screens/log/log_entry/log_meal_detail.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LogMealListScreen extends StatefulWidget {
|
||||||
|
final LogEntry logEntry;
|
||||||
|
final List<LogMeal> logMeals;
|
||||||
|
final Function() reload;
|
||||||
|
|
||||||
|
const LogMealListScreen(
|
||||||
|
{Key? key, required this.logEntry, this.logMeals = const [], required this.reload})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogMealListScreenState createState() => _LogMealListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogMealListScreenState extends State<LogMealListScreen> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
widget.reload();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleEditAction(LogMeal meal) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LogMealDetailScreen(
|
||||||
|
logEntryId: widget.logEntry.id,
|
||||||
|
id: meal.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDelete(LogMeal logMeal) {
|
||||||
|
LogMeal.remove(logMeal.id);
|
||||||
|
reload(message: 'Meal deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeleteAction(LogMeal meal) async {
|
||||||
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
|
DialogUtils.showConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
onConfirm: () => onDelete(meal),
|
||||||
|
message: 'Are you sure you want to delete this Meal?',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onDelete(meal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return widget.logMeals.isNotEmpty
|
||||||
|
? Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
controller: _scrollController,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: widget.logMeals.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final meal = widget.logMeals[index];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () => handleEditAction(meal),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(
|
||||||
|
meal.value.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
)),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: ((meal.totalCarbs ?? 0) > 0)
|
||||||
|
? [
|
||||||
|
Text(meal.totalCarbs!.toStringAsPrecision(3)),
|
||||||
|
Text(
|
||||||
|
'${Settings.nutritionMeasurementSuffix} carbs',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleDeleteAction(meal),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text(
|
||||||
|
'You have not added any Meals to this Log Entry yet!'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,185 +0,0 @@
|
|||||||
import 'package:diameter/components/forms.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/settings.dart';
|
|
||||||
import 'package:diameter/utils/utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LogEntryForm extends StatefulWidget {
|
|
||||||
final GlobalKey<FormState> formState;
|
|
||||||
final Map<String, TextEditingController> controllers;
|
|
||||||
|
|
||||||
const LogEntryForm(
|
|
||||||
{Key? key, required this.formState, required this.controllers})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LogEntryFormState createState() => _LogEntryFormState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogEntryFormState extends State<LogEntryForm> {
|
|
||||||
void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) {
|
|
||||||
int? mgPerDl;
|
|
||||||
double? mmolPerL;
|
|
||||||
final _mgPerDlController = widget.controllers['mgPerDl'];
|
|
||||||
final _mmolPerLController = widget.controllers['mmolPerL'];
|
|
||||||
|
|
||||||
if (calculateFrom != GlucoseMeasurement.mmolPerL &&
|
|
||||||
_mgPerDlController!.text != '') {
|
|
||||||
mgPerDl = int.tryParse(_mgPerDlController.text);
|
|
||||||
}
|
|
||||||
if (calculateFrom != GlucoseMeasurement.mgPerDl &&
|
|
||||||
_mmolPerLController!.text != '') {
|
|
||||||
mmolPerL = double.tryParse(_mmolPerLController.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mgPerDl != null && mmolPerL == null) {
|
|
||||||
setState(() {
|
|
||||||
_mmolPerLController!.text =
|
|
||||||
Utils.convertMgPerDlToMmolPerL(mgPerDl!).toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (mmolPerL != null && mgPerDl == null) {
|
|
||||||
setState(() {
|
|
||||||
_mgPerDlController!.text =
|
|
||||||
Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// final _timeController = widget.controllers['time'];
|
|
||||||
final _mgPerDlController = widget.controllers['mgPerDl'];
|
|
||||||
final _mmolPerLController = widget.controllers['mmolPerL'];
|
|
||||||
final _bolusGlucoseController = widget.controllers['bolusGlucose'];
|
|
||||||
final _delayedBolusRateController = widget.controllers['delayedBolusRate'];
|
|
||||||
final _delayedBolusDurationController =
|
|
||||||
widget.controllers['delayedBolusDuration'];
|
|
||||||
final _notesController = widget.controllers['notes'];
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: <
|
|
||||||
Widget>[
|
|
||||||
StyledForm(
|
|
||||||
formState: widget.formState,
|
|
||||||
fields: [
|
|
||||||
// TODO: insert time picker
|
|
||||||
// Expanded(
|
|
||||||
// child: StyledTimeOfDayFormField(
|
|
||||||
// label: 'Time',
|
|
||||||
// controller: _timeController,
|
|
||||||
// onChanged: (newEndTime) {
|
|
||||||
// if (newEndTime != null) {
|
|
||||||
// setState(() {
|
|
||||||
// _endTime = newEndTime;
|
|
||||||
// });
|
|
||||||
// updateEndTime();
|
|
||||||
// }
|
|
||||||
//),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
glucoseMeasurement == GlucoseMeasurement.mgPerDl ||
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.both ||
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.bothForDetail
|
|
||||||
? Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'mg/dl',
|
|
||||||
suffixText: 'mg/dl',
|
|
||||||
),
|
|
||||||
controller: _mgPerDlController,
|
|
||||||
onChanged: (_) => convertBetweenMgPerDlAndMmolPerL(
|
|
||||||
calculateFrom: GlucoseMeasurement.mgPerDl),
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty &&
|
|
||||||
_mmolPerLController!.text.trim().isEmpty) {
|
|
||||||
return 'How many mg/dl or mmol/l does the rate make up for?';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.both ||
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.bothForDetail
|
|
||||||
? IconButton(
|
|
||||||
onPressed: () => convertBetweenMgPerDlAndMmolPerL(
|
|
||||||
calculateFrom: GlucoseMeasurement.mmolPerL),
|
|
||||||
icon: const Icon(Icons.calculate),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
glucoseMeasurement == GlucoseMeasurement.mmolPerL ||
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.both ||
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.bothForDetail
|
|
||||||
? Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'mmol/l',
|
|
||||||
suffixText: 'mmol/l',
|
|
||||||
),
|
|
||||||
controller: _mmolPerLController,
|
|
||||||
onChanged: (_) => convertBetweenMgPerDlAndMmolPerL(
|
|
||||||
calculateFrom: GlucoseMeasurement.mmolPerL),
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: true),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty &&
|
|
||||||
_mgPerDlController!.text.trim().isEmpty) {
|
|
||||||
return 'How many mg/dl or mmol/l does rhe rate make up for?';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.both ||
|
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.bothForDetail
|
|
||||||
? IconButton(
|
|
||||||
onPressed: () => convertBetweenMgPerDlAndMmolPerL(
|
|
||||||
calculateFrom: GlucoseMeasurement.mgPerDl),
|
|
||||||
icon: const Icon(Icons.calculate),
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Bolus Units',
|
|
||||||
suffixText: 'U',
|
|
||||||
),
|
|
||||||
controller: _bolusGlucoseController,
|
|
||||||
keyboardType:
|
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
),
|
|
||||||
// TODO: change field functionality according to time format
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Delayed Bolus Duration',
|
|
||||||
suffixText: ' min',
|
|
||||||
),
|
|
||||||
controller: _delayedBolusDurationController,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Delayed Bolus Units',
|
|
||||||
),
|
|
||||||
controller: _delayedBolusRateController,
|
|
||||||
keyboardType:
|
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: _notesController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Notes',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
511
lib/screens/log/log_event/log_event_detail.dart
Normal file
511
lib/screens/log/log_event/log_event_detail.dart
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
import 'package:diameter/components/detail.dart';
|
||||||
|
import 'package:diameter/components/forms/boolean_form_field.dart';
|
||||||
|
import 'package:diameter/components/forms/date_time_form_field.dart';
|
||||||
|
import 'package:diameter/components/forms/time_of_day_form_field.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
|
import 'package:diameter/models/basal_profile.dart';
|
||||||
|
import 'package:diameter/models/bolus_profile.dart';
|
||||||
|
import 'package:diameter/models/log_event.dart';
|
||||||
|
import 'package:diameter/models/log_event_type.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/basal/basal_profile_detail.dart';
|
||||||
|
import 'package:diameter/screens/bolus/bolus_profile_detail.dart';
|
||||||
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LogEventDetailScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/log-event';
|
||||||
|
|
||||||
|
final int logEntryId;
|
||||||
|
final int endLogEntryId;
|
||||||
|
final int id;
|
||||||
|
final DateTime? suggestedDate;
|
||||||
|
|
||||||
|
const LogEventDetailScreen(
|
||||||
|
{Key? key, this.logEntryId = 0, this.endLogEntryId = 0, this.id = 0, this.suggestedDate})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogEventDetailScreenState createState() => _LogEventDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
|
||||||
|
LogEvent? _logEvent;
|
||||||
|
bool _isNew = true;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
List<BolusProfile> _bolusProfiles = [];
|
||||||
|
List<BasalProfile> _basalProfiles = [];
|
||||||
|
|
||||||
|
final GlobalKey<FormState> _logEventForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
late DateTime _time;
|
||||||
|
DateTime? _endTime;
|
||||||
|
|
||||||
|
final _timeController = TextEditingController(text: '');
|
||||||
|
final _endTimeController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
final _dateController = TextEditingController(text: '');
|
||||||
|
final _endDateController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
final _reminderDurationController = TextEditingController(text: '');
|
||||||
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
LogEventType? _eventType;
|
||||||
|
final _eventTypeController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
bool _hasEndTime = false;
|
||||||
|
|
||||||
|
BolusProfile? _bolusProfile;
|
||||||
|
BasalProfile? _basalProfile;
|
||||||
|
final _bolusProfileController = TextEditingController(text: '');
|
||||||
|
final _basalProfileController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
List<LogEventType> _logEventTypes = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
reload();
|
||||||
|
|
||||||
|
_bolusProfiles = BolusProfile.getAll();
|
||||||
|
_basalProfiles = BasalProfile.getAll();
|
||||||
|
|
||||||
|
if (widget.id != 0) {
|
||||||
|
_reminderDurationController.text =
|
||||||
|
(_logEvent!.reminderDuration ?? '').toString();
|
||||||
|
_hasEndTime = _logEvent!.hasEndTime;
|
||||||
|
_notesController.text = _logEvent!.notes ?? '';
|
||||||
|
|
||||||
|
_eventType = _logEvent!.eventType.target;
|
||||||
|
_eventTypeController.text = (_eventType ?? '').toString();
|
||||||
|
|
||||||
|
_basalProfile = _logEvent!.basalProfile.target;
|
||||||
|
_basalProfileController.text = (_basalProfile ?? '').toString();
|
||||||
|
|
||||||
|
_bolusProfile = _logEvent!.bolusProfile.target;
|
||||||
|
_bolusProfileController.text = (_bolusProfile ?? '').toString();
|
||||||
|
|
||||||
|
_time = _logEvent!.time;
|
||||||
|
_endTime = _logEvent!.endTime;
|
||||||
|
} else {
|
||||||
|
_time = widget.suggestedDate ?? DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logEventTypes = LogEventType.getAll();
|
||||||
|
|
||||||
|
updateTime();
|
||||||
|
updateEndTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_timeController.dispose();
|
||||||
|
_endTimeController.dispose();
|
||||||
|
_dateController.dispose();
|
||||||
|
_endDateController.dispose();
|
||||||
|
_reminderDurationController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
_eventTypeController.dispose();
|
||||||
|
_bolusProfileController.dispose();
|
||||||
|
_basalProfileController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_logEvent = LogEvent.get(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _logEvent == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTime() {
|
||||||
|
_timeController.text = DateTimeUtils.displayTime(_time);
|
||||||
|
_dateController.text = DateTimeUtils.displayDate(_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateEndTime() {
|
||||||
|
_endTimeController.text = DateTimeUtils.displayTime(_endTime);
|
||||||
|
_endDateController.text = DateTimeUtils.displayDate(_endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateBasalProfile(BasalProfile? value) {
|
||||||
|
setState(() {
|
||||||
|
_basalProfile = value;
|
||||||
|
_basalProfileController.text = (_basalProfile ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateBolusProfile(BolusProfile? value) {
|
||||||
|
setState(() {
|
||||||
|
_bolusProfile = value;
|
||||||
|
_bolusProfileController.text = (_bolusProfile ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSelectEventType(LogEventType? eventType) {
|
||||||
|
setState(() {
|
||||||
|
_eventType = eventType;
|
||||||
|
_eventTypeController.text = (_eventType ?? '').toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventType != null) {
|
||||||
|
setState(() {
|
||||||
|
_hasEndTime = eventType.hasEndTime;
|
||||||
|
if (eventType.defaultReminderDuration != null) {
|
||||||
|
_reminderDurationController.text =
|
||||||
|
eventType.defaultReminderDuration.toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventType.basalProfile.target != null) {
|
||||||
|
updateBasalProfile(eventType.basalProfile.target);
|
||||||
|
}
|
||||||
|
if (eventType.bolusProfile.target != null) {
|
||||||
|
updateBolusProfile(eventType.bolusProfile.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> checkIfActiveEventOfTypeExistsBeforeSaving() async {
|
||||||
|
if (_eventType != null &&
|
||||||
|
LogEvent.eventTypeExistsForTime(_eventType!.id, _time)) {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: const Text(
|
||||||
|
'An Event of this type is already active within the set time frame. What would you like to do?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, 'DISCARD'),
|
||||||
|
child: const Text('DISCARD'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, 'EDIT'),
|
||||||
|
child: const Text('KEEP EDITING'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, 'SAVE'),
|
||||||
|
child: const Text('SAVE'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).then((value) async {
|
||||||
|
if (value == 'DISCARD') {
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else if (value == 'SAVE') {
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSave() {
|
||||||
|
LogEvent event = LogEvent(
|
||||||
|
id: widget.id,
|
||||||
|
time: _time,
|
||||||
|
endTime: _endTime,
|
||||||
|
hasEndTime: _hasEndTime,
|
||||||
|
reminderDuration: int.tryParse(_reminderDurationController.text),
|
||||||
|
notes: _notesController.text,
|
||||||
|
);
|
||||||
|
event.eventType.target = _eventType;
|
||||||
|
event.basalProfile.target = _basalProfile;
|
||||||
|
event.bolusProfile.target = _bolusProfile;
|
||||||
|
LogEvent.put(event);
|
||||||
|
Navigator.pop(context, ['${_isNew ? 'New' : ''} Event Saved', event]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSaveAction() async {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
if (_logEventForm.currentState!.validate()) {
|
||||||
|
await checkIfActiveEventOfTypeExistsBeforeSaving();
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCancelAction() {
|
||||||
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
|
((_isNew &&
|
||||||
|
(_notesController.text != '' ||
|
||||||
|
_eventType != null ||
|
||||||
|
_hasEndTime)) ||
|
||||||
|
(!_isNew &&
|
||||||
|
(_notesController.text != (_logEvent!.notes ?? '') ||
|
||||||
|
_eventType != _logEvent!.eventType.target ||
|
||||||
|
_hasEndTime != _logEvent!.hasEndTime)))) {
|
||||||
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
isNew: _isNew,
|
||||||
|
onSave: handleSaveAction,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_isNew ? 'New Event' : 'Edit Event'),
|
||||||
|
),
|
||||||
|
drawer: const Navigation(currentLocation: LogEventDetailScreen.routeName),
|
||||||
|
body: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
FormWrapper(
|
||||||
|
formState: _logEventForm,
|
||||||
|
fields: [
|
||||||
|
AutoCompleteDropdownButton<LogEventType>(
|
||||||
|
controller: _eventTypeController,
|
||||||
|
selectedItem: _eventType,
|
||||||
|
label: 'Event Type',
|
||||||
|
items: _logEventTypes,
|
||||||
|
onChanged: onSelectEventType,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: DateTimeFormField(
|
||||||
|
date: _time,
|
||||||
|
label: _hasEndTime ? 'Start Date' : 'Date',
|
||||||
|
controller: _dateController,
|
||||||
|
onChanged: (newTime) {
|
||||||
|
if (newTime != null) {
|
||||||
|
setState(() {
|
||||||
|
_time = DateTime(newTime.year, newTime.month,
|
||||||
|
newTime.day, _time.hour, _time.minute);
|
||||||
|
});
|
||||||
|
updateTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: TimeOfDayFormField(
|
||||||
|
time: TimeOfDay.fromDateTime(_time),
|
||||||
|
label: _hasEndTime ? 'Start Time' : 'Time',
|
||||||
|
controller: _timeController,
|
||||||
|
onChanged: (newTime) {
|
||||||
|
if (newTime != null) {
|
||||||
|
setState(() {
|
||||||
|
_time = DateTime(_time.year, _time.month,
|
||||||
|
_time.day, newTime.hour, newTime.minute);
|
||||||
|
});
|
||||||
|
updateTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
BooleanFormField(
|
||||||
|
value: _hasEndTime,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_hasEndTime = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
label: 'has end time',
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: _hasEndTime
|
||||||
|
? [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: DateTimeFormField(
|
||||||
|
date: _endTime ?? now,
|
||||||
|
label: 'End Date',
|
||||||
|
controller: _endDateController,
|
||||||
|
onChanged: (newTime) {
|
||||||
|
if (newTime != null) {
|
||||||
|
setState(() {
|
||||||
|
_endTime = DateTime(
|
||||||
|
newTime.year,
|
||||||
|
newTime.month,
|
||||||
|
newTime.day,
|
||||||
|
_endTime?.hour ?? 0,
|
||||||
|
_endTime?.minute ?? 0);
|
||||||
|
});
|
||||||
|
updateEndTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: TimeOfDayFormField(
|
||||||
|
time: TimeOfDay.fromDateTime(
|
||||||
|
_endTime ?? now),
|
||||||
|
label: 'End Time',
|
||||||
|
controller: _endTimeController,
|
||||||
|
onChanged: (newTime) {
|
||||||
|
if (newTime != null) {
|
||||||
|
setState(() {
|
||||||
|
_endTime = DateTime(
|
||||||
|
_endTime?.year ?? now.year,
|
||||||
|
_endTime?.month ?? now.month,
|
||||||
|
_endTime?.day ?? now.day,
|
||||||
|
newTime.hour,
|
||||||
|
newTime.minute);
|
||||||
|
});
|
||||||
|
updateEndTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _reminderDurationController,
|
||||||
|
keyboardType:
|
||||||
|
const TextInputType.numberWithOptions(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Default Reminder Duration',
|
||||||
|
suffixText: ' min',
|
||||||
|
enabled: _hasEndTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<
|
||||||
|
BolusProfile>(
|
||||||
|
controller: _bolusProfileController,
|
||||||
|
selectedItem: _bolusProfile,
|
||||||
|
label: 'Bolus Profile',
|
||||||
|
items: _bolusProfiles,
|
||||||
|
onChanged: updateBolusProfile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _bolusProfile ==
|
||||||
|
null
|
||||||
|
? const BolusProfileDetailScreen()
|
||||||
|
: BolusProfileDetailScreen(
|
||||||
|
id: _basalProfile!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updateBolusProfile(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_bolusProfile == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<
|
||||||
|
BasalProfile>(
|
||||||
|
controller: _basalProfileController,
|
||||||
|
selectedItem: _basalProfile,
|
||||||
|
label: 'Basal Profile',
|
||||||
|
items: _basalProfiles,
|
||||||
|
onChanged: updateBasalProfile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _basalProfile ==
|
||||||
|
null
|
||||||
|
? const BasalProfileDetailScreen()
|
||||||
|
: BasalProfileDetailScreen(
|
||||||
|
id: _basalProfile!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updateBasalProfile(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_basalProfile == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: _isSaving ? null : handleSaveAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
366
lib/screens/log/log_event/log_event_list.dart
Normal file
366
lib/screens/log/log_event/log_event_list.dart
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/models/log_event.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/screens/log/log_event/log_event_detail.dart';
|
||||||
|
import 'package:diameter/utils/date_time_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
|
||||||
|
class LogEventListScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/log-events';
|
||||||
|
const LogEventListScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogEventListScreenState createState() => _LogEventListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogEventListScreenState extends State<LogEventListScreen> {
|
||||||
|
List<LogEvent> _activeEvents = [];
|
||||||
|
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());
|
||||||
|
_logEvents = LogEvent.getAllForDate(_date);
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleAddNewEvent() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return LogEventDetailScreen(
|
||||||
|
suggestedDate: _date.isAtSameMomentAs(DateTime(now.year, now.month, now.day)) ? now : _date,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleEditAction(LogEvent event) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LogEventDetailScreen(
|
||||||
|
id: event.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDelete(LogEvent logEvent) {
|
||||||
|
LogEvent.remove(logEvent.id);
|
||||||
|
reload(message: 'Event deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeleteAction(LogEvent logEvent) async {
|
||||||
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
|
DialogUtils.showConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
onConfirm: () => onDelete(logEvent),
|
||||||
|
message: 'Are you sure you want to delete this Event?',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onDelete(logEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onStop(LogEvent event) async {
|
||||||
|
event.endTime = DateTime.now();
|
||||||
|
LogEvent.put(event);
|
||||||
|
reload(message: 'Event ended');
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleStopAction(LogEvent event) async {
|
||||||
|
if (Settings.get().showConfirmationDialogOnStopEvent) {
|
||||||
|
DialogUtils.showConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
onConfirm: () => onStop(event),
|
||||||
|
message: 'Are you sure you want to end this Event?',
|
||||||
|
confirmationLabel: 'END EVENT',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onStop(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChangeDate(DateTime? date) {
|
||||||
|
if (date != null) {
|
||||||
|
setState(() {
|
||||||
|
_date = DateTime(date.year, date.month, date.day);
|
||||||
|
_dateController.text = DateTimeUtils.displayDate(date);
|
||||||
|
});
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Log Events'),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => onChangeDate(DateTime.now()),
|
||||||
|
icon: const Icon(Icons.today)),
|
||||||
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
drawer: const Navigation(currentLocation: LogEventListScreen.routeName),
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() {
|
||||||
|
_showActive = !_showActive;
|
||||||
|
}),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'ACTIVE EVENTS',
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(_showActive
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
!_showActive ? Container() :
|
||||||
|
_activeEvents.isNotEmpty
|
||||||
|
? ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _activeEvents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
LogEvent event = _activeEvents[index];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
handleEditAction(event);
|
||||||
|
},
|
||||||
|
title: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateTimeUtils.displayDateTime(event.time),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
(event.title ??
|
||||||
|
event.eventType.target?.value ??
|
||||||
|
'')
|
||||||
|
.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
event.hasEndTime && event.endTime == null
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.stop,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleStopAction(event),
|
||||||
|
)
|
||||||
|
: const SizedBox(width: 50),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleEditAction(event),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleDeleteAction(event),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text('There are no Active Events!'),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(10.0),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _date.isAtSameMomentAs(DateTime(2000, 1, 1))
|
||||||
|
? null
|
||||||
|
: () =>
|
||||||
|
onChangeDate(_date.subtract(const Duration(days: 1))),
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final newTime = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _date,
|
||||||
|
firstDate: DateTime(2000, 1, 1),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
onChangeDate(newTime);
|
||||||
|
},
|
||||||
|
child: Expanded(
|
||||||
|
child: Text(
|
||||||
|
DateTimeUtils.displayDate(_date).toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed:
|
||||||
|
_date.add(const Duration(days: 1)).isBefore(DateTime.now())
|
||||||
|
? () => onChangeDate(_date.add(const Duration(days: 1)))
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.arrow_forward),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _logEvents.isNotEmpty
|
||||||
|
? Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _logEvents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
LogEvent event = _logEvents[index];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
handleEditAction(event);
|
||||||
|
},
|
||||||
|
title: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateTimeUtils.displayTime(event.isEndEvent
|
||||||
|
? event.endTime
|
||||||
|
: event.time),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
(event.title ??
|
||||||
|
event.eventType.target?.value ??
|
||||||
|
'')
|
||||||
|
.toUpperCase(),
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
event.hasEndTime && event.endTime == null
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.stop,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
handleStopAction(event),
|
||||||
|
)
|
||||||
|
: const SizedBox(width: 50),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleEditAction(event),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () => handleDeleteAction(event),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
: const Center(
|
||||||
|
child: Text('There are no Events for that date!'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: handleAddNewEvent,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
304
lib/screens/log/log_event/log_event_type_detail.dart
Normal file
304
lib/screens/log/log_event/log_event_type_detail.dart
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import 'package:diameter/components/detail.dart';
|
||||||
|
import 'package:diameter/components/forms/boolean_form_field.dart';
|
||||||
|
import 'package:diameter/components/forms/duration_form_field.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
|
import 'package:diameter/models/basal_profile.dart';
|
||||||
|
import 'package:diameter/models/bolus_profile.dart';
|
||||||
|
import 'package:diameter/models/log_event_type.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/basal/basal_profile_detail.dart';
|
||||||
|
import 'package:diameter/screens/bolus/bolus_profile_detail.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class EventTypeDetailScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/log-event-type';
|
||||||
|
final int id;
|
||||||
|
const EventTypeDetailScreen({Key? key, this.id = 0}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EventTypeDetailScreenState createState() => _EventTypeDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EventTypeDetailScreenState extends State<EventTypeDetailScreen> {
|
||||||
|
LogEventType? _logEventType;
|
||||||
|
bool _isNew = true;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
List<BolusProfile> _bolusProfiles = [];
|
||||||
|
List<BasalProfile> _basalProfiles = [];
|
||||||
|
|
||||||
|
final GlobalKey<FormState> _logEventTypeForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
final _valueController = TextEditingController(text: '');
|
||||||
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
bool _hasEndTime = false;
|
||||||
|
int _defaultReminderDuration = 0;
|
||||||
|
BolusProfile? _bolusProfile;
|
||||||
|
BasalProfile? _basalProfile;
|
||||||
|
final _bolusProfileController = TextEditingController(text: '');
|
||||||
|
final _basalProfileController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
reload();
|
||||||
|
|
||||||
|
_bolusProfiles = BolusProfile.getAll();
|
||||||
|
_basalProfiles = BasalProfile.getAll();
|
||||||
|
|
||||||
|
if (_logEventType != null) {
|
||||||
|
_valueController.text = _logEventType!.value;
|
||||||
|
_defaultReminderDuration =
|
||||||
|
_logEventType!.defaultReminderDuration ?? 0;
|
||||||
|
_hasEndTime = _logEventType!.hasEndTime;
|
||||||
|
_notesController.text = _logEventType!.notes ?? '';
|
||||||
|
_basalProfile = _logEventType!.basalProfile.target;
|
||||||
|
_basalProfileController.text = (_basalProfile ?? '').toString();
|
||||||
|
_bolusProfile = _logEventType!.bolusProfile.target;
|
||||||
|
_bolusProfileController.text = (_bolusProfile ?? '').toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_valueController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
_bolusProfileController.dispose();
|
||||||
|
_basalProfileController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_logEventType = LogEventType.get(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _logEventType == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateBasalProfile(BasalProfile? value) {
|
||||||
|
setState(() {
|
||||||
|
_basalProfile = value;
|
||||||
|
_basalProfileController.text = (_basalProfile ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateBolusProfile(BolusProfile? value) {
|
||||||
|
setState(() {
|
||||||
|
_bolusProfile = value;
|
||||||
|
_bolusProfileController.text = (_bolusProfile ?? '').toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSaveAction() async {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
if (_logEventTypeForm.currentState!.validate()) {
|
||||||
|
LogEventType eventType = LogEventType(
|
||||||
|
id: widget.id,
|
||||||
|
value: _valueController.text,
|
||||||
|
notes: _notesController.text,
|
||||||
|
defaultReminderDuration: _defaultReminderDuration,
|
||||||
|
hasEndTime: _hasEndTime,
|
||||||
|
);
|
||||||
|
eventType.basalProfile.target = _basalProfile;
|
||||||
|
eventType.bolusProfile.target = _bolusProfile;
|
||||||
|
LogEventType.put(eventType);
|
||||||
|
Navigator.pop(
|
||||||
|
context, ['${_isNew ? 'New' : ''} Log Event Type Saved', eventType]);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCancelAction() {
|
||||||
|
bool isNew = _logEventType == null;
|
||||||
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
|
((isNew &&
|
||||||
|
(_valueController.text != '' ||
|
||||||
|
_defaultReminderDuration != 0 ||
|
||||||
|
_notesController.text != '' ||
|
||||||
|
_hasEndTime)) ||
|
||||||
|
(!isNew &&
|
||||||
|
(_valueController.text != _logEventType!.value ||
|
||||||
|
_defaultReminderDuration !=
|
||||||
|
_logEventType!.defaultReminderDuration ||
|
||||||
|
_notesController.text != (_logEventType!.notes ?? '') ||
|
||||||
|
_hasEndTime != _logEventType!.hasEndTime)))) {
|
||||||
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
isNew: isNew,
|
||||||
|
onSave: handleSaveAction,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_isNew ? 'New Log Event Type' : _logEventType!.value),
|
||||||
|
),
|
||||||
|
drawer:
|
||||||
|
const Navigation(currentLocation: EventTypeDetailScreen.routeName),
|
||||||
|
body: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
FormWrapper(formState: _logEventTypeForm, fields: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _valueController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BooleanFormField(
|
||||||
|
value: _hasEndTime,
|
||||||
|
label: 'has end time',
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_hasEndTime = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: _hasEndTime
|
||||||
|
? [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: DurationFormField(
|
||||||
|
minutes: _defaultReminderDuration,
|
||||||
|
label: 'Default Reminder Duration',
|
||||||
|
onChanged: (value) => _defaultReminderDuration = value ?? 0,
|
||||||
|
showSteppers: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<
|
||||||
|
BolusProfile>(
|
||||||
|
selectedItem: _bolusProfile,
|
||||||
|
controller: _bolusProfileController,
|
||||||
|
label: 'Bolus Profile',
|
||||||
|
items: _bolusProfiles,
|
||||||
|
onChanged: updateBolusProfile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _bolusProfile ==
|
||||||
|
null
|
||||||
|
? const BolusProfileDetailScreen()
|
||||||
|
: BolusProfileDetailScreen(
|
||||||
|
id: _bolusProfile!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
setState(() {
|
||||||
|
updateBolusProfile(result?[1]);
|
||||||
|
});
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_bolusProfile == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
AutoCompleteDropdownButton<BasalProfile>(
|
||||||
|
controller: _basalProfileController,
|
||||||
|
selectedItem: _basalProfile,
|
||||||
|
label: 'Basal Profile',
|
||||||
|
items: _basalProfiles,
|
||||||
|
onChanged: updateBasalProfile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _basalProfile ==
|
||||||
|
null
|
||||||
|
? const BasalProfileDetailScreen()
|
||||||
|
: BasalProfileDetailScreen(
|
||||||
|
id: _basalProfile!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
updateBasalProfile(result?[1]);
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_basalProfile == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: _isSaving ? null : handleSaveAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
123
lib/screens/log/log_event/log_event_type_list.dart
Normal file
123
lib/screens/log/log_event/log_event_type_list.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import 'package:diameter/models/log_event_type.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/log/log_event/log_event_type_detail.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LogEventTypeListScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/log-event-types';
|
||||||
|
const LogEventTypeListScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LogEventTypeListScreenState createState() => _LogEventTypeListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogEventTypeListScreenState extends State<LogEventTypeListScreen> {
|
||||||
|
List<LogEventType> _logEventTypes = [];
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
setState(() {
|
||||||
|
_logEventTypes = LogEventType.getAll();
|
||||||
|
});
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Log Event Types'), actions: <Widget>[
|
||||||
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
|
]),
|
||||||
|
drawer:
|
||||||
|
const Navigation(currentLocation: LogEventTypeListScreen.routeName),
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: _logEventTypes.isNotEmpty
|
||||||
|
? Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _logEventTypes.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final logEventType = _logEventTypes[index];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EventTypeDetailScreen(
|
||||||
|
id: logEventType.id),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
logEventType.value.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
subtitle: Text(logEventType.notes ?? ''),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
LogEventType.remove(logEventType.id);
|
||||||
|
reload(message: 'Log Event Type deleted');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete,
|
||||||
|
color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child:
|
||||||
|
Text('You have not created any Log Event Types yet!'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const EventTypeDetailScreen(),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,172 +0,0 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
|
||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/components/forms.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/log_entry.dart';
|
|
||||||
import 'package:diameter/models/log_event.dart';
|
|
||||||
import 'package:diameter/models/log_event_type.dart';
|
|
||||||
import 'package:diameter/navigation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LogEventDetailScreen extends StatefulWidget {
|
|
||||||
static const String routeName = '/log-event';
|
|
||||||
final LogEntry? logEntry;
|
|
||||||
final LogEntry? endLogEntry;
|
|
||||||
final LogEvent? logEvent;
|
|
||||||
const LogEventDetailScreen(
|
|
||||||
{Key? key, this.logEntry, this.endLogEntry, this.logEvent})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LogEventDetailScreenState createState() => _LogEventDetailScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogEventDetailScreenState extends State<LogEventDetailScreen> {
|
|
||||||
final GlobalKey<FormState> _logEventForm = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
final _notesController = TextEditingController(text: '');
|
|
||||||
LogEventType? _eventType;
|
|
||||||
bool _hasEndTime = false;
|
|
||||||
|
|
||||||
List<LogEventType> _logEventTypes = [];
|
|
||||||
|
|
||||||
bool _isSaving = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
if (widget.logEvent != null) {
|
|
||||||
_notesController.text = widget.logEvent!.notes ?? '';
|
|
||||||
_eventType = widget.logEvent!.eventType.target;
|
|
||||||
_hasEndTime = widget.logEvent!.hasEndTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logEventTypes = LogEventType.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleSaveAction() async {
|
|
||||||
setState(() {
|
|
||||||
_isSaving = true;
|
|
||||||
});
|
|
||||||
if (_logEventForm.currentState!.validate()) {
|
|
||||||
bool isNew = widget.logEvent == null;
|
|
||||||
// isNew
|
|
||||||
// ? await LogEvent.save(
|
|
||||||
// logEntry: widget.logEntry!.objectId!,
|
|
||||||
// eventType: _eventType!,
|
|
||||||
// time: widget.logEntry!.time,
|
|
||||||
// hasEndTime: _hasEndTime,
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// )
|
|
||||||
// : await LogEvent.update(
|
|
||||||
// widget.logEvent!.objectId!,
|
|
||||||
// eventType: _eventType!,
|
|
||||||
// time: widget.logEntry!.time,
|
|
||||||
// hasEndTime: _hasEndTime,
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// );
|
|
||||||
LogEvent event = LogEvent(
|
|
||||||
id: widget.logEvent?.id ?? 0,
|
|
||||||
time: widget.logEntry!.time,
|
|
||||||
hasEndTime: _hasEndTime,
|
|
||||||
notes: _notesController.text,
|
|
||||||
);
|
|
||||||
event.eventType.target = _eventType;
|
|
||||||
LogEvent.put(event);
|
|
||||||
Navigator.pop(context, '${isNew ? 'New' : ''} Event Saved');
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_isSaving = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleCancelAction() {
|
|
||||||
bool isNew = widget.logEvent == null;
|
|
||||||
if (showConfirmationDialogOnCancel &&
|
|
||||||
((isNew &&
|
|
||||||
(_notesController.text != '' ||
|
|
||||||
_eventType != null ||
|
|
||||||
_hasEndTime)) ||
|
|
||||||
(!isNew &&
|
|
||||||
(_notesController.text != (widget.logEvent!.notes ?? '') ||
|
|
||||||
_eventType != widget.logEvent!.eventType.target ||
|
|
||||||
_hasEndTime != widget.logEvent!.hasEndTime)))) {
|
|
||||||
Dialogs.showCancelConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
isNew: isNew,
|
|
||||||
onSave: handleSaveAction,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
bool isNew = widget.logEvent == null;
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(isNew ? 'New Event' : 'Edit Event'),
|
|
||||||
),
|
|
||||||
drawer: const Navigation(currentLocation: LogEventDetailScreen.routeName),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
StyledForm(
|
|
||||||
formState: _logEventForm,
|
|
||||||
fields: [
|
|
||||||
StyledDropdownButton<LogEventType>(
|
|
||||||
selectedItem: _eventType,
|
|
||||||
label: 'Event Type',
|
|
||||||
items: _logEventTypes,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_eventType = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// StyledFutureDropdownButton<LogEventType>(
|
|
||||||
// selectedItem: _eventType,
|
|
||||||
// label: 'Event Type',
|
|
||||||
// items: _logEventTypes,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
// renderItem: (item) => Text(item.value),
|
|
||||||
// onChanged: (value) {
|
|
||||||
// setState(() {
|
|
||||||
// _eventType = value;
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
StyledBooleanFormField(
|
|
||||||
value: _hasEndTime,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_hasEndTime = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
label: 'active',
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: _notesController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Notes',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// ActiveLogEventListScreen(onSetEndTime: onSetEndTime)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: DetailBottomRow(
|
|
||||||
onCancel: handleCancelAction,
|
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/log_entry.dart';
|
|
||||||
import 'package:diameter/models/log_event.dart';
|
|
||||||
import 'package:diameter/screens/log/log_event_detail.dart';
|
|
||||||
import 'package:diameter/utils/date_time_utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LogEventListScreen extends StatefulWidget {
|
|
||||||
final LogEntry logEntry;
|
|
||||||
final Function() reload;
|
|
||||||
|
|
||||||
const LogEventListScreen({Key? key, required this.logEntry, required this.reload})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LogEventListScreenState createState() => _LogEventListScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogEventListScreenState extends State<LogEventListScreen> {
|
|
||||||
void reload({String? message}) {
|
|
||||||
widget.reload();
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
if (message != null) {
|
|
||||||
var snackBar = SnackBar(
|
|
||||||
content: Text(message),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
..removeCurrentSnackBar()
|
|
||||||
..showSnackBar(snackBar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleEditAction(LogEvent event) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => LogEventDetailScreen(
|
|
||||||
endLogEntry: widget.logEntry,
|
|
||||||
logEvent: event,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).then((message) => reload(message: message));
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDelete(LogEvent logEvent) {
|
|
||||||
LogEvent.remove(logEvent.id);
|
|
||||||
reload(message: 'Event deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleDeleteAction(LogEvent logEvent) async {
|
|
||||||
if (showConfirmationDialogOnDelete) {
|
|
||||||
Dialogs.showConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
onConfirm: () => onDelete(logEvent),
|
|
||||||
message: 'Are you sure you want to delete this Event?',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
onDelete(logEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// TODO: add button for active events
|
|
||||||
Expanded(
|
|
||||||
child: (widget.logEntry.events.isNotEmpty || widget.logEntry.endedEvents.isNotEmpty)
|
|
||||||
? ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: widget.logEntry.events.length + widget.logEntry.endedEvents.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final event = (widget.logEntry.events + widget.logEntry.endedEvents)[index];
|
|
||||||
return ListTile(
|
|
||||||
onTap: () {
|
|
||||||
handleEditAction(event);
|
|
||||||
},
|
|
||||||
title: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(event.eventType.target?.value ?? '')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'${DateTimeUtils.displayDateTime(event.time)}${event.hasEndTime ? ' - ${DateTimeUtils.displayDateTime(event.endTime, fallback: '(ongoing)')}' : ''}'),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
onPressed: () => handleDeleteAction(event),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: const Center(
|
|
||||||
child: Text('You have not added any Events to this Log Entry yet!'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,171 +0,0 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
|
||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/components/forms.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/log_event_type.dart';
|
|
||||||
import 'package:diameter/navigation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LogEventTypeDetailScreen extends StatefulWidget {
|
|
||||||
static const String routeName = '/log-event-type';
|
|
||||||
final LogEventType? logEventType;
|
|
||||||
const LogEventTypeDetailScreen({Key? key, this.logEventType})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LogEventTypeDetailScreenState createState() =>
|
|
||||||
_LogEventTypeDetailScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogEventTypeDetailScreenState extends State<LogEventTypeDetailScreen> {
|
|
||||||
final GlobalKey<FormState> _logEventTypeForm = GlobalKey<FormState>();
|
|
||||||
final _valueController = TextEditingController(text: '');
|
|
||||||
final _defaultReminderDurationController = TextEditingController(text: '');
|
|
||||||
final _notesController = TextEditingController(text: '');
|
|
||||||
bool _hasEndTime = false;
|
|
||||||
|
|
||||||
bool _isSaving = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
if (widget.logEventType != null) {
|
|
||||||
_valueController.text = widget.logEventType!.value;
|
|
||||||
_defaultReminderDurationController.text =
|
|
||||||
(widget.logEventType!.defaultReminderDuration ?? '').toString();
|
|
||||||
_notesController.text = widget.logEventType!.notes ?? '';
|
|
||||||
_hasEndTime = widget.logEventType!.hasEndTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleSaveAction() async {
|
|
||||||
setState(() {
|
|
||||||
_isSaving = true;
|
|
||||||
});
|
|
||||||
if (_logEventTypeForm.currentState!.validate()) {
|
|
||||||
bool isNew = widget.logEventType == null;
|
|
||||||
// isNew
|
|
||||||
// ? await LogEventType.save(
|
|
||||||
// value: _valueController.text,
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// defaultReminderDuration:
|
|
||||||
// int.tryParse(_defaultReminderDurationController.text),
|
|
||||||
// hasEndTime: _hasEndTime,
|
|
||||||
// )
|
|
||||||
// : await LogEventType.update(
|
|
||||||
// widget.logEventType!.objectId!,
|
|
||||||
// value: _valueController.text,
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// defaultReminderDuration:
|
|
||||||
// int.tryParse(_defaultReminderDurationController.text),
|
|
||||||
// hasEndTime: _hasEndTime,
|
|
||||||
// );
|
|
||||||
LogEventType.put(LogEventType(
|
|
||||||
id: widget.logEventType?.id ?? 0,
|
|
||||||
value: _valueController.text,
|
|
||||||
notes: _notesController.text,
|
|
||||||
defaultReminderDuration:
|
|
||||||
int.tryParse(_defaultReminderDurationController.text),
|
|
||||||
hasEndTime: _hasEndTime,
|
|
||||||
));
|
|
||||||
Navigator.pop(context, '${isNew ? 'New' : ''} Log Event Type Saved');
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_isSaving = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleCancelAction() {
|
|
||||||
bool isNew = widget.logEventType == null;
|
|
||||||
if (showConfirmationDialogOnCancel &&
|
|
||||||
((isNew &&
|
|
||||||
(_valueController.text != '' ||
|
|
||||||
int.tryParse(_defaultReminderDurationController.text) !=
|
|
||||||
null ||
|
|
||||||
_notesController.text != '' ||
|
|
||||||
_hasEndTime)) ||
|
|
||||||
(!isNew &&
|
|
||||||
(_valueController.text != widget.logEventType!.value ||
|
|
||||||
int.tryParse(_defaultReminderDurationController.text) !=
|
|
||||||
widget.logEventType!.defaultReminderDuration ||
|
|
||||||
_notesController.text !=
|
|
||||||
(widget.logEventType!.notes ?? '') ||
|
|
||||||
_hasEndTime != widget.logEventType!.hasEndTime)))) {
|
|
||||||
Dialogs.showCancelConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
isNew: isNew,
|
|
||||||
onSave: handleSaveAction,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
bool isNew = widget.logEventType == null;
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(isNew ? 'New Log Event Type' : widget.logEventType!.value),
|
|
||||||
),
|
|
||||||
drawer:
|
|
||||||
const Navigation(currentLocation: LogEventTypeDetailScreen.routeName),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
StyledForm(
|
|
||||||
formState: _logEventTypeForm,
|
|
||||||
fields: [
|
|
||||||
TextFormField(
|
|
||||||
controller: _valueController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Name',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty) {
|
|
||||||
return 'Empty name';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StyledBooleanFormField(
|
|
||||||
value: _hasEndTime,
|
|
||||||
label: 'has end time',
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_hasEndTime = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: _defaultReminderDurationController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Default Reminder Duration',
|
|
||||||
suffixText: ' min',
|
|
||||||
enabled: _hasEndTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: _notesController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Notes',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: DetailBottomRow(
|
|
||||||
onCancel: handleCancelAction,
|
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
// import 'package:diameter/components/progress_indicator.dart';
|
|
||||||
import 'package:diameter/models/log_event_type.dart';
|
|
||||||
import 'package:diameter/navigation.dart';
|
|
||||||
import 'package:diameter/screens/log/log_event_type_detail.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LogEventTypeListScreen extends StatefulWidget {
|
|
||||||
static const String routeName = '/log-event-types';
|
|
||||||
const LogEventTypeListScreen({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LogEventTypeListScreenState createState() => _LogEventTypeListScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogEventTypeListScreenState extends State<LogEventTypeListScreen> {
|
|
||||||
List<LogEventType> _logEventTypes = [];
|
|
||||||
|
|
||||||
void refresh({String? message}) {
|
|
||||||
setState(() {
|
|
||||||
_logEventTypes = LogEventType.getAll();
|
|
||||||
});
|
|
||||||
setState(() {
|
|
||||||
if (message != null) {
|
|
||||||
var snackBar = SnackBar(
|
|
||||||
content: Text(message),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
..removeCurrentSnackBar()
|
|
||||||
..showSnackBar(snackBar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: const Text('Log Event Types'), actions: <Widget>[
|
|
||||||
IconButton(onPressed: refresh, icon: const Icon(Icons.refresh))
|
|
||||||
]),
|
|
||||||
drawer:
|
|
||||||
const Navigation(currentLocation: LogEventTypeListScreen.routeName),
|
|
||||||
body: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: _logEventTypes.isNotEmpty ? ListView.builder(
|
|
||||||
padding: const EdgeInsets.all(10.0),
|
|
||||||
itemCount: _logEventTypes.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
// final logEventType = snapshot.data![index];
|
|
||||||
final logEventType = _logEventTypes[index];
|
|
||||||
return ListTile(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
LogEventTypeDetailScreen(
|
|
||||||
logEventType: logEventType),
|
|
||||||
),
|
|
||||||
).then((message) => refresh(message: message));
|
|
||||||
},
|
|
||||||
title: Text(logEventType.value),
|
|
||||||
subtitle: Text(logEventType.notes ?? ''),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
LogEventType.remove(logEventType.id);
|
|
||||||
// await logEventType.delete().then((_) {
|
|
||||||
refresh(
|
|
||||||
message: 'Log Event Type deleted');
|
|
||||||
// });
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete,
|
|
||||||
color: Colors.blue),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
) : const Center(
|
|
||||||
child: Text('You have not created any Log Event Types yet!'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const LogEventTypeDetailScreen(),
|
|
||||||
),
|
|
||||||
).then((message) => refresh(message: message));
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,561 +0,0 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
|
||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/components/forms.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/accuracy.dart';
|
|
||||||
import 'package:diameter/models/log_entry.dart';
|
|
||||||
import 'package:diameter/models/log_meal.dart';
|
|
||||||
import 'package:diameter/models/meal.dart';
|
|
||||||
import 'package:diameter/models/meal_category.dart';
|
|
||||||
import 'package:diameter/models/meal_portion_type.dart';
|
|
||||||
import 'package:diameter/models/meal_source.dart';
|
|
||||||
import 'package:diameter/navigation.dart';
|
|
||||||
import 'package:diameter/settings.dart';
|
|
||||||
import 'package:diameter/utils/utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LogMealDetailScreen extends StatefulWidget {
|
|
||||||
static const String routeName = '/log-meal';
|
|
||||||
final LogEntry logEntry;
|
|
||||||
final LogMeal? logMeal;
|
|
||||||
const LogMealDetailScreen({Key? key, required this.logEntry, this.logMeal})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LogMealDetailScreenState createState() => _LogMealDetailScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
|
|
||||||
final GlobalKey<FormState> _logMealForm = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
final _valueController = TextEditingController(text: '');
|
|
||||||
final _carbsRatioController = TextEditingController(text: '');
|
|
||||||
final _portionSizeController = TextEditingController(text: '');
|
|
||||||
final _carbsPerPortionController = TextEditingController(text: '');
|
|
||||||
final _bolusController = TextEditingController(text: '');
|
|
||||||
final _delayedBolusRateController = TextEditingController(text: '');
|
|
||||||
final _delayedBolusDurationController = TextEditingController(text: '');
|
|
||||||
final _notesController = TextEditingController(text: '');
|
|
||||||
Meal? _meal;
|
|
||||||
MealSource? _mealSource;
|
|
||||||
MealCategory? _mealCategory;
|
|
||||||
MealPortionType? _mealPortionType;
|
|
||||||
Accuracy? _portionSizeAccuracy;
|
|
||||||
Accuracy? _carbsRatioAccuracy;
|
|
||||||
|
|
||||||
List<Meal> _meals = [];
|
|
||||||
List<MealCategory> _mealCategories = [];
|
|
||||||
List<MealPortionType> _mealPortionTypes = [];
|
|
||||||
List<MealSource> _mealSources = [];
|
|
||||||
List<Accuracy> _portionSizeAccuracies = [];
|
|
||||||
List<Accuracy> _carbsRatioAccuracies = [];
|
|
||||||
|
|
||||||
bool _isSaving = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_portionSizeAccuracies = Accuracy.getAllForPortionSize();
|
|
||||||
_carbsRatioAccuracies = Accuracy.getAllForCarbsRatio();
|
|
||||||
_meals = Meal.getAll();
|
|
||||||
_mealCategories = MealCategory.getAll();
|
|
||||||
_mealPortionTypes = MealPortionType.getAll();
|
|
||||||
_mealSources = MealSource.getAll();
|
|
||||||
|
|
||||||
if (widget.logMeal != null) {
|
|
||||||
_valueController.text = widget.logMeal!.value;
|
|
||||||
_carbsRatioController.text =
|
|
||||||
(widget.logMeal!.carbsRatio ?? '').toString();
|
|
||||||
_portionSizeController.text =
|
|
||||||
(widget.logMeal!.portionSize ?? '').toString();
|
|
||||||
_carbsPerPortionController.text =
|
|
||||||
(widget.logMeal!.carbsPerPortion ?? '').toString();
|
|
||||||
_bolusController.text = (widget.logMeal!.bolus ?? '').toString();
|
|
||||||
_delayedBolusRateController.text =
|
|
||||||
(widget.logMeal!.delayedBolusRate ?? '').toString();
|
|
||||||
_delayedBolusDurationController.text =
|
|
||||||
(widget.logMeal!.delayedBolusDuration ?? '').toString();
|
|
||||||
_notesController.text = widget.logMeal!.notes ?? '';
|
|
||||||
|
|
||||||
// _meal = widget.logMeal!.meal;
|
|
||||||
// _source = widget.logMeal!.source;
|
|
||||||
// _category = widget.logMeal!.category;
|
|
||||||
// _portionType = widget.logMeal!.portionType;
|
|
||||||
// _portionSizeAccuracy = _portionSizeAccuracies.firstWhere((element) =>
|
|
||||||
// element.id ==
|
|
||||||
// int.tryParse(widget.logMeal!.portionSizeAccuracy ?? ''));
|
|
||||||
// _carbsRatioAccuracy = _carbsRatioAccuracies.firstWhere((element) =>
|
|
||||||
// element.id == int.tryParse(widget.logMeal!.carbsRatioAccuracy ?? ''));
|
|
||||||
// _portionSizeAccuracy = widget.meal!.portionSizeAccuracy;
|
|
||||||
// _carbsRatioAccuracy = widget.meal!.carbsRatioAccuracy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> onSelectMeal(Meal meal) async {
|
|
||||||
setState(() {
|
|
||||||
_meal = meal;
|
|
||||||
_valueController.text = meal.value;
|
|
||||||
if (meal.carbsRatio != null) {
|
|
||||||
_carbsRatioController.text = meal.carbsRatio.toString();
|
|
||||||
}
|
|
||||||
if (meal.portionSize != null) {
|
|
||||||
_portionSizeController.text = meal.portionSize.toString();
|
|
||||||
}
|
|
||||||
if (meal.carbsPerPortion != null) {
|
|
||||||
_carbsPerPortionController.text = meal.carbsPerPortion.toString();
|
|
||||||
}
|
|
||||||
if (meal.delayedBolusRate != null) {
|
|
||||||
_delayedBolusRateController.text = meal.delayedBolusRate.toString();
|
|
||||||
}
|
|
||||||
if (meal.delayedBolusDuration != null) {
|
|
||||||
_delayedBolusDurationController.text =
|
|
||||||
meal.delayedBolusDuration.toString();
|
|
||||||
}
|
|
||||||
if (meal.mealSource.hasValue) {
|
|
||||||
_mealSource = meal.mealSource.target;
|
|
||||||
}
|
|
||||||
if (meal.mealCategory.hasValue) {
|
|
||||||
_mealCategory = meal.mealCategory.target;
|
|
||||||
}
|
|
||||||
if (meal.mealPortionType.hasValue) {
|
|
||||||
_mealPortionType = meal.mealPortionType.target;
|
|
||||||
}
|
|
||||||
if (meal.portionSizeAccuracy.hasValue) {
|
|
||||||
_portionSizeAccuracy = meal.portionSizeAccuracy.target;
|
|
||||||
}
|
|
||||||
if (meal.carbsRatioAccuracy.hasValue) {
|
|
||||||
_carbsRatioAccuracy = meal.carbsRatioAccuracy.target;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleSaveAction() async {
|
|
||||||
setState(() {
|
|
||||||
_isSaving = true;
|
|
||||||
});
|
|
||||||
if (_logMealForm.currentState!.validate()) {
|
|
||||||
bool isNew = widget.logMeal == null;
|
|
||||||
// isNew
|
|
||||||
// ? await LogMeal.save(
|
|
||||||
// logEntry: widget.logEntry.objectId!,
|
|
||||||
// meal: _meal,
|
|
||||||
// value: _valueController.text,
|
|
||||||
// source: _mealSource,
|
|
||||||
// category: _category,
|
|
||||||
// portionType: _portionType,
|
|
||||||
// carbsRatio: double.tryParse(_carbsRatioController.text),
|
|
||||||
// portionSize: double.tryParse(_portionSizeController.text),
|
|
||||||
// carbsPerPortion: double.tryParse(_carbsPerPortionController.text),
|
|
||||||
// // portionSizeAccuracy: _portionSizeAccuracy,
|
|
||||||
// // carbsRatioAccuracy: _carbsRatioAccuracy,
|
|
||||||
// portionSizeAccuracy: _portionSizeAccuracy?.id.toString(),
|
|
||||||
// carbsRatioAccuracy: _carbsRatioAccuracy?.id.toString(),
|
|
||||||
// bolus: double.tryParse(_bolusController.text),
|
|
||||||
// delayedBolusDuration:
|
|
||||||
// int.tryParse(_delayedBolusDurationController.text),
|
|
||||||
// delayedBolusRate:
|
|
||||||
// double.tryParse(_delayedBolusRateController.text),
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// )
|
|
||||||
// : await LogMeal.update(
|
|
||||||
// widget.logMeal!.objectId!,
|
|
||||||
// meal: _meal,
|
|
||||||
// value: _valueController.text,
|
|
||||||
// source: _mealSource,
|
|
||||||
// category: _category,
|
|
||||||
// portionType: _portionType,
|
|
||||||
// carbsRatio: double.tryParse(_carbsRatioController.text),
|
|
||||||
// portionSize: double.tryParse(_portionSizeController.text),
|
|
||||||
// carbsPerPortion: double.tryParse(_carbsPerPortionController.text),
|
|
||||||
// // portionSizeAccuracy: _portionSizeAccuracy,
|
|
||||||
// // carbsRatioAccuracy: _carbsRatioAccuracy,
|
|
||||||
// portionSizeAccuracy: _portionSizeAccuracy?.id.toString(),
|
|
||||||
// carbsRatioAccuracy: _carbsRatioAccuracy?.id.toString(),
|
|
||||||
// bolus: double.tryParse(_bolusController.text),
|
|
||||||
// delayedBolusDuration:
|
|
||||||
// int.tryParse(_delayedBolusDurationController.text),
|
|
||||||
// delayedBolusRate:
|
|
||||||
// double.tryParse(_delayedBolusRateController.text),
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// );
|
|
||||||
LogMeal logMeal = LogMeal(
|
|
||||||
id: widget.logMeal?.id ?? 0,
|
|
||||||
value: _valueController.text,
|
|
||||||
carbsRatio: double.tryParse(_carbsRatioController.text),
|
|
||||||
portionSize: double.tryParse(_portionSizeController.text),
|
|
||||||
carbsPerPortion: double.tryParse(_carbsPerPortionController.text),
|
|
||||||
bolus: double.tryParse(_bolusController.text),
|
|
||||||
delayedBolusDuration:
|
|
||||||
int.tryParse(_delayedBolusDurationController.text),
|
|
||||||
delayedBolusRate: double.tryParse(_delayedBolusRateController.text),
|
|
||||||
notes: _notesController.text,
|
|
||||||
);
|
|
||||||
logMeal.meal.target = _meal;
|
|
||||||
logMeal.mealSource.target = _mealSource;
|
|
||||||
logMeal.mealCategory.target = _mealCategory;
|
|
||||||
logMeal.mealPortionType.target = _mealPortionType;
|
|
||||||
logMeal.portionSizeAccuracy.target = _portionSizeAccuracy;
|
|
||||||
logMeal.carbsRatioAccuracy.target = _carbsRatioAccuracy;
|
|
||||||
|
|
||||||
LogMeal.put(logMeal);
|
|
||||||
Navigator.pop(context, '${isNew ? 'New' : ''} Meal Saved');
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_isSaving = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleCancelAction() {
|
|
||||||
bool isNew = widget.logMeal == null;
|
|
||||||
if (showConfirmationDialogOnCancel &&
|
|
||||||
((isNew &&
|
|
||||||
(_valueController.text != '' ||
|
|
||||||
_meal != null ||
|
|
||||||
_mealSource != null ||
|
|
||||||
_mealCategory != null ||
|
|
||||||
_mealPortionType != null ||
|
|
||||||
double.tryParse(_carbsRatioController.text) != null ||
|
|
||||||
double.tryParse(_portionSizeController.text) != null ||
|
|
||||||
double.tryParse(_carbsPerPortionController.text) != null ||
|
|
||||||
_carbsRatioAccuracy != null ||
|
|
||||||
_portionSizeAccuracy != null ||
|
|
||||||
double.tryParse(_bolusController.text) != null ||
|
|
||||||
int.tryParse(_delayedBolusDurationController.text) !=
|
|
||||||
null ||
|
|
||||||
double.tryParse(_delayedBolusRateController.text) != null ||
|
|
||||||
_notesController.text != '')) ||
|
|
||||||
(!isNew &&
|
|
||||||
(_valueController.text != widget.logMeal!.value ||
|
|
||||||
_meal != widget.logMeal!.meal.target ||
|
|
||||||
_mealSource != widget.logMeal!.mealSource.target ||
|
|
||||||
_mealCategory != widget.logMeal!.mealCategory.target ||
|
|
||||||
_mealPortionType != widget.logMeal!.mealPortionType.target ||
|
|
||||||
double.tryParse(_carbsRatioController.text) !=
|
|
||||||
widget.logMeal!.carbsRatio ||
|
|
||||||
double.tryParse(_portionSizeController.text) !=
|
|
||||||
widget.logMeal!.portionSize ||
|
|
||||||
double.tryParse(_carbsPerPortionController.text) !=
|
|
||||||
widget.logMeal!.carbsPerPortion ||
|
|
||||||
// _carbsRatioAccuracy != widget.logMeal!.carbsRatioAccuracy ||
|
|
||||||
// _portionSizeAccuracy !=
|
|
||||||
// widget.logMeal!.portionSizeAccuracy ||
|
|
||||||
_carbsRatioAccuracy !=
|
|
||||||
widget.logMeal!.carbsRatioAccuracy.target ||
|
|
||||||
_portionSizeAccuracy !=
|
|
||||||
widget.logMeal!.portionSizeAccuracy.target ||
|
|
||||||
double.tryParse(_bolusController.text) !=
|
|
||||||
widget.logMeal!.bolus ||
|
|
||||||
int.tryParse(_delayedBolusDurationController.text) !=
|
|
||||||
widget.logMeal!.delayedBolusDuration ||
|
|
||||||
double.tryParse(_delayedBolusRateController.text) !=
|
|
||||||
widget.logMeal!.delayedBolusRate ||
|
|
||||||
_notesController.text != (widget.logMeal!.notes ?? ''))))) {
|
|
||||||
Dialogs.showCancelConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
isNew: isNew,
|
|
||||||
onSave: handleSaveAction,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void calculateThirdMeasurementOfPortionCarbsRelation(
|
|
||||||
{PortionCarbsParameter? parameterToBeCalculated}) {
|
|
||||||
double? carbsRatio;
|
|
||||||
double? portionSize;
|
|
||||||
double? carbsPerPortion;
|
|
||||||
|
|
||||||
if (parameterToBeCalculated != PortionCarbsParameter.carbsRatio &&
|
|
||||||
_carbsRatioController.text != '') {
|
|
||||||
carbsRatio = double.tryParse(_carbsRatioController.text);
|
|
||||||
}
|
|
||||||
if (parameterToBeCalculated != PortionCarbsParameter.portionSize &&
|
|
||||||
_portionSizeController.text != '') {
|
|
||||||
portionSize = double.tryParse(_portionSizeController.text);
|
|
||||||
}
|
|
||||||
if (parameterToBeCalculated != PortionCarbsParameter.carbsPerPortion &&
|
|
||||||
_carbsRatioController.text != '') {
|
|
||||||
carbsPerPortion = double.tryParse(_carbsPerPortionController.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (carbsRatio != null && portionSize != null && carbsPerPortion == null) {
|
|
||||||
setState(() {
|
|
||||||
_carbsPerPortionController.text =
|
|
||||||
Utils.calculateCarbsPerPortion(carbsRatio!, portionSize!)
|
|
||||||
.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (carbsRatio == null && portionSize != null && carbsPerPortion != null) {
|
|
||||||
setState(() {
|
|
||||||
_carbsRatioController.text =
|
|
||||||
Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!)
|
|
||||||
.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (carbsRatio != null && portionSize == null && carbsPerPortion != null) {
|
|
||||||
setState(() {
|
|
||||||
_portionSizeController.text =
|
|
||||||
Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!)
|
|
||||||
.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
bool isNew = widget.logMeal == null;
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(isNew ? 'New Meal' : widget.logMeal!.value),
|
|
||||||
),
|
|
||||||
drawer: const Navigation(currentLocation: LogMealDetailScreen.routeName),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
StyledForm(
|
|
||||||
formState: _logMealForm,
|
|
||||||
fields: [
|
|
||||||
TextFormField(
|
|
||||||
controller: _valueController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Name',
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty) {
|
|
||||||
return 'Empty name';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StyledDropdownButton<Meal>(
|
|
||||||
selectedItem: _meal,
|
|
||||||
label: 'Meal',
|
|
||||||
items: _meals,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value != null) {
|
|
||||||
onSelectMeal(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StyledDropdownButton<MealSource>(
|
|
||||||
selectedItem: _mealSource,
|
|
||||||
label: 'Meal Source',
|
|
||||||
items: _mealSources,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_mealSource = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StyledDropdownButton<MealCategory>(
|
|
||||||
selectedItem: _mealCategory,
|
|
||||||
label: 'Meal Category',
|
|
||||||
items: _mealCategories,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_mealCategory = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StyledDropdownButton<MealPortionType>(
|
|
||||||
selectedItem: _mealPortionType,
|
|
||||||
label: 'Meal Portion Type',
|
|
||||||
items: _mealPortionTypes,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_mealPortionType = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Carbs ratio',
|
|
||||||
suffixText: '%',
|
|
||||||
),
|
|
||||||
controller: _carbsRatioController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: true),
|
|
||||||
onChanged: (_) =>
|
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () =>
|
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation(
|
|
||||||
parameterToBeCalculated:
|
|
||||||
PortionCarbsParameter.carbsRatio),
|
|
||||||
icon: const Icon(Icons.calculate),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Portion size',
|
|
||||||
suffixText:
|
|
||||||
nutritionMeasurement == NutritionMeasurement.grams
|
|
||||||
? 'g'
|
|
||||||
: nutritionMeasurement ==
|
|
||||||
NutritionMeasurement.ounces
|
|
||||||
? 'oz'
|
|
||||||
: '',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
controller: _portionSizeController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: true),
|
|
||||||
onChanged: (_) =>
|
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () =>
|
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation(
|
|
||||||
parameterToBeCalculated:
|
|
||||||
PortionCarbsParameter.portionSize),
|
|
||||||
icon: const Icon(Icons.calculate),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
StyledDropdownButton<Accuracy>(
|
|
||||||
selectedItem: _portionSizeAccuracy,
|
|
||||||
label: 'Portion Size Accuracy',
|
|
||||||
items: _portionSizeAccuracies,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_portionSizeAccuracy = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// StyledFutureDropdownButton<Accuracy>(
|
|
||||||
// selectedItem: _portionSizeAccuracy,
|
|
||||||
// label: 'Portion Size Accuracy',
|
|
||||||
// items: _portionSizeAccuracies,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
// renderItem: (item) => Text(item.value),
|
|
||||||
// onChanged: (value) {
|
|
||||||
// setState(() {
|
|
||||||
// _portionSizeAccuracy = value;
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Carbs per portion',
|
|
||||||
suffixText:
|
|
||||||
nutritionMeasurement == NutritionMeasurement.grams
|
|
||||||
? 'g'
|
|
||||||
: nutritionMeasurement ==
|
|
||||||
NutritionMeasurement.ounces
|
|
||||||
? 'oz'
|
|
||||||
: '',
|
|
||||||
),
|
|
||||||
controller: _carbsPerPortionController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: true),
|
|
||||||
onChanged: (_) =>
|
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () =>
|
|
||||||
calculateThirdMeasurementOfPortionCarbsRelation(
|
|
||||||
parameterToBeCalculated:
|
|
||||||
PortionCarbsParameter.carbsPerPortion),
|
|
||||||
icon: const Icon(Icons.calculate),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
StyledDropdownButton<Accuracy>(
|
|
||||||
selectedItem: _carbsRatioAccuracy,
|
|
||||||
label: 'Carbs Ratio Accuracy',
|
|
||||||
items: _carbsRatioAccuracies,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_carbsRatioAccuracy = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// StyledFutureDropdownButton<Accuracy>(
|
|
||||||
// selectedItem: _carbsRatioAccuracy,
|
|
||||||
// label: 'Carbs Ratio Accuracy',
|
|
||||||
// items: _carbsRatioAccuracies,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
// renderItem: (item) => Text(item.value),
|
|
||||||
// onChanged: (value) {
|
|
||||||
// setState(() {
|
|
||||||
// _carbsRatioAccuracy = value;
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Bolus Units',
|
|
||||||
suffixText: ' U',
|
|
||||||
),
|
|
||||||
controller: _bolusController,
|
|
||||||
keyboardType:
|
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Delayed Bolus Duration',
|
|
||||||
suffixText: ' min',
|
|
||||||
),
|
|
||||||
controller: _delayedBolusDurationController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(),
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Delayed Bolus Units',
|
|
||||||
suffixText: ' U',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
controller: _delayedBolusRateController,
|
|
||||||
keyboardType:
|
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: _notesController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Notes',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: DetailBottomRow(
|
|
||||||
onCancel: handleCancelAction,
|
|
||||||
onSave: _isSaving ? null : handleSaveAction,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/log_entry.dart';
|
|
||||||
import 'package:diameter/models/log_meal.dart';
|
|
||||||
import 'package:diameter/screens/log/log_meal_detail.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class LogMealListScreen extends StatefulWidget {
|
|
||||||
final LogEntry logEntry;
|
|
||||||
final Function() reload;
|
|
||||||
|
|
||||||
const LogMealListScreen({Key? key, required this.logEntry, required this.reload})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LogMealListScreenState createState() => _LogMealListScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogMealListScreenState extends State<LogMealListScreen> {
|
|
||||||
void reload({String? message}) {
|
|
||||||
widget.reload();
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
if (message != null) {
|
|
||||||
var snackBar = SnackBar(
|
|
||||||
content: Text(message),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
);
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
..removeCurrentSnackBar()
|
|
||||||
..showSnackBar(snackBar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleEditAction(LogMeal meal) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => LogMealDetailScreen(
|
|
||||||
logEntry: widget.logEntry,
|
|
||||||
logMeal: meal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).then((message) => reload(message: message));
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDelete(LogMeal logMeal) {
|
|
||||||
LogMeal.remove(logMeal.id);
|
|
||||||
reload(message: 'Meal deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleDeleteAction(LogMeal meal) async {
|
|
||||||
if (showConfirmationDialogOnDelete) {
|
|
||||||
Dialogs.showConfirmationDialog(
|
|
||||||
context: context,
|
|
||||||
onConfirm: () => onDelete(meal),
|
|
||||||
message: 'Are you sure you want to delete this Meal?',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
onDelete(meal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: widget.logEntry.meals.isNotEmpty ? ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: widget.logEntry.meals.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final meal = widget.logEntry.meals[index];
|
|
||||||
return ListTile(
|
|
||||||
onTap: () => handleEditAction(meal),
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: Text(meal.value)),
|
|
||||||
Expanded(
|
|
||||||
child: Text(meal.carbsPerPortion != null
|
|
||||||
? '${meal.carbsPerPortion} g carbs'
|
|
||||||
: '')),
|
|
||||||
Expanded(
|
|
||||||
child: Text(meal.bolus != null
|
|
||||||
? '${meal.bolus} U'
|
|
||||||
: ''))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
onPressed: () => handleDeleteAction(meal),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
) : const Center(
|
|
||||||
child: Text(
|
|
||||||
'You have not added any Meals to this Log Entry yet!'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +1,16 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
import 'package:diameter/components/detail.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/models/meal_category.dart';
|
import 'package:diameter/models/meal_category.dart';
|
||||||
|
|
||||||
class MealCategoryDetailScreen extends StatefulWidget {
|
class MealCategoryDetailScreen extends StatefulWidget {
|
||||||
static const String routeName = '/meal-category';
|
static const String routeName = '/meal-category';
|
||||||
final MealCategory? mealCategory;
|
final int id;
|
||||||
|
|
||||||
const MealCategoryDetailScreen({Key? key, this.mealCategory})
|
const MealCategoryDetailScreen({Key? key, this.id = 0}) : super(key: key);
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MealCategoryDetailScreenState createState() =>
|
_MealCategoryDetailScreenState createState() =>
|
||||||
@ -19,45 +18,79 @@ class MealCategoryDetailScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MealCategoryDetailScreenState extends State<MealCategoryDetailScreen> {
|
class _MealCategoryDetailScreenState extends State<MealCategoryDetailScreen> {
|
||||||
|
MealCategory? _mealCategory;
|
||||||
|
bool _isNew = true;
|
||||||
|
|
||||||
final GlobalKey<FormState> _mealCategoryForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _mealCategoryForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
final _valueController = TextEditingController(text: '');
|
final _valueController = TextEditingController(text: '');
|
||||||
final _notesController = TextEditingController(text: '');
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.mealCategory != null) {
|
reload();
|
||||||
_valueController.text = widget.mealCategory!.value;
|
|
||||||
_notesController.text = widget.mealCategory!.notes ?? '';
|
if (_mealCategory != null) {
|
||||||
|
_valueController.text = _mealCategory!.value;
|
||||||
|
_notesController.text = _mealCategory!.notes ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_valueController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_mealCategory = MealCategory.get(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _mealCategory == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void handleSaveAction() async {
|
void handleSaveAction() async {
|
||||||
if (_mealCategoryForm.currentState!.validate()) {
|
if (_mealCategoryForm.currentState!.validate()) {
|
||||||
bool isNew = widget.mealCategory == null;
|
MealCategory mealCategory = MealCategory(
|
||||||
// isNew
|
id: widget.id,
|
||||||
// ? await MealCategory.save(
|
value: _valueController.text,
|
||||||
// value: _valueController.text, notes: _notesController.text)
|
notes: _notesController.text,
|
||||||
// : await MealCategory.update(widget.mealCategory!.objectId!,
|
);
|
||||||
// value: _valueController.text, notes: _notesController.text);
|
MealCategory.put(mealCategory);
|
||||||
MealCategory.put(MealCategory(id: widget.mealCategory?.id ?? 0,
|
Navigator.pop(context, [
|
||||||
value: _valueController.text, notes: _notesController.text));
|
'${_isNew ? 'New' : ''} Meal Category saved', mealCategory
|
||||||
Navigator.pop(context, '${isNew ? 'New' : ''} Meal Category saved');
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleCancelAction() {
|
void handleCancelAction() {
|
||||||
bool isNew = widget.mealCategory == null;
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
|
(_isNew &&
|
||||||
if (showConfirmationDialogOnCancel &&
|
|
||||||
(isNew &&
|
|
||||||
(_valueController.text != '' || _notesController.text != '')) ||
|
(_valueController.text != '' || _notesController.text != '')) ||
|
||||||
(!isNew &&
|
(!_isNew &&
|
||||||
(widget.mealCategory!.value != _valueController.text ||
|
(_mealCategory!.value != _valueController.text ||
|
||||||
(widget.mealCategory!.notes ?? '') != _notesController.text))) {
|
(_mealCategory!.notes ?? '') != _notesController.text))) {
|
||||||
Dialogs.showCancelConfirmationDialog(
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
isNew: isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -67,48 +100,52 @@ class _MealCategoryDetailScreenState extends State<MealCategoryDetailScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool isNew = widget.mealCategory == null;
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(isNew ? 'New Meal Category' : widget.mealCategory!.value),
|
title: Text(_isNew ? 'New Meal Category' : _mealCategory!.value),
|
||||||
),
|
),
|
||||||
drawer:
|
drawer:
|
||||||
const Navigation(currentLocation: MealCategoryDetailScreen.routeName),
|
const Navigation(currentLocation: MealCategoryDetailScreen.routeName),
|
||||||
body: SingleChildScrollView(
|
body: Scrollbar(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: SingleChildScrollView(
|
||||||
children: <Widget>[
|
controller: _scrollController,
|
||||||
StyledForm(
|
child: Column(
|
||||||
formState: _mealCategoryForm,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
fields: [
|
children: <Widget>[
|
||||||
TextFormField(
|
FormWrapper(
|
||||||
controller: _valueController,
|
formState: _mealCategoryForm,
|
||||||
decoration: const InputDecoration(
|
fields: [
|
||||||
labelText: 'Name',
|
TextFormField(
|
||||||
|
controller: _valueController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
validator: (value) {
|
TextFormField(
|
||||||
if (value!.trim().isEmpty) {
|
controller: _notesController,
|
||||||
return 'Empty name';
|
decoration: const InputDecoration(
|
||||||
}
|
labelText: 'Notes',
|
||||||
return null;
|
),
|
||||||
},
|
keyboardType: TextInputType.multiline,
|
||||||
),
|
minLines: 2,
|
||||||
TextFormField(
|
maxLines: 5,
|
||||||
controller: _notesController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Notes',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.multiline,
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: DetailBottomRow(
|
bottomNavigationBar: DetailBottomRow(
|
||||||
onCancel: handleCancelAction,
|
onCancel: handleCancelAction,
|
||||||
onSave: handleSaveAction,
|
onAction: handleSaveAction,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
// import 'package:diameter/components/progress_indicator.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/screens/meal/meal_category_detail.dart';
|
import 'package:diameter/screens/meal/meal_category_detail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -18,7 +17,21 @@ class MealCategoryListScreen extends StatefulWidget {
|
|||||||
class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
|
class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
|
||||||
List<MealCategory> _mealCategories = [];
|
List<MealCategory> _mealCategories = [];
|
||||||
|
|
||||||
void refresh({String? message}) {
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_mealCategories = MealCategory.getAll();
|
_mealCategories = MealCategory.getAll();
|
||||||
});
|
});
|
||||||
@ -37,12 +50,12 @@ class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
|
|||||||
|
|
||||||
void onDelete(MealCategory mealCategory) {
|
void onDelete(MealCategory mealCategory) {
|
||||||
MealCategory.remove(mealCategory.id);
|
MealCategory.remove(mealCategory.id);
|
||||||
refresh(message: 'Meal Category deleted');
|
reload(message: 'Meal Category deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(MealCategory mealCategory) async {
|
void handleDeleteAction(MealCategory mealCategory) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(mealCategory),
|
onConfirm: () => onDelete(mealCategory),
|
||||||
message: 'Are you sure you want to delete this Meal Category?',
|
message: 'Are you sure you want to delete this Meal Category?',
|
||||||
@ -52,12 +65,6 @@ class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -65,7 +72,7 @@ class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
|
|||||||
title: const Text('Meal Categories'),
|
title: const Text('Meal Categories'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: refresh,
|
onPressed: reload,
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -76,41 +83,48 @@ class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _mealCategories.isNotEmpty ? ListView.builder(
|
child: _mealCategories.isNotEmpty ? Scrollbar(
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
controller: _scrollController,
|
||||||
itemCount: _mealCategories.length,
|
child: ListView.builder(
|
||||||
itemBuilder: (context, index) {
|
controller: _scrollController,
|
||||||
final mealCategory = _mealCategories[index];
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _mealCategories.length,
|
||||||
return ListTile(
|
itemBuilder: (context, index) {
|
||||||
onTap: () {
|
final mealCategory = _mealCategories[index];
|
||||||
Navigator.push(
|
return Card(
|
||||||
context,
|
child: ListTile(
|
||||||
MaterialPageRoute(
|
onTap: () {
|
||||||
builder: (context) =>
|
Navigator.push(
|
||||||
MealCategoryDetailScreen(
|
context,
|
||||||
mealCategory: mealCategory,
|
MaterialPageRoute(
|
||||||
),
|
builder: (context) =>
|
||||||
|
MealCategoryDetailScreen(
|
||||||
|
id: mealCategory.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
mealCategory.value.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
),
|
),
|
||||||
).then((message) => refresh(message: message));
|
trailing: Row(
|
||||||
},
|
mainAxisSize: MainAxisSize.min,
|
||||||
title: Text(mealCategory.value),
|
children: [
|
||||||
subtitle: Text(mealCategory.notes ?? ''),
|
IconButton(
|
||||||
trailing: Row(
|
icon: const Icon(
|
||||||
mainAxisSize: MainAxisSize.min,
|
Icons.delete,
|
||||||
children: [
|
color: Colors.blue,
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(
|
onPressed: () =>
|
||||||
Icons.delete,
|
handleDeleteAction(mealCategory),
|
||||||
color: Colors.blue,
|
),
|
||||||
),
|
],
|
||||||
onPressed: () =>
|
|
||||||
handleDeleteAction(mealCategory),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
): const Center(
|
): const Center(
|
||||||
child: Text('You have not created any Meal Categories yet!'),
|
child: Text('You have not created any Meal Categories yet!'),
|
||||||
),
|
),
|
||||||
@ -124,7 +138,7 @@ class _MealCategoryListScreenState extends State<MealCategoryListScreen> {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const MealCategoryDetailScreen(),
|
builder: (context) => const MealCategoryDetailScreen(),
|
||||||
),
|
),
|
||||||
).then((message) => refresh(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
// import 'package:diameter/components/progress_indicator.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/meal.dart';
|
import 'package:diameter/models/meal.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/screens/meal/meal_detail.dart';
|
import 'package:diameter/screens/meal/meal_detail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -18,7 +17,21 @@ class MealListScreen extends StatefulWidget {
|
|||||||
class _MealListScreenState extends State<MealListScreen> {
|
class _MealListScreenState extends State<MealListScreen> {
|
||||||
List<Meal> _meals = [];
|
List<Meal> _meals = [];
|
||||||
|
|
||||||
void refresh({String? message}) {
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_meals = Meal.getAll();
|
_meals = Meal.getAll();
|
||||||
});
|
});
|
||||||
@ -37,12 +50,12 @@ class _MealListScreenState extends State<MealListScreen> {
|
|||||||
|
|
||||||
void onDelete(Meal meal) {
|
void onDelete(Meal meal) {
|
||||||
Meal.remove(meal.id);
|
Meal.remove(meal.id);
|
||||||
refresh(message: 'Meal deleted');
|
reload(message: 'Meal deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(Meal meal) async {
|
void handleDeleteAction(Meal meal) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(meal),
|
onConfirm: () => onDelete(meal),
|
||||||
message: 'Are you sure you want to delete this Meal?',
|
message: 'Are you sure you want to delete this Meal?',
|
||||||
@ -52,53 +65,107 @@ class _MealListScreenState extends State<MealListScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Meals'), actions: <Widget>[
|
appBar: AppBar(title: const Text('Meals'), actions: <Widget>[
|
||||||
IconButton(onPressed: refresh, icon: const Icon(Icons.refresh))
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
]),
|
]),
|
||||||
drawer: const Navigation(currentLocation: MealListScreen.routeName),
|
drawer: const Navigation(currentLocation: MealListScreen.routeName),
|
||||||
body: Column(
|
body: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _meals.isNotEmpty ? ListView.builder(
|
child: _meals.isNotEmpty ? Scrollbar(
|
||||||
padding: const EdgeInsets.all(10.0),
|
controller: _scrollController,
|
||||||
itemCount: _meals.length,
|
child: ListView.builder(
|
||||||
itemBuilder: (context, index) {
|
controller: _scrollController,
|
||||||
final meal = _meals[index];
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _meals.length,
|
||||||
return ListTile(
|
itemBuilder: (context, index) {
|
||||||
onTap: () {
|
final meal = _meals[index];
|
||||||
Navigator.push(
|
String portionType = meal.mealPortionType.hasValue ? ' per ${meal.mealPortionType.target!.value}' : '';
|
||||||
context,
|
return Card(
|
||||||
MaterialPageRoute(
|
child: ListTile(
|
||||||
builder: (context) =>
|
isThreeLine: true,
|
||||||
MealDetailScreen(meal: meal),
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
MealDetailScreen(id: meal.id),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
meal.value.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
),
|
),
|
||||||
).then((message) => refresh(message: message));
|
subtitle: Padding(
|
||||||
},
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
title: Text(meal.value),
|
child: Column(
|
||||||
subtitle: Text(meal.notes ?? ''),
|
children: [
|
||||||
trailing: Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
IconButton(
|
child: Text(meal.mealSource.target?.value ?? ''),
|
||||||
onPressed: () => handleDeleteAction(meal),
|
),
|
||||||
icon: const Icon(Icons.delete,
|
Expanded(
|
||||||
color: Colors.blue),
|
child: Column(
|
||||||
)
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
],
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
children: ((meal.carbsPerPortion ?? 0) > 0)
|
||||||
);
|
? [
|
||||||
},
|
Text(meal.carbsPerPortion!.toStringAsPrecision(3)),
|
||||||
|
Text(
|
||||||
|
'${Settings.nutritionMeasurementSuffix} carbs',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: (meal.mealPortionType.hasValue)
|
||||||
|
? [
|
||||||
|
Text(meal.portionSize?.toStringAsPrecision(3) ?? ''),
|
||||||
|
Text(
|
||||||
|
'${Settings.nutritionMeasurementSuffix}$portionType',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textScaleFactor: 0.75
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
meal.notes != null && meal.notes!.trim() != '' ? Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(meal.notes ?? '')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
) : Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => handleDeleteAction(meal),
|
||||||
|
icon: const Icon(Icons.delete,
|
||||||
|
color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
): const Center(
|
): const Center(
|
||||||
child: Text('You have not created any Meals yet!'),
|
child: Text('You have not created any Meals yet!'),
|
||||||
),
|
),
|
||||||
@ -112,7 +179,7 @@ class _MealListScreenState extends State<MealListScreen> {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const MealDetailScreen(),
|
builder: (context) => const MealDetailScreen(),
|
||||||
),
|
),
|
||||||
).then((message) => refresh(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
import 'package:diameter/components/detail.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:diameter/models/meal_portion_type.dart';
|
import 'package:diameter/models/meal_portion_type.dart';
|
||||||
@ -9,10 +9,9 @@ import 'package:diameter/models/meal_portion_type.dart';
|
|||||||
class MealPortionTypeDetailScreen extends StatefulWidget {
|
class MealPortionTypeDetailScreen extends StatefulWidget {
|
||||||
static const String routeName = '/meal-portion-type';
|
static const String routeName = '/meal-portion-type';
|
||||||
|
|
||||||
final MealPortionType? mealPortionType;
|
final int id;
|
||||||
|
|
||||||
const MealPortionTypeDetailScreen({Key? key, this.mealPortionType})
|
const MealPortionTypeDetailScreen({Key? key, this.id = 0}) : super(key: key);
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MealPortionTypeDetailScreenState createState() =>
|
_MealPortionTypeDetailScreenState createState() =>
|
||||||
@ -21,53 +20,79 @@ class MealPortionTypeDetailScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _MealPortionTypeDetailScreenState
|
class _MealPortionTypeDetailScreenState
|
||||||
extends State<MealPortionTypeDetailScreen> {
|
extends State<MealPortionTypeDetailScreen> {
|
||||||
|
MealPortionType? _mealPortionType;
|
||||||
|
bool _isNew = true;
|
||||||
|
|
||||||
final GlobalKey<FormState> _mealPortionTypeForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _mealPortionTypeForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
final _valueController = TextEditingController(text: '');
|
final _valueController = TextEditingController(text: '');
|
||||||
final _notesController = TextEditingController(text: '');
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.mealPortionType != null) {
|
reload();
|
||||||
_valueController.text = widget.mealPortionType!.value;
|
|
||||||
_notesController.text = widget.mealPortionType!.notes ?? '';
|
if (_mealPortionType != null) {
|
||||||
|
_valueController.text = _mealPortionType!.value;
|
||||||
|
_notesController.text = _mealPortionType!.notes ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_valueController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_mealPortionType = MealPortionType.get(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _mealPortionType == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void handleSaveAction() async {
|
void handleSaveAction() async {
|
||||||
if (_mealPortionTypeForm.currentState!.validate()) {
|
if (_mealPortionTypeForm.currentState!.validate()) {
|
||||||
bool isNew = widget.mealPortionType == null;
|
MealPortionType mealPortionType = MealPortionType(
|
||||||
// isNew
|
id: _mealPortionType?.id ?? 0,
|
||||||
// ? MealPortionType.save(
|
|
||||||
// value: _valueController.text,
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// )
|
|
||||||
// : MealPortionType.update(
|
|
||||||
// widget.mealPortionType!.objectId!,
|
|
||||||
// value: _valueController.text,
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// );
|
|
||||||
MealPortionType.put(MealPortionType(
|
|
||||||
id: widget.mealPortionType?.id ?? 0,
|
|
||||||
value: _valueController.text,
|
value: _valueController.text,
|
||||||
notes: _notesController.text,
|
notes: _notesController.text,
|
||||||
));
|
);
|
||||||
Navigator.pop(context, '${isNew ? 'New' : ''} Meal Portion Type saved');
|
MealPortionType.put(mealPortionType);
|
||||||
|
Navigator.pop(context,
|
||||||
|
['${_isNew ? 'New' : ''} Meal Portion Type saved', mealPortionType]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleCancelAction() {
|
void handleCancelAction() {
|
||||||
bool isNew = widget.mealPortionType == null;
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
if (showConfirmationDialogOnCancel &&
|
((_isNew &&
|
||||||
((isNew &&
|
|
||||||
(_valueController.text != '' || _notesController.text != '')) ||
|
(_valueController.text != '' || _notesController.text != '')) ||
|
||||||
(!isNew &&
|
(!_isNew &&
|
||||||
(_valueController.text != widget.mealPortionType!.value ||
|
(_valueController.text != _mealPortionType!.value ||
|
||||||
_notesController.text !=
|
_notesController.text !=
|
||||||
(widget.mealPortionType!.notes ?? ''))))) {
|
(_mealPortionType!.notes ?? ''))))) {
|
||||||
Dialogs.showCancelConfirmationDialog(
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
isNew: isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -77,49 +102,53 @@ class _MealPortionTypeDetailScreenState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool isNew = widget.mealPortionType == null;
|
bool isNew = _mealPortionType == null;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(isNew ? 'New Meal Portion Type' : _mealPortionType!.value),
|
||||||
isNew ? 'New Meal Portion Type' : widget.mealPortionType!.value),
|
|
||||||
),
|
),
|
||||||
drawer: const Navigation(
|
drawer: const Navigation(
|
||||||
currentLocation: MealPortionTypeDetailScreen.routeName),
|
currentLocation: MealPortionTypeDetailScreen.routeName),
|
||||||
body: SingleChildScrollView(
|
body: Scrollbar(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: SingleChildScrollView(
|
||||||
children: <Widget>[
|
controller: _scrollController,
|
||||||
StyledForm(
|
child: Column(
|
||||||
formState: _mealPortionTypeForm,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
fields: [
|
children: <Widget>[
|
||||||
TextFormField(
|
FormWrapper(
|
||||||
controller: _valueController,
|
formState: _mealPortionTypeForm,
|
||||||
decoration: const InputDecoration(
|
fields: [
|
||||||
labelText: 'Name',
|
TextFormField(
|
||||||
|
controller: _valueController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
validator: (value) {
|
TextFormField(
|
||||||
if (value!.trim().isEmpty) {
|
controller: _notesController,
|
||||||
return 'Empty name';
|
decoration: const InputDecoration(
|
||||||
}
|
labelText: 'Notes',
|
||||||
return null;
|
),
|
||||||
},
|
keyboardType: TextInputType.multiline,
|
||||||
),
|
minLines: 2,
|
||||||
TextFormField(
|
maxLines: 5,
|
||||||
controller: _notesController,
|
)
|
||||||
decoration: const InputDecoration(
|
],
|
||||||
labelText: 'Notes',
|
),
|
||||||
alignLabelWithHint: true,
|
],
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: DetailBottomRow(
|
bottomNavigationBar: DetailBottomRow(
|
||||||
onCancel: handleCancelAction,
|
onCancel: handleCancelAction,
|
||||||
onSave: handleSaveAction,
|
onAction: handleSaveAction,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
// import 'package:diameter/components/progress_indicator.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/screens/meal/meal_portion_type_detail.dart';
|
import 'package:diameter/screens/meal/meal_portion_type_detail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -19,7 +18,21 @@ class MealPortionTypeListScreen extends StatefulWidget {
|
|||||||
class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
|
class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
|
||||||
List<MealPortionType> _mealPortionTypes = [];
|
List<MealPortionType> _mealPortionTypes = [];
|
||||||
|
|
||||||
void refresh({String? message}) {
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_mealPortionTypes = MealPortionType.getAll();
|
_mealPortionTypes = MealPortionType.getAll();
|
||||||
});
|
});
|
||||||
@ -38,12 +51,12 @@ class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
|
|||||||
|
|
||||||
void onDelete(MealPortionType mealPortionType) {
|
void onDelete(MealPortionType mealPortionType) {
|
||||||
MealPortionType.remove(mealPortionType.id);
|
MealPortionType.remove(mealPortionType.id);
|
||||||
refresh(message: 'Meal Portion Type deleted');
|
reload(message: 'Meal Portion Type deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(MealPortionType mealPortionType) async {
|
void handleDeleteAction(MealPortionType mealPortionType) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(mealPortionType),
|
onConfirm: () => onDelete(mealPortionType),
|
||||||
message: 'Are you sure you want to delete this Meal Portion Type?',
|
message: 'Are you sure you want to delete this Meal Portion Type?',
|
||||||
@ -52,20 +65,14 @@ class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
|
|||||||
onDelete(mealPortionType);
|
onDelete(mealPortionType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Meal Portion Types'),
|
title: const Text('Meal Portion Types'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(onPressed: refresh, icon: const Icon(Icons.refresh))
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: const Navigation(
|
drawer: const Navigation(
|
||||||
@ -74,43 +81,50 @@ class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _mealPortionTypes.isNotEmpty ? ListView.builder(
|
child: _mealPortionTypes.isNotEmpty ? Scrollbar(
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
controller: _scrollController,
|
||||||
itemCount: _mealPortionTypes.length,
|
child: ListView.builder(
|
||||||
itemBuilder: (context, index) {
|
controller: _scrollController,
|
||||||
final mealPortionType = _mealPortionTypes[index];
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _mealPortionTypes.length,
|
||||||
return ListTile(
|
itemBuilder: (context, index) {
|
||||||
onTap: () {
|
final mealPortionType = _mealPortionTypes[index];
|
||||||
Navigator.push(
|
return Card(
|
||||||
context,
|
child: ListTile(
|
||||||
MaterialPageRoute(
|
onTap: () {
|
||||||
builder: (context) =>
|
Navigator.push(
|
||||||
MealPortionTypeDetailScreen(
|
context,
|
||||||
mealPortionType: mealPortionType,
|
MaterialPageRoute(
|
||||||
),
|
builder: (context) =>
|
||||||
|
MealPortionTypeDetailScreen(
|
||||||
|
id: mealPortionType.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then(
|
||||||
|
(message) => reload(message: message));
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
mealPortionType.value.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
),
|
),
|
||||||
).then(
|
trailing: Row(
|
||||||
(message) => refresh(message: message));
|
mainAxisSize: MainAxisSize.min,
|
||||||
},
|
children: [
|
||||||
title: Text(mealPortionType.value),
|
IconButton(
|
||||||
subtitle: Text(mealPortionType.notes ?? ''),
|
icon: const Icon(
|
||||||
trailing: Row(
|
Icons.delete,
|
||||||
mainAxisSize: MainAxisSize.min,
|
color: Colors.blue,
|
||||||
children: [
|
),
|
||||||
IconButton(
|
onPressed: () =>
|
||||||
icon: const Icon(
|
handleDeleteAction(mealPortionType),
|
||||||
Icons.delete,
|
),
|
||||||
color: Colors.blue,
|
],
|
||||||
),
|
|
||||||
onPressed: () =>
|
|
||||||
handleDeleteAction(mealPortionType),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
) : const Center(
|
) : const Center(
|
||||||
child: Text('You have not created any Meal Portion Types yet!'),
|
child: Text('You have not created any Meal Portion Types yet!'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -123,7 +137,7 @@ class _MealPortionTypeListScreenState extends State<MealPortionTypeListScreen> {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const MealPortionTypeDetailScreen(),
|
builder: (context) => const MealPortionTypeDetailScreen(),
|
||||||
),
|
),
|
||||||
).then((message) => refresh(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
|
@ -1,130 +1,152 @@
|
|||||||
import 'package:diameter/components/detail.dart';
|
import 'package:diameter/components/detail.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
// import 'package:diameter/main.dart';
|
|
||||||
import 'package:diameter/models/accuracy.dart';
|
import 'package:diameter/models/accuracy.dart';
|
||||||
import 'package:diameter/models/meal_category.dart';
|
import 'package:diameter/models/meal_category.dart';
|
||||||
import 'package:diameter/models/meal_portion_type.dart';
|
import 'package:diameter/models/meal_portion_type.dart';
|
||||||
import 'package:diameter/models/meal_source.dart';
|
import 'package:diameter/models/meal_source.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
// import 'package:diameter/objectbox.g.dart';
|
import 'package:diameter/screens/accuracy_detail.dart';
|
||||||
|
import 'package:diameter/screens/meal/meal_category_detail.dart';
|
||||||
|
import 'package:diameter/screens/meal/meal_portion_type_detail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class MealSourceDetailScreen extends StatefulWidget {
|
class MealSourceDetailScreen extends StatefulWidget {
|
||||||
static const String routeName = '/meal-source';
|
static const String routeName = '/meal-source';
|
||||||
|
|
||||||
final MealSource? mealSource;
|
final int id;
|
||||||
|
|
||||||
const MealSourceDetailScreen({Key? key, this.mealSource}) : super(key: key);
|
const MealSourceDetailScreen({Key? key, this.id = 0}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MealSourceDetailScreenState createState() => _MealSourceDetailScreenState();
|
_MealSourceDetailScreenState createState() => _MealSourceDetailScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MealSourceDetailScreenState extends State<MealSourceDetailScreen> {
|
class _MealSourceDetailScreenState extends State<MealSourceDetailScreen> {
|
||||||
|
MealSource? _mealSource;
|
||||||
|
bool _isNew = true;
|
||||||
|
|
||||||
List<Accuracy> _portionSizeAccuracies = [];
|
List<Accuracy> _portionSizeAccuracies = [];
|
||||||
List<Accuracy> _carbsRatioAccuracies = [];
|
List<Accuracy> _carbsRatioAccuracies = [];
|
||||||
List<MealCategory> _mealCategories = [];
|
List<MealCategory> _mealCategories = [];
|
||||||
List<MealPortionType> _mealPortionTypes = [];
|
List<MealPortionType> _mealPortionTypes = [];
|
||||||
|
|
||||||
final GlobalKey<FormState> _mealSourceForm = GlobalKey<FormState>();
|
final GlobalKey<FormState> _mealSourceForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
final _valueController = TextEditingController(text: '');
|
final _valueController = TextEditingController(text: '');
|
||||||
final _notesController = TextEditingController(text: '');
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
Accuracy? _defaultCarbsRatioAccuracy;
|
Accuracy? _defaultCarbsRatioAccuracy;
|
||||||
Accuracy? _defaultPortionSizeAccuracy;
|
Accuracy? _defaultPortionSizeAccuracy;
|
||||||
MealCategory? _defaultMealCategory;
|
MealCategory? _defaultMealCategory;
|
||||||
MealPortionType? _defaultMealPortionType;
|
MealPortionType? _defaultMealPortionType;
|
||||||
|
final _defaultCarbsRatioAccuracyController = TextEditingController(text: '');
|
||||||
|
final _defaultPortionSizeAccuracyController = TextEditingController(text: '');
|
||||||
|
final _defaultMealCategoryController = TextEditingController(text: '');
|
||||||
|
final _defaultMealPortionTypeController = TextEditingController(text: '');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
reload();
|
||||||
|
|
||||||
_portionSizeAccuracies = Accuracy.getAllForPortionSize();
|
_portionSizeAccuracies = Accuracy.getAllForPortionSize();
|
||||||
_carbsRatioAccuracies = Accuracy.getAllForCarbsRatio();
|
_carbsRatioAccuracies = Accuracy.getAllForCarbsRatio();
|
||||||
_mealCategories = MealCategory.getAll();
|
_mealCategories = MealCategory.getAll();
|
||||||
_mealPortionTypes = MealPortionType.getAll();
|
_mealPortionTypes = MealPortionType.getAll();
|
||||||
|
|
||||||
if (widget.mealSource != null) {
|
if (_mealSource != null) {
|
||||||
_valueController.text = widget.mealSource!.value;
|
_valueController.text = _mealSource!.value;
|
||||||
_notesController.text = widget.mealSource!.notes ?? '';
|
_notesController.text = _mealSource!.notes ?? '';
|
||||||
|
|
||||||
_defaultPortionSizeAccuracy =
|
_defaultPortionSizeAccuracy =
|
||||||
widget.mealSource!.defaultPortionSizeAccuracy.target;
|
_mealSource!.defaultPortionSizeAccuracy.target;
|
||||||
_defaultCarbsRatioAccuracy = widget.mealSource!.defaultCarbsRatioAccuracy.target;
|
_defaultPortionSizeAccuracyController.text = (_defaultPortionSizeAccuracy ?? '').toString();
|
||||||
|
_defaultCarbsRatioAccuracy =
|
||||||
|
_mealSource!.defaultCarbsRatioAccuracy.target;
|
||||||
|
_defaultCarbsRatioAccuracyController.text = (_defaultCarbsRatioAccuracy ?? '').toString();
|
||||||
|
|
||||||
_defaultMealCategory = widget.mealSource!.defaultMealCategory.target;
|
_defaultMealCategory = _mealSource!.defaultMealCategory.target;
|
||||||
_defaultMealPortionType =
|
_defaultMealCategoryController.text = (_defaultMealCategory ?? '').toString();
|
||||||
widget.mealSource!.defaultMealPortionType.target;
|
_defaultMealPortionType = _mealSource!.defaultMealPortionType.target;
|
||||||
|
_defaultMealPortionTypeController.text = (_defaultMealPortionType ?? '').toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_valueController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
_defaultCarbsRatioAccuracyController.dispose();
|
||||||
|
_defaultPortionSizeAccuracyController.dispose();
|
||||||
|
_defaultMealCategoryController.dispose();
|
||||||
|
_defaultMealPortionTypeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_mealSource = MealSource.get(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _mealSource == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void handleSaveAction() async {
|
void handleSaveAction() async {
|
||||||
bool isNew = widget.mealSource == null;
|
MealSource mealSource = MealSource(
|
||||||
if (_mealSourceForm.currentState!.validate()) {
|
id: widget.id,
|
||||||
// isNew
|
value: _valueController.text,
|
||||||
// ? await MealSource.save(
|
notes: _notesController.text,
|
||||||
// value: _valueController.text,
|
);
|
||||||
// defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy?.id.toString(),
|
mealSource.defaultCarbsRatioAccuracy.target = _defaultCarbsRatioAccuracy;
|
||||||
// defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy?.id.toString(),
|
mealSource.defaultPortionSizeAccuracy.target = _defaultPortionSizeAccuracy;
|
||||||
// // defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy,
|
mealSource.defaultMealCategory.target = _defaultMealCategory;
|
||||||
// // defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy,
|
mealSource.defaultMealPortionType.target = _defaultMealPortionType;
|
||||||
// defaultMealCategory: _defaultMealCategory,
|
MealSource.put(mealSource);
|
||||||
// defaultMealPortionType: _defaultMealPortionType,
|
Navigator.pop(context, ['${_isNew ? 'New' : ''} Meal Source saved', mealSource]);
|
||||||
// notes: _notesController.text,
|
|
||||||
// )
|
|
||||||
// : await MealSource.update(
|
|
||||||
// widget.mealSource!.objectId!,
|
|
||||||
// value: _valueController.text,
|
|
||||||
// defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy?.id.toString(),
|
|
||||||
// defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy?.id.toString(),
|
|
||||||
// // defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy,
|
|
||||||
// // defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy,
|
|
||||||
// defaultMealCategory: _defaultMealCategory,
|
|
||||||
// defaultMealPortionType: _defaultMealPortionType,
|
|
||||||
// notes: _notesController.text,
|
|
||||||
// );
|
|
||||||
MealSource mealSource = MealSource(
|
|
||||||
id: widget.mealSource?.id ?? 0,
|
|
||||||
value: _valueController.text,
|
|
||||||
notes: _notesController.text,
|
|
||||||
);
|
|
||||||
mealSource.defaultCarbsRatioAccuracy.target = _defaultCarbsRatioAccuracy;
|
|
||||||
mealSource.defaultPortionSizeAccuracy.target =
|
|
||||||
_defaultPortionSizeAccuracy;
|
|
||||||
mealSource.defaultMealCategory.target = _defaultMealCategory;
|
|
||||||
mealSource.defaultMealPortionType.target = _defaultMealPortionType;
|
|
||||||
MealSource.put(mealSource);
|
|
||||||
Navigator.pop(context, '${isNew ? 'New' : ''} Meal Source saved');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleCancelAction() {
|
void handleCancelAction() {
|
||||||
bool isNew = widget.mealSource == null;
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
if (showConfirmationDialogOnCancel &&
|
((_isNew &&
|
||||||
((isNew &&
|
|
||||||
(_valueController.text != '' ||
|
(_valueController.text != '' ||
|
||||||
_defaultCarbsRatioAccuracy != null ||
|
_defaultCarbsRatioAccuracy != null ||
|
||||||
_defaultPortionSizeAccuracy != null ||
|
_defaultPortionSizeAccuracy != null ||
|
||||||
_defaultMealCategory != null ||
|
_defaultMealCategory != null ||
|
||||||
_defaultMealPortionType != null ||
|
_defaultMealPortionType != null ||
|
||||||
_notesController.text != '')) ||
|
_notesController.text != '')) ||
|
||||||
(!isNew &&
|
(!_isNew &&
|
||||||
(_valueController.text != widget.mealSource!.value ||
|
(_valueController.text != _mealSource!.value ||
|
||||||
_defaultCarbsRatioAccuracy !=
|
_defaultCarbsRatioAccuracy !=
|
||||||
widget.mealSource!.defaultCarbsRatioAccuracy.target ||
|
_mealSource!.defaultCarbsRatioAccuracy.target ||
|
||||||
_defaultPortionSizeAccuracy !=
|
_defaultPortionSizeAccuracy !=
|
||||||
widget.mealSource!.defaultPortionSizeAccuracy.target ||
|
_mealSource!.defaultPortionSizeAccuracy.target ||
|
||||||
_defaultMealCategory !=
|
_defaultMealCategory !=
|
||||||
widget.mealSource!.defaultMealCategory.target ||
|
_mealSource!.defaultMealCategory.target ||
|
||||||
_defaultMealPortionType !=
|
_defaultMealPortionType !=
|
||||||
widget.mealSource!.defaultMealPortionType.target ||
|
_mealSource!.defaultMealPortionType.target ||
|
||||||
_notesController.text !=
|
_notesController.text != (_mealSource!.notes ?? ''))))) {
|
||||||
(widget.mealSource!.notes ?? ''))))) {
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
Dialogs.showCancelConfirmationDialog(
|
|
||||||
context: context,
|
context: context,
|
||||||
isNew: isNew,
|
isNew: _isNew,
|
||||||
onSave: handleSaveAction,
|
onSave: handleSaveAction,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -134,118 +156,225 @@ class _MealSourceDetailScreenState extends State<MealSourceDetailScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool isNew = widget.mealSource == null;
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(isNew ? 'New Meal Source' : widget.mealSource!.value),
|
title: Text(_isNew ? 'New Meal Source' : _mealSource!.value),
|
||||||
),
|
),
|
||||||
drawer:
|
drawer:
|
||||||
const Navigation(currentLocation: MealSourceDetailScreen.routeName),
|
const Navigation(currentLocation: MealSourceDetailScreen.routeName),
|
||||||
body: SingleChildScrollView(
|
body: Scrollbar(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: SingleChildScrollView(
|
||||||
children: <Widget>[
|
controller: _scrollController,
|
||||||
StyledForm(
|
child: Column(
|
||||||
formState: _mealSourceForm,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
fields: [
|
children: <Widget>[
|
||||||
TextFormField(
|
FormWrapper(
|
||||||
controller: _valueController,
|
formState: _mealSourceForm,
|
||||||
decoration: const InputDecoration(
|
fields: [
|
||||||
labelText: 'Name',
|
TextFormField(
|
||||||
|
controller: _valueController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
validator: (value) {
|
Row(
|
||||||
if (value!.trim().isEmpty) {
|
children: [
|
||||||
return 'Empty name';
|
Expanded(
|
||||||
}
|
child: AutoCompleteDropdownButton<Accuracy>(
|
||||||
return null;
|
selectedItem: _defaultCarbsRatioAccuracy,
|
||||||
},
|
controller: _defaultCarbsRatioAccuracyController,
|
||||||
),
|
label: 'Default Carbs Ratio Accuracy',
|
||||||
StyledDropdownButton<Accuracy>(
|
items: _carbsRatioAccuracies,
|
||||||
selectedItem: _defaultCarbsRatioAccuracy,
|
onChanged: (value) {
|
||||||
label: 'Default Carbs Ratio Accuracy',
|
setState(() {
|
||||||
items: _carbsRatioAccuracies,
|
_defaultCarbsRatioAccuracy = value;
|
||||||
renderItem: (item) => Text(item.value),
|
_defaultCarbsRatioAccuracyController.text =
|
||||||
onChanged: (value) {
|
(_defaultCarbsRatioAccuracy ?? '').toString();
|
||||||
setState(() {
|
});
|
||||||
_defaultCarbsRatioAccuracy = value;
|
},
|
||||||
});
|
),
|
||||||
},
|
),
|
||||||
),
|
IconButton(
|
||||||
StyledDropdownButton<Accuracy>(
|
onPressed: () {
|
||||||
selectedItem: _defaultPortionSizeAccuracy,
|
Navigator.push(
|
||||||
label: 'Default Portion Size Accuracy',
|
context,
|
||||||
items: _portionSizeAccuracies,
|
MaterialPageRoute(
|
||||||
renderItem: (item) => Text(item.value),
|
builder: (context) =>
|
||||||
onChanged: (value) {
|
_defaultCarbsRatioAccuracy == null
|
||||||
setState(() {
|
? const AccuracyDetailScreen()
|
||||||
_defaultPortionSizeAccuracy = value;
|
: AccuracyDetailScreen(
|
||||||
});
|
id: _defaultCarbsRatioAccuracy!.id),
|
||||||
},
|
),
|
||||||
),
|
).then((result) {
|
||||||
// StyledFutureDropdownButton<Accuracy>(
|
setState(() {
|
||||||
// selectedItem: _defaultCarbsRatioAccuracy,
|
_defaultCarbsRatioAccuracy = result?[1];
|
||||||
// label: 'Default Carbs Ratio Accuracy',
|
_defaultCarbsRatioAccuracyController.text =
|
||||||
// items: _carbsRatioAccuracies,
|
(_defaultCarbsRatioAccuracy ?? '').toString();
|
||||||
// getItemValue: (item) => item.objectId,
|
});
|
||||||
// renderItem: (item) => Text(item.value),
|
reload(message: result?[0]);
|
||||||
// onChanged: (value) {
|
});
|
||||||
// setState(() {
|
},
|
||||||
// _defaultCarbsRatioAccuracy = value;
|
icon: Icon(_defaultCarbsRatioAccuracy == null
|
||||||
// });
|
? Icons.add
|
||||||
// },
|
: Icons.edit),
|
||||||
// ),
|
),
|
||||||
// StyledFutureDropdownButton<Accuracy>(
|
],
|
||||||
// selectedItem: _defaultPortionSizeAccuracy,
|
|
||||||
// label: 'Default Portion Size Accuracy',
|
|
||||||
// items: _portionSizeAccuracies,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
// renderItem: (item) => Text(item.value),
|
|
||||||
// onChanged: (value) {
|
|
||||||
// setState(() {
|
|
||||||
// _defaultPortionSizeAccuracy = value;
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
StyledDropdownButton<MealCategory>(
|
|
||||||
selectedItem: _defaultMealCategory,
|
|
||||||
label: 'Default Meal Category',
|
|
||||||
items: _mealCategories,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_defaultMealCategory = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StyledDropdownButton<MealPortionType>(
|
|
||||||
selectedItem: _defaultMealPortionType,
|
|
||||||
label: 'Default Meal Portion Type',
|
|
||||||
items: _mealPortionTypes,
|
|
||||||
// getItemValue: (item) => item.objectId,
|
|
||||||
renderItem: (item) => Text(item.value),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_defaultMealPortionType = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: _notesController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Notes',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.multiline,
|
Row(
|
||||||
)
|
children: [
|
||||||
],
|
Expanded(
|
||||||
),
|
child: AutoCompleteDropdownButton<Accuracy>(
|
||||||
],
|
selectedItem: _defaultPortionSizeAccuracy,
|
||||||
|
controller: _defaultPortionSizeAccuracyController,
|
||||||
|
label: 'Default Portion Size Accuracy',
|
||||||
|
items: _portionSizeAccuracies,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_defaultPortionSizeAccuracy = value;
|
||||||
|
_defaultPortionSizeAccuracyController.text =
|
||||||
|
(_defaultPortionSizeAccuracy ?? '')
|
||||||
|
.toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
_defaultPortionSizeAccuracy == null
|
||||||
|
? const AccuracyDetailScreen()
|
||||||
|
: AccuracyDetailScreen(
|
||||||
|
id: _defaultPortionSizeAccuracy!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
setState(() {
|
||||||
|
_defaultPortionSizeAccuracy = result?[1];
|
||||||
|
_defaultPortionSizeAccuracyController.text =
|
||||||
|
(_defaultPortionSizeAccuracy ?? '')
|
||||||
|
.toString();
|
||||||
|
});
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_defaultPortionSizeAccuracy == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<MealCategory>(
|
||||||
|
selectedItem: _defaultMealCategory,
|
||||||
|
controller: _defaultMealCategoryController,
|
||||||
|
label: 'Default Meal Category',
|
||||||
|
items: _mealCategories,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_defaultMealCategory = value;
|
||||||
|
_defaultMealCategoryController.text =
|
||||||
|
(_defaultMealCategory ?? '').toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => _defaultMealCategory == null
|
||||||
|
? const MealCategoryDetailScreen()
|
||||||
|
: MealCategoryDetailScreen(
|
||||||
|
id: _defaultMealCategory!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
setState(() {
|
||||||
|
_defaultMealCategory = result?[1];
|
||||||
|
_defaultMealCategoryController.text =
|
||||||
|
(_defaultMealCategory ?? '').toString();
|
||||||
|
});
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_defaultMealCategory == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<MealPortionType>(
|
||||||
|
selectedItem: _defaultMealPortionType,
|
||||||
|
controller: _defaultMealPortionTypeController,
|
||||||
|
label: 'Default Meal Portion Type',
|
||||||
|
items: _mealPortionTypes,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_defaultMealPortionType = value;
|
||||||
|
_defaultMealPortionTypeController.text =
|
||||||
|
(_defaultMealPortionType ?? '').toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
_defaultMealPortionType == null
|
||||||
|
? const MealPortionTypeDetailScreen()
|
||||||
|
: MealPortionTypeDetailScreen(
|
||||||
|
id: _defaultMealPortionType!.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
setState(() {
|
||||||
|
_defaultMealPortionType = result?[1];
|
||||||
|
_defaultMealPortionTypeController.text =
|
||||||
|
(_defaultMealPortionType ?? '').toString();
|
||||||
|
});
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_defaultMealPortionType == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: DetailBottomRow(
|
bottomNavigationBar: DetailBottomRow(
|
||||||
onCancel: handleCancelAction,
|
onCancel: handleCancelAction,
|
||||||
onSave: handleSaveAction,
|
onAction: handleSaveAction,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// import 'package:diameter/components/progress_indicator.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
import 'package:diameter/components/dialogs.dart';
|
|
||||||
import 'package:diameter/config.dart';
|
|
||||||
import 'package:diameter/models/meal_source.dart';
|
import 'package:diameter/models/meal_source.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
import 'package:diameter/screens/meal/meal_source_detail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -18,7 +17,21 @@ class MealSourceListScreen extends StatefulWidget {
|
|||||||
class _MealSourceListScreenState extends State<MealSourceListScreen> {
|
class _MealSourceListScreenState extends State<MealSourceListScreen> {
|
||||||
List<MealSource> _mealSources = [];
|
List<MealSource> _mealSources = [];
|
||||||
|
|
||||||
void refresh({String? message}) {
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_mealSources = MealSource.getAll();
|
_mealSources = MealSource.getAll();
|
||||||
});
|
});
|
||||||
@ -37,12 +50,12 @@ class _MealSourceListScreenState extends State<MealSourceListScreen> {
|
|||||||
|
|
||||||
void onDelete(MealSource mealSource) {
|
void onDelete(MealSource mealSource) {
|
||||||
MealSource.remove(mealSource.id);
|
MealSource.remove(mealSource.id);
|
||||||
refresh(message: 'Meal Source deleted');
|
reload(message: 'Meal Source deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDeleteAction(MealSource mealSource) async {
|
void handleDeleteAction(MealSource mealSource) async {
|
||||||
if (showConfirmationDialogOnDelete) {
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: () => onDelete(mealSource),
|
onConfirm: () => onDelete(mealSource),
|
||||||
message: 'Are you sure you want to delete this Meal Source?',
|
message: 'Are you sure you want to delete this Meal Source?',
|
||||||
@ -52,19 +65,13 @@ class _MealSourceListScreenState extends State<MealSourceListScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Meal Sources'),
|
title: const Text('Meal Sources'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(onPressed: refresh, icon: const Icon(Icons.refresh))
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: MealSourceListScreen.routeName),
|
drawer: const Navigation(currentLocation: MealSourceListScreen.routeName),
|
||||||
@ -72,41 +79,49 @@ class _MealSourceListScreenState extends State<MealSourceListScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _mealSources.isNotEmpty ? ListView.builder(
|
child: _mealSources.isNotEmpty ? Scrollbar(
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
controller: _scrollController,
|
||||||
itemCount: _mealSources.length,
|
child: ListView.builder(
|
||||||
itemBuilder: (context, index) {
|
controller: _scrollController,
|
||||||
final mealSource = _mealSources[index];
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _mealSources.length,
|
||||||
return ListTile(
|
itemBuilder: (context, index) {
|
||||||
onTap: () {
|
final mealSource = _mealSources[index];
|
||||||
Navigator.push(
|
|
||||||
context,
|
return Card(
|
||||||
MaterialPageRoute(
|
child: ListTile(
|
||||||
builder: (context) => MealSourceDetailScreen(
|
onTap: () {
|
||||||
mealSource: mealSource,
|
Navigator.push(
|
||||||
),
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MealSourceDetailScreen(
|
||||||
|
id: mealSource.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
mealSource.value.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
),
|
),
|
||||||
).then((message) => refresh(message: message));
|
trailing: Row(
|
||||||
},
|
mainAxisSize: MainAxisSize.min,
|
||||||
title: Text(mealSource.value),
|
children: [
|
||||||
subtitle: Text(mealSource.notes ?? ''),
|
IconButton(
|
||||||
trailing: Row(
|
icon: const Icon(
|
||||||
mainAxisSize: MainAxisSize.min,
|
Icons.delete,
|
||||||
children: [
|
color: Colors.blue,
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(
|
onPressed: () async {
|
||||||
Icons.delete,
|
handleDeleteAction(mealSource);
|
||||||
color: Colors.blue,
|
},
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
],
|
||||||
handleDeleteAction(mealSource);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
),
|
||||||
) : const Center(
|
) : const Center(
|
||||||
child: Text('You have not created any Meal Sources yet!'),
|
child: Text('You have not created any Meal Sources yet!'),
|
||||||
),
|
),
|
||||||
@ -120,7 +135,7 @@ class _MealSourceListScreenState extends State<MealSourceListScreen> {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const MealSourceDetailScreen(),
|
builder: (context) => const MealSourceDetailScreen(),
|
||||||
),
|
),
|
||||||
).then((message) => refresh(message: message));
|
).then((result) => reload(message: result?[0]));
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
|
322
lib/screens/recipe/recipe_detail.dart
Normal file
322
lib/screens/recipe/recipe_detail.dart
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
import 'package:diameter/components/detail.dart';
|
||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
|
import 'package:diameter/components/forms/form_wrapper.dart';
|
||||||
|
import 'package:diameter/models/ingredient.dart';
|
||||||
|
import 'package:diameter/models/meal.dart';
|
||||||
|
import 'package:diameter/models/recipe.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/meal/meal_detail.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RecipeDetailScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/recipe';
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const RecipeDetailScreen({Key? key, this.id = 0}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RecipeDetailScreenState createState() => _RecipeDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeDetailScreenState extends State<RecipeDetailScreen> {
|
||||||
|
Recipe? _recipe;
|
||||||
|
List<Ingredient> _ingredients = [];
|
||||||
|
|
||||||
|
bool _isNew = true;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
final GlobalKey<FormState> _recipeForm = GlobalKey<FormState>();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
final _nameController = TextEditingController(text: '');
|
||||||
|
final _notesController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
double _servings = 1;
|
||||||
|
|
||||||
|
final List<TextEditingController> _ingredientControllers = [];
|
||||||
|
|
||||||
|
List<Meal> _meals = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
reload();
|
||||||
|
|
||||||
|
_meals = Meal.getAll();
|
||||||
|
|
||||||
|
if (_recipe != null) {
|
||||||
|
_nameController.text = _recipe!.name;
|
||||||
|
_servings = _recipe!.servings ?? 1;
|
||||||
|
_notesController.text = _recipe!.notes ?? '';
|
||||||
|
|
||||||
|
if (_ingredients.isNotEmpty) {
|
||||||
|
for (Ingredient ingredient in _ingredients) {
|
||||||
|
_ingredientControllers.add(
|
||||||
|
TextEditingController(text: ingredient.ingredient.target?.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
if (widget.id != 0) {
|
||||||
|
setState(() {
|
||||||
|
_recipe = Recipe.get(widget.id);
|
||||||
|
_ingredients = Ingredient.getAllForRecipe(widget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_isNew = _recipe == null;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddIngredient() {
|
||||||
|
final newIngredient = Ingredient(amount: 0);
|
||||||
|
setState(() {
|
||||||
|
newIngredient.recipe.target = _recipe;
|
||||||
|
_ingredients.add(newIngredient);
|
||||||
|
_ingredientControllers.add(TextEditingController(text: ''));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSaveAction({bool close = false}) async {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
if (_recipeForm.currentState!.validate()) {
|
||||||
|
Recipe recipe = Recipe(
|
||||||
|
id: widget.id,
|
||||||
|
name: _nameController.text,
|
||||||
|
servings: _servings,
|
||||||
|
notes: _notesController.text,
|
||||||
|
);
|
||||||
|
Recipe.put(recipe);
|
||||||
|
List<Ingredient> ingredients = _ingredients.map((ingredient) {
|
||||||
|
if (ingredient.id != 0 &&
|
||||||
|
(!ingredient.ingredient.hasValue || ingredient.amount == 0)) {
|
||||||
|
ingredient.deleted = true;
|
||||||
|
}
|
||||||
|
return ingredient;
|
||||||
|
}).toList();
|
||||||
|
ingredients.retainWhere((ingredient) {
|
||||||
|
return ingredient.id != 0 ||
|
||||||
|
(ingredient.amount > 0 && ingredient.ingredient.hasValue);
|
||||||
|
});
|
||||||
|
Ingredient.putMany(ingredients);
|
||||||
|
|
||||||
|
if (close) {
|
||||||
|
Navigator.pop(context, ['${_isNew ? 'New' : ''} Recipe Saved', recipe]);
|
||||||
|
} else {
|
||||||
|
if (_isNew) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => RecipeDetailScreen(id: recipe.id),
|
||||||
|
),
|
||||||
|
).then((result) => Navigator.pop(context, result));
|
||||||
|
} else {
|
||||||
|
reload(message: 'Recipe saved');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCancelAction() {
|
||||||
|
if (Settings.get().showConfirmationDialogOnCancel &&
|
||||||
|
((_isNew &&
|
||||||
|
(_nameController.text != '' ||
|
||||||
|
_servings != 1 ||
|
||||||
|
_notesController.text != '')) ||
|
||||||
|
(!_isNew &&
|
||||||
|
(_nameController.text != _recipe!.name ||
|
||||||
|
_servings != _recipe!.servings ||
|
||||||
|
_notesController.text != (_recipe!.notes ?? ''))))) {
|
||||||
|
DialogUtils.showCancelConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
isNew: _isNew,
|
||||||
|
onSave: handleSaveAction,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_isNew ? 'New Recipe' : _recipe!.name),
|
||||||
|
),
|
||||||
|
drawer: const Navigation(currentLocation: RecipeDetailScreen.routeName),
|
||||||
|
body: Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
FormWrapper(
|
||||||
|
formState: _recipeForm,
|
||||||
|
fields: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// NumberFormField(
|
||||||
|
// value: _servings,
|
||||||
|
// label: 'Servings',
|
||||||
|
// suffix: ' portions',
|
||||||
|
// min: 0,
|
||||||
|
// onChanged: (value) {
|
||||||
|
// if (value != null && value >= 0) {
|
||||||
|
// setState(() {
|
||||||
|
// _servings = value.toDouble();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
TextFormField(
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
),
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onAddIngredient,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'INGREDIENTS',
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onAddIngredient,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
!_isNew && _ingredients.isNotEmpty
|
||||||
|
? ListBody(
|
||||||
|
children: _ingredients.map((item) {
|
||||||
|
final ingredient = item.ingredient.target;
|
||||||
|
final index = _ingredients.indexOf(item);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10.0, vertical: 5.0),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AutoCompleteDropdownButton<Meal>(
|
||||||
|
controller: _ingredientControllers[index],
|
||||||
|
selectedItem: ingredient,
|
||||||
|
label: 'Meal Category',
|
||||||
|
items: _meals,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_ingredients[index]
|
||||||
|
.ingredient
|
||||||
|
.target = value;
|
||||||
|
_ingredientControllers[index].text =
|
||||||
|
value?.value ?? '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
ingredient == null
|
||||||
|
? const MealDetailScreen()
|
||||||
|
: MealDetailScreen(
|
||||||
|
id: ingredient.id),
|
||||||
|
),
|
||||||
|
).then((result) {
|
||||||
|
_ingredients[index].ingredient.target =
|
||||||
|
result?[1];
|
||||||
|
_ingredientControllers[index].text =
|
||||||
|
result?[1].value ?? '';
|
||||||
|
reload(message: result?[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(ingredient == null
|
||||||
|
? Icons.add
|
||||||
|
: Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Padding(
|
||||||
|
// padding: const EdgeInsets.only(top: 10.0),
|
||||||
|
// child: NumberFormField(
|
||||||
|
// controller:
|
||||||
|
// _ingredients[index].amount,
|
||||||
|
// label: 'Amount',
|
||||||
|
// suffix: Settings.nutritionMeasurementSuffix,
|
||||||
|
// min: 0,
|
||||||
|
// onChanged: (value) {
|
||||||
|
// if (value != null && value >= 0) {
|
||||||
|
// setState(() {
|
||||||
|
// _ingredients[index].amount = value.toDouble();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(_isNew
|
||||||
|
? 'Save the Recipe in order to add ingredients!'
|
||||||
|
: 'You have not added any Ingredients yet!'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: DetailBottomRow(
|
||||||
|
onCancel: handleCancelAction,
|
||||||
|
onAction: _isSaving ? null : handleSaveAction,
|
||||||
|
onMiddleAction: _isSaving ? null : () => handleSaveAction(close: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
200
lib/screens/recipe/recipe_list.dart
Normal file
200
lib/screens/recipe/recipe_list.dart
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/models/ingredient.dart';
|
||||||
|
import 'package:diameter/models/recipe.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/screens/recipe/recipe_detail.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RecipeListScreen extends StatefulWidget {
|
||||||
|
static const String routeName = '/recipes';
|
||||||
|
|
||||||
|
const RecipeListScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RecipeListScreenState createState() => _RecipeListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeListScreenState extends State<RecipeListScreen> {
|
||||||
|
List<Recipe> _recipes = [];
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
|
setState(() {
|
||||||
|
_recipes = Recipe.getAll();
|
||||||
|
});
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDelete(Recipe recipe) {
|
||||||
|
Recipe.remove(recipe.id);
|
||||||
|
reload(message: 'Recipe deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeleteAction(Recipe recipe) async {
|
||||||
|
if (Settings.get().showConfirmationDialogOnDelete) {
|
||||||
|
DialogUtils.showConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
onConfirm: () => onDelete(recipe),
|
||||||
|
message: 'Are you sure you want to delete this Recipe?',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onDelete(recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Recipes'), actions: <Widget>[
|
||||||
|
IconButton(onPressed: reload, icon: const Icon(Icons.refresh))
|
||||||
|
]),
|
||||||
|
drawer: const Navigation(currentLocation: RecipeListScreen.routeName),
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: _recipes.isNotEmpty
|
||||||
|
? Scrollbar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
itemCount: _recipes.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final recipe = _recipes[index];
|
||||||
|
final carbsRatio =
|
||||||
|
Ingredient.getCarbsRatioForRecipe(recipe.id);
|
||||||
|
final carbsPerPortion = Recipe.getCarbsPerPortion(recipe.id);
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
RecipeDetailScreen(id: recipe.id),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
recipe.name.toUpperCase(),
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(recipe.notes ?? ''),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.center,
|
||||||
|
children:
|
||||||
|
((carbsRatio ?? 0) > 0)
|
||||||
|
? [
|
||||||
|
Text(carbsRatio!.toString()),
|
||||||
|
const Text('% carbs',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.center,
|
||||||
|
children:
|
||||||
|
(recipe.servings != null)
|
||||||
|
? [
|
||||||
|
Text(recipe.servings!
|
||||||
|
.toStringAsPrecision(3)),
|
||||||
|
const Text('servings',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.center,
|
||||||
|
children:
|
||||||
|
((carbsPerPortion ?? 0) > 0)
|
||||||
|
? [
|
||||||
|
Text(carbsPerPortion!
|
||||||
|
.toStringAsPrecision(3)),
|
||||||
|
Text(
|
||||||
|
'${Settings.nutritionMeasurementSuffix} carbs per serving',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => handleDeleteAction(recipe),
|
||||||
|
icon: const Icon(Icons.delete,
|
||||||
|
color: Colors.blue),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Text('You have not created any Recipes yet!'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const RecipeDetailScreen(),
|
||||||
|
),
|
||||||
|
).then((result) => reload(message: result?[0]));
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +1,12 @@
|
|||||||
import 'package:diameter/components/dialogs.dart';
|
import 'package:diameter/components/forms/boolean_form_field.dart';
|
||||||
import 'package:diameter/components/forms.dart';
|
import 'package:diameter/components/forms/number_form_field.dart';
|
||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/utils/dialog_utils.dart';
|
||||||
|
import 'package:diameter/components/forms/auto_complete_dropdown_button.dart';
|
||||||
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:diameter/navigation.dart';
|
import 'package:diameter/navigation.dart';
|
||||||
|
import 'package:diameter/utils/utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
enum GlucoseDisplayMode { activeOnly, bothForList, bothForDetail, both }
|
|
||||||
|
|
||||||
enum GlucoseMeasurement {
|
|
||||||
mgPerDl,
|
|
||||||
mmolPerL,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NutritionMeasurement {
|
|
||||||
grams,
|
|
||||||
ounces,
|
|
||||||
cups,
|
|
||||||
}
|
|
||||||
|
|
||||||
class Settings {
|
|
||||||
static void loadSettingsIntoConfig() async {
|
|
||||||
nutritionMeasurement = await getNutritionMeasurement();
|
|
||||||
glucoseMeasurement = await getGlucoseMeasurement();
|
|
||||||
glucoseDisplayMode = await getGlucoseDisplayMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<GlucoseDisplayMode> getGlucoseDisplayMode() async {
|
|
||||||
final settings = await SharedPreferences.getInstance();
|
|
||||||
int? index = settings.getInt('glucoseDisplayMode');
|
|
||||||
return index != null && index < GlucoseDisplayMode.values.length
|
|
||||||
? GlucoseDisplayMode.values[index]
|
|
||||||
: GlucoseDisplayMode.bothForList;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<GlucoseMeasurement> getGlucoseMeasurement() async {
|
|
||||||
final settings = await SharedPreferences.getInstance();
|
|
||||||
int? index = settings.getInt('glucoseMeasurement');
|
|
||||||
return index != null && index < GlucoseMeasurement.values.length
|
|
||||||
? GlucoseMeasurement.values[index]
|
|
||||||
: GlucoseMeasurement.mgPerDl;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<NutritionMeasurement> getNutritionMeasurement() async {
|
|
||||||
final settings = await SharedPreferences.getInstance();
|
|
||||||
int? index = settings.getInt('nutritionMeasurement');
|
|
||||||
return index != null && index < NutritionMeasurement.values.length
|
|
||||||
? NutritionMeasurement.values[index]
|
|
||||||
: NutritionMeasurement.grams;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setGlucoseDisplayMode(
|
|
||||||
GlucoseDisplayMode? glucoseDisplayMode) async {
|
|
||||||
final settings = await SharedPreferences.getInstance();
|
|
||||||
if (glucoseDisplayMode != null) {
|
|
||||||
settings.setInt('glucoseDisplayMode', glucoseDisplayMode.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setGlucoseMeasurement(
|
|
||||||
GlucoseMeasurement? glucoseMeasurement) async {
|
|
||||||
final settings = await SharedPreferences.getInstance();
|
|
||||||
if (glucoseMeasurement != null) {
|
|
||||||
settings.setInt('preferredGlucoseMeasurement', glucoseMeasurement.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setNutritionMeasurement(
|
|
||||||
NutritionMeasurement? nutritionMeasurement) async {
|
|
||||||
final settings = await SharedPreferences.getInstance();
|
|
||||||
if (nutritionMeasurement != null) {
|
|
||||||
settings.setInt(
|
|
||||||
'preferredNutritionMeasurement', nutritionMeasurement.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void resetAll() async {
|
|
||||||
final settings = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
settings.remove('glucoseDisplayMode');
|
|
||||||
settings.remove('preferredGlucoseMeasurement');
|
|
||||||
settings.remove('preferredNutritionMeasurement');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
static const String routeName = '/settings';
|
static const String routeName = '/settings';
|
||||||
@ -93,29 +18,156 @@ class SettingsScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsScreenState extends State<SettingsScreen> {
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
final GlobalKey<FormState> _settingsForm = GlobalKey<FormState>();
|
late Settings _settings;
|
||||||
|
|
||||||
void onReset() {
|
final ScrollController _scrollController = ScrollController();
|
||||||
Settings.resetAll();
|
|
||||||
|
bool _measurementsIsExpanded = true;
|
||||||
|
bool _promptsIsExpanded = true;
|
||||||
|
bool _formatIsExpanded = true;
|
||||||
|
|
||||||
|
final _nutritionMeasurementLabelController = TextEditingController(text: '');
|
||||||
|
final _glucoseMeasurementLabelController = TextEditingController(text: '');
|
||||||
|
final _dateFormatController = TextEditingController(text: '');
|
||||||
|
final _longDateFormatController = TextEditingController(text: '');
|
||||||
|
final _timeFormatController = TextEditingController(text: '');
|
||||||
|
final _longTimeFormatController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
final _insulinIncrementsController = TextEditingController(text: '');
|
||||||
|
final _nutritionIncrementsController = TextEditingController(text: '');
|
||||||
|
final _mmolPerLIncrementsController = TextEditingController(text: '');
|
||||||
|
final _targetGlucoseMgPerDlController = TextEditingController(text: '');
|
||||||
|
final _targetGlucoseMmolPerLController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
late bool _onlyDisplayActiveGlucoseMeasurement;
|
||||||
|
late bool _displayBothGlucoseMeasurementsInDetailView;
|
||||||
|
late bool _displayBothGlucoseMeasurementsInListView;
|
||||||
|
|
||||||
|
late bool _showConfirmationDialogOnCancel;
|
||||||
|
late bool _showConfirmationDialogOnDelete;
|
||||||
|
late bool _showConfirmationDialogOnStopEvent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_settings = Settings.get();
|
||||||
|
_nutritionMeasurementLabelController.text =
|
||||||
|
nutritionMeasurementLabels[_settings.nutritionMeasurementIndex];
|
||||||
|
_glucoseMeasurementLabelController.text =
|
||||||
|
glucoseMeasurementLabels[_settings.glucoseMeasurementIndex];
|
||||||
|
_insulinIncrementsController.text = _settings.insulinIncrements.toString();
|
||||||
|
_nutritionIncrementsController.text =
|
||||||
|
_settings.nutritionIncrements.toString();
|
||||||
|
_mmolPerLIncrementsController.text =
|
||||||
|
_settings.mmolPerLIncrements.toString();
|
||||||
|
_targetGlucoseMgPerDlController.text =
|
||||||
|
_settings.targetGlucoseMgPerDl.toInt().toString();
|
||||||
|
_targetGlucoseMmolPerLController.text =
|
||||||
|
_settings.targetGlucoseMmolPerL.toString();
|
||||||
|
_onlyDisplayActiveGlucoseMeasurement = _settings.glucoseDisplayModeIndex ==
|
||||||
|
GlucoseDisplayMode.activeOnly.index;
|
||||||
|
_displayBothGlucoseMeasurementsInDetailView =
|
||||||
|
_settings.glucoseDisplayModeIndex == GlucoseDisplayMode.both.index ||
|
||||||
|
_settings.glucoseDisplayModeIndex ==
|
||||||
|
GlucoseDisplayMode.bothForDetail.index;
|
||||||
|
_displayBothGlucoseMeasurementsInListView =
|
||||||
|
_settings.glucoseDisplayModeIndex == GlucoseDisplayMode.both.index ||
|
||||||
|
_settings.glucoseDisplayModeIndex ==
|
||||||
|
GlucoseDisplayMode.bothForList.index;
|
||||||
|
_dateFormatController.text = _settings.dateFormat;
|
||||||
|
_longDateFormatController.text = _settings.longDateFormat ?? '';
|
||||||
|
_timeFormatController.text = _settings.timeFormat;
|
||||||
|
_longTimeFormatController.text = _settings.longTimeFormat ?? '';
|
||||||
|
_showConfirmationDialogOnCancel = _settings.showConfirmationDialogOnCancel;
|
||||||
|
_showConfirmationDialogOnDelete = _settings.showConfirmationDialogOnDelete;
|
||||||
|
_showConfirmationDialogOnStopEvent =
|
||||||
|
_settings.showConfirmationDialogOnStopEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_nutritionMeasurementLabelController.dispose();
|
||||||
|
_glucoseMeasurementLabelController.dispose();
|
||||||
|
_dateFormatController.dispose();
|
||||||
|
_longDateFormatController.dispose();
|
||||||
|
_timeFormatController.dispose();
|
||||||
|
_longTimeFormatController.dispose();
|
||||||
|
_insulinIncrementsController.dispose();
|
||||||
|
_nutritionIncrementsController.dispose();
|
||||||
|
_mmolPerLIncrementsController.dispose();
|
||||||
|
_targetGlucoseMgPerDlController.dispose();
|
||||||
|
_targetGlucoseMmolPerLController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload({String? message}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
Settings.loadSettingsIntoConfig();
|
_settings = Settings.get();
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (message != null) {
|
||||||
|
var snackBar = SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void saveSettings() {
|
||||||
|
Settings.put(Settings(
|
||||||
|
id: _settings.id,
|
||||||
|
nutritionMeasurementIndex: nutritionMeasurementLabels
|
||||||
|
.indexOf(_nutritionMeasurementLabelController.text),
|
||||||
|
glucoseMeasurementIndex: glucoseMeasurementLabels
|
||||||
|
.indexOf(_glucoseMeasurementLabelController.text),
|
||||||
|
glucoseDisplayModeIndex: _onlyDisplayActiveGlucoseMeasurement
|
||||||
|
? GlucoseDisplayMode.activeOnly.index
|
||||||
|
: _displayBothGlucoseMeasurementsInDetailView &&
|
||||||
|
_displayBothGlucoseMeasurementsInListView
|
||||||
|
? GlucoseDisplayMode.both.index
|
||||||
|
: _displayBothGlucoseMeasurementsInDetailView
|
||||||
|
? GlucoseDisplayMode.bothForDetail.index
|
||||||
|
: GlucoseDisplayMode.bothForList.index,
|
||||||
|
targetGlucoseMgPerDl:
|
||||||
|
int.tryParse(_targetGlucoseMgPerDlController.text) ?? _settings.targetGlucoseMgPerDl,
|
||||||
|
targetGlucoseMmolPerL:
|
||||||
|
double.tryParse(_targetGlucoseMmolPerLController.text) ?? _settings.targetGlucoseMmolPerL,
|
||||||
|
insulinIncrements:
|
||||||
|
double.tryParse(_insulinIncrementsController.text) ?? _settings.insulinIncrements,
|
||||||
|
nutritionIncrements:
|
||||||
|
double.tryParse(_nutritionIncrementsController.text) ?? _settings.nutritionIncrements,
|
||||||
|
mmolPerLIncrements:
|
||||||
|
double.tryParse(_mmolPerLIncrementsController.text) ?? _settings.mmolPerLIncrements,
|
||||||
|
dateFormat: _dateFormatController.text,
|
||||||
|
longDateFormat: _longDateFormatController.text,
|
||||||
|
timeFormat: _timeFormatController.text,
|
||||||
|
longTimeFormat: _longTimeFormatController.text,
|
||||||
|
showConfirmationDialogOnCancel: _showConfirmationDialogOnCancel,
|
||||||
|
showConfirmationDialogOnDelete: _showConfirmationDialogOnDelete,
|
||||||
|
showConfirmationDialogOnStopEvent: _showConfirmationDialogOnStopEvent,
|
||||||
|
));
|
||||||
|
reload(message: 'Settings updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
void onReset() {
|
||||||
|
Settings.reset();
|
||||||
|
reload(message: 'Settings have been reset to default');
|
||||||
|
}
|
||||||
|
|
||||||
void handleResetAction() async {
|
void handleResetAction() async {
|
||||||
Dialogs.showConfirmationDialog(
|
DialogUtils.showConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
onConfirm: onReset,
|
onConfirm: onReset,
|
||||||
message: 'Are you sure you want to reset all settings?',
|
message: 'Are you sure you want to reset all settings?',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
initState() {
|
|
||||||
super.initState();
|
|
||||||
Settings.loadSettingsIntoConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -124,93 +176,399 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
drawer: const Navigation(currentLocation: SettingsScreen.routeName),
|
drawer: const Navigation(currentLocation: SettingsScreen.routeName),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
StyledForm(
|
Padding(
|
||||||
formState: _settingsForm,
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
fields: [
|
child: GestureDetector(
|
||||||
StyledDropdownButton<NutritionMeasurement>(
|
onTap: () => setState(() {
|
||||||
selectedItem: nutritionMeasurement,
|
_measurementsIsExpanded = !_measurementsIsExpanded;
|
||||||
label: 'Preferred Nutrition Measurement',
|
}),
|
||||||
items: NutritionMeasurement.values,
|
child: Row(
|
||||||
renderItem: (item) => Text(item.toString().split('.')[1]),
|
children: [
|
||||||
onChanged: (value) {
|
Expanded(
|
||||||
if (value != null) {
|
child: Text(
|
||||||
Settings.setNutritionMeasurement(value);
|
'MEASUREMENTS',
|
||||||
setState(() {
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
nutritionMeasurement = value;
|
),
|
||||||
});
|
),
|
||||||
}
|
Icon(_measurementsIsExpanded
|
||||||
},
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
StyledDropdownButton<GlucoseMeasurement>(
|
),
|
||||||
selectedItem: glucoseMeasurement,
|
),
|
||||||
label: 'Preferred Glucose Measurement',
|
Column(
|
||||||
items: GlucoseMeasurement.values,
|
children: _measurementsIsExpanded
|
||||||
renderItem: (item) => Text(item.toString().split('.')[1]),
|
? [
|
||||||
onChanged: (value) {
|
AutoCompleteDropdownButton<String>(
|
||||||
if (value != null) {
|
controller: _nutritionMeasurementLabelController,
|
||||||
Settings.setGlucoseMeasurement(value);
|
selectedItem: _nutritionMeasurementLabelController.text,
|
||||||
setState(() {
|
label: 'Preferred Nutrition Measurement',
|
||||||
glucoseMeasurement = value;
|
items: nutritionMeasurementLabels,
|
||||||
});
|
onChanged: (value) {
|
||||||
}
|
_nutritionMeasurementLabelController.text =
|
||||||
},
|
value ?? '';
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
child: AutoCompleteDropdownButton<String>(
|
||||||
|
controller: _glucoseMeasurementLabelController,
|
||||||
|
selectedItem: _glucoseMeasurementLabelController.text,
|
||||||
|
label: 'Preferred Glucose Measurement',
|
||||||
|
items: glucoseMeasurementLabels,
|
||||||
|
onChanged: (value) {
|
||||||
|
_glucoseMeasurementLabelController.text =
|
||||||
|
value ?? '';
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl
|
||||||
|
? NumberFormField(
|
||||||
|
label: 'Target glucose',
|
||||||
|
suffix: 'mg/dl',
|
||||||
|
controller: _targetGlucoseMgPerDlController,
|
||||||
|
showSteppers: false,
|
||||||
|
onChanged: (_) async {
|
||||||
|
await Future.delayed(
|
||||||
|
const Duration(seconds: 1));
|
||||||
|
if (Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mgPerDl) {
|
||||||
|
final value = int.tryParse(
|
||||||
|
_targetGlucoseMgPerDlController.text);
|
||||||
|
_targetGlucoseMmolPerLController.text = Utils
|
||||||
|
.toStringMatchingTemplateFractionPrecision(
|
||||||
|
Utils.convertMgPerDlToMmolPerL(value ?? 0),
|
||||||
|
Settings.mmolPerLSteps);
|
||||||
|
await Future.delayed(
|
||||||
|
const Duration(seconds: 1));
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: NumberFormField(
|
||||||
|
label: 'Target glucose',
|
||||||
|
suffix: 'mmol/l',
|
||||||
|
controller: _targetGlucoseMmolPerLController,
|
||||||
|
showSteppers: false,
|
||||||
|
onChanged: (_) async {
|
||||||
|
await Future.delayed(
|
||||||
|
const Duration(seconds: 1));
|
||||||
|
if (Settings.glucoseMeasurement ==
|
||||||
|
GlucoseMeasurement.mmolPerL) {
|
||||||
|
final value = double.tryParse(
|
||||||
|
_targetGlucoseMmolPerLController.text);
|
||||||
|
_targetGlucoseMgPerDlController.text =
|
||||||
|
Utils.convertMmolPerLToMgPerDl(value ?? 0)
|
||||||
|
.toString();
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
controller: _insulinIncrementsController,
|
||||||
|
showSteppers: false,
|
||||||
|
label: 'Insulin increment',
|
||||||
|
onChanged: (value) {
|
||||||
|
_insulinIncrementsController.text =
|
||||||
|
(value ?? 0).toString();
|
||||||
|
saveSettings();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
NumberFormField(
|
||||||
|
controller: _nutritionIncrementsController,
|
||||||
|
showSteppers: false,
|
||||||
|
label: 'Nutrition increment',
|
||||||
|
onChanged: (value) {
|
||||||
|
_nutritionIncrementsController.text =
|
||||||
|
(value ?? 0).toString();
|
||||||
|
saveSettings();
|
||||||
|
}),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
child: NumberFormField(
|
||||||
|
controller: _mmolPerLIncrementsController,
|
||||||
|
showSteppers: false,
|
||||||
|
label: 'Mmol/L increment',
|
||||||
|
onChanged: (value) {
|
||||||
|
_mmolPerLIncrementsController.text =
|
||||||
|
(value ?? 0).toString();
|
||||||
|
saveSettings();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
BooleanFormField(
|
||||||
|
value: _onlyDisplayActiveGlucoseMeasurement,
|
||||||
|
label: 'only display active glucose measurement',
|
||||||
|
onChanged: (value) {
|
||||||
|
_onlyDisplayActiveGlucoseMeasurement = value;
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BooleanFormField(
|
||||||
|
value: _displayBothGlucoseMeasurementsInDetailView,
|
||||||
|
enabled: !_onlyDisplayActiveGlucoseMeasurement,
|
||||||
|
label:
|
||||||
|
'display both glucose measurements in detail view',
|
||||||
|
onChanged: (value) {
|
||||||
|
_displayBothGlucoseMeasurementsInDetailView = value;
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BooleanFormField(
|
||||||
|
value: _displayBothGlucoseMeasurementsInListView,
|
||||||
|
enabled: !_onlyDisplayActiveGlucoseMeasurement,
|
||||||
|
label: 'display both glucose measurements in list view',
|
||||||
|
onChanged: (value) {
|
||||||
|
_displayBothGlucoseMeasurementsInListView = value;
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => setState(() {
|
||||||
|
_promptsIsExpanded = !_promptsIsExpanded;
|
||||||
|
}),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'CONFIRMATION PROMPTS',
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(_promptsIsExpanded
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
StyledBooleanFormField(
|
),
|
||||||
value: glucoseDisplayMode == GlucoseDisplayMode.activeOnly,
|
),
|
||||||
label: 'only display active glucose measurement',
|
Column(
|
||||||
onChanged: (_) {
|
children: _promptsIsExpanded
|
||||||
GlucoseDisplayMode mode =
|
? [
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.activeOnly
|
BooleanFormField(
|
||||||
? GlucoseDisplayMode.both
|
value: _showConfirmationDialogOnCancel,
|
||||||
: GlucoseDisplayMode.activeOnly;
|
label:
|
||||||
Settings.setGlucoseDisplayMode(mode);
|
'on cancelling edit or creation of a record if changes have already been made',
|
||||||
setState(() {
|
onChanged: (value) {
|
||||||
glucoseDisplayMode = mode;
|
_showConfirmationDialogOnCancel = value;
|
||||||
});
|
saveSettings();
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
BooleanFormField(
|
||||||
|
value: _showConfirmationDialogOnDelete,
|
||||||
|
label: 'on deleting a record',
|
||||||
|
onChanged: (value) {
|
||||||
|
_showConfirmationDialogOnDelete = value;
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BooleanFormField(
|
||||||
|
value: _showConfirmationDialogOnStopEvent,
|
||||||
|
label: 'on stopping (ending) an event',
|
||||||
|
onChanged: (value) {
|
||||||
|
_showConfirmationDialogOnStopEvent = value;
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => setState(() {
|
||||||
|
_formatIsExpanded = !_formatIsExpanded;
|
||||||
|
}),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'TIME & DATE FORMAT',
|
||||||
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(_formatIsExpanded
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
StyledBooleanFormField(
|
),
|
||||||
value: glucoseDisplayMode == GlucoseDisplayMode.both ||
|
),
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.bothForDetail,
|
Column(
|
||||||
enabled: glucoseDisplayMode != GlucoseDisplayMode.activeOnly,
|
children: _formatIsExpanded
|
||||||
label: 'display both glucose measurements in detail view',
|
? [
|
||||||
onChanged: (_) {
|
Row(
|
||||||
GlucoseDisplayMode mode = glucoseDisplayMode ==
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
GlucoseDisplayMode.both
|
children: [
|
||||||
? GlucoseDisplayMode.bothForList
|
Expanded(
|
||||||
: glucoseDisplayMode == GlucoseDisplayMode.bothForList
|
child: TextFormField(
|
||||||
? GlucoseDisplayMode.both
|
controller: _dateFormatController,
|
||||||
: GlucoseDisplayMode.activeOnly;
|
decoration: const InputDecoration(
|
||||||
Settings.setGlucoseDisplayMode(mode);
|
labelText: 'Date Format',
|
||||||
setState(() {
|
),
|
||||||
glucoseDisplayMode = mode;
|
validator: (value) {
|
||||||
});
|
if (value!.trim().isEmpty) {
|
||||||
},
|
return 'Empty title';
|
||||||
),
|
}
|
||||||
StyledBooleanFormField(
|
return null;
|
||||||
value: glucoseDisplayMode == GlucoseDisplayMode.both ||
|
},
|
||||||
glucoseDisplayMode == GlucoseDisplayMode.bothForList,
|
),
|
||||||
enabled: glucoseDisplayMode != GlucoseDisplayMode.activeOnly,
|
),
|
||||||
label: 'display both glucose measurements in list view',
|
Expanded(
|
||||||
onChanged: (_) {
|
child: Padding(
|
||||||
GlucoseDisplayMode mode = glucoseDisplayMode ==
|
padding: const EdgeInsets.only(
|
||||||
GlucoseDisplayMode.both
|
left: 5.0, bottom: 10.0),
|
||||||
? GlucoseDisplayMode.bothForDetail
|
child: Column(
|
||||||
: glucoseDisplayMode == GlucoseDisplayMode.bothForDetail
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? GlucoseDisplayMode.both
|
children: [
|
||||||
: GlucoseDisplayMode.activeOnly;
|
const Text('Example', textScaleFactor: 0.75),
|
||||||
Settings.setGlucoseDisplayMode(mode);
|
Text(
|
||||||
setState(() {
|
DateFormat(_dateFormatController.text)
|
||||||
glucoseDisplayMode = mode;
|
.format(DateTime.now()),
|
||||||
});
|
textScaleFactor: 1.25,
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
// TODO: add fields for date and time formats
|
),
|
||||||
// TODO: add fields for glucose target
|
),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _longDateFormatController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Long Date Format',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 5.0, bottom: 10.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Example',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
Text(
|
||||||
|
DateFormat(_longDateFormatController.text)
|
||||||
|
.format(DateTime.now()),
|
||||||
|
textScaleFactor: 1.25,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _timeFormatController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Time Format',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 5.0, bottom: 10.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Example', textScaleFactor: 0.75),
|
||||||
|
Text(
|
||||||
|
DateFormat(_timeFormatController.text)
|
||||||
|
.format(DateTime.now()),
|
||||||
|
textScaleFactor: 1.25,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _longTimeFormatController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Long Time Format',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value!.trim().isEmpty) {
|
||||||
|
return 'Empty title';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 5.0, bottom: 10.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Example',
|
||||||
|
textScaleFactor: 0.75),
|
||||||
|
Text(
|
||||||
|
DateFormat(_longTimeFormatController.text)
|
||||||
|
.format(DateTime.now()),
|
||||||
|
textScaleFactor: 1.25,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import 'package:diameter/config.dart';
|
import 'package:diameter/models/settings.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
final DateTime dummyDate = DateTime(2000);
|
||||||
|
|
||||||
class DateTimeUtils {
|
class DateTimeUtils {
|
||||||
static String displayDateTime(DateTime? date, {String fallback = ''}) {
|
static String displayDateTime(DateTime? date, {String fallback = ''}) {
|
||||||
if (date == null) {
|
if (date == null) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
DateTime localDate = date.toLocal();
|
DateTime localDate = date.toLocal();
|
||||||
final DateFormat formatter = DateFormat('$dateFormat $timeFormat');
|
final DateFormat formatter =
|
||||||
|
DateFormat('${Settings.get().dateFormat} ${Settings.get().timeFormat}');
|
||||||
return formatter.format(localDate);
|
return formatter.format(localDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +20,8 @@ class DateTimeUtils {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
DateTime localDate = date.toLocal();
|
DateTime localDate = date.toLocal();
|
||||||
final DateFormat formatter = DateFormat(longDateFormat ?? dateFormat);
|
final DateFormat formatter =
|
||||||
|
DateFormat(Settings.get().longDateFormat ?? Settings.get().dateFormat);
|
||||||
return formatter.format(localDate);
|
return formatter.format(localDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,8 +31,9 @@ class DateTimeUtils {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
DateTime localDate = date.toLocal();
|
DateTime localDate = date.toLocal();
|
||||||
final DateFormat formatter = DateFormat(
|
final DateFormat formatter = DateFormat(longFormat == true
|
||||||
longFormat == true ? longTimeFormat ?? timeFormat : timeFormat);
|
? Settings.get().longTimeFormat ?? Settings.get().timeFormat
|
||||||
|
: Settings.get().timeFormat);
|
||||||
return formatter.format(localDate);
|
return formatter.format(localDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,8 +42,9 @@ class DateTimeUtils {
|
|||||||
if (time == null) {
|
if (time == null) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
final DateFormat formatter = DateFormat(
|
final DateFormat formatter = DateFormat(longFormat == true
|
||||||
longFormat == true ? longTimeFormat ?? timeFormat : timeFormat);
|
? Settings.get().longTimeFormat ?? Settings.get().timeFormat
|
||||||
|
: Settings.get().timeFormat);
|
||||||
return formatter.format(convertTimeOfDayToDateTime(time));
|
return formatter.format(convertTimeOfDayToDateTime(time));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Dialogs {
|
class DialogUtils {
|
||||||
static void showCancelConfirmationDialog(
|
static void showCancelConfirmationDialog(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
required bool isNew,
|
required bool isNew,
|
@ -1,31 +1,67 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
class Utils {
|
class Utils {
|
||||||
static double roundToDecimalPlaces(double value, int places) {
|
// static double roundToDecimalPlaces(double value, int precision) {
|
||||||
double mod = pow(10.0, places).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 convertMgPerDlToMmolPerL(int mgPerDl) {
|
static double addDoublesWithPrecision(double a, double b, int precision) {
|
||||||
return Utils.roundToDecimalPlaces(mgPerDl * 0.0555, 2);
|
double mod = pow(10.0, precision).toDouble();
|
||||||
|
double difference = (a * mod) + (b * mod);
|
||||||
|
return difference.round() / mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int getFractionDigitsLength(double value) {
|
||||||
|
final fractionDigits = value.toString().split('.');
|
||||||
|
return fractionDigits[1] == '0' ? 0 : fractionDigits[1].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String toStringMatchingTemplateFractionPrecision(
|
||||||
|
double value, double template) {
|
||||||
|
final precision = getFractionDigitsLength(template);
|
||||||
|
return value.toStringAsFixed(precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double convertMgPerDlToMmolPerL(int mgPerDl, {double step = 0.01}) {
|
||||||
|
return Utils.roundToMultipleOfBase(mgPerDl * 0.0555, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int convertMmolPerLToMgPerDl(double mmolPerL) {
|
static int convertMmolPerLToMgPerDl(double mmolPerL) {
|
||||||
return (mmolPerL * 18.018).round();
|
return (mmolPerL * 18.018).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
static double calculateCarbsPerPortion(
|
static double calculateCarbs(double carbsRatio, double portionSize,
|
||||||
double carbsRatio, double portionSize) {
|
{double step = 0.01}) {
|
||||||
return Utils.roundToDecimalPlaces(carbsRatio * portionSize / 100, 2);
|
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 Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2);
|
return portionSize > 0
|
||||||
|
? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / portionSize, step)
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static double calculatePortionSize(
|
static double calculatePortionSize(
|
||||||
double carbsRatio, double carbsPerPortion) {
|
double carbsRatio, double carbsPerPortion, {double step = 0.01}) {
|
||||||
return Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2);
|
return carbsRatio > 0
|
||||||
|
? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / carbsRatio, step)
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
objectbox/data.mdb
Normal file
BIN
objectbox/data.mdb
Normal file
Binary file not shown.
BIN
objectbox/lock.mdb
Normal file
BIN
objectbox/lock.mdb
Normal file
Binary file not shown.
326
pubspec.lock
326
pubspec.lock
@ -7,14 +7,14 @@ packages:
|
|||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "30.0.0"
|
version: "31.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.8.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -63,14 +63,14 @@ packages:
|
|||||||
name: build_resolvers
|
name: build_resolvers
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.5"
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.5"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -141,48 +141,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.15.0"
|
||||||
connectivity_plus:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: connectivity_plus
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
connectivity_plus_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: connectivity_plus_linux
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.0"
|
|
||||||
connectivity_plus_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: connectivity_plus_macos
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.1"
|
|
||||||
connectivity_plus_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: connectivity_plus_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.0"
|
|
||||||
connectivity_plus_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: connectivity_plus_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.0+1"
|
|
||||||
connectivity_plus_windows:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: connectivity_plus_windows
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.0"
|
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -203,7 +161,7 @@ packages:
|
|||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.4"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -211,20 +169,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
dbus:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dbus
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.5.6"
|
|
||||||
dio:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dio
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.1"
|
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -277,11 +221,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_web_plugins:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
frontend_server_client:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -303,13 +242,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
http:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.4"
|
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -324,13 +256,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
idb_shim:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: idb_shim
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -358,7 +283,7 @@ packages:
|
|||||||
name: json_annotation
|
name: json_annotation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.0"
|
version: "4.4.0"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -380,6 +305,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.11"
|
version: "0.12.11"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -394,13 +326,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
mime_type:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: mime_type
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -408,34 +333,27 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
nm:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: nm
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.0"
|
|
||||||
objectbox:
|
objectbox:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: objectbox
|
name: objectbox
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
objectbox_flutter_libs:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: objectbox_flutter_libs
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.0"
|
|
||||||
objectbox_generator:
|
objectbox_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: objectbox_generator
|
name: objectbox_generator
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
|
objectbox_sync_flutter_libs:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: objectbox_sync_flutter_libs
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -443,62 +361,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
package_info_plus:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_info_plus
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.0"
|
|
||||||
package_info_plus_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_info_plus_linux
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.3"
|
|
||||||
package_info_plus_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_info_plus_macos
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.0"
|
|
||||||
package_info_plus_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_info_plus_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
package_info_plus_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_info_plus_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
package_info_plus_windows:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: package_info_plus_windows
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
parse_server_sdk:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: parse_server_sdk
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.0"
|
|
||||||
parse_server_sdk_flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: parse_server_sdk_flutter
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.0"
|
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -512,21 +374,35 @@ packages:
|
|||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.7"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.9"
|
||||||
|
path_provider_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_ios
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.7"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.2"
|
||||||
path_provider_macos:
|
path_provider_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_macos
|
name: path_provider_macos
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.4"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -540,28 +416,14 @@ packages:
|
|||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.4"
|
||||||
pedantic:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pedantic
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.11.1"
|
|
||||||
petitparser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: petitparser
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.4.0"
|
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.1.0"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -603,63 +465,7 @@ packages:
|
|||||||
name: pubspec_parse
|
name: pubspec_parse
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.2.0"
|
||||||
sembast:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sembast
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.1"
|
|
||||||
sembast_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sembast_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1+1"
|
|
||||||
shared_preferences:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: shared_preferences
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.8"
|
|
||||||
shared_preferences_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_linux
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.2"
|
|
||||||
shared_preferences_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_macos
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.2"
|
|
||||||
shared_preferences_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
shared_preferences_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.2"
|
|
||||||
shared_preferences_windows:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_windows
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.2"
|
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -685,7 +491,7 @@ packages:
|
|||||||
name: source_gen
|
name: source_gen
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.2.0"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -693,20 +499,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
version: "1.8.1"
|
||||||
sqflite:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: sqflite
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0+4"
|
|
||||||
sqflite_common:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sqflite_common
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1+1"
|
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -735,13 +527,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
synchronized:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: synchronized
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.0"
|
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -755,7 +540,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.3"
|
version: "0.4.8"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -770,13 +555,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
uuid:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: uuid
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.5"
|
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -804,7 +582,7 @@ packages:
|
|||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.10"
|
version: "2.3.1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -812,20 +590,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0"
|
||||||
xml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: xml
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.3.1"
|
|
||||||
xxtea:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: xxtea
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -9,17 +9,14 @@ environment:
|
|||||||
sdk: ">=2.12.0 <3.0.0"
|
sdk: ">=2.12.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
parse_server_sdk_flutter: ^3.1.0
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
sqflite: ^2.0.0+4
|
|
||||||
path_provider: ^2.0.5
|
path_provider: ^2.0.5
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
flex_color_scheme: ^3.0.1
|
flex_color_scheme: ^3.0.1
|
||||||
shared_preferences: ^2.0.8
|
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
objectbox: ^1.2.0
|
objectbox: ^1.2.0
|
||||||
objectbox_flutter_libs: any
|
objectbox_sync_flutter_libs: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -31,7 +28,3 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
fonts:
|
|
||||||
- family: RobotoCondensed
|
|
||||||
fonts:
|
|
||||||
- asset: assets/fonts/RobotoCondensed-Regular.ttf
|
|
||||||
|
BIN
sync-server
Executable file
BIN
sync-server
Executable file
Binary file not shown.
12
sync-server-config.js
Normal file
12
sync-server-config.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"dbDirectory": "objectbox",
|
||||||
|
"dbMaxSize": "100G",
|
||||||
|
"modelFile": "lib/objectbox-model.json",
|
||||||
|
"bind": "ws://192.168.1.184:9999",
|
||||||
|
"browserBind": "http://127.0.0.1:9980",
|
||||||
|
"browserThreads": 4,
|
||||||
|
"certificatePath": "",
|
||||||
|
"auth": {
|
||||||
|
"sharedSecret": "m4Gwehzgv18jZ5gCVUBZl5li3Z0FX2Yb"
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tide",
|
"name": "diameter",
|
||||||
"short_name": "tide",
|
"short_name": "diameter",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0175C2",
|
"background_color": "#0175C2",
|
||||||
|
Loading…
Reference in New Issue
Block a user