commit a38886d6f2b7906a9b5bbfdce6fe6582fb377bf2 Author: spinel Date: Sat Oct 23 01:08:09 2021 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.gradle/6.1.1/executionHistory/executionHistory.lock b/.gradle/6.1.1/executionHistory/executionHistory.lock new file mode 100644 index 0000000..8e1f595 Binary files /dev/null and b/.gradle/6.1.1/executionHistory/executionHistory.lock differ diff --git a/.gradle/6.1.1/fileChanges/last-build.bin b/.gradle/6.1.1/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/.gradle/6.1.1/fileChanges/last-build.bin differ diff --git a/.gradle/6.1.1/fileHashes/fileHashes.lock b/.gradle/6.1.1/fileHashes/fileHashes.lock new file mode 100644 index 0000000..9dd4b67 Binary files /dev/null and b/.gradle/6.1.1/fileHashes/fileHashes.lock differ diff --git a/.gradle/6.1.1/gc.properties b/.gradle/6.1.1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..6319954 Binary files /dev/null and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..2330339 --- /dev/null +++ b/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Mon Sep 20 23:44:21 CEST 2021 +gradle.version=6.1.1 diff --git a/.gradle/checksums/checksums.lock b/.gradle/checksums/checksums.lock new file mode 100644 index 0000000..65db146 Binary files /dev/null and b/.gradle/checksums/checksums.lock differ diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..245aa62 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: a7fb06d6faa2f0ad0da124c79a4eb26ae091baa5 + channel: beta + +project_type: app diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7b02b6 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# tide + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..03f030d --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,68 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 30 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.diameter" + minSdkVersion 16 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..9074832 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bebdc2e --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/diameter/MainActivity.kt b/android/app/src/main/kotlin/com/example/diameter/MainActivity.kt new file mode 100644 index 0000000..791e59a --- /dev/null +++ b/android/app/src/main/kotlin/com/example/diameter/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.diameter + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d74aa35 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..9074832 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..ed45c65 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bc6a58a --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/fonts/RobotoCondensed-Bold.ttf b/assets/fonts/RobotoCondensed-Bold.ttf new file mode 100644 index 0000000..7fe3128 Binary files /dev/null and b/assets/fonts/RobotoCondensed-Bold.ttf differ diff --git a/assets/fonts/RobotoCondensed-BoldItalic.ttf b/assets/fonts/RobotoCondensed-BoldItalic.ttf new file mode 100644 index 0000000..52ef6f3 Binary files /dev/null and b/assets/fonts/RobotoCondensed-BoldItalic.ttf differ diff --git a/assets/fonts/RobotoCondensed-Italic.ttf b/assets/fonts/RobotoCondensed-Italic.ttf new file mode 100644 index 0000000..12216d6 Binary files /dev/null and b/assets/fonts/RobotoCondensed-Italic.ttf differ diff --git a/assets/fonts/RobotoCondensed-Light.ttf b/assets/fonts/RobotoCondensed-Light.ttf new file mode 100644 index 0000000..43dd8f4 Binary files /dev/null and b/assets/fonts/RobotoCondensed-Light.ttf differ diff --git a/assets/fonts/RobotoCondensed-LightItalic.ttf b/assets/fonts/RobotoCondensed-LightItalic.ttf new file mode 100644 index 0000000..99d491b Binary files /dev/null and b/assets/fonts/RobotoCondensed-LightItalic.ttf differ diff --git a/assets/fonts/RobotoCondensed-Regular.ttf b/assets/fonts/RobotoCondensed-Regular.ttf new file mode 100644 index 0000000..62dd61e Binary files /dev/null and b/assets/fonts/RobotoCondensed-Regular.ttf differ diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..151026b --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,33 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..902bdcb --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,471 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.tide; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.tide; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.tide; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a28140c --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..441fb6d --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + tide + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/lib/components/app_theme.dart b/lib/components/app_theme.dart new file mode 100644 index 0000000..658b771 --- /dev/null +++ b/lib/components/app_theme.dart @@ -0,0 +1,23 @@ +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:flutter/material.dart'; + +class AppTheme { + AppTheme._(); + + static ThemeData lightTheme = FlexColorScheme.light( + scheme: FlexScheme.mandyRed, + fontFamily: 'RobotoCondensed', + ).toTheme; + + static ThemeData darkTheme = FlexColorScheme.light( + scheme: FlexScheme.mandyRed, + fontFamily: 'RobotoCondensed', + ).toTheme; + + static ThemeData makeTheme(ThemeData baseThemeData) { + return baseThemeData.copyWith( + visualDensity: VisualDensity.compact, + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: baseThemeData.primaryColor)); + } +} diff --git a/lib/components/data_table.dart b/lib/components/data_table.dart new file mode 100644 index 0000000..90c2101 --- /dev/null +++ b/lib/components/data_table.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +abstract class DataTableContent { + bool selected = false; + List asDataTableCells(List actions) => []; + static List asDataTableColumns() => []; +} + +class DataTableSourceBuilder extends DataTableSource { + final List 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([]), + ); + } +} diff --git a/lib/components/detail.dart b/lib/components/detail.dart new file mode 100644 index 0000000..2f1c56d --- /dev/null +++ b/lib/components/detail.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class DetailBottomRow extends StatefulWidget { + final void Function() onCancel; + final void Function() onSave; + + const DetailBottomRow( + {Key? key, required this.onCancel, required this.onSave}) + : super(key: key); + + @override + _DetailBottomRowState createState() => _DetailBottomRowState(); +} + +class _DetailBottomRowState extends State { + @override + Widget build(BuildContext context) { + return BottomAppBar( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + ElevatedButton.icon( + onPressed: widget.onCancel, + icon: const Icon( + Icons.close, + size: 18.0, + ), + label: const Text('CANCEL'), + ), + const Spacer(), + ElevatedButton.icon( + onPressed: widget.onSave, + icon: const Icon( + Icons.save, + size: 18.0, + ), + label: const Text('SAVE'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/dialogs.dart b/lib/components/dialogs.dart new file mode 100644 index 0000000..79380fd --- /dev/null +++ b/lib/components/dialogs.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +class Dialogs { + static void showCancelConfirmationDialog( + {required BuildContext context, + required bool isNew, + required void Function() onSave, + String? message, + void Function(BuildContext context)? onDiscard}) { + showDialog( + context: context, + builder: (BuildContext context) { + List actions = [ + TextButton( + onPressed: () => Navigator.pop(context, 'CANCEL'), + child: const Text('CANCEL'), + ), + ]; + + actions.add(isNew + ? ElevatedButton( + onPressed: () => Navigator.pop(context, 'DISCARD'), + child: const Text('DISCARD'), + ) + : TextButton( + onPressed: () => Navigator.pop(context, 'DISCARD'), + child: const Text('DISCARD'), + )); + + if (!isNew) { + actions.add(ElevatedButton( + onPressed: () => Navigator.pop(context, 'SAVE'), + child: const Text('SAVE'), + )); + } + + return AlertDialog( + content: Text(message ?? + 'You already made some changes. Discard your input?'), + actions: actions, + ); + }).then((value) { + if (value == 'DISCARD') { + onDiscard != null ? onDiscard(context) : Navigator.pop(context); + } + if (value == 'SAVE') { + onSave(); + } + }); + } + + static void showConfirmationDialog( + {required BuildContext context, + required void Function() onConfirm, + String message = 'Are you sure you want to delete this record?', + String confirmationLabel = 'DELETE'}) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'CANCEL'), + child: const Text('CANCEL'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, 'CONFIRM'), + child: Text(confirmationLabel), + ), + ], + ); + }).then((value) { + if (value == 'CONFIRM') { + onConfirm(); + } + }); + } +} diff --git a/lib/components/forms.dart b/lib/components/forms.dart new file mode 100644 index 0000000..a09f32c --- /dev/null +++ b/lib/components/forms.dart @@ -0,0 +1,222 @@ +import 'package:diameter/components/progress_indicator.dart'; +import 'package:flutter/material.dart'; + +class StyledForm extends StatefulWidget { + final List? fields; + final List? buttons; + final GlobalKey? formState; + + const StyledForm({Key? key, this.formState, this.fields, this.buttons}) + : super(key: key); + + @override + _StyledFormState createState() => _StyledFormState(); +} + +class _StyledFormState extends State { + @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 { + @override + Widget build(BuildContext context) { + return FormField(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 { + @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 extends StatefulWidget { + final String label; + final T? selectedItem; + final List 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 createState() => _StyledDropdownButtonState(); +} + +class _StyledDropdownButtonState extends State> { + @override + Widget build(BuildContext context) { + return DropdownButtonFormField( + decoration: InputDecoration( + labelText: widget.label, + ), + value: widget.selectedItem, + onChanged: widget.onChanged, + items: widget.items + .map((item) => DropdownMenuItem( + value: item, + child: widget.renderItem(item), + )) + .toList(), + ); + } +} + +class StyledFutureDropdownButton extends StatefulWidget { + final String label; + final String? selectedItem; + final Future> 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 createState() => + _StyledFutureDropdownButtonState(); +} + +class _StyledFutureDropdownButtonState + extends State> { + @override + Widget build(BuildContext context) { + return FutureBuilder>( + 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( + decoration: InputDecoration( + labelText: widget.label, + ), + value: widget.selectedItem, + onChanged: widget.onChanged, + items: snapshot.data! + .map((item) => DropdownMenuItem( + value: widget.getItemValue(item), + child: widget.renderItem(item), + )) + .toList(), + ), + ); + }, + ); + } +} diff --git a/lib/components/progress_indicator.dart b/lib/components/progress_indicator.dart new file mode 100644 index 0000000..69e18e5 --- /dev/null +++ b/lib/components/progress_indicator.dart @@ -0,0 +1,55 @@ +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 { + @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: const CircularProgressIndicator(), + ), + ), + ); + 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; + } + } + } +} diff --git a/lib/config.dart b/lib/config.dart new file mode 100644 index 0000000..d242e7e --- /dev/null +++ b/lib/config.dart @@ -0,0 +1,28 @@ +import 'package:diameter/settings.dart'; + +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; diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..e7afb58 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,68 @@ +import 'package:diameter/components/app_theme.dart'; +import 'package:diameter/screens/accuracy_detail.dart'; +import 'package:diameter/screens/basal/basal_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_entry.dart'; +import 'package:diameter/screens/log/log_event_detail.dart'; +import 'package:diameter/screens/log/log_event_type_detail.dart'; +import 'package:diameter/screens/log/log_event_type_list.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_detail.dart'; +import 'package:diameter/screens/meal/meal_list.dart'; +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_source_detail.dart'; +import 'package:diameter/screens/meal/meal_source_list.dart'; +import 'package:diameter/settings.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/config.dart'; +import 'package:diameter/screens/basal/basal_profiles_list.dart'; +import 'package:diameter/screens/bolus/bolus_profile_list.dart'; +import 'package:diameter/navigation.dart'; +import 'package:flex_color_scheme/flex_color_scheme.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Parse().initialize(keyApplicationId, keyParseServerUrl, + clientKey: keyClientKey, debug: true); + + Settings.loadSettingsIntoConfig(); + + runApp( + MaterialApp( + theme: AppTheme.makeTheme(AppTheme.lightTheme), + darkTheme: AppTheme.makeTheme(AppTheme.darkTheme), + themeMode: ThemeMode.system, + initialRoute: '/', + routes: { + '/': (context) => const LogScreen(), + Routes.log: (context) => const LogScreen(), + Routes.logEntry: (context) => const LogEntryScreen(), + Routes.logEvent: (context) => const LogEventDetailScreen(), + Routes.logEventTypes: (context) => const LogEventTypeListScreen(), + Routes.logEventType: (context) => const LogEventTypeDetailScreen(), + Routes.accuracies: (context) => const AccuracyListScreen(), + Routes.accuracy: (context) => const AccuracyDetailScreen(), + Routes.meals: (context) => const MealListScreen(), + Routes.meal: (context) => const MealDetailScreen(), + Routes.mealCategories: (context) => const MealCategoryListScreen(), + Routes.mealCategory: (context) => const MealCategoryDetailScreen(), + Routes.mealPortionTypes: (context) => const MealPortionTypeListScreen(), + Routes.mealPortionType: (context) => + const MealPortionTypeDetailScreen(), + Routes.mealSources: (context) => const MealSourceListScreen(), + Routes.mealSource: (context) => const MealSourceDetailScreen(), + Routes.bolusProfiles: (context) => const BolusProfileListScreen(), + Routes.bolusProfile: (context) => const BolusProfileDetailScreen(), + Routes.basalProfiles: (context) => const BasalProfileListScreen(), + Routes.basalProfile: (context) => const BasalProfileDetailScreen(), + Routes.settings: (context) => const SettingsScreen(), + }, + ), + ); +} diff --git a/lib/models/accuracy.dart b/lib/models/accuracy.dart new file mode 100644 index 0000000..b01a896 --- /dev/null +++ b/lib/models/accuracy.dart @@ -0,0 +1,126 @@ +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; + +class Accuracy { + late String? objectId; + late String value; + late bool forCarbsRatio = false; + late bool forPortionSize = false; + late int? confidenceRating; + late String? notes; + + Accuracy(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + value = object.get('value')!; + forCarbsRatio = object.get('forCarbsRatio')!; + forPortionSize = object.get('forPortionSize')!; + confidenceRating = object.get('confidenceRating') != null + ? object.get('confidenceRating')!.toInt() + : null; + notes = object.get('notes'); + } + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('Accuracy')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => Accuracy(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future> fetchAllForCarbsRatio() async { + QueryBuilder query = + QueryBuilder(ParseObject('Accuracy')) + ..whereEqualTo('forCarbsRatio', true); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => Accuracy(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future> fetchAllForPortionSize() async { + QueryBuilder query = + QueryBuilder(ParseObject('Accuracy')) + ..whereEqualTo('forPortionSize', true); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => Accuracy(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('Accuracy')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return Accuracy(apiResponse.result.first); + } + } + + static Future save({ + required String value, + bool forCarbsRatio = false, + bool forPortionSize = false, + int? confidenceRating, + String? notes, + }) async { + final accuracy = ParseObject('Accuracy') + ..set('value', value) + ..set('forCarbsRatio', forCarbsRatio) + ..set('forPortionSize', forPortionSize) + ..set('confidenceRating', confidenceRating) + ..set('notes', notes); + await accuracy.save(); + } + + static Future update( + String objectId, { + String? value, + bool? forCarbsRatio, + bool? forPortionSize, + int? confidenceRating, + String? notes, + }) async { + var accuracy = ParseObject('Accuracy')..objectId = objectId; + if (value != null) { + accuracy.set('value', value); + } + if (forCarbsRatio != null) { + accuracy.set('forCarbsRatio', forCarbsRatio); + } + if (forPortionSize != null) { + accuracy.set('forPortionSize', forPortionSize); + } + if (confidenceRating != null) { + accuracy.set('confidenceRating', confidenceRating); + } + if (notes != null) { + accuracy.set('notes', notes); + } + await accuracy.save(); + } + + Future delete() async { + var accuracy = ParseObject('Accuracy')..objectId = objectId; + await accuracy.delete(); + } +} diff --git a/lib/models/basal.dart b/lib/models/basal.dart new file mode 100644 index 0000000..3658c53 --- /dev/null +++ b/lib/models/basal.dart @@ -0,0 +1,114 @@ +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +import 'package:diameter/models/basal_profile.dart'; +import 'package:diameter/components/data_table.dart'; + +class Basal extends DataTableContent { + late String? objectId; + late DateTime startTime; + late DateTime endTime; + late double units; + late String basalProfile; + + Basal(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + startTime = object.get('startTime')!; + endTime = object.get('endTime')!; + units = object.get('units')! / 100; + basalProfile = + object.get('basalProfile')!.get('objectId')!; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('Basal')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return Basal(apiResponse.result.first); + } + } + + static Future> fetchAllForBasalProfile( + BasalProfile basalProfile) async { + QueryBuilder query = + QueryBuilder(ParseObject('Basal')) + ..whereEqualTo( + 'basalProfile', + (ParseObject('BasalProfile')..objectId = basalProfile.objectId!) + .toPointer()); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results!.map((e) => Basal(e as ParseObject)).toList(); + } else { + return []; + } + } + + static Future save({ + required DateTime startTime, + required DateTime endTime, + required double units, + required String basalProfile, + }) async { + final basal = ParseObject('Basal') + ..set('startTime', startTime) + ..set('endTime', endTime) + ..set('units', units * 100) + ..set('basalProfile', + (ParseObject('BasalProfile')..objectId = basalProfile).toPointer()); + await basal.save(); + } + + static Future update( + String objectId, { + DateTime? startTime, + DateTime? endTime, + double? units, + }) async { + var basal = ParseObject('Basal')..objectId = objectId; + if (startTime != null) { + basal.set('startTime', startTime); + } + if (endTime != null) { + basal.set('endTime', endTime); + } + if (units != null) { + basal.set('units', units * 100); + } + await basal.save(); + } + + Future delete() async { + var basal = ParseObject('Basal')..objectId = objectId; + await basal.delete(); + } + + @override + List asDataTableCells(List? actions) { + return [ + DataCell(Text(DateTimeUtils.displayTime(startTime))), + DataCell(Text(DateTimeUtils.displayTime(endTime))), + DataCell(Text('${units.toString()} U')), + DataCell( + Row( + children: actions ?? [], + ), + ), + ]; + } + + static List asDataTableColumns() { + return [ + const DataColumn(label: Expanded(child: Text('Start Time'))), + const DataColumn(label: Expanded(child: Text('End Time'))), + const DataColumn(label: Expanded(child: Text('Units'))), + const DataColumn(label: Expanded(child: Text('Actions'))), + ]; + } +} diff --git a/lib/models/basal_profile.dart b/lib/models/basal_profile.dart new file mode 100644 index 0000000..ba4e587 --- /dev/null +++ b/lib/models/basal_profile.dart @@ -0,0 +1,88 @@ +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +import 'package:diameter/models/basal.dart'; + +class BasalProfile { + late String? objectId; + late String name; + late bool active = false; + late Future> basalRates; + late String? notes; + + BasalProfile(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + name = object.get('name')!; + active = object.get('active')!; + basalRates = Basal.fetchAllForBasalProfile(this); + notes = object.get('notes'); + } + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('BasalProfile')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => BasalProfile(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('BasalProfile')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return BasalProfile(apiResponse.result.first); + } + } + + static Future setAllInactiveButOne(String? objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('BasalProfile')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + for (var basalProfile in apiResponse.results as List) { + basalProfile.set( + 'active', basalProfile.objectId == objectId ? true : false); + await basalProfile.save(); + } + } + } + + static Future save( + {required String name, bool active = false, String? notes}) async { + final basalProfile = ParseObject('BasalProfile') + ..set('name', name) + ..set('active', active) + ..set('notes', notes); + await basalProfile.save(); + } + + static Future update(String objectId, + {String? name, bool? active, String? notes}) async { + var basalProfile = ParseObject('BasalProfile')..objectId = objectId; + if (name != null) { + basalProfile.set('name', name); + } + if (active != null) { + basalProfile.set('active', active); + } + if (notes != null) { + basalProfile.set('notes', notes); + } + await basalProfile.save(); + } + + Future delete() async { + var basalProfile = ParseObject('BasalProfile')..objectId = objectId; + await basalProfile.delete(); + } +} diff --git a/lib/models/bolus.dart b/lib/models/bolus.dart new file mode 100644 index 0000000..a73912a --- /dev/null +++ b/lib/models/bolus.dart @@ -0,0 +1,199 @@ +import 'package:diameter/config.dart'; +import 'package:diameter/settings.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +import 'package:diameter/components/data_table.dart'; +import 'package:diameter/models/bolus_profile.dart'; + +class Bolus extends DataTableContent { + late String? objectId; + late DateTime startTime; + late DateTime endTime; + late double units; + late double carbs; + late int? mgPerDl; + late double? mmolPerL; + late String bolusProfile; + + Bolus(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + startTime = object.get('startTime')!; + endTime = object.get('endTime')!; + units = object.get('units')! / 100; + carbs = object.get('carbs')!.toDouble(); + mgPerDl = object.get('mgPerDl') != null + ? object.get('mgPerDl')!.toInt() + : null; + mmolPerL = object.get('mmolPerL') != null + ? object.get('mmolPerL')! / 100 + : null; + bolusProfile = + object.get('bolusProfile')!.get('objectId')!; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('Bolus')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return Bolus(apiResponse.result.first); + } + } + + static Future> fetchAllForBolusProfile( + BolusProfile bolusProfile) async { + QueryBuilder query = + QueryBuilder(ParseObject('Bolus')) + ..whereEqualTo( + 'bolusProfile', + (ParseObject('BolusProfile')..objectId = bolusProfile.objectId!) + .toPointer()); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results!.map((e) => Bolus(e as ParseObject)).toList(); + } else { + return []; + } + } + + static Future save({ + required DateTime startTime, + required DateTime endTime, + required double units, + required double carbs, + int? mgPerDl, + double? mmolPerL, + required String bolusProfile, + }) async { + final bolus = ParseObject('Bolus') + ..set('startTime', startTime) + ..set('endTime', endTime) + ..set('units', units * 100) + ..set('carbs', carbs.round()) + ..set('bolusProfile', + (ParseObject('BolusProfile')..objectId = bolusProfile).toPointer()); + + bolus.set( + 'mgPerDl', + mgPerDl != null + ? mgPerDl.round() + : Utils.convertMmolPerLToMgPerDl(mmolPerL ?? 0)); + bolus.set( + 'mmolPerL', + mmolPerL != null + ? mmolPerL * 100 + : Utils.convertMgPerDlToMmolPerL(mgPerDl ?? 0) * 100); + + await bolus.save(); + } + + static Future update( + String objectId, { + DateTime? startTime, + DateTime? endTime, + double? units, + double? carbs, + int? mgPerDl, + double? mmolPerL, + }) async { + var bolus = ParseObject('Bolus')..objectId = objectId; + if (startTime != null) { + bolus.set('startTime', startTime); + } + if (endTime != null) { + bolus.set('endTime', endTime); + } + if (units != null) { + bolus.set('units', units * 100); + } + if (carbs != null) { + bolus.set('carbs', carbs); + } + + if (mgPerDl != null || mmolPerL != null) { + bolus.set( + 'mgPerDl', + mgPerDl != null + ? mgPerDl.round() + : Utils.convertMmolPerLToMgPerDl(mmolPerL ?? 0)); + bolus.set( + 'mmolPerL', + mmolPerL != null + ? mmolPerL * 100 + : Utils.convertMgPerDlToMmolPerL(mgPerDl ?? 0) * 100); + } + + await bolus.save(); + } + + Future delete() async { + var bolus = ParseObject('Bolus')..objectId = objectId; + await bolus.delete(); + } + + @override + List asDataTableCells(List? actions) { + var cols = [ + DataCell(Text(DateTimeUtils.displayTime(startTime))), + DataCell(Text(DateTimeUtils.displayTime(endTime))), + DataCell(Text('${units.toString()} U')), + DataCell(Text('${carbs.toString()} g')), + ]; + + if (glucoseMeasurement == GlucoseMeasurement.mgPerDl || + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForList) { + cols.add(DataCell(Text('${mgPerDl.toString()} mg/dl'))); + } + + if (glucoseMeasurement == GlucoseMeasurement.mmolPerL || + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForList) { + cols.add(DataCell(Text('${mmolPerL.toString()} mmol/l'))); + } + + cols.add( + DataCell( + Row( + children: actions ?? [], + ), + ), + ); + + return cols; + } + + static List asDataTableColumns() { + var cols = [ + const DataColumn(label: Expanded(child: Text('Start Time'))), + const DataColumn(label: Expanded(child: Text('End Time'))), + const DataColumn(label: Expanded(child: Text('Units'))), + const DataColumn(label: Expanded(child: Text('per Carbs'))), + ]; + + if (glucoseMeasurement == GlucoseMeasurement.mgPerDl || + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForList) { + cols.add(const DataColumn(label: Expanded(child: Text('per mg/dl')))); + } + + if (glucoseMeasurement == GlucoseMeasurement.mmolPerL || + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForList) { + cols.add(const DataColumn(label: Expanded(child: Text('per mmol/l')))); + } + + cols.add( + const DataColumn(label: Expanded(child: Text('Actions'))), + ); + + return cols; + } +} diff --git a/lib/models/bolus_profile.dart b/lib/models/bolus_profile.dart new file mode 100644 index 0000000..335e60d --- /dev/null +++ b/lib/models/bolus_profile.dart @@ -0,0 +1,88 @@ +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +import 'package:diameter/models/bolus.dart'; + +class BolusProfile { + late String? objectId; + late String name; + late bool active = false; + late Future> bolusRates; + late String? notes; + + BolusProfile(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + name = object.get('name')!; + active = object.get('active')!; + bolusRates = Bolus.fetchAllForBolusProfile(this); + notes = object.get('notes'); + } + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('BolusProfile')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => BolusProfile(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('BolusProfile')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return BolusProfile(apiResponse.result.first); + } + } + + static Future setAllInactiveButOne(String? objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('BolusProfile')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + for (var bolusProfile in apiResponse.results as List) { + bolusProfile.set( + 'active', bolusProfile.objectId == objectId ? true : false); + await bolusProfile.save(); + } + } + } + + static Future save( + {required String name, bool active = false, String? notes}) async { + final bolusProfile = ParseObject('BolusProfile') + ..set('name', name) + ..set('active', active) + ..set('notes', notes); + await bolusProfile.save(); + } + + static Future update(String objectId, + {String? name, bool? active, String? notes}) async { + var bolusProfile = ParseObject('BolusProfile')..objectId = objectId; + if (name != null) { + bolusProfile.set('name', name); + } + if (active != null) { + bolusProfile.set('active', active); + } + if (notes != null) { + bolusProfile.set('notes', notes); + } + await bolusProfile.save(); + } + + Future delete() async { + var bolusProfile = ParseObject('BolusProfile')..objectId = objectId; + await bolusProfile.delete(); + } +} diff --git a/lib/models/log_entry.dart b/lib/models/log_entry.dart new file mode 100644 index 0000000..bfefcef --- /dev/null +++ b/lib/models/log_entry.dart @@ -0,0 +1,169 @@ +import 'package:diameter/models/log_meal.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +import 'package:diameter/models/log_event.dart'; + +class LogEntry { + late String? objectId; + late DateTime time; + late int? mgPerDl; + late double? mmolPerL; + late double? bolusGlucose; + late int? delayedBolusDuration; + // TODO: either rename this or all other fields using delayedBolusRate + late double? delayedBolusRatio; + late String? notes; + late Future> events; + late Future> endedEvents; + late Future> meals; + + LogEntry(ParseObject object) { + objectId = object.get('objectId'); + time = object.get('time')!; + mgPerDl = object.get('mgPerDl') != null + ? object.get('mgPerDl')!.toInt() + : null; + mmolPerL = object.get('mmolPerL') != null + ? object.get('mmolPerL')!.toDouble() + : null; + bolusGlucose = object.get('bolusGlucose') != null + ? object.get('bolusGlucose')!.toDouble() + : null; + delayedBolusDuration = object.get('delayedBolusDuration') != null + ? object.get('delayedBolusDuration')!.toInt() + : null; + delayedBolusRatio = object.get('delayedBolusRatio') != null + ? object.get('delayedBolusRatio')!.toDouble() + : null; + events = LogEvent.fetchAllForLogEntry(this); + endedEvents = LogEvent.fetchAllEndedByEntry(this); + meals = LogMeal.fetchAllForLogEntry(this); + notes = object.get('notes'); + } + + static Future> fetchAllForRange(DateTimeRange range) async { + QueryBuilder query = + QueryBuilder(ParseObject('LogEntry')) + ..whereGreaterThanOrEqualsTo('time', range.start) + ..whereLessThanOrEqualTo('time', range.end); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => LogEntry(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('LogEntry')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => LogEntry(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('LogEntry')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return LogEntry(apiResponse.result.first); + } + } + + static Future save({ + required DateTime time, + int? mgPerDl, + double? mmolPerL, + double? bolusGlucose, + int? delayedBolusDuration, + double? delayedBolusRatio, + String? notes, + }) async { + final logEntry = ParseObject('LogEntry') + ..set('time', time) + ..set('bolusGlucose', bolusGlucose) + ..set('delayedBolusDuration', delayedBolusDuration) + ..set('delayedBolusRatio', delayedBolusRatio) + ..set('notes', notes); + + if (!(mgPerDl == null && mmolPerL == null)) { + logEntry.set( + 'mgPerDl', + mgPerDl != null + ? mgPerDl.round() + : Utils.convertMmolPerLToMgPerDl(mmolPerL ?? 0)); + logEntry.set( + 'mmolPerL', + mmolPerL != null + ? mmolPerL * 100 + : Utils.convertMgPerDlToMmolPerL(mgPerDl ?? 0) * 100); + } + + await logEntry.save(); + } + + static Future update( + String objectId, { + DateTime? time, + int? mgPerDl, + double? mmolPerL, + double? bolusGlucose, + int? delayedBolusDuration, + double? delayedBolusRatio, + String? notes, + }) async { + final logEntry = ParseObject('LogEntry'); + + if (time != null) { + logEntry.set('time', time); + } + + if (bolusGlucose != null) { + logEntry.set('bolusGlucose', bolusGlucose); + } + + if (delayedBolusDuration != null) { + logEntry.set('delayedBolusDuration', delayedBolusDuration); + } + + if (delayedBolusRatio != null) { + logEntry.set('delayedBolusRatio', delayedBolusRatio); + } + + if (notes != null) { + logEntry.set('notes', notes); + } + + if (!(mgPerDl == null && mmolPerL == null)) { + logEntry.set( + 'mgPerDl', + mgPerDl != null + ? mgPerDl.round() + : Utils.convertMmolPerLToMgPerDl(mmolPerL ?? 0)); + logEntry.set( + 'mmolPerL', + mmolPerL != null + ? mmolPerL * 100 + : Utils.convertMgPerDlToMmolPerL(mgPerDl ?? 0) * 100); + } + await logEntry.save(); + } + + Future delete() async { + var logEntry = ParseObject('LogEntry')..objectId = objectId; + await logEntry.delete(); + } +} diff --git a/lib/models/log_event.dart b/lib/models/log_event.dart new file mode 100644 index 0000000..d0869ba --- /dev/null +++ b/lib/models/log_event.dart @@ -0,0 +1,176 @@ +import 'package:diameter/components/data_table.dart'; +import 'package:diameter/models/log_entry.dart'; +import 'package:diameter/models/log_event_type.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; + +class LogEvent extends DataTableContent { + late String? objectId; + late String logEntry; + late String? endLogEntry; + late String eventType; + late DateTime time; + late DateTime? endTime; + late bool hasEndTime; + late String? notes; + + LogEvent(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + logEntry = object.get('logEntry')!.get('objectId')!; + endLogEntry = + object.get('endLogEntry')?.get('objectId'); + eventType = + object.get('eventType')!.get('objectId')!; + time = object.get('time')!; + endTime = object.get('endTime'); + hasEndTime = object.get('hasEndTime')!; + notes = object.get('notes'); + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('LogEvent')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return LogEvent(apiResponse.result.first); + } + } + + static Future> fetchAllActive() async { + QueryBuilder query = + QueryBuilder(ParseObject('LogEvent')) + ..whereEqualTo('hasEndTime', true) + ..whereEqualTo('endTime', null); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => LogEvent(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future> fetchAllForLogEntry(LogEntry logEntry) async { + QueryBuilder query = QueryBuilder( + ParseObject('LogEvent')) + ..whereEqualTo('logEntry', + (ParseObject('LogEntry')..objectId = logEntry.objectId!).toPointer()); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => LogEvent(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future> fetchAllEndedByEntry(LogEntry logEntry) async { + QueryBuilder query = QueryBuilder( + ParseObject('LogEvent')) + ..whereEqualTo('endLogEntry', + (ParseObject('LogEntry')..objectId = logEntry.objectId!).toPointer()); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => LogEvent(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future save({ + required String logEntry, + required String eventType, + required DateTime time, + required bool hasEndTime, + String? notes, + }) async { + final logEvent = ParseObject('LogEvent') + ..set('logEntry', + (ParseObject('LogEntry')..objectId = logEntry).toPointer()) + ..set('eventType', + (ParseObject('LogEventType')..objectId = eventType).toPointer()) + ..set('time', time) + ..set('hasEndTime', hasEndTime) + ..set('notes', notes); + await logEvent.save(); + } + + static Future update( + String objectId, { + String? eventType, + String? endLogEntry, + DateTime? time, + DateTime? endTime, + bool? hasEndTime, + String? notes, + }) async { + var logEvent = ParseObject('LogEvent')..objectId = objectId; + if (eventType != null) { + logEvent.set('eventType', + (ParseObject('LogEventType')..objectId = eventType).toPointer()); + } + if (endLogEntry != null) { + logEvent.set('endLogEntry', + (ParseObject('LogEntry')..objectId = endLogEntry).toPointer()); + } + if (time != null) { + logEvent.set('time', time); + } + if (endTime != null) { + logEvent.set('endTime', endTime); + } + if (hasEndTime != null) { + logEvent.set('hasEndTime', hasEndTime); + } + if (notes != null) { + logEvent.set('notes', notes); + } + await logEvent.save(); + } + + Future delete() async { + var logEvent = ParseObject('LogEvent')..objectId = objectId; + await logEvent.delete(); + } + + @override + List asDataTableCells(List actions, + {List? types}) { + return [ + DataCell(Text( + types?.firstWhere((element) => element.objectId == eventType).value ?? + types?.length.toString() ?? + '')), + DataCell(Text(DateTimeUtils.displayDateTime(time))), + DataCell(Text(hasEndTime + ? DateTimeUtils.displayDateTime(endTime, fallback: 'ongoing') + : '-')), + DataCell( + Row( + children: actions, + ), + ), + ]; + } + + static List asDataTableColumns() { + return [ + const DataColumn(label: Expanded(child: Text('Event Type'))), + const DataColumn(label: Expanded(child: Text('Start Time'))), + const DataColumn(label: Expanded(child: Text('End Time'))), + const DataColumn(label: Expanded(child: Text('Actions'))), + ]; + } +} diff --git a/lib/models/log_event_type.dart b/lib/models/log_event_type.dart new file mode 100644 index 0000000..afa95dc --- /dev/null +++ b/lib/models/log_event_type.dart @@ -0,0 +1,86 @@ +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; + +class LogEventType { + late String? objectId; + late String value; + late bool hasEndTime; + late int? defaultReminderDuration; + late String? notes; + + LogEventType(ParseObject object) { + objectId = object.get('objectId'); + value = object.get('value')!; + hasEndTime = object.get('hasEndTime')!; + defaultReminderDuration = object.get('defaultReminderDuration') != null + ? object.get('defaultReminderDuration')!.toInt() + : null; + notes = object.get('notes'); + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('LogEventType')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => LogEventType(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('LogEventType')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return LogEventType(apiResponse.result.first); + } + } + + static Future save({ + required String value, + required bool hasEndTime, + int? defaultReminderDuration, + String? notes, + }) async { + final logEventType = ParseObject('LogEventType') + ..set('value', value) + ..set('hasEndTime', hasEndTime) + ..set('defaultReminderDuration', defaultReminderDuration) + ..set('notes', notes); + await logEventType.save(); + } + + static Future update( + String objectId, { + String? value, + bool? hasEndTime, + int? defaultReminderDuration, + String? notes, + }) async { + var logEventType = ParseObject('LogEventType')..objectId = objectId; + if (value != null) { + logEventType.set('value', value); + } + if (hasEndTime != null) { + logEventType.set('hasEndTime', hasEndTime); + } + if (defaultReminderDuration != null) { + logEventType.set('defaultReminderDuration', defaultReminderDuration); + } + if (notes != null) { + logEventType.set('notes', notes); + } + await logEventType.save(); + } + + Future delete() async { + var logEventType = ParseObject('LogEventType')..objectId = objectId; + await logEventType.delete(); + } +} diff --git a/lib/models/log_meal.dart b/lib/models/log_meal.dart new file mode 100644 index 0000000..46ce504 --- /dev/null +++ b/lib/models/log_meal.dart @@ -0,0 +1,248 @@ +import 'package:diameter/components/data_table.dart'; +import 'package:diameter/models/log_entry.dart'; +import 'package:flutter/material.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; + +class LogMeal extends DataTableContent { + late String? objectId; + late String logEntry; + late String? meal; + late String value; + late String? source; + late String? category; + late String? portionType; + late double? carbsRatio; + late double? portionSize; + late double? carbsPerPortion; + late String? portionSizeAccuracy; + late String? carbsRatioAccuracy; + late double? bolus; + late int? delayedBolusDuration; + late double? delayedBolusRate; + late String? notes; + + LogMeal(ParseObject object) { + objectId = object.get('objectId'); + logEntry = object.get('logEntry')!.get('objectId')!; + meal = object.get('meal') != null + ? object.get('meal')!.get('objectId') + : null; + value = object.get('value')!; + source = object.get('source') != null + ? object.get('source')!.get('objectId') + : null; + category = object.get('category') != null + ? object.get('category')!.get('objectId') + : null; + portionType = object.get('portionType') != null + ? object.get('portionType')!.get('objectId') + : null; + carbsRatio = object.get('carbsRatio')!.toDouble(); + portionSize = object.get('portionSize')!.toDouble(); + carbsPerPortion = object.get('carbsPerPortion')!.toDouble(); + portionSizeAccuracy = object.get('portionSizeAccuracy') != null + ? object + .get('portionSizeAccuracy')! + .get('objectId') + : null; + carbsRatioAccuracy = object.get('carbsRatioAccuracy') != null + ? object.get('carbsRatioAccuracy')!.get('objectId') + : null; + bolus = object.get('bolus') != null + ? object.get('bolus')!.toDouble() + : null; + delayedBolusDuration = object.get('delayedBolusDuration') != null + ? object.get('delayedBolusDuration')!.toInt() + : null; + delayedBolusRate = object.get('delayedBolusRate') != null + ? object.get('delayedBolusRate')!.toDouble() + : null; + notes = object.get('notes'); + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('LogMeal')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return LogMeal(apiResponse.result.first); + } + } + + static Future> fetchAllForLogEntry(LogEntry logEntry) async { + QueryBuilder query = QueryBuilder( + ParseObject('LogMeal')) + ..whereEqualTo('logEntry', + (ParseObject('LogEntry')..objectId = logEntry.objectId!).toPointer()); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => LogMeal(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future save({ + required String value, + required String logEntry, + String? meal, + String? source, + String? category, + String? portionType, + double? carbsRatio, + double? portionSize, + double? carbsPerPortion, + String? portionSizeAccuracy, + String? carbsRatioAccuracy, + double? bolus, + int? delayedBolusDuration, + double? delayedBolusRate, + String? notes, + }) async { + final logMeal = ParseObject('Meal') + ..set('value', value) + ..set('logEntry', + (ParseObject('LogEntry')..objectId = logEntry).toPointer()) + ..set('carbsRatio', carbsRatio) + ..set('portionSize', portionSize) + ..set('carbsPerPortion', carbsPerPortion) + ..set('bolus', bolus) + ..set('delayedBolusDuration', delayedBolusDuration) + ..set('delayedBolusRate', delayedBolusRate) + ..set('notes', notes); + + if (meal != null) { + logMeal.set('meal', (ParseObject('Meal')..objectId = meal).toPointer()); + } + if (source != null) { + logMeal.set( + 'source', (ParseObject('MealSource')..objectId = source).toPointer()); + } + if (category != null) { + logMeal.set('category', + (ParseObject('MealCategory')..objectId = category).toPointer()); + } + if (portionType != null) { + logMeal.set('portionType', + (ParseObject('MealPortionType')..objectId = portionType).toPointer()); + } + if (portionSizeAccuracy != null) { + logMeal.set( + 'portionSizeAccuracy', + (ParseObject('Accuracy')..objectId = portionSizeAccuracy) + .toPointer()); + } + if (carbsRatioAccuracy != null) { + logMeal.set('carbsRatioAccuracy', + (ParseObject('Accuracy')..objectId = carbsRatioAccuracy).toPointer()); + } + await logMeal.save(); + } + + static Future update( + String objectId, { + String? value, + String? meal, + String? source, + String? category, + String? portionType, + double? carbsRatio, + double? portionSize, + double? carbsPerPortion, + String? portionSizeAccuracy, + String? carbsRatioAccuracy, + double? bolus, + int? delayedBolusDuration, + double? delayedBolusRate, + String? notes, + }) async { + var logMeal = ParseObject('Meal')..objectId = objectId; + if (value != null) { + logMeal.set('value', value); + } + if (meal != null) { + logMeal.set('meal', (ParseObject('Meal')..objectId = meal).toPointer()); + } + if (source != null) { + logMeal.set( + 'source', (ParseObject('MealSource')..objectId = source).toPointer()); + } + if (category != null) { + logMeal.set('category', + (ParseObject('MealCategory')..objectId = category).toPointer()); + } + if (portionType != null) { + logMeal.set('portionType', + (ParseObject('MealPortionType')..objectId = portionType).toPointer()); + } + if (carbsRatio != null) { + logMeal.set('carbsRatio', carbsRatio); + } + if (portionSize != null) { + logMeal.set('portionSize', portionSize); + } + if (carbsPerPortion != null) { + logMeal.set('carbsPerPortion', carbsPerPortion); + } + if (portionSizeAccuracy != null) { + logMeal.set( + 'portionSizeAccuracy', + (ParseObject('Accuracy')..objectId = portionSizeAccuracy) + .toPointer()); + } + if (carbsRatioAccuracy != null) { + logMeal.set('carbsRatioAccuracy', + (ParseObject('Accuracy')..objectId = carbsRatioAccuracy).toPointer()); + } + if (bolus != null) { + logMeal.set('bolus', bolus); + } + if (delayedBolusDuration != null) { + logMeal.set('delayedBolusDuration', delayedBolusDuration); + } + if (delayedBolusRate != null) { + logMeal.set('delayedBolusRate', delayedBolusRate); + } + if (notes != null) { + logMeal.set('notes', notes); + } + await logMeal.save(); + } + + Future delete() async { + var logMeal = ParseObject('LogMeal')..objectId = objectId; + await logMeal.delete(); + } + + @override + List asDataTableCells(List? actions) { + return [ + DataCell(Text(value)), + DataCell(Text('${(carbsPerPortion ?? '').toString()} g')), + DataCell(Text('${(bolus ?? '').toString()} U')), + DataCell(Text(delayedBolusRate != null + ? '${delayedBolusRate.toString()} U/${(delayedBolusDuration ?? '').toString()} min' + : '')), + DataCell( + Row( + children: actions ?? [], + ), + ), + ]; + } + + static List asDataTableColumns() { + return [ + const DataColumn(label: Expanded(child: Text('Meal'))), + const DataColumn(label: Expanded(child: Text('Carbs'))), + const DataColumn(label: Expanded(child: Text('Bolus'))), + const DataColumn(label: Expanded(child: Text('Delayed Bolus'))), + const DataColumn(label: Expanded(child: Text('Actions'))), + ]; + } +} diff --git a/lib/models/meal.dart b/lib/models/meal.dart new file mode 100644 index 0000000..21bc628 --- /dev/null +++ b/lib/models/meal.dart @@ -0,0 +1,193 @@ +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; + +class Meal { + late String? objectId; + late String value; + late String? source; + late String? category; + late String? portionType; + late double? carbsRatio; + late double? portionSize; + late double? carbsPerPortion; + late String? portionSizeAccuracy; + late String? carbsRatioAccuracy; + late int? delayedBolusDuration; + late double? delayedBolusRate; + late String? notes; + + Meal(ParseObject object) { + objectId = object.get('objectId'); + value = object.get('value')!; + source = object.get('source') != null + ? object.get('source')!.get('objectId') + : null; + category = object.get('category') != null + ? object.get('category')!.get('objectId') + : null; + portionType = object.get('portionType') != null + ? object.get('portionType')!.get('objectId') + : null; + carbsRatio = object.get('carbsRatio') != null + ? object.get('carbsRatio')!.toDouble() + : null; + portionSize = object.get('portionSize') != null + ? object.get('portionSize')!.toDouble() + : null; + carbsPerPortion = object.get('carbsPerPortion') != null + ? object.get('carbsPerPortion')!.toDouble() + : null; + portionSizeAccuracy = object.get('portionSizeAccuracy') != null + ? object + .get('portionSizeAccuracy')! + .get('objectId') + : null; + carbsRatioAccuracy = object.get('carbsRatioAccuracy') != null + ? object.get('carbsRatioAccuracy')!.get('objectId') + : null; + delayedBolusDuration = object.get('delayedBolusDuration') != null + ? object.get('delayedBolusDuration')!.toInt() + : null; + delayedBolusRate = object.get('delayedBolusRate') != null + ? object.get('delayedBolusRate')!.toDouble() + : null; + notes = object.get('notes'); + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('Meal')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results!.map((e) => Meal(e as ParseObject)).toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('Meal')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return Meal(apiResponse.result.first); + } + } + + static Future save({ + required String value, + String? source, + String? category, + String? portionType, + double? carbsRatio, + double? portionSize, + double? carbsPerPortion, + String? portionSizeAccuracy, + String? carbsRatioAccuracy, + int? delayedBolusDuration, + double? delayedBolusRate, + String? notes, + }) async { + final meal = ParseObject('Meal') + ..set('value', value) + ..set('carbsRatio', carbsRatio) + ..set('portionSize', portionSize) + ..set('carbsPerPortion', carbsPerPortion) + ..set('delayedBolusDuration', delayedBolusDuration) + ..set('delayedBolusRate', delayedBolusRate) + ..set('notes', notes); + + if (source != null) { + meal.set( + 'source', (ParseObject('MealSource')..objectId = source).toPointer()); + } + if (category != null) { + meal.set('category', + (ParseObject('MealCategory')..objectId = category).toPointer()); + } + if (portionType != null) { + meal.set('portionType', + (ParseObject('MealPortionType')..objectId = portionType).toPointer()); + } + if (portionSizeAccuracy != null) { + meal.set( + 'portionSizeAccuracy', + (ParseObject('Accuracy')..objectId = portionSizeAccuracy) + .toPointer()); + } + if (carbsRatioAccuracy != null) { + meal.set('carbsRatioAccuracy', + (ParseObject('Accuracy')..objectId = carbsRatioAccuracy).toPointer()); + } + await meal.save(); + } + + static Future update( + String objectId, { + String? value, + String? source, + String? category, + String? portionType, + double? carbsRatio, + double? portionSize, + double? carbsPerPortion, + String? portionSizeAccuracy, + String? carbsRatioAccuracy, + int? delayedBolusDuration, + double? delayedBolusRate, + String? notes, + }) async { + var meal = ParseObject('Meal')..objectId = objectId; + if (value != null) { + meal.set('value', value); + } + if (source != null) { + meal.set( + 'source', (ParseObject('MealSource')..objectId = source).toPointer()); + } + if (category != null) { + meal.set('category', + (ParseObject('MealCategory')..objectId = category).toPointer()); + } + if (portionType != null) { + meal.set('portionType', + (ParseObject('MealPortionType')..objectId = portionType).toPointer()); + } + if (carbsRatio != null) { + meal.set('carbsRatio', carbsRatio); + } + if (portionSize != null) { + meal.set('portionSize', portionSize); + } + if (carbsPerPortion != null) { + meal.set('carbsPerPortion', carbsPerPortion); + } + if (portionSizeAccuracy != null) { + meal.set( + 'portionSizeAccuracy', + (ParseObject('Accuracy')..objectId = portionSizeAccuracy) + .toPointer()); + } + if (carbsRatioAccuracy != null) { + meal.set('carbsRatioAccuracy', + (ParseObject('Accuracy')..objectId = carbsRatioAccuracy).toPointer()); + } + if (delayedBolusDuration != null) { + meal.set('delayedBolusDuration', delayedBolusDuration); + } + if (delayedBolusRate != null) { + meal.set('delayedBolusRate', delayedBolusRate); + } + if (notes != null) { + meal.set('notes', notes); + } + await meal.save(); + } + + Future delete() async { + var meal = ParseObject('Meal')..objectId = objectId; + await meal.delete(); + } +} diff --git a/lib/models/meal_category.dart b/lib/models/meal_category.dart new file mode 100644 index 0000000..318f706 --- /dev/null +++ b/lib/models/meal_category.dart @@ -0,0 +1,68 @@ +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; + +class MealCategory { + late String? objectId; + late String value; + late String? notes; + + MealCategory(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + value = object.get('value')!; + notes = object.get('notes'); + } + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('MealCategory')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results!.map((e) => MealCategory(e as ParseObject)).toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('MealCategory')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return MealCategory(apiResponse.result.first); + } + } + + static Future save({ + required String value, + String? notes, + }) async { + final mealCategory = ParseObject('MealCategory') + ..set('value', value) + ..set('notes', notes); + await mealCategory.save(); + } + + static Future update( + String objectId, { + String? value, + String? notes, + }) async { + var mealCategory = ParseObject('MealCategory')..objectId = objectId; + if (value != null) { + mealCategory.set('value', value); + } + if (notes != null) { + mealCategory.set('notes', notes); + } + await mealCategory.save(); + } + + Future delete() async { + var mealCategory = ParseObject('MealCategory')..objectId = objectId; + await mealCategory.delete(); + } +} diff --git a/lib/models/meal_portion_type.dart b/lib/models/meal_portion_type.dart new file mode 100644 index 0000000..650cab6 --- /dev/null +++ b/lib/models/meal_portion_type.dart @@ -0,0 +1,69 @@ +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; + +class MealPortionType { + late String? objectId; + late String value; + late String? notes; + + MealPortionType(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + value = object.get('value')!; + notes = object.get('notes'); + } + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('MealPortionType')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + // return apiResponse.results as List; + return apiResponse.results!.map((e) => MealPortionType(e as ParseObject)).toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('MealPortionType')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return MealPortionType(apiResponse.result.first); + } + } + + static Future save({ + required String value, + String? notes, + }) async { + final mealPortionType = ParseObject('MealPortionType') + ..set('value', value) + ..set('notes', notes); + await mealPortionType.save(); + } + + static Future update( + String objectId, { + String? value, + String? notes, + }) async { + var mealPortionType = ParseObject('MealPortionType')..objectId = objectId; + if (value != null) { + mealPortionType.set('value', value); + } + if (notes != null) { + mealPortionType.set('notes', notes); + } + await mealPortionType.save(); + } + + Future delete() async { + var mealPortionType = ParseObject('MealPortionType')..objectId = objectId; + await mealPortionType.delete(); + } +} diff --git a/lib/models/meal_source.dart b/lib/models/meal_source.dart new file mode 100644 index 0000000..1a91042 --- /dev/null +++ b/lib/models/meal_source.dart @@ -0,0 +1,157 @@ +// import 'package:diameter/models/accuracy.dart'; +// import 'package:diameter/models/meal_category.dart'; +// import 'package:diameter/models/meal_portion_type.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; + +class MealSource { + late String? objectId; + late String value; + late String? defaultCarbsRatioAccuracy; + late String? defaultPortionSizeAccuracy; + late String? defaultMealCategory; + late String? defaultMealPortionType; + late String? notes; + + MealSource(ParseObject? object) { + if (object != null) { + objectId = object.get('objectId'); + value = object.get('value')!; + defaultCarbsRatioAccuracy = + object.get('defaultCarbsRatioAccuracy') != null + ? object + .get('defaultCarbsRatioAccuracy')! + .get('objectId') + : null; + defaultPortionSizeAccuracy = + object.get('defaultPortionSizeAccuracy') != null + ? object + .get('defaultPortionSizeAccuracy')! + .get('objectId') + : null; + defaultMealCategory = + object.get('defaultMealCategory') != null + ? object + .get('defaultMealCategory')! + .get('objectId') + : null; + defaultMealPortionType = + object.get('defaultMealPortionType') != null + ? object + .get('defaultMealPortionType')! + .get('objectId') + : null; + notes = object.get('notes'); + } + } + + static Future> fetchAll() async { + QueryBuilder query = + QueryBuilder(ParseObject('MealSource')); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return apiResponse.results! + .map((e) => MealSource(e as ParseObject)) + .toList(); + } else { + return []; + } + } + + static Future get(String objectId) async { + QueryBuilder query = + QueryBuilder(ParseObject('MealSource')) + ..whereEqualTo('objectId', objectId); + final ParseResponse apiResponse = await query.query(); + + if (apiResponse.success && apiResponse.results != null) { + return MealSource(apiResponse.result.first); + } + } + + static Future save({ + required String value, + String? defaultCarbsRatioAccuracy, + String? defaultPortionSizeAccuracy, + String? defaultMealCategory, + String? defaultMealPortionType, + String? notes, + }) async { + final mealSource = ParseObject('MealSource') + ..set('value', value) + ..set('notes', notes); + if (defaultCarbsRatioAccuracy != null) { + mealSource.set( + 'defaultCarbsRatioAccuracy', + (ParseObject('Accuracy')..objectId = defaultCarbsRatioAccuracy) + .toPointer()); + } + if (defaultPortionSizeAccuracy != null) { + mealSource.set( + 'defaultCarbsRatioAccuracy', + (ParseObject('Accuracy')..objectId = defaultPortionSizeAccuracy) + .toPointer()); + } + if (defaultMealCategory != null) { + mealSource.set( + 'defaultMealCategory', + (ParseObject('MealCategory')..objectId = defaultMealCategory) + .toPointer()); + } + if (defaultMealPortionType != null) { + mealSource.set( + 'defaultMealPortionType', + (ParseObject('MealPortionType')..objectId = defaultMealPortionType) + .toPointer()); + } + await mealSource.save(); + } + + static Future update( + String objectId, { + String? value, + String? defaultCarbsRatioAccuracy, + String? defaultPortionSizeAccuracy, + String? defaultMealCategory, + String? defaultMealPortionType, + String? notes, + }) async { + final mealSource = ParseObject('MealSource')..objectId = objectId; + if (value != null) { + mealSource.set('value', value); + } + if (defaultCarbsRatioAccuracy != null) { + mealSource.set( + 'defaultCarbsRatioAccuracy', + (ParseObject('Accuracy')..objectId = defaultCarbsRatioAccuracy) + .toPointer()); + } + if (defaultPortionSizeAccuracy != null) { + mealSource.set( + 'defaultCarbsRatioAccuracy', + (ParseObject('Accuracy')..objectId = defaultPortionSizeAccuracy) + .toPointer()); + } + if (defaultMealCategory != null) { + mealSource.set( + 'defaultMealCategory', + (ParseObject('MealCategory')..objectId = defaultMealCategory) + .toPointer()); + } + if (defaultMealPortionType != null) { + mealSource.set( + 'defaultMealPortionType', + (ParseObject('MealPortionType')..objectId = defaultMealPortionType) + .toPointer()); + } + if (notes != null) { + mealSource.set('notes', notes); + } + await mealSource.save(); + } + + Future delete() async { + var mealSource = ParseObject('MealSource')..objectId = objectId; + await mealSource.delete(); + } +} diff --git a/lib/navigation.dart b/lib/navigation.dart new file mode 100644 index 0000000..1b16cc4 --- /dev/null +++ b/lib/navigation.dart @@ -0,0 +1,182 @@ +import 'package:diameter/screens/accuracy_detail.dart'; +import 'package:diameter/screens/accuracy_list.dart'; +import 'package:diameter/screens/basal/basal_detail.dart'; +import 'package:diameter/screens/basal/basal_profile_detail.dart'; +import 'package:diameter/screens/basal/basal_profiles_list.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_list.dart'; +import 'package:diameter/screens/log/log.dart'; +import 'package:diameter/screens/log/log_entry.dart'; +import 'package:diameter/screens/log/log_event_detail.dart'; +import 'package:diameter/screens/log/log_event_type_detail.dart'; +import 'package:diameter/screens/log/log_event_type_list.dart'; +import 'package:diameter/screens/log/log_meal_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_detail.dart'; +import 'package:diameter/screens/meal/meal_list.dart'; +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_source_detail.dart'; +import 'package:diameter/screens/meal/meal_source_list.dart'; +import 'package:diameter/settings.dart'; +import 'package:flutter/material.dart'; + +class Routes { + static const String basal = BasalDetailScreen.routeName; + static const String basalProfile = BasalProfileDetailScreen.routeName; + static const String basalProfiles = BasalProfileListScreen.routeName; + static const List basalRoutes = [basal, basalProfile, basalProfiles]; + + static const String bolus = BolusDetailScreen.routeName; + static const String bolusProfile = BolusProfileDetailScreen.routeName; + static const String bolusProfiles = BolusProfileListScreen.routeName; + static const List bolusRoutes = [bolus, bolusProfile, bolusProfiles]; + + static const String log = LogScreen.routeName; + static const String logEntry = LogEntryScreen.routeName; + static const String logEvent = LogEventDetailScreen.routeName; + static const String logMeal = LogMealDetailScreen.routeName; + static const List logEntryRoutes = [logEntry, logEvent, logMeal]; + static const String logEventType = LogEventTypeDetailScreen.routeName; + static const String logEventTypes = LogEventTypeListScreen.routeName; + static const List logEventTypeRoutes = [logEventType, logEventTypes]; + + static const String meal = MealDetailScreen.routeName; + static const String meals = MealListScreen.routeName; + static const List mealRoutes = [meal, meals]; + static const String mealCategory = MealCategoryDetailScreen.routeName; + static const String mealCategories = MealCategoryListScreen.routeName; + static const List mealCategoryRoutes = [mealCategory, mealCategories]; + static const String mealPortionType = MealPortionTypeDetailScreen.routeName; + static const String mealPortionTypes = MealPortionTypeListScreen.routeName; + static const List mealPortionTypeRoutes = [ + mealPortionType, + mealPortionTypes + ]; + static const String mealSource = MealSourceDetailScreen.routeName; + static const String mealSources = MealSourceListScreen.routeName; + static const List mealSourceRoutes = [mealSource, mealSources]; + + static const String accuracy = AccuracyDetailScreen.routeName; + static const String accuracies = AccuracyListScreen.routeName; + static const List accuracyRoutes = [accuracy, accuracies]; + + static const String settings = SettingsScreen.routeName; +} + +class Navigation extends StatefulWidget { + final String? currentLocation; + const Navigation({Key? key, this.currentLocation}) : super(key: key); + + @override + State createState() => _NavigationState(); +} + +class _NavigationState extends State { + void selectDestination(String destination) { + Navigator.pushReplacementNamed(context, destination); + } + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView(padding: EdgeInsets.zero, children: [ + const SizedBox( + child: UserAccountsDrawerHeader( + accountName: Text('Sarah'), + accountEmail: Text('sarah@sudo.ca'), + ), + ), + ListTile( + title: const Text('Log'), + leading: const Icon(Icons.dashboard), + onTap: () { + selectDestination(Routes.log); + }, + selected: widget.currentLocation == Routes.log, + ), + ListTile( + title: const Text('Log Entry'), + leading: const Icon(Icons.description), + onTap: () { + selectDestination(Routes.logEntry); + }, + selected: Routes.logEntryRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Meals'), + leading: const Icon(Icons.restaurant), + onTap: () { + selectDestination(Routes.meals); + }, + selected: Routes.mealRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Meal Categories'), + leading: const Icon(Icons.category), + onTap: () { + selectDestination(Routes.mealCategories); + }, + selected: Routes.mealCategoryRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Meal Portion Types'), + leading: const Icon(Icons.pie_chart), + onTap: () { + selectDestination(Routes.mealPortionTypes); + }, + selected: Routes.mealPortionTypeRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Meal Sources'), + leading: const Icon(Icons.local_grocery_store), + onTap: () { + selectDestination(Routes.mealSources); + }, + selected: Routes.mealSourceRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Accuracies'), + leading: const Icon(Icons.architecture), + onTap: () { + selectDestination(Routes.accuracies); + }, + selected: Routes.accuracyRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Log Event Types'), + leading: const Icon(Icons.event), + onTap: () { + selectDestination(Routes.logEventTypes); + }, + selected: Routes.logEventTypeRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Basal Profiles'), + leading: const Icon(Icons.access_time), + onTap: () { + selectDestination(Routes.basalProfiles); + }, + selected: Routes.basalRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Bolus Profiles'), + leading: const Icon(Icons.medication), + onTap: () { + selectDestination(Routes.bolusProfiles); + }, + selected: Routes.bolusRoutes.contains(widget.currentLocation), + ), + ListTile( + title: const Text('Settings'), + leading: const Icon(Icons.settings), + onTap: () { + selectDestination(Routes.settings); + }, + selected: widget.currentLocation == Routes.settings, + ) + ])); + } +} diff --git a/lib/screens/accuracy_detail.dart b/lib/screens/accuracy_detail.dart new file mode 100644 index 0000000..370764f --- /dev/null +++ b/lib/screens/accuracy_detail.dart @@ -0,0 +1,161 @@ +import 'package:diameter/components/detail.dart'; +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/forms.dart'; +import 'package:diameter/models/accuracy.dart'; + +class AccuracyDetailScreen extends StatefulWidget { + static const String routeName = '/accuracy'; + final Accuracy? accuracy; + + const AccuracyDetailScreen({Key? key, this.accuracy}) : super(key: key); + + @override + _AccuracyDetailScreenState createState() => _AccuracyDetailScreenState(); +} + +class _AccuracyDetailScreenState extends State { + final GlobalKey _accuracyForm = GlobalKey(); + final _valueController = TextEditingController(text: ''); + final _confidenceRatingController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + bool _forCarbsRatio = false; + bool _forPortionSize = false; + + @override + void initState() { + super.initState(); + if (widget.accuracy != null) { + _valueController.text = widget.accuracy!.value; + _forCarbsRatio = widget.accuracy!.forCarbsRatio; + _forPortionSize = widget.accuracy!.forPortionSize; + _confidenceRatingController.text = + (widget.accuracy!.confidenceRating ?? '').toString(); + _notesController.text = widget.accuracy!.notes ?? ''; + } + } + + void handleSaveAction() async { + if (_accuracyForm.currentState!.validate()) { + bool isNew = widget.accuracy == null; + isNew + ? await Accuracy.save( + value: _valueController.text, + forCarbsRatio: _forCarbsRatio, + forPortionSize: _forPortionSize, + confidenceRating: int.tryParse(_confidenceRatingController.text), + notes: _notesController.text, + ) + : await Accuracy.update( + widget.accuracy!.objectId!, + value: _valueController.text, + forCarbsRatio: _forCarbsRatio, + forPortionSize: _forPortionSize, + confidenceRating: int.tryParse(_confidenceRatingController.text), + notes: _notesController.text, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Accuracy saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.accuracy == null; + + if (showConfirmationDialogOnCancel && + (isNew && + (_forCarbsRatio || + _forPortionSize || + _valueController.text != '' || + int.tryParse(_confidenceRatingController.text) != null || + _notesController.text != '')) || + (!isNew && + (_forCarbsRatio != widget.accuracy!.forCarbsRatio || + _forPortionSize != widget.accuracy!.forPortionSize || + widget.accuracy!.value != _valueController.text || + int.tryParse(_confidenceRatingController.text) != + widget.accuracy!.confidenceRating || + (widget.accuracy!.notes ?? '') != _notesController.text))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.accuracy == null; + return Scaffold( + appBar: AppBar( + title: Text(isNew ? 'New Accuracy' : widget.accuracy!.value), + ), + drawer: const Navigation(currentLocation: AccuracyDetailScreen.routeName), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _accuracyForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), + 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', + ), + ), + TextFormField( + controller: _notesController, + keyboardType: TextInputType.multiline, + decoration: const InputDecoration( + labelText: 'Notes', + alignLabelWithHint: true, + ), + ), + ], + ), + ], + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/accuracy_list.dart b/lib/screens/accuracy_list.dart new file mode 100644 index 0000000..b46917e --- /dev/null +++ b/lib/screens/accuracy_list.dart @@ -0,0 +1,185 @@ +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/progress_indicator.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/accuracy_detail.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/models/accuracy.dart'; + +class AccuracyListScreen extends StatefulWidget { + static const String routeName = '/accuracies'; + const AccuracyListScreen({Key? key}) : super(key: key); + + @override + _AccuracyListScreenState createState() => _AccuracyListScreenState(); +} + +class _AccuracyListScreenState extends State { + late Future?> _accuracies; + + void refresh({String? message}) { + setState(() { + _accuracies = Accuracy.fetchAll(); + }); + + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void onDelete(Accuracy accuracy) { + accuracy.delete().then((_) => refresh(message: 'Accuracy deleted')); + } + + void handleDeleteAction(Accuracy accuracy) async { + if (showConfirmationDialogOnDelete) { + Dialogs.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(accuracy), + message: 'Are you sure you want to delete this Accuracy?', + ); + } else { + onDelete(accuracy); + } + } + + void handleToggleForPortionSizeAction(Accuracy accuracy) async { + await Accuracy.update( + accuracy.objectId!, + forPortionSize: !accuracy.forPortionSize, + ); + refresh(); + } + + void handleToggleForCarbsRatioAction(Accuracy accuracy) async { + await Accuracy.update( + accuracy.objectId!, + forCarbsRatio: !accuracy.forCarbsRatio, + ); + refresh(); + } + + @override + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Accuracies'), + actions: [ + IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + ], + ), + drawer: const Navigation(currentLocation: AccuracyListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: FutureBuilder?>( + future: _accuracies, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Accuracies'), + ) + ], + ) + : ListView.builder( + padding: const EdgeInsets.only(top: 10.0), + itemCount: snapshot.data != null + ? snapshot.data!.length + : 0, + itemBuilder: (context, index) { + final accuracy = snapshot.data![index]; + return ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AccuracyDetailScreen( + accuracy: accuracy), + ), + ).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 + }, + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.square_foot, + color: accuracy.forPortionSize + ? Colors.blue + : Colors.grey.shade300), + onPressed: () => + handleToggleForPortionSizeAction( + accuracy)), + IconButton( + icon: Icon(Icons.pie_chart, + color: accuracy.forCarbsRatio + ? Colors.blue + : Colors.grey.shade300), + onPressed: () => + handleToggleForCarbsRatioAction( + accuracy), + ), + const SizedBox(width: 24), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => + handleDeleteAction(accuracy), + ) + ], + ), + ); + })); + }), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AccuracyDetailScreen(), + )).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/basal/basal_detail.dart b/lib/screens/basal/basal_detail.dart new file mode 100644 index 0000000..0ab2bab --- /dev/null +++ b/lib/screens/basal/basal_detail.dart @@ -0,0 +1,175 @@ +import 'package:diameter/components/detail.dart'; +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:diameter/components/forms.dart'; +import 'package:diameter/models/basal.dart'; +import 'package:diameter/models/basal_profile.dart'; + +class BasalDetailScreen extends StatefulWidget { + static const String routeName = '/basal'; + + final BasalProfile basalProfile; + final Basal? basal; + + const BasalDetailScreen({Key? key, required this.basalProfile, this.basal}) + : super(key: key); + + @override + _BasalDetailScreenState createState() => _BasalDetailScreenState(); +} + +class _BasalDetailScreenState extends State { + final GlobalKey _basalForm = GlobalKey(); + TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); + TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); + + final _startTimeController = TextEditingController(text: ''); + final _endTimeController = TextEditingController(text: ''); + final _unitsController = TextEditingController(text: ''); + + @override + void initState() { + super.initState(); + if (widget.basal != null) { + _startTime = TimeOfDay.fromDateTime(widget.basal!.startTime); + _endTime = TimeOfDay.fromDateTime(widget.basal!.endTime); + + _unitsController.text = widget.basal!.units.toString(); + } + updateStartTime(); + updateEndTime(); + } + + void updateStartTime() { + _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); + } + + void updateEndTime() { + _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); + } + + void handleSaveAction() async { + // TODO: add confirmation dialog in case time period is already covered + if (_basalForm.currentState!.validate()) { + bool isNew = widget.basal == null; + isNew + ? await Basal.save( + startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), + endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), + units: double.parse(_unitsController.text), + basalProfile: widget.basalProfile.objectId!, + ) + : await Basal.update( + widget.basal!.objectId!, + startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), + endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), + units: double.parse(_unitsController.text), + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Basal Rate saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.basal == null; + if (showConfirmationDialogOnCancel && + ((isNew && + (_startTime.hour != 0 || + _endTime.hour != 0 || + _startTime.minute != 0 || + _endTime.minute != 0 || + double.tryParse(_unitsController.text) != null)) || + (!isNew && + (TimeOfDay.fromDateTime(widget.basal!.startTime) != + _startTime || + TimeOfDay.fromDateTime(widget.basal!.endTime) != _endTime || + (double.tryParse(_unitsController.text) ?? 0) != + widget.basal!.units)))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.basal == null; + + return Scaffold( + appBar: AppBar( + title: Text( + '${isNew ? 'New' : 'Edit'} Basal Rate for ${widget.basalProfile.name}'), + ), + drawer: const Navigation(currentLocation: BasalDetailScreen.routeName), + body: Column( + children: [ + StyledForm( + formState: _basalForm, + fields: [ + Row( + children: [ + Expanded( + child: StyledTimeOfDayFormField( + label: 'Start Time', + controller: _startTimeController, + time: _startTime, + onChanged: (newStartTime) { + if (newStartTime != null) { + setState(() { + _startTime = newStartTime; + }); + updateStartTime(); + } + }, + ), + ), + Expanded( + child: StyledTimeOfDayFormField( + label: 'End Time', + controller: _endTimeController, + time: _endTime, + onChanged: (newEndTime) { + if (newEndTime != null) { + setState(() { + _endTime = newEndTime; + }); + updateEndTime(); + } + }, + ), + ), + ], + ), + TextFormField( + controller: _unitsController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + labelText: 'Units', + suffixText: 'U', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty amount of units'; + } + return null; + }, + ), + ], + ), + ], + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/basal/basal_list.dart b/lib/screens/basal/basal_list.dart new file mode 100644 index 0000000..14c014d --- /dev/null +++ b/lib/screens/basal/basal_list.dart @@ -0,0 +1,125 @@ +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/config.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/components/progress_indicator.dart'; +import 'package:diameter/models/basal.dart'; +import 'package:diameter/models/basal_profile.dart'; +import 'package:diameter/screens/basal/basal_detail.dart'; + +class BasalListScreen extends StatefulWidget { + final BasalProfile? basalProfile; + + const BasalListScreen({Key? key, this.basalProfile}) : super(key: key); + + @override + _BasalListScreenState createState() => _BasalListScreenState(); +} + +class _BasalListScreenState extends State { + void refresh({String? message}) { + setState(() { + if (widget.basalProfile != null) { + widget.basalProfile!.basalRates = + Basal.fetchAllForBasalProfile(widget.basalProfile!); + } + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void handleEditAction(Basal basal) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BasalDetailScreen( + basalProfile: widget.basalProfile!, + basal: basal, + ), + ), + ).then((message) => refresh(message: message)); + } + + void onDelete(Basal basal) { + basal.delete().then((_) => refresh(message: 'Basal Rate deleted')); + } + + void handleDeleteAction(Basal basal) async { + if (showConfirmationDialogOnDelete) { + Dialogs.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(basal), + message: 'Are you sure you want to delete this Basal Rate?', + ); + } else { + onDelete(basal); + } + } + + @override + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.only(top: 10.0), + child: Column( + children: [ + FutureBuilder>( + future: widget.basalProfile!.basalRates, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + // TODO: add warning if time period is missing or has multiple rates + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? const Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Basal Rates for this Profile'), + ) + : ListBody( + children: [ + DataTable( + columnSpacing: 10.0, + showCheckboxColumn: false, + rows: snapshot.data != null + ? snapshot.data!.map((basal) { + return DataRow( + cells: basal.asDataTableCells([ + IconButton( + icon: const Icon(Icons.edit), + iconSize: 16.0, + onPressed: () => + handleEditAction(basal)), + IconButton( + icon: const Icon(Icons.delete), + iconSize: 16.0, + onPressed: () => + handleDeleteAction(basal), + ), + ]), + ); + }).toList() + : [], + columns: Basal.asDataTableColumns(), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/basal/basal_profile_detail.dart b/lib/screens/basal/basal_profile_detail.dart new file mode 100644 index 0000000..eab97f1 --- /dev/null +++ b/lib/screens/basal/basal_profile_detail.dart @@ -0,0 +1,244 @@ +import 'package:diameter/components/detail.dart'; +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/models/basal.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/basal/basal_detail.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/components/forms.dart'; +import 'package:diameter/models/basal_profile.dart'; +import 'package:diameter/screens/basal/basal_list.dart'; + +class BasalProfileDetailScreen extends StatefulWidget { + static const String routeName = '/basal-profile'; + + final BasalProfile? basalProfile; + final bool active; + + const BasalProfileDetailScreen( + {Key? key, this.active = false, this.basalProfile}) + : super(key: key); + + @override + _BasalProfileDetailScreenState createState() => + _BasalProfileDetailScreenState(); +} + +class _BasalProfileDetailScreenState extends State { + final GlobalKey _basalProfileForm = GlobalKey(); + + late FloatingActionButton addBasalButton; + late IconButton refreshButton; + late IconButton closeButton; + + final _nameController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + bool _active = false; + + @override + void initState() { + super.initState(); + if (widget.basalProfile != null) { + _nameController.text = widget.basalProfile!.name; + _active = widget.basalProfile!.active; + _notesController.text = widget.basalProfile!.notes ?? ''; + } + if (widget.active) { + _active = true; + } + + addBasalButton = FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return BasalDetailScreen(basalProfile: widget.basalProfile!); + }, + ), + ).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ); + + refreshButton = IconButton( + icon: const Icon(Icons.refresh), + onPressed: refresh, + ); + + closeButton = IconButton( + onPressed: handleCancelAction, + icon: const Icon(Icons.close), + ); + + actionButton = null; + appBarActions = [closeButton]; + } + + void refresh({String? message}) { + setState(() { + if (widget.basalProfile != null) { + widget.basalProfile!.basalRates = + Basal.fetchAllForBasalProfile(widget.basalProfile!); + } + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void handleSaveAction() async { + // TODO: if this would be the second active profile, prompt for deactivating that one + if (_basalProfileForm.currentState!.validate()) { + bool isNew = widget.basalProfile == null; + isNew + ? await BasalProfile.save( + name: _nameController.text, + active: _active, + notes: _notesController.text) + : await BasalProfile.update( + widget.basalProfile!.objectId!, + name: _nameController.text, + active: _active, + notes: _notesController.text, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Basal Profile saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.basalProfile == null; + + if (showConfirmationDialogOnCancel && + (isNew && + (_active || + _nameController.text != '' || + _notesController.text != '')) || + (!isNew && + (widget.basalProfile!.active != _active || + widget.basalProfile!.name != _nameController.text || + (widget.basalProfile!.notes ?? '') != _notesController.text))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + FloatingActionButton? actionButton; + List appBarActions = []; + + void renderTabButtons(index) { + if (widget.basalProfile != null) { + setState(() { + switch (index) { + case 1: + actionButton = addBasalButton; + appBarActions = [refreshButton, closeButton]; + break; + default: + actionButton = null; + appBarActions = [closeButton]; + } + }); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.basalProfile == null; + return DefaultTabController( + length: 2, + child: Builder(builder: (BuildContext context) { + final TabController tabController = DefaultTabController.of(context)!; + tabController.addListener(() { + if (tabController.indexIsChanging) { + renderTabButtons(tabController.index); + } + }); + return Scaffold( + appBar: AppBar( + title: + Text(isNew ? 'New Basal Profile' : widget.basalProfile!.name), + bottom: isNew + ? PreferredSize(child: Container(), preferredSize: Size.zero) + : const TabBar( + tabs: [ + Tab(text: 'PROFILE'), + Tab(text: 'RATES'), + ], + ), + actions: appBarActions, + ), + drawer: const Navigation( + currentLocation: BasalProfileDetailScreen.routeName), + body: TabBarView( + children: [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _basalProfileForm, + fields: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty title'; + } + return null; + }, + ), + TextFormField( + keyboardType: TextInputType.multiline, + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + suffixText: '', + alignLabelWithHint: true, + ), + ), + StyledBooleanFormField( + value: _active, + onChanged: (value) { + setState(() { + _active = value; + }); + }, + label: 'active', + ), + ], + ), + ], + ), + ), + BasalListScreen(basalProfile: widget.basalProfile), + ], + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + floatingActionButton: actionButton, + floatingActionButtonLocation: + FloatingActionButtonLocation.centerDocked, + ); + }), + ); + } +} diff --git a/lib/screens/basal/basal_profiles_list.dart b/lib/screens/basal/basal_profiles_list.dart new file mode 100644 index 0000000..25315dc --- /dev/null +++ b/lib/screens/basal/basal_profiles_list.dart @@ -0,0 +1,218 @@ +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 { + late Future?> _basalProfiles; + Widget banner = Container(); + bool pickActiveProfileMode = false; + + void refresh({String? message}) { + setState(() { + pickActiveProfileMode = false; + _basalProfiles = BasalProfile.fetchAll(); + }); + _basalProfiles.then((list) => updateBanner( + list?.where((element) => element.active).length ?? 0, + list?.isNotEmpty ?? false)); + 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, bool isNotEmpty) { + 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 + ? [ + 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 + .delete() + .then((_) => 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.setAllInactiveButOne(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( + basalProfile: basalProfile, 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: [ + IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + ], + ), + drawer: + const Navigation(currentLocation: BasalProfileListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + banner, + Expanded( + child: FutureBuilder?>( + future: _basalProfiles, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Basal Profiles'), + ), + ]) + : ListView.builder( + itemCount: + snapshot.data != null ? snapshot.data!.length : 0, + itemBuilder: (context, index) { + final basalProfile = snapshot.data![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), + ), + ], + ), + ); + }, + ), + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => onNew(false), + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/bolus/bolus_detail.dart b/lib/screens/bolus/bolus_detail.dart new file mode 100644 index 0000000..ed70a5b --- /dev/null +++ b/lib/screens/bolus/bolus_detail.dart @@ -0,0 +1,306 @@ +import 'package:diameter/components/detail.dart'; +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/settings.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:diameter/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:diameter/components/forms.dart'; +import 'package:diameter/models/bolus.dart'; +import 'package:diameter/models/bolus_profile.dart'; +import 'package:flutter/widgets.dart'; + +class BolusDetailScreen extends StatefulWidget { + static const String routeName = '/bolus'; + + final BolusProfile bolusProfile; + final Bolus? bolus; + + const BolusDetailScreen({Key? key, required this.bolusProfile, this.bolus}) + : super(key: key); + + @override + _BolusDetailScreenState createState() => _BolusDetailScreenState(); +} + +class _BolusDetailScreenState extends State { + final GlobalKey _bolusForm = GlobalKey(); + + TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); + TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); + + final _startTimeController = TextEditingController(); + final _endTimeController = TextEditingController(); + final _unitsController = TextEditingController(); + final _carbsController = TextEditingController(); + final _mgPerDlController = TextEditingController(); + final _mmolPerLController = TextEditingController(); + + @override + void initState() { + super.initState(); + if (widget.bolus != null) { + _startTime = TimeOfDay.fromDateTime(widget.bolus!.startTime); + _endTime = TimeOfDay.fromDateTime(widget.bolus!.endTime); + + _unitsController.text = widget.bolus!.units.toString(); + _carbsController.text = widget.bolus!.carbs.toString(); + _mgPerDlController.text = widget.bolus!.mgPerDl.toString(); + _mmolPerLController.text = widget.bolus!.mmolPerL.toString(); + } + updateStartTime(); + updateEndTime(); + } + + void updateStartTime() { + _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); + } + + void updateEndTime() { + _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); + } + + void updateMgPerDl() { + _mgPerDlController.text = Utils.convertMmolPerLToMgPerDl( + double.tryParse(_mmolPerLController.text) ?? 0) + .toString(); + } + + void updateMmolPerL() { + _mmolPerLController.text = Utils.convertMgPerDlToMmolPerL( + int.tryParse(_mgPerDlController.text) ?? 0) + .toString(); + } + + void handleSaveAction() async { + // TODO: add confirmation dialog in case time period is already covered + if (_bolusForm.currentState!.validate()) { + bool isNew = widget.bolus == null; + isNew + ? await Bolus.save( + startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), + endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), + units: double.parse(_unitsController.text), + bolusProfile: widget.bolusProfile.objectId!, + carbs: double.parse(_carbsController.text), + mgPerDl: int.tryParse(_mgPerDlController.text), + mmolPerL: double.tryParse(_mmolPerLController.text), + ) + : await Bolus.update( + widget.bolus!.objectId!, + startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), + endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), + units: double.tryParse(_unitsController.text), + carbs: double.tryParse(_carbsController.text), + mgPerDl: int.tryParse(_mgPerDlController.text), + mmolPerL: double.parse(_mmolPerLController.text), + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Bolus Rate saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.bolus == null; + if (showConfirmationDialogOnCancel && + ((isNew && + (_startTime.hour != 0 || + _endTime.hour != 0 || + _startTime.minute != 0 || + _endTime.minute != 0 || + (double.tryParse(_unitsController.text) ?? 0) != 0.0 || + (double.tryParse(_carbsController.text) ?? 0) != 0.0 || + (int.tryParse(_mgPerDlController.text) ?? 0) != 0 || + (double.tryParse(_mmolPerLController.text) ?? 0) != 0.0)) || + (!isNew && + (TimeOfDay.fromDateTime(widget.bolus!.startTime) != + _startTime || + TimeOfDay.fromDateTime(widget.bolus!.endTime) != _endTime || + (double.tryParse(_unitsController.text) ?? 0) != + widget.bolus!.units || + (double.tryParse(_carbsController.text) ?? 0) != + widget.bolus!.carbs || + (double.tryParse(_mgPerDlController.text) ?? 0) != + widget.bolus!.mgPerDl || + (double.tryParse(_mmolPerLController.text) ?? 0) != + widget.bolus!.mmolPerL)))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.bolus == null; + + return Scaffold( + appBar: AppBar( + title: Text( + '${isNew ? 'New' : 'Edit'} Bolus Rate for ${widget.bolusProfile.name}'), + ), + drawer: const Navigation(currentLocation: BolusDetailScreen.routeName), + body: SingleChildScrollView( + child: Column( + children: [ + StyledForm( + formState: _bolusForm, + fields: [ + Row( + children: [ + Expanded( + child: StyledTimeOfDayFormField( + label: 'Start Time', + controller: _startTimeController, + time: _startTime, + onChanged: (newStartTime) { + if (newStartTime != null) { + setState(() { + _startTime = newStartTime; + }); + updateStartTime(); + } + }, + ), + ), + Expanded( + child: StyledTimeOfDayFormField( + label: 'End Time', + controller: _endTimeController, + time: _endTime, + onChanged: (newEndTime) { + if (newEndTime != null) { + setState(() { + _endTime = newEndTime; + }); + updateEndTime(); + } + }, + ), + ), + ], + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Units', + suffixText: 'U', + ), + controller: _unitsController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty amount of units'; + } + return null; + }, + ), + TextFormField( + decoration: InputDecoration( + labelText: 'per carbs', + suffixText: nutritionMeasurement == + NutritionMeasurement.grams + ? 'g' + : nutritionMeasurement == NutritionMeasurement.ounces + ? 'oz' + : '', + ), + controller: _carbsController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + validator: (value) { + if (value!.trim().isEmpty) { + return 'How many carbs does the rate make up for?'; + } + return null; + }, + ), + Row( + // TODO: improve conversion of mg/dl and mmol/l + children: [ + glucoseMeasurement == GlucoseMeasurement.mgPerDl || + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'per mg/dl', + suffixText: 'mg/dl', + ), + controller: _mgPerDlController, + onChanged: (_) { + setState(() { + _mmolPerLController + .text = Utils.convertMgPerDlToMmolPerL( + int.tryParse( + _mmolPerLController.text) ?? + 0) + .toString(); + }); + }, + keyboardType: + const TextInputType.numberWithOptions(), + validator: (value) { + if (value!.trim().isEmpty && + _mmolPerLController.text.trim().isEmpty) { + return 'How many mg/dl does the rate make up for?'; + } + return null; + }, + ), + ) + : Container(), + glucoseMeasurement == GlucoseMeasurement.mmolPerL || + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'per mmol/l', + suffixText: 'mmol/l', + ), + controller: _mmolPerLController, + onChanged: (_) { + setState(() { + _mgPerDlController + .text = Utils.convertMmolPerLToMgPerDl( + double.tryParse( + _mgPerDlController.text) ?? + 0) + .toString(); + }); + }, + 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(), + ], + ), + ], + ), + ], + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/bolus/bolus_list.dart b/lib/screens/bolus/bolus_list.dart new file mode 100644 index 0000000..854825d --- /dev/null +++ b/lib/screens/bolus/bolus_list.dart @@ -0,0 +1,126 @@ +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/config.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/components/progress_indicator.dart'; +import 'package:diameter/models/bolus.dart'; +import 'package:diameter/models/bolus_profile.dart'; +import 'package:diameter/screens/bolus/bolus_detail.dart'; + +class BolusListScreen extends StatefulWidget { + final BolusProfile? bolusProfile; + + const BolusListScreen({Key? key, this.bolusProfile}) : super(key: key); + + @override + _BolusListScreenState createState() => _BolusListScreenState(); +} + +class _BolusListScreenState extends State { + void refresh({String? message}) { + setState(() { + if (widget.bolusProfile != null) { + widget.bolusProfile!.bolusRates = + Bolus.fetchAllForBolusProfile(widget.bolusProfile!); + } + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void handleEditAction(Bolus bolus) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BolusDetailScreen( + bolusProfile: widget.bolusProfile!, + bolus: bolus, + ), + ), + ).then((message) => refresh(message: message)); + } + + void onDelete(Bolus bolus) { + bolus.delete().then((_) => refresh(message: 'Bolus Rate deleted')); + } + + void handleDeleteAction(Bolus bolus) async { + if (showConfirmationDialogOnDelete) { + Dialogs.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(bolus), + message: 'Are you sure you want to delete this Bolus Rate?', + ); + } else { + onDelete(bolus); + } + } + + @override + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.only(top: 10.0), + child: Column( + children: [ + FutureBuilder>( + future: widget.bolusProfile!.bolusRates, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + // TODO: add warning if time period is missing or has multiple rates + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? const Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Bolus Rates for this Profile'), + ) + : ListBody( + children: [ + DataTable( + columnSpacing: 10.0, + showCheckboxColumn: false, + rows: snapshot.data != null + ? snapshot.data!.map((bolus) { + return DataRow( + cells: bolus.asDataTableCells( + [ + IconButton( + icon: const Icon(Icons.edit), + iconSize: 16.0, + onPressed: () => + handleEditAction(bolus)), + IconButton( + icon: const Icon(Icons.delete), + iconSize: 16.0, + onPressed: () => + handleDeleteAction(bolus)), + ], + ), + ); + }).toList() + : [], + columns: Bolus.asDataTableColumns(), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/bolus/bolus_profile_detail.dart b/lib/screens/bolus/bolus_profile_detail.dart new file mode 100644 index 0000000..bea7225 --- /dev/null +++ b/lib/screens/bolus/bolus_profile_detail.dart @@ -0,0 +1,244 @@ +import 'package:diameter/components/detail.dart'; +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/models/bolus.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/bolus/bolus_detail.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/components/forms.dart'; +import 'package:diameter/models/bolus_profile.dart'; +import 'package:diameter/screens/bolus/bolus_list.dart'; +import 'package:flutter/widgets.dart'; + +class BolusProfileDetailScreen extends StatefulWidget { + static const String routeName = '/bolus-profile'; + + final BolusProfile? bolusProfile; + final bool active; + + const BolusProfileDetailScreen( + {Key? key, this.active = false, this.bolusProfile}) + : super(key: key); + + @override + _BolusProfileDetailScreenState createState() => + _BolusProfileDetailScreenState(); +} + +class _BolusProfileDetailScreenState extends State { + final GlobalKey _bolusProfileForm = GlobalKey(); + + late FloatingActionButton addBolusButton; + late IconButton refreshButton; + late IconButton closeButton; + + final _nameController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + bool _active = false; + + @override + void initState() { + super.initState(); + if (widget.bolusProfile != null) { + _nameController.text = widget.bolusProfile!.name; + _active = widget.bolusProfile!.active; + _notesController.text = widget.bolusProfile!.notes ?? ''; + } + if (widget.active) { + _active = true; + } + + addBolusButton = FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return BolusDetailScreen(bolusProfile: widget.bolusProfile!); + }, + ), + ).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ); + + refreshButton = IconButton( + icon: const Icon(Icons.refresh), + onPressed: refresh, + ); + + closeButton = IconButton( + onPressed: handleCancelAction, + icon: const Icon(Icons.close), + ); + + actionButton = null; + appBarActions = [closeButton]; + } + + void refresh({String? message}) { + setState(() { + if (widget.bolusProfile != null) { + widget.bolusProfile!.bolusRates = + Bolus.fetchAllForBolusProfile(widget.bolusProfile!); + } + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void handleSaveAction() async { + // TODO: if this would be the second active profile, prompt for deactivating that one + if (_bolusProfileForm.currentState!.validate()) { + bool isNew = widget.bolusProfile == null; + isNew + ? await BolusProfile.save( + name: _nameController.text, + active: _active, + notes: _notesController.text) + : await BolusProfile.update( + widget.bolusProfile!.objectId!, + name: _nameController.text, + active: _active, + notes: _notesController.text, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Bolus Profile saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.bolusProfile == null; + + if (showConfirmationDialogOnCancel && + (isNew && + (_active || + _nameController.text != '' || + _notesController.text != '')) || + (!isNew && + (widget.bolusProfile!.active != _active || + widget.bolusProfile!.name != _nameController.text || + (widget.bolusProfile!.notes ?? '') != _notesController.text))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + FloatingActionButton? actionButton; + List appBarActions = []; + + void renderTabButtons(index) { + if (widget.bolusProfile != null) { + setState(() { + switch (index) { + case 1: + actionButton = addBolusButton; + appBarActions = [refreshButton, closeButton]; + break; + default: + actionButton = null; + appBarActions = [closeButton]; + } + }); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.bolusProfile == null; + return DefaultTabController( + length: 2, + child: Builder(builder: (BuildContext context) { + final TabController tabController = DefaultTabController.of(context)!; + tabController.addListener(() { + if (tabController.indexIsChanging) { + renderTabButtons(tabController.index); + } + }); + return Scaffold( + appBar: AppBar( + title: + Text(isNew ? 'New Bolus Profile' : widget.bolusProfile!.name), + bottom: isNew + ? PreferredSize(child: Container(), preferredSize: Size.zero) + : const TabBar( + tabs: [ + Tab(text: 'PROFILE'), + Tab(text: 'RATES'), + ], + ), + actions: appBarActions, + ), + drawer: const Navigation( + currentLocation: BolusProfileDetailScreen.routeName), + body: TabBarView( + children: [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _bolusProfileForm, + fields: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty title'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Notes', + alignLabelWithHint: true, + ), + controller: _notesController, + keyboardType: TextInputType.multiline, + ), + StyledBooleanFormField( + value: _active, + onChanged: (value) { + setState(() { + _active = value; + }); + }, + label: 'active', + ), + ], + ), + ], + ), + ), + BolusListScreen(bolusProfile: widget.bolusProfile), + ], + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + floatingActionButton: actionButton, + floatingActionButtonLocation: + FloatingActionButtonLocation.centerDocked, + ); + }), + ); + } +} diff --git a/lib/screens/bolus/bolus_profile_list.dart b/lib/screens/bolus/bolus_profile_list.dart new file mode 100644 index 0000000..629b5d3 --- /dev/null +++ b/lib/screens/bolus/bolus_profile_list.dart @@ -0,0 +1,218 @@ +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/bolus_profile.dart'; +import 'package:diameter/screens/bolus/bolus_profile_detail.dart'; + +class BolusProfileListScreen extends StatefulWidget { + static const String routeName = '/bolus-profiles'; + const BolusProfileListScreen({Key? key}) : super(key: key); + + @override + _BolusProfileListScreenState createState() => _BolusProfileListScreenState(); +} + +class _BolusProfileListScreenState extends State { + late Future?> _bolusProfiles; + Widget banner = Container(); + bool pickActiveProfileMode = false; + + void refresh({String? message}) { + setState(() { + pickActiveProfileMode = false; + _bolusProfiles = BolusProfile.fetchAll(); + }); + _bolusProfiles.then((list) => updateBanner( + list?.where((element) => element.active).length ?? 0, + list?.isNotEmpty ?? false)); + 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, bool isNotEmpty) { + setState(() { + banner = activeProfileCount != 1 + ? MaterialBanner( + content: Text(activeProfileCount == 0 + ? 'You currently do not have an active Bolus Profile.' + : 'More than one active Bolus Profile has been found.'), + leading: const CircleAvatar(child: Icon(Icons.warning)), + forceActionsBelow: true, + actions: activeProfileCount == 0 + ? [ + 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(BolusProfile bolusProfile) { + bolusProfile + .delete() + .then((_) => refresh(message: 'Bolus Profile deleted')); + } + + void handleDeleteAction(BolusProfile bolusProfile) async { + if (showConfirmationDialogOnDelete) { + Dialogs.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(bolusProfile), + message: 'Are you sure you want to delete this Bolus Profile?', + ); + } else { + onDelete(bolusProfile); + } + } + + void onPickActive(BolusProfile bolusProfile) { + BolusProfile.setAllInactiveButOne(bolusProfile.objectId!).then((_) => + refresh( + message: + '${bolusProfile.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({BolusProfile? bolusProfile, bool active = false}) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BolusProfileDetailScreen( + bolusProfile: bolusProfile, active: active), + ), + ).then((message) => refresh(message: message)); + } + + void onNew(bool active) { + showDetailScreen(active: active); + } + + void onEdit(BolusProfile bolusProfile) { + showDetailScreen(bolusProfile: bolusProfile); + } + + @override + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Bolus Profiles'), + actions: [ + IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + ], + ), + drawer: + const Navigation(currentLocation: BolusProfileListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + banner, + Expanded( + child: FutureBuilder?>( + future: _bolusProfiles, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Bolus Profiles'), + ), + ]) + : ListView.builder( + itemCount: + snapshot.data != null ? snapshot.data!.length : 0, + itemBuilder: (context, index) { + final bolusProfile = snapshot.data![index]; + return ListTile( + tileColor: bolusProfile.active + ? Colors.green.shade100 + : null, + onTap: () { + pickActiveProfileMode + ? onPickActive(bolusProfile) + : onEdit(bolusProfile); + }, + title: Text( + bolusProfile.name, + ), + subtitle: Text(bolusProfile.notes!), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => + handleDeleteAction(bolusProfile), + ), + ], + ), + ); + }, + ), + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => onNew(false), + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/log/active_log_event_list.dart b/lib/screens/log/active_log_event_list.dart new file mode 100644 index 0000000..8f69c85 --- /dev/null +++ b/lib/screens/log/active_log_event_list.dart @@ -0,0 +1,188 @@ +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: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 { + late Future> _activeLogEvents; + late Future> _logEventTypes; + + void refresh({String? message}) { + setState(() { + _logEventTypes = LogEventType.fetchAll(); + }); + + setState(() { + _activeLogEvents = LogEvent.fetchAllForLogEntry(widget.endLogEntry!); + }); + + 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 { + // TODO: create new entry if no existing entry is given + await LogEvent.update( + event.objectId!, + endTime: DateTime.now(), + endLogEntry: widget.endLogEntry!.objectId!, + ); + 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) { + event.delete().then((_) => 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) { + bool isNew = widget.endLogEntry == null; + return isNew + ? Container() + : Container( + 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), + ], + ), + FutureBuilder>( + future: _activeLogEvents, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? const Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Active Events'), + ) + : ListBody( + children: [ + // TODO: fix problems that this futurebuilder in futurebuilder creates + FutureBuilder>( + future: _logEventTypes, + builder: (context, types) { + return DataTable( + columnSpacing: 10.0, + showCheckboxColumn: false, + rows: snapshot.data != null + ? snapshot.data!.map((event) { + return DataRow( + cells: event.asDataTableCells( + [ + IconButton( + icon: const Icon( + Icons.stop), + iconSize: 16.0, + onPressed: () => + handleStopAction( + event), + ), + IconButton( + icon: const Icon( + Icons.delete), + iconSize: 16.0, + onPressed: () => + handleDeleteAction( + event), + ), + ], + types: types.data, + ), + ); + }).toList() + : [], + columns: LogEvent.asDataTableColumns(), + ); + }) + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/log/log.dart b/lib/screens/log/log.dart new file mode 100644 index 0000000..81316cc --- /dev/null +++ b/lib/screens/log/log.dart @@ -0,0 +1,147 @@ +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/progress_indicator.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.dart'; +import 'package:diameter/utils/date_time_utils.dart'; +import 'package:flutter/material.dart'; + +class LogScreen extends StatefulWidget { + static const String routeName = '/log'; + const LogScreen({Key? key}) : super(key: key); + + @override + _LogScreenState createState() => _LogScreenState(); +} + +class _LogScreenState extends State { + late Future?> _logEntries; + + void refresh({String? message}) { + setState(() { + _logEntries = LogEntry.fetchAll(); + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void onDelete(LogEntry logEntry) { + logEntry.delete().then((_) => refresh(message: 'Log Entry deleted')); + } + + void handleDeleteAction(LogEntry logEntry) async { + if (showConfirmationDialogOnDelete) { + Dialogs.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(logEntry), + message: 'Are you sure you want to delete this Log Entry?', + ); + } else { + onDelete(logEntry); + } + } + + @override + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Log Entries'), actions: [ + IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + ]), + drawer: const Navigation(currentLocation: LogScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: FutureBuilder?>( + future: _logEntries, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Log Entries'), + ), + ], + ) + : ListView.builder( + padding: const EdgeInsets.all(10.0), + itemCount: + snapshot.data != null ? snapshot.data!.length : 0, + itemBuilder: (context, index) { + final logEntry = snapshot.data![index]; + // TODO: split entries by day + return ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + LogEntryScreen(entry: logEntry), + ), + ).then((message) => refresh(message: message)); + }, + title: + // TODO: only display time here + Text(DateTimeUtils.displayDateTime( + logEntry.time)), + // TODO: display according to settings + // 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), + ) + ], + ), + ); + }, + ), + ); + }, + ), + ), + ], + ), + // TODO: add button for active events + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LogEntryScreen(), + ), + ).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/log/log_entry.dart b/lib/screens/log/log_entry.dart new file mode 100644 index 0000000..7e86dbe --- /dev/null +++ b/lib/screens/log/log_entry.dart @@ -0,0 +1,262 @@ +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/models/log_event.dart'; +import 'package:diameter/models/log_meal.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 LogEntry? entry; + const LogEntryScreen({Key? key, this.entry}) : super(key: key); + + @override + _LogEntryScreenState createState() => _LogEntryScreenState(); +} + +class _LogEntryScreenState extends State { + final GlobalKey logEntryForm = GlobalKey(); + + late FloatingActionButton addMealButton; + late FloatingActionButton addEventButton; + late IconButton refreshButton; + late IconButton closeButton; + + static FloatingActionButton? actionButton; + static List appBarActions = []; + + final formDataControllers = { + 'time': TextEditingController(text: ''), + 'mgPerDl': TextEditingController(text: ''), + 'mmolPerL': TextEditingController(text: ''), + 'bolusGlucose': TextEditingController(text: ''), + 'delayedBolusRate': TextEditingController(text: ''), + 'delayedBolusDuration': TextEditingController(text: ''), + 'notes': TextEditingController(text: ''), + }; + + void refreshLists({String? message}) { + if (widget.entry != null) { + setState(() { + widget.entry!.meals = LogMeal.fetchAllForLogEntry(widget.entry!); + widget.entry!.events = LogEvent.fetchAllForLogEntry(widget.entry!); + widget.entry!.endedEvents = + LogEvent.fetchAllEndedByEntry(widget.entry!); + }); + } + } + + void handleSaveAction() async { + if (logEntryForm.currentState!.validate()) { + bool isNew = widget.entry == null; + isNew + ? await LogEntry.save( + time: DateTime.parse(formDataControllers['time']!.text), + mgPerDl: int.tryParse(formDataControllers['mgPerDl']!.text), + mmolPerL: double.tryParse(formDataControllers['mmolPerL']!.text), + bolusGlucose: + double.tryParse(formDataControllers['bolusGlucose']!.text), + delayedBolusDuration: int.tryParse( + formDataControllers['delayedBolusDuration']!.text), + delayedBolusRatio: double.tryParse( + formDataControllers['delayedBolusRate']!.text), + notes: formDataControllers['notes']!.text, + ) + : await LogEntry.update( + widget.entry!.objectId!, + 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), + delayedBolusRatio: double.tryParse( + formDataControllers['delayedBolusRate']!.text), + notes: formDataControllers['notes']!.text, + ); + Navigator.pushReplacementNamed(context, '/log', + arguments: '${isNew ? 'New' : ''} Log Entry Saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.entry == null; + 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 ?? '') != + widget.entry!.mgPerDl || + double.tryParse(formDataControllers['mmolPerL']?.text ?? '') != + widget.entry!.mmolPerL || + double.tryParse(formDataControllers['bolusGlucose']?.text ?? '') != + widget.entry!.bolusGlucose || + int.tryParse( + formDataControllers['delayedBolusDuration']?.text ?? + '') != + widget.entry!.delayedBolusDuration || + double.tryParse(formDataControllers['delayedBolusRate']?.text ?? '') != + widget.entry!.delayedBolusRatio || + formDataControllers['notes']?.text != + (widget.entry!.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'); + } + } + + @override + void initState() { + super.initState(); + + if (widget.entry != null) { + formDataControllers['time']!.text = widget.entry!.time.toString(); + formDataControllers['mgPerDl']!.text = + (widget.entry!.mgPerDl ?? '').toString(); + formDataControllers['mmolPerL']!.text = + (widget.entry!.mmolPerL ?? '').toString(); + formDataControllers['bolusGlucose']!.text = + (widget.entry!.bolusGlucose ?? '').toString(); + formDataControllers['delayedBolusRate']!.text = + (widget.entry!.delayedBolusRatio ?? '').toString(); + formDataControllers['delayedBolusDuration']!.text = + (widget.entry!.delayedBolusDuration ?? '').toString(); + formDataControllers['notes']!.text = widget.entry!.notes ?? ''; + } else { + formDataControllers['time']!.text = DateTime.now().toString(); + } + + addMealButton = FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return LogMealDetailScreen(logEntry: widget.entry!); + }, + ), + ).then((message) => refreshLists(message: message)); + }, + child: const Icon(Icons.add), + ); + + addEventButton = FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return LogEventDetailScreen(logEntry: widget.entry!); + }, + ), + ).then((message) => refreshLists(message: message)); + }, + child: const Icon(Icons.add), + ); + + refreshButton = IconButton( + icon: const Icon(Icons.refresh), + onPressed: refreshLists, + ); + + closeButton = IconButton( + onPressed: handleCancelAction, + icon: const Icon(Icons.close), + ); + + actionButton = null; + appBarActions = [closeButton]; + } + + void renderTabButtons(index) { + if (widget.entry != null) { + setState(() { + switch (index) { + case 1: + actionButton = addMealButton; + appBarActions = [refreshButton, closeButton]; + break; + case 2: + actionButton = addEventButton; + appBarActions = [refreshButton, closeButton]; + break; + default: + actionButton = null; + appBarActions = [closeButton]; + } + }); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.entry == null; + + return DefaultTabController( + length: 3, + child: Builder(builder: (BuildContext context) { + final TabController tabController = DefaultTabController.of(context)!; + tabController.addListener(() { + if (tabController.indexIsChanging) { + renderTabButtons(tabController.index); + } + }); + 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: [ + LogEntryForm( + formState: logEntryForm, controllers: formDataControllers), + LogMealListScreen(logEntry: widget.entry), + LogEventListScreen(logEntry: widget.entry), + ], + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + floatingActionButton: actionButton, + floatingActionButtonLocation: + FloatingActionButtonLocation.centerDocked, + ); + }), + ); + } +} diff --git a/lib/screens/log/log_entry_form.dart b/lib/screens/log/log_entry_form.dart new file mode 100644 index 0000000..42a2bc9 --- /dev/null +++ b/lib/screens/log/log_entry_form.dart @@ -0,0 +1,159 @@ +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; + final Map controllers; + + const LogEntryForm( + {Key? key, required this.formState, required this.controllers}) + : super(key: key); + + @override + _LogEntryFormState createState() => _LogEntryFormState(); +} + +class _LogEntryFormState extends State { + @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( + // TODO: improve conversion of mg/dl and mmol/l + // TODO: display according to settings + children: [ + glucoseMeasurement == GlucoseMeasurement.mgPerDl || + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForDetail + ? Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'mg/dl', + suffixText: 'mg/dl', + ), + controller: _mgPerDlController, + 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(), + glucoseMeasurement == GlucoseMeasurement.mmolPerL || + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForDetail + ? Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'mmol/l', + suffixText: 'mmol/l', + alignLabelWithHint: true, + ), + controller: _mmolPerLController, + onChanged: (_) { + setState(() { + _mgPerDlController!.text = + Utils.convertMmolPerLToMgPerDl( + double.tryParse( + _mgPerDlController.text) ?? + 0) + .toString(); + }); + }, + 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(), + ], + ), + 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, + ), + ], + // buttons: [ + // ElevatedButton( + // onPressed: handleCancelAction, + // child: const Text('CANCEL'), + // ), + // ElevatedButton( + // onPressed: handleSaveAction, + // child: const Text('SAVE'), + // ), + // ], + ), + ]), + ); + } +} diff --git a/lib/screens/log/log_event_detail.dart b/lib/screens/log/log_event_detail.dart new file mode 100644 index 0000000..b9e50a7 --- /dev/null +++ b/lib/screens/log/log_event_detail.dart @@ -0,0 +1,145 @@ +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 { + final GlobalKey _logEventForm = GlobalKey(); + + final _notesController = TextEditingController(text: ''); + String? _eventType; + bool _hasEndTime = false; + + late Future> _logEventTypes; + + @override + void initState() { + super.initState(); + + if (widget.logEvent != null) { + _notesController.text = widget.logEvent!.notes ?? ''; + _eventType = widget.logEvent!.eventType; + _hasEndTime = widget.logEvent!.hasEndTime; + } + + _logEventTypes = LogEventType.fetchAll(); + } + + void handleSaveAction() async { + 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, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Event Saved'); + } + } + + 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 || + _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: [ + StyledForm( + formState: _logEventForm, + fields: [ + StyledFutureDropdownButton( + 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: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/log/log_event_list.dart b/lib/screens/log/log_event_list.dart new file mode 100644 index 0000000..3e5460e --- /dev/null +++ b/lib/screens/log/log_event_list.dart @@ -0,0 +1,183 @@ +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/active_log_event_list.dart'; +import 'package:diameter/screens/log/log_event_detail.dart'; +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/components/progress_indicator.dart'; + +class LogEventListScreen extends StatefulWidget { + final LogEntry? logEntry; + + const LogEventListScreen({Key? key, this.logEntry}) : super(key: key); + + @override + _LogEventListScreenState createState() => _LogEventListScreenState(); +} + +class _LogEventListScreenState extends State { + late Future> _logEventTypes; + + void refresh({String? message}) { + if (widget.logEntry != null) { + setState(() { + widget.logEntry!.events = + LogEvent.fetchAllForLogEntry(widget.logEntry!); + widget.logEntry!.endedEvents = + LogEvent.fetchAllEndedByEntry(widget.logEntry!); + }); + } + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + _logEventTypes = LogEventType.fetchAll(); + } + + void handleEditAction(LogEvent event) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LogEventDetailScreen( + endLogEntry: widget.logEntry!, + logEvent: event, + ), + ), + ).then((message) => refresh(message: message)); + } + + void onDelete(LogEvent logEvent) { + logEvent.delete().then((_) => refresh(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 + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.logEntry == null; + return SingleChildScrollView( + padding: const EdgeInsets.only(top: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // TODO: add button for active events + FutureBuilder>( + future: widget.logEntry!.events, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? const Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Events for this Log Entry'), + ) + : ListBody( + children: [ + FutureBuilder>( + future: _logEventTypes, + builder: (context, types) { + return DataTable( + columnSpacing: 10.0, + showCheckboxColumn: false, + rows: snapshot.data != null + ? snapshot.data!.map((event) { + return DataRow( + cells: event.asDataTableCells([ + IconButton( + icon: const Icon(Icons.edit), + iconSize: 16.0, + onPressed: () => + handleEditAction(event)), + IconButton( + icon: + const Icon(Icons.delete), + iconSize: 16.0, + onPressed: () => + handleDeleteAction( + event)), + ], types: types.data), + ); + }).toList() + : [], + columns: LogEvent.asDataTableColumns(), + ); + }) + ], + ), + ); + }, + ), + FutureBuilder>( + future: widget.logEntry!.endedEvents, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Container() + : ListBody( + children: [ + FutureBuilder>( + future: _logEventTypes, + builder: (context, types) { + return DataTable( + columnSpacing: 10.0, + showCheckboxColumn: false, + rows: snapshot.data != null + ? snapshot.data!.map((event) { + return DataRow( + cells: event.asDataTableCells([ + IconButton( + icon: const Icon(Icons.edit), + iconSize: 16.0, + onPressed: () => + handleEditAction(event)), + IconButton( + icon: + const Icon(Icons.delete), + iconSize: 16.0, + onPressed: () => + handleDeleteAction( + event)), + ], types: types.data), + ); + }).toList() + : [], + columns: LogEvent.asDataTableColumns(), + ); + }) + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/log/log_event_type_detail.dart b/lib/screens/log/log_event_type_detail.dart new file mode 100644 index 0000000..3aa973b --- /dev/null +++ b/lib/screens/log/log_event_type_detail.dart @@ -0,0 +1,155 @@ +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 { + final GlobalKey _logEventTypeForm = GlobalKey(); + final _valueController = TextEditingController(text: ''); + final _defaultReminderDurationController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + bool _hasEndTime = 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 { + 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, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Log Event Type Saved'); + } + } + + 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: [ + 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: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/log/log_event_type_list.dart b/lib/screens/log/log_event_type_list.dart new file mode 100644 index 0000000..55ba33a --- /dev/null +++ b/lib/screens/log/log_event_type_list.dart @@ -0,0 +1,124 @@ +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 { + late Future?> _logEventTypes; + + void refresh({String? message}) { + setState(() { + _logEventTypes = LogEventType.fetchAll(); + }); + 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: [ + IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + ]), + drawer: + const Navigation(currentLocation: LogEventTypeListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: FutureBuilder?>( + future: _logEventTypes, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Log Event Types'), + ), + ], + ) + : ListView.builder( + padding: const EdgeInsets.all(10.0), + itemCount: + snapshot.data != null ? snapshot.data!.length : 0, + itemBuilder: (context, index) { + final logEventType = snapshot.data![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 { + await logEventType.delete().then((_) { + refresh( + message: 'Log Event Type deleted'); + }); + }, + icon: const Icon(Icons.delete, + color: Colors.blue), + ) + ], + ), + ); + }, + ), + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LogEventTypeDetailScreen(), + ), + ).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/log/log_meal_detail.dart b/lib/screens/log/log_meal_detail.dart new file mode 100644 index 0000000..e6255f3 --- /dev/null +++ b/lib/screens/log/log_meal_detail.dart @@ -0,0 +1,363 @@ +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: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 { + final GlobalKey _logMealForm = GlobalKey(); + 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: ''); + String? _meal; + String? _source; + String? _category; + String? _portionType; + String? _portionSizeAccuracy; + String? _carbsRatioAccuracy; + + late Future> _meals; + late Future> _mealCategories; + late Future> _mealPortionTypes; + late Future> _mealSources; + late Future> _portionSizeAccuracies; + late Future> _carbsRatioAccuracies; + + @override + void initState() { + super.initState(); + + 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 = widget.logMeal!.portionSizeAccuracy; + _carbsRatioAccuracy = widget.logMeal!.carbsRatioAccuracy; + } + + _meals = Meal.fetchAll(); + _mealCategories = MealCategory.fetchAll(); + _mealPortionTypes = MealPortionType.fetchAll(); + _mealSources = MealSource.fetchAll(); + _portionSizeAccuracies = Accuracy.fetchAllForPortionSize(); + _carbsRatioAccuracies = Accuracy.fetchAllForCarbsRatio(); + } + + void handleSaveAction() async { + if (_logMealForm.currentState!.validate()) { + bool isNew = widget.logMeal == null; + isNew + ? await LogMeal.save( + logEntry: widget.logEntry.objectId!, + meal: _meal, + value: _valueController.text, + source: _source, + category: _category, + portionType: _portionType, + carbsRatio: double.tryParse(_carbsRatioController.text), + portionSize: double.tryParse(_portionSizeController.text), + carbsPerPortion: double.tryParse(_carbsPerPortionController.text), + portionSizeAccuracy: _portionSizeAccuracy, + carbsRatioAccuracy: _carbsRatioAccuracy, + 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: _source, + category: _category, + portionType: _portionType, + carbsRatio: double.tryParse(_carbsRatioController.text), + portionSize: double.tryParse(_portionSizeController.text), + carbsPerPortion: double.tryParse(_carbsPerPortionController.text), + portionSizeAccuracy: _portionSizeAccuracy, + carbsRatioAccuracy: _carbsRatioAccuracy, + delayedBolusDuration: + int.tryParse(_delayedBolusDurationController.text), + delayedBolusRate: + double.tryParse(_delayedBolusRateController.text), + notes: _notesController.text, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Meal Saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.logMeal == null; + if (showConfirmationDialogOnCancel && + ((isNew && + (_valueController.text != '' || + _meal != null || + _source != null || + _category != null || + _portionType != null || + double.tryParse(_carbsRatioController.text) != null || + double.tryParse(_portionSizeController.text) != null || + double.tryParse(_carbsPerPortionController.text) != null || + _carbsRatioAccuracy != null || + _portionSizeAccuracy != null || + int.tryParse(_delayedBolusDurationController.text) != + null || + double.tryParse(_delayedBolusRateController.text) != null || + _notesController.text != '')) || + (!isNew && + (_valueController.text != widget.logMeal!.value || + _meal != widget.logMeal!.meal || + _source != widget.logMeal!.source || + _category != widget.logMeal!.category || + _portionType != widget.logMeal!.portionType || + 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 || + 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); + } + } + + @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: [ + StyledForm( + formState: _logMealForm, + fields: [ + // TODO: autofill all associated fields on selecting a meal + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), + StyledFutureDropdownButton( + selectedItem: _meal, + label: 'Meal', + items: _meals, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _meal = value; + }); + }, + ), + StyledFutureDropdownButton( + selectedItem: _source, + label: 'Meal Source', + items: _mealSources, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _source = value; + }); + }, + ), + StyledFutureDropdownButton( + selectedItem: _category, + label: 'Meal Category', + items: _mealCategories, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _category = value; + }); + }, + ), + StyledFutureDropdownButton( + selectedItem: _portionType, + label: 'Meal Portion Type', + items: _mealPortionTypes, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _portionType = value; + }); + }, + ), + // TODO: if 2 out of the 3 following fields are given, calc 3rd + TextFormField( + decoration: const InputDecoration( + labelText: 'Carbs ratio', + suffixText: '%', + ), + controller: _carbsRatioController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + ), + TextFormField( + decoration: InputDecoration( + labelText: 'Portion size', + suffixText: nutritionMeasurement == + NutritionMeasurement.grams + ? 'g' + : nutritionMeasurement == NutritionMeasurement.ounces + ? 'oz' + : '', + ), + controller: _portionSizeController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + ), + TextFormField( + decoration: InputDecoration( + labelText: 'Carbs per portion', + suffixText: nutritionMeasurement == + NutritionMeasurement.grams + ? 'g' + : nutritionMeasurement == NutritionMeasurement.ounces + ? 'oz' + : '', + ), + controller: _carbsPerPortionController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + ), + StyledFutureDropdownButton( + 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), + ), + // TODO: autofill the following fields on selecting a source + StyledFutureDropdownButton( + selectedItem: _portionSizeAccuracy, + label: 'Portion Size Accuracy', + items: _portionSizeAccuracies, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _portionSizeAccuracy = value; + }); + }, + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + alignLabelWithHint: true, + ), + keyboardType: TextInputType.multiline, + ), + ], + ), + ], + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/log/log_meal_list.dart b/lib/screens/log/log_meal_list.dart new file mode 100644 index 0000000..072280f --- /dev/null +++ b/lib/screens/log/log_meal_list.dart @@ -0,0 +1,125 @@ +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'; +import 'package:diameter/components/progress_indicator.dart'; + +class LogMealListScreen extends StatefulWidget { + final LogEntry? logEntry; + + const LogMealListScreen({Key? key, this.logEntry}) : super(key: key); + + @override + _LogMealListScreenState createState() => _LogMealListScreenState(); +} + +class _LogMealListScreenState extends State { + void refresh({String? message}) { + if (widget.logEntry != null) { + setState(() { + widget.logEntry!.meals = LogMeal.fetchAllForLogEntry(widget.logEntry!); + }); + } + 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) => refresh(message: message)); + } + + void onDelete(LogMeal meal) { + meal.delete().then((_) => refresh(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 + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.only(top: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FutureBuilder>( + future: widget.logEntry!.meals, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? const Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Meals for this Log Entry'), + ) + : ListBody( + children: [ + DataTable( + columnSpacing: 10.0, + showCheckboxColumn: false, + rows: snapshot.data != null + ? snapshot.data!.map((meal) { + return DataRow( + cells: meal.asDataTableCells( + [ + IconButton( + icon: const Icon(Icons.edit), + iconSize: 16.0, + onPressed: () => + handleEditAction(meal)), + IconButton( + icon: const Icon(Icons.delete), + iconSize: 16.0, + onPressed: () => + handleDeleteAction(meal)), + ], + ), + ); + }).toList() + : [], + columns: LogMeal.asDataTableColumns(), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/meal/meal_category_detail.dart b/lib/screens/meal/meal_category_detail.dart new file mode 100644 index 0000000..4dc5c8e --- /dev/null +++ b/lib/screens/meal/meal_category_detail.dart @@ -0,0 +1,113 @@ +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/navigation.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/models/meal_category.dart'; + +class MealCategoryDetailScreen extends StatefulWidget { + static const String routeName = '/meal-category'; + final MealCategory? mealCategory; + + const MealCategoryDetailScreen({Key? key, this.mealCategory}) + : super(key: key); + + @override + _MealCategoryDetailScreenState createState() => + _MealCategoryDetailScreenState(); +} + +class _MealCategoryDetailScreenState extends State { + final GlobalKey _mealCategoryForm = GlobalKey(); + final _valueController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + + @override + void initState() { + super.initState(); + if (widget.mealCategory != null) { + _valueController.text = widget.mealCategory!.value; + _notesController.text = widget.mealCategory!.notes ?? ''; + } + } + + void handleSaveAction() async { + if (_mealCategoryForm.currentState!.validate()) { + bool isNew = widget.mealCategory == null; + isNew + ? await MealCategory.save( + value: _valueController.text, notes: _notesController.text) + : await MealCategory.update(widget.mealCategory!.objectId!, + value: _valueController.text, notes: _notesController.text); + Navigator.pop(context, '${isNew ? 'New' : ''} Meal Category saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.mealCategory == null; + + if (showConfirmationDialogOnCancel && + (isNew && + (_valueController.text != '' || _notesController.text != '')) || + (!isNew && + (widget.mealCategory!.value != _valueController.text || + (widget.mealCategory!.notes ?? '') != _notesController.text))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.mealCategory == null; + return Scaffold( + appBar: AppBar( + title: Text(isNew ? 'New Meal Category' : widget.mealCategory!.value), + ), + drawer: + const Navigation(currentLocation: MealCategoryDetailScreen.routeName), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _mealCategoryForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + alignLabelWithHint: true, + ), + keyboardType: TextInputType.multiline, + ), + ], + ), + ], + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/meal/meal_category_list.dart b/lib/screens/meal/meal_category_list.dart new file mode 100644 index 0000000..0304a01 --- /dev/null +++ b/lib/screens/meal/meal_category_list.dart @@ -0,0 +1,149 @@ +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/progress_indicator.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/meal/meal_category_detail.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/models/meal_category.dart'; + +class MealCategoryListScreen extends StatefulWidget { + static const String routeName = '/meal-categories'; + + const MealCategoryListScreen({Key? key}) : super(key: key); + + @override + _MealCategoryListScreenState createState() => _MealCategoryListScreenState(); +} + +class _MealCategoryListScreenState extends State { + late Future?> _mealCategories; + + void refresh({String? message}) { + setState(() { + _mealCategories = MealCategory.fetchAll(); + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void onDelete(MealCategory mealCategory) { + mealCategory + .delete() + .then((_) => refresh(message: 'Meal Category deleted')); + } + + void handleDeleteAction(MealCategory mealCategory) async { + if (showConfirmationDialogOnDelete) { + Dialogs.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(mealCategory), + message: 'Are you sure you want to delete this Meal Category?', + ); + } else { + onDelete(mealCategory); + } + } + + @override + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Meal Categories'), + actions: [ + IconButton( + onPressed: refresh, + icon: const Icon(Icons.refresh), + ), + ], + ), + drawer: + const Navigation(currentLocation: MealCategoryListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: FutureBuilder?>( + future: _mealCategories, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Meal Categories'), + ), + ], + ) + : ListView.builder( + padding: const EdgeInsets.only(top: 10.0), + itemCount: + snapshot.data != null ? snapshot.data!.length : 0, + itemBuilder: (context, index) { + final mealCategory = snapshot.data![index]; + return ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MealCategoryDetailScreen( + mealCategory: mealCategory, + ), + ), + ).then((message) => refresh(message: message)); + }, + title: Text(mealCategory.value), + subtitle: Text(mealCategory.notes ?? ''), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => + handleDeleteAction(mealCategory), + ), + ], + ), + ); + }), + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MealCategoryDetailScreen(), + ), + ).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/meal/meal_detail.dart b/lib/screens/meal/meal_detail.dart new file mode 100644 index 0000000..c030675 --- /dev/null +++ b/lib/screens/meal/meal_detail.dart @@ -0,0 +1,326 @@ +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/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:flutter/material.dart'; + +class MealDetailScreen extends StatefulWidget { + static const String routeName = '/meal'; + + final Meal? meal; + const MealDetailScreen({Key? key, this.meal}) : super(key: key); + + @override + _MealDetailScreenState createState() => _MealDetailScreenState(); +} + +class _MealDetailScreenState extends State { + final GlobalKey _mealForm = GlobalKey(); + final _valueController = TextEditingController(); + final _carbsRatioController = TextEditingController(); + final _portionSizeController = TextEditingController(); + final _carbsPerPortionController = TextEditingController(); + final _delayedBolusRateController = TextEditingController(); + final _delayedBolusDurationController = TextEditingController(); + final _notesController = TextEditingController(); + String? _source; + String? _category; + String? _portionType; + String? _portionSizeAccuracy; + String? _carbsRatioAccuracy; + + late Future> _mealCategories; + late Future> _mealPortionTypes; + late Future> _mealSources; + late Future> _portionSizeAccuracies; + late Future> _carbsRatioAccuracies; + + @override + void initState() { + super.initState(); + + if (widget.meal != null) { + _valueController.text = widget.meal!.value; + _carbsRatioController.text = (widget.meal!.carbsRatio ?? '').toString(); + _portionSizeController.text = (widget.meal!.portionSize ?? '').toString(); + _carbsPerPortionController.text = + (widget.meal!.carbsPerPortion ?? '').toString(); + _delayedBolusRateController.text = + (widget.meal!.delayedBolusRate ?? '').toString(); + _delayedBolusDurationController.text = + (widget.meal!.delayedBolusDuration ?? '').toString(); + _notesController.text = widget.meal!.notes ?? ''; + + _source = widget.meal!.source; + _category = widget.meal!.category; + _portionType = widget.meal!.portionType; + _portionSizeAccuracy = widget.meal!.portionSizeAccuracy; + _carbsRatioAccuracy = widget.meal!.carbsRatioAccuracy; + } + + _mealCategories = MealCategory.fetchAll(); + _mealPortionTypes = MealPortionType.fetchAll(); + _mealSources = MealSource.fetchAll(); + _portionSizeAccuracies = Accuracy.fetchAllForPortionSize(); + _carbsRatioAccuracies = Accuracy.fetchAllForCarbsRatio(); + } + + void handleSaveAction() async { + if (_mealForm.currentState!.validate()) { + bool isNew = widget.meal == null; + isNew + ? await Meal.save( + value: _valueController.text, + source: _source, + category: _category, + portionType: _portionType, + carbsRatio: double.tryParse(_carbsRatioController.text), + portionSize: double.tryParse(_portionSizeController.text), + carbsPerPortion: double.tryParse(_carbsPerPortionController.text), + portionSizeAccuracy: _portionSizeAccuracy, + carbsRatioAccuracy: _carbsRatioAccuracy, + delayedBolusDuration: + int.tryParse(_delayedBolusDurationController.text), + delayedBolusRate: + double.tryParse(_delayedBolusRateController.text), + notes: _notesController.text, + ) + : await Meal.update( + widget.meal!.objectId!, + value: _valueController.text, + source: _source, + category: _category, + portionType: _portionType, + carbsRatio: double.tryParse(_carbsRatioController.text), + portionSize: double.tryParse(_portionSizeController.text), + carbsPerPortion: double.tryParse(_carbsPerPortionController.text), + portionSizeAccuracy: _portionSizeAccuracy, + carbsRatioAccuracy: _carbsRatioAccuracy, + delayedBolusDuration: + int.tryParse(_delayedBolusDurationController.text), + delayedBolusRate: + double.tryParse(_delayedBolusRateController.text), + notes: _notesController.text, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Meal Saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.meal == null; + if (showConfirmationDialogOnCancel && + ((isNew && + (_valueController.text != '' || + _source != null || + _category != null || + _portionType != null || + double.tryParse(_carbsRatioController.text) != null || + double.tryParse(_portionSizeController.text) != null || + double.tryParse(_carbsPerPortionController.text) != null || + _carbsRatioAccuracy != null || + _portionSizeAccuracy != null || + int.tryParse(_delayedBolusDurationController.text) != + null || + double.tryParse(_delayedBolusRateController.text) != null || + _notesController.text != '')) || + (!isNew && + (_valueController.text != widget.meal!.value || + _source != widget.meal!.source || + _category != widget.meal!.category || + _portionType != widget.meal!.portionType || + double.tryParse(_carbsRatioController.text) != + widget.meal!.carbsRatio || + double.tryParse(_portionSizeController.text) != + widget.meal!.portionSize || + double.tryParse(_carbsPerPortionController.text) != + widget.meal!.carbsPerPortion || + _carbsRatioAccuracy != widget.meal!.carbsRatioAccuracy || + _portionSizeAccuracy != widget.meal!.portionSizeAccuracy || + int.tryParse(_delayedBolusDurationController.text) != + widget.meal!.delayedBolusDuration || + double.tryParse(_delayedBolusRateController.text) != + widget.meal!.delayedBolusRate || + _notesController.text != (widget.meal!.notes ?? ''))))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.meal == null; + return Scaffold( + appBar: AppBar( + title: Text(isNew ? 'New Meal' : widget.meal!.value), + ), + drawer: const Navigation(currentLocation: MealDetailScreen.routeName), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _mealForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), + StyledFutureDropdownButton( + selectedItem: _source, + label: 'Meal Source', + items: _mealSources, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _source = value; + }); + }, + ), + // TODO: autofill the following fields on selecting a source + StyledFutureDropdownButton( + selectedItem: _category, + label: 'Meal Category', + items: _mealCategories, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _category = value; + }); + }, + ), + StyledFutureDropdownButton( + selectedItem: _portionType, + label: 'Meal Portion Type', + items: _mealPortionTypes, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _portionType = value; + }); + }, + ), + // TODO: if 2 out of the 3 following fields are given, calc 3rd + TextFormField( + decoration: const InputDecoration( + labelText: 'Carbs ratio', + suffixText: '%', + ), + controller: _carbsRatioController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + ), + 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), + ), + TextFormField( + decoration: InputDecoration( + labelText: 'Carbs per portion', + suffixText: nutritionMeasurement == + NutritionMeasurement.grams + ? 'g' + : nutritionMeasurement == NutritionMeasurement.ounces + ? 'oz' + : '', + ), + controller: _carbsPerPortionController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + ), + StyledFutureDropdownButton( + selectedItem: _carbsRatioAccuracy, + label: 'Carbs Ratio Accuracy', + items: _carbsRatioAccuracies, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _carbsRatioAccuracy = value; + }); + }, + ), + // TODO: display according to time format + 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', + ), + controller: _delayedBolusRateController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + ), + // TODO: autofill the following fields on selecting a source + StyledFutureDropdownButton( + selectedItem: _portionSizeAccuracy, + label: 'Portion Size Accuracy', + items: _portionSizeAccuracies, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _portionSizeAccuracy = value; + }); + }, + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + alignLabelWithHint: true, + ), + keyboardType: TextInputType.multiline, + ), + ], + ), + ], + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/meal/meal_list.dart b/lib/screens/meal/meal_list.dart new file mode 100644 index 0000000..af43d46 --- /dev/null +++ b/lib/screens/meal/meal_list.dart @@ -0,0 +1,136 @@ +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/progress_indicator.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/models/meal.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/meal/meal_detail.dart'; +import 'package:flutter/material.dart'; + +class MealListScreen extends StatefulWidget { + static const String routeName = '/meals'; + + const MealListScreen({Key? key}) : super(key: key); + + @override + _MealListScreenState createState() => _MealListScreenState(); +} + +class _MealListScreenState extends State { + late Future?> _meals; + + void refresh({String? message}) { + setState(() { + _meals = Meal.fetchAll(); + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void onDelete(Meal meal) { + meal.delete().then((_) => refresh(message: 'Meal deleted')); + } + + void handleDeleteAction(Meal 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 + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Meals'), actions: [ + IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + ]), + drawer: const Navigation(currentLocation: MealListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: FutureBuilder?>( + future: _meals, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Meals'), + ), + ], + ) + : ListView.builder( + padding: const EdgeInsets.all(10.0), + itemCount: + snapshot.data != null ? snapshot.data!.length : 0, + itemBuilder: (context, index) { + final meal = snapshot.data![index]; + return ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MealDetailScreen(meal: meal), + ), + ).then((message) => refresh(message: message)); + }, + title: Text(meal.value), + subtitle: Text(meal.notes ?? ''), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => handleDeleteAction(meal), + icon: const Icon(Icons.delete, + color: Colors.blue), + ) + ], + ), + ); + }, + ), + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MealDetailScreen(), + ), + ).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/meal/meal_portion_type_detail.dart b/lib/screens/meal/meal_portion_type_detail.dart new file mode 100644 index 0000000..b19ae40 --- /dev/null +++ b/lib/screens/meal/meal_portion_type_detail.dart @@ -0,0 +1,121 @@ +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/navigation.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/models/meal_portion_type.dart'; + +class MealPortionTypeDetailScreen extends StatefulWidget { + static const String routeName = '/meal-portion-type'; + + final MealPortionType? mealPortionType; + + const MealPortionTypeDetailScreen({Key? key, this.mealPortionType}) + : super(key: key); + + @override + _MealPortionTypeDetailScreenState createState() => + _MealPortionTypeDetailScreenState(); +} + +class _MealPortionTypeDetailScreenState + extends State { + final GlobalKey _mealPortionTypeForm = GlobalKey(); + final _valueController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + + @override + void initState() { + super.initState(); + if (widget.mealPortionType != null) { + _valueController.text = widget.mealPortionType!.value; + _notesController.text = widget.mealPortionType!.notes ?? ''; + } + } + + void handleSaveAction() async { + if (_mealPortionTypeForm.currentState!.validate()) { + bool isNew = widget.mealPortionType == null; + isNew + ? MealPortionType.save( + value: _valueController.text, + notes: _notesController.text, + ) + : MealPortionType.update( + widget.mealPortionType!.objectId!, + value: _valueController.text, + notes: _notesController.text, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Meal Portion Type saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.mealPortionType == null; + if (showConfirmationDialogOnCancel && + ((isNew && + (_valueController.text != '' || _notesController.text != '')) || + (!isNew && + (_valueController.text != widget.mealPortionType!.value || + _notesController.text != + (widget.mealPortionType!.notes ?? ''))))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.mealPortionType == null; + return Scaffold( + appBar: AppBar( + title: Text( + isNew ? 'New Meal Portion Type' : widget.mealPortionType!.value), + ), + drawer: const Navigation( + currentLocation: MealPortionTypeDetailScreen.routeName), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _mealPortionTypeForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), + TextFormField( + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + alignLabelWithHint: true, + ), + keyboardType: TextInputType.multiline, + ) + ], + ), + ], + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/meal/meal_portion_type_list.dart b/lib/screens/meal/meal_portion_type_list.dart new file mode 100644 index 0000000..a67409d --- /dev/null +++ b/lib/screens/meal/meal_portion_type_list.dart @@ -0,0 +1,148 @@ +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/progress_indicator.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/meal/meal_portion_type_detail.dart'; +import 'package:flutter/material.dart'; +import 'package:diameter/models/meal_portion_type.dart'; + +class MealPortionTypeListScreen extends StatefulWidget { + static const String routeName = '/meal-portion-types'; + + const MealPortionTypeListScreen({Key? key}) : super(key: key); + + @override + _MealPortionTypeListScreenState createState() => + _MealPortionTypeListScreenState(); +} + +class _MealPortionTypeListScreenState extends State { + late Future?> _mealPortionTypes; + + void refresh({String? message}) { + setState(() { + _mealPortionTypes = MealPortionType.fetchAll(); + }); + setState(() { + if (message != null) { + var snackBar = SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + ); + ScaffoldMessenger.of(context) + ..removeCurrentSnackBar() + ..showSnackBar(snackBar); + } + }); + } + + void onDelete(MealPortionType mealPortionType) { + mealPortionType + .delete() + .then((_) => refresh(message: 'Meal Portion Type deleted')); + } + + void handleDeleteAction(MealPortionType mealPortionType) async { + if (showConfirmationDialogOnDelete) { + Dialogs.showConfirmationDialog( + context: context, + onConfirm: () => onDelete(mealPortionType), + message: 'Are you sure you want to delete this Meal Portion Type?', + ); + } else { + onDelete(mealPortionType); + } + } + + @override + void initState() { + super.initState(); + refresh(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Meal Portion Types'), + actions: [ + IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + ], + ), + drawer: const Navigation( + currentLocation: MealPortionTypeListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: FutureBuilder?>( + future: _mealPortionTypes, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Meal Portion Types'), + ) + ], + ) + : ListView.builder( + padding: const EdgeInsets.only(top: 10.0), + itemCount: snapshot.data != null + ? snapshot.data!.length + : 0, + itemBuilder: (context, index) { + final mealPortionType = snapshot.data![index]; + return ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MealPortionTypeDetailScreen( + mealPortionType: mealPortionType, + ), + ), + ).then( + (message) => refresh(message: message)); + }, + title: Text(mealPortionType.value), + subtitle: Text(mealPortionType.notes ?? ''), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => + handleDeleteAction(mealPortionType), + ), + ], + ), + ); + })); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MealPortionTypeDetailScreen(), + ), + ).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/screens/meal/meal_source_detail.dart b/lib/screens/meal/meal_source_detail.dart new file mode 100644 index 0000000..1c53056 --- /dev/null +++ b/lib/screens/meal/meal_source_detail.dart @@ -0,0 +1,210 @@ +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/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:flutter/material.dart'; + +class MealSourceDetailScreen extends StatefulWidget { + static const String routeName = '/meal-source'; + + final MealSource? mealSource; + + const MealSourceDetailScreen({Key? key, this.mealSource}) : super(key: key); + + @override + _MealSourceDetailScreenState createState() => _MealSourceDetailScreenState(); +} + +class _MealSourceDetailScreenState extends State { + late Future> _portionSizeAccuracies; + late Future> _carbsRatioAccuracies; + late Future> _mealCategories; + late Future> _mealPortionTypes; + + final GlobalKey _mealSourceForm = GlobalKey(); + final _valueController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); + String? _defaultCarbsRatioAccuracy; + String? _defaultPortionSizeAccuracy; + String? _defaultMealCategory; + String? _defaultMealPortionType; + + @override + void initState() { + super.initState(); + + if (widget.mealSource != null) { + _valueController.text = widget.mealSource!.value; + _notesController.text = widget.mealSource!.notes ?? ''; + + _defaultCarbsRatioAccuracy = widget.mealSource!.defaultCarbsRatioAccuracy; + _defaultPortionSizeAccuracy = + widget.mealSource!.defaultPortionSizeAccuracy; + _defaultMealCategory = widget.mealSource!.defaultMealCategory; + _defaultMealPortionType = widget.mealSource!.defaultMealPortionType; + } + + _portionSizeAccuracies = Accuracy.fetchAllForPortionSize(); + _carbsRatioAccuracies = Accuracy.fetchAllForCarbsRatio(); + _mealCategories = MealCategory.fetchAll(); + _mealPortionTypes = MealPortionType.fetchAll(); + } + + void handleSaveAction() async { + bool isNew = widget.mealSource == null; + if (_mealSourceForm.currentState!.validate()) { + isNew + ? await MealSource.save( + value: _valueController.text, + defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy, + defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy, + defaultMealCategory: _defaultMealCategory, + defaultMealPortionType: _defaultMealPortionType, + notes: _notesController.text, + ) + : await MealSource.update( + widget.mealSource!.objectId!, + value: _valueController.text, + defaultCarbsRatioAccuracy: _defaultCarbsRatioAccuracy, + defaultPortionSizeAccuracy: _defaultPortionSizeAccuracy, + defaultMealCategory: _defaultMealCategory, + defaultMealPortionType: _defaultMealPortionType, + notes: _notesController.text, + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Meal Source saved'); + } + } + + void handleCancelAction() { + bool isNew = widget.mealSource == null; + if (showConfirmationDialogOnCancel && + ((isNew && + (_valueController.text != '' || + _defaultCarbsRatioAccuracy != null || + _defaultPortionSizeAccuracy != null || + _defaultMealCategory != null || + _defaultMealPortionType != null || + _notesController.text != '')) || + (!isNew && + (_valueController.text != widget.mealSource!.value || + _defaultCarbsRatioAccuracy != + widget.mealSource!.defaultCarbsRatioAccuracy || + _defaultPortionSizeAccuracy != + widget.mealSource!.defaultPortionSizeAccuracy || + _defaultMealCategory != + widget.mealSource!.defaultMealCategory || + _defaultMealPortionType != + widget.mealSource!.defaultMealPortionType || + _notesController.text != + (widget.mealSource!.notes ?? ''))))) { + Dialogs.showCancelConfirmationDialog( + context: context, + isNew: isNew, + onSave: handleSaveAction, + ); + } else { + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + bool isNew = widget.mealSource == null; + return Scaffold( + appBar: AppBar( + title: Text(isNew ? 'New Meal Source' : widget.mealSource!.value), + ), + drawer: + const Navigation(currentLocation: MealSourceDetailScreen.routeName), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _mealSourceForm, + fields: [ + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), + StyledFutureDropdownButton( + selectedItem: _defaultCarbsRatioAccuracy, + label: 'Default Carbs Ratio Accuracy', + items: _carbsRatioAccuracies, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _defaultCarbsRatioAccuracy = value; + }); + }, + ), + StyledFutureDropdownButton( + selectedItem: _defaultPortionSizeAccuracy, + label: 'Default Portion Size Accuracy', + items: _portionSizeAccuracies, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _defaultPortionSizeAccuracy = value; + }); + }, + ), + StyledFutureDropdownButton( + selectedItem: _defaultMealCategory, + label: 'Default Meal Category', + items: _mealCategories, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _defaultMealCategory = value; + }); + }, + ), + StyledFutureDropdownButton( + 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, + ) + ], + ), + ], + ), + ), + bottomNavigationBar: DetailBottomRow( + onCancel: handleCancelAction, + onSave: handleSaveAction, + ), + ); + } +} diff --git a/lib/screens/meal/meal_source_list.dart b/lib/screens/meal/meal_source_list.dart new file mode 100644 index 0000000..9478972 --- /dev/null +++ b/lib/screens/meal/meal_source_list.dart @@ -0,0 +1,131 @@ +import 'package:diameter/components/progress_indicator.dart'; +import 'package:diameter/models/meal_source.dart'; +import 'package:diameter/navigation.dart'; +import 'package:diameter/screens/meal/meal_source_detail.dart'; +import 'package:flutter/material.dart'; + +class MealSourceListScreen extends StatefulWidget { + static const String routeName = '/meal-sources'; + + const MealSourceListScreen({Key? key}) : super(key: key); + + @override + _MealSourceListScreenState createState() => _MealSourceListScreenState(); +} + +class _MealSourceListScreenState extends State { + late Future?> _mealSources; + + void refresh({String? message}) { + setState(() { + _mealSources = MealSource.fetchAll(); + }); + 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('Meal Sources'), + actions: [ + IconButton(onPressed: refresh, icon: const Icon(Icons.refresh)) + ], + ), + drawer: const Navigation(currentLocation: MealSourceListScreen.routeName), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: FutureBuilder?>( + future: _mealSources, + builder: (context, snapshot) { + return ViewWithProgressIndicator( + snapshot: snapshot, + child: snapshot.data == null || snapshot.data!.isEmpty + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(10.0), + child: Text('No Meal Sources'), + ) + ], + ) + : ListView.builder( + padding: const EdgeInsets.only(top: 10.0), + itemCount: snapshot.data != null + ? snapshot.data!.length + : 0, + itemBuilder: (context, index) { + final mealSource = snapshot.data![index]; + return ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MealSourceDetailScreen( + mealSource: mealSource, + ), + ), + ).then( + (message) => refresh(message: message)); + }, + title: Text(mealSource.value), + subtitle: Text(mealSource.notes ?? ''), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () async { + // add confirmation dialog + await mealSource.delete().then((_) { + refresh( + message: 'Meal Source deleted'); + }); + }, + ), + ], + ), + ); + })); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MealSourceDetailScreen(), + ), + ).then((message) => refresh(message: message)); + }, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/settings.dart b/lib/settings.dart new file mode 100644 index 0000000..f17dd65 --- /dev/null +++ b/lib/settings.dart @@ -0,0 +1,238 @@ +import 'package:diameter/components/dialogs.dart'; +import 'package:diameter/components/forms.dart'; +import 'package:diameter/config.dart'; +import 'package:diameter/navigation.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.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 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 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 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 { + static const String routeName = '/settings'; + + const SettingsScreen({Key? key}) : super(key: key); + + @override + _SettingsScreenState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + final GlobalKey _settingsForm = GlobalKey(); + + void onReset() { + Settings.resetAll(); + setState(() { + Settings.loadSettingsIntoConfig(); + }); + } + + void handleResetAction() async { + Dialogs.showConfirmationDialog( + context: context, + onConfirm: onReset, + message: 'Are you sure you want to reset all settings?', + ); + } + + @override + initState() { + super.initState(); + Settings.loadSettingsIntoConfig(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Application Settings'), + ), + drawer: const Navigation(currentLocation: SettingsScreen.routeName), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _settingsForm, + fields: [ + StyledDropdownButton( + selectedItem: nutritionMeasurement, + label: 'Preferred Nutrition Measurement', + items: NutritionMeasurement.values, + renderItem: (item) => Text(item.toString().split('.')[1]), + onChanged: (value) { + if (value != null) { + Settings.setNutritionMeasurement(value); + setState(() { + nutritionMeasurement = value; + }); + } + }, + ), + StyledDropdownButton( + selectedItem: glucoseMeasurement, + label: 'Preferred Glucose Measurement', + items: GlucoseMeasurement.values, + renderItem: (item) => Text(item.toString().split('.')[1]), + onChanged: (value) { + if (value != null) { + Settings.setGlucoseMeasurement(value); + setState(() { + glucoseMeasurement = value; + }); + } + }, + ), + StyledBooleanFormField( + value: glucoseDisplayMode == GlucoseDisplayMode.activeOnly, + label: 'only display active glucose measurement', + onChanged: (_) { + GlucoseDisplayMode mode = + glucoseDisplayMode == GlucoseDisplayMode.activeOnly + ? GlucoseDisplayMode.both + : GlucoseDisplayMode.activeOnly; + Settings.setGlucoseDisplayMode(mode); + setState(() { + glucoseDisplayMode = mode; + }); + }, + ), + StyledBooleanFormField( + value: glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForDetail, + enabled: glucoseDisplayMode != GlucoseDisplayMode.activeOnly, + label: 'display both glucose measurements in detail view', + onChanged: (_) { + GlucoseDisplayMode mode = glucoseDisplayMode == + GlucoseDisplayMode.both + ? GlucoseDisplayMode.bothForList + : glucoseDisplayMode == GlucoseDisplayMode.bothForList + ? GlucoseDisplayMode.both + : GlucoseDisplayMode.activeOnly; + Settings.setGlucoseDisplayMode(mode); + setState(() { + glucoseDisplayMode = mode; + }); + }, + ), + StyledBooleanFormField( + value: glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForList, + enabled: glucoseDisplayMode != GlucoseDisplayMode.activeOnly, + label: 'display both glucose measurements in list view', + onChanged: (_) { + GlucoseDisplayMode mode = glucoseDisplayMode == + GlucoseDisplayMode.both + ? GlucoseDisplayMode.bothForDetail + : glucoseDisplayMode == GlucoseDisplayMode.bothForDetail + ? GlucoseDisplayMode.both + : GlucoseDisplayMode.activeOnly; + Settings.setGlucoseDisplayMode(mode); + setState(() { + glucoseDisplayMode = mode; + }); + }, + ), + // TODO: add fields for date and time formats + // TODO: add fields for glucose target + ], + ), + ], + ), + ), + bottomNavigationBar: BottomAppBar( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + ElevatedButton.icon( + onPressed: handleResetAction, + icon: const Icon( + Icons.settings_backup_restore, + size: 18.0, + ), + label: const Text('RESET ALL'), + ), + const Spacer(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/utils/date_time_utils.dart b/lib/utils/date_time_utils.dart new file mode 100644 index 0000000..9023e30 --- /dev/null +++ b/lib/utils/date_time_utils.dart @@ -0,0 +1,49 @@ +import 'package:diameter/config.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class DateTimeUtils { + static String displayDateTime(DateTime? date, {String fallback = ''}) { + if (date == null) { + return fallback; + } + DateTime localDate = date.toLocal(); + final DateFormat formatter = DateFormat('$dateFormat $timeFormat'); + return formatter.format(localDate); + } + + static String displayDate(DateTime? date, {String fallback = ''}) { + if (date == null) { + return fallback; + } + DateTime localDate = date.toLocal(); + final DateFormat formatter = DateFormat(longDateFormat ?? dateFormat); + return formatter.format(localDate); + } + + static String displayTime(DateTime? date, + {String fallback = '', bool? longFormat}) { + if (date == null) { + return fallback; + } + DateTime localDate = date.toLocal(); + final DateFormat formatter = DateFormat( + longFormat == true ? longTimeFormat ?? timeFormat : timeFormat); + return formatter.format(localDate); + } + + static String displayTimeOfDay(TimeOfDay? time, + {String fallback = '', bool? longFormat}) { + if (time == null) { + return fallback; + } + final DateFormat formatter = DateFormat( + longFormat == true ? longTimeFormat ?? timeFormat : timeFormat); + return formatter.format(convertTimeOfDayToDateTime(time)); + } + + static DateTime convertTimeOfDayToDateTime(TimeOfDay time) { + return DateTime( + dummyDate.year, dummyDate.month, dummyDate.day, time.hour, time.minute); + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 0000000..680ae69 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,9 @@ +class Utils { + static double convertMgPerDlToMmolPerL(int mgPerDl) { + return (mgPerDl / 18.018).roundToDouble(); + } + + static int convertMmolPerLToMgPerDl(double mmolPerL) { + return (mmolPerL * 18.018).round(); + } +} diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..e2c73ef --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Mon Sep 20 23:21:46 CEST 2021 +sdk.dir=/home/spinel/Android/Sdk diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..206c9af --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,572 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + 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.0" + 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" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + 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.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + flex_color_scheme: + dependency: "direct main" + description: + name: flex_color_scheme + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + idb_shim: + dependency: transitive + description: + name: idb_shim + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime_type: + dependency: transitive + description: + name: mime_type + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + 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.2.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: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + 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.3.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.3" + provider: + dependency: "direct dev" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" + 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" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + 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: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.9" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.0" + xxtea: + dependency: transitive + description: + name: xxtea + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" +sdks: + dart: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..f1e788a --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,33 @@ +name: diameter +description: A logging app for type 1 diabetics. + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + parse_server_sdk_flutter: ^3.1.0 + flutter: + sdk: flutter + sqflite: ^2.0.0+4 + path_provider: ^2.0.5 + cupertino_icons: ^1.0.2 + flex_color_scheme: ^3.0.1 + shared_preferences: ^2.0.8 + intl: ^0.17.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.4 + provider: ^6.0.1 + +flutter: + uses-material-design: true + fonts: + - family: RobotoCondensed + fonts: + - asset: assets/fonts/RobotoCondensed-Regular.ttf diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..5c45780 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; + +// import 'package:tide/main.dart'; + +void main() { + // testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // // Build our app and trigger a frame. + // await tester.pumpWidget(Home()); + + // // Verify that our counter starts at 0. + // expect(find.text('0'), findsOneWidget); + // expect(find.text('1'), findsNothing); + + // // Tap the '+' icon and trigger a frame. + // await tester.tap(find.byIcon(Icons.add)); + // await tester.pump(); + + // // Verify that our counter has incremented. + // expect(find.text('0'), findsNothing); + // expect(find.text('1'), findsOneWidget); + // }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..ec9bfb5 --- /dev/null +++ b/web/index.html @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + tide + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..59cac49 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "tide", + "short_name": "tide", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}