diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..0fdcb487 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.27.1" +} \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..96ef1a1c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: simonoppowa +patreon: SimonOppowa +open_collective: # Replace with a single Open Collective username +ko_fi: simonoppowa +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/default_workflow.yml b/.github/workflows/default_workflow.yml index a20d2627..bc962081 100644 --- a/.github/workflows/default_workflow.yml +++ b/.github/workflows/default_workflow.yml @@ -27,7 +27,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.19' + flutter-version: '3.27.1' channel: 'stable' cache: true diff --git a/.gitignore b/.gitignore index 652faab8..52b87174 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related @@ -50,3 +52,6 @@ app.*.map.json # nix related result .direnv/ + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/README.md b/README.md index a1b61593..bf95af06 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - +

## Description @@ -52,7 +52,7 @@ See [Data Protection](https://www.iubenda.com/privacy-policy/53501884) ## TODOs - Add serving sizes to meals -- Add Imperial unit support +~~- Add Imperial unit support~~ - Add support for Material You themes ## Contribution diff --git a/android/app/build.gradle b/android/app/build.gradle index e99dc195..6d47339a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -29,7 +29,8 @@ if (flutterVersionName == null) { } android { - compileSdkVersion 34 + namespace "com.opennutritracker.ont.opennutritracker" + compileSdkVersion 35 ndkVersion flutter.ndkVersion compileOptions { @@ -38,7 +39,7 @@ android { } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = JavaVersion.VERSION_1_8 } sourceSets { @@ -48,7 +49,7 @@ android { defaultConfig { applicationId "com.opennutritracker.ont.opennutritracker" minSdkVersion 21 - targetSdkVersion 34 + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -72,7 +73,3 @@ android { flutter { source '../..' } - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22" -} diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ceccc3a8..0b38039c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Thu Dec 19 18:19:34 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 07a4ad1f..f6908ffc 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.1.2" apply false + id "com.android.application" version "8.3.1" apply false id "org.jetbrains.kotlin.android" version "1.8.22" apply false } diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/flake.nix b/flake.nix index 30ab5ff1..b94953f0 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Flutter 3.19.x"; + description = "Flutter 3.27.x"; inputs = { nixpkgs.url = "nixpkgs"; flake-utils.url = "github:numtide/flake-utils"; diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c569640..acf56eb0 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 15.5 diff --git a/ios/Podfile b/ios/Podfile index c236dc0f..9ec1cb3d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '14.0' +platform :ios, '15.5' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c96b4a36..6168b197 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,88 +1,74 @@ PODS: - - app_links (0.0.1): + - app_links (0.0.2): - Flutter - Flutter (1.0.0) - flutter_keyboard_visibility (0.0.1): - Flutter - flutter_secure_storage (6.0.0): - Flutter - - GoogleDataTransport (9.4.1): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleMLKit/BarcodeScanning (4.0.0): + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMLKit/BarcodeScanning (7.0.0): - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 3.0.0) - - GoogleMLKit/MLKitCore (4.0.0): - - MLKitCommon (~> 9.0.0) - - GoogleToolboxForMac/DebugUtils (2.3.2): - - GoogleToolboxForMac/Defines (= 2.3.2) - - GoogleToolboxForMac/Defines (2.3.2) - - GoogleToolboxForMac/Logger (2.3.2): - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSData+zlib (2.3.2)": - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)": - - GoogleToolboxForMac/DebugUtils (= 2.3.2) - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)" - - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)" - - GoogleUtilities/Environment (7.13.3): + - MLKitBarcodeScanning (~> 6.0.0) + - GoogleMLKit/MLKitCore (7.0.0): + - MLKitCommon (~> 12.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (8.0.2): - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.3) - - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilitiesComponents (1.1.0): - - GoogleUtilities/Logger - - GTMSessionFetcher/Core (2.3.0) - - MLImage (1.0.0-beta4) - - MLKitBarcodeScanning (3.0.0): - - MLKitCommon (~> 9.0) - - MLKitVision (~> 5.0) - - MLKitCommon (9.0.0): - - GoogleDataTransport (~> 9.0) - - GoogleToolboxForMac/Logger (~> 2.1) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" - - GoogleUtilities/UserDefaults (~> 7.0) - - GoogleUtilitiesComponents (~> 1.0) - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - MLKitVision (5.0.0): - - GoogleToolboxForMac/Logger (~> 2.1) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - MLImage (= 1.0.0-beta4) - - MLKitCommon (~> 9.0) - - mobile_scanner (3.5.6): + - GTMSessionFetcher/Core (3.5.0) + - MLImage (1.0.0-beta6) + - MLKitBarcodeScanning (6.0.0): + - MLKitCommon (~> 12.0) + - MLKitVision (~> 8.0) + - MLKitCommon (12.0.0): + - GoogleDataTransport (~> 10.0) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/Logger (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (8.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta6) + - MLKitCommon (~> 12.0) + - mobile_scanner (6.0.2): - Flutter - - GoogleMLKit/BarcodeScanning (~> 4.0.0) - - nanopb (2.30910.0): - - nanopb/decode (= 2.30910.0) - - nanopb/encode (= 2.30910.0) - - nanopb/decode (2.30910.0) - - nanopb/encode (2.30910.0) + - GoogleMLKit/BarcodeScanning (~> 7.0.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - PromisesObjC (2.4.0) - - Sentry/HybridSDK (8.21.0): - - SentryPrivate (= 8.21.0) - - sentry_flutter (0.0.1): + - Sentry/HybridSDK (8.42.0) + - sentry_flutter (8.12.0): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.21.0) - - SentryPrivate (8.21.0) + - Sentry/HybridSDK (= 8.42.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - url_launcher_ios (0.0.1): @@ -98,7 +84,7 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -107,7 +93,6 @@ SPEC REPOS: - GoogleMLKit - GoogleToolboxForMac - GoogleUtilities - - GoogleUtilitiesComponents - GTMSessionFetcher - MLImage - MLKitBarcodeScanning @@ -116,7 +101,6 @@ SPEC REPOS: - nanopb - PromisesObjC - Sentry - - SentryPrivate EXTERNAL SOURCES: app_links: @@ -137,38 +121,36 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sentry_flutter/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 + app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 - flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e - GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 - GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 - GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe - GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 - MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b - MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 - MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 - MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 38dcd8a49d7d485f632b7de65e4900010187aef2 - nanopb: 438bc412db1928dac798aa6fd75726007be04262 - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 + MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 + MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d + MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e + mobile_scanner: fd0054c52ede661e80bf5a4dea477a2467356bee + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - Sentry: ebc12276bd17613a114ab359074096b6b3725203 - sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e - SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 + Sentry: 38ed8bf38eab5812787274bf591e528074c19e02 + sentry_flutter: 7d1f1df30f3768c411603ed449519bbb90a7d87b + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe -PODFILE CHECKSUM: e60e17f8bfffff789408fce3f968c37c5c63400e +PODFILE CHECKSUM: ea1096d657c31e2d1110caeae903d5267e26d5a8 COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e689c188..26a86659 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -139,6 +139,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 37662AF917CA6F2C19F0345E /* [CP] Embed Pods Frameworks */, + CFB800FE155ACD1740E06022 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -267,6 +268,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + CFB800FE155ACD1740E06022 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -364,6 +382,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -501,6 +520,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -532,6 +552,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 31e1ea8f..0a8fe9c3 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/lib/core/data/data_source/config_data_source.dart b/lib/core/data/data_source/config_data_source.dart index 171f7199..9f2208ca 100644 --- a/lib/core/data/data_source/config_data_source.dart +++ b/lib/core/data/data_source/config_data_source.dart @@ -50,6 +50,13 @@ class ConfigDataSource { config?.save(); } + Future setConfigUsesImperialUnits(bool usesImperialUnits) async { + _log.fine('Updating config usesImperialUnits to $usesImperialUnits'); + final config = _configBox.get(_configKey); + config?.usesImperialUnits = usesImperialUnits; + config?.save(); + } + Future getConfig() async { return _configBox.get(_configKey) ?? ConfigDBO.empty(); } diff --git a/lib/core/data/dbo/app_theme_dbo.dart b/lib/core/data/dbo/app_theme_dbo.dart index 53b17714..6c2b09f3 100644 --- a/lib/core/data/dbo/app_theme_dbo.dart +++ b/lib/core/data/dbo/app_theme_dbo.dart @@ -26,9 +26,7 @@ enum AppThemeDBO { case AppThemeEntity.system: dbo = AppThemeDBO.system; break; - default: - return AppThemeDBO.system; - } + } return dbo; } } diff --git a/lib/core/data/dbo/config_dbo.dart b/lib/core/data/dbo/config_dbo.dart index b2116146..96301d70 100644 --- a/lib/core/data/dbo/config_dbo.dart +++ b/lib/core/data/dbo/config_dbo.dart @@ -14,9 +14,12 @@ class ConfigDBO extends HiveObject { bool hasAcceptedSendAnonymousData; @HiveField(3) AppThemeDBO selectedAppTheme; + @HiveField(4) + bool? usesImperialUnits; ConfigDBO(this.hasAcceptedDisclaimer, this.hasAcceptedPolicy, - this.hasAcceptedSendAnonymousData, this.selectedAppTheme); + this.hasAcceptedSendAnonymousData, this.selectedAppTheme, + {this.usesImperialUnits = false}); factory ConfigDBO.empty() => ConfigDBO(false, false, false, AppThemeDBO.system); @@ -25,5 +28,6 @@ class ConfigDBO extends HiveObject { entity.hasAcceptedDisclaimer, entity.hasAcceptedPolicy, entity.hasAcceptedSendAnonymousData, - AppThemeDBO.fromAppThemeEntity(entity.appTheme)); + AppThemeDBO.fromAppThemeEntity(entity.appTheme), + usesImperialUnits: entity.usesImperialUnits); } diff --git a/lib/core/data/dbo/config_dbo.g.dart b/lib/core/data/dbo/config_dbo.g.dart index 788bdc10..6c1bea2b 100644 --- a/lib/core/data/dbo/config_dbo.g.dart +++ b/lib/core/data/dbo/config_dbo.g.dart @@ -21,13 +21,14 @@ class ConfigDBOAdapter extends TypeAdapter { fields[1] as bool, fields[2] as bool, fields[3] as AppThemeDBO, + usesImperialUnits: fields[4] as bool?, ); } @override void write(BinaryWriter writer, ConfigDBO obj) { writer - ..writeByte(4) + ..writeByte(5) ..writeByte(0) ..write(obj.hasAcceptedDisclaimer) ..writeByte(1) @@ -35,7 +36,9 @@ class ConfigDBOAdapter extends TypeAdapter { ..writeByte(2) ..write(obj.hasAcceptedSendAnonymousData) ..writeByte(3) - ..write(obj.selectedAppTheme); + ..write(obj.selectedAppTheme) + ..writeByte(4) + ..write(obj.usesImperialUnits); } @override diff --git a/lib/core/data/repository/config_repository.dart b/lib/core/data/repository/config_repository.dart index 43a1571a..ee279a9b 100644 --- a/lib/core/data/repository/config_repository.dart +++ b/lib/core/data/repository/config_repository.dart @@ -41,4 +41,8 @@ class ConfigRepository { final configDBO = await _configDataSource.getConfig(); return ConfigEntity.fromConfigDBO(configDBO); } + + Future setConfigUsesImperialUnits(bool usesImperialUnits) async { + _configDataSource.setConfigUsesImperialUnits(usesImperialUnits); + } } diff --git a/lib/core/domain/entity/app_theme_entity.dart b/lib/core/domain/entity/app_theme_entity.dart index a13e9caf..df6ac23b 100644 --- a/lib/core/domain/entity/app_theme_entity.dart +++ b/lib/core/domain/entity/app_theme_entity.dart @@ -18,9 +18,7 @@ enum AppThemeEntity { case AppThemeDBO.system: entity = AppThemeEntity.system; break; - default: - return AppThemeEntity.system; - } + } return entity; } diff --git a/lib/core/domain/entity/config_entity.dart b/lib/core/domain/entity/config_entity.dart index 0ef39159..ec8ac52c 100644 --- a/lib/core/domain/entity/config_entity.dart +++ b/lib/core/domain/entity/config_entity.dart @@ -7,17 +7,25 @@ class ConfigEntity extends Equatable { final bool hasAcceptedPolicy; final bool hasAcceptedSendAnonymousData; final AppThemeEntity appTheme; + final bool usesImperialUnits; const ConfigEntity(this.hasAcceptedDisclaimer, this.hasAcceptedPolicy, - this.hasAcceptedSendAnonymousData, this.appTheme); + this.hasAcceptedSendAnonymousData, this.appTheme, + {this.usesImperialUnits = false}); factory ConfigEntity.fromConfigDBO(ConfigDBO dbo) => ConfigEntity( - dbo.hasAcceptedDisclaimer, - dbo.hasAcceptedPolicy, - dbo.hasAcceptedSendAnonymousData, - AppThemeEntity.fromAppThemeDBO(dbo.selectedAppTheme)); + dbo.hasAcceptedDisclaimer, + dbo.hasAcceptedPolicy, + dbo.hasAcceptedSendAnonymousData, + AppThemeEntity.fromAppThemeDBO(dbo.selectedAppTheme), + usesImperialUnits: dbo.usesImperialUnits ?? false, + ); @override - List get props => - [hasAcceptedDisclaimer, hasAcceptedPolicy, hasAcceptedSendAnonymousData]; + List get props => [ + hasAcceptedDisclaimer, + hasAcceptedPolicy, + hasAcceptedSendAnonymousData, + usesImperialUnits + ]; } diff --git a/lib/core/domain/usecase/add_config_usecase.dart b/lib/core/domain/usecase/add_config_usecase.dart index 79f5a273..8fccb299 100644 --- a/lib/core/domain/usecase/add_config_usecase.dart +++ b/lib/core/domain/usecase/add_config_usecase.dart @@ -24,4 +24,8 @@ class AddConfigUsecase { Future setConfigAppTheme(AppThemeEntity appTheme) async { await _configRepository.setConfigAppTheme(appTheme); } + + Future setConfigUsesImperialUnits(bool usesImperialUnits) async { + _configRepository.setConfigUsesImperialUnits(usesImperialUnits); + } } diff --git a/lib/core/presentation/widgets/activity_card.dart b/lib/core/presentation/widgets/activity_card.dart index 602058e8..4a5a1f8e 100644 --- a/lib/core/presentation/widgets/activity_card.dart +++ b/lib/core/presentation/widgets/activity_card.dart @@ -44,7 +44,7 @@ class ActivityCard extends StatelessWidget { color: Theme.of(context) .colorScheme .tertiaryContainer - .withOpacity(0.8), + .withValues(alpha: 0.8), borderRadius: BorderRadius.circular(12)), child: Text( "🔥${activityEntity.burnedKcal.toInt()} kcal", @@ -75,7 +75,7 @@ class ActivityCard extends StatelessWidget { child: Text( activityEntity.physicalActivityEntity.getName(context), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -87,8 +87,7 @@ class ActivityCard extends StatelessWidget { style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.8)), + .onSurface.withValues(alpha: 0.8)), maxLines: 1, )) ], diff --git a/lib/core/presentation/widgets/activity_vertial_list.dart b/lib/core/presentation/widgets/activity_vertial_list.dart index 02e69fa6..000825a2 100644 --- a/lib/core/presentation/widgets/activity_vertial_list.dart +++ b/lib/core/presentation/widgets/activity_vertial_list.dart @@ -28,12 +28,12 @@ class ActivityVerticalList extends StatelessWidget { child: Row( children: [ Icon(UserActivityEntity.getIconData(), - size: 24, color: Theme.of(context).colorScheme.onBackground), + size: 24, color: Theme.of(context).colorScheme.onSurface), const SizedBox(width: 4.0), Text( title, style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), ), ], ), diff --git a/lib/core/presentation/widgets/add_item_bottom_sheet.dart b/lib/core/presentation/widgets/add_item_bottom_sheet.dart index 5037d93e..f20d729b 100644 --- a/lib/core/presentation/widgets/add_item_bottom_sheet.dart +++ b/lib/core/presentation/widgets/add_item_bottom_sheet.dart @@ -39,7 +39,7 @@ class AddItemBottomSheet extends StatelessWidget { S.of(context).activityExample, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.7)), + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)), ), // ignore: sized_box_for_whitespace leading: Container( @@ -65,7 +65,7 @@ class AddItemBottomSheet extends StatelessWidget { S.of(context).breakfastExample, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.7)), + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)), ), // ignore: sized_box_for_whitespace leading: Container( @@ -87,7 +87,7 @@ class AddItemBottomSheet extends StatelessWidget { S.of(context).lunchExample, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.7)), + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)), ), // ignore: sized_box_for_whitespace leading: Container( @@ -109,7 +109,7 @@ class AddItemBottomSheet extends StatelessWidget { S.of(context).dinnerExample, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.7)), + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)), ), // ignore: sized_box_for_whitespace leading: Container( @@ -131,7 +131,7 @@ class AddItemBottomSheet extends StatelessWidget { S.of(context).snackExample, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.7)), + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)), ), // ignore: sized_box_for_whitespace leading: Container( diff --git a/lib/core/presentation/widgets/app_banner_version.dart b/lib/core/presentation/widgets/app_banner_version.dart index 0f3170f0..d4410911 100644 --- a/lib/core/presentation/widgets/app_banner_version.dart +++ b/lib/core/presentation/widgets/app_banner_version.dart @@ -14,13 +14,13 @@ class AppBannerVersion extends StatelessWidget { const SizedBox(height: 70, child: DynamicOntLogo()), Text(S.of(context).appTitle, style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600)), Text( S.of(context).appVersionName(versionNumber), style: Theme.of(context).textTheme.titleMedium?.copyWith( color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.7)), + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7)), ) ], ); diff --git a/lib/core/presentation/widgets/copy_dialog.dart b/lib/core/presentation/widgets/copy_dialog.dart new file mode 100644 index 00000000..d450a149 --- /dev/null +++ b/lib/core/presentation/widgets/copy_dialog.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:opennutritracker/features/add_meal/presentation/add_meal_type.dart'; +import 'package:opennutritracker/generated/l10n.dart'; + +class CopyDialog extends StatefulWidget { + const CopyDialog({super.key}); + @override + State createState() { + return CopyDialogState(); + } +} + +class CopyDialogState extends State { + AddMealType _selectedValue = AddMealType.breakfastType; + AddMealType get selectedMealType => _selectedValue; + + @override + void initState() { + super.initState(); + } + + @override + Widget build( + BuildContext context, + ) { + return AlertDialog( + title: Text(S.of(context).copyDialogTitle), + content: DropdownButton( + value: _selectedValue, + onChanged: (AddMealType? addMealType) { + if (addMealType != null) { + setState(() { + _selectedValue = addMealType; + }); + } + }, + items: AddMealType.values.map((AddMealType addMealType) { + return DropdownMenuItem( + value: addMealType, + child: Text(addMealType.getTypeName(context))); + }).toList()), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(_selectedValue); + }, + child: Text(S.of(context).dialogOKLabel)), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(S.of(context).dialogCancelLabel)) + ]); + } +} diff --git a/lib/core/presentation/widgets/copy_or_delete_dialog.dart b/lib/core/presentation/widgets/copy_or_delete_dialog.dart new file mode 100644 index 00000000..bdfda477 --- /dev/null +++ b/lib/core/presentation/widgets/copy_or_delete_dialog.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:opennutritracker/generated/l10n.dart'; + +class CopyOrDeleteDialog extends StatelessWidget { + const CopyOrDeleteDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(S.of(context).copyOrDeleteTimeDialogTitle), + content: Text(S.of(context).copyOrDeleteTimeDialogContent), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(S.of(context).dialogCopyLabel)), + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(S.of(context).dialogDeleteLabel)) + ], + ); + } +} diff --git a/lib/core/presentation/widgets/dynamic_ont_logo.dart b/lib/core/presentation/widgets/dynamic_ont_logo.dart index 7db41118..c50fbf22 100644 --- a/lib/core/presentation/widgets/dynamic_ont_logo.dart +++ b/lib/core/presentation/widgets/dynamic_ont_logo.dart @@ -22,7 +22,7 @@ class DynamicOntLogo extends StatelessWidget { svgString = svgString.replaceAll(_circleColor, Theme.of(context).colorScheme.primaryContainer.toHex()); svgString = svgString.replaceAll( - _spoonColor, Theme.of(context).colorScheme.onBackground.toHex()); + _spoonColor, Theme.of(context).colorScheme.onSurface.toHex()); return SvgPicture.string(svgString); } else if (snapshot.connectionState == ConnectionState.waiting) { return const SizedBox(); diff --git a/lib/core/presentation/widgets/edit_dialog.dart b/lib/core/presentation/widgets/edit_dialog.dart index b7ff4845..054f2b8b 100644 --- a/lib/core/presentation/widgets/edit_dialog.dart +++ b/lib/core/presentation/widgets/edit_dialog.dart @@ -1,22 +1,33 @@ import 'package:flutter/material.dart'; import 'package:opennutritracker/core/domain/entity/intake_entity.dart'; +import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/generated/l10n.dart'; class EditDialog extends StatefulWidget { final IntakeEntity intakeEntity; + final bool usesImperialUnits; - const EditDialog({super.key, required this.intakeEntity}); + const EditDialog( + {super.key, required this.intakeEntity, required this.usesImperialUnits}); @override State createState() => _EditDialogState(); } class _EditDialogState extends State { + late TextEditingController amountEditingController; + @override + void initState() { + super.initState(); + double initialAmount = _convertValue( + widget.intakeEntity.amount, widget.intakeEntity.meal.mealUnit); + amountEditingController = + TextEditingController(text: initialAmount.toStringAsFixed(2)); + } @override Widget build(BuildContext context) { - var amountEditingController = TextEditingController(text: widget.intakeEntity.amount.toInt().toString()); return AlertDialog( title: Text(S.of(context).editItemDialogTitle), content: Column(mainAxisSize: MainAxisSize.min, children: [ @@ -24,16 +35,17 @@ class _EditDialogState extends State { controller: amountEditingController, keyboardType: TextInputType.number, decoration: InputDecoration( - labelText: S.of(context).quantityLabel, - suffixText: widget.intakeEntity.meal.mealUnit ?? S.of(context).gramMilliliterUnit - ), + labelText: S.of(context).quantityLabel, + suffixText: + _convertUnit(widget.intakeEntity.meal.mealUnit ?? '')), ) ]), actions: [ TextButton( onPressed: () { double newAmount = double.parse(amountEditingController.text); - Navigator.of(context).pop(newAmount); + Navigator.of(context).pop(_convertBackToMetricValue( + newAmount, widget.intakeEntity.meal.mealUnit)); }, child: Text(S.of(context).dialogOKLabel)), TextButton( @@ -44,4 +56,37 @@ class _EditDialogState extends State { ], ); } + + double _convertValue(double value, String? unit) { + switch (unit) { + case 'g': + return widget.usesImperialUnits ? UnitCalc.gToOz(value) : value; + case 'ml': + return widget.usesImperialUnits ? UnitCalc.mlToFlOz(value) : value; + default: + return value; + } + } + + double _convertBackToMetricValue(double value, String? unit) { + switch (unit) { + case 'g': + return widget.usesImperialUnits ? UnitCalc.ozToG(value) : value; + case 'ml': + return widget.usesImperialUnits ? UnitCalc.flOzToMl(value) : value; + default: + return value; + } + } + + String _convertUnit(String unit) { + switch (unit) { + case 'g': + return widget.usesImperialUnits ? 'oz' : 'g'; + case 'ml': + return widget.usesImperialUnits ? 'fl.oz' : 'ml'; + default: + return unit; + } + } } diff --git a/lib/core/presentation/widgets/home_appbar.dart b/lib/core/presentation/widgets/home_appbar.dart index 5e541f60..84963592 100644 --- a/lib/core/presentation/widgets/home_appbar.dart +++ b/lib/core/presentation/widgets/home_appbar.dart @@ -17,13 +17,13 @@ class HomeAppbar extends StatelessWidget implements PreferredSizeWidget { text: TextSpan( text: S.of(context).appTitle, style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), children: [ TextSpan( text: ' ${S.of(context).betaVersionName}', style: TextStyle( fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onBackground)), + color: Theme.of(context).colorScheme.onSurface)), ], ), maxLines: 1, @@ -35,7 +35,7 @@ class HomeAppbar extends StatelessWidget implements PreferredSizeWidget { actions: [ IconButton( icon: Icon(Icons.settings_outlined, - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), tooltip: S.of(context).settingsLabel, onPressed: () { Navigator.of(context).pushNamed(NavigationOptions.settingsRoute); diff --git a/lib/core/presentation/widgets/intake_card.dart b/lib/core/presentation/widgets/intake_card.dart index 00409b7c..5630855e 100644 --- a/lib/core/presentation/widgets/intake_card.dart +++ b/lib/core/presentation/widgets/intake_card.dart @@ -3,20 +3,23 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:opennutritracker/core/domain/entity/intake_entity.dart'; +import 'package:opennutritracker/core/presentation/widgets/meal_value_unit_text.dart'; import 'package:opennutritracker/core/utils/locator.dart'; class IntakeCard extends StatelessWidget { final IntakeEntity intake; final Function(BuildContext, IntakeEntity)? onItemLongPressed; - final Function(BuildContext, IntakeEntity)? onItemTapped; + final Function(BuildContext, IntakeEntity, bool)? onItemTapped; final bool firstListElement; + final bool usesImperialUnits; const IntakeCard( {required super.key, required this.intake, this.onItemLongPressed, this.onItemTapped, - required this.firstListElement}); + required this.firstListElement, + required this.usesImperialUnits}); @override Widget build(BuildContext context) { @@ -38,7 +41,7 @@ class IntakeCard extends StatelessWidget { ? () => onLongPressedItem(context) : null, onTap: onItemTapped != null - ? () => onTappedItem(context) + ? () => onTappedItem(context, usesImperialUnits) : null, child: Stack( children: [ @@ -62,8 +65,7 @@ class IntakeCard extends StatelessWidget { decoration: BoxDecoration( color: Theme.of(context) .colorScheme - .secondaryContainer - .withOpacity(0.5), + .secondaryContainer.withValues(alpha: 0.5), ), ), Container( @@ -72,8 +74,7 @@ class IntakeCard extends StatelessWidget { decoration: BoxDecoration( color: Theme.of(context) .colorScheme - .tertiaryContainer - .withOpacity(0.8), + .tertiaryContainer.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(20)), child: Text( '${intake.totalKcal.toInt()} kcal', @@ -103,17 +104,18 @@ class IntakeCard extends StatelessWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - Text( - '${intake.amount.toInt().toString()} ${intake.unit}', - style: Theme.of(context) + MealValueUnitText( + value: intake.amount, + meal: intake.meal, + usesImperialUnits: usesImperialUnits, + textStyle: Theme.of(context) .textTheme .titleSmall ?.copyWith( color: Theme.of(context) .colorScheme - .onSecondaryContainer - .withOpacity(0.7)), - maxLines: 1, + .onSecondaryContainer.withValues( + alpha: 0.7)), ), ], )) @@ -130,7 +132,7 @@ class IntakeCard extends StatelessWidget { onItemLongPressed?.call(context, intake); } - void onTappedItem(BuildContext context) { - onItemTapped?.call(context, intake); + void onTappedItem(BuildContext context, bool usesImperialUnits) { + onItemTapped?.call(context, intake, usesImperialUnits); } } diff --git a/lib/core/presentation/widgets/meal_value_unit_text.dart b/lib/core/presentation/widgets/meal_value_unit_text.dart new file mode 100644 index 00000000..588d9ca6 --- /dev/null +++ b/lib/core/presentation/widgets/meal_value_unit_text.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; +import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; +import 'package:opennutritracker/generated/l10n.dart'; + +class MealValueUnitText extends StatelessWidget { + final double value; + final MealEntity meal; + final bool usesImperialUnits; + final TextStyle? textStyle; + final String? prefix; + + const MealValueUnitText( + {super.key, + required this.value, + required this.meal, + required this.usesImperialUnits, + this.textStyle, + this.prefix = ''}); + + @override + Widget build(BuildContext context) { + final mealUnit = meal.mealUnit ?? S.of(context).gramMilliliterUnit; + final displayUnit = _convertUnit(context, mealUnit); + final displayValue = _convertValue(value, mealUnit); + + return Text( + '$prefix${_formatValue(displayValue)} $displayUnit', + style: textStyle, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ); + } + + double _convertValue(double value, String unit) { + switch (unit) { + case 'g': + return usesImperialUnits ? UnitCalc.gToOz(value) : value; + case 'ml': + return usesImperialUnits ? UnitCalc.mlToFlOz(value) : value; + default: + return value; + } + } + + String _convertUnit(BuildContext context, String unit) { + switch (unit) { + case 'g': + return usesImperialUnits + ? S.of(context).ozUnit + : S.of(context).gramUnit; + case 'ml': + return usesImperialUnits + ? S.of(context).flOzUnit + : S.of(context).milliliterUnit; + default: + return unit; + } + } + + String _formatValue(double value) { + final formattedValue = value.toStringAsFixed(2); + return formattedValue.endsWith('.00') + ? formattedValue.substring(0, formattedValue.length - 3) + : formattedValue.endsWith('0') + ? formattedValue.substring(0, formattedValue.length - 1) + : formattedValue; + } +} diff --git a/lib/core/presentation/widgets/placeholder_card.dart b/lib/core/presentation/widgets/placeholder_card.dart index 9670edca..fd2d9e87 100644 --- a/lib/core/presentation/widgets/placeholder_card.dart +++ b/lib/core/presentation/widgets/placeholder_card.dart @@ -35,8 +35,7 @@ class PlaceholderCard extends StatelessWidget { size: 36, color: Theme.of(context) .colorScheme - .onSurface - .withOpacity(0.5)), + .onSurface.withValues(alpha: 0.5)), ), ), ), diff --git a/lib/core/styles/color_schemes.dart b/lib/core/styles/color_schemes.dart index 4eef1df8..973cff6e 100644 --- a/lib/core/styles/color_schemes.dart +++ b/lib/core/styles/color_schemes.dart @@ -22,11 +22,9 @@ const lightColorScheme = ColorScheme( errorContainer: Color(0xFFFFDAD6), onError: Color(0xFFFFFFFF), onErrorContainer: Color(0xFF410002), - background: Color(0xFFFCFDF7), - onBackground: Color(0xFF1A1C19), surface: Color(0xFFFCFDF7), onSurface: Color(0xFF1A1C19), - surfaceVariant: Color(0xFFDDE5D9), + surfaceContainerHighest: Color(0xFFDDE5D9), onSurfaceVariant: Color(0xFF424940), outline: Color(0xFF727970), onInverseSurface: Color(0xFFF0F1EB), @@ -56,11 +54,9 @@ const darkColorScheme = ColorScheme( errorContainer: Color(0xFF93000A), onError: Color(0xFF690005), onErrorContainer: Color(0xFFFFDAD6), - background: Color(0xFF1A1C19), - onBackground: Color(0xFFE2E3DD), surface: Color(0xFF1A1C19), onSurface: Color(0xFFE2E3DD), - surfaceVariant: Color(0xFF424940), + surfaceContainerHighest: Color(0xFF424940), onSurfaceVariant: Color(0xFFC1C9BE), outline: Color(0xFF8B9389), onInverseSurface: Color(0xFF1A1C19), diff --git a/lib/core/utils/calc/unit_calc.dart b/lib/core/utils/calc/unit_calc.dart new file mode 100644 index 00000000..f28b0c98 --- /dev/null +++ b/lib/core/utils/calc/unit_calc.dart @@ -0,0 +1,47 @@ +import 'package:opennutritracker/core/utils/extensions.dart'; + +class UnitCalc { + static double cmToInches(double cm) { + return cm / 2.54; + } + + static double inchesToCm(double inches) { + return inches * 2.54; + } + + /// Converts centimeters to feet and rounds the result + static double cmToFeet(double cm) { + return (cm / 30.48).roundToPrecision(2); + } + + /// Converts feet to centimeters and rounds the result + static double feetToCm(double feet) { + return (feet * 30.48).roundToDouble(); + } + + /// Converts feet to inches and rounds the result + static double kgToLbs(double kg) { + return (kg * 2.20462).roundToDouble(); + } + + /// Converts pounds to kilograms and rounds the result + static double lbsToKg(double lbs) { + return (lbs / 2.20462).roundToDouble(); + } + + static double gToOz(double g) { + return g / 28.3495; + } + + static double ozToG(double oz) { + return oz * 28.3495; + } + + static double mlToFlOz(double ml) { + return ml / 29.5735; + } + + static double flOzToMl(double flOz) { + return flOz * 29.5735; + } +} diff --git a/lib/core/utils/extensions.dart b/lib/core/utils/extensions.dart index defb8378..3b31c063 100644 --- a/lib/core/utils/extensions.dart +++ b/lib/core/utils/extensions.dart @@ -64,9 +64,10 @@ extension FormatString on DateTime { extension ColorExtension on Color { /// Converts the color to a hexadecimal string. String toHex() { - return '#${red.toRadixString(16).padLeft(2, '0')}' - '${green.toRadixString(16).padLeft(2, '0')}' - '${blue.toRadixString(16).padLeft(2, '0')}' - '${alpha.toRadixString(16).padLeft(2, '0')}'; // Includes the alpha channel + final red = (r * 255).toInt().toRadixString(16).padLeft(2, '0'); + final green = (g * 255).toInt().toRadixString(16).padLeft(2, '0'); + final blue = (b * 255).toInt().toRadixString(16).padLeft(2, '0'); + + return '#' '$red$green$blue'.toUpperCase(); } } diff --git a/lib/core/utils/locator.dart b/lib/core/utils/locator.dart index 4d93cce7..a1337469 100644 --- a/lib/core/utils/locator.dart +++ b/lib/core/utils/locator.dart @@ -38,6 +38,7 @@ import 'package:opennutritracker/features/add_meal/data/data_sources/off_data_so import 'package:opennutritracker/features/add_meal/data/data_sources/sp_fdc_data_source.dart'; import 'package:opennutritracker/features/add_meal/data/repository/products_repository.dart'; import 'package:opennutritracker/features/add_meal/domain/usecase/search_products_usecase.dart'; +import 'package:opennutritracker/features/add_meal/presentation/bloc/add_meal_bloc.dart'; import 'package:opennutritracker/features/add_meal/presentation/bloc/food_bloc.dart'; import 'package:opennutritracker/features/add_meal/presentation/bloc/products_bloc.dart'; import 'package:opennutritracker/features/add_meal/presentation/bloc/recent_meal_bloc.dart'; @@ -74,13 +75,21 @@ Future initLocator() async { // BLoCs locator.registerLazySingleton( () => OnboardingBloc(locator(), locator())); - locator.registerLazySingleton(() => HomeBloc(locator(), locator(), locator(), - locator(), locator(), locator(), locator(), locator(), locator())); - locator.registerLazySingleton(() => DiaryBloc(locator())); + locator.registerLazySingleton(() => HomeBloc( + locator(), + locator(), + locator(), + locator(), + locator(), + locator(), + locator(), + locator(), + locator())); + locator.registerLazySingleton(() => DiaryBloc(locator(), locator())); locator.registerLazySingleton(() => CalendarDayBloc( locator(), locator(), locator(), locator(), locator(), locator())); locator.registerLazySingleton( - () => ProfileBloc(locator(), locator(), locator(), locator())); + () => ProfileBloc(locator(), locator(), locator(), locator(), locator())); locator.registerLazySingleton(() => SettingsBloc(locator(), locator())); locator.registerFactory(() => ActivitiesBloc(locator())); @@ -90,11 +99,13 @@ Future initLocator() async { () => ActivityDetailBloc(locator(), locator(), locator())); locator.registerFactory( () => MealDetailBloc(locator(), locator(), locator())); - locator.registerFactory(() => ScannerBloc(locator())); - locator.registerFactory(() => EditMealBloc()); - locator.registerFactory(() => ProductsBloc(locator())); - locator.registerFactory(() => FoodBloc(locator())); - locator.registerFactory(() => RecentMealBloc(locator())); + locator.registerFactory(() => ScannerBloc(locator(), locator())); + locator.registerFactory(() => EditMealBloc(locator())); + locator.registerFactory(() => AddMealBloc(locator())); + locator + .registerFactory(() => ProductsBloc(locator(), locator())); + locator.registerFactory(() => FoodBloc(locator(), locator())); + locator.registerFactory(() => RecentMealBloc(locator(), locator())); // UseCases locator.registerLazySingleton( diff --git a/lib/features/activity_detail/activity_detail_screen.dart b/lib/features/activity_detail/activity_detail_screen.dart index 7117c718..e1ff8725 100644 --- a/lib/features/activity_detail/activity_detail_screen.dart +++ b/lib/features/activity_detail/activity_detail_screen.dart @@ -119,7 +119,7 @@ class _ActivityDetailScreenState extends State { ?.copyWith( color: Theme.of(context) .colorScheme - .onBackground)) + .onSurface)) : const SizedBox(), ), ); diff --git a/lib/features/activity_detail/presentation/widget/activity_title_expanded.dart b/lib/features/activity_detail/presentation/widget/activity_title_expanded.dart index 4eead001..1980ee07 100644 --- a/lib/features/activity_detail/presentation/widget/activity_title_expanded.dart +++ b/lib/features/activity_detail/presentation/widget/activity_title_expanded.dart @@ -21,7 +21,7 @@ class ActivityTitleExpanded extends StatelessWidget { TextSpan( text: activity.getName(context), style: Theme.of(context).textTheme.displaySmall?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), children: [ TextSpan( text: ' ${activity.getDescription(context)} ', @@ -31,8 +31,7 @@ class ActivityTitleExpanded extends StatelessWidget { ?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.7)), + .onSurface.withValues(alpha: 0.7)), ) ]), textAlign: TextAlign.center, diff --git a/lib/features/add_activity/presentation/widgets/activity_item_card.dart b/lib/features/add_activity/presentation/widgets/activity_item_card.dart index 7af04a94..5adcbb10 100644 --- a/lib/features/add_activity/presentation/widgets/activity_item_card.dart +++ b/lib/features/add_activity/presentation/widgets/activity_item_card.dart @@ -34,14 +34,14 @@ class ActivityItemCard extends StatelessWidget { title: AutoSizeText( physicalActivityEntity.getName(context), style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: AutoSizeText( physicalActivityEntity.getDescription(context), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), maxLines: 3, overflow: TextOverflow.ellipsis, ), diff --git a/lib/features/add_meal/data/dto/fdc/fdc_food_nutriment_dto.g.dart b/lib/features/add_meal/data/dto/fdc/fdc_food_nutriment_dto.g.dart index de97fad7..71ed656e 100644 --- a/lib/features/add_meal/data/dto/fdc/fdc_food_nutriment_dto.g.dart +++ b/lib/features/add_meal/data/dto/fdc/fdc_food_nutriment_dto.g.dart @@ -8,7 +8,7 @@ part of 'fdc_food_nutriment_dto.dart'; FDCFoodNutrimentDTO _$FDCFoodNutrimentDTOFromJson(Map json) => FDCFoodNutrimentDTO( - nutrientId: json['nutrient_id'] as int?, + nutrientId: (json['nutrient_id'] as num?)?.toInt(), amount: (json['amount'] as num?)?.toDouble(), ); diff --git a/lib/features/add_meal/data/dto/fdc/fdc_word_response_dto.g.dart b/lib/features/add_meal/data/dto/fdc/fdc_word_response_dto.g.dart index 673e694d..24d32f78 100644 --- a/lib/features/add_meal/data/dto/fdc/fdc_word_response_dto.g.dart +++ b/lib/features/add_meal/data/dto/fdc/fdc_word_response_dto.g.dart @@ -8,8 +8,8 @@ part of 'fdc_word_response_dto.dart'; FDCWordResponseDTO _$FDCWordResponseDTOFromJson(Map json) => FDCWordResponseDTO( - totalHits: json['totalHits'] as int?, - currentPage: json['currentPage'] as int?, + totalHits: (json['totalHits'] as num?)?.toInt(), + currentPage: (json['currentPage'] as num?)?.toInt(), foods: (json['foods'] as List) .map((e) => FDCFoodDTO.fromJson(e as Map)) .toList(), diff --git a/lib/features/add_meal/data/dto/fdc_sp/sp_const.dart b/lib/features/add_meal/data/dto/fdc_sp/sp_const.dart index b8a62415..6cffd5ac 100644 --- a/lib/features/add_meal/data/dto/fdc_sp/sp_const.dart +++ b/lib/features/add_meal/data/dto/fdc_sp/sp_const.dart @@ -26,9 +26,7 @@ class SPConst { return fdcFoodDescriptionEn; case SupportedLanguage.de: return fdcFoodDescriptionDe; - default: - return fdcFoodDescriptionEn; - } + } } } diff --git a/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_food_dto.dart b/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_food_dto.dart index 8d80ba76..704ef040 100644 --- a/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_food_dto.dart +++ b/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_food_dto.dart @@ -29,9 +29,7 @@ class SpFdcFoodDTO { return descriptionEn; case SupportedLanguage.de: return descriptionDe; - default: - return descriptionEn; - } + } } get servingSize => portions diff --git a/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_food_dto.g.dart b/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_food_dto.g.dart index 6424ab0d..9e809a6a 100644 --- a/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_food_dto.g.dart +++ b/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_food_dto.g.dart @@ -7,7 +7,7 @@ part of 'sp_fdc_food_dto.dart'; // ************************************************************************** SpFdcFoodDTO _$SpFdcFoodDTOFromJson(Map json) => SpFdcFoodDTO( - fdcId: json['fdc_id'] as int?, + fdcId: (json['fdc_id'] as num?)?.toInt(), descriptionEn: json['description_en'] as String?, descriptionDe: json['description_de'] as String?, nutrients: (json['fdc_nutrients'] as List) diff --git a/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_portion_dto.g.dart b/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_portion_dto.g.dart index c4a0a4ce..c71e0812 100644 --- a/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_portion_dto.g.dart +++ b/lib/features/add_meal/data/dto/fdc_sp/sp_fdc_portion_dto.g.dart @@ -9,7 +9,7 @@ part of 'sp_fdc_portion_dto.dart'; SpFdcPortionDTO _$SpFdcPortionDTOFromJson(Map json) => SpFdcPortionDTO( amount: (json['amount'] as num?)?.toDouble(), - measureUnitId: json['measure_unit_id'] as int?, + measureUnitId: (json['measure_unit_id'] as num?)?.toInt(), gramWeight: (json['gram_weight'] as num?)?.toDouble(), ); diff --git a/lib/features/add_meal/data/dto/off/off_product_dto.dart b/lib/features/add_meal/data/dto/off/off_product_dto.dart index 204d1069..841c1b5c 100644 --- a/lib/features/add_meal/data/dto/off/off_product_dto.dart +++ b/lib/features/add_meal/data/dto/off/off_product_dto.dart @@ -40,9 +40,7 @@ class OFFProductDTO { case SupportedLanguage.de: localeName = product_name_de; break; - default: - return product_name_en; - } + } // If local language is not available, return available language if (localeName == null || localeName.isEmpty) { diff --git a/lib/features/add_meal/data/dto/off/off_product_response_dto.g.dart b/lib/features/add_meal/data/dto/off/off_product_response_dto.g.dart index 6028dae2..0c00eafe 100644 --- a/lib/features/add_meal/data/dto/off/off_product_response_dto.g.dart +++ b/lib/features/add_meal/data/dto/off/off_product_response_dto.g.dart @@ -9,7 +9,7 @@ part of 'off_product_response_dto.dart'; OFFProductResponseDTO _$OFFProductResponseDTOFromJson( Map json) => OFFProductResponseDTO( - status: json['status'] as int, + status: (json['status'] as num).toInt(), status_verbose: json['status_verbose'] as String, product: OFFProductDTO.fromJson(json['product'] as Map), ); diff --git a/lib/features/add_meal/data/dto/off/off_word_response_dto.g.dart b/lib/features/add_meal/data/dto/off/off_word_response_dto.g.dart index 2c93e281..a15af35d 100644 --- a/lib/features/add_meal/data/dto/off/off_word_response_dto.g.dart +++ b/lib/features/add_meal/data/dto/off/off_word_response_dto.g.dart @@ -10,8 +10,8 @@ OFFWordResponseDTO _$OFFWordResponseDTOFromJson(Map json) => OFFWordResponseDTO( count: json['count'], page: json['page'], - page_count: json['page_count'] as int?, - page_size: json['page_size'] as int?, + page_count: (json['page_count'] as num?)?.toInt(), + page_size: (json['page_size'] as num?)?.toInt(), products: (json['products'] as List) .map((e) => OFFProductDTO.fromJson(e as Map)) .toList(), diff --git a/lib/features/add_meal/domain/entity/meal_entity.dart b/lib/features/add_meal/domain/entity/meal_entity.dart index 6a60603b..4d887c95 100644 --- a/lib/features/add_meal/domain/entity/meal_entity.dart +++ b/lib/features/add_meal/domain/entity/meal_entity.dart @@ -11,6 +11,15 @@ import 'package:opennutritracker/features/add_meal/data/dto/off/off_product_dto. import 'package:opennutritracker/features/add_meal/domain/entity/meal_nutriments_entity.dart'; class MealEntity extends Equatable { + static const liquidUnits = {'ml', 'l', 'dl', 'cl', 'fl oz', 'fl.oz'}; + static const solidUnits = { + 'kg', + 'g', + 'mg', + 'µg', + 'oz', + }; + final String? code; final String? name; @@ -30,6 +39,10 @@ class MealEntity extends Equatable { final MealNutrimentsEntity nutriments; + bool get isLiquid => liquidUnits.contains(mealUnit); + + bool get isSolid => solidUnits.contains(mealUnit); + const MealEntity( {required this.code, required this.name, @@ -49,9 +62,9 @@ class MealEntity extends Equatable { name: null, url: null, mealQuantity: null, - mealUnit: 'g', + mealUnit: 'gml', servingQuantity: null, - servingUnit: 'g', + servingUnit: 'gml', nutriments: MealNutrimentsEntity.empty(), source: MealSourceEntity.custom); diff --git a/lib/features/add_meal/presentation/add_meal_screen.dart b/lib/features/add_meal/presentation/add_meal_screen.dart index a47231e9..af395e3c 100644 --- a/lib/features/add_meal/presentation/add_meal_screen.dart +++ b/lib/features/add_meal/presentation/add_meal_screen.dart @@ -4,6 +4,7 @@ import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/core/utils/navigation_options.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; import 'package:opennutritracker/features/add_meal/presentation/add_meal_type.dart'; +import 'package:opennutritracker/features/add_meal/presentation/bloc/add_meal_bloc.dart'; import 'package:opennutritracker/features/add_meal/presentation/bloc/food_bloc.dart'; import 'package:opennutritracker/features/add_meal/presentation/bloc/recent_meal_bloc.dart'; import 'package:opennutritracker/features/add_meal/presentation/widgets/default_results_widget.dart'; @@ -70,9 +71,18 @@ class _AddMealScreenState extends State appBar: AppBar( title: Text(_mealType.getTypeName(context)), actions: [ - IconButton( - onPressed: _onCustomAddButtonPressed, - icon: const Icon(Icons.add_circle_outline), + BlocBuilder( + bloc: locator()..add(InitializeAddMealEvent()), + builder: (BuildContext context, AddMealState state) { + if (state is AddMealLoadedState) { + return IconButton( + onPressed: () => + _onCustomAddButtonPressed(state.usesImperialUnits), + icon: const Icon(Icons.add_circle_outline), + ); + } + return const SizedBox(); + }, ) ], ), @@ -122,9 +132,12 @@ class _AddMealScreenState extends State itemCount: state.products.length, itemBuilder: (context, index) { return MealItemCard( - day: _day, - mealEntity: state.products[index], - addMealType: _mealType); + day: _day, + mealEntity: state.products[index], + addMealType: _mealType, + usesImperialUnits: + state.usesImperialUnits, + ); })) : const NoResultsWidget(); } else if (state is ProductsFailedState) { @@ -164,9 +177,12 @@ class _AddMealScreenState extends State itemCount: state.food.length, itemBuilder: (context, index) { return MealItemCard( - day: _day, - mealEntity: state.food[index], - addMealType: _mealType); + day: _day, + mealEntity: state.food[index], + addMealType: _mealType, + usesImperialUnits: + state.usesImperialUnits, + ); })) : const NoResultsWidget(); } else if (state is FoodFailedState) { @@ -187,8 +203,8 @@ class _AddMealScreenState extends State bloc: _recentMealBloc, builder: (context, state) { if (state is RecentMealInitial) { - _recentMealBloc - .add(const LoadRecentMealEvent(searchString: "")); + _recentMealBloc.add( + const LoadRecentMealEvent(searchString: "")); return const SizedBox(); } else if (state is RecentMealLoadingState) { return const Padding( @@ -202,10 +218,13 @@ class _AddMealScreenState extends State itemCount: state.recentMeals.length, itemBuilder: (context, index) { return MealItemCard( - day: _day, - mealEntity: - state.recentMeals[index], - addMealType: _mealType); + day: _day, + mealEntity: + state.recentMeals[index], + addMealType: _mealType, + usesImperialUnits: + state.usesImperialUnits, + ); })) : const NoResultsWidget(); } else if (state is RecentMealFailedState) { @@ -255,7 +274,7 @@ class _AddMealScreenState extends State arguments: ScannerScreenArguments(_day, _mealType.getIntakeType())); } - void _onCustomAddButtonPressed() { + void _onCustomAddButtonPressed(bool usesImperialUnits) { showDialog( context: context, builder: (context) { @@ -269,7 +288,7 @@ class _AddMealScreenState extends State TextButton( onPressed: () { Navigator.of(context).pop(); // Close dialog - _openEditMealScreen(); + _openEditMealScreen(usesImperialUnits); }, child: Text(S.of(context).buttonYesLabel)), ], @@ -277,10 +296,15 @@ class _AddMealScreenState extends State }); } - void _openEditMealScreen() { + void _openEditMealScreen(bool usesImperialUnits) { + // TODO Navigator.of(context).pushNamed(NavigationOptions.editMealRoute, arguments: EditMealScreenArguments( - _day, MealEntity.empty(), _mealType.getIntakeType())); + _day, + MealEntity.empty(), + _mealType.getIntakeType(), + usesImperialUnits, + )); } } diff --git a/lib/features/add_meal/presentation/bloc/add_meal_bloc.dart b/lib/features/add_meal/presentation/bloc/add_meal_bloc.dart new file mode 100644 index 00000000..a498eefd --- /dev/null +++ b/lib/features/add_meal/presentation/bloc/add_meal_bloc.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; + +part 'add_meal_event.dart'; + +part 'add_meal_state.dart'; + +class AddMealBloc extends Bloc { + final GetConfigUsecase _getConfigUsecase; + + AddMealBloc(this._getConfigUsecase) : super(AddMealInitialState()) { + on((event, emit) async { + emit(AddMealLoadingState()); + + final config = await _getConfigUsecase.getConfig(); + emit(AddMealLoadedState(usesImperialUnits: config.usesImperialUnits)); + }); + } +} diff --git a/lib/features/add_meal/presentation/bloc/add_meal_event.dart b/lib/features/add_meal/presentation/bloc/add_meal_event.dart new file mode 100644 index 00000000..30716b6f --- /dev/null +++ b/lib/features/add_meal/presentation/bloc/add_meal_event.dart @@ -0,0 +1,12 @@ +part of 'add_meal_bloc.dart'; + +abstract class AddMealEvent extends Equatable { + const AddMealEvent(); +} + +class InitializeAddMealEvent extends AddMealEvent { + const InitializeAddMealEvent(); + + @override + List get props => []; +} diff --git a/lib/features/add_meal/presentation/bloc/add_meal_state.dart b/lib/features/add_meal/presentation/bloc/add_meal_state.dart new file mode 100644 index 00000000..6ed027eb --- /dev/null +++ b/lib/features/add_meal/presentation/bloc/add_meal_state.dart @@ -0,0 +1,28 @@ +part of 'add_meal_bloc.dart'; + +abstract class AddMealState extends Equatable { + const AddMealState(); +} + +class AddMealInitialState extends AddMealState { + const AddMealInitialState(); + + @override + List get props => []; +} + +class AddMealLoadingState extends AddMealState { + const AddMealLoadingState(); + + @override + List get props => []; +} + +class AddMealLoadedState extends AddMealState { + final bool usesImperialUnits; + + const AddMealLoadedState({this.usesImperialUnits = false}); + + @override + List get props => [usesImperialUnits]; +} diff --git a/lib/features/add_meal/presentation/bloc/food_bloc.dart b/lib/features/add_meal/presentation/bloc/food_bloc.dart index 36559af2..f7e8912a 100644 --- a/lib/features/add_meal/presentation/bloc/food_bloc.dart +++ b/lib/features/add_meal/presentation/bloc/food_bloc.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; +import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; import 'package:opennutritracker/features/add_meal/domain/usecase/search_products_usecase.dart'; @@ -12,10 +13,12 @@ class FoodBloc extends Bloc { final log = Logger('FoodBloc'); final SearchProductsUseCase _searchProductUseCase; + final GetConfigUsecase _getConfigUsecase; String _searchString = ""; - FoodBloc(this._searchProductUseCase) : super(FoodInitial()) { + FoodBloc(this._searchProductUseCase, this._getConfigUsecase) + : super(FoodInitial()) { on((event, emit) async { if (event.searchString != _searchString) { _searchString = event.searchString; @@ -23,7 +26,10 @@ class FoodBloc extends Bloc { try { final result = await _searchProductUseCase.searchFDCFoodByString(_searchString); - emit(FoodLoadedState(food: result)); + final config = await _getConfigUsecase.getConfig(); + + emit(FoodLoadedState( + food: result, usesImperialUnits: config.usesImperialUnits)); } catch (error) { log.severe(error); emit(FoodFailedState()); diff --git a/lib/features/add_meal/presentation/bloc/food_state.dart b/lib/features/add_meal/presentation/bloc/food_state.dart index d1dadd83..74bf1e48 100644 --- a/lib/features/add_meal/presentation/bloc/food_state.dart +++ b/lib/features/add_meal/presentation/bloc/food_state.dart @@ -16,11 +16,12 @@ class FoodLoadingState extends FoodState { class FoodLoadedState extends FoodState { final List food; + final bool usesImperialUnits; - const FoodLoadedState({required this.food}); + const FoodLoadedState({required this.food, this.usesImperialUnits = false}); @override - List get props => [food]; + List get props => [food, usesImperialUnits]; } class FoodFailedState extends FoodState { diff --git a/lib/features/add_meal/presentation/bloc/products_bloc.dart b/lib/features/add_meal/presentation/bloc/products_bloc.dart index 34050a0d..ea259ee6 100644 --- a/lib/features/add_meal/presentation/bloc/products_bloc.dart +++ b/lib/features/add_meal/presentation/bloc/products_bloc.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; +import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; import 'package:opennutritracker/features/add_meal/domain/usecase/search_products_usecase.dart'; @@ -12,10 +13,12 @@ class ProductsBloc extends Bloc { final log = Logger('ProductsBloc'); final SearchProductsUseCase _searchProductUseCase; + final GetConfigUsecase _getConfigUsecase; String _searchString = ""; - ProductsBloc(this._searchProductUseCase) : super(ProductsInitial()) { + ProductsBloc(this._searchProductUseCase, this._getConfigUsecase) + : super(ProductsInitial()) { on((event, emit) async { if (event.searchString != _searchString) { _searchString = event.searchString; @@ -23,7 +26,10 @@ class ProductsBloc extends Bloc { try { final result = await _searchProductUseCase .searchOFFProductsByString(_searchString); - emit(ProductsLoadedState(products: result)); + final config = await _getConfigUsecase.getConfig(); + + emit(ProductsLoadedState( + products: result, usesImperialUnits: config.usesImperialUnits)); } catch (error) { log.severe(error); emit(ProductsFailedState()); diff --git a/lib/features/add_meal/presentation/bloc/products_state.dart b/lib/features/add_meal/presentation/bloc/products_state.dart index 43416f8b..91f4f185 100644 --- a/lib/features/add_meal/presentation/bloc/products_state.dart +++ b/lib/features/add_meal/presentation/bloc/products_state.dart @@ -16,8 +16,10 @@ class ProductsLoadingState extends ProductsState { class ProductsLoadedState extends ProductsState { final List products; + final bool usesImperialUnits; - const ProductsLoadedState({required this.products}); + const ProductsLoadedState( + {required this.products, this.usesImperialUnits = false}); @override List get props => [products]; diff --git a/lib/features/add_meal/presentation/bloc/recent_meal_bloc.dart b/lib/features/add_meal/presentation/bloc/recent_meal_bloc.dart index 9c6b647a..42b88e46 100644 --- a/lib/features/add_meal/presentation/bloc/recent_meal_bloc.dart +++ b/lib/features/add_meal/presentation/bloc/recent_meal_bloc.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; import 'package:opennutritracker/core/domain/entity/intake_entity.dart'; +import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_intake_usecase.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; @@ -13,23 +14,28 @@ class RecentMealBloc extends Bloc { final log = Logger('RecentMealBloc'); final GetIntakeUsecase _getIntakeUsecase; + final GetConfigUsecase _getConfigUsecase; - RecentMealBloc(this._getIntakeUsecase) : super(RecentMealInitial()) { + RecentMealBloc(this._getIntakeUsecase, this._getConfigUsecase) + : super(RecentMealInitial()) { on((event, emit) async { emit(RecentMealLoadingState()); try { + final config = await _getConfigUsecase.getConfig(); final recentIntake = await _getIntakeUsecase.getRecentIntake(); final searchString = (event.searchString).toLowerCase(); if (searchString.isEmpty) { emit(RecentMealLoadedState( - recentMeals: recentIntake.map((intake) => intake.meal).toList())); + recentMeals: recentIntake.map((intake) => intake.meal).toList(), + usesImperialUnits: config.usesImperialUnits)); } else { emit(RecentMealLoadedState( recentMeals: recentIntake .where(matchesSearchString(searchString)) .map((intake) => intake.meal) - .toList())); + .toList(), + usesImperialUnits: config.usesImperialUnits)); } } catch (error) { log.severe(error); @@ -40,8 +46,7 @@ class RecentMealBloc extends Bloc { bool Function(IntakeEntity) matchesSearchString(String searchString) { return (intake) => - (intake.meal.name?.toLowerCase().contains(searchString) ?? false) || + (intake.meal.name?.toLowerCase().contains(searchString) ?? false) || (intake.meal.brands?.toLowerCase().contains(searchString) ?? false); } - } diff --git a/lib/features/add_meal/presentation/bloc/recent_meal_state.dart b/lib/features/add_meal/presentation/bloc/recent_meal_state.dart index 6a961d0c..d44128cb 100644 --- a/lib/features/add_meal/presentation/bloc/recent_meal_state.dart +++ b/lib/features/add_meal/presentation/bloc/recent_meal_state.dart @@ -16,11 +16,13 @@ class RecentMealLoadingState extends RecentMealState { class RecentMealLoadedState extends RecentMealState { final List recentMeals; + final bool usesImperialUnits; - const RecentMealLoadedState({required this.recentMeals}); + const RecentMealLoadedState( + {required this.recentMeals, this.usesImperialUnits = false}); @override - List get props => [recentMeals]; + List get props => [recentMeals, usesImperialUnits]; } class RecentMealFailedState extends RecentMealState { diff --git a/lib/features/add_meal/presentation/widgets/meal_item_card.dart b/lib/features/add_meal/presentation/widgets/meal_item_card.dart index 6e4ba96b..4af3bcee 100644 --- a/lib/features/add_meal/presentation/widgets/meal_item_card.dart +++ b/lib/features/add_meal/presentation/widgets/meal_item_card.dart @@ -2,23 +2,25 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:opennutritracker/core/presentation/widgets/meal_value_unit_text.dart'; import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/core/utils/navigation_options.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; import 'package:opennutritracker/features/add_meal/presentation/add_meal_type.dart'; import 'package:opennutritracker/features/meal_detail/meal_detail_screen.dart'; -import 'package:opennutritracker/generated/l10n.dart'; class MealItemCard extends StatelessWidget { final DateTime day; final AddMealType addMealType; final MealEntity mealEntity; + final bool usesImperialUnits; const MealItemCard( {super.key, required this.day, required this.mealEntity, - required this.addMealType}); + required this.addMealType, + required this.usesImperialUnits}); @override Widget build(BuildContext context) { @@ -55,7 +57,7 @@ class MealItemCard extends StatelessWidget { TextSpan( text: mealEntity.name ?? "?", style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), children: [ TextSpan( text: ' ${mealEntity.brands ?? ""}', @@ -65,18 +67,17 @@ class MealItemCard extends StatelessWidget { ?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.8))) + .onSurface + .withValues(alpha: 0.8))), ]), style: Theme.of(context).textTheme.titleLarge, maxLines: 2, overflow: TextOverflow.ellipsis), subtitle: mealEntity.mealQuantity != null - ? AutoSizeText( - '${mealEntity.mealQuantity}${mealEntity.mealUnit ?? S.of(context).gramMilliliterUnit}', - style: Theme.of(context).textTheme.titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis) + ? MealValueUnitText( + value: double.parse(mealEntity.mealQuantity ?? "0"), + meal: mealEntity, + usesImperialUnits: usesImperialUnits) : const SizedBox(), trailing: IconButton( style: IconButton.styleFrom( @@ -95,6 +96,6 @@ class MealItemCard extends StatelessWidget { void _onItemPressed(BuildContext context) { Navigator.of(context).pushNamed(NavigationOptions.mealDetailRoute, arguments: MealDetailScreenArguments( - mealEntity, addMealType.getIntakeType(), day)); + mealEntity, addMealType.getIntakeType(), day, usesImperialUnits)); } } diff --git a/lib/features/diary/diary_page.dart b/lib/features/diary/diary_page.dart index fde9c008..28c21c8f 100644 --- a/lib/features/diary/diary_page.dart +++ b/lib/features/diary/diary_page.dart @@ -5,12 +5,16 @@ import 'package:opennutritracker/core/domain/entity/intake_entity.dart'; import 'package:opennutritracker/core/domain/entity/tracked_day_entity.dart'; import 'package:opennutritracker/core/domain/entity/user_activity_entity.dart'; import 'package:opennutritracker/core/utils/locator.dart'; +import 'package:opennutritracker/features/add_meal/presentation/add_meal_type.dart'; import 'package:opennutritracker/features/diary/presentation/bloc/calendar_day_bloc.dart'; import 'package:opennutritracker/features/diary/presentation/bloc/diary_bloc.dart'; import 'package:opennutritracker/features/diary/presentation/widgets/diary_table_calendar.dart'; import 'package:opennutritracker/features/diary/presentation/widgets/day_info_widget.dart'; +import 'package:opennutritracker/features/meal_detail/presentation/bloc/meal_detail_bloc.dart'; import 'package:opennutritracker/generated/l10n.dart'; +import '../../core/domain/entity/intake_type_entity.dart'; + class DiaryPage extends StatefulWidget { const DiaryPage({super.key}); @@ -23,6 +27,7 @@ class _DiaryPageState extends State with WidgetsBindingObserver { late DiaryBloc _diaryBloc; late CalendarDayBloc _calendarDayBloc; + late MealDetailBloc _mealDetailBloc; static const _calendarDurationDays = Duration(days: 356); final _currentDate = DateTime.now(); @@ -34,6 +39,7 @@ class _DiaryPageState extends State with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); _diaryBloc = locator(); _calendarDayBloc = locator(); + _mealDetailBloc = locator(); super.initState(); } @@ -53,7 +59,8 @@ class _DiaryPageState extends State with WidgetsBindingObserver { } else if (state is DiaryLoadingState) { return _getLoadingContent(); } else if (state is DiaryLoadedState) { - return _getLoadedContent(context, state.trackedDayMap); + return _getLoadedContent( + context, state.trackedDayMap, state.usesImperialUnits); } return const SizedBox(); }, @@ -72,8 +79,8 @@ class _DiaryPageState extends State with WidgetsBindingObserver { Widget _getLoadingContent() => const Center(child: CircularProgressIndicator()); - Widget _getLoadedContent( - BuildContext context, Map trackedDaysMap) { + Widget _getLoadedContent(BuildContext context, + Map trackedDaysMap, bool usesImperialUnits) { return ListView( children: [ DiaryTableCalendar( @@ -103,6 +110,9 @@ class _DiaryPageState extends State with WidgetsBindingObserver { snackIntake: state.snackIntakeList, onDeleteIntake: _onDeleteIntakeItem, onDeleteActivity: _onDeleteActivityItem, + onCopyIntake: _onCopyIntakeItem, + onCopyActivity: _onCopyActivityItem, + usesImperialUnits: usesImperialUnits, ); } return const SizedBox(); @@ -138,6 +148,29 @@ class _DiaryPageState extends State with WidgetsBindingObserver { } } + void _onCopyIntakeItem(IntakeEntity intakeEntity, + TrackedDayEntity? trackedDayEntity, AddMealType? type) async { + IntakeTypeEntity finalType; + if (type == null) { + finalType = intakeEntity.type; + } else { + finalType = type.getIntakeType(); + } + _mealDetailBloc.addIntake( + context, + intakeEntity.unit, + intakeEntity.amount.toString(), + finalType, + intakeEntity.meal, + DateTime.now()); + _diaryBloc.updateHomePage(); + } + + void _onCopyActivityItem(UserActivityEntity userActivityEntity, + TrackedDayEntity? trackedDayEntity) async { + log.info("Should copy activity"); + } + void _onDateSelected( DateTime newDate, Map trackedDaysMap) { setState(() { diff --git a/lib/features/diary/presentation/bloc/diary_bloc.dart b/lib/features/diary/presentation/bloc/diary_bloc.dart index 264bccfb..e55f22f2 100644 --- a/lib/features/diary/presentation/bloc/diary_bloc.dart +++ b/lib/features/diary/presentation/bloc/diary_bloc.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:opennutritracker/core/domain/entity/tracked_day_entity.dart'; +import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_tracked_day_usecase.dart'; import 'package:opennutritracker/core/utils/extensions.dart'; import 'package:opennutritracker/core/utils/locator.dart'; @@ -12,13 +13,18 @@ part 'diary_state.dart'; class DiaryBloc extends Bloc { final GetTrackedDayUsecase _getDayTrackedUsecase; + final GetConfigUsecase _getConfigUsecase; DateTime currentDay = DateTime.now(); - DiaryBloc(this._getDayTrackedUsecase) : super(DiaryInitial()) { + DiaryBloc(this._getDayTrackedUsecase, this._getConfigUsecase) + : super(DiaryInitial()) { on((event, emit) async { emit(DiaryLoadingState()); + final usesImperialUnits = + (await _getConfigUsecase.getConfig()).usesImperialUnits; + currentDay = DateTime.now(); const yearDuration = Duration(days: 356); @@ -30,7 +36,7 @@ class DiaryBloc extends Bloc { trackedDay.day.toParsedDay(): trackedDay }; - emit(DiaryLoadedState(trackedDaysMap)); + emit(DiaryLoadedState(trackedDaysMap, usesImperialUnits)); }); } diff --git a/lib/features/diary/presentation/bloc/diary_state.dart b/lib/features/diary/presentation/bloc/diary_state.dart index 752627f8..95410dec 100644 --- a/lib/features/diary/presentation/bloc/diary_state.dart +++ b/lib/features/diary/presentation/bloc/diary_state.dart @@ -16,9 +16,10 @@ class DiaryLoadingState extends DiaryState { class DiaryLoadedState extends DiaryState { final Map trackedDayMap; + final bool usesImperialUnits; - const DiaryLoadedState(this.trackedDayMap); + const DiaryLoadedState(this.trackedDayMap, this.usesImperialUnits); @override - List get props => [trackedDayMap]; + List get props => [trackedDayMap, usesImperialUnits]; } diff --git a/lib/features/diary/presentation/widgets/day_info_widget.dart b/lib/features/diary/presentation/widgets/day_info_widget.dart index 851c818d..865ff9ea 100644 --- a/lib/features/diary/presentation/widgets/day_info_widget.dart +++ b/lib/features/diary/presentation/widgets/day_info_widget.dart @@ -4,12 +4,15 @@ import 'package:opennutritracker/core/domain/entity/intake_entity.dart'; import 'package:opennutritracker/core/domain/entity/tracked_day_entity.dart'; import 'package:opennutritracker/core/domain/entity/user_activity_entity.dart'; import 'package:opennutritracker/core/presentation/widgets/activity_vertial_list.dart'; +import 'package:opennutritracker/core/presentation/widgets/copy_dialog.dart'; import 'package:opennutritracker/core/presentation/widgets/delete_dialog.dart'; import 'package:opennutritracker/core/utils/custom_icons.dart'; import 'package:opennutritracker/features/add_meal/presentation/add_meal_type.dart'; import 'package:opennutritracker/features/home/presentation/widgets/intake_vertical_list.dart'; import 'package:opennutritracker/generated/l10n.dart'; +import '../../../../core/presentation/widgets/copy_or_delete_dialog.dart'; + class DayInfoWidget extends StatelessWidget { final DateTime selectedDay; final TrackedDayEntity? trackedDayEntity; @@ -18,22 +21,32 @@ class DayInfoWidget extends StatelessWidget { final List lunchIntake; final List dinnerIntake; final List snackIntake; + + final bool usesImperialUnits; final Function(IntakeEntity intake, TrackedDayEntity? trackedDayEntity) onDeleteIntake; final Function(UserActivityEntity userActivityEntity, TrackedDayEntity? trackedDayEntity) onDeleteActivity; + final Function(IntakeEntity intake, TrackedDayEntity? trackedDayEntity, + AddMealType? type) onCopyIntake; + final Function(UserActivityEntity userActivityEntity, + TrackedDayEntity? trackedDayEntity) onCopyActivity; - const DayInfoWidget( - {super.key, - required this.selectedDay, - required this.trackedDayEntity, - required this.userActivities, - required this.breakfastIntake, - required this.lunchIntake, - required this.dinnerIntake, - required this.snackIntake, - required this.onDeleteIntake, - required this.onDeleteActivity}); + const DayInfoWidget({ + super.key, + required this.selectedDay, + required this.trackedDayEntity, + required this.userActivities, + required this.breakfastIntake, + required this.lunchIntake, + required this.dinnerIntake, + required this.snackIntake, + required this.usesImperialUnits, + required this.onDeleteIntake, + required this.onDeleteActivity, + required this.onCopyIntake, + required this.onCopyActivity, + }); @override Widget build(BuildContext context) { @@ -57,8 +70,7 @@ class DayInfoWidget extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.7))), + .onSurface.withValues(alpha: 0.7))), ) : const SizedBox(), trackedDay != null @@ -97,8 +109,7 @@ class DayInfoWidget extends StatelessWidget { ?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.7))) + .onSurface.withValues(alpha: 0.7))), ], ), ) @@ -116,6 +127,7 @@ class DayInfoWidget extends StatelessWidget { addMealType: AddMealType.breakfastType, intakeList: breakfastIntake, onItemLongPressedCallback: onIntakeItemLongPressed, + usesImperialUnits: usesImperialUnits, ), IntakeVerticalList( day: selectedDay, @@ -124,6 +136,7 @@ class DayInfoWidget extends StatelessWidget { addMealType: AddMealType.lunchType, intakeList: lunchIntake, onItemLongPressedCallback: onIntakeItemLongPressed, + usesImperialUnits: usesImperialUnits, ), IntakeVerticalList( day: selectedDay, @@ -132,6 +145,7 @@ class DayInfoWidget extends StatelessWidget { addMealType: AddMealType.dinnerType, intakeList: dinnerIntake, onItemLongPressedCallback: onIntakeItemLongPressed, + usesImperialUnits: usesImperialUnits, ), IntakeVerticalList( day: selectedDay, @@ -140,6 +154,7 @@ class DayInfoWidget extends StatelessWidget { addMealType: AddMealType.snackType, intakeList: snackIntake, onItemLongPressedCallback: onIntakeItemLongPressed, + usesImperialUnits: usesImperialUnits, ), const SizedBox(height: 16.0) ], @@ -171,16 +186,46 @@ class DayInfoWidget extends StatelessWidget { return 'Carbs: $carbsTracked/${carbsGoal}g, Fat: $fatTracked/${fatGoal}g, Protein: $proteinTracked/${proteinGoal}g'; } - void onIntakeItemLongPressed( + void showCopyOrDeleteIntakeDialog( + BuildContext context, IntakeEntity intakeEntity) async { + final copyOrDelete = await showDialog( + context: context, builder: (context) => const CopyOrDeleteDialog()); + if (context.mounted) { + if (copyOrDelete != null && !copyOrDelete) { + showDeleteIntakeDialog(context, intakeEntity); + } else if (copyOrDelete != null && copyOrDelete) { + showCopyDialog(context, intakeEntity); + } + } + } + + void showCopyDialog(BuildContext context, IntakeEntity intakeEntity) async { + const copyDialog = CopyDialog(); + final selectedMealType = await showDialog( + context: context, builder: (context) => copyDialog); + if (selectedMealType != null) { + onCopyIntake(intakeEntity, null, selectedMealType); + } + } + + void showDeleteIntakeDialog( BuildContext context, IntakeEntity intakeEntity) async { final shouldDeleteIntake = await showDialog( context: context, builder: (context) => const DeleteDialog()); - if (shouldDeleteIntake != null) { onDeleteIntake(intakeEntity, trackedDayEntity); } } + void onIntakeItemLongPressed( + BuildContext context, IntakeEntity intakeEntity) async { + if (DateUtils.isSameDay(selectedDay, DateTime.now())) { + showDeleteIntakeDialog(context, intakeEntity); + } else { + showCopyOrDeleteIntakeDialog(context, intakeEntity); + } + } + void onActivityItemLongPressed( BuildContext context, UserActivityEntity activityEntity) async { final shouldDeleteActivity = await showDialog( diff --git a/lib/features/diary/presentation/widgets/diary_table_calendar.dart b/lib/features/diary/presentation/widgets/diary_table_calendar.dart index ccc2b01f..80d48af8 100644 --- a/lib/features/diary/presentation/widgets/diary_table_calendar.dart +++ b/lib/features/diary/presentation/widgets/diary_table_calendar.dart @@ -44,7 +44,7 @@ class _DiaryTableCalendarState extends State { Theme.of(context).textTheme.bodyMedium ?? const TextStyle(), todayDecoration: BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, width: 2.0), shape: BoxShape.circle), selectedTextStyle: Theme.of(context) diff --git a/lib/features/edit_meal/presentation/bloc/edit_meal_bloc.dart b/lib/features/edit_meal/presentation/bloc/edit_meal_bloc.dart index 34bf89ef..0e353e65 100644 --- a/lib/features/edit_meal/presentation/bloc/edit_meal_bloc.dart +++ b/lib/features/edit_meal/presentation/bloc/edit_meal_bloc.dart @@ -1,8 +1,26 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/utils/extensions.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_nutriments_entity.dart'; -class EditMealBloc { +part 'edit_meal_state.dart'; + +part 'edit_meal_event.dart'; + +class EditMealBloc extends Bloc { + final GetConfigUsecase _getConfigUsecase; + + EditMealBloc(this._getConfigUsecase) : super(EditMealInitial()) { + on((event, emit) async { + emit(EditMealLoadingState()); + + final config = await _getConfigUsecase.getConfig(); + emit(EditMealLoadedState(usesImperialUnits: config.usesImperialUnits)); + }); + } + MealEntity createNewMealEntity( MealEntity oldMealEntity, String nameText, @@ -15,13 +33,13 @@ class EditMealBloc { String carbsText, String fatText, String proteinText) { + final baseQuantityDouble = double.tryParse(baseQuantity); + + final double factorTo100g = + baseQuantityDouble != null ? (100 / baseQuantityDouble) : 1; - final baseQuantityDouble = double.tryParse(baseQuantity); - - final double factorTo100g = baseQuantityDouble != null ? (100/baseQuantityDouble): 1; - - double? multiplyIfNotNull(double? nutritmentValue) { - return nutritmentValue != null ? nutritmentValue * factorTo100g: null; + double? multiplyIfNotNull(double? nutrimentValue) { + return nutrimentValue != null ? nutrimentValue * factorTo100g : null; } final newMealNutriments = MealNutrimentsEntity( @@ -30,7 +48,8 @@ class EditMealBloc { fat100: multiplyIfNotNull(fatText.toDoubleOrNull()), proteins100: multiplyIfNotNull(proteinText.toDoubleOrNull()), sugars100: multiplyIfNotNull(oldMealEntity.nutriments.sugars100), - saturatedFat100: multiplyIfNotNull(oldMealEntity.nutriments.saturatedFat100), + saturatedFat100: + multiplyIfNotNull(oldMealEntity.nutriments.saturatedFat100), fiber100: multiplyIfNotNull(oldMealEntity.nutriments.fiber100)); return MealEntity( diff --git a/lib/features/edit_meal/presentation/bloc/edit_meal_event.dart b/lib/features/edit_meal/presentation/bloc/edit_meal_event.dart new file mode 100644 index 00000000..e764a92e --- /dev/null +++ b/lib/features/edit_meal/presentation/bloc/edit_meal_event.dart @@ -0,0 +1,13 @@ +part of 'edit_meal_bloc.dart'; + +abstract class EditMealEvent extends Equatable { + const EditMealEvent(); +} + +class InitializeEditMealEvent extends EditMealEvent { + + const InitializeEditMealEvent(); + + @override + List get props => []; +} diff --git a/lib/features/edit_meal/presentation/bloc/edit_meal_state.dart b/lib/features/edit_meal/presentation/bloc/edit_meal_state.dart new file mode 100644 index 00000000..806ac4f4 --- /dev/null +++ b/lib/features/edit_meal/presentation/bloc/edit_meal_state.dart @@ -0,0 +1,24 @@ +part of 'edit_meal_bloc.dart'; + +abstract class EditMealState extends Equatable { + const EditMealState(); +} + +class EditMealInitial extends EditMealState { + @override + List get props => []; +} + +class EditMealLoadingState extends EditMealState { + @override + List get props => []; +} + +class EditMealLoadedState extends EditMealState { + final bool usesImperialUnits; + + const EditMealLoadedState({this.usesImperialUnits = false}); + + @override + List get props => [usesImperialUnits]; +} diff --git a/lib/features/edit_meal/presentation/edit_meal_screen.dart b/lib/features/edit_meal/presentation/edit_meal_screen.dart index ae851fab..2a8d5d00 100644 --- a/lib/features/edit_meal/presentation/edit_meal_screen.dart +++ b/lib/features/edit_meal/presentation/edit_meal_screen.dart @@ -1,8 +1,10 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:logging/logging.dart'; import 'package:opennutritracker/core/domain/entity/intake_type_entity.dart'; +import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/core/utils/custom_text_input_formatter.dart'; import 'package:opennutritracker/core/utils/extensions.dart'; import 'package:opennutritracker/core/utils/locator.dart'; @@ -26,6 +28,7 @@ class _EditMealScreenState extends State { late MealEntity _mealEntity; late DateTime _day; late IntakeTypeEntity _intakeTypeEntity; + late bool _usesImperialUnits; late EditMealBloc _editMealBloc; @@ -40,12 +43,14 @@ class _EditMealScreenState extends State { final _proteinTextController = TextEditingController(); final _units = ['g', 'ml', 'g/ml']; - late String? _dropdownUnitValue; - late List _mealUnitDropdownItems; + late String? selectedUnit; - String baseQuantity = "100"; - String baseQuantityUnit = " g/ml"; + // late List _mealUnitDropdownItems; + late List> _mealUnitButtonSegment; + // TODO: Add base quantity and unit + String baseQuantity = "100"; + String baseQuantityUnit = " g/ml"; @override void initState() { @@ -66,6 +71,7 @@ class _EditMealScreenState extends State { _mealEntity = args.mealEntity; _day = args.day; _intakeTypeEntity = args.intakeTypeEntity; + _usesImperialUnits = args.usesImperialUnits; _nameTextController.text = _mealEntity.name ?? ""; _brandsTextController.text = _mealEntity.brands ?? ""; @@ -79,16 +85,32 @@ class _EditMealScreenState extends State { _fatTextController.text = _mealEntity.nutriments.fat100.toStringOrEmpty(); _proteinTextController.text = _mealEntity.nutriments.proteins100.toStringOrEmpty(); - _dropdownUnitValue = _switchDropdownUnit(_mealEntity.mealUnit); + selectedUnit = _switchButtonUnit(_mealEntity.mealUnit); - _mealUnitDropdownItems = [ - DropdownMenuItem( - value: _units[0], child: Text(S.of(context).gramUnit)), - DropdownMenuItem( - value: _units[1], child: Text(S.of(context).milliliterUnit)), - DropdownMenuItem( - value: _units[2], - child: Text(S.of(context).gramMilliliterUnit)), + // Convert meal size to imperial units if necessary + if (_usesImperialUnits) { + _mealQuantityTextController.text = _convertToImperial( + _mealQuantityTextController.text, _mealEntity.mealUnit ?? "0"); + _servingQuantityTextController.text = _convertToImperial( + _servingQuantityTextController.text, _mealEntity.mealUnit ?? "0"); + } + + _mealUnitButtonSegment = [ + ButtonSegment( + value: _units[0], + label: Text( + _usesImperialUnits ? S.of(context).ozUnit : S.of(context).gramUnit), + ), + ButtonSegment( + value: _units[1], + label: Text(_usesImperialUnits + ? S.of(context).flOzUnit + : S.of(context).milliliterUnit), + ), + ButtonSegment( + value: _units[2], + label: Text(S.of(context).gramMilliliterUnit), + ), ]; super.didChangeDependencies(); @@ -103,131 +125,166 @@ class _EditMealScreenState extends State { Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), child: FilledButton( - onPressed: _onSavePressed, + onPressed: () => _onSavePressed(_usesImperialUnits), child: Text(S.of(context).buttonSaveLabel)), ) ], ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - Center( - child: ClipOval( - child: CachedNetworkImage( - cacheManager: locator(), - width: 120, - height: 120, - placeholder: (context, string) => const DefaultMealImage(), - errorWidget: (context, exception, stacktrace) => - const DefaultMealImage(), - fit: BoxFit.cover, - imageUrl: _mealEntity.mainImageUrl ?? "", - ), - )), - const SizedBox(height: 32), - TextFormField( - controller: _nameTextController, - decoration: InputDecoration( - labelText: S.of(context).mealNameLabel, - border: const OutlineInputBorder()), - keyboardType: TextInputType.text, - ), - const SizedBox(height: 16), - TextFormField( - controller: _brandsTextController, - decoration: InputDecoration( - labelText: S.of(context).mealBrandsLabel, - border: const OutlineInputBorder()), - keyboardType: TextInputType.text, - ), - const SizedBox(height: 32), - TextFormField( - controller: _mealQuantityTextController, - decoration: InputDecoration( - labelText: S.of(context).mealSizeLabel, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - TextFormField( - controller: _servingQuantityTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).servingSizeLabel, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - DropdownButtonFormField( - value: _dropdownUnitValue ?? _units.first, - items: _mealUnitDropdownItems, - onChanged: (text) { - _dropdownUnitValue = _switchDropdownUnit(text); - }, - decoration: InputDecoration( - labelText: S.of(context).mealUnitLabel, - border: const OutlineInputBorder()), - ), - const SizedBox(height: 48), - TextFormField( - controller: _baseQuantityTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).baseQuantityLabel, - border: const OutlineInputBorder()), - keyboardType: TextInputType.number, - ), - const SizedBox(height: 48), - TextFormField( - controller: _kcalTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).mealKcalLabel+baseQuantity+baseQuantityUnit, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - TextFormField( - controller: _carbsTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).mealCarbsLabel+baseQuantity+baseQuantityUnit, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - TextFormField( - controller: _fatTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).mealFatLabel+baseQuantity+baseQuantityUnit, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - const SizedBox(height: 16), - TextFormField( - controller: _proteinTextController, - inputFormatters: CustomTextInputFormatter.doubleOnly(), - decoration: InputDecoration( - labelText: S.of(context).mealProteinLabel+baseQuantity+baseQuantityUnit, - border: const OutlineInputBorder()), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - ], + body: BlocBuilder( + bloc: locator()..add(InitializeEditMealEvent()), + builder: (BuildContext context, EditMealState state) { + if (state is EditMealLoadingState) { + return _getLoadingContent(); + } else if (state is EditMealLoadedState) { + return _getLoadedContent(state.usesImperialUnits); + } + return const SizedBox.shrink(); + }, ), ); } - void _onSavePressed() { + Widget _getLoadingContent() { + return const Center( + child: CircularProgressIndicator(), + ); + } + + Widget _getLoadedContent(bool usesImperialUnits) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + Center( + child: ClipOval( + child: CachedNetworkImage( + cacheManager: locator(), + width: 120, + height: 120, + placeholder: (context, string) => const DefaultMealImage(), + errorWidget: (context, exception, stacktrace) => + const DefaultMealImage(), + fit: BoxFit.cover, + imageUrl: _mealEntity.mainImageUrl ?? "", + ), + )), + const SizedBox(height: 32), + TextFormField( + controller: _nameTextController, + decoration: InputDecoration( + labelText: S.of(context).mealNameLabel, + border: const OutlineInputBorder()), + keyboardType: TextInputType.text, + ), + const SizedBox(height: 16), + TextFormField( + controller: _brandsTextController, + decoration: InputDecoration( + labelText: S.of(context).mealBrandsLabel, + border: const OutlineInputBorder()), + keyboardType: TextInputType.text, + ), + const SizedBox(height: 32), + TextFormField( + controller: _mealQuantityTextController, + decoration: InputDecoration( + labelText: _usesImperialUnits + ? S.of(context).mealSizeLabelImperial + : S.of(context).mealSizeLabel, + border: const OutlineInputBorder()), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + ), + const SizedBox(height: 16), + TextFormField( + controller: _servingQuantityTextController, + inputFormatters: CustomTextInputFormatter.doubleOnly(), + decoration: InputDecoration( + labelText: _usesImperialUnits + ? S.of(context).servingSizeLabelImperial + : S.of(context).servingSizeLabel, + border: const OutlineInputBorder()), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + ), + const SizedBox(height: 16), + SegmentedButton( + segments: _mealUnitButtonSegment, + selected: {selectedUnit ?? _units[2]}, + onSelectionChanged: (Set newSelection) { + setState(() { + selectedUnit = newSelection.first; + }); + }, + ), + const SizedBox(height: 48), + TextFormField( + controller: _baseQuantityTextController, + inputFormatters: CustomTextInputFormatter.doubleOnly(), + decoration: InputDecoration( + labelText: S.of(context).baseQuantityLabel, + border: const OutlineInputBorder()), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 48), + TextFormField( + controller: _kcalTextController, + inputFormatters: CustomTextInputFormatter.doubleOnly(), + decoration: InputDecoration( + labelText: + S.of(context).mealKcalLabel + baseQuantity + baseQuantityUnit, + border: const OutlineInputBorder()), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + ), + const SizedBox(height: 16), + TextFormField( + controller: _carbsTextController, + inputFormatters: CustomTextInputFormatter.doubleOnly(), + decoration: InputDecoration( + labelText: S.of(context).mealCarbsLabel + + baseQuantity + + baseQuantityUnit, + border: const OutlineInputBorder()), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + ), + const SizedBox(height: 16), + TextFormField( + controller: _fatTextController, + inputFormatters: CustomTextInputFormatter.doubleOnly(), + decoration: InputDecoration( + labelText: + S.of(context).mealFatLabel + baseQuantity + baseQuantityUnit, + border: const OutlineInputBorder()), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + ), + const SizedBox(height: 16), + TextFormField( + controller: _proteinTextController, + inputFormatters: CustomTextInputFormatter.doubleOnly(), + decoration: InputDecoration( + labelText: S.of(context).mealProteinLabel + + baseQuantity + + baseQuantityUnit, + border: const OutlineInputBorder()), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + ), + ], + ); + } + + void _onSavePressed(bool usesImperialUnits) { try { + // Convert meal size back to metric units if necessary + final mealQuantity = usesImperialUnits + ? _convertToMetric( + _mealQuantityTextController.text, _mealEntity.mealUnit ?? "0") + : _mealQuantityTextController.text; + final newMealEntity = _editMealBloc.createNewMealEntity( _mealEntity, _nameTextController.text, _brandsTextController.text, - _mealQuantityTextController.text, + mealQuantity, _servingQuantityTextController.text, _baseQuantityTextController.text, - _dropdownUnitValue, + selectedUnit, _kcalTextController.text, _carbsTextController.text, _fatTextController.text, @@ -237,7 +294,7 @@ class _EditMealScreenState extends State { NavigationOptions.mealDetailRoute, ModalRoute.withName(NavigationOptions.addMealRoute), arguments: MealDetailScreenArguments( - newMealEntity, _intakeTypeEntity, _day)); + newMealEntity, _intakeTypeEntity, _day, usesImperialUnits)); } catch (exception, stacktrace) { log.warning("Error while creating new meal entity"); Sentry.captureException(exception, stackTrace: stacktrace); @@ -247,14 +304,38 @@ class _EditMealScreenState extends State { } } - String? _switchDropdownUnit(String? unit) { - String? dropdownValue; + String? _switchButtonUnit(String? unit) { + String? selectedUnit; if (!_units.contains(unit)) { - dropdownValue = _units.first; + selectedUnit = _units[2]; // Default to g/ml } else { - dropdownValue = unit; + selectedUnit = unit; + } + return selectedUnit; + } + + String _convertToImperial(String value, String unit) { + final double quantityValue = double.tryParse(value) ?? 0.0; + switch (unit) { + case 'g': + return (UnitCalc.gToOz(quantityValue)).toStringAsFixed(2); + case 'ml': + return (UnitCalc.mlToFlOz(quantityValue)).toStringAsFixed(2); + default: + return value; + } + } + + String _convertToMetric(String value, String unit) { + final double quantityValue = double.tryParse(value) ?? 0.0; + switch (unit) { + case 'g': + return (UnitCalc.ozToG(quantityValue)).toStringAsFixed(2); + case 'ml': + return (UnitCalc.flOzToMl(quantityValue)).toStringAsFixed(2); + default: + return value; } - return dropdownValue; } } @@ -262,6 +343,8 @@ class EditMealScreenArguments { final DateTime day; final MealEntity mealEntity; final IntakeTypeEntity intakeTypeEntity; + final bool usesImperialUnits; - EditMealScreenArguments(this.day, this.mealEntity, this.intakeTypeEntity); + EditMealScreenArguments( + this.day, this.mealEntity, this.intakeTypeEntity, this.usesImperialUnits); } diff --git a/lib/features/home/home_page.dart b/lib/features/home/home_page.dart index ff986366..0b54320c 100644 --- a/lib/features/home/home_page.dart +++ b/lib/features/home/home_page.dart @@ -69,7 +69,8 @@ class _HomePageState extends State with WidgetsBindingObserver { state.lunchIntakeList, state.dinnerIntakeList, state.snackIntakeList, - state.userActivityList); + state.userActivityList, + state.usesImperialUnits); } else { return _getLoadingContent(); } @@ -109,7 +110,8 @@ class _HomePageState extends State with WidgetsBindingObserver { List lunchIntakeList, List dinnerIntakeList, List snackIntakeList, - List userActivities) { + List userActivities, + bool usesImperialUnits) { if (showDisclaimerDialog) { _showDisclaimerDialog(context); } @@ -141,6 +143,7 @@ class _HomePageState extends State with WidgetsBindingObserver { intakeList: breakfastIntakeList, onItemDragCallback: onIntakeItemDrag, onItemTappedCallback: onIntakeItemTapped, + usesImperialUnits: usesImperialUnits, ), IntakeVerticalList( day: DateTime.now(), @@ -150,6 +153,7 @@ class _HomePageState extends State with WidgetsBindingObserver { intakeList: lunchIntakeList, onItemDragCallback: onIntakeItemDrag, onItemTappedCallback: onIntakeItemTapped, + usesImperialUnits: usesImperialUnits, ), IntakeVerticalList( day: DateTime.now(), @@ -159,6 +163,7 @@ class _HomePageState extends State with WidgetsBindingObserver { intakeList: dinnerIntakeList, onItemDragCallback: onIntakeItemDrag, onItemTappedCallback: onIntakeItemTapped, + usesImperialUnits: usesImperialUnits, ), IntakeVerticalList( day: DateTime.now(), @@ -168,6 +173,7 @@ class _HomePageState extends State with WidgetsBindingObserver { intakeList: snackIntakeList, onItemDragCallback: onIntakeItemDrag, onItemTappedCallback: onIntakeItemTapped, + usesImperialUnits: usesImperialUnits, ), const SizedBox(height: 48.0) ]), @@ -177,7 +183,8 @@ class _HomePageState extends State with WidgetsBindingObserver { visible: _isDragging, child: Container( height: 70, - color: Theme.of(context).colorScheme.error.withOpacity(0.3), + color: Theme.of(context).colorScheme.error + ..withValues(alpha: 0.3), child: DragTarget( onAcceptWithDetails: (data) { _confirmDelete(context, data.data); @@ -239,14 +246,15 @@ class _HomePageState extends State with WidgetsBindingObserver { }); } - void onIntakeItemTapped( - BuildContext context, IntakeEntity intakeEntity) async { + void onIntakeItemTapped(BuildContext context, IntakeEntity intakeEntity, + bool usesImperialUnits) async { final changeIntakeAmount = await showDialog( - context: context, builder: (context) => EditDialog( - intakeEntity: intakeEntity) - ); - if(changeIntakeAmount != null) { - _homeBloc.updateIntakeItem(intakeEntity.id, { 'amount': changeIntakeAmount }); + context: context, + builder: (context) => EditDialog( + intakeEntity: intakeEntity, usesImperialUnits: usesImperialUnits)); + if (changeIntakeAmount != null) { + _homeBloc + .updateIntakeItem(intakeEntity.id, {'amount': changeIntakeAmount}); _homeBloc.add(const LoadItemsEvent()); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/features/home/presentation/bloc/home_bloc.dart b/lib/features/home/presentation/bloc/home_bloc.dart index c2220d66..f1109ce8 100644 --- a/lib/features/home/presentation/bloc/home_bloc.dart +++ b/lib/features/home/presentation/bloc/home_bloc.dart @@ -51,6 +51,7 @@ class HomeBloc extends Bloc { currentDay = DateTime.now(); final configData = await _getConfigUsecase.getConfig(); + final usesImperialUnits = configData.usesImperialUnits; final showDisclaimerDialog = !configData.hasAcceptedDisclaimer; final breakfastIntakeList = @@ -126,7 +127,8 @@ class HomeBloc extends Bloc { lunchIntakeList: lunchIntakeList, dinnerIntakeList: dinnerIntakeList, snackIntakeList: snackIntakeList, - userActivityList: userActivities)); + userActivityList: userActivities, + usesImperialUnits: usesImperialUnits)); }); } @@ -155,22 +157,28 @@ class HomeBloc extends Bloc { final newIntakeObject = await _updateIntakeUsecase.updateIntake(intakeId, fields); assert(newIntakeObject != null); - if(oldIntakeObject!.amount > newIntakeObject!.amount) { + if (oldIntakeObject!.amount > newIntakeObject!.amount) { // Amounts shrunk await _addTrackedDayUseCase.removeDayCaloriesTracked( dateTime, oldIntakeObject.totalKcal - newIntakeObject.totalKcal); await _addTrackedDayUseCase.removeDayMacrosTracked(dateTime, - carbsTracked: oldIntakeObject.totalCarbsGram - newIntakeObject.totalCarbsGram, - fatTracked: oldIntakeObject.totalFatsGram - newIntakeObject.totalFatsGram, - proteinTracked: oldIntakeObject.totalProteinsGram - newIntakeObject.totalProteinsGram); - } else if(newIntakeObject.amount > oldIntakeObject.amount) { + carbsTracked: + oldIntakeObject.totalCarbsGram - newIntakeObject.totalCarbsGram, + fatTracked: + oldIntakeObject.totalFatsGram - newIntakeObject.totalFatsGram, + proteinTracked: oldIntakeObject.totalProteinsGram - + newIntakeObject.totalProteinsGram); + } else if (newIntakeObject.amount > oldIntakeObject.amount) { // Amounts gained await _addTrackedDayUseCase.addDayCaloriesTracked( dateTime, newIntakeObject.totalKcal - oldIntakeObject.totalKcal); await _addTrackedDayUseCase.addDayMacrosTracked(dateTime, - carbsTracked: newIntakeObject.totalCarbsGram - oldIntakeObject.totalCarbsGram, - fatTracked: newIntakeObject.totalFatsGram - oldIntakeObject.totalFatsGram, - proteinTracked: newIntakeObject.totalProteinsGram - oldIntakeObject.totalProteinsGram); + carbsTracked: + newIntakeObject.totalCarbsGram - oldIntakeObject.totalCarbsGram, + fatTracked: + newIntakeObject.totalFatsGram - oldIntakeObject.totalFatsGram, + proteinTracked: newIntakeObject.totalProteinsGram - + oldIntakeObject.totalProteinsGram); } _updateDiaryPage(dateTime); } diff --git a/lib/features/home/presentation/bloc/home_state.dart b/lib/features/home/presentation/bloc/home_state.dart index 70172474..db0b1287 100644 --- a/lib/features/home/presentation/bloc/home_state.dart +++ b/lib/features/home/presentation/bloc/home_state.dart @@ -31,6 +31,7 @@ class HomeLoadedState extends HomeState { final List lunchIntakeList; final List dinnerIntakeList; final List snackIntakeList; + final bool usesImperialUnits; const HomeLoadedState({ required this.showDisclaimerDialog, @@ -49,9 +50,15 @@ class HomeLoadedState extends HomeState { required this.lunchIntakeList, required this.dinnerIntakeList, required this.snackIntakeList, + required this.usesImperialUnits, }); @override - List get props => - [breakfastIntakeList, lunchIntakeList, dinnerIntakeList, snackIntakeList]; + List get props => [ + breakfastIntakeList, + lunchIntakeList, + dinnerIntakeList, + snackIntakeList, + usesImperialUnits + ]; } diff --git a/lib/features/home/presentation/widgets/intake_vertical_list.dart b/lib/features/home/presentation/widgets/intake_vertical_list.dart index 6b379861..1b4464c9 100644 --- a/lib/features/home/presentation/widgets/intake_vertical_list.dart +++ b/lib/features/home/presentation/widgets/intake_vertical_list.dart @@ -18,21 +18,22 @@ class IntakeVerticalList extends StatefulWidget { final IconData listIcon; final AddMealType addMealType; final List intakeList; + final bool usesImperialUnits; final Function(BuildContext, IntakeEntity)? onItemLongPressedCallback; final Function(bool)? onItemDragCallback; - final Function(BuildContext, IntakeEntity)? onItemTappedCallback; + final Function(BuildContext, IntakeEntity, bool)? onItemTappedCallback; - const IntakeVerticalList({ - super.key, - required this.day, - required this.title, - required this.listIcon, - required this.addMealType, - required this.intakeList, - this.onItemLongPressedCallback, - this.onItemDragCallback, - this.onItemTappedCallback - }); + const IntakeVerticalList( + {super.key, + required this.day, + required this.title, + required this.listIcon, + required this.addMealType, + required this.intakeList, + required this.usesImperialUnits, + this.onItemLongPressedCallback, + this.onItemDragCallback, + this.onItemTappedCallback}); @override State createState() => _IntakeVerticalListState(); @@ -50,7 +51,8 @@ class _IntakeVerticalListState extends State { } double get totalKcal { - return widget.intakeList.fold(0, (previousValue, element) => previousValue + element.totalKcal); + return widget.intakeList + .fold(0, (previousValue, element) => previousValue + element.totalKcal); } @override @@ -63,23 +65,25 @@ class _IntakeVerticalListState extends State { child: Row( children: [ Icon(widget.listIcon, - size: 24, color: Theme.of(context).colorScheme.onBackground), + size: 24, color: Theme.of(context).colorScheme.onSurface), const SizedBox(width: 4.0), Text( widget.title, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: Theme.of(context).colorScheme.onSurface), ), const Spacer(), if (totalKcal > 0) - Text( - '${totalKcal.toInt()} ${S.of(context).kcalLabel}', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.7)), - ), + Text( + '${totalKcal.toInt()} ${S.of(context).kcalLabel}', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7)), + ), ], ), ), @@ -121,6 +125,7 @@ class _IntakeVerticalListState extends State { key: ValueKey(intakeEntity.meal.code), intake: intakeEntity, firstListElement: false, + usesImperialUnits: widget.usesImperialUnits, ), ), ), @@ -134,8 +139,7 @@ class _IntakeVerticalListState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), ), - color: - Theme.of(context).cardColor, + color: Theme.of(context).cardColor, ), ), ], @@ -146,6 +150,7 @@ class _IntakeVerticalListState extends State { onItemLongPressed: widget.onItemLongPressedCallback, onItemTapped: widget.onItemTappedCallback, firstListElement: firstListElement, + usesImperialUnits: widget.usesImperialUnits, ), ); } diff --git a/lib/features/meal_detail/meal_detail_screen.dart b/lib/features/meal_detail/meal_detail_screen.dart index f0c4d363..87075bed 100644 --- a/lib/features/meal_detail/meal_detail_screen.dart +++ b/lib/features/meal_detail/meal_detail_screen.dart @@ -1,8 +1,10 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:logging/logging.dart'; import 'package:opennutritracker/core/domain/entity/intake_type_entity.dart'; +import 'package:opennutritracker/core/presentation/widgets/meal_value_unit_text.dart'; import 'package:opennutritracker/core/presentation/widgets/image_full_screen.dart'; import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/core/utils/navigation_options.dart'; @@ -28,6 +30,9 @@ class MealDetailScreen extends StatefulWidget { class _MealDetailScreenState extends State { static const _containerSize = 350.0; + static const String _initialQuantityMetric = '100'; + static const String _initialQuantityImperial = '1'; + final log = Logger('ItemDetailScreen'); late MealDetailBloc _mealDetailBloc; @@ -36,20 +41,17 @@ class _MealDetailScreenState extends State { late MealEntity meal; late DateTime _day; late IntakeTypeEntity intakeTypeEntity; - late TextEditingController quantityTextController; - late double totalQuantity; - late double totalKcal; - late double totalCarbs; - late double totalFat; - late double totalProtein; + final quantityTextController = TextEditingController(); + late bool _usesImperialUnits; + + String _initialUnit = ""; + String _initialQuantity = ""; @override void initState() { _mealDetailBloc = locator(); - quantityTextController = TextEditingController(); - quantityTextController.text = '100'; - totalQuantity = 100; + super.initState(); } @@ -60,162 +62,227 @@ class _MealDetailScreenState extends State { meal = args.mealEntity; _day = args.day; intakeTypeEntity = args.intakeTypeEntity; - totalKcal = meal.nutriments.energyKcal100 ?? 0; - totalCarbs = meal.nutriments.carbohydrates100 ?? 0; - totalFat = meal.nutriments.fat100 ?? 0; - totalProtein = meal.nutriments.proteins100 ?? 0; - quantityTextController.addListener(() { - _onQuantityChanged(quantityTextController.text); - }); + _usesImperialUnits = args.usesImperialUnits; + + // Set initial unit + if (_initialUnit == "") { + if (meal.isLiquid) { + if (_usesImperialUnits) { + _initialUnit = UnitDropdownItem.flOz.toString(); + } else { + _initialUnit = UnitDropdownItem.ml.toString(); + } + } else if (meal.isSolid) { + if (_usesImperialUnits) { + _initialUnit = UnitDropdownItem.oz.toString(); + } else { + _initialUnit = UnitDropdownItem.g.toString(); + } + } else { + _initialUnit = UnitDropdownItem.gml.toString(); + } + _mealDetailBloc + .add(UpdateKcalEvent(meal: meal, selectedUnit: _initialUnit)); + } + + // Set initial quantity + if (_initialQuantity == "") { + if (_usesImperialUnits) { + _initialQuantity = _initialQuantityImperial; + quantityTextController.text = _initialQuantityImperial; + } else { + _initialQuantity = _initialQuantityMetric; + quantityTextController.text = _initialQuantityMetric; + } + _mealDetailBloc.add(UpdateKcalEvent( + meal: meal, totalQuantity: quantityTextController.text)); + } + super.didChangeDependencies(); } @override Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - controller: _scrollController, - slivers: [ - SliverAppBar( - pinned: true, - expandedHeight: 200, - flexibleSpace: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - final top = constraints.biggest.height; - final barsHeight = - MediaQuery.of(context).padding.top + kToolbarHeight; - const offset = 10; - return FlexibleSpaceBar( - expandedTitleScale: 1, // don't scale title - background: MealTitleExpanded(meal: meal), - title: AnimatedOpacity( - opacity: 1.0, - duration: const Duration(milliseconds: 300), - child: - top > barsHeight - offset && top < barsHeight + offset - ? Text(meal.name ?? '', - style: Theme.of(context).textTheme.titleLarge, - overflow: TextOverflow.ellipsis) - : const SizedBox())); - }), - actions: [ - IconButton( - onPressed: () { + return BlocBuilder( + bloc: _mealDetailBloc, + builder: (context, state) { + if (state is MealDetailInitial) { + return Scaffold( + body: _getLoadedContent( + context, + state.totalQuantityConverted, + state.totalKcal, + state.totalCarbs, + state.totalFat, + state.totalProtein, + state.selectedUnit, + ), + bottomSheet: MealDetailBottomSheet( + product: meal, + day: _day, + intakeTypeEntity: intakeTypeEntity, + selectedUnit: state.selectedUnit, + mealDetailBloc: _mealDetailBloc, + quantityTextController: quantityTextController, + onQuantityOrUnitChanged: onQuantityOrUnitChanged, + ), + ); + } else { + return Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + }, + ); + } + + Widget _getLoadedContent( + BuildContext context, + String totalQuantity, + double totalKcal, + double totalCarbs, + double totalFat, + double totalProtein, + String selectedUnit) { + return CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + pinned: true, + expandedHeight: 200, + flexibleSpace: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final top = constraints.biggest.height; + final barsHeight = + MediaQuery.of(context).padding.top + kToolbarHeight; + const offset = 10; + return FlexibleSpaceBar( + expandedTitleScale: 1, // don't scale title + background: MealTitleExpanded( + meal: meal, usesImperialUnits: _usesImperialUnits), + title: AnimatedOpacity( + opacity: 1.0, + duration: const Duration(milliseconds: 300), + child: + top > barsHeight - offset && top < barsHeight + offset + ? Text(meal.name ?? '', + style: Theme.of(context).textTheme.titleLarge, + overflow: TextOverflow.ellipsis) + : const SizedBox())); + }), + actions: [ + IconButton( + onPressed: () { + Navigator.of(context) + .pushNamed(NavigationOptions.editMealRoute, + arguments: EditMealScreenArguments( + _day, + meal, + intakeTypeEntity, + _usesImperialUnits, + )); + }, + icon: const Icon(Icons.edit_outlined)) + ], + ), + SliverList( + delegate: SliverChildListDelegate([ + const SizedBox(height: 16), + Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(80), + child: GestureDetector( + child: Hero( + tag: ImageFullScreen.fullScreenHeroTag, + child: CachedNetworkImage( + width: 250, + height: 250, + cacheManager: locator(), + imageUrl: meal.mainImageUrl ?? "", + fit: BoxFit.cover, + placeholder: (context, string) => const MealPlaceholder(), + errorWidget: (context, url, error) => + const MealPlaceholder(), + ), + ), + onTap: () { Navigator.of(context).pushNamed( - NavigationOptions.editMealRoute, - arguments: EditMealScreenArguments( - _day, meal, intakeTypeEntity)); - }, - icon: const Icon(Icons.edit_outlined)) - ], + NavigationOptions.imageFullScreenRoute, + arguments: + ImageFullScreenArguments(meal.mainImageUrl ?? "")); + }), + ), ), - SliverList( - delegate: SliverChildListDelegate([ - const SizedBox(height: 16), - Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(80), - child: GestureDetector( - child: Hero( - tag: ImageFullScreen.fullScreenHeroTag, - child: CachedNetworkImage( - width: 250, - height: 250, - cacheManager: locator(), - imageUrl: meal.mainImageUrl ?? "", - fit: BoxFit.cover, - placeholder: (context, string) => const MealPlaceholder(), - errorWidget: (context, url, error) => - const MealPlaceholder(), - ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + children: [ + Text('${totalKcal.toInt()} ${S.of(context).kcalLabel}', + style: Theme.of(context).textTheme.headlineSmall), + MealValueUnitText( + value: double.parse(totalQuantity), + meal: meal, + usesImperialUnits: _usesImperialUnits, + textStyle: Theme.of(context).textTheme.bodyMedium, + prefix: ' / ', ), - onTap: () { - Navigator.of(context).pushNamed( - NavigationOptions.imageFullScreenRoute, - arguments: ImageFullScreenArguments( - meal.mainImageUrl ?? "")); - }), - ), + ], + ), + const SizedBox(height: 8.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + MealDetailMacroNutrients( + typeString: S.of(context).carbsLabel, + value: totalCarbs), + MealDetailMacroNutrients( + typeString: S.of(context).fatLabel, value: totalFat), + MealDetailMacroNutrients( + typeString: S.of(context).proteinLabel, + value: totalProtein) + ], + ), + const Divider(), + const SizedBox(height: 16.0), + MealDetailNutrimentsTable(product: meal), + const SizedBox(height: 32.0), + MealInfoButton(url: meal.url, source: meal.source), + meal.source == MealSourceEntity.off + ? const Column( + children: [ + SizedBox(height: 32), + OffDisclaimer(), + ], + ) + : const SizedBox(), + const SizedBox(height: 200.0) // height added to scroll + ], ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Row( - children: [ - Text('${totalKcal.toInt()} ${S.of(context).kcalLabel}', - style: Theme.of(context).textTheme.headlineSmall), - Text( - ' / ${totalQuantity.toInt()} ${meal.mealUnit ?? S.of(context).gramMilliliterUnit}') - ], - ), - const SizedBox(height: 8.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - MealDetailMacroNutrients( - typeString: S.of(context).carbsLabel, - value: totalCarbs), - MealDetailMacroNutrients( - typeString: S.of(context).fatLabel, value: totalFat), - MealDetailMacroNutrients( - typeString: S.of(context).proteinLabel, - value: totalProtein) - ], - ), - const Divider(), - const SizedBox(height: 16.0), - MealDetailNutrimentsTable(product: meal), - const SizedBox(height: 32.0), - MealInfoButton(url: meal.url, source: meal.source), - meal.source == MealSourceEntity.off - ? const Column( - children: [ - SizedBox(height: 32), - OffDisclaimer(), - ], - ) - : const SizedBox(), - const SizedBox(height: 200.0) // height added to scroll - ], - ), - ) - ])) - ], - ), - bottomSheet: MealDetailBottomSheet( - product: meal, - day: _day, - intakeTypeEntity: intakeTypeEntity, - quantityTextController: quantityTextController, - mealDetailBloc: _mealDetailBloc), + ) + ])) + ], ); } - void scrollToCalorieText() { - _scrollController.animateTo(_containerSize - 50, - duration: const Duration(seconds: 1), curve: Curves.easeInOut); + void onQuantityOrUnitChanged(String? quantityString, String? unit) { + if (quantityString == null || unit == null) { + return; + } + _mealDetailBloc.add(UpdateKcalEvent( + meal: meal, totalQuantity: quantityString, selectedUnit: unit)); + _scrollToCalorieText(); } - void _onQuantityChanged(String quantityString) { - setState(() { - try { - final energyPerUnit = (meal.nutriments.energyPerUnit ?? 0); - final carbsPerUnit = (meal.nutriments.carbohydratesPerUnit ?? 0); - final fatPerUnit = (meal.nutriments.fatPerUnit ?? 0); - final proteinPerUnit = (meal.nutriments.proteinsPerUnit ?? 0); - - final quantity = double.parse(quantityString); - totalQuantity = quantity; - totalKcal = (quantity * energyPerUnit); - totalCarbs = (quantity * carbsPerUnit); - totalFat = (quantity * fatPerUnit); - totalProtein = (quantity * proteinPerUnit); - scrollToCalorieText(); - } on FormatException catch (_) { - log.warning("Error while parsing: \"$quantityString\""); - } - }); + void _scrollToCalorieText() { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _containerSize - 50, + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, + ); + } } } @@ -223,6 +290,8 @@ class MealDetailScreenArguments { final MealEntity mealEntity; final IntakeTypeEntity intakeTypeEntity; final DateTime day; + final bool usesImperialUnits; - MealDetailScreenArguments(this.mealEntity, this.intakeTypeEntity, this.day); + MealDetailScreenArguments( + this.mealEntity, this.intakeTypeEntity, this.day, this.usesImperialUnits); } diff --git a/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart b/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart index adb55aa7..88c0f11f 100644 --- a/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart +++ b/lib/features/meal_detail/presentation/bloc/meal_detail_bloc.dart @@ -1,4 +1,7 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:logging/logging.dart'; import 'package:opennutritracker/core/domain/entity/intake_entity.dart'; import 'package:opennutritracker/core/domain/entity/intake_type_entity.dart'; import 'package:opennutritracker/core/domain/usecase/add_intake_usecase.dart'; @@ -6,16 +9,64 @@ import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dar import 'package:opennutritracker/core/domain/usecase/get_user_usecase.dart'; import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; import 'package:opennutritracker/core/utils/calc/macro_calc.dart'; +import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/core/utils/id_generator.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; -class MealDetailBloc { +part 'meal_detail_event.dart'; + +part 'meal_detail_state.dart'; + +class MealDetailBloc extends Bloc { + final log = Logger('MealDetailBloc'); final AddIntakeUsecase _addIntakeUseCase; final AddTrackedDayUsecase _addTrackedDayUsecase; final GetUserUsecase _getUserUsecase; MealDetailBloc( - this._getUserUsecase, this._addTrackedDayUsecase, this._addIntakeUseCase); + this._addIntakeUseCase, this._addTrackedDayUsecase, this._getUserUsecase) + : super(MealDetailInitial( + totalQuantityConverted: '100', + selectedUnit: UnitDropdownItem.gml.toString())) { + on((event, emit) async { + try { + final selectedTotalQuantity = + event.totalQuantity ?? state.totalQuantityConverted; + final selectedUnit = event.selectedUnit ?? state.selectedUnit; + + if (selectedUnit.isEmpty || selectedTotalQuantity.isEmpty) { + return; + } + + final energyPerUnit = (event.meal.nutriments.energyPerUnit ?? 0); + final carbsPerUnit = (event.meal.nutriments.carbohydratesPerUnit ?? 0); + final fatPerUnit = (event.meal.nutriments.fatPerUnit ?? 0); + final proteinPerUnit = (event.meal.nutriments.proteinsPerUnit ?? 0); + + final quantity = double.parse(selectedTotalQuantity); + + // Convert imperial quantity to metric + double convertedQuantity = quantity; + if (selectedUnit == UnitDropdownItem.oz.toString()) { + convertedQuantity = UnitCalc.ozToG(quantity); + } else if (selectedUnit == UnitDropdownItem.flOz.toString()) { + convertedQuantity = UnitCalc.flOzToMl(quantity); + } + + emit(state.copyWith( + totalQuantityConverted: convertedQuantity.toString(), + totalKcal: convertedQuantity * energyPerUnit, + totalCarbs: convertedQuantity * carbsPerUnit, + totalFat: convertedQuantity * fatPerUnit, + totalProtein: convertedQuantity * proteinPerUnit, + selectedUnit: selectedUnit)); + } catch (e) { + log.severe('Error calculating kcal: $e'); + Sentry.captureException(e); + } + }); + } void addIntake(BuildContext context, String unit, String amountText, IntakeTypeEntity type, MealEntity meal, DateTime day) async { @@ -53,3 +104,44 @@ class MealDetailBloc { proteinTracked: intakeEntity.totalProteinsGram); } } + +enum UnitDropdownItem { + g, + ml, + gml, + oz, + flOz; + + UnitDropdownItem fromString(String value) { + switch (value) { + case 'g': + return UnitDropdownItem.g; + case 'ml': + return UnitDropdownItem.ml; + case 'g/ml': + return UnitDropdownItem.gml; + case 'oz': + return UnitDropdownItem.oz; + case 'fl oz' || 'fl.oz': + return UnitDropdownItem.flOz; + default: + return UnitDropdownItem.gml; + } + } + + @override + String toString() { + switch (this) { + case UnitDropdownItem.g: + return 'g'; + case UnitDropdownItem.ml: + return 'ml'; + case UnitDropdownItem.gml: + return 'g/ml'; + case UnitDropdownItem.oz: + return 'oz'; + case UnitDropdownItem.flOz: + return 'fl.oz'; + } + } +} diff --git a/lib/features/meal_detail/presentation/bloc/meal_detail_event.dart b/lib/features/meal_detail/presentation/bloc/meal_detail_event.dart new file mode 100644 index 00000000..89995604 --- /dev/null +++ b/lib/features/meal_detail/presentation/bloc/meal_detail_event.dart @@ -0,0 +1,26 @@ +part of 'meal_detail_bloc.dart'; + +abstract class MealDetailEvent extends Equatable { + const MealDetailEvent(); +} + +class UpdateKcalEvent extends MealDetailEvent { + final MealEntity meal; + final double? totalCarbs; + final double? totalFat; + final double? totalProtein; + final String? totalQuantity; + final String? selectedUnit; + + const UpdateKcalEvent( + {required this.meal, + this.totalCarbs, + this.totalFat, + this.totalProtein, + this.totalQuantity, + this.selectedUnit}); + + @override + List get props => + [meal, totalCarbs, totalFat, totalProtein, totalQuantity, selectedUnit]; +} diff --git a/lib/features/meal_detail/presentation/bloc/meal_detail_state.dart b/lib/features/meal_detail/presentation/bloc/meal_detail_state.dart new file mode 100644 index 00000000..9889ac34 --- /dev/null +++ b/lib/features/meal_detail/presentation/bloc/meal_detail_state.dart @@ -0,0 +1,58 @@ +part of 'meal_detail_bloc.dart'; + +abstract class MealDetailState extends Equatable { + final String totalQuantityConverted; + final double totalKcal; + final double totalCarbs; + final double totalFat; + final double totalProtein; + + final String selectedUnit; + + const MealDetailState( + {required this.totalQuantityConverted, + this.totalKcal = 0, + this.totalCarbs = 0, + this.totalFat = 0, + this.totalProtein = 0, + required this.selectedUnit}); + + @override + List get props => [ + totalQuantityConverted, + totalKcal, + totalCarbs, + totalFat, + totalProtein, + selectedUnit + ]; + + MealDetailInitial copyWith({ + String? totalQuantityConverted, + double? totalKcal, + double? totalCarbs, + double? totalFat, + double? totalProtein, + String? selectedUnit, + }) { + return MealDetailInitial( + totalQuantityConverted: + totalQuantityConverted ?? this.totalQuantityConverted, + totalKcal: totalKcal ?? this.totalKcal, + totalCarbs: totalCarbs ?? this.totalCarbs, + totalFat: totalFat ?? this.totalFat, + totalProtein: totalProtein ?? this.totalProtein, + selectedUnit: selectedUnit ?? this.selectedUnit, + ); + } +} + +class MealDetailInitial extends MealDetailState { + const MealDetailInitial( + {required super.totalQuantityConverted, + super.totalKcal, + super.totalCarbs, + super.totalFat, + super.totalProtein, + required super.selectedUnit}); +} diff --git a/lib/features/meal_detail/presentation/widgets/meal_detail_bottom_sheet.dart b/lib/features/meal_detail/presentation/widgets/meal_detail_bottom_sheet.dart index d441205a..97770da4 100644 --- a/lib/features/meal_detail/presentation/widgets/meal_detail_bottom_sheet.dart +++ b/lib/features/meal_detail/presentation/widgets/meal_detail_bottom_sheet.dart @@ -10,36 +10,30 @@ import 'package:opennutritracker/features/home/presentation/bloc/home_bloc.dart' import 'package:opennutritracker/features/meal_detail/presentation/bloc/meal_detail_bloc.dart'; import 'package:opennutritracker/generated/l10n.dart'; -class MealDetailBottomSheet extends StatefulWidget { +class MealDetailBottomSheet extends StatelessWidget { final MealEntity product; final DateTime day; final IntakeTypeEntity intakeTypeEntity; final TextEditingController quantityTextController; final MealDetailBloc mealDetailBloc; + final String selectedUnit; + + final Function(String?, String?) onQuantityOrUnitChanged; + const MealDetailBottomSheet( {super.key, required this.product, required this.day, required this.intakeTypeEntity, + required this.quantityTextController, + required this.onQuantityOrUnitChanged, required this.mealDetailBloc, - required this.quantityTextController}); - - @override - State createState() => _MealDetailBottomSheetState(); -} - -class _MealDetailBottomSheetState extends State { - late bool _productMissingRequiredInfo; - - @override - void initState() { - _productMissingRequiredInfo = _hasRequiredProductInfoMissing(); - super.initState(); - } + required this.selectedUnit}); @override Widget build(BuildContext context) { + final productMissingRequiredInfo = _hasRequiredProductInfoMissing(); return BottomSheet( elevation: 10, onClosing: () {}, @@ -67,8 +61,13 @@ class _MealDetailBottomSheetState extends State { children: [ Expanded( child: TextFormField( - enabled: !_productMissingRequiredInfo, - controller: widget.quantityTextController, + enabled: !productMissingRequiredInfo, + controller: quantityTextController + ..addListener(() { + onQuantityOrUnitChanged( + quantityTextController.text, + selectedUnit); + }), keyboardType: TextInputType.number, inputFormatters: CustomTextInputFormatter.doubleOnly(), @@ -81,22 +80,28 @@ class _MealDetailBottomSheetState extends State { const SizedBox(width: 16.0), Expanded( child: DropdownButtonFormField( + value: selectedUnit, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: S.of(context).unitLabel), items: >[ - DropdownMenuItem( - child: Text(widget.product.mealUnit ?? - S.of(context).gramMilliliterUnit)) + if (product.isSolid && !product.isLiquid) + ..._getSolidUnitDropdownItems(context) + else if (product.isLiquid && + !product.isSolid) + ..._getLiquidUnitDropdownItems(context), + ..._getOtherDropdownItems(context) ], - onChanged: null // deactivate item, - )) + onChanged: (value) { + onQuantityOrUnitChanged( + quantityTextController.text, value); + })) ], ), SizedBox( width: double.infinity, // Make button full width child: ElevatedButton.icon( - onPressed: !_productMissingRequiredInfo + onPressed: !productMissingRequiredInfo ? () { onAddButtonPressed(context); } @@ -113,7 +118,7 @@ class _MealDetailBottomSheetState extends State { icon: const Icon(Icons.add_outlined), label: Text(S.of(context).addLabel)), ), - _productMissingRequiredInfo + productMissingRequiredInfo ? Text(S.of(context).missingProductInfo, style: Theme.of(context) .textTheme @@ -132,7 +137,7 @@ class _MealDetailBottomSheetState extends State { } bool _hasRequiredProductInfoMissing() { - final productNutriments = widget.product.nutriments; + final productNutriments = product.nutriments; if (productNutriments.energyKcal100 == null || productNutriments.carbohydrates100 == null || productNutriments.fat100 == null || @@ -144,13 +149,13 @@ class _MealDetailBottomSheetState extends State { } void onAddButtonPressed(BuildContext context) { - widget.mealDetailBloc.addIntake( + mealDetailBloc.addIntake( context, - widget.product.mealUnit ?? S.of(context).gramMilliliterUnit, - widget.quantityTextController.text, - widget.intakeTypeEntity, - widget.product, - widget.day); + mealDetailBloc.state.selectedUnit, + mealDetailBloc.state.totalQuantityConverted, + intakeTypeEntity, + product, + day); // Refresh Home Page locator().add(const LoadItemsEvent()); @@ -165,4 +170,36 @@ class _MealDetailBottomSheetState extends State { Navigator.of(context) .popUntil(ModalRoute.withName(NavigationOptions.mainRoute)); } + + List> _getSolidUnitDropdownItems( + BuildContext context) { + return [ + DropdownMenuItem( + value: UnitDropdownItem.g.toString(), + child: Text(S.of(context).gramUnit)), + DropdownMenuItem( + value: UnitDropdownItem.oz.toString(), + child: Text(S.of(context).ozUnit)), + ]; + } + + List> _getLiquidUnitDropdownItems( + BuildContext context) { + return [ + DropdownMenuItem( + value: UnitDropdownItem.ml.toString(), + child: Text(S.of(context).milliliterUnit)), + DropdownMenuItem( + value: UnitDropdownItem.flOz.toString(), + child: Text(S.of(context).flOzUnit)), + ]; + } + + List> _getOtherDropdownItems(BuildContext context) { + return [ + DropdownMenuItem( + value: UnitDropdownItem.gml.toString(), + child: Text(S.of(context).gramMilliliterUnit)), + ]; + } } diff --git a/lib/features/meal_detail/presentation/widgets/meal_detail_nutriments_table.dart b/lib/features/meal_detail/presentation/widgets/meal_detail_nutriments_table.dart index 23f2c05b..dfe578c4 100644 --- a/lib/features/meal_detail/presentation/widgets/meal_detail_nutriments_table.dart +++ b/lib/features/meal_detail/presentation/widgets/meal_detail_nutriments_table.dart @@ -28,10 +28,10 @@ class MealDetailNutrimentsTable extends StatelessWidget { defaultVerticalAlignment: TableCellVerticalAlignment.middle, border: TableBorder.all( color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.5)), + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5)), children: [ _getNutrimentsTableRow( - "", S.of(context).per100gLabel, textStyleBold), + "", S.of(context).per100gmlLabel, textStyleBold), _getNutrimentsTableRow( S.of(context).energyLabel, "${product.nutriments.energyKcal100?.toInt() ?? "?"} ${S.of(context).kcalLabel}", diff --git a/lib/features/meal_detail/presentation/widgets/meal_title_expanded.dart b/lib/features/meal_detail/presentation/widgets/meal_title_expanded.dart index a4fd6ff3..e8ad80c2 100644 --- a/lib/features/meal_detail/presentation/widgets/meal_title_expanded.dart +++ b/lib/features/meal_detail/presentation/widgets/meal_title_expanded.dart @@ -1,11 +1,14 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:opennutritracker/core/presentation/widgets/meal_value_unit_text.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; class MealTitleExpanded extends StatelessWidget { final MealEntity meal; + final bool usesImperialUnits; - const MealTitleExpanded({super.key, required this.meal}); + const MealTitleExpanded( + {super.key, required this.meal, required this.usesImperialUnits}); @override Widget build(BuildContext context) { @@ -21,28 +24,37 @@ class MealTitleExpanded extends StatelessWidget { TextSpan( text: meal.name ?? '', style: Theme.of(context).textTheme.displaySmall?.copyWith( - color: Theme.of(context).colorScheme.onBackground), + color: Theme.of(context).colorScheme.onSurface), children: [ TextSpan( text: ' ${meal.brands ?? ''}', - style: Theme.of(context).textTheme.displaySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.7))) + style: Theme.of(context) + .textTheme + .displaySmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7))), ]), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis), meal.mealQuantity != null ? Center( - child: Text( - '${meal.mealQuantity ?? ""} ${meal.mealUnit ?? ""} ', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground - .withOpacity(0.8))), + child: MealValueUnitText( + value: double.tryParse(meal.mealQuantity ?? '') ?? 0, + meal: meal, + usesImperialUnits: usesImperialUnits, + textStyle: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.8)), + prefix: ''), ) : const SizedBox(), ], diff --git a/lib/features/meal_detail/presentation/widgets/off_disclaimer.dart b/lib/features/meal_detail/presentation/widgets/off_disclaimer.dart index 171ed2a9..164af7f3 100644 --- a/lib/features/meal_detail/presentation/widgets/off_disclaimer.dart +++ b/lib/features/meal_detail/presentation/widgets/off_disclaimer.dart @@ -8,7 +8,8 @@ class OffDisclaimer extends StatelessWidget { Widget build(BuildContext context) { return Text(S.of(context).offDisclaimer, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), fontStyle: FontStyle.italic) - ); + color: + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), + fontStyle: FontStyle.italic)); } } diff --git a/lib/features/onboarding/domain/entity/user_data_mask_entity.dart b/lib/features/onboarding/domain/entity/user_data_mask_entity.dart index 8cf56dbd..607f5a21 100644 --- a/lib/features/onboarding/domain/entity/user_data_mask_entity.dart +++ b/lib/features/onboarding/domain/entity/user_data_mask_entity.dart @@ -16,13 +16,17 @@ class UserDataMaskEntity { bool acceptDataCollection = false; + bool usesImperialUnits = false; + UserDataMaskEntity( {this.gender, this.birthday, this.height, this.weight, this.activity, - this.goal}); + this.goal, + this.acceptDataCollection = false, + this.usesImperialUnits = false}); bool checkDataProvided() { if (gender != null && diff --git a/lib/features/onboarding/onboarding_screen.dart b/lib/features/onboarding/onboarding_screen.dart index 2e2ba346..4092d2cd 100644 --- a/lib/features/onboarding/onboarding_screen.dart +++ b/lib/features/onboarding/onboarding_screen.dart @@ -215,11 +215,12 @@ class _OnboardingScreenState extends State { }); } - void _setSecondPageData( - bool active, double? selectedHeight, double? selectedWeight) { + void _setSecondPageData(bool active, double? selectedHeight, + double? selectedWeight, bool usesImperial) { setState(() { _onboardingBloc.userSelection.height = selectedHeight; _onboardingBloc.userSelection.weight = selectedWeight; + _onboardingBloc.userSelection.usesImperialUnits = usesImperial; _secondPageButtonActive = active; }); @@ -263,9 +264,10 @@ class _OnboardingScreenState extends State { final userEntity = _onboardingBloc.userSelection.toUserEntity(); final hasAcceptedDataCollection = _onboardingBloc.userSelection.acceptDataCollection; + final usesImperialUnits = _onboardingBloc.userSelection.usesImperialUnits; if (userEntity != null) { _onboardingBloc.saveOnboardingData( - context, userEntity, hasAcceptedDataCollection); + context, userEntity, hasAcceptedDataCollection, usesImperialUnits); Navigator.pushReplacementNamed(context, NavigationOptions.mainRoute); } else { // Error with user input diff --git a/lib/features/onboarding/presentation/bloc/onboarding_bloc.dart b/lib/features/onboarding/presentation/bloc/onboarding_bloc.dart index 83039887..2d7e1d16 100644 --- a/lib/features/onboarding/presentation/bloc/onboarding_bloc.dart +++ b/lib/features/onboarding/presentation/bloc/onboarding_bloc.dart @@ -27,10 +27,11 @@ class OnboardingBloc extends Bloc { } void saveOnboardingData(BuildContext context, UserEntity userEntity, - bool hasAcceptedDataCollection) async { + bool hasAcceptedDataCollection, bool usesImperialUnits) async { _addUserUsecase.addUser(userEntity); _addConfigUsecase .setConfigHasAcceptedAnonymousData(hasAcceptedDataCollection); + _addConfigUsecase.setConfigUsesImperialUnits(usesImperialUnits); } double? getOverviewCalorieGoal() { diff --git a/lib/features/onboarding/presentation/widgets/onboarding_overview_page_body.dart b/lib/features/onboarding/presentation/widgets/onboarding_overview_page_body.dart index 1961d98e..5a185e06 100644 --- a/lib/features/onboarding/presentation/widgets/onboarding_overview_page_body.dart +++ b/lib/features/onboarding/presentation/widgets/onboarding_overview_page_body.dart @@ -44,8 +44,7 @@ class OnboardingOverviewPageBody extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.6))) + .onSurface.withValues(alpha: 0.6))) ], ), ), @@ -66,8 +65,7 @@ class OnboardingOverviewPageBody extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.6))), + .onSurface.withValues(alpha: 0.6))), const SizedBox(height: 8.0), Text('$fatGoalString g', style: Theme.of(context).textTheme.headlineSmall?.copyWith( @@ -76,8 +74,7 @@ class OnboardingOverviewPageBody extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.6))), + .onSurface.withValues(alpha: 0.6))), const SizedBox(height: 8.0), Text('$proteinGoalString g', style: Theme.of(context).textTheme.headlineSmall?.copyWith( @@ -86,8 +83,7 @@ class OnboardingOverviewPageBody extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context) .colorScheme - .onBackground - .withOpacity(0.6))), + .onSurface.withValues(alpha: 0.6))), ], ), ), diff --git a/lib/features/onboarding/presentation/widgets/onboarding_second_page_body.dart b/lib/features/onboarding/presentation/widgets/onboarding_second_page_body.dart index 90ac426e..1936af72 100644 --- a/lib/features/onboarding/presentation/widgets/onboarding_second_page_body.dart +++ b/lib/features/onboarding/presentation/widgets/onboarding_second_page_body.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/generated/l10n.dart'; class OnboardingSecondPageBody extends StatefulWidget { - final Function(bool active, double? selectedHeight, double? selectedWeight) - setButtonContent; + final Function(bool active, double? selectedHeight, double? selectedWeight, + bool usesImperialUnits) setButtonContent; const OnboardingSecondPageBody({super.key, required this.setButtonContent}); @@ -16,9 +17,12 @@ class OnboardingSecondPageBody extends StatefulWidget { class _OnboardingSecondPageBodyState extends State { final _heightFormKey = GlobalKey(); final _weightFormKey = GlobalKey(); + final _isUnitSelected = [true, false]; double? _parsedHeight; double? _parsedWeight; + bool get _isImperialSelected => _isUnitSelected[1]; + @override Widget build(BuildContext context) { return SizedBox( @@ -46,15 +50,48 @@ class _OnboardingSecondPageBodyState extends State { }, validator: validateHeight, decoration: InputDecoration( - labelText: 'cm', - hintText: S.of(context).onboardingHeightExampleHint, + labelText: _isImperialSelected ? 'ft' : 'cm', + hintText: _isImperialSelected + ? S.of(context).onboardingHeightExampleHintFt + : S.of(context).onboardingHeightExampleHintCm, filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), ), ), keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly]), + inputFormatters: [ + !_isImperialSelected + ? FilteringTextInputFormatter.digitsOnly + : FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')) + ]), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ToggleButtons( + borderRadius: const BorderRadius.all(Radius.circular(8)), + isSelected: _isUnitSelected, + onPressed: (int index) { + setState(() { + // Toggle height unit + for (int i = 0; i < _isUnitSelected.length; i++) { + _isUnitSelected[i] = i == index; + } + _heightFormKey.currentState!.validate(); + checkCorrectInput(); + }); + }, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text(S.of(context).cmLabel), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text(S.of(context).ftLabel), + ), + ], + ), ), const SizedBox(height: 32.0), Text(S.of(context).weightLabel, @@ -70,14 +107,17 @@ class _OnboardingSecondPageBodyState extends State { _parsedWeight = double.tryParse(text); checkCorrectInput(); } else { - _parsedWeight = null; checkCorrectInput(); } }, validator: validateWeight, decoration: InputDecoration( - labelText: 'kg', - hintText: S.of(context).onboardingWeightExampleHint, + labelText: _isImperialSelected + ? S.of(context).lbsLabel + : S.of(context).kgLabel, + hintText: _isImperialSelected + ? S.of(context).onboardingWeightExampleHintLbs + : S.of(context).onboardingWeightExampleHintKg, filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), @@ -86,6 +126,33 @@ class _OnboardingSecondPageBodyState extends State { keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly]), ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ToggleButtons( + borderRadius: const BorderRadius.all(Radius.circular(8)), + isSelected: _isUnitSelected, + onPressed: (int index) { + setState(() { + // Toggle height unit + for (int i = 0; i < _isUnitSelected.length; i++) { + _isUnitSelected[i] = i == index; + } + _weightFormKey.currentState!.validate(); + checkCorrectInput(); + }); + }, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text(S.of(context).kgLabel), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text(S.of(context).lbsLabel), + ), + ], + ), + ), ], ), ); @@ -93,10 +160,21 @@ class _OnboardingSecondPageBodyState extends State { String? validateHeight(String? value) { if (value == null) return S.of(context).onboardingWrongHeightLabel; - if (value.isEmpty || !RegExp(r'^[0-9]').hasMatch(value)) { - return S.of(context).onboardingWrongHeightLabel; + + if (_isImperialSelected) { + // Regex for feet and inches + if (value.isEmpty || !RegExp(r'^[0-9]+(\.[0-9]+)?$').hasMatch(value)) { + return S.of(context).onboardingWrongHeightLabel; + } else { + return null; + } } else { - return null; + // Regex for cm + if (value.isEmpty || !RegExp(r'^[0-9]+$').hasMatch(value)) { + return S.of(context).onboardingWrongHeightLabel; + } else { + return null; + } } } @@ -109,11 +187,26 @@ class _OnboardingSecondPageBodyState extends State { } } + /// Check if the input is correct and update the button content void checkCorrectInput() { - if (_parsedHeight != null && _parsedWeight != null) { - widget.setButtonContent(true, _parsedHeight, _parsedWeight); + final isHeightValid = _heightFormKey.currentState?.validate() ?? false; + final isWeightValid = _weightFormKey.currentState?.validate() ?? false; + + if (isHeightValid && isWeightValid) { + if (_parsedHeight != null && _parsedWeight != null) { + final heightCm = _isImperialSelected + ? UnitCalc.feetToCm(_parsedHeight!) + : _parsedHeight!; + final weightKg = _isImperialSelected + ? UnitCalc.lbsToKg(_parsedWeight!) + : _parsedWeight!; + + widget.setButtonContent(true, heightCm, weightKg, _isImperialSelected); + } else { + widget.setButtonContent(false, null, null, _isImperialSelected); + } } else { - widget.setButtonContent(false, null, null); + widget.setButtonContent(false, null, null, _isImperialSelected); } } } diff --git a/lib/features/profile/presentation/bloc/profile_bloc.dart b/lib/features/profile/presentation/bloc/profile_bloc.dart index a606bfaf..512caf75 100644 --- a/lib/features/profile/presentation/bloc/profile_bloc.dart +++ b/lib/features/profile/presentation/bloc/profile_bloc.dart @@ -5,10 +5,12 @@ import 'package:opennutritracker/core/domain/entity/user_bmi_entity.dart'; import 'package:opennutritracker/core/domain/entity/user_entity.dart'; import 'package:opennutritracker/core/domain/usecase/add_tracked_day_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/add_user_usecase.dart'; +import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_activity_usecase.dart'; import 'package:opennutritracker/core/domain/usecase/get_user_usecase.dart'; import 'package:opennutritracker/core/utils/calc/bmi_calc.dart'; import 'package:opennutritracker/core/utils/calc/calorie_goal_calc.dart'; +import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/features/diary/presentation/bloc/calendar_day_bloc.dart'; import 'package:opennutritracker/features/diary/presentation/bloc/diary_bloc.dart'; @@ -24,8 +26,14 @@ class ProfileBloc extends Bloc { final AddTrackedDayUsecase _addTrackedDayUsecase; final GetUserActivityUsecase _getUserActivityUsecase; - ProfileBloc(this._getUserUsecase, this._addUserUsecase, - this._addTrackedDayUsecase, this._getUserActivityUsecase) + final GetConfigUsecase _getConfigUsecase; + + ProfileBloc( + this._getUserUsecase, + this._addUserUsecase, + this._addTrackedDayUsecase, + this._getUserActivityUsecase, + this._getConfigUsecase) : super(ProfileInitial()) { on((event, emit) async { emit(ProfileLoadingState()); @@ -35,8 +43,12 @@ class ProfileBloc extends Bloc { final userBMIEntity = UserBMIEntity( bmiValue: userBMIValue, nutritionalStatus: BMICalc.getNutritionalStatus(userBMIValue)); + final userConfig = await _getConfigUsecase.getConfig(); - emit(ProfileLoadedState(userBMI: userBMIEntity, userEntity: user)); + emit(ProfileLoadedState( + userBMI: userBMIEntity, + userEntity: user, + usesImperialUnits: userConfig.usesImperialUnits)); }); } @@ -64,9 +76,29 @@ class ProfileBloc extends Bloc { await _getUserActivityUsecase.getTodayUserActivity(); final totalActivityKcal = activityDayList.map((activity) => activity.burnedKcal).sum; - final totalKcalGoal = CalorieGoalCalc.getTotalKcalGoal(user, totalActivityKcal); + final totalKcalGoal = + CalorieGoalCalc.getTotalKcalGoal(user, totalActivityKcal); await _addTrackedDayUsecase.updateDayCalorieGoal(day, totalKcalGoal); } } + + /// Returns the user's height in cm or ft/in based on the user's config + String getDisplayHeight(UserEntity user, bool usesImperialUnits) { + if (usesImperialUnits) { + // Convert cm to feet and inches + return UnitCalc.cmToFeet(user.heightCM).toStringAsFixed(1); + } else { + return user.heightCM.roundToDouble().toStringAsFixed(0); + } + } + + /// Returns the user's weight in kg or lbs based on the user's config + String getDisplayWeight(UserEntity user, bool usesImperialUnits) { + if (usesImperialUnits) { + return UnitCalc.kgToLbs(user.weightKG).toStringAsFixed(0); + } else { + return user.weightKG.roundToDouble().toStringAsFixed(0); + } + } } diff --git a/lib/features/profile/presentation/bloc/profile_state.dart b/lib/features/profile/presentation/bloc/profile_state.dart index 819f47be..5e1ca40c 100644 --- a/lib/features/profile/presentation/bloc/profile_state.dart +++ b/lib/features/profile/presentation/bloc/profile_state.dart @@ -18,8 +18,13 @@ class ProfileLoadedState extends ProfileState { final UserBMIEntity userBMI; final UserEntity userEntity; - const ProfileLoadedState({required this.userBMI, required this.userEntity}); + final bool usesImperialUnits; + + const ProfileLoadedState( + {required this.userBMI, + required this.userEntity, + required this.usesImperialUnits}); @override - List get props => []; + List get props => [userBMI, userEntity, usesImperialUnits]; } diff --git a/lib/features/profile/presentation/widgets/bmi_overview.dart b/lib/features/profile/presentation/widgets/bmi_overview.dart index 065a2f58..480181fb 100644 --- a/lib/features/profile/presentation/widgets/bmi_overview.dart +++ b/lib/features/profile/presentation/widgets/bmi_overview.dart @@ -59,8 +59,10 @@ class BMIOverview extends StatelessWidget { S.of(context).nutritionalStatusRiskLabel( nutritionalStatus.getRiskStatus(context)), style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.7)), + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7)), ) ], ); @@ -70,19 +72,26 @@ class BMIOverview extends StatelessWidget { Color theme; switch (nutritionalStatus) { case UserNutritionalStatus.underWeight: - theme = Theme.of(context).colorScheme.errorContainer.withOpacity(0.1); + theme = Theme.of(context).colorScheme.errorContainer + ..withValues(alpha: 0.1); break; case UserNutritionalStatus.normalWeight: - theme = Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6); + theme = Theme.of(context) + .colorScheme + .primaryContainer + .withValues(alpha: 0.6); break; case UserNutritionalStatus.preObesity: - theme = Theme.of(context).colorScheme.errorContainer.withOpacity(0.2); + theme = + Theme.of(context).colorScheme.errorContainer.withValues(alpha: 0.2); break; case UserNutritionalStatus.obesityClassI: - theme = Theme.of(context).colorScheme.errorContainer.withOpacity(0.4); + theme = + Theme.of(context).colorScheme.errorContainer.withValues(alpha: 0.4); break; case UserNutritionalStatus.obesityClassII: - theme = Theme.of(context).colorScheme.errorContainer.withOpacity(0.7); + theme = + Theme.of(context).colorScheme.errorContainer.withValues(alpha: 0.7); break; case UserNutritionalStatus.obesityClassIII: theme = Theme.of(context).colorScheme.errorContainer; diff --git a/lib/features/profile/presentation/widgets/set_height_dialog.dart b/lib/features/profile/presentation/widgets/set_height_dialog.dart index 14ac892d..6cf65431 100644 --- a/lib/features/profile/presentation/widgets/set_height_dialog.dart +++ b/lib/features/profile/presentation/widgets/set_height_dialog.dart @@ -4,14 +4,17 @@ import 'package:opennutritracker/generated/l10n.dart'; class SetHeightDialog extends StatelessWidget { static const _heightRangeCM = 100.0; + static const _heightRangeFt = 10; - final double userHeightCM; + final double userHeight; + final bool usesImperialUnits; - const SetHeightDialog({super.key, required this.userHeightCM}); + const SetHeightDialog( + {super.key, required this.userHeight, required this.usesImperialUnits}); @override Widget build(BuildContext context) { - double selectedHeightCM = userHeightCM; + double selectedHeight = userHeight; return AlertDialog( title: Text(S.of(context).selectHeightDialogLabel), content: Wrap( @@ -21,12 +24,18 @@ class SetHeightDialog extends StatelessWidget { HorizontalPicker( height: 100, backgroundColor: Colors.transparent, - minValue: selectedHeightCM - _heightRangeCM, - maxValue: selectedHeightCM + _heightRangeCM, + minValue: usesImperialUnits + ? selectedHeight - _heightRangeFt + : selectedHeight - _heightRangeCM, + maxValue: usesImperialUnits + ? selectedHeight + _heightRangeFt + : selectedHeight + _heightRangeCM, divisions: 400, - suffix: ' ${S.of(context).cmLabel}', + suffix: usesImperialUnits + ? S.of(context).ftLabel + : S.of(context).cmLabel, onChanged: (value) { - selectedHeightCM = value; + selectedHeight = value; }) ], ) @@ -41,7 +50,7 @@ class SetHeightDialog extends StatelessWidget { TextButton( onPressed: () { // TODO validate selected height - Navigator.pop(context, selectedHeightCM); + Navigator.pop(context, selectedHeight); }, child: Text(S.of(context).dialogOKLabel)) ], diff --git a/lib/features/profile/presentation/widgets/set_weight_dialog.dart b/lib/features/profile/presentation/widgets/set_weight_dialog.dart index 156409cf..6c2471f4 100644 --- a/lib/features/profile/presentation/widgets/set_weight_dialog.dart +++ b/lib/features/profile/presentation/widgets/set_weight_dialog.dart @@ -3,11 +3,14 @@ import 'package:horizontal_picker/horizontal_picker.dart'; import 'package:opennutritracker/generated/l10n.dart'; class SetWeightDialog extends StatelessWidget { - static const weightRange = 50.0; + static const weightRangeKg = 50.0; + static const weightRangeLbs = 100.0; final double userWeight; + final bool usesImperialUnits; - const SetWeightDialog({super.key, required this.userWeight}); + const SetWeightDialog( + {super.key, required this.userWeight, required this.usesImperialUnits}); @override Widget build(BuildContext context) { @@ -20,11 +23,17 @@ class SetWeightDialog extends StatelessWidget { HorizontalPicker( height: 100, backgroundColor: Colors.transparent, - minValue: userWeight - weightRange, - maxValue: userWeight + weightRange, + minValue: usesImperialUnits + ? userWeight - weightRangeLbs + : userWeight - weightRangeKg, + maxValue: usesImperialUnits + ? userWeight + weightRangeLbs + : userWeight + weightRangeKg, initialPosition: InitialPosition.center, divisions: 1000, - suffix: S.of(context).kgLabel, + suffix: usesImperialUnits + ? S.of(context).lbsLabel + : S.of(context).kgLabel, onChanged: (value) { selectedWeight = value; }) diff --git a/lib/features/profile/profile_page.dart b/lib/features/profile/profile_page.dart index e4217b9d..5ec422d4 100644 --- a/lib/features/profile/profile_page.dart +++ b/lib/features/profile/profile_page.dart @@ -5,6 +5,7 @@ import 'package:opennutritracker/core/domain/entity/user_entity.dart'; import 'package:opennutritracker/core/domain/entity/user_gender_entity.dart'; import 'package:opennutritracker/core/domain/entity/user_pal_entity.dart'; import 'package:opennutritracker/core/domain/entity/user_weight_goal_entity.dart'; +import 'package:opennutritracker/core/utils/calc/unit_calc.dart'; import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/features/profile/presentation/bloc/profile_bloc.dart'; import 'package:opennutritracker/features/profile/presentation/widgets/bmi_overview.dart'; @@ -42,7 +43,8 @@ class _ProfilePageState extends State { } else if (state is ProfileLoadingState) { return _getLoadingContent(); } else if (state is ProfileLoadedState) { - return _getLoadedContent(context, state.userBMI, state.userEntity); + return _getLoadedContent(context, state.userBMI, state.userEntity, + state.usesImperialUnits); } else { return _getLoadingContent(); } @@ -56,8 +58,8 @@ class _ProfilePageState extends State { ); } - Widget _getLoadedContent( - BuildContext context, UserBMIEntity userBMIEntity, UserEntity user) { + Widget _getLoadedContent(BuildContext context, UserBMIEntity userBMIEntity, + UserEntity user, bool usesImperialUnits) { return ListView( children: [ const SizedBox(height: 32.0), @@ -102,7 +104,7 @@ class _ProfilePageState extends State { style: Theme.of(context).textTheme.titleLarge, ), subtitle: Text( - '${user.weightKG} kg', + '${_profileBloc.getDisplayWeight(user, usesImperialUnits)} ${usesImperialUnits ? S.of(context).lbsLabel : S.of(context).kgLabel}', style: Theme.of(context).textTheme.titleMedium, ), leading: const SizedBox( @@ -110,7 +112,7 @@ class _ProfilePageState extends State { child: Icon(Icons.monitor_weight_outlined), ), onTap: () { - _showSetWeightDialog(context, user); + _showSetWeightDialog(context, user, usesImperialUnits); }, ), ListTile( @@ -119,7 +121,7 @@ class _ProfilePageState extends State { style: Theme.of(context).textTheme.titleLarge, ), subtitle: Text( - '${user.heightCM.toInt()} cm', + '${_profileBloc.getDisplayHeight(user, usesImperialUnits)} ${usesImperialUnits ? S.of(context).ftLabel : S.of(context).cmLabel}', style: Theme.of(context).textTheme.titleMedium, ), leading: const SizedBox( @@ -127,7 +129,7 @@ class _ProfilePageState extends State { child: Icon(Icons.height_outlined), ), onTap: () { - _showSetHeightDialog(context, user); + _showSetHeightDialog(context, user, usesImperialUnits); }, ), ListTile( @@ -190,27 +192,43 @@ class _ProfilePageState extends State { } } - Future _showSetHeightDialog( - BuildContext context, UserEntity userEntity) async { + Future _showSetHeightDialog(BuildContext context, UserEntity userEntity, + bool usesImperialUnits) async { final selectedHeight = await showDialog( context: context, builder: (context) => SetHeightDialog( - userHeightCM: userEntity.heightCM, + userHeight: usesImperialUnits + ? UnitCalc.cmToFeet(userEntity.heightCM) + : userEntity.heightCM, + usesImperialUnits: usesImperialUnits, )); if (selectedHeight != null) { - userEntity.heightCM = selectedHeight; + if (usesImperialUnits) { + userEntity.heightCM = UnitCalc.feetToCm(selectedHeight); + } else { + userEntity.heightCM = selectedHeight; + } _profileBloc.updateUser(userEntity); } } - Future _showSetWeightDialog( - BuildContext context, UserEntity userEntity) async { + Future _showSetWeightDialog(BuildContext context, UserEntity userEntity, + bool usesImperialSystem) async { final selectedWeight = await showDialog( context: context, - builder: (context) => SetWeightDialog(userWeight: userEntity.weightKG)); + builder: (context) => SetWeightDialog( + userWeight: usesImperialSystem + ? UnitCalc.kgToLbs(userEntity.weightKG) + : userEntity.weightKG, + usesImperialUnits: usesImperialSystem, + )); if (selectedWeight != null) { - userEntity.weightKG = selectedWeight; + if (usesImperialSystem) { + userEntity.weightKG = UnitCalc.lbsToKg(selectedWeight); + } else { + userEntity.weightKG = selectedWeight; + } _profileBloc.updateUser(userEntity); } } diff --git a/lib/features/scanner/presentation/scanner_bloc.dart b/lib/features/scanner/presentation/scanner_bloc.dart index 90498823..7e4d1b12 100644 --- a/lib/features/scanner/presentation/scanner_bloc.dart +++ b/lib/features/scanner/presentation/scanner_bloc.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:opennutritracker/core/domain/usecase/get_config_usecase.dart'; import 'package:opennutritracker/features/add_meal/domain/entity/meal_entity.dart'; import 'package:opennutritracker/features/scanner/data/product_not_found_exception.dart'; import 'package:opennutritracker/features/scanner/domain/usecase/search_product_by_barcode_usecase.dart'; @@ -11,15 +12,19 @@ part 'scanner_state.dart'; class ScannerBloc extends Bloc { final SearchProductByBarcodeUseCase _searchProductUseCase; + final GetConfigUsecase _getConfigUsecase; - ScannerBloc(this._searchProductUseCase) : super(ScannerInitial()) { + ScannerBloc(this._searchProductUseCase, this._getConfigUsecase) + : super(ScannerInitial()) { on((event, emit) async { emit(ScannerLoadingState()); try { final result = await _searchProductUseCase.searchProductByBarcode(event.barcode); - emit(ScannerLoadedState(product: result)); + final config = await _getConfigUsecase.getConfig(); + emit(ScannerLoadedState( + product: result, usesImperialUnits: config.usesImperialUnits)); } catch (exception) { if (exception == ProductNotFoundException) { emit( diff --git a/lib/features/scanner/presentation/scanner_state.dart b/lib/features/scanner/presentation/scanner_state.dart index 86a78133..fcd47a34 100644 --- a/lib/features/scanner/presentation/scanner_state.dart +++ b/lib/features/scanner/presentation/scanner_state.dart @@ -17,8 +17,10 @@ class ScannerLoadingState extends ScannerState { class ScannerLoadedState extends ScannerState { final MealEntity product; + final bool usesImperialUnits; - const ScannerLoadedState({required this.product}); + const ScannerLoadedState( + {required this.product, this.usesImperialUnits = false}); @override List get props => [product]; diff --git a/lib/features/scanner/scanner_screen.dart b/lib/features/scanner/scanner_screen.dart index 655837c1..56760387 100644 --- a/lib/features/scanner/scanner_screen.dart +++ b/lib/features/scanner/scanner_screen.dart @@ -54,10 +54,14 @@ class _ScannerScreenState extends State { body: const Center(child: CircularProgressIndicator())); } else if (state is ScannerLoadedState) { // Push new route after build - Future.microtask(() => Navigator.of(context).pushReplacementNamed( - NavigationOptions.mealDetailRoute, - arguments: MealDetailScreenArguments( - state.product, _intakeTypeEntity, _day))); + Future.microtask(() { + if (context.mounted) { + return Navigator.of(context).pushReplacementNamed( + NavigationOptions.mealDetailRoute, + arguments: MealDetailScreenArguments(state.product, + _intakeTypeEntity, _day, state.usesImperialUnits)); + } + }); } else if (state is ScannerFailedState) { return Scaffold( appBar: AppBar(), @@ -84,13 +88,13 @@ class _ScannerScreenState extends State { actions: [ IconButton( icon: ValueListenableBuilder( - valueListenable: cameraController.torchState, + valueListenable: cameraController, builder: (context, state, child) { - switch (state) { - case TorchState.off: + switch (state.torchState) { + case TorchState.off || TorchState.unavailable: return const Icon(Icons.flash_off_outlined, color: Colors.grey); - case TorchState.on: + case TorchState.on || TorchState.auto: return const Icon(Icons.flash_on_outlined); } }, diff --git a/lib/features/settings/presentation/bloc/settings_bloc.dart b/lib/features/settings/presentation/bloc/settings_bloc.dart index 6e0a8fc0..b5a40b5e 100644 --- a/lib/features/settings/presentation/bloc/settings_bloc.dart +++ b/lib/features/settings/presentation/bloc/settings_bloc.dart @@ -23,9 +23,13 @@ class SettingsBloc extends Bloc { final userConfig = await _getConfigUsecase.getConfig(); final appVersion = await AppConst.getVersionNumber(); + final usesImperialUnits = userConfig.usesImperialUnits; - emit(SettingsLoadedState(appVersion, - userConfig.hasAcceptedSendAnonymousData, userConfig.appTheme)); + emit(SettingsLoadedState( + appVersion, + userConfig.hasAcceptedSendAnonymousData, + userConfig.appTheme, + usesImperialUnits)); }); } @@ -37,4 +41,10 @@ class SettingsBloc extends Bloc { void setAppTheme(AppThemeEntity appTheme) async { await _addConfigUsecase.setConfigAppTheme(appTheme); } + + void setUsesImperialUnits(bool usesImperialUnits) { + _addConfigUsecase.setConfigUsesImperialUnits(usesImperialUnits); + } } + +enum SystemDropDownType { metric, imperial } diff --git a/lib/features/settings/presentation/bloc/settings_state.dart b/lib/features/settings/presentation/bloc/settings_state.dart index a461cf20..7e8f96b1 100644 --- a/lib/features/settings/presentation/bloc/settings_state.dart +++ b/lib/features/settings/presentation/bloc/settings_state.dart @@ -18,10 +18,12 @@ class SettingsLoadedState extends SettingsState { final String versionNumber; final bool sendAnonymousData; final AppThemeEntity appTheme; + final bool usesImperialUnits; - const SettingsLoadedState( - this.versionNumber, this.sendAnonymousData, this.appTheme); + const SettingsLoadedState(this.versionNumber, this.sendAnonymousData, + this.appTheme, this.usesImperialUnits); @override - List get props => [versionNumber, sendAnonymousData, appTheme]; + List get props => + [versionNumber, sendAnonymousData, appTheme, usesImperialUnits]; } diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index d0ac5d68..f72e8148 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -7,6 +7,9 @@ import 'package:opennutritracker/core/utils/app_const.dart'; import 'package:opennutritracker/core/utils/locator.dart'; import 'package:opennutritracker/core/utils/theme_mode_provider.dart'; import 'package:opennutritracker/core/utils/url_const.dart'; +import 'package:opennutritracker/features/diary/presentation/bloc/diary_bloc.dart'; +import 'package:opennutritracker/features/home/presentation/bloc/home_bloc.dart'; +import 'package:opennutritracker/features/profile/presentation/bloc/profile_bloc.dart'; import 'package:opennutritracker/features/settings/presentation/bloc/settings_bloc.dart'; import 'package:opennutritracker/generated/l10n.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -23,10 +26,16 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { late SettingsBloc _settingsBloc; + late ProfileBloc _profileBloc; + late HomeBloc _homeBloc; + late DiaryBloc _diaryBloc; @override void initState() { _settingsBloc = locator(); + _profileBloc = locator(); + _homeBloc = locator(); + _diaryBloc = locator(); super.initState(); } @@ -50,7 +59,8 @@ class _SettingsScreenState extends State { ListTile( leading: const Icon(Icons.ac_unit_outlined), title: Text(S.of(context).settingsUnitsLabel), - onTap: () => _showUnitsDialog(context), + onTap: () => + _showUnitsDialog(context, state.usesImperialUnits), ), ListTile( leading: const Icon(Icons.calculate_outlined), @@ -94,8 +104,11 @@ class _SettingsScreenState extends State { ); } - void _showUnitsDialog(BuildContext context) { - showDialog( + void _showUnitsDialog(BuildContext context, bool usesImperialUnits) async { + SystemDropDownType selectedUnit = usesImperialUnits + ? SystemDropDownType.imperial + : SystemDropDownType.metric; + final shouldUpdate = await showDialog( context: context, builder: (context) { return AlertDialog( @@ -104,45 +117,45 @@ class _SettingsScreenState extends State { Column( children: [ DropdownButtonFormField( + value: selectedUnit, decoration: InputDecoration( - enabled: false, + enabled: true, filled: false, - labelText: S.of(context).settingsMassLabel, + labelText: S.of(context).settingsSystemLabel, ), - onChanged: null, - items: const [ - DropdownMenuItem(child: Text('kg, g, mg')) - ], // TODO add units - ), - DropdownButtonFormField( - decoration: InputDecoration( - enabled: false, - filled: false, - labelText: S.of(context).settingsDistanceLabel, - ), - onChanged: null, - items: const [DropdownMenuItem(child: Text('cm, m, km'))], - ), - DropdownButtonFormField( - decoration: InputDecoration( - enabled: false, - filled: false, - labelText: S.of(context).settingsVolumeLabel, - ), - onChanged: null, - items: const [DropdownMenuItem(child: Text('ml, cl, l'))], - ), + onChanged: (value) { + selectedUnit = value ?? SystemDropDownType.metric; + }, + items: [ + DropdownMenuItem( + value: SystemDropDownType.metric, + child: Text(S.of(context).settingsMetricLabel)), + DropdownMenuItem( + value: SystemDropDownType.imperial, + child: Text(S.of(context).settingsImperialLabel)) + ], + ) ], ), ]), actions: [ TextButton( onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(true); }, child: Text(S.of(context).dialogOKLabel)) ]); }); + if (shouldUpdate == true) { + _settingsBloc + .setUsesImperialUnits(selectedUnit == SystemDropDownType.imperial); + _settingsBloc.add(LoadSettingsEvent()); + + // Update blocs + _profileBloc.add(LoadProfileEvent()); + _homeBloc.add(LoadItemsEvent()); + _diaryBloc.add(const LoadDiaryYearEvent()); + } } void _showCalculationsDialog(BuildContext context) { diff --git a/lib/generated/intl/messages_de.dart b/lib/generated/intl/messages_de.dart index 73f21113..9ff314aa 100644 --- a/lib/generated/intl/messages_de.dart +++ b/lib/generated/intl/messages_de.dart @@ -84,6 +84,12 @@ class MessageLookup extends MessageLookupByLibrary { "chooseWeightGoalLabel": MessageLookupByLibrary.simpleMessage("Gewichtsziel wählen"), "cmLabel": MessageLookupByLibrary.simpleMessage("cm"), + "copyDialogTitle": MessageLookupByLibrary.simpleMessage( + "Zu welcher Mahlzeit hinzufügen?"), + "copyOrDeleteTimeDialogContent": MessageLookupByLibrary.simpleMessage( + "Auf \"Nach heute kopieren\" klicken, um die Mahlzeit nach heute zu kopieren. Mit \"Löschen\" kann die Mahlzeit entfernt werden"), + "copyOrDeleteTimeDialogTitle": + MessageLookupByLibrary.simpleMessage("Was soll getan werden?"), "createCustomDialogContent": MessageLookupByLibrary.simpleMessage( "Möchten Sie einen benutzerdefinierte Mahlzeit erstellen?"), "createCustomDialogTitle": MessageLookupByLibrary.simpleMessage( @@ -95,6 +101,9 @@ class MessageLookup extends MessageLookupByLibrary { "deleteTimeDialogTitle": MessageLookupByLibrary.simpleMessage("Eintrag löschen?"), "dialogCancelLabel": MessageLookupByLibrary.simpleMessage("ABBRECHEN"), + "dialogCopyLabel": + MessageLookupByLibrary.simpleMessage("NACH HEUTE KOPIEREN"), + "dialogDeleteLabel": MessageLookupByLibrary.simpleMessage("LÖSCHEN"), "dialogOKLabel": MessageLookupByLibrary.simpleMessage("OK"), "diaryLabel": MessageLookupByLibrary.simpleMessage("Tagebuch"), "dinnerExample": MessageLookupByLibrary.simpleMessage( @@ -161,6 +170,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Protein pro 100 g/ml"), "mealSizeLabel": MessageLookupByLibrary.simpleMessage("Mahlzeitsgröße (g/ml)"), + "mealSizeLabelImperial": + MessageLookupByLibrary.simpleMessage("Mahlzeitsgröße (oz/fl oz)"), "mealUnitLabel": MessageLookupByLibrary.simpleMessage("Mahlzeiteinheit"), "milliliterUnit": MessageLookupByLibrary.simpleMessage("ml"), @@ -216,7 +227,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Was ist Ihr Geschlecht?"), "onboardingGoalQuestionSubtitle": MessageLookupByLibrary.simpleMessage( "Was ist Ihr aktuelles Gewichtsziel?"), - "onboardingHeightExampleHint": + "onboardingHeightExampleHintCm": MessageLookupByLibrary.simpleMessage("z. B. 170"), "onboardingHeightQuestionSubtitle": MessageLookupByLibrary.simpleMessage("Wie groß sind Sie derzeit?"), @@ -228,7 +239,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Übersicht"), "onboardingSaveUserError": MessageLookupByLibrary.simpleMessage( "Falsche Eingabe, bitte versuchen Sie es erneut"), - "onboardingWeightExampleHint": + "onboardingWeightExampleHintKg": MessageLookupByLibrary.simpleMessage("z. B. 60"), "onboardingWeightQuestionSubtitle": MessageLookupByLibrary.simpleMessage( @@ -567,7 +578,7 @@ class MessageLookup extends MessageLookupByLibrary { "Überwiegend Gehen, Laufen oder Gewichte tragen bei der Arbeit und aktive Freizeitaktivitäten"), "palVeryActiveLabel": MessageLookupByLibrary.simpleMessage("Sehr aktiv"), - "per100gLabel": MessageLookupByLibrary.simpleMessage("Pro 100 g"), + "per100gmlLabel": MessageLookupByLibrary.simpleMessage("Pro 100 g/ml"), "privacyPolicyLabel": MessageLookupByLibrary.simpleMessage("Datenschutzrichtlinie"), "profileLabel": MessageLookupByLibrary.simpleMessage("Profil"), diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 6867887e..ddb324b5 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -84,6 +84,12 @@ class MessageLookup extends MessageLookupByLibrary { "chooseWeightGoalLabel": MessageLookupByLibrary.simpleMessage("Choose Weight Goal"), "cmLabel": MessageLookupByLibrary.simpleMessage("cm"), + "copyDialogTitle": MessageLookupByLibrary.simpleMessage( + "Which meal type do you want to copy to?"), + "copyOrDeleteTimeDialogContent": MessageLookupByLibrary.simpleMessage( + "With \"Copy to today\" you can copy the meal to today. With \"Delete\" you can delete the meal."), + "copyOrDeleteTimeDialogTitle": + MessageLookupByLibrary.simpleMessage("What do you want to do?"), "createCustomDialogContent": MessageLookupByLibrary.simpleMessage( "Do you want create a custom meal item?"), "createCustomDialogTitle": @@ -95,6 +101,9 @@ class MessageLookup extends MessageLookupByLibrary { "deleteTimeDialogTitle": MessageLookupByLibrary.simpleMessage("Delete Item?"), "dialogCancelLabel": MessageLookupByLibrary.simpleMessage("CANCEL"), + "dialogCopyLabel": + MessageLookupByLibrary.simpleMessage("COPY TO TODAY"), + "dialogDeleteLabel": MessageLookupByLibrary.simpleMessage("DELETE"), "dialogOKLabel": MessageLookupByLibrary.simpleMessage("OK"), "diaryLabel": MessageLookupByLibrary.simpleMessage("Diary"), "dinnerExample": MessageLookupByLibrary.simpleMessage( @@ -120,6 +129,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Product not found"), "fatLabel": MessageLookupByLibrary.simpleMessage("fat"), "fiberLabel": MessageLookupByLibrary.simpleMessage("fiber"), + "flOzUnit": MessageLookupByLibrary.simpleMessage("fl.oz"), + "ftLabel": MessageLookupByLibrary.simpleMessage("ft"), "genderFemaleLabel": MessageLookupByLibrary.simpleMessage("♀ female"), "genderLabel": MessageLookupByLibrary.simpleMessage("Gender"), "genderMaleLabel": MessageLookupByLibrary.simpleMessage("♂ male"), @@ -143,6 +154,7 @@ class MessageLookup extends MessageLookupByLibrary { "kcalLabel": MessageLookupByLibrary.simpleMessage("kcal"), "kcalLeftLabel": MessageLookupByLibrary.simpleMessage("kcal left"), "kgLabel": MessageLookupByLibrary.simpleMessage("kg"), + "lbsLabel": MessageLookupByLibrary.simpleMessage("lbs"), "lunchExample": MessageLookupByLibrary.simpleMessage("e.g. pizza, salad, rice ..."), "lunchLabel": MessageLookupByLibrary.simpleMessage("Lunch"), @@ -155,6 +167,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("protein per 100 g/ml"), "mealSizeLabel": MessageLookupByLibrary.simpleMessage("Meal size (g/ml)"), + "mealSizeLabelImperial": + MessageLookupByLibrary.simpleMessage("Meal size (oz/fl oz)"), "mealUnitLabel": MessageLookupByLibrary.simpleMessage("Meal unit"), "milliliterUnit": MessageLookupByLibrary.simpleMessage("ml"), "missingProductInfo": MessageLookupByLibrary.simpleMessage( @@ -209,8 +223,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("What\'s your gender?"), "onboardingGoalQuestionSubtitle": MessageLookupByLibrary.simpleMessage( "What\'s your current weight goal?"), - "onboardingHeightExampleHint": + "onboardingHeightExampleHintCm": MessageLookupByLibrary.simpleMessage("e.g. 170"), + "onboardingHeightExampleHintFt": + MessageLookupByLibrary.simpleMessage("e.g. 5.8"), "onboardingHeightQuestionSubtitle": MessageLookupByLibrary.simpleMessage("Whats your current height?"), "onboardingIntroDescription": MessageLookupByLibrary.simpleMessage( @@ -221,8 +237,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Overview"), "onboardingSaveUserError": MessageLookupByLibrary.simpleMessage( "Wrong input, please try again"), - "onboardingWeightExampleHint": + "onboardingWeightExampleHintKg": MessageLookupByLibrary.simpleMessage("e.g. 60"), + "onboardingWeightExampleHintLbs": + MessageLookupByLibrary.simpleMessage("e.g. 132"), "onboardingWeightQuestionSubtitle": MessageLookupByLibrary.simpleMessage("Whats your current weight?"), "onboardingWelcomeLabel": @@ -235,6 +253,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Your calorie goal:"), "onboardingYourMacrosGoalLabel": MessageLookupByLibrary.simpleMessage("Your macronutrient goals:"), + "ozUnit": MessageLookupByLibrary.simpleMessage("oz"), "paAmericanFootballGeneral": MessageLookupByLibrary.simpleMessage("football"), "paAmericanFootballGeneralDesc": @@ -549,7 +568,7 @@ class MessageLookup extends MessageLookupByLibrary { "Mostly walking, running or carrying weight in job and active free time activities"), "palVeryActiveLabel": MessageLookupByLibrary.simpleMessage("Very Active"), - "per100gLabel": MessageLookupByLibrary.simpleMessage("Per 100g"), + "per100gmlLabel": MessageLookupByLibrary.simpleMessage("Per 100g/ml"), "privacyPolicyLabel": MessageLookupByLibrary.simpleMessage("Privacy policy"), "profileLabel": MessageLookupByLibrary.simpleMessage("Profile"), @@ -584,6 +603,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Send anonymous usage data"), "servingSizeLabel": MessageLookupByLibrary.simpleMessage("Serving size (g/ml)"), + "servingSizeLabelImperial": + MessageLookupByLibrary.simpleMessage("Serving size (oz/fl oz)"), "settingAboutLabel": MessageLookupByLibrary.simpleMessage("About"), "settingFeedbackLabel": MessageLookupByLibrary.simpleMessage("Feedback"), @@ -593,16 +614,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Disclaimer"), "settingsDistanceLabel": MessageLookupByLibrary.simpleMessage("Distance"), + "settingsImperialLabel": + MessageLookupByLibrary.simpleMessage("Imperial (lbs, ft, oz)"), "settingsLabel": MessageLookupByLibrary.simpleMessage("Settings"), "settingsLicensesLabel": MessageLookupByLibrary.simpleMessage("Licenses"), "settingsMassLabel": MessageLookupByLibrary.simpleMessage("Mass"), + "settingsMetricLabel": + MessageLookupByLibrary.simpleMessage("Metric (kg, cm, ml)"), "settingsPrivacySettings": MessageLookupByLibrary.simpleMessage("Privacy Settings"), "settingsReportErrorLabel": MessageLookupByLibrary.simpleMessage("Report Error"), "settingsSourceCodeLabel": MessageLookupByLibrary.simpleMessage("Source Code"), + "settingsSystemLabel": MessageLookupByLibrary.simpleMessage("System"), "settingsThemeDarkLabel": MessageLookupByLibrary.simpleMessage("Dark"), "settingsThemeLabel": MessageLookupByLibrary.simpleMessage("Theme"), "settingsThemeLightLabel": diff --git a/lib/generated/intl/messages_tr.dart b/lib/generated/intl/messages_tr.dart index 9d56d446..810cc0b0 100644 --- a/lib/generated/intl/messages_tr.dart +++ b/lib/generated/intl/messages_tr.dart @@ -82,6 +82,12 @@ class MessageLookup extends MessageLookupByLibrary { "chooseWeightGoalLabel": MessageLookupByLibrary.simpleMessage("Kilo Hedefi Seçin"), "cmLabel": MessageLookupByLibrary.simpleMessage("cm"), + "copyDialogTitle": MessageLookupByLibrary.simpleMessage( + "Hangi öğüne eklemek istiyorsunuz?"), + "copyOrDeleteTimeDialogContent": MessageLookupByLibrary.simpleMessage( + "\"Bugüne kopyala\" seçeneğine tıklayarak öğünü bugüne kopyalayabilirsiniz. \"Sil\" seçeneği ile öğün kaldırılabilir."), + "copyOrDeleteTimeDialogTitle": + MessageLookupByLibrary.simpleMessage("Ne yapılmalı?"), "createCustomDialogContent": MessageLookupByLibrary.simpleMessage( "Özel bir yemek öğesi oluşturmak istiyor musunuz?"), "createCustomDialogTitle": MessageLookupByLibrary.simpleMessage( @@ -93,6 +99,9 @@ class MessageLookup extends MessageLookupByLibrary { "deleteTimeDialogTitle": MessageLookupByLibrary.simpleMessage("Öğe Silinsin mi?"), "dialogCancelLabel": MessageLookupByLibrary.simpleMessage("İPTAL"), + "dialogCopyLabel": + MessageLookupByLibrary.simpleMessage("BUGÜNE KOPYALA"), + "dialogDeleteLabel": MessageLookupByLibrary.simpleMessage("SİL"), "dialogOKLabel": MessageLookupByLibrary.simpleMessage("TAMAM"), "diaryLabel": MessageLookupByLibrary.simpleMessage("Günlük"), "dinnerExample": MessageLookupByLibrary.simpleMessage( @@ -156,6 +165,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("100 g/ml başına protein"), "mealSizeLabel": MessageLookupByLibrary.simpleMessage("Yemek boyutu (g/ml)"), + "mealSizeLabelImperial": + MessageLookupByLibrary.simpleMessage("Yemek boyutu (oz/fl oz)"), "mealUnitLabel": MessageLookupByLibrary.simpleMessage("Yemek birimi"), "milliliterUnit": MessageLookupByLibrary.simpleMessage("ml"), "missingProductInfo": MessageLookupByLibrary.simpleMessage( @@ -210,7 +221,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Cinsiyetiniz nedir?"), "onboardingGoalQuestionSubtitle": MessageLookupByLibrary.simpleMessage( "Mevcut kilo hedefiniz nedir?"), - "onboardingHeightExampleHint": + "onboardingHeightExampleHintCm": MessageLookupByLibrary.simpleMessage("örn. 170"), "onboardingHeightQuestionSubtitle": MessageLookupByLibrary.simpleMessage("Mevcut boyunuz nedir?"), @@ -222,7 +233,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Genel Bakış"), "onboardingSaveUserError": MessageLookupByLibrary.simpleMessage( "Yanlış giriş, lütfen tekrar deneyin"), - "onboardingWeightExampleHint": + "onboardingWeightExampleHintKg": MessageLookupByLibrary.simpleMessage("örn. 60"), "onboardingWeightQuestionSubtitle": MessageLookupByLibrary.simpleMessage("Mevcut kilonuz nedir?"), @@ -542,7 +553,8 @@ class MessageLookup extends MessageLookupByLibrary { "palVeryActiveDescriptionLabel": MessageLookupByLibrary.simpleMessage( "Çoğunlukla işte yürümek, koşmak veya ağırlık taşımak ve aktif boş zaman aktiviteleri"), "palVeryActiveLabel": MessageLookupByLibrary.simpleMessage("Çok Aktif"), - "per100gLabel": MessageLookupByLibrary.simpleMessage("100g başına"), + "per100gmlLabel": + MessageLookupByLibrary.simpleMessage("100g/ml başına"), "privacyPolicyLabel": MessageLookupByLibrary.simpleMessage("Gizlilik politikası"), "profileLabel": MessageLookupByLibrary.simpleMessage("Profil"), @@ -576,6 +588,8 @@ class MessageLookup extends MessageLookupByLibrary { "Anonim kullanım verilerini gönder"), "servingSizeLabel": MessageLookupByLibrary.simpleMessage("Porsiyon boyutu (g/ml)"), + "servingSizeLabelImperial": + MessageLookupByLibrary.simpleMessage("Porsiyon boyutu (oz/fl oz)"), "settingAboutLabel": MessageLookupByLibrary.simpleMessage("Hakkında"), "settingFeedbackLabel": MessageLookupByLibrary.simpleMessage("Geri Bildirim"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 12a0372b..b16afebc 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -461,20 +461,40 @@ class S { } /// `e.g. 60` - String get onboardingWeightExampleHint { + String get onboardingWeightExampleHintKg { return Intl.message( 'e.g. 60', - name: 'onboardingWeightExampleHint', + name: 'onboardingWeightExampleHintKg', + desc: '', + args: [], + ); + } + + /// `e.g. 132` + String get onboardingWeightExampleHintLbs { + return Intl.message( + 'e.g. 132', + name: 'onboardingWeightExampleHintLbs', desc: '', args: [], ); } /// `e.g. 170` - String get onboardingHeightExampleHint { + String get onboardingHeightExampleHintCm { return Intl.message( 'e.g. 170', - name: 'onboardingHeightExampleHint', + name: 'onboardingHeightExampleHintCm', + desc: '', + args: [], + ); + } + + /// `e.g. 5.8` + String get onboardingHeightExampleHintFt { + return Intl.message( + 'e.g. 5.8', + name: 'onboardingHeightExampleHintFt', desc: '', args: [], ); @@ -650,6 +670,36 @@ class S { ); } + /// `System` + String get settingsSystemLabel { + return Intl.message( + 'System', + name: 'settingsSystemLabel', + desc: '', + args: [], + ); + } + + /// `Metric (kg, cm, ml)` + String get settingsMetricLabel { + return Intl.message( + 'Metric (kg, cm, ml)', + name: 'settingsMetricLabel', + desc: '', + args: [], + ); + } + + /// `Imperial (lbs, ft, oz)` + String get settingsImperialLabel { + return Intl.message( + 'Imperial (lbs, ft, oz)', + name: 'settingsImperialLabel', + desc: '', + args: [], + ); + } + /// `Distance` String get settingsDistanceLabel { return Intl.message( @@ -921,6 +971,56 @@ class S { ); } + /// `Which meal type do you want to copy to?` + String get copyDialogTitle { + return Intl.message( + 'Which meal type do you want to copy to?', + name: 'copyDialogTitle', + desc: '', + args: [], + ); + } + + /// `What do you want to do?` + String get copyOrDeleteTimeDialogTitle { + return Intl.message( + 'What do you want to do?', + name: 'copyOrDeleteTimeDialogTitle', + desc: '', + args: [], + ); + } + + /// `With "Copy to today" you can copy the meal to today. With "Delete" you can delete the meal.` + String get copyOrDeleteTimeDialogContent { + return Intl.message( + 'With "Copy to today" you can copy the meal to today. With "Delete" you can delete the meal.', + name: 'copyOrDeleteTimeDialogContent', + desc: '', + args: [], + ); + } + + /// `COPY TO TODAY` + String get dialogCopyLabel { + return Intl.message( + 'COPY TO TODAY', + name: 'dialogCopyLabel', + desc: '', + args: [], + ); + } + + /// `DELETE` + String get dialogDeleteLabel { + return Intl.message( + 'DELETE', + name: 'dialogDeleteLabel', + desc: '', + args: [], + ); + } + /// `supplied` String get suppliedLabel { return Intl.message( @@ -1051,11 +1151,11 @@ class S { ); } - /// `Per 100g` - String get per100gLabel { + /// `Per 100g/ml` + String get per100gmlLabel { return Intl.message( - 'Per 100g', - name: 'per100gLabel', + 'Per 100g/ml', + name: 'per100gmlLabel', desc: '', args: [], ); @@ -1191,6 +1291,26 @@ class S { ); } + /// `oz` + String get ozUnit { + return Intl.message( + 'oz', + name: 'ozUnit', + desc: '', + args: [], + ); + } + + /// `fl.oz` + String get flOzUnit { + return Intl.message( + 'fl.oz', + name: 'flOzUnit', + desc: '', + args: [], + ); + } + /// `Product missing required kcal or macronutrients information` String get missingProductInfo { return Intl.message( @@ -1261,6 +1381,16 @@ class S { ); } + /// `Meal size (oz/fl oz)` + String get mealSizeLabelImperial { + return Intl.message( + 'Meal size (oz/fl oz)', + name: 'mealSizeLabelImperial', + desc: '', + args: [], + ); + } + /// `Serving size (g/ml)` String get servingSizeLabel { return Intl.message( @@ -1271,6 +1401,16 @@ class S { ); } + /// `Serving size (oz/fl oz)` + String get servingSizeLabelImperial { + return Intl.message( + 'Serving size (oz/fl oz)', + name: 'servingSizeLabelImperial', + desc: '', + args: [], + ); + } + /// `Meal unit` String get mealUnitLabel { return Intl.message( @@ -1551,6 +1691,16 @@ class S { ); } + /// `ft` + String get ftLabel { + return Intl.message( + 'ft', + name: 'ftLabel', + desc: '', + args: [], + ); + } + /// `Select Weight` String get selectWeightDialogLabel { return Intl.message( @@ -1581,6 +1731,16 @@ class S { ); } + /// `lbs` + String get lbsLabel { + return Intl.message( + 'lbs', + name: 'lbsLabel', + desc: '', + args: [], + ); + } + /// `Age` String get ageLabel { return Intl.message( diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index ac1bb059..22e9bcab 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -43,8 +43,8 @@ "onboardingWeightQuestionSubtitle": "Wie viel wiegen Sie derzeit?", "onboardingWrongHeightLabel": "Geben Sie eine korrekte Größe ein", "onboardingWrongWeightLabel": "Geben Sie ein korrekte Gewicht ein", - "onboardingWeightExampleHint": "z. B. 60", - "onboardingHeightExampleHint": "z. B. 170", + "onboardingWeightExampleHintKg": "z. B. 60", + "onboardingHeightExampleHintCm": "z. B. 170", "onboardingActivityQuestionSubtitle": "Wie aktiv sind Sie? (Ohne Trainingseinheiten)", "onboardingGoalQuestionSubtitle": "Was ist Ihr aktuelles Gewichtsziel?", "onboardingSaveUserError": "Falsche Eingabe, bitte versuchen Sie es erneut", @@ -94,6 +94,13 @@ "deleteTimeDialogContent": "Möchten Sie den ausgewählten Eintrag löschen?", "itemDeletedSnackbar": "Eintrag gelöscht", + "copyDialogTitle": "Zu welcher Mahlzeit hinzufügen?", + + "copyOrDeleteTimeDialogTitle": "Was soll getan werden?", + "copyOrDeleteTimeDialogContent": "Auf \"Nach heute kopieren\" klicken, um die Mahlzeit nach heute zu kopieren. Mit \"Löschen\" kann die Mahlzeit entfernt werden", + "dialogCopyLabel": "NACH HEUTE KOPIEREN", + "dialogDeleteLabel": "LÖSCHEN", + "suppliedLabel": "zugeführt", "burnedLabel": "verbrannt", "kcalLeftLabel": "kcal übrig", @@ -108,7 +115,7 @@ "carbohydrateLabel": "Kohlenhydrate", "sugarLabel": "Zucker", "fiberLabel": "Ballaststoffe", - "per100gLabel": "Pro 100 g", + "per100gmlLabel": "Pro 100 g/ml", "additionalInfoLabelOFF": "Weitere Informationen unter\nOpenFoodFacts", "offDisclaimer": "Die Daten, die Ihnen mit dieser App zur Verfügung gestellt werden, stammen aus der Open Food Facts-Datenbank. Es kann keine Garantie für die Richtigkeit, Vollständigkeit oder Zuverlässigkeit der bereitgestellten Informationen übernommen werden. Die Daten werden ohne Mängelgewähr zur Verfügung gestellt, und die Ursprungsquelle der Daten (Open Food Facts) haftet nicht für Schäden, die aus der Verwendung der Daten entstehen.", "additionalInfoLabelFDC": "Weitere Informationen unter\nFoodData Central", @@ -130,6 +137,7 @@ "mealNameLabel": "Mahlzeitenname", "mealBrandsLabel": "Marken", "mealSizeLabel": "Mahlzeitsgröße (g/ml)", + "mealSizeLabelImperial": "Mahlzeitsgröße (oz/fl oz)", "servingSizeLabel": "Portionsgröße (g/ml)", "mealUnitLabel": "Mahlzeiteinheit", "mealKcalLabel": "kcal pro 100 g/ml", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 477e40d0..a0397914 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -4,7 +4,6 @@ "appDescription": "OpenNutriTracker is a free and open-source calorie and nutrient tracker that respects your privacy.", "alphaVersionName": "[Alpha]", "betaVersionName": "[Beta]", - "addLabel": "Add", "createCustomDialogTitle": "Create custom meal item?", "createCustomDialogContent": "Do you want create a custom meal item?", @@ -21,14 +20,12 @@ "recentlyAddedLabel": "Recently", "noMealsRecentlyAddedLabel": "No meals recently added", "noActivityRecentlyAddedLabel": "No activity recently added", - "dialogOKLabel": "OK", "dialogCancelLabel": "CANCEL", "buttonStartLabel": "START", "buttonNextLabel": "NEXT", "buttonSaveLabel": "Save", "buttonYesLabel": "YES", - "onboardingWelcomeLabel": "Welcome to", "onboardingOverviewLabel": "Overview", "onboardingYourGoalLabel": "Your calorie goal:", @@ -43,12 +40,13 @@ "onboardingWeightQuestionSubtitle": "Whats your current weight?", "onboardingWrongHeightLabel": "Enter correct height", "onboardingWrongWeightLabel": "Enter correct weight", - "onboardingWeightExampleHint": "e.g. 60", - "onboardingHeightExampleHint": "e.g. 170", + "onboardingWeightExampleHintKg": "e.g. 60", + "onboardingWeightExampleHintLbs": "e.g. 132", + "onboardingHeightExampleHintCm": "e.g. 170", + "onboardingHeightExampleHintFt": "e.g. 5.8", "onboardingActivityQuestionSubtitle": "How active are you? (without workouts)", "onboardingGoalQuestionSubtitle": "What's your current weight goal?", "onboardingSaveUserError": "Wrong input, please try again", - "settingsUnitsLabel": "Units", "settingsCalculationsLabel": "Calculations", "settingsThemeLabel": "Theme", @@ -63,6 +61,9 @@ "settingFeedbackLabel": "Feedback", "settingAboutLabel": "About", "settingsMassLabel": "Mass", + "settingsSystemLabel": "System", + "settingsMetricLabel": "Metric (kg, cm, ml)", + "settingsImperialLabel": "Imperial (lbs, ft, oz)", "settingsDistanceLabel": "Distance", "settingsVolumeLabel": "Volume", "disclaimerText": "OpenNutriTracker is not a medical application. All data provided is not validated and should be used with caution. Please maintain a healthy lifestyle and consult a professional if you have any problems. Use during illness, pregnancy or lactation is not recommended.\n\n\nThe application is still under development. Errors, bugs and crashes may occur.", @@ -74,7 +75,6 @@ "calculationsRecommendedLabel": "(recommended)", "calculationsMacronutrientsDistributionLabel": "Macros distribution", "calculationsMacrosDistribution": "{pctCarbs}% carbs, {pctFats}% fats, {pctProteins}% proteins", - "addItemLabel": "Add new Item:", "activityLabel": "Activity", "activityExample": "e.g. running, biking, yoga ...", @@ -86,18 +86,19 @@ "dinnerExample": "e.g. soup, chicken, wine ...", "snackLabel": "Snack", "snackExample": "e.g. apple, ice cream, chocolate ...", - "editItemDialogTitle": "Edit item", "itemUpdatedSnackbar": "Item updated", - "deleteTimeDialogTitle": "Delete Item?", "deleteTimeDialogContent": "Do want to delete the selected item?", "itemDeletedSnackbar": "Item deleted", - + "copyDialogTitle": "Which meal type do you want to copy to?", + "copyOrDeleteTimeDialogTitle": "What do you want to do?", + "copyOrDeleteTimeDialogContent": "With \"Copy to today\" you can copy the meal to today. With \"Delete\" you can delete the meal.", + "dialogCopyLabel": "COPY TO TODAY", + "dialogDeleteLabel": "DELETE", "suppliedLabel": "supplied", "burnedLabel": "burned", "kcalLeftLabel": "kcal left", - "nutritionInfoLabel": "Nutrition Information", "kcalLabel": "kcal", "carbsLabel": "carbs", @@ -108,7 +109,7 @@ "carbohydrateLabel": "carbohydrate", "sugarLabel": "sugar", "fiberLabel": "fiber", - "per100gLabel": "Per 100g", + "per100gmlLabel": "Per 100g/ml", "additionalInfoLabelOFF": "More Information at\nOpenFoodFacts", "offDisclaimer": "The data provided to you by this app are retrieved from the Open Food Facts database. No guarantees can be made for the accuracy, completeness, or reliability of the information provided. The data are provided “as is” and the originating source for the data (Open Food Facts) is not liable for any damages arising out of the use of the data.", "additionalInfoLabelFDC": "More Information at\nFoodData Central", @@ -122,29 +123,29 @@ "gramUnit": "g", "milliliterUnit": "ml", "gramMilliliterUnit": "g/ml", + "ozUnit": "oz", + "flOzUnit": "fl.oz", "missingProductInfo": "Product missing required kcal or macronutrients information", - "infoAddedIntakeLabel": "Added new intake", "infoAddedActivityLabel": "Added new activity", - "editMealLabel": "Edit meal", "mealNameLabel": "Meal name", "mealBrandsLabel": "Brands", "mealSizeLabel": "Meal size (g/ml)", + "mealSizeLabelImperial": "Meal size (oz/fl oz)", "servingSizeLabel": "Serving size (g/ml)", + "servingSizeLabelImperial": "Serving size (oz/fl oz)", "mealUnitLabel": "Meal unit", "mealKcalLabel": "kcal per", "mealCarbsLabel": "carbs per", "mealFatLabel": "fat per", "mealProteinLabel": "protein per 100 g/ml", "errorMealSave": "Error while saving meal. Did you input the correct meal information?", - "bmiLabel": "BMI", "bmiInfo": "Body Mass Index (BMI) is a index to classify overweight and obesity in adults. It is defined as weight in kilograms divided by the square of height in meters (kg/m²).\n\nBMI does not differentiate between fat and muscle mass and can be misleading for some individuals.", "readLabel": "I have read and accept the privacy policy.", "privacyPolicyLabel": "Privacy policy", "dataCollectionLabel": "Support development by providing anonymous usage data", - "palSedentaryLabel": "Sedentary", "palSedentaryDescriptionLabel": "e.g. office job and mostly sitting free time activities", "palLowLActiveLabel": "Low Active", @@ -153,7 +154,6 @@ "palActiveDescriptionLabel": "Mostly standing or walking in job and active free time activities", "palVeryActiveLabel": "Very Active", "palVeryActiveDescriptionLabel": "Mostly walking, running or carrying weight in job and active free time activities", - "selectPalCategoryLabel": "Select Activity Level", "chooseWeightGoalLabel": "Choose Weight Goal", "goalLoseWeight": "Lose Weight", @@ -163,25 +163,24 @@ "selectHeightDialogLabel": "Select Height", "heightLabel": "Height", "cmLabel": "cm", + "ftLabel": "ft", "selectWeightDialogLabel": "Select Weight", "weightLabel": "Weight", "kgLabel": "kg", + "lbsLabel": "lbs", "ageLabel": "Age", "yearsLabel": "{age} years", "selectGenderDialogLabel": "Select Gender", "genderLabel": "Gender", "genderMaleLabel": "♂ male", "genderFemaleLabel": "♀ female", - "nothingAddedLabel": "Nothing added", - "nutritionalStatusUnderweight": "Underweight", "nutritionalStatusNormalWeight": "Normal Weight", "nutritionalStatusPreObesity": "Pre-obesity", "nutritionalStatusObeseClassI": "Obesity Class I", "nutritionalStatusObeseClassII": "Obesity Class II", "nutritionalStatusObeseClassIII": "Obesity Class III", - "nutritionalStatusRiskLabel": "Risk of comorbidities: {riskValue}", "nutritionalStatusRiskLow": "Low \n(but risk of other \nclinical problems increased)", "nutritionalStatusRiskAverage": "Average", @@ -189,7 +188,6 @@ "nutritionalStatusRiskModerate": "Moderate", "nutritionalStatusRiskSevere": "Severe", "nutritionalStatusRiskVerySevere": "Very severe", - "errorOpeningEmail": "Error while opening email app", "errorOpeningBrowser": "Error while opening browser app", "errorFetchingProductData": "Error while fetching product data", @@ -197,9 +195,7 @@ "errorLoadingActivities": "Error while loading activities", "noResultsFound": "No results found", "retryLabel": "Retry", - "@PHYSICAL_ACTIVITIES": {}, - "paHeadingBicycling": "bicycling", "paHeadingConditionalExercise": "conditioning exercise", "paHeadingDancing": "dancing", @@ -208,9 +204,7 @@ "paHeadingWalking": "walking", "paHeadingWaterActivities": "water activities", "paHeadingWinterActivities": "winter activities", - "paGeneralDesc": "general", - "paBicyclingGeneral": "bicycling", "paBicyclingGeneralDesc": "general", "paBicyclingMountainGeneral": "bicycling, mountain", @@ -401,5 +395,4 @@ "paSkiingGeneralDesc": "general", "paSnowShovingModerate": "snow shoveling", "paSnowShovingModerateDesc": "by hand, moderate effort" - } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index b8974358..d664f9d4 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -43,8 +43,8 @@ "onboardingWeightQuestionSubtitle": "Mevcut kilonuz nedir?", "onboardingWrongHeightLabel": "Doğru boyu girin", "onboardingWrongWeightLabel": "Doğru kiloyu girin", - "onboardingWeightExampleHint": "örn. 60", - "onboardingHeightExampleHint": "örn. 170", + "onboardingWeightExampleHintKg": "örn. 60", + "onboardingHeightExampleHintCm": "örn. 170", "onboardingActivityQuestionSubtitle": "Ne kadar aktifsiniz? (antrenmanlar hariç)", "onboardingGoalQuestionSubtitle": "Mevcut kilo hedefiniz nedir?", "onboardingSaveUserError": "Yanlış giriş, lütfen tekrar deneyin", @@ -94,6 +94,13 @@ "deleteTimeDialogContent": "Seçili öğeyi silmek istiyor musunuz?", "itemDeletedSnackbar": "Öğe silindi", + "copyDialogTitle": "Hangi öğüne eklemek istiyorsunuz?", + + "copyOrDeleteTimeDialogTitle": "Ne yapılmalı?", + "copyOrDeleteTimeDialogContent": "\"Bugüne kopyala\" seçeneğine tıklayarak öğünü bugüne kopyalayabilirsiniz. \"Sil\" seçeneği ile öğün kaldırılabilir.", + "dialogCopyLabel": "BUGÜNE KOPYALA", + "dialogDeleteLabel": "SİL", + "suppliedLabel": "alınan", "burnedLabel": "yakılan", "kcalLeftLabel": "kalan kalori", @@ -108,7 +115,7 @@ "carbohydrateLabel": "karbonhidrat", "sugarLabel": "şeker", "fiberLabel": "lif", - "per100gLabel": "100g başına", + "per100gmlLabel": "100g/ml başına", "additionalInfoLabelOFF": "OpenFoodFacts'te\nDaha Fazla Bilgi", "offDisclaimer": "Bu uygulama tarafından size sağlanan veriler Open Food Facts veritabanından alınmıştır. Sağlanan bilgilerin doğruluğu, eksiksizliği veya güvenilirliği konusunda hiçbir garanti verilemez. Veriler \"olduğu gibi\" sağlanır ve verilerin kaynağı (Open Food Facts), verilerin kullanımından kaynaklanan herhangi bir zarardan sorumlu değildir.", "additionalInfoLabelFDC": "FoodData Central'da\nDaha Fazla Bilgi", @@ -130,7 +137,9 @@ "mealNameLabel": "Yemek adı", "mealBrandsLabel": "Markalar", "mealSizeLabel": "Yemek boyutu (g/ml)", + "mealSizeLabelImperial": "Yemek boyutu (oz/fl oz)", "servingSizeLabel": "Porsiyon boyutu (g/ml)", + "servingSizeLabelImperial": "Porsiyon boyutu (oz/fl oz)", "mealUnitLabel": "Yemek birimi", "mealKcalLabel": "100 g/ml başına kalori", "mealCarbsLabel": "100 g/ml başına karbonhidrat", diff --git a/pubspec.lock b/pubspec.lock index fce235db..54d275e4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,50 +5,79 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.11.0" animated_flip_counter: dependency: "direct main" description: name: animated_flip_counter - sha256: "2c25ace054ac3932a3f203875a90ddae760ba7016cb9233573067a12ae51e3c6" + sha256: "73f852d84c461c3e4c1ddf320bee334dde8dba89441922ab11a8013be0b2fad1" url: "https://pub.dev" source: hosted - version: "0.2.6" + version: "0.3.4" app_links: dependency: transitive description: name: app_links - sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "1.0.4" archive: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "4.0.2" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" async: dependency: transitive description: @@ -69,10 +98,10 @@ packages: dependency: transitive description: name: bloc - sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -85,50 +114,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.3" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "8.0.0" built_collection: dependency: transitive description: @@ -141,34 +170,34 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.3" cached_network_image: dependency: "direct main" description: name: cached_network_image - sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.1" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.1" characters: dependency: transitive description: @@ -189,10 +218,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -205,50 +234,50 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dots_indicator: dependency: transitive description: @@ -261,26 +290,26 @@ packages: dependency: "direct main" description: name: envied - sha256: dab29e21452c3d57ec10889d96b06b4a006b01375d4df10b33c9704800c208c4 + sha256: "129a0dbf32b90344fa2e9d6943569fdec8f17904e66161e0a1f09ee3416508ae" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "1.0.0" envied_generator: dependency: "direct dev" description: name: envied_generator - sha256: b8655d5cb39b4d1d449a79ff6f1367b252c23955ff17ec7c03aacdff938598bd + sha256: "76aec98907872ce8488f021e68d213bd0d9bf224eb393a094be1708cc3180d41" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "1.0.0" equatable: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fake_async: dependency: transitive description: @@ -293,26 +322,26 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -322,26 +351,26 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.1.6" flutter_cache_manager: dependency: "direct main" description: name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.1" flutter_keyboard_visibility: dependency: transitive description: name: flutter_keyboard_visibility - sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "6.0.0" flutter_keyboard_visibility_linux: dependency: transitive description: @@ -386,18 +415,18 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.2" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "5.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -407,58 +436,58 @@ packages: dependency: "direct main" description: name: flutter_secure_storage - sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 + sha256: "1913841ac4c7bf57cd2e05b717e1fbff7841b542962feff827b16525a781b3e4" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.2.3" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.3" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.2" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter @@ -473,26 +502,26 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" functions_client: dependency: transitive description: name: functions_client - sha256: "9a0ab83a525c8691a6724746e642de755a299afa04158807787364cd9e718001" + sha256: "61597ed93be197b1be6387855e4b760e6aac2355fcfc4df6d20d2b4579982158" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.4.0" get_it: dependency: "direct main" description: name: get_it - sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 + sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 url: "https://pub.dev" source: hosted - version: "7.6.7" + version: "8.0.3" glob: dependency: transitive description: @@ -505,18 +534,18 @@ packages: dependency: transitive description: name: gotrue - sha256: "1bf6354278a98b8a1867263e94921da8a239de07e9babceab2b4e80af651a098" + sha256: d6362dff9a54f8c1c372bb137c858b4024c16407324d34e6473e59623c9b9f50 url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.11.1" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" gtk: dependency: transitive description: @@ -561,58 +590,58 @@ packages: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: transitive description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.5.2" intl: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" introduction_screen: dependency: "direct main" description: name: introduction_screen - sha256: "72d25ceb71471773783f72783608e17585af93d4bc6474df577fcfe9e7842852" + sha256: "325f26e86fa3c3e86e6ab2bbc1fda860c9e6eae5ff29166fc2a3cab8f710d5b5" url: "https://pub.dev" source: hosted - version: "3.1.12" + version: "3.1.14" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -625,18 +654,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.9.0" jwt_decode: dependency: transitive description: @@ -649,42 +678,50 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "5.1.1" logging: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -697,34 +734,34 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mobile_scanner: dependency: "direct main" description: name: mobile_scanner - sha256: "827765afbd4792ff3fd105ad593821ac0f6d8a7d352689013b07ee85be336312" + sha256: "728828a798d1a2ee506beb652ca23d974c542c96ed03dcbd5eaf97bef96cdaad" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "6.0.2" nested: dependency: transitive description: @@ -737,34 +774,34 @@ packages: dependency: transitive description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" package_config: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "8.1.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.2" path: dependency: transitive description: @@ -777,34 +814,34 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -825,18 +862,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" percent_indicator: dependency: "direct main" description: name: percent_indicator - sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c + sha256: "0d77d5c6fa9b7f60202cedf748b568ba9ba38d3f30405d6ceae4da76f5185462" url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.4" petitparser: dependency: transitive description: @@ -849,10 +886,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -861,14 +898,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -877,14 +906,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.dev" + source: hosted + version: "6.0.1" postgrest: dependency: transitive description: name: postgrest - sha256: "9a3b590cf123f8d323b6a918702e037f037027d12a01902f9dc6ee38fdc05d6c" + sha256: b74dc0f57b5dca5ce9f57a54b08110bf41d6fc8a0483c0fec10c79e9aa0fb2bb url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.4.1" provider: dependency: "direct main" description: @@ -897,26 +934,26 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.4.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: "41d6c5e0327d6c270b98b79bfed672928244af60e2856770f3eff697f9efe459" + sha256: "1bfcb7455fdcf15953bf18ac2817634ea5b8f7f350c7e8c9873141a3ee2c3e9c" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.4.1" recase: dependency: transitive description: @@ -937,98 +974,98 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" sentry: dependency: transitive description: name: sentry - sha256: a524a87d096799b775530176c8c082afe7aa1f10cc31ba078fecdd74e9afc923 + sha256: "576ad83415102ba2060142a6701611abc6e67a55af1d7ab339cedd3ba1b0f84c" url: "https://pub.dev" source: hosted - version: "7.17.0" + version: "8.12.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: e0f8367f8f7c74dba9f7521f71700bce6c6ee065cf342f065d4fce411b84fc7b + sha256: dc3761e8659839cc67a18432d9f12e5531affb7ff68e196dbb56846909b5dfdc url: "https://pub.dev" source: hosted - version: "7.17.0" + version: "8.12.0" shared_preferences: dependency: transitive description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" simple_gesture_detector: dependency: transitive description: @@ -1041,7 +1078,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -1054,10 +1091,10 @@ packages: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_span: dependency: transitive description: @@ -1078,34 +1115,58 @@ packages: dependency: transitive description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + url: "https://pub.dev" + source: hosted + version: "2.5.4+6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + url: "https://pub.dev" + source: hosted + version: "2.4.1+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" storage_client: dependency: transitive description: name: storage_client - sha256: bf5589d5de61a2451edb1b8960a0e673d4bb5c42ecc4dddf7c051a93789ced34 + sha256: d80d34f0aa60e5199646bc301f5750767ee37310c2ecfe8d4bbdd29351e09ab0 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.3.0" stream_channel: dependency: transitive description: @@ -1118,50 +1179,50 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" supabase: dependency: transitive description: name: supabase - sha256: f431753d2a4cb9dacd72c7378154f806c2b2cef23859bd9cee1add23821e874d + sha256: "270f63cd87a16578fee87e40cbf61062e8cdbce68d5e723e665f4651d70ddd8c" url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.6.2" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "30e966b89ee61dc9de845e2d7e1c60967b3189c410d105c6d42f09b6259f4cb6" + sha256: ca8dfe3d4b109e7338cdf7778f3ec2c660a0178006876bfac343eb39b0f3d1e3 url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.8.3" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" table_calendar: dependency: "direct main" description: name: table_calendar - sha256: "1e3521a3e6d3fc7f645a58b135ab663d458ab12504f1ea7f9b4b81d47086c478" + sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63 url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.1.3" term_glyph: dependency: transitive description: @@ -1174,66 +1235,66 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.3" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1246,50 +1307,50 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.3" uuid: dependency: "direct main" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.5.1" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.15" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.12" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1302,50 +1363,58 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.3.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.10.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1358,18 +1427,18 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: name: yet_another_json_isolate - sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069 + sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.3" sdks: - dart: ">=3.2.3 <3.4.0" - flutter: ">=3.16.6" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index b6395133..e74c6d2d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,10 +15,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.6.16+23 +version: 0.7.0+29 environment: - sdk: '>=3.0.0 <3.4.0' + sdk: '>=3.0.0 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -33,53 +33,53 @@ dependencies: sdk: flutter - collection: ^1.17.1 - intl: ^0.18.0 - get_it: ^7.6.0 - provider: ^6.0.5 - http: ^1.1.0 - uuid: ^4.3.3 - json_annotation: ^4.8.1 - logging: ^1.1.1 - path_provider: ^2.0.14 - flutter_secure_storage: ^9.0.0 - cupertino_icons: ^1.0.5 - package_info_plus: ^5.0.1 - url_launcher: ^6.1.10 - flutter_svg: ^2.0.7 - cached_network_image: ^3.2.3 - flutter_cache_manager: ^3.3.0 - envied: ^0.5.3 - - flutter_bloc: ^8.1.2 - equatable: ^2.0.5 - - percent_indicator: ^4.2.3 + collection: ^1.19.0 + intl: ^0.19.0 + get_it: ^8.0.3 + provider: ^6.1.2 + http: ^1.2.2 + uuid: ^4.5.1 + json_annotation: ^4.9.0 + logging: ^1.3.0 + path_provider: ^2.1.5 + flutter_secure_storage: ^9.2.2 + cupertino_icons: ^1.0.8 + package_info_plus: ^8.1.2 + url_launcher: ^6.3.1 + flutter_svg: ^2.0.16 + cached_network_image: ^3.4.1 + flutter_cache_manager: ^3.4.1 + envied: ^1.0.0 + + flutter_bloc: ^8.1.6 + equatable: ^2.0.7 + + percent_indicator: ^4.2.4 horizontal_picker: ^1.2.0 - table_calendar: ^3.0.9 - introduction_screen: ^3.1.8 + table_calendar: ^3.1.3 + introduction_screen: ^3.1.14 auto_size_text: ^3.0.0 - animated_flip_counter: ^0.2.6 + animated_flip_counter: ^0.3.4 - mobile_scanner: ^4.0.0 + mobile_scanner: ^6.0.2 hive: ^2.2.3 hive_flutter: ^1.1.0 - sentry_flutter: ^7.8.0 + sentry_flutter: ^8.11.2 - supabase_flutter: ^2.3.4 + supabase_flutter: ^2.8.2 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.3.3 - json_serializable: ^6.6.1 - flutter_lints: ^3.0.1 - flutter_launcher_icons: ^0.13.1 - hive_generator: ^2.0.0 - envied_generator: ^0.5.3 + build_runner: ^2.4.14 + json_serializable: ^6.7.1 + flutter_lints: ^5.0.0 + flutter_launcher_icons: ^0.14.2 + hive_generator: ^2.0.1 + envied_generator: ^1.0.0 flutter: