diff --git a/mobile_app/.gitignore b/mobile_app/.gitignore new file mode 100644 index 0000000..af77a49 --- /dev/null +++ b/mobile_app/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.lock +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# 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 +.pub-cache/ +.pub/ +/build/ + +# 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 +*.g.dart +*.lock +pubspec.lock \ No newline at end of file diff --git a/mobile_app/.metadata b/mobile_app/.metadata new file mode 100644 index 0000000..9b1ffbe --- /dev/null +++ b/mobile_app/.metadata @@ -0,0 +1,33 @@ +# 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: "2e9cb0aa71a386a91f73f7088d115c0d96654829" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + - platform: android + create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + - platform: ios + create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/mobile_app/analysis_options.yaml b/mobile_app/analysis_options.yaml new file mode 100644 index 0000000..f337e6a --- /dev/null +++ b/mobile_app/analysis_options.yaml @@ -0,0 +1,182 @@ +include: package:lints/recommended.yaml +analyzer: + exclude: + - lib/data/data_source/remote/*.g.dart + - lib/data/models/request/*.g.dart + - lib/data/models/response/*.g.dart + +linter: + rules: + - always_declare_return_types + - always_use_package_imports + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_catching_errors + - avoid_double_and_int_checks + - avoid_dynamic_calls + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cast_nullable_to_non_nullable + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - deprecated_consistency + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - collection_methods_unrelated_type + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - library_private_types_in_public_api + - lines_longer_than_80_chars + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_logic_in_create_state + - no_runtimeType_toString + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_null_aware_method_calls + - prefer_null_aware_operators + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - recursive_getters + - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_constructor_name + - unnecessary_getters_setters + - unnecessary_late + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - unrelated_type_equality_checks + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks diff --git a/mobile_app/android/.gitignore b/mobile_app/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/mobile_app/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/mobile_app/android/app/build.gradle b/mobile_app/android/app/build.gradle new file mode 100644 index 0000000..9072968 --- /dev/null +++ b/mobile_app/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.mobile_app" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + 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.mobile_app" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 21 + targetSdkVersion flutter.targetSdkVersion + 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 {} diff --git a/mobile_app/android/app/src/debug/AndroidManifest.xml b/mobile_app/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/mobile_app/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/mobile_app/android/app/src/main/AndroidManifest.xml b/mobile_app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..15f6f74 --- /dev/null +++ b/mobile_app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile_app/android/app/src/main/kotlin/com/example/mobile_app/MainActivity.kt b/mobile_app/android/app/src/main/kotlin/com/example/mobile_app/MainActivity.kt new file mode 100644 index 0000000..ee1330d --- /dev/null +++ b/mobile_app/android/app/src/main/kotlin/com/example/mobile_app/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.mobile_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/mobile_app/android/app/src/main/res/drawable-v21/launch_background.xml b/mobile_app/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/mobile_app/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mobile_app/android/app/src/main/res/drawable/launch_background.xml b/mobile_app/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/mobile_app/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mobile_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/mobile_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/mobile_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/mobile_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/mobile_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/mobile_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile_app/android/app/src/main/res/values-night/styles.xml b/mobile_app/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/mobile_app/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mobile_app/android/app/src/main/res/values/styles.xml b/mobile_app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/mobile_app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mobile_app/android/app/src/profile/AndroidManifest.xml b/mobile_app/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/mobile_app/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/mobile_app/android/build.gradle b/mobile_app/android/build.gradle new file mode 100644 index 0000000..e83fb5d --- /dev/null +++ b/mobile_app/android/build.gradle @@ -0,0 +1,30 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/mobile_app/android/gradle.properties b/mobile_app/android/gradle.properties new file mode 100644 index 0000000..598d13f --- /dev/null +++ b/mobile_app/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/mobile_app/android/gradle/wrapper/gradle-wrapper.properties b/mobile_app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/mobile_app/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/mobile_app/android/settings.gradle b/mobile_app/android/settings.gradle new file mode 100644 index 0000000..7cd7128 --- /dev/null +++ b/mobile_app/android/settings.gradle @@ -0,0 +1,29 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false +} + +include ":app" diff --git a/mobile_app/assets/arrow-down.svg b/mobile_app/assets/arrow-down.svg new file mode 100644 index 0000000..e06680a --- /dev/null +++ b/mobile_app/assets/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile_app/assets/arrow-up.svg b/mobile_app/assets/arrow-up.svg new file mode 100644 index 0000000..eb3fc47 --- /dev/null +++ b/mobile_app/assets/arrow-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile_app/assets/blood.svg b/mobile_app/assets/blood.svg new file mode 100644 index 0000000..fff128c --- /dev/null +++ b/mobile_app/assets/blood.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile_app/assets/fork_knife.svg b/mobile_app/assets/fork_knife.svg new file mode 100644 index 0000000..8b62989 --- /dev/null +++ b/mobile_app/assets/fork_knife.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile_app/assets/glucose.svg b/mobile_app/assets/glucose.svg new file mode 100644 index 0000000..79fbf0b --- /dev/null +++ b/mobile_app/assets/glucose.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile_app/assets/middle.svg b/mobile_app/assets/middle.svg new file mode 100644 index 0000000..3cdc512 --- /dev/null +++ b/mobile_app/assets/middle.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile_app/assets/needle.svg b/mobile_app/assets/needle.svg new file mode 100644 index 0000000..89b0f5d --- /dev/null +++ b/mobile_app/assets/needle.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile_app/assets/run.svg b/mobile_app/assets/run.svg new file mode 100644 index 0000000..eea3fe3 --- /dev/null +++ b/mobile_app/assets/run.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile_app/assets/variability.svg b/mobile_app/assets/variability.svg new file mode 100644 index 0000000..afbdd47 --- /dev/null +++ b/mobile_app/assets/variability.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile_app/devtools_options.yaml b/mobile_app/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/mobile_app/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/mobile_app/ios/.gitignore b/mobile_app/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/mobile_app/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.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/mobile_app/ios/Flutter/AppFrameworkInfo.plist b/mobile_app/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/mobile_app/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 + 11.0 + + diff --git a/mobile_app/ios/Flutter/Debug.xcconfig b/mobile_app/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/mobile_app/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/mobile_app/ios/Flutter/Release.xcconfig b/mobile_app/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/mobile_app/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/mobile_app/ios/Runner.xcodeproj/project.pbxproj b/mobile_app/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..7039396 --- /dev/null +++ b/mobile_app/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,614 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + 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 */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* 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 = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + 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 */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807E294A63A400263BE5 /* Frameworks */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 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 = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 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 */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 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; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + 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; + alwaysOutOfDate = 1; + 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 */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency 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 = 11.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.mobileApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + 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 = 11.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 = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + 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.mobileApp; + 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.mobileApp; + 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 */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 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/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/mobile_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/mobile_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..87131a0 --- /dev/null +++ b/mobile_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile_app/ios/Runner.xcworkspace/contents.xcworkspacedata b/mobile_app/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/mobile_app/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/mobile_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/mobile_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/mobile_app/ios/Runner/AppDelegate.swift b/mobile_app/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/mobile_app/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/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/mobile_app/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/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/mobile_app/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/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/mobile_app/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/mobile_app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/mobile_app/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/mobile_app/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile_app/ios/Runner/Base.lproj/Main.storyboard b/mobile_app/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/mobile_app/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile_app/ios/Runner/Info.plist b/mobile_app/ios/Runner/Info.plist new file mode 100644 index 0000000..1ba2500 --- /dev/null +++ b/mobile_app/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Mobile App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + mobile_app + 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 + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/mobile_app/ios/Runner/Runner-Bridging-Header.h b/mobile_app/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/mobile_app/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/mobile_app/ios/RunnerTests/RunnerTests.swift b/mobile_app/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/mobile_app/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/mobile_app/lib/config/colors.dart b/mobile_app/lib/config/colors.dart new file mode 100644 index 0000000..3221985 --- /dev/null +++ b/mobile_app/lib/config/colors.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +const green5 = Color(0xFFEAF8E4); +const green30 = Color(0xFF66C64F); +const green50 = Color(0xFF009E00); +const base50 = Color(0xFF5D5D5D); +const shadowColor = Color(0x40000000); +const base30 = Color(0xFF888888); +const base40 = Color(0xFF6D6D6D); +const base70 = Color(0xFF454545); +const base80 = Color(0xFF383838); +const base90 = Color(0xFF212121); diff --git a/mobile_app/lib/config/const.dart b/mobile_app/lib/config/const.dart new file mode 100644 index 0000000..761037e --- /dev/null +++ b/mobile_app/lib/config/const.dart @@ -0,0 +1,11 @@ +const logoSvg = 'assets/glucose.svg'; +const arrowUpSvg = 'assets/arrow-up.svg'; +const arrowDownSvg = 'assets/arrow-down.svg'; +const middle = 'assets/middle.svg'; +const variability = 'assets/variability.svg'; +const bloodSvg = 'assets/blood.svg'; +const activitySvg = 'assets/run.svg'; +const injectionSvg = 'assets/needle.svg'; +const foodSvg = 'assets/fork_knife.svg'; +const baseUrl = 'http://31.129.107.206:5000'; +const mockBleDevice = 'mockBleDevice'; diff --git a/mobile_app/lib/config/navigation.dart b/mobile_app/lib/config/navigation.dart new file mode 100644 index 0000000..09e1bd2 --- /dev/null +++ b/mobile_app/lib/config/navigation.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/routes.dart'; +import 'package:mobile_app/data/repository/auth_repository.dart'; +import 'package:mobile_app/presentation/bluetooth/cubit/bluetooth_cubit.dart'; +import 'package:mobile_app/presentation/bluetooth/screen.dart'; +import 'package:mobile_app/presentation/login/cubit/login_cubit.dart'; +import 'package:mobile_app/presentation/login/login_page.dart'; +import 'package:mobile_app/presentation/main/cubit/home_page_cubit.dart'; +import 'package:mobile_app/presentation/main/main_page.dart'; +import 'package:mobile_app/presentation/registration/cubit/registration_cubit.dart'; +import 'package:mobile_app/presentation/registration/registration_page.dart'; +import 'package:mobile_app/presentation/splash/cubit/splash_cubit.dart'; +import 'package:mobile_app/presentation/splash/splash_screen.dart'; + +class MainNavigation { + static Route onGenerateRoute(RouteSettings settings) { + Widget page; + switch (settings.name) { + case Routes.splash: + page = BlocProvider( + create: (_) => SplashCubit(), + child: const SplashScreen(), + ); + case Routes.auth: + page = BlocProvider( + create: (_) => LoginCubit(authRepo: IAuthRepository()), + child: const LoginPage(), + ); + case Routes.main: + page = BlocProvider( + create: (_) => HomePageCubit(), + child: const MainPageWidget(), + ); + case Routes.registration: + page = BlocProvider( + create: (_) => RegistrationCubit( + authRepo: IAuthRepository(), + ), + child: const RegistrationPage(), + ); + case Routes.bluetooth: + page = BlocProvider( + create: (context) => BluetoothCubit(), + child: const BluetoothScreen(), + ); + default: + page = const Scaffold( + body: Center( + child: Text('error'), + ), + ); + } + return MaterialPageRoute(builder: (context) => page); + } +} diff --git a/mobile_app/lib/config/routes.dart b/mobile_app/lib/config/routes.dart new file mode 100644 index 0000000..b3cc653 --- /dev/null +++ b/mobile_app/lib/config/routes.dart @@ -0,0 +1,7 @@ +abstract class Routes { + static const splash = '/'; + static const auth = '/auth'; + static const main = '/main'; + static const registration = '/registration'; + static const bluetooth = '$main/bluetooth'; +} diff --git a/mobile_app/lib/config/styles.dart b/mobile_app/lib/config/styles.dart new file mode 100644 index 0000000..1ca012b --- /dev/null +++ b/mobile_app/lib/config/styles.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +const h4 = TextStyle( + fontSize: 26, + fontWeight: FontWeight.w500, +); + +const captionRegular = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, +); + +const bodyMRegular = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, +); + +const bodySMedium = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, +); + +const bodyMMedium = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, +); + +const bodySRegular = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, +); + diff --git a/mobile_app/lib/config/theme.dart b/mobile_app/lib/config/theme.dart new file mode 100644 index 0000000..cbb5c3e --- /dev/null +++ b/mobile_app/lib/config/theme.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; + +final lightTheme = ThemeData( + brightness: Brightness.light, + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Colors.white, + ), + scaffoldBackgroundColor: Colors.white, + appBarTheme: const AppBarTheme(backgroundColor: Colors.white), + colorSchemeSeed: green50, + textTheme: textLightTheme, +); + +final textLightTheme = TextTheme( + headlineMedium: h4, + bodyMedium: bodyMRegular.copyWith(color: base90), + bodySmall: bodyMRegular.copyWith(color: base90), + displaySmall: captionRegular.copyWith(color: base90), + bodyLarge: bodyMMedium.copyWith(color: base90), +); + +final darkTheme = ThemeData( + brightness: Brightness.dark, + colorSchemeSeed: green50, +); + +extension IsLight on ThemeData { + bool isLight() => brightness == Brightness.light; +} diff --git a/mobile_app/lib/config/utils/month_converter.dart b/mobile_app/lib/config/utils/month_converter.dart new file mode 100644 index 0000000..e24a03f --- /dev/null +++ b/mobile_app/lib/config/utils/month_converter.dart @@ -0,0 +1,30 @@ +String getMonthByNumber(int monthNumber) { + switch (monthNumber) { + case 1: + return 'January'; + case 2: + return 'February'; + case 3: + return 'March'; + case 4: + return 'April'; + case 5: + return 'May'; + case 6: + return 'June'; + case 7: + return 'July'; + case 8: + return 'August'; + case 9: + return 'September'; + case 10: + return 'October'; + case 11: + return 'November'; + case 12: + return 'December'; + default: + return ''; + } +} \ No newline at end of file diff --git a/mobile_app/lib/data/data_source/local/database.dart b/mobile_app/lib/data/data_source/local/database.dart new file mode 100644 index 0000000..a7b6138 --- /dev/null +++ b/mobile_app/lib/data/data_source/local/database.dart @@ -0,0 +1,53 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:mobile_app/data/data_source/local/tables/glucose_events.dart'; +import 'package:mobile_app/data/data_source/local/tables/measurements_table.dart'; +import 'package:mobile_app/data/models/db/glucose_event.dart'; +import 'package:mobile_app/data/models/db/measurement.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; + +part 'database.g.dart'; + +@DriftDatabase(tables: [GlucoseMeasuremnts, GlucoseEvents]) +class AppDatabase extends _$AppDatabase { + AppDatabase() : super(_openConnection()); + + @override + int get schemaVersion => 2; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (m) async { + await m.createAll(); + }, + onUpgrade: (m, from, to) async { + if(from < 2){ + await m.createTable(glucoseEvents); + } + }, + ); + } +} + +LazyDatabase _openConnection() { + return LazyDatabase(() async { + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(join(dbFolder.path, 'db.sqlite')); + + if (Platform.isAndroid) { + await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); + } + + final cachebase = (await getTemporaryDirectory()).path; + + sqlite3.tempDirectory = cachebase; + + return NativeDatabase.createInBackground(file); + }); +} diff --git a/mobile_app/lib/data/data_source/local/shared_prefs/shared_prefs.dart b/mobile_app/lib/data/data_source/local/shared_prefs/shared_prefs.dart new file mode 100644 index 0000000..0211dce --- /dev/null +++ b/mobile_app/lib/data/data_source/local/shared_prefs/shared_prefs.dart @@ -0,0 +1,13 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPreferencesManager { + static const _isDeviceConnected = 'isDeviceConnected'; + + final SharedPreferences _prefs; + + SharedPreferencesManager(this._prefs); + + bool? get isDeviceConnected => _prefs.getBool(_isDeviceConnected); + + Future setIsConnected(bool value) async => _prefs.setBool(_isDeviceConnected, value); +} \ No newline at end of file diff --git a/mobile_app/lib/data/data_source/local/tables/glucose_events.dart b/mobile_app/lib/data/data_source/local/tables/glucose_events.dart new file mode 100644 index 0000000..6de80df --- /dev/null +++ b/mobile_app/lib/data/data_source/local/tables/glucose_events.dart @@ -0,0 +1,9 @@ +import 'package:drift/drift.dart'; + +class GlucoseEvents extends Table { + IntColumn get id => integer().autoIncrement()(); + RealColumn get value => real()(); + DateTimeColumn get createdAt => dateTime()(); + TextColumn get description => text()(); + IntColumn get eventEnumIndex => integer()(); +} \ No newline at end of file diff --git a/mobile_app/lib/data/data_source/local/tables/measurements_table.dart b/mobile_app/lib/data/data_source/local/tables/measurements_table.dart new file mode 100644 index 0000000..d363c57 --- /dev/null +++ b/mobile_app/lib/data/data_source/local/tables/measurements_table.dart @@ -0,0 +1,7 @@ +import 'package:drift/drift.dart'; + +class GlucoseMeasuremnts extends Table { + IntColumn get id => integer().autoIncrement()(); + RealColumn get value => real()(); + DateTimeColumn get createdAt => dateTime()(); +} \ No newline at end of file diff --git a/mobile_app/lib/data/data_source/mock/mock_ble_device.dart b/mobile_app/lib/data/data_source/mock/mock_ble_device.dart new file mode 100644 index 0000000..320cd5f --- /dev/null +++ b/mobile_app/lib/data/data_source/mock/mock_ble_device.dart @@ -0,0 +1,24 @@ +import 'dart:math'; + +class MockBluetoothDevice { + static bool isConnected = false; + static final _instance = MockBluetoothDevice._(); + + factory MockBluetoothDevice() => _instance; + + MockBluetoothDevice._(); + + double lastValue = 3; + Stream get dataStream { + isConnected = true; + return Stream.periodic(const Duration(seconds: 30), (i) => _getNewValue()); + } + + double _getNewValue() { + final sign = Random().nextBool() ? 1 : -1; + lastValue += sign * 0.05; + if (lastValue > 6) lastValue = 5.9; + if (lastValue < 0) lastValue = 0.1; + return lastValue; + } +} diff --git a/mobile_app/lib/data/data_source/remote/auth_data_source.dart b/mobile_app/lib/data/data_source/remote/auth_data_source.dart new file mode 100644 index 0000000..19c38aa --- /dev/null +++ b/mobile_app/lib/data/data_source/remote/auth_data_source.dart @@ -0,0 +1,20 @@ +import 'package:dio/dio.dart'; +import 'package:mobile_app/data/models/request/login_data.dart'; +import 'package:mobile_app/data/models/request/registration_data.dart'; +import 'package:mobile_app/data/models/response/login_data.dart'; +import 'package:mobile_app/data/models/response/registration_data.dart'; +import 'package:retrofit/http.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'auth_data_source.g.dart'; + +@RestApi() +abstract class AuthClient { + factory AuthClient(Dio dio, {String baseUrl}) = _AuthClient; + + @POST('/register') + Future register(@Body() RegistrationData data); + + @POST('/login') + Future login(@Body() LoginData data); +} diff --git a/mobile_app/lib/data/models/db/glucose_event.dart b/mobile_app/lib/data/models/db/glucose_event.dart new file mode 100644 index 0000000..c51aa48 --- /dev/null +++ b/mobile_app/lib/data/models/db/glucose_event.dart @@ -0,0 +1,114 @@ +import 'package:drift/drift.dart'; +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/data/data_source/local/database.dart'; +import 'package:mobile_app/domain/glucose_event_types.dart'; + +class GlucoseEvent extends DataClass implements Insertable { + final int id; + final double value; + final DateTime createdAt; + final String description; + final int eventEnumIndex; + const GlucoseEvent( + {required this.id, + required this.value, + required this.createdAt, + required this.description, + required this.eventEnumIndex}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['value'] = Variable(value); + map['created_at'] = Variable(createdAt); + map['description'] = Variable(description); + map['event_enum_index'] = Variable(eventEnumIndex); + return map; + } + + GlucoseEventsCompanion toCompanion(bool nullToAbsent) { + return GlucoseEventsCompanion( + id: Value(id), + value: Value(value), + createdAt: Value(createdAt), + description: Value(description), + eventEnumIndex: Value(eventEnumIndex), + ); + } + + factory GlucoseEvent.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GlucoseEvent( + id: serializer.fromJson(json['id']), + value: serializer.fromJson(json['value']), + createdAt: serializer.fromJson(json['createdAt']), + description: serializer.fromJson(json['description']), + eventEnumIndex: serializer.fromJson(json['eventEnumIndex']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'value': serializer.toJson(value), + 'createdAt': serializer.toJson(createdAt), + 'description': serializer.toJson(description), + 'eventEnumIndex': serializer.toJson(eventEnumIndex), + }; + } + + GlucoseEvent copyWith( + {int? id, + double? value, + DateTime? createdAt, + String? description, + int? eventEnumIndex}) => + GlucoseEvent( + id: id ?? this.id, + value: value ?? this.value, + createdAt: createdAt ?? this.createdAt, + description: description ?? this.description, + eventEnumIndex: eventEnumIndex ?? this.eventEnumIndex, + ); + @override + String toString() { + return (StringBuffer('GlucoseEvent(') + ..write('id: $id, ') + ..write('value: $value, ') + ..write('createdAt: $createdAt, ') + ..write('description: $description, ') + ..write('eventEnumIndex: $eventEnumIndex') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, value, createdAt, description, eventEnumIndex); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GlucoseEvent && + other.id == this.id && + other.value == this.value && + other.createdAt == this.createdAt && + other.description == this.description && + other.eventEnumIndex == this.eventEnumIndex); + + String get iconPath { + if (eventEnumIndex == GlucoseEventType.food.index) { + return foodSvg; + } else if (eventEnumIndex == GlucoseEventType.insulin.index) { + return injectionSvg; + } else if (eventEnumIndex == GlucoseEventType.food.index) { + return foodSvg; + } else { + return activitySvg; + } + } + + String get time => + '${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}'; +} diff --git a/mobile_app/lib/data/models/db/measurement.dart b/mobile_app/lib/data/models/db/measurement.dart new file mode 100644 index 0000000..15c59d8 --- /dev/null +++ b/mobile_app/lib/data/models/db/measurement.dart @@ -0,0 +1,72 @@ +import 'package:drift/drift.dart'; +import 'package:mobile_app/data/data_source/local/database.dart'; + +class GlucoseMeasuremnt extends DataClass + implements Insertable { + final int id; + final double value; + final DateTime createdAt; + const GlucoseMeasuremnt( + {required this.id, required this.value, required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['value'] = Variable(value); + map['created_at'] = Variable(createdAt); + return map; + } + + GlucoseMeasuremntsCompanion toCompanion(bool nullToAbsent) { + return GlucoseMeasuremntsCompanion( + id: Value(id), + value: Value(value), + createdAt: Value(createdAt), + ); + } + + factory GlucoseMeasuremnt.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GlucoseMeasuremnt( + id: serializer.fromJson(json['id']), + value: serializer.fromJson(json['value']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'value': serializer.toJson(value), + 'createdAt': serializer.toJson(createdAt), + }; + } + + GlucoseMeasuremnt copyWith({int? id, double? value, DateTime? createdAt}) => + GlucoseMeasuremnt( + id: id ?? this.id, + value: value ?? this.value, + createdAt: createdAt ?? this.createdAt, + ); + @override + String toString() { + return (StringBuffer('GlucoseMeasuremnt(') + ..write('id: $id, ') + ..write('value: $value, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, value, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GlucoseMeasuremnt && + other.id == this.id && + other.value == this.value && + other.createdAt == this.createdAt); +} diff --git a/mobile_app/lib/data/models/request/login_data.dart b/mobile_app/lib/data/models/request/login_data.dart new file mode 100644 index 0000000..e55490c --- /dev/null +++ b/mobile_app/lib/data/models/request/login_data.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'login_data.g.dart'; + +@JsonSerializable() +class LoginData { + final String username; + final String password; + + LoginData({ + required this.username, + required this.password, + }); + + Map toJson() => _$LoginDataToJson(this); + + factory LoginData.fromJson(Map json) => + _$LoginDataFromJson(json); +} diff --git a/mobile_app/lib/data/models/request/registration_data.dart b/mobile_app/lib/data/models/request/registration_data.dart new file mode 100644 index 0000000..3bcf818 --- /dev/null +++ b/mobile_app/lib/data/models/request/registration_data.dart @@ -0,0 +1,20 @@ + +import 'package:json_annotation/json_annotation.dart'; + +part 'registration_data.g.dart'; + +@JsonSerializable() +class RegistrationData { + final String username; + final String password; + + RegistrationData({ + required this.username, + required this.password, + }); + + Map toJson() => _$RegistrationDataToJson(this); + + factory RegistrationData.fromJson(Map json) => + _$RegistrationDataFromJson(json); +} diff --git a/mobile_app/lib/data/models/response/login_data.dart b/mobile_app/lib/data/models/response/login_data.dart new file mode 100644 index 0000000..ac61c71 --- /dev/null +++ b/mobile_app/lib/data/models/response/login_data.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'login_data.g.dart'; + +@JsonSerializable() +class LoginDataResponse{ + final String message; + + LoginDataResponse({required this.message}); + + Map toJson() => _$LoginDataResponseToJson(this); + + factory LoginDataResponse.fromJson(Map json) => + _$LoginDataResponseFromJson(json); +} diff --git a/mobile_app/lib/data/models/response/registration_data.dart b/mobile_app/lib/data/models/response/registration_data.dart new file mode 100644 index 0000000..46331cd --- /dev/null +++ b/mobile_app/lib/data/models/response/registration_data.dart @@ -0,0 +1,13 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'registration_data.g.dart'; + +@JsonSerializable(createToJson: false) +class RegistrationDataResponse{ + final String message; + + RegistrationDataResponse({required this.message}); + + factory RegistrationDataResponse.fromJson(Map json) => + _$RegistrationDataResponseFromJson(json); +} diff --git a/mobile_app/lib/data/repository/auth_repository.dart b/mobile_app/lib/data/repository/auth_repository.dart new file mode 100644 index 0000000..50ee1af --- /dev/null +++ b/mobile_app/lib/data/repository/auth_repository.dart @@ -0,0 +1,39 @@ +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:mobile_app/data/data_source/remote/auth_data_source.dart'; +import 'package:mobile_app/data/models/request/login_data.dart'; +import 'package:mobile_app/data/models/request/registration_data.dart'; +import 'package:mobile_app/data/models/response/login_data.dart'; +import 'package:mobile_app/data/models/response/registration_data.dart'; +import 'package:mobile_app/di/di_container.dart'; +import 'package:mobile_app/domain/repository/auth_repository.dart'; + +class IAuthRepository implements AuthRepository { + final client = AuthClient(sl()); + + @override + Future> login(LoginData data) async { + try { + final response = await client.login(data); + return right(response); + } catch (exception) { + if(exception is DioException){ + final error = exception.response?.data['error']; + print(error); + return left(UnitMonoid().zero()); + } + return left(UnitMonoid().zero()); + } + } + + @override + Future> register( + RegistrationData data) async { + try { + final response = await client.register(data); + return right(response); + } catch (exception) { + return left(UnitMonoid().zero()); + } + } +} diff --git a/mobile_app/lib/data/repository/ble_device_repo.dart b/mobile_app/lib/data/repository/ble_device_repo.dart new file mode 100644 index 0000000..eadbd59 --- /dev/null +++ b/mobile_app/lib/data/repository/ble_device_repo.dart @@ -0,0 +1,20 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first + +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:mobile_app/domain/repository/ble_device_repo.dart'; + +class IBLEDeviceRepository extends BLEDeviceRepository { + + final _bleStateStream = FlutterBluePlus.adapterState; + + @override + void setValueStream(Stream valueStream) { + super.setValueStream(valueStream); + _bleStateStream.listen((status) { + if (status == BluetoothAdapterState.off) { + connectionStateController.sink.add(false); + } + }); + } + +} diff --git a/mobile_app/lib/data/repository/glucose_event_repo.dart b/mobile_app/lib/data/repository/glucose_event_repo.dart new file mode 100644 index 0000000..b20ff9b --- /dev/null +++ b/mobile_app/lib/data/repository/glucose_event_repo.dart @@ -0,0 +1,41 @@ +import 'package:dartz/dartz.dart'; +import 'package:mobile_app/data/data_source/local/database.dart'; +import 'package:mobile_app/data/models/db/glucose_event.dart'; +import 'package:mobile_app/domain/repository/local/glucose_events_repository.dart'; + +class IGlucoseEventsRepository implements GlucoseEventsRepository { + final AppDatabase _database; + + IGlucoseEventsRepository({required AppDatabase database}) + : _database = database; + + @override + Future addClucoseEvent( + {required double measurementValue, + required DateTime measurementDate, + required String descripton, + required int eventTypeIndex}) async { + try { + await _database.managers.glucoseEvents.create((o) => o( + createdAt: measurementDate, + value: measurementValue, + description: descripton, + eventEnumIndex: eventTypeIndex)); + return true; + } catch (e) { + return false; + } + } + + @override + Future>> getGlucoseEvents() async { + try { + final glucoseEvents = await _database.managers.glucoseEvents.orderBy( + (o) => o.createdAt.desc(), + ).get(); + return right(glucoseEvents); + } catch (e) { + return left(UnitMonoid().zero()); + } + } +} diff --git a/mobile_app/lib/data/repository/measurements_repository.dart b/mobile_app/lib/data/repository/measurements_repository.dart new file mode 100644 index 0000000..744bb8e --- /dev/null +++ b/mobile_app/lib/data/repository/measurements_repository.dart @@ -0,0 +1,43 @@ +import 'package:dartz/dartz.dart'; +import 'package:drift/drift.dart'; +import 'package:mobile_app/data/data_source/local/database.dart'; +import 'package:mobile_app/data/models/db/measurement.dart'; +import 'package:mobile_app/domain/repository/local/glucose_data_repository.dart'; + +class GlucoseDataRepository implements IGlucoseDataRepository { + final AppDatabase _database; + + GlucoseDataRepository({required AppDatabase database}) : _database = database; + + @override + Future>> getGlucoseDataForDay( + {DateTime? date}) async { + date ??= DateTime.now(); + try { + final glucoseData = await _database.managers.glucoseMeasuremnts + .filter((f) => f.createdAt.isBetween( + DateTime(date!.year, date.month, date.day), + DateTime(date.year, date.month, date.day + 1), + )) + .get(); + return right(glucoseData); + } catch (e) { + return left(UnitMonoid().zero()); + } + } + + @override + Future addClucoseMeasurement( + {required double measurementValue, + required DateTime measurementDate}) async { + try { + await _database.managers.glucoseMeasuremnts.create((o) => o( + createdAt: measurementDate, + value: measurementValue, + )); + return true; + } catch (e) { + return false; + } + } +} diff --git a/mobile_app/lib/di/di_container.dart b/mobile_app/lib/di/di_container.dart new file mode 100644 index 0000000..8df5eec --- /dev/null +++ b/mobile_app/lib/di/di_container.dart @@ -0,0 +1,45 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/data/data_source/local/database.dart'; +import 'package:mobile_app/data/data_source/local/shared_prefs/shared_prefs.dart'; +import 'package:mobile_app/data/repository/ble_device_repo.dart'; +import 'package:mobile_app/data/repository/glucose_event_repo.dart'; +import 'package:mobile_app/data/repository/measurements_repository.dart'; +import 'package:mobile_app/domain/repository/ble_device_repo.dart'; +import 'package:mobile_app/domain/repository/local/glucose_events_repository.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final sl = GetIt.instance; +final regLs = sl.registerLazySingleton; +final regS = sl.registerSingleton; + +Future init() async { + final sharedPrefs = await SharedPreferences.getInstance(); + + regLs( + () => SharedPreferencesManager(sharedPrefs), + ); + + regLs(() => AppDatabase()); + + regLs( + () => Dio() + ..options.baseUrl = baseUrl + ..options.headers['Content-Type'] = 'application/json', + ); + + regLs( + () => GlucoseDataRepository(database: sl()), + ); + + regLs( + () => IBLEDeviceRepository(), + ); + + regLs( + () => IGlucoseEventsRepository( + database: sl(), + ), + ); +} diff --git a/mobile_app/lib/domain/entities/glucose_evens_for_day.dart b/mobile_app/lib/domain/entities/glucose_evens_for_day.dart new file mode 100644 index 0000000..bc3459f --- /dev/null +++ b/mobile_app/lib/domain/entities/glucose_evens_for_day.dart @@ -0,0 +1,11 @@ +import 'package:mobile_app/data/models/db/glucose_event.dart'; + +class GlucoseEventsPerDay { + final DateTime date; + final List events; + + GlucoseEventsPerDay({ + required this.date, + required this.events, + }); +} diff --git a/mobile_app/lib/domain/glucose_event_types.dart b/mobile_app/lib/domain/glucose_event_types.dart new file mode 100644 index 0000000..28d5612 --- /dev/null +++ b/mobile_app/lib/domain/glucose_event_types.dart @@ -0,0 +1,13 @@ + +enum GlucoseEventType { + sport('Sport', 'assets/run.svg'), + insulin('Insulin','assets/needle.svg'), + food('Food','assets/fork_knife.svg'); + + const GlucoseEventType(this.title, this.iconPath); + final String title; + final String iconPath; +} + + + diff --git a/mobile_app/lib/domain/repository/auth_repository.dart b/mobile_app/lib/domain/repository/auth_repository.dart new file mode 100644 index 0000000..044e9d4 --- /dev/null +++ b/mobile_app/lib/domain/repository/auth_repository.dart @@ -0,0 +1,11 @@ +import 'package:dartz/dartz.dart'; +import 'package:mobile_app/data/models/request/login_data.dart'; +import 'package:mobile_app/data/models/request/registration_data.dart'; +import 'package:mobile_app/data/models/response/login_data.dart'; +import 'package:mobile_app/data/models/response/registration_data.dart'; + +abstract interface class AuthRepository{ + Future> register(RegistrationData data); + + Future> login(LoginData data); +} diff --git a/mobile_app/lib/domain/repository/ble_device_repo.dart b/mobile_app/lib/domain/repository/ble_device_repo.dart new file mode 100644 index 0000000..d82ee35 --- /dev/null +++ b/mobile_app/lib/domain/repository/ble_device_repo.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +abstract class BLEDeviceRepository { + Stream? valueStream; + + Stream get isConnected => connectionStateController.stream; + + @protected + final connectionStateController = StreamController(); + + void setValueStream(Stream valueStream) { + this.valueStream = valueStream; + connectionStateController.sink.add(true); + } + +} diff --git a/mobile_app/lib/domain/repository/local/glucose_data_repository.dart b/mobile_app/lib/domain/repository/local/glucose_data_repository.dart new file mode 100644 index 0000000..89dfa25 --- /dev/null +++ b/mobile_app/lib/domain/repository/local/glucose_data_repository.dart @@ -0,0 +1,10 @@ +import 'package:dartz/dartz.dart'; +import 'package:mobile_app/data/models/db/measurement.dart'; + +abstract interface class IGlucoseDataRepository { + Future>> getGlucoseDataForDay( + {DateTime? date}); + + Future addClucoseMeasurement( + {required double measurementValue, required DateTime measurementDate}); +} diff --git a/mobile_app/lib/domain/repository/local/glucose_events_repository.dart b/mobile_app/lib/domain/repository/local/glucose_events_repository.dart new file mode 100644 index 0000000..2553d67 --- /dev/null +++ b/mobile_app/lib/domain/repository/local/glucose_events_repository.dart @@ -0,0 +1,13 @@ +import 'package:dartz/dartz.dart'; +import 'package:mobile_app/data/models/db/glucose_event.dart'; + +abstract interface class GlucoseEventsRepository { + Future>> getGlucoseEvents(); + + Future addClucoseEvent({ + required double measurementValue, + required DateTime measurementDate, + required String descripton, + required int eventTypeIndex, + }); +} diff --git a/mobile_app/lib/main.dart b/mobile_app/lib/main.dart new file mode 100644 index 0000000..0d9ac97 --- /dev/null +++ b/mobile_app/lib/main.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/navigation.dart'; +import 'package:mobile_app/config/routes.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/theme/theme_cubit.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ThemeCubit(), + child: BlocBuilder( + builder: (context, state) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: + state.brightness == Brightness.light ? lightTheme : darkTheme, + initialRoute: Routes.splash, + onGenerateRoute: MainNavigation.onGenerateRoute, + ); + }, + ), + ); + } +} diff --git a/mobile_app/lib/managers/foreground_service/task_handler.dart b/mobile_app/lib/managers/foreground_service/task_handler.dart new file mode 100644 index 0000000..bf6d307 --- /dev/null +++ b/mobile_app/lib/managers/foreground_service/task_handler.dart @@ -0,0 +1,64 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:async'; +import 'dart:isolate'; + +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; + +import 'package:mobile_app/config/routes.dart'; +import 'package:mobile_app/data/data_source/local/database.dart'; +import 'package:mobile_app/data/data_source/mock/mock_ble_device.dart'; +import 'package:mobile_app/data/repository/measurements_repository.dart'; + +class FirstTaskHandler extends TaskHandler { + SendPort? _sendPort; + StreamSubscription? streamSubscription; + final measurementsTableRepo = GlucoseDataRepository(database: AppDatabase()); + // Called when the task is started. + @override + void onStart(DateTime timestamp, SendPort? sendPort) async { + final mockBluetoothDevice = MockBluetoothDevice(); + _sendPort = sendPort; + + streamSubscription = mockBluetoothDevice.dataStream.listen((value) { + sendPort?.send(value); + measurementsTableRepo.addClucoseMeasurement( + measurementDate: DateTime.now(), + measurementValue: value, + ); + FlutterForegroundTask.updateService( + notificationText: 'Current glucose value: ${value.toStringAsFixed(2)} mmol/L'); + }); + } + + // Called every [interval] milliseconds in [ForegroundTaskOptions]. + @override + void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async { + // Send data to the main isolate. + sendPort?.send('sent data'); + } + + // Called when the notification button on the Android platform is pressed. + @override + void onDestroy(DateTime timestamp, SendPort? sendPort) async { + await streamSubscription?.cancel(); + } + + // Called when the notification button on the Android platform is pressed. + @override + void onNotificationButtonPressed(String id) { + print('onNotificationButtonPressed >> $id'); + } + + // Called when the notification itself on the Android platform is pressed. + // + // "android.permission.SYSTEM_ALERT_WINDOW" permission must be granted for + // this function to be called. + @override + void onNotificationPressed() { + // Note that the app will only route to "/resume-route" when it is exited so + // it will usually be necessary to send a message through the send port to + // signal it to restore state when the app is already started. + FlutterForegroundTask.launchApp(Routes.splash); + _sendPort?.send('onNotificationPressed'); + } +} diff --git a/mobile_app/lib/presentation/add_note/add_note_page.dart b/mobile_app/lib/presentation/add_note/add_note_page.dart new file mode 100644 index 0000000..b4fa08b --- /dev/null +++ b/mobile_app/lib/presentation/add_note/add_note_page.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/domain/glucose_event_types.dart'; +import 'package:mobile_app/presentation/add_note/cubit/add_note_cubit.dart'; +import 'package:mobile_app/presentation/components/custom_button.dart'; +import 'package:mobile_app/presentation/components/custom_text_field.dart'; + +class AddNotePage extends StatelessWidget { + + AddNotePage({super.key}); + + final descriptionController = TextEditingController(); + + final valueController = TextEditingController(); + + final dateController = TextEditingController(); + + final timeController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Type of event', + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base40) + : bodySRegular, + ), + const SizedBox( + height: 4, + ), + DropdownMenu( + width: MediaQuery.of(context).size.width - 16 * 2, + hintText: 'Select an option', + initialSelection: state.selectedGlucoseEventType, + leadingIcon: state.selectedGlucoseEventType?.iconPath == null + ? null + : SvgPicture.asset( + state.selectedGlucoseEventType!.iconPath, + fit: BoxFit.scaleDown, + ), + dropdownMenuEntries: GlucoseEventType.values + .map( + (eventType) => DropdownMenuEntry( + value: eventType, + label: eventType.title, + leadingIcon: SvgPicture.asset(eventType.iconPath), + ), + ) + .toList(), + onSelected: (value) => context + .read() + .onSelectedEventTypeChange(value), + ), + ], + ), + const SizedBox( + height: 16, + ), + CustomTextField( + title: 'Description', + controller: descriptionController, + ), + const SizedBox( + height: 16, + ), + CustomTextField( + onlyNumbers: true, + title: 'Level of glucose', + controller: valueController, + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: CustomTextField( + onTap: () async { + final date = await showDatePicker( + context: context, + firstDate: DateTime(1900), + lastDate: DateTime(DateTime.now().year + 50), + initialDate: DateTime.now(), + ); + if (date != null) { + dateController.text = + date.toIso8601String().substring(0, 10); + } + }, + title: 'Date', + controller: dateController, + enabled: false, + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: CustomTextField( + onTap: () async { + final time = await showTimePicker( + context: context, initialTime: TimeOfDay.now()); + if (time != null) { + timeController.text = '${time.hour}:${time.minute}'; + } + }, + title: 'Time', + controller: timeController, + enabled: false, + ), + ), + ], + ), + const Spacer(), + Row( + children: [ + CustomButton( + text: 'Cancel', + buttonType: ButtonType.teritary, + onPressed: () => null, + ), + const Spacer(), + CustomButton( + text: 'Add note', + buttonType: ButtonType.primary, + onPressed: () { + final date = DateTime.tryParse( + '${dateController.text} ${timeController.text}:00'); + final value = double.tryParse(valueController.text); + if (date == null || value == null) { + return; + } + context.read().addNote( + value: value, + date: date, + description: descriptionController.text, + eventEnumIndex: + state.selectedGlucoseEventType?.index ?? 0); + }, + ), + ], + ), + ], + ); + }, + ); + } +} diff --git a/mobile_app/lib/presentation/add_note/cubit/add_note_cubit.dart b/mobile_app/lib/presentation/add_note/cubit/add_note_cubit.dart new file mode 100644 index 0000000..050063d --- /dev/null +++ b/mobile_app/lib/presentation/add_note/cubit/add_note_cubit.dart @@ -0,0 +1,29 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:mobile_app/di/di_container.dart'; +import 'package:mobile_app/domain/glucose_event_types.dart'; +import 'package:mobile_app/domain/repository/local/glucose_events_repository.dart'; + +part 'add_note_state.dart'; + +class AddNoteCubit extends Cubit { + final eventsRepo = sl(); + + AddNoteCubit() : super(const AddNoteState()); + + void onSelectedEventTypeChange(GlucoseEventType? pickedEventType) { + emit(state.copyWith(selectedGlucoseEventType: pickedEventType)); + } + + void addNote( + {required double value, + required DateTime date, + required String description, + required int eventEnumIndex}) { + eventsRepo.addClucoseEvent( + measurementValue: value, + measurementDate: date, + descripton: description, + eventTypeIndex: eventEnumIndex); + } +} diff --git a/mobile_app/lib/presentation/add_note/cubit/add_note_state.dart b/mobile_app/lib/presentation/add_note/cubit/add_note_state.dart new file mode 100644 index 0000000..6d3c16c --- /dev/null +++ b/mobile_app/lib/presentation/add_note/cubit/add_note_state.dart @@ -0,0 +1,18 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +part of 'add_note_cubit.dart'; + +@immutable +class AddNoteState { + final GlucoseEventType? selectedGlucoseEventType; + + const AddNoteState({this.selectedGlucoseEventType}); + + AddNoteState copyWith({ + GlucoseEventType? selectedGlucoseEventType, + }) { + return AddNoteState( + selectedGlucoseEventType: + selectedGlucoseEventType ?? this.selectedGlucoseEventType, + ); + } +} diff --git a/mobile_app/lib/presentation/bluetooth/cubit/bluetooth_cubit.dart b/mobile_app/lib/presentation/bluetooth/cubit/bluetooth_cubit.dart new file mode 100644 index 0000000..7a58bb7 --- /dev/null +++ b/mobile_app/lib/presentation/bluetooth/cubit/bluetooth_cubit.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:meta/meta.dart'; +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/data/data_source/mock/mock_ble_device.dart'; +import 'package:mobile_app/di/di_container.dart'; +import 'package:mobile_app/domain/repository/ble_device_repo.dart'; + +part 'bluetooth_state.dart'; + +class BluetoothCubit extends Cubit { + late final StreamSubscription + bluetoothStatusSubscription; + final BLEDeviceRepository bleDeviceRepo = sl(); + BluetoothCubit() : super(BluetoothLoading()) { + checkBluetoothStatus(); + } + + Future checkBluetoothStatus() async { + if (await FlutterBluePlus.isSupported == false) { + return; + } + bluetoothStatusSubscription = FlutterBluePlus.adapterState + .listen((BluetoothAdapterState state) async { + switch (state) { + case BluetoothAdapterState.on: + emit(BluetoothLoading()); + await getAllDevices(); + case BluetoothAdapterState.off: + emit(BluetoothOff()); + case BluetoothAdapterState.unknown: + case BluetoothAdapterState.unavailable: + case BluetoothAdapterState.unauthorized: + case BluetoothAdapterState.turningOn: + case BluetoothAdapterState.turningOff: + } + }); + } + + Future toggleBluetooth({required bool turnedOn}) async { + if (turnedOn) { + await FlutterBluePlus.turnOn(); + } else { + //await FlutterBluePlus.turnOff(); + } + } + + Future connectToBluetoothDevice(BluetoothDevice device) async { + if (device.remoteId.str == mockBleDevice) { + emit(BluetoothLoading()); + final mockDevice = MockBluetoothDevice(); + bleDeviceRepo.setValueStream(mockDevice.dataStream); + await getAllDevices(); + return; + } + emit(BluetoothLoading()); + await device.connect(autoConnect: true); + final services = await device.discoverServices(); + final charactericstics = services.first.characteristics; + final valueStream = charactericstics.first.onValueReceived; + bleDeviceRepo.setValueStream(valueStream); + await getAllDevices(); + } + + Future getAllDevices() async { + final mockDevice = + BluetoothDevice(remoteId: const DeviceIdentifier(mockBleDevice)); + + final pairedDevices = await FlutterBluePlus.bondedDevices; + if (MockBluetoothDevice.isConnected) pairedDevices.add(mockDevice); + emit( + BluetoothOn( + pairedDevices: pairedDevices, + availableDevices: const [], + ), + ); + final subscription = FlutterBluePlus.onScanResults.listen( + (results) { + if (results.isNotEmpty) { + final availableDevices = results.map((e) => e.device).toList(); + if (!MockBluetoothDevice.isConnected) { + availableDevices.insert(0, mockDevice); + } + emit( + BluetoothOn( + pairedDevices: pairedDevices, + availableDevices: availableDevices, + ), + ); + } + }, + onError: (e) => print(e), + ); + FlutterBluePlus.cancelWhenScanComplete(subscription); + await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15)); + } + + @override + Future close() { + bluetoothStatusSubscription.cancel(); + return super.close(); + } +} diff --git a/mobile_app/lib/presentation/bluetooth/cubit/bluetooth_state.dart b/mobile_app/lib/presentation/bluetooth/cubit/bluetooth_state.dart new file mode 100644 index 0000000..0ea59b3 --- /dev/null +++ b/mobile_app/lib/presentation/bluetooth/cubit/bluetooth_state.dart @@ -0,0 +1,29 @@ +part of 'bluetooth_cubit.dart'; + +@immutable +sealed class BluetoothState {} + +final class BluetoothOff extends BluetoothState {} + +final class BluetoothOn extends BluetoothState { + final List pairedDevices; + final List availableDevices; + + BluetoothOn({ + required this.pairedDevices, + required this.availableDevices, + }); + + BluetoothOn copyWith({ + List? pairedDevices, + List? availableDevices, + }) => + BluetoothOn( + pairedDevices: pairedDevices ?? this.pairedDevices, + availableDevices: availableDevices ?? this.availableDevices, + ); +} + +final class BluetoothError extends BluetoothState {} + +final class BluetoothLoading extends BluetoothState {} diff --git a/mobile_app/lib/presentation/bluetooth/screen.dart b/mobile_app/lib/presentation/bluetooth/screen.dart new file mode 100644 index 0000000..f016880 --- /dev/null +++ b/mobile_app/lib/presentation/bluetooth/screen.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:mobile_app/presentation/bluetooth/cubit/bluetooth_cubit.dart'; +import 'package:mobile_app/presentation/bluetooth/widgets/devices_column.dart'; +import 'package:mobile_app/presentation/bluetooth/widgets/toggle_row.dart'; + +class BluetoothScreen extends StatefulWidget { + const BluetoothScreen({super.key}); + + @override + State createState() => _BluetoothScreenState(); +} + +class _BluetoothScreenState extends State { + bool checked = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Bluetooth')), + body: Padding( + padding: const EdgeInsets.all(16), + child: BlocBuilder( + builder: (context, state) { + return switch (state) { + BluetoothOff() => const Column( + children: [ + ToggleRow(), + Expanded( + child: Center( + child: Text('Bluetooth is off'), + ), + ) + ], + ), + BluetoothOn( + pairedDevices: final pairedDevices, + availableDevices: final availableDevices + ) => + Column( + children: [ + const ToggleRow(), + Expanded( + child: ListView( + children: [ + DevicesColumn( + title: 'Paired Devices', + devices: pairedDevices, + ), + DevicesColumn( + canBeConnected: true, + title: 'Available Devices', + devices: + availableDevices, + ), + ], + ), + ), + ], + ), + BluetoothError() => const Center( + child: Text('Что-то пошло не так'), + ), + BluetoothLoading() => const Center( + child: CircularProgressIndicator(), + ), + }; + }), + ), + ); + } +} diff --git a/mobile_app/lib/presentation/bluetooth/widgets/devices_column.dart b/mobile_app/lib/presentation/bluetooth/widgets/devices_column.dart new file mode 100644 index 0000000..6ecab01 --- /dev/null +++ b/mobile_app/lib/presentation/bluetooth/widgets/devices_column.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/presentation/bluetooth/cubit/bluetooth_cubit.dart'; +import 'package:mobile_app/presentation/bluetooth/widgets/devices_row.dart'; + +class DevicesColumn extends StatelessWidget { + final List devices; + final String title; + final bool canBeConnected; + const DevicesColumn({ + super.key, + required this.title, + required this.devices, + this.canBeConnected = false, + }); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).isLight() + ? bodyMMedium.copyWith(color: base90) + : bodyMMedium, + ), + for (final device in devices) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: InkWell( + onTap: canBeConnected + ? () => cubit.connectToBluetoothDevice(device) + : null, + child: BluetoothDeviceRow( + deviceName: device.platformName.isNotEmpty + ? device.platformName + : device.remoteId.str, + connectionState: !device.isConnected && + device.remoteId.str != mockBleDevice + ? 'Disconnected' + : 'Connected', + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile_app/lib/presentation/bluetooth/widgets/devices_row.dart b/mobile_app/lib/presentation/bluetooth/widgets/devices_row.dart new file mode 100644 index 0000000..22f3b4a --- /dev/null +++ b/mobile_app/lib/presentation/bluetooth/widgets/devices_row.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; + +class BluetoothDeviceRow extends StatelessWidget { + final String deviceName; + final String connectionState; + + const BluetoothDeviceRow({ + super.key, + required this.deviceName, + required this.connectionState, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const Icon( + Icons.bluetooth, + color: green50, + size: 24, + ), + const SizedBox( + width: 8, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + deviceName, + style: Theme.of(context).isLight() + ? bodySMedium.copyWith(color: base90) + : bodySMedium, + ), + Text( + connectionState, + style: Theme.of(context).isLight() + ? captionRegular.copyWith(color: base50) + : captionRegular, + ), + ], + ) + ], + ); + } +} diff --git a/mobile_app/lib/presentation/bluetooth/widgets/toggle_row.dart b/mobile_app/lib/presentation/bluetooth/widgets/toggle_row.dart new file mode 100644 index 0000000..7f45d72 --- /dev/null +++ b/mobile_app/lib/presentation/bluetooth/widgets/toggle_row.dart @@ -0,0 +1,36 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/presentation/bluetooth/cubit/bluetooth_cubit.dart'; + +class ToggleRow extends StatelessWidget { + const ToggleRow({super.key}); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Bluetooth', + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base80) + : bodySRegular, + ), + CupertinoSwitch( + value: cubit.state is BluetoothOn, + onChanged: (status) { + cubit.toggleBluetooth(turnedOn: status); + }, + trackColor: base30, + activeColor: green50, + ), + ], + ); + } +} diff --git a/mobile_app/lib/presentation/chart/cubit/chart_cubit.dart b/mobile_app/lib/presentation/chart/cubit/chart_cubit.dart new file mode 100644 index 0000000..d8759b2 --- /dev/null +++ b/mobile_app/lib/presentation/chart/cubit/chart_cubit.dart @@ -0,0 +1,51 @@ +import 'package:bloc/bloc.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:meta/meta.dart'; +import 'package:mobile_app/data/repository/measurements_repository.dart'; +import 'package:mobile_app/di/di_container.dart'; + +part 'chart_state.dart'; + +class ChartCubit extends Cubit { + final glucoseDataRepo = sl(); + ChartCubit() : super(ChartLoading()) { + getStatsForDay(DateTime.now(), 6); + } + + + Future getStatsForDay(DateTime date, int dayIndex) async { + emit(ChartLoading()); + final data = await glucoseDataRepo.getGlucoseDataForDay(date: date); + data.fold((l) => null, (r) { + if(r.isEmpty){ + emit(EmptyDayData(pickedDayIndex: dayIndex)); + return; + } + var minValue = 6.0; + var maxValue = 0.0; + var valuesSum = 0.0; + final spots = r.map((measurement) { + final time = + measurement.createdAt.hour + measurement.createdAt.minute / 60; + if (measurement.value < minValue) minValue = measurement.value; + if (measurement.value > maxValue) maxValue = measurement.value; + valuesSum += measurement.value; + return FlSpot(time, measurement.value); + }).toList(); + + final averageValue = valuesSum / r.length; + final variabilityValue = maxValue - minValue; + + emit( + ChartData( + spots: spots, + minValue: minValue, + maxValue: maxValue, + averageValue: averageValue, + variabilityValue: variabilityValue, + pickedDayIndex: dayIndex, + ), + ); + }); + } +} diff --git a/mobile_app/lib/presentation/chart/cubit/chart_state.dart b/mobile_app/lib/presentation/chart/cubit/chart_state.dart new file mode 100644 index 0000000..26f001b --- /dev/null +++ b/mobile_app/lib/presentation/chart/cubit/chart_state.dart @@ -0,0 +1,56 @@ +part of 'chart_cubit.dart'; + +@immutable +sealed class ChartState {} + +final class ChartLoading extends ChartState {} + + +final class ChartData extends ChartState { + final int pickedDayIndex; + final List spots; + final double minValue; + final double maxValue; + final double averageValue; + final double variabilityValue; + + ChartData({ + required this.spots, + required this.minValue, + required this.maxValue, + required this.averageValue, + required this.variabilityValue, + required this.pickedDayIndex, + }); + + ChartData copyWith( + {int? pickedDayIndex, + + List? spots, + double? minValue, + double? maxValue, + double? averageValue, + double? variabilityValue}) => + ChartData( + pickedDayIndex: pickedDayIndex ?? this.pickedDayIndex, + + spots: spots ?? this.spots, + minValue: minValue ?? this.minValue, + maxValue: maxValue ?? this.maxValue, + averageValue: averageValue ?? this.averageValue, + variabilityValue: variabilityValue ?? this.variabilityValue, + ); +} + +final class EmptyDayData extends ChartState{ + final int pickedDayIndex; + + EmptyDayData({ + required this.pickedDayIndex, + }); + + EmptyDayData copyWith({int? pickedDayIndex, DateTime? pickedDate}) => + EmptyDayData( + pickedDayIndex: pickedDayIndex ?? this.pickedDayIndex, + ); +} diff --git a/mobile_app/lib/presentation/chart/screen.dart b/mobile_app/lib/presentation/chart/screen.dart new file mode 100644 index 0000000..0d0c72a --- /dev/null +++ b/mobile_app/lib/presentation/chart/screen.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/presentation/chart/cubit/chart_cubit.dart'; +import 'package:mobile_app/presentation/chart/widgets/date_card.dart'; +import 'package:mobile_app/presentation/chart/widgets/line_chart.dart'; +import 'package:mobile_app/presentation/components/card.dart'; + +class ChartScreen extends StatelessWidget { + const ChartScreen({super.key}); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + return BlocBuilder( + builder: (context, state) { + return switch (state) { + ChartLoading() => const Center( + child: CircularProgressIndicator(), + ), + ChartData() => Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + height: 60, + child: ListView.separated( + separatorBuilder: (context, index) => const SizedBox( + width: 13, + ), + itemBuilder: (context, index) { + final day = DateTime.now().day - 6 + index; + final date = DateTime( + DateTime.now().year, + DateTime.now().month, + day, + ); + return DateCard( + date: date, + status: index == state.pickedDayIndex + ? DateCardStatus.picked + : DateCardStatus.notPicked, + onCardTap: () => cubit.getStatsForDay(date, index), + ); + }, + scrollDirection: Axis.horizontal, + itemCount: 7, + ), + ), + SizedBox( + height: 250, + child: Row( + children: [ + Column( + children: [ + Text( + 'mmol/L', + style: Theme.of(context).isLight() + ? captionRegular.copyWith(color: base40) + : captionRegular, + ), + Text( + '6', + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base40) + : bodySRegular, + ), + const Spacer(), + Text( + '4', + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base40) + : bodySRegular, + ), + const Spacer(), + Text( + '2', + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base40) + : bodySRegular, + ), + const Spacer(), + ], + ), + const Expanded( + child: LineChartSample2(), + ), + ], + ), + ), + Row( + children: [ + Expanded( + child: InfoCard( + iconPath: arrowUpSvg, + title: 'Highest', + value: state.maxValue, + ), + ), + Expanded( + child: InfoCard( + iconPath: arrowDownSvg, + title: 'Lowest', + value: state.minValue, + ), + ), + ], + ), + InfoCard( + iconPath: middle, + title: 'Average', + value: state.averageValue, + ), + InfoCard( + iconPath: variability, + title: 'Variability', + value: state.variabilityValue, + ), + ], + ), + EmptyDayData() => Column( + children: [ + SizedBox( + height: 60, + child: ListView.separated( + separatorBuilder: (context, index) => const SizedBox( + width: 13, + ), + itemBuilder: (context, index) { + final day = DateTime.now().day - 6 + index; + final date = DateTime( + DateTime.now().year, + DateTime.now().month, + day, + ); + return DateCard( + date: date, + status: index == state.pickedDayIndex + ? DateCardStatus.picked + : DateCardStatus.notPicked, + onCardTap: () => cubit.getStatsForDay(date, index), + ); + }, + scrollDirection: Axis.horizontal, + itemCount: 7, + ), + ), + const Expanded( + child: Center( + child: Text('No data'), + ), + ), + ], + ), + }; + }, + ); + } +} diff --git a/mobile_app/lib/presentation/chart/widgets/date_card.dart b/mobile_app/lib/presentation/chart/widgets/date_card.dart new file mode 100644 index 0000000..253b047 --- /dev/null +++ b/mobile_app/lib/presentation/chart/widgets/date_card.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/presentation/chart/widgets/date_column.dart'; + +enum DateCardStatus { + picked, + notPicked, +} + +class DateCard extends StatelessWidget { + final DateCardStatus status; + final VoidCallback? onCardTap; + final DateTime date; + + const DateCard({ + super.key, + required this.status, + this.onCardTap, + required this.date, + }); + + String get _dayOfWeek { + return switch (date.weekday) { + 1 => 'Mon', + 2 => 'Tue', + 3 => 'Wed', + 4 => 'Thu', + 5 => 'Fri', + 6 => 'Sat', + 7 => 'Sun', + _ => '...', + }; + } + + @override + Widget build(BuildContext context) { + return status == DateCardStatus.picked + ? DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).isLight() ? green5 : green30, + border: Border.all(color: green50), + borderRadius: BorderRadius.circular(4), + ), + child: DateColumn( + textColor: base90, + day: date.day, + dayOfWeek: _dayOfWeek, + ), + ) + : InkWell( + onTap: onCardTap, + child: DateColumn( + textColor: base40, + day: date.day, + dayOfWeek: _dayOfWeek, + ), + ); + } +} diff --git a/mobile_app/lib/presentation/chart/widgets/date_column.dart b/mobile_app/lib/presentation/chart/widgets/date_column.dart new file mode 100644 index 0000000..3394b43 --- /dev/null +++ b/mobile_app/lib/presentation/chart/widgets/date_column.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; + +class DateColumn extends StatelessWidget { + final Color textColor; + final int day; + final String dayOfWeek; + const DateColumn({ + super.key, + required this.textColor, + required this.day, + required this.dayOfWeek, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + dayOfWeek, + style: Theme.of(context).isLight() + ? bodyMRegular.copyWith(color: textColor) + : bodyMRegular, + ), + const SizedBox( + height: 4, + ), + Text( + '$day', + style: Theme.of(context).isLight() + ? bodyMRegular.copyWith(color: textColor) + : bodyMRegular, + ), + ], + ), + ); + } +} diff --git a/mobile_app/lib/presentation/chart/widgets/line_chart.dart b/mobile_app/lib/presentation/chart/widgets/line_chart.dart new file mode 100644 index 0000000..90f43ea --- /dev/null +++ b/mobile_app/lib/presentation/chart/widgets/line_chart.dart @@ -0,0 +1,133 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/presentation/chart/cubit/chart_cubit.dart'; + +class LineChartSample2 extends StatefulWidget { + const LineChartSample2({super.key}); + + @override + State createState() => _LineChartSample2State(); +} + +class _LineChartSample2State extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: 1200, + child: Padding( + padding: const EdgeInsets.only( + right: 18, + left: 20, + top: 24, + bottom: 12, + ), + child: LineChart( + mainData((state as ChartData).spots), + ), + ), + ), + ); + }, + ); + } + + Widget bottomTitleWidgets(double value, TitleMeta meta) { + final time = value.toInt() % 12; + final partOfDay = value >= 12 ? 'pm' : 'am'; + final text = Text( + '$time$partOfDay', + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base40) + : bodySRegular, + ); + + return SideTitleWidget( + axisSide: meta.axisSide, + child: text, + ); + } + + Widget leftTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ); + String text; + switch (value) { + case 2.0: + text = '2'; + break; + case 4.0: + text = '4'; + break; + case 6.0: + text = '6'; + break; + default: + return Container(); + } + + return Text(text, style: style, textAlign: TextAlign.left); + } + + LineChartData mainData(List spots) { + return LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (touchedSpots) => touchedSpots + .map((e) => LineTooltipItem( + e.y.toStringAsFixed(2), const TextStyle(color: green5))) + .toList(), + )), + borderData: FlBorderData(show: false), + gridData: FlGridData( + drawVerticalLine: false, + horizontalInterval: 2, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey[400], + strokeWidth: 1, + dashArray: [7, 7], + ); + }, + ), + titlesData: FlTitlesData( + rightTitles: const AxisTitles(), + topTitles: const AxisTitles(), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: 1, + getTitlesWidget: bottomTitleWidgets, + ), + ), + leftTitles: const AxisTitles(), + ), + minX: 0, + maxX: 23, + minY: 1, + maxY: 6.1, + lineBarsData: [ + LineChartBarData( + spots: spots, + color: green30, + isCurved: true, + barWidth: 3, + isStrokeCapRound: true, + dotData: const FlDotData( + show: false, + ), + ), + ], + ); + } +} diff --git a/mobile_app/lib/presentation/components/bottom_nav_bar.dart b/mobile_app/lib/presentation/components/bottom_nav_bar.dart new file mode 100644 index 0000000..e69de29 diff --git a/mobile_app/lib/presentation/components/card.dart b/mobile_app/lib/presentation/components/card.dart new file mode 100644 index 0000000..1484959 --- /dev/null +++ b/mobile_app/lib/presentation/components/card.dart @@ -0,0 +1,72 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; + +class InfoCard extends StatelessWidget { + final String iconPath; + final String title; + final double value; + final String? time; + + const InfoCard({ + super.key, + required this.iconPath, + required this.title, + required this.value, + this.time, + }); + + @override + Widget build(BuildContext context) { + return Card( + surfaceTintColor: Theme.of(context).isLight() ? Colors.white : null, + color: Theme.of(context).isLight() ? Colors.white : null, + elevation: 20, + shadowColor: Theme.of(context).isLight() ? shadowColor : null, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Row( + children: [ + SvgPicture.asset( + iconPath, + color: Theme.of(context).isLight() ? null : green30, + ), + Text( + title, + style: Theme.of(context).isLight() + ? bodyMMedium.copyWith(color: base90) + : bodyMMedium, + ), + ], + ), + Row( + children: [ + Text( + value.toStringAsFixed(1), + style: h4, + ), + Expanded( + child: Text( + ' mmol/L', + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base30) + : bodySRegular, + ), + ), + Visibility( + visible: time != null, + child: Text(time ?? 'dfdf'), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/mobile_app/lib/presentation/components/custom_button.dart b/mobile_app/lib/presentation/components/custom_button.dart new file mode 100644 index 0000000..bf20ebc --- /dev/null +++ b/mobile_app/lib/presentation/components/custom_button.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; + +enum ButtonType { + teritary, + secondary, + primary; + + TextStyle get textStyle => switch (this) { + ButtonType.teritary => bodyMMedium.copyWith(color: base70), + ButtonType.secondary => bodyMMedium.copyWith(color: green50), + ButtonType.primary => bodyMMedium.copyWith(color: Colors.white), + }; + + Color get backgroundColor => switch (this) { + ButtonType.teritary => Colors.transparent, + ButtonType.secondary => Colors.transparent, + ButtonType.primary => green50, + }; +} + +class CustomButton extends StatelessWidget { + final String text; + final ButtonType buttonType; + final VoidCallback onPressed; + const CustomButton({ + super.key, + required this.text, + required this.buttonType, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 52, + child: ElevatedButton( + onPressed: onPressed, + style: ButtonStyle( + padding: const MaterialStatePropertyAll( + EdgeInsets.symmetric(horizontal: 24), + ), + elevation: const MaterialStatePropertyAll(0), + backgroundColor: MaterialStatePropertyAll(buttonType.backgroundColor), + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + ), + child: Text( + text, + style: buttonType.textStyle, + ), + ), + ); + } +} diff --git a/mobile_app/lib/presentation/components/custom_text_field.dart b/mobile_app/lib/presentation/components/custom_text_field.dart new file mode 100644 index 0000000..307c2ea --- /dev/null +++ b/mobile_app/lib/presentation/components/custom_text_field.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; + +class CustomTextField extends StatelessWidget { + final String title; + final TextEditingController controller; + final bool enabled; + final VoidCallback? onTap; + final bool onlyNumbers; + + const CustomTextField({ + super.key, + required this.title, + required this.controller, + this.enabled = true, + this.onTap, + this.onlyNumbers = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base40) + : bodySRegular, + ), + const SizedBox( + height: 4, + ), + TextFormField( + controller: controller, + keyboardType: onlyNumbers + ? const TextInputType.numberWithOptions(decimal: true) + : null, + onTap: onTap, + readOnly: !enabled, + decoration: InputDecoration( + helperText: title == 'Level of glucose' ? 'mmol/L' : null, + border: const OutlineInputBorder(), + ), + ), + ], + ); + } +} diff --git a/mobile_app/lib/presentation/history/cubit/history_cubit.dart b/mobile_app/lib/presentation/history/cubit/history_cubit.dart new file mode 100644 index 0000000..9bf6564 --- /dev/null +++ b/mobile_app/lib/presentation/history/cubit/history_cubit.dart @@ -0,0 +1,39 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:mobile_app/data/models/db/glucose_event.dart'; +import 'package:mobile_app/di/di_container.dart'; +import 'package:mobile_app/domain/entities/glucose_evens_for_day.dart'; +import 'package:mobile_app/domain/repository/local/glucose_events_repository.dart'; + +part 'history_state.dart'; + +class HistoryCubit extends Cubit { + final eventsRepo = sl(); + + HistoryCubit() : super(HistoryLoading()){ + getAndGroupGlucoseEvents(); + } + + Future getAndGroupGlucoseEvents() async { + final result = await eventsRepo.getGlucoseEvents(); + result.fold((error) { + emit(HistoryError()); + }, (events) { + final daysMap = >{}; + for (final event in events) { + final date = DateTime( + event.createdAt.year, event.createdAt.month, event.createdAt.day); + if (daysMap.containsKey(date)) { + daysMap[date]?.add(event); + } else { + daysMap[date] = [event]; + } + } + final eventsPerDay = []; + daysMap.forEach((date, list) { + eventsPerDay.add(GlucoseEventsPerDay(date: date, events: list)); + }); + emit(HistoryData(eventDays: eventsPerDay)); + }); + } +} diff --git a/mobile_app/lib/presentation/history/cubit/history_state.dart b/mobile_app/lib/presentation/history/cubit/history_state.dart new file mode 100644 index 0000000..878ed47 --- /dev/null +++ b/mobile_app/lib/presentation/history/cubit/history_state.dart @@ -0,0 +1,23 @@ +part of 'history_cubit.dart'; + +@immutable +sealed class HistoryState {} + +final class HistoryError extends HistoryState {} + +final class HistoryLoading extends HistoryState {} + +final class HistoryIsEmpty extends HistoryState {} + +final class HistoryData extends HistoryState { + final List eventDays; + + HistoryData({required this.eventDays}); + + HistoryData copyWith({ + List? eventDays, + }) => + HistoryData( + eventDays: eventDays ?? this.eventDays, + ); +} diff --git a/mobile_app/lib/presentation/history/history_page.dart b/mobile_app/lib/presentation/history/history_page.dart new file mode 100644 index 0000000..648a430 --- /dev/null +++ b/mobile_app/lib/presentation/history/history_page.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/utils/month_converter.dart'; +import 'package:mobile_app/presentation/history/cubit/history_cubit.dart'; +import 'package:mobile_app/presentation/history/widgets/day_column.dart'; + +class HistoryPage extends StatelessWidget { + const HistoryPage({super.key}); + + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return switch (state) { + HistoryLoading() => const Center( + child: CircularProgressIndicator(), + ), + HistoryData() => ListView.builder( + clipBehavior: Clip.none, + itemBuilder: (context, index) { + final today = + DateTime.now().day == state.eventDays[index].date.day + ? ', today' + : ''; + return DayColumn( + day: + '${getMonthByNumber(state.eventDays[index].date.month)} ${state.eventDays[index].date.day}th$today', + glucoseEvents: state.eventDays[index].events, + ); + }, + itemCount: state.eventDays.length, + ), + HistoryError() => const Center( + child: Text('Something went wrong'), + ), + HistoryIsEmpty() => const Center( + child: Text('No data'), + ), + }; + }, + ); + } +} diff --git a/mobile_app/lib/presentation/history/widgets/day_column.dart b/mobile_app/lib/presentation/history/widgets/day_column.dart new file mode 100644 index 0000000..566d66f --- /dev/null +++ b/mobile_app/lib/presentation/history/widgets/day_column.dart @@ -0,0 +1,38 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:flutter/material.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/data/models/db/glucose_event.dart'; +import 'package:mobile_app/presentation/components/card.dart'; + +class DayColumn extends StatelessWidget { + final String day; + final List glucoseEvents; + const DayColumn({ + super.key, + required this.day, + required this.glucoseEvents, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Text( + day, + style: h4, + ), + ), + for (final event in glucoseEvents) + InfoCard( + iconPath: event.iconPath, + title: event.description, + value: event.value, + time: event.time, + ), + ], + ); + } +} diff --git a/mobile_app/lib/presentation/home/cubit/home_cubit.dart b/mobile_app/lib/presentation/home/cubit/home_cubit.dart new file mode 100644 index 0000000..562b987 --- /dev/null +++ b/mobile_app/lib/presentation/home/cubit/home_cubit.dart @@ -0,0 +1,172 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:bloc/bloc.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:meta/meta.dart'; +import 'package:mobile_app/data/data_source/local/shared_prefs/shared_prefs.dart'; +import 'package:mobile_app/data/repository/measurements_repository.dart'; +import 'package:mobile_app/di/di_container.dart'; +import 'package:mobile_app/domain/repository/ble_device_repo.dart'; +import 'package:mobile_app/managers/foreground_service/task_handler.dart'; + +part 'home_state.dart'; + +@pragma('vm:entry-point') +void startCallback() { + // The setTaskHandler function must be called to handle the task in the background. + FlutterForegroundTask.setTaskHandler(FirstTaskHandler()); +} + +class HomeCubit extends Cubit { + ReceivePort? _receivePort; + //StreamSubscription? _streamSubscription; + final bleDeviceRepo = sl(); + final sharedPrefs = sl(); + final glucoseDataRepo = sl(); + HomeCubit() : super(HomeLoading()) { + _initListener(); + _initForegroundService(); + } + + Future _initForegroundService() async { + await _requestPermissionForAndroid(); + _initForegroundTask(); + + // You can get the previous ReceivePort without restarting the service. + if (await FlutterForegroundTask.isRunningService) { + final newReceivePort = FlutterForegroundTask.receivePort; + _registerReceivePort(newReceivePort); + } + } + + void _initListener() { + bleDeviceRepo.isConnected.listen((isConnected) { + switch (isConnected) { + case true: + _startForegroundTask(); + sharedPrefs.setIsConnected(true); + case false: + sharedPrefs.setIsConnected(false); + emit(DeviceDisconnected()); + } + }); + } + + void _initForegroundTask() { + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + foregroundServiceType: AndroidForegroundServiceType.HEALTH, + channelId: 'foreground_service', + channelName: 'Foreground Service Notification', + channelDescription: + 'This notification appears when the foreground service is running.', + channelImportance: NotificationChannelImportance.HIGH, + priority: NotificationPriority.HIGH, + iconData: const NotificationIconData( + resType: ResourceType.mipmap, + resPrefix: ResourcePrefix.ic, + name: 'launcher', + ), + buttons: [ + const NotificationButton(id: 'sendButton', text: 'Send'), + const NotificationButton(id: 'testButton', text: 'Test'), + ], + ), + iosNotificationOptions: const IOSNotificationOptions(), + foregroundTaskOptions: const ForegroundTaskOptions( + isOnceEvent: true, + autoRunOnBoot: true, + allowWakeLock: true, + allowWifiLock: true, + ), + ); + } + + Future _requestPermissionForAndroid() async { + if (!Platform.isAndroid) { + return; + } + if (!await FlutterForegroundTask.canDrawOverlays) { + await FlutterForegroundTask.openSystemAlertWindowSettings(); + } + + if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { + await FlutterForegroundTask.requestIgnoreBatteryOptimization(); + } + final notificationPermissionStatus = + await FlutterForegroundTask.checkNotificationPermission(); + if (notificationPermissionStatus != NotificationPermission.granted) { + await FlutterForegroundTask.requestNotificationPermission(); + } + } + + Future _startForegroundTask() async { + // Register the receivePort before starting the service. + final receivePort = FlutterForegroundTask.receivePort; + final isRegistered = _registerReceivePort(receivePort); + if (!isRegistered) { + print('Failed to register receivePort!'); + return false; + } + + if (await FlutterForegroundTask.isRunningService) { + return FlutterForegroundTask.restartService(); + } else { + return FlutterForegroundTask.startService( + notificationTitle: 'Foreground Service is running', + notificationText: 'Tap to return to the app', + callback: startCallback, + ); + } + } + + bool _registerReceivePort(ReceivePort? newReceivePort) { + if (newReceivePort == null) { + return false; + } + + closeReceivePort(); + + _receivePort = newReceivePort; + _receivePort?.listen((data) async { + if (data is double && !isClosed) { + final res = await glucoseDataRepo.getGlucoseDataForDay(); + res.fold((l) => null, (r) { + var minValue = 6.0; + var maxValue = 0.0; + var valuesSum = 0.0; + for (final measurement in r) { + if (measurement.value < minValue) minValue = measurement.value; + if (measurement.value > maxValue) maxValue = measurement.value; + valuesSum += measurement.value; + } + + final averageValue = valuesSum / r.length; + final variabilityValue = maxValue - minValue; + emit(DeviceConnected( + currentGlucoseValue: data, + averageValue: averageValue, + variabilityValue: variabilityValue, + minValue: minValue, + maxValue: maxValue, + )); + }); + } + }); + + return _receivePort != null; + } + + void closeReceivePort() { + _receivePort?.close(); + _receivePort = null; + } + + @override + Future close() { + closeReceivePort(); + return super.close(); + } +} diff --git a/mobile_app/lib/presentation/home/cubit/home_state.dart b/mobile_app/lib/presentation/home/cubit/home_state.dart new file mode 100644 index 0000000..b8901d2 --- /dev/null +++ b/mobile_app/lib/presentation/home/cubit/home_state.dart @@ -0,0 +1,39 @@ +part of 'home_cubit.dart'; + +@immutable +sealed class HomeState {} + +final class HomeLoading extends HomeState {} + +final class DeviceConnected extends HomeState { + final double minValue; + final double maxValue; + final double averageValue; + final double variabilityValue; + final double currentGlucoseValue; + + DeviceConnected({ + required this.minValue, + required this.maxValue, + required this.averageValue, + required this.variabilityValue, + required this.currentGlucoseValue, + }); + + DeviceConnected copyWith({ + double? currentGlucoseValue, + double? averageValue, + double? variabilityValue, + double? minValue, + double? maxValue, + }) => + DeviceConnected( + currentGlucoseValue: currentGlucoseValue ?? this.currentGlucoseValue, + minValue: minValue ?? this.minValue, + maxValue: maxValue ?? this.maxValue, + averageValue: averageValue ?? this.averageValue, + variabilityValue: variabilityValue ?? this.variabilityValue, + ); +} + +final class DeviceDisconnected extends HomeState {} diff --git a/mobile_app/lib/presentation/home/home_page.dart b/mobile_app/lib/presentation/home/home_page.dart new file mode 100644 index 0000000..eb41b3f --- /dev/null +++ b/mobile_app/lib/presentation/home/home_page.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/presentation/components/card.dart'; +import 'package:mobile_app/presentation/home/cubit/home_cubit.dart'; +import 'package:mobile_app/presentation/home/widgets/gauge_indicator.dart'; + +class HomePageWidget extends StatelessWidget { + const HomePageWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return switch (state) { + HomeLoading() => const Center( + child: CircularProgressIndicator(), + ), + DeviceConnected() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Hello, Maxim', + style: h4, + ), + Text( + 'Your glucose data today are', + style: Theme.of(context).isLight() + ? bodyMRegular.copyWith(color: base90) + : bodyMRegular, + ), + ], + ), + const SizedBox( + height: 20, + ), + SizedBox( + height: 200, + width: double.infinity, + child: GaugeIndicator( + value: state.currentGlucoseValue, + ), + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + Expanded( + child: InfoCard( + iconPath: arrowUpSvg, + title: 'Highest', + value: state.maxValue, + ), + ), + Expanded( + child: InfoCard( + iconPath: arrowDownSvg, + title: 'Lowest', + value: state.minValue, + ), + ), + ], + ), + InfoCard( + iconPath: middle, + title: 'Average', + value: state.averageValue, + ), + GestureDetector( + onTap:() => context.read().closeReceivePort(), + child: InfoCard( + iconPath: variability, + title: 'Variability', + value: state.variabilityValue, + ), + ), + ], + ), + DeviceDisconnected() => const Center( + child: Text('First of all please connect your device'), + ), + }; + }, + ); + } +} diff --git a/mobile_app/lib/presentation/home/widgets/gauge_indicator.dart b/mobile_app/lib/presentation/home/widgets/gauge_indicator.dart new file mode 100644 index 0000000..a371c9b --- /dev/null +++ b/mobile_app/lib/presentation/home/widgets/gauge_indicator.dart @@ -0,0 +1,156 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/config/styles.dart'; + +class GaugeIndicator extends StatelessWidget { + final double value; + const GaugeIndicator({ + super.key, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: GaugePainter(value: value), + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only( + bottom: 12, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(bloodSvg), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + value.toStringAsFixed(2), + style: h4, + ), + const Text( + 'mmol/L', + style: h4, + ) + ], + ), + ], + ), + ), + ), + ); + } +} + +class GaugePainter extends CustomPainter { + final double value; + final _minValue = 0; + final _maxValue = 6; + + GaugePainter({ + required this.value, + }); + + @override + void paint(Canvas canvas, Size size) { + final valuePercentage = value / (_maxValue - _minValue); + + final width = size.width; + final height = size.height; + const strokeWidth = 20.0; + const halfOfStrokeWidth = strokeWidth / 2; + const top = 0 + halfOfStrokeWidth; + final bottom = height * 2 - halfOfStrokeWidth * 2; + const left = 0 + halfOfStrokeWidth; + final right = width - halfOfStrokeWidth; + final radius = (bottom - top) / 2; + final center = Offset((right - left) / 2, radius); + final paint = Paint() + ..shader = ui.Gradient.sweep( + center, + [ + Colors.red, + Colors.yellow, + Colors.green, + Colors.yellow, + Colors.red, + ], + [ + 0, + 0.2, + 0.5, + 0.8, + 1, + ], + TileMode.mirror, + pi, + 2 * pi, + ) + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round; + final paint2 = Paint() + ..color = Colors.grey[200]! + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round; + final drawRect = Rect.fromLTRB( + left, + top, + right, + bottom, + ); + canvas.drawArc(drawRect, pi, pi, false, paint2); + canvas.drawArc(drawRect, pi, pi * valuePercentage, false, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; + + GaugePainter copyWith({ + double? value, + }) { + return GaugePainter( + value: value ?? this.value, + ); + } + + Map toMap() { + return { + 'value': value, + }; + } + + factory GaugePainter.fromMap(Map map) { + return GaugePainter( + value: map['value'] as double, + ); + } + + String toJson() => json.encode(toMap()); + + factory GaugePainter.fromJson(String source) => + GaugePainter.fromMap(json.decode(source) as Map); + + @override + String toString() => 'GaugePainter(value: $value)'; + + @override + bool operator ==(covariant GaugePainter other) { + if (identical(this, other)) return true; + + return other.value == value; + } + + @override + int get hashCode => value.hashCode; +} diff --git a/mobile_app/lib/presentation/login/cubit/login_cubit.dart b/mobile_app/lib/presentation/login/cubit/login_cubit.dart new file mode 100644 index 0000000..691b2e6 --- /dev/null +++ b/mobile_app/lib/presentation/login/cubit/login_cubit.dart @@ -0,0 +1,32 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:mobile_app/data/models/request/login_data.dart'; +import 'package:mobile_app/domain/repository/auth_repository.dart'; + +part 'login_state.dart'; + +class LoginCubit extends Cubit { + final AuthRepository authRepo; + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + + LoginCubit({required this.authRepo}) : super(LoginInitial()); + + Future login() async { + emit(LoginLoading()); + final loginData = LoginData( + username: emailController.text, + password: passwordController.text, + ); + final result = await authRepo.login(loginData); + result.fold( + (error) => emit(LoginError(error: 'Неверный логин или пароль')), + (data) => emit(LoginSuccessful()), + ); + } + + void goRegistration() { + emit(NotRegisteredYet()); + } + +} diff --git a/mobile_app/lib/presentation/login/cubit/login_state.dart b/mobile_app/lib/presentation/login/cubit/login_state.dart new file mode 100644 index 0000000..7caf4bb --- /dev/null +++ b/mobile_app/lib/presentation/login/cubit/login_state.dart @@ -0,0 +1,19 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +part of 'login_cubit.dart'; + +@immutable +sealed class LoginState {} + +final class LoginInitial extends LoginState {} + +final class LoginLoading extends LoginState {} + +final class LoginError extends LoginState { + final String error; + + LoginError({required this.error}); +} + +final class LoginSuccessful extends LoginState {} + +final class NotRegisteredYet extends LoginState {} diff --git a/mobile_app/lib/presentation/login/login_page.dart b/mobile_app/lib/presentation/login/login_page.dart new file mode 100644 index 0000000..60ec4ed --- /dev/null +++ b/mobile_app/lib/presentation/login/login_page.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/config/routes.dart'; +import 'package:mobile_app/presentation/login/cubit/login_cubit.dart'; + +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: BlocConsumer( + listener: (context, state) { + if (state is LoginSuccessful) { + Navigator.of(context).pushReplacementNamed(Routes.main); + } else if (state is NotRegisteredYet) { + Navigator.of(context).pushReplacementNamed(Routes.registration); + } + }, + builder: (context, state) { + switch (state.runtimeType) { + case LoginLoading: + return const Center( + child: CircularProgressIndicator(), + ); + default: + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(logoSvg), + ], + ), + ), + Expanded( + child: Column( + children: [ + if (state is LoginError) + Align( + alignment: Alignment.centerLeft, + child: Text( + state.error, + style: const TextStyle(color: Colors.red), + ), + ), + const SizedBox( + height: 8, + ), + TextFormField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'E-mail', + ), + controller: + context.read().emailController, + ), + const SizedBox( + height: 8, + ), + TextFormField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Password', + ), + controller: + context.read().passwordController, + ), + const SizedBox( + height: 8, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () {}, + child: const Text('Forgot password?'), + ), + ], + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 52), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + backgroundColor: green50, + foregroundColor: Colors.white, + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onPressed: () => context.read().login(), + child: const Text('Login'), + ), + const SizedBox( + height: 8, + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 52), + side: const BorderSide(color: green50, width: 1.25), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + backgroundColor: Colors.white, + foregroundColor: green50, + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onPressed: context.read().goRegistration, + child: const Text("I don't have an account"), + ), + ], + ), + ); + } + }, + ), + ); + } +} diff --git a/mobile_app/lib/presentation/main/cubit/home_page_cubit.dart b/mobile_app/lib/presentation/main/cubit/home_page_cubit.dart new file mode 100644 index 0000000..461ecc5 --- /dev/null +++ b/mobile_app/lib/presentation/main/cubit/home_page_cubit.dart @@ -0,0 +1,8 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +part 'home_page_state.dart'; + +class HomePageCubit extends Cubit { + HomePageCubit() : super(HomePageInitial()); +} diff --git a/mobile_app/lib/presentation/main/cubit/home_page_state.dart b/mobile_app/lib/presentation/main/cubit/home_page_state.dart new file mode 100644 index 0000000..c9d02b0 --- /dev/null +++ b/mobile_app/lib/presentation/main/cubit/home_page_state.dart @@ -0,0 +1,6 @@ +part of 'home_page_cubit.dart'; + +@immutable +sealed class HomePageState {} + +final class HomePageInitial extends HomePageState {} diff --git a/mobile_app/lib/presentation/main/main_page.dart b/mobile_app/lib/presentation/main/main_page.dart new file mode 100644 index 0000000..3875da5 --- /dev/null +++ b/mobile_app/lib/presentation/main/main_page.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/presentation/add_note/add_note_page.dart'; +import 'package:mobile_app/presentation/add_note/cubit/add_note_cubit.dart'; +import 'package:mobile_app/presentation/chart/cubit/chart_cubit.dart'; +import 'package:mobile_app/presentation/chart/screen.dart'; +import 'package:mobile_app/presentation/history/cubit/history_cubit.dart'; +import 'package:mobile_app/presentation/history/history_page.dart'; +import 'package:mobile_app/presentation/home/cubit/home_cubit.dart'; +import 'package:mobile_app/presentation/home/home_page.dart'; +import 'package:mobile_app/presentation/settings/screen.dart'; +import 'package:mobile_app/presentation/tabs/home_tab.dart'; + +class MainPageWidget extends StatefulWidget { + const MainPageWidget({super.key}); + + @override + State createState() => _MainPageWidgetState(); +} + +class _MainPageWidgetState extends State { + final pageController = PageController(); + int _selectedIndex = 0; + + void _onItemTapped(int index) { + pageController.jumpToPage(index); + setState(() { + _selectedIndex = index; + }); + } + + String getTitle(int index) { + return switch (index) { + 0 => 'Home', + 1 => 'History', + 2 => 'Add', + 3 => 'Chart', + 4 => 'Settings', + _ => 'Unknown', + }; + } + + @override + Widget build(BuildContext context) { + final pages = [ + const HomeTab(), + BlocProvider( + create: (context) => HistoryCubit(), + child: const HistoryPage(), + ), + BlocProvider( + create: (context) => AddNoteCubit(), + child: AddNotePage(), + ), + BlocProvider( + create: (context) => ChartCubit(), + child: const ChartScreen(), + ), + const SettingsScreen(), + ]; + + return WithForegroundTask( + child: Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: Text( + getTitle(_selectedIndex), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: PageView( + physics: const NeverScrollableScrollPhysics(), + clipBehavior: Clip.none, + controller: pageController, + children: pages, + onPageChanged: (page) => setState(() => _selectedIndex = page), + ), + ), + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home_outlined), + label: 'fdsf', + ), + BottomNavigationBarItem( + icon: Icon(Icons.history), + label: 'fdsf', + ), + BottomNavigationBarItem( + icon: Icon(Icons.add), + label: 'fdsf', + ), + BottomNavigationBarItem( + icon: Icon(Icons.auto_graph), + label: 'fdsf', + ), + BottomNavigationBarItem( + icon: Icon(Icons.settings), + label: 'fdsf', + ), + ], + onTap: _onItemTapped, + selectedItemColor: Theme.of(context).isLight() ? green50 : green30, + currentIndex: _selectedIndex, + unselectedItemColor: base50, + showSelectedLabels: false, + showUnselectedLabels: false, + iconSize: 30, + type: BottomNavigationBarType.fixed, + ), + ), + ); + } +} diff --git a/mobile_app/lib/presentation/main/widgets/bottom_data_widget.dart b/mobile_app/lib/presentation/main/widgets/bottom_data_widget.dart new file mode 100644 index 0000000..52d6877 --- /dev/null +++ b/mobile_app/lib/presentation/main/widgets/bottom_data_widget.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class BottomDataWidget extends StatelessWidget { + const BottomDataWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const DecoratedBox( + decoration: BoxDecoration(color: Colors.amber), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text('156'), + Text('156'), + Text('156'), + ], + ), + Text( + '650.9', + style: TextStyle( + fontSize: 24, + ), + ), + ], + ), + ); + } +} diff --git a/mobile_app/lib/presentation/main/widgets/graph_widget.dart b/mobile_app/lib/presentation/main/widgets/graph_widget.dart new file mode 100644 index 0000000..012c57d --- /dev/null +++ b/mobile_app/lib/presentation/main/widgets/graph_widget.dart @@ -0,0 +1,30 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +final lineChartBarData = LineChartBarData( + spots: const [ + FlSpot.zero, + FlSpot(1, 2), + FlSpot(3, 1), + FlSpot(5, 2), + FlSpot(6, 3), + FlSpot(8, 6), + ], + isCurved: true, + gradient: const LinearGradient(colors: [Colors.blue, Colors.yellow]), +); + +class GraphWidget extends StatelessWidget { + const GraphWidget({super.key}); + + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + lineBarsData: [lineChartBarData], + minY: 0, + minX: 0, + ), + ); + } +} diff --git a/mobile_app/lib/presentation/main/widgets/stack_graph_widget.dart b/mobile_app/lib/presentation/main/widgets/stack_graph_widget.dart new file mode 100644 index 0000000..895a8b4 --- /dev/null +++ b/mobile_app/lib/presentation/main/widgets/stack_graph_widget.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_app/presentation/main/widgets/graph_widget.dart'; + +class StackGraphWidget extends StatelessWidget { + const StackGraphWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Stack( + children: [ + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [Text('3.5 моль/л')], + ), + Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Что то'), + Text('Что то'), + Text('Что то'), + ], + ), + ], + ), + Padding( + padding: EdgeInsets.all(20), + child: Center( + child: GraphWidget(), + ), + ), + ], + ); + } +} diff --git a/mobile_app/lib/presentation/registration/cubit/registration_cubit.dart b/mobile_app/lib/presentation/registration/cubit/registration_cubit.dart new file mode 100644 index 0000000..614fe2e --- /dev/null +++ b/mobile_app/lib/presentation/registration/cubit/registration_cubit.dart @@ -0,0 +1,27 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:mobile_app/data/models/request/registration_data.dart'; +import 'package:mobile_app/domain/repository/auth_repository.dart'; + +part 'registration_state.dart'; + +class RegistrationCubit extends Cubit { + final AuthRepository authRepo; + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + + RegistrationCubit({required this.authRepo}) : super(RegistrationInitial()); + + Future register() async { + emit(RegistrationLoading()); + final registerData = RegistrationData( + username: emailController.text, + password: passwordController.text, + ); + final result = await authRepo.register(registerData); + result.fold( + (error) => emit(RegistrationError(error: 'Неверный логин или пароль')), + (data) => emit(RegistrationSuccessful()), + ); + } +} diff --git a/mobile_app/lib/presentation/registration/cubit/registration_state.dart b/mobile_app/lib/presentation/registration/cubit/registration_state.dart new file mode 100644 index 0000000..77b540d --- /dev/null +++ b/mobile_app/lib/presentation/registration/cubit/registration_state.dart @@ -0,0 +1,16 @@ +part of 'registration_cubit.dart'; + +@immutable +sealed class RegistrationState {} + +final class RegistrationInitial extends RegistrationState {} + +final class RegistrationLoading extends RegistrationState {} + +final class RegistrationError extends RegistrationState { + final String error; + + RegistrationError({required this.error}); +} + +final class RegistrationSuccessful extends RegistrationState {} diff --git a/mobile_app/lib/presentation/registration/registration_page.dart b/mobile_app/lib/presentation/registration/registration_page.dart new file mode 100644 index 0000000..006fccb --- /dev/null +++ b/mobile_app/lib/presentation/registration/registration_page.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/routes.dart'; +import 'package:mobile_app/presentation/registration/cubit/registration_cubit.dart'; + +class RegistrationPage extends StatelessWidget { + const RegistrationPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(8), + child: BlocConsumer( + listener: (context, state) { + if (state is RegistrationSuccessful) { + Navigator.of(context).pushReplacementNamed(Routes.main); + } + }, builder: (_, state) { + return Column( + children: [ + const SizedBox( + height: 8, + ), + TextFormField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'E-mail', + ), + controller: context.read().emailController, + ), + const SizedBox( + height: 8, + ), + TextFormField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Password', + ), + controller: + context.read().passwordController, + ), + Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 52), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + backgroundColor: green50, + foregroundColor: Colors.white, + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onPressed: () => context.read().register(), + child: const Text('Register'), + ), + ], + ); + }), + ), + ), + ); + } +} diff --git a/mobile_app/lib/presentation/settings/cubit/settings_cubit.dart b/mobile_app/lib/presentation/settings/cubit/settings_cubit.dart new file mode 100644 index 0000000..c1f78f3 --- /dev/null +++ b/mobile_app/lib/presentation/settings/cubit/settings_cubit.dart @@ -0,0 +1,8 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +part 'settings_state.dart'; + +class SettingsCubit extends Cubit { + SettingsCubit() : super(SettingsInitial()); +} diff --git a/mobile_app/lib/presentation/settings/cubit/settings_state.dart b/mobile_app/lib/presentation/settings/cubit/settings_state.dart new file mode 100644 index 0000000..bd112be --- /dev/null +++ b/mobile_app/lib/presentation/settings/cubit/settings_state.dart @@ -0,0 +1,6 @@ +part of 'settings_cubit.dart'; + +@immutable +sealed class SettingsState {} + +final class SettingsInitial extends SettingsState {} diff --git a/mobile_app/lib/presentation/settings/screen.dart b/mobile_app/lib/presentation/settings/screen.dart new file mode 100644 index 0000000..5e6a964 --- /dev/null +++ b/mobile_app/lib/presentation/settings/screen.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/routes.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/presentation/settings/widgets/radio_button_row.dart'; +import 'package:mobile_app/presentation/settings/widgets/test_text.dart'; +import 'package:mobile_app/theme/theme_cubit.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Choose theme', + style: Theme.of(context).isLight() + ? bodyMRegular.copyWith(color: base90) + : bodyMRegular, + ), + const SizedBox( + height: 8, + ), + const RadioButtonRow( + title: 'System', + value: ThemeMode.system, + ), + const SizedBox( + height: 8, + ), + const RadioButtonRow( + title: 'Light', + value: ThemeMode.light, + ), + const SizedBox( + height: 8, + ), + const RadioButtonRow( + title: 'Dark', + value: ThemeMode.dark, + ), + // TestTextWidget( + // text: state.brightness.index.toString(), + // ), + Text( + 'Connect bluetooth device', + style: Theme.of(context).isLight() + ? bodyMRegular.copyWith(color: base90) + : bodyMRegular, + ), + OutlinedButton( + onPressed: () => + Navigator.of(context).pushNamed(Routes.bluetooth), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.add, + color: green50, + size: 16, + ), + Text( + 'Add device', + style: Theme.of(context).isLight() + ? bodySMedium.copyWith(color: green50) + : bodySMedium, + ), + ], + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/mobile_app/lib/presentation/settings/widgets/radio_button_row.dart b/mobile_app/lib/presentation/settings/widgets/radio_button_row.dart new file mode 100644 index 0000000..c4c5e51 --- /dev/null +++ b/mobile_app/lib/presentation/settings/widgets/radio_button_row.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/config/colors.dart'; +import 'package:mobile_app/config/styles.dart'; +import 'package:mobile_app/config/theme.dart'; +import 'package:mobile_app/theme/theme_cubit.dart'; + +class RadioButtonRow extends StatelessWidget { + final String title; + final ThemeMode value; + const RadioButtonRow({ + super.key, + required this.title, + required this.value, + }); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + return Row( + children: [ + SizedBox( + height: 24, + width: 24, + child: Radio.adaptive( + value: value, + groupValue: cubit.themeMode, + onChanged: (value) => cubit.setThemeMode(value!), + ), + ), + Text( + title, + style: Theme.of(context).isLight() + ? bodySRegular.copyWith(color: base80) + : bodySRegular, + ), + ], + ); + } +} diff --git a/mobile_app/lib/presentation/settings/widgets/test_text.dart b/mobile_app/lib/presentation/settings/widgets/test_text.dart new file mode 100644 index 0000000..1cf9101 --- /dev/null +++ b/mobile_app/lib/presentation/settings/widgets/test_text.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + + +class TestTextWidget extends StatelessWidget { + final String text; + const TestTextWidget({ + super.key, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return Text(text); + } +} diff --git a/mobile_app/lib/presentation/splash/cubit/splash_cubit.dart b/mobile_app/lib/presentation/splash/cubit/splash_cubit.dart new file mode 100644 index 0000000..4ac6e5f --- /dev/null +++ b/mobile_app/lib/presentation/splash/cubit/splash_cubit.dart @@ -0,0 +1,18 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:mobile_app/config/routes.dart'; +import 'package:mobile_app/di/di_container.dart'; + +part 'splash_state.dart'; + +class SplashCubit extends Cubit { + SplashCubit() : super(SplashInitial()) { + _init(); + } + + Future _init() async{ + init(); + await Future.delayed(const Duration(seconds: 2)); + emit(SplashInitialized(nextRoute: Routes.main)); + } +} diff --git a/mobile_app/lib/presentation/splash/cubit/splash_state.dart b/mobile_app/lib/presentation/splash/cubit/splash_state.dart new file mode 100644 index 0000000..fc135f3 --- /dev/null +++ b/mobile_app/lib/presentation/splash/cubit/splash_state.dart @@ -0,0 +1,12 @@ +part of 'splash_cubit.dart'; + +@immutable +sealed class SplashState {} + +final class SplashInitial extends SplashState {} + +final class SplashInitialized extends SplashState { + final String nextRoute; + + SplashInitialized({required this.nextRoute}); +} diff --git a/mobile_app/lib/presentation/splash/splash_screen.dart b/mobile_app/lib/presentation/splash/splash_screen.dart new file mode 100644 index 0000000..9d83b00 --- /dev/null +++ b/mobile_app/lib/presentation/splash/splash_screen.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mobile_app/config/const.dart'; +import 'package:mobile_app/presentation/splash/cubit/splash_cubit.dart'; + +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocListener( + listener: (context, state) { + if (state is SplashInitialized) { + Navigator.of(context).pushReplacementNamed(state.nextRoute); + } + }, + child: Center( + child: SvgPicture.asset(logoSvg), + ), + ), + ); + } +} diff --git a/mobile_app/lib/presentation/tabs/home_tab.dart b/mobile_app/lib/presentation/tabs/home_tab.dart new file mode 100644 index 0000000..248b28b --- /dev/null +++ b/mobile_app/lib/presentation/tabs/home_tab.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_app/presentation/home/cubit/home_cubit.dart'; +import 'package:mobile_app/presentation/home/home_page.dart'; + +class HomeTab extends StatefulWidget { + const HomeTab({super.key}); + + @override + State createState() => _HomeTabState(); +} + +class _HomeTabState extends State with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return BlocProvider( + create: (context) => HomeCubit(), + child: const HomePageWidget(), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/mobile_app/lib/theme/theme_cubit.dart b/mobile_app/lib/theme/theme_cubit.dart new file mode 100644 index 0000000..f46808f --- /dev/null +++ b/mobile_app/lib/theme/theme_cubit.dart @@ -0,0 +1,26 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; + +part 'theme_state.dart'; + +class ThemeCubit extends Cubit { + ThemeCubit() : super(const ThemeState(brightness: Brightness.light)); + + void setThemeMode(ThemeMode mode) { + switch (mode) { + case ThemeMode.system: + case ThemeMode.light: + emit(const ThemeState(brightness: Brightness.light)); + case ThemeMode.dark: + emit(const ThemeState(brightness: Brightness.dark)); + } + } + + ThemeMode get themeMode{ + return switch (state.brightness){ + Brightness.dark => ThemeMode.dark, + Brightness.light => ThemeMode.light, + }; + } + +} diff --git a/mobile_app/lib/theme/theme_state.dart b/mobile_app/lib/theme/theme_state.dart new file mode 100644 index 0000000..341a8ea --- /dev/null +++ b/mobile_app/lib/theme/theme_state.dart @@ -0,0 +1,8 @@ +part of 'theme_cubit.dart'; + +@immutable +class ThemeState { + final Brightness brightness; + + const ThemeState({required this.brightness}); +} diff --git a/mobile_app/pubspec.lock b/mobile_app/pubspec.lock new file mode 100644 index 0000000..800abe3 --- /dev/null +++ b/mobile_app/pubspec.lock @@ -0,0 +1,970 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + bloc: + dependency: "direct main" + description: + name: bloc + sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e + url: "https://pub.dev" + source: hosted + version: "8.1.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + url: "https://pub.dev" + source: hosted + version: "2.4.8" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + url: "https://pub.dev" + source: hosted + version: "8.9.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" + url: "https://pub.dev" + source: hosted + version: "5.4.1" + drift: + dependency: "direct main" + description: + name: drift + sha256: "6acedc562ffeed308049f78fb1906abad3d65714580b6745441ee6d50ec564cd" + url: "https://pub.dev" + source: hosted + version: "2.18.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: d9b020736ea85fff1568699ce18b89fabb3f0f042e8a7a05e84a3ec20d39acde + url: "https://pub.dev" + source: hosted + version: "2.18.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: b5e2b0f13d93f8c532b5a2786bfb44580de1f50b927bf95813fa1af617e9caf8 + url: "https://pub.dev" + source: hosted + version: "0.66.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" + url: "https://pub.dev" + source: hosted + version: "8.1.4" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: abb08b5a7e44202290ff3dfc8fbb33fde7d4a82c39b20f3803a7f14ce2420aa7 + url: "https://pub.dev" + source: hosted + version: "1.32.5" + flutter_foreground_task: + dependency: "direct main" + description: + name: flutter_foreground_task + sha256: d40a1ddd5f275450d2e32055e7f884796c028a02ac26c751c20916576f79e132 + url: "https://pub.dev" + source: hosted + version: "6.2.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + url: "https://pub.dev" + source: hosted + version: "2.0.10+1" + 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" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 + url: "https://pub.dev" + source: hosted + version: "7.6.7" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" + source: hosted + version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: b3ff55aeb08d9d8901b767650285872cb1bb8f508373b3e348d60268b0c7f770 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: "direct main" + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + retrofit: + dependency: "direct main" + description: + name: retrofit + sha256: "13a2865c0d97da580ea4e3c64d412d81f365fd5b26be2a18fca9582e021da37a" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + retrofit_generator: + dependency: "direct dev" + description: + name: retrofit_generator + sha256: "9499eb46b3657a62192ddbc208ff7e6c6b768b19e83c1ee6f6b119c864b99690" + url: "https://pub.dev" + source: hosted + version: "7.0.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "1abbeb84bf2b1a10e5e1138c913123c8aa9d83cd64e5f9a0dd847b3c83063202" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: fb2a106a2ea6042fe57de2c47074cc31539a941819c91e105b864744605da3f5 + url: "https://pub.dev" + source: hosted + version: "0.5.21" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: ade9a67fd70d0369329ed3373208de7ebd8662470e8c396fc8d0d60f9acdfc9f + url: "https://pub.dev" + source: hosted + version: "0.36.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/mobile_app/pubspec.yaml b/mobile_app/pubspec.yaml new file mode 100644 index 0000000..a012112 --- /dev/null +++ b/mobile_app/pubspec.yaml @@ -0,0 +1,49 @@ +name: mobile_app +description: "A new Flutter project." +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: '>=3.2.3 <4.0.0' + +dependencies: + flutter: + sdk: flutter + fl_chart: ^0.66.1 + flutter_svg: ^2.0.10+1 + flutter_bloc: ^8.1.4 + bloc: ^8.1.3 + cupertino_icons: ^1.0.2 + meta: ^1.10.0 + retrofit: '>=4.0.0 <5.0.0' + logger: any #for logging purpose + json_annotation: ^4.8.1 + dio: ^5.4.1 + dartz: ^0.10.1 + get_it: ^7.6.7 + drift: ^2.18.0 + sqlite3_flutter_libs: ^0.5.0 + path_provider: ^2.0.0 + path: ^1.9.0 + flutter_blue_plus: ^1.32.5 + flutter_foreground_task: ^6.2.0 + shared_preferences: ^2.2.3 + + + + + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + retrofit_generator: '>=7.0.0 <8.0.0' + drift_dev: ^2.18.0 + build_runner: '>=2.3.0 <4.0.0' + json_serializable: ^6.6.2 + +flutter: + uses-material-design: true + assets: + - assets/ diff --git a/mobile_app/test/widget_test.dart b/mobile_app/test/widget_test.dart new file mode 100644 index 0000000..a0e7a4e --- /dev/null +++ b/mobile_app/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 in the flutter_test package. 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:mobile_app/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // 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); + }); +}