From 5b1dc4d24be2719c901cfd2fe8b3d31384ef2f30 Mon Sep 17 00:00:00 2001 From: amaralkaff Date: Tue, 5 Nov 2024 03:04:07 +0800 Subject: [PATCH] feat: add new bug --- .metadata | 15 - android/.gitignore | 13 - android/app/build.gradle | 43 -- android/app/proguard-rules.pro | 10 - android/app/src/debug/AndroidManifest.xml | 7 - android/app/src/main/AndroidManifest.xml | 37 - .../kotlin/com/example/test_1/MainActivity.kt | 5 - .../res/drawable-v21/launch_background.xml | 12 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../main/res/mipmap-hdpi/launcher_icon.png | Bin 2883 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../main/res/mipmap-mdpi/launcher_icon.png | Bin 1726 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xhdpi/launcher_icon.png | Bin 4105 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxhdpi/launcher_icon.png | Bin 6831 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../main/res/mipmap-xxxhdpi/launcher_icon.png | Bin 9954 -> 0 bytes .../app/src/main/res/values-night/styles.xml | 18 - android/app/src/main/res/values/styles.xml | 18 - android/app/src/profile/AndroidManifest.xml | 7 - android/build.gradle | 43 -- android/gradle.properties | 26 - .../gradle/wrapper/gradle-wrapper.properties | 5 - android/settings.gradle | 25 - lib/services/auth_service.dart | 142 +++- lib/services/situp_service.dart | 65 ++ lib/utils/sit_up_utils.dart | 81 ++- lib/views/auth/login_screen.dart | 74 +- lib/views/camera_view.dart | 435 ++++++----- lib/views/detector_view.dart | 2 +- lib/views/pose_detection_view.dart | 1 + lib/views/sit_up_detector_view.dart | 89 ++- lib/views/splash_screen.dart | 684 ++++++++++++------ lib/widgets/progress_tracker.dart | 414 ++++++++--- lib/widgets/situp_completion_dialog.dart | 193 +++++ lib/widgets/workout_completion_dialog.dart | 142 ++-- pubspec.lock | 16 + pubspec.yaml | 3 +- 40 files changed, 1663 insertions(+), 974 deletions(-) delete mode 100644 android/.gitignore delete mode 100644 android/app/build.gradle delete mode 100644 android/app/proguard-rules.pro delete mode 100644 android/app/src/debug/AndroidManifest.xml delete mode 100644 android/app/src/main/AndroidManifest.xml delete mode 100644 android/app/src/main/kotlin/com/example/test_1/MainActivity.kt delete mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml delete mode 100644 android/app/src/main/res/drawable/launch_background.xml delete mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-hdpi/launcher_icon.png delete mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-mdpi/launcher_icon.png delete mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xhdpi/launcher_icon.png delete mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png delete mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png delete mode 100644 android/app/src/main/res/values-night/styles.xml delete mode 100644 android/app/src/main/res/values/styles.xml delete mode 100644 android/app/src/profile/AndroidManifest.xml delete mode 100644 android/build.gradle delete mode 100644 android/gradle.properties delete mode 100644 android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 android/settings.gradle create mode 100644 lib/services/situp_service.dart create mode 100644 lib/widgets/situp_completion_dialog.dart diff --git a/.metadata b/.metadata index c2aa44b..706ff77 100644 --- a/.metadata +++ b/.metadata @@ -18,21 +18,6 @@ migration: - platform: android create_revision: 603104015dd692ea3403755b55d07813d5cf8965 base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: ios - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: linux - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: macos - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: web - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: windows - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 # User provided section diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 55afd91..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index 6345168..0000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -android { - namespace "com.workout.ai" - compileSdk 35 // Updated to support latest androidx dependencies - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 // Updated for Gradle 8.0 compatibility - targetCompatibility JavaVersion.VERSION_11 // Updated for Gradle 8.0 compatibility - } - - kotlinOptions { - jvmTarget = '11' // Updated for Gradle 8.0 compatibility - } - - defaultConfig { - applicationId "com.workout.ai" - minSdk 23 - targetSdk 35 // Updated to match compileSdk - versionCode flutter.versionCode - versionName flutter.versionName - multiDexEnabled true - } - - buildTypes { - release { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.debug - debuggable false - } - } -} - -flutter { - source '../..' -} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro deleted file mode 100644 index 9fd4c1a..0000000 --- a/android/app/proguard-rules.pro +++ /dev/null @@ -1,10 +0,0 @@ -# Keep MLKit Vision Text Recognition classes --keep class com.google.mlkit.vision.text.** { *; } --keep class com.google.mlkit.vision.text.chinese.** { *; } --keep class com.google.mlkit.vision.text.devanagari.** { *; } --keep class com.google.mlkit.vision.text.japanese.** { *; } --keep class com.google.mlkit.vision.text.korean.** { *; } --dontwarn com.google.mlkit.vision.text.chinese.** --dontwarn com.google.mlkit.vision.text.devanagari.** --dontwarn com.google.mlkit.vision.text.japanese.** --dontwarn com.google.mlkit.vision.text.korean.** diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f698..0000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 1dac6bb..0000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/test_1/MainActivity.kt b/android/app/src/main/kotlin/com/example/test_1/MainActivity.kt deleted file mode 100644 index 2e1d38d..0000000 --- a/android/app/src/main/kotlin/com/example/test_1/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.workout.ai - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f..0000000 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png deleted file mode 100644 index 888a78ec9b448e5ed0ce2c3b771ac1f774faf83f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2883 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 diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png deleted file mode 100644 index cb277a86ee91f67fa611ba8a675025a7ff96c9ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4105 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 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png deleted file mode 100644 index 8326c4f92856bbc3ebd82e5fbe99f21c3eff3496..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6831 zcmcJUSuzLA)h>1y~XXrP*owvIfKs+A6O$F!we_j0C*w}D;0P0a1rj895K1Mxk3^WQ~isJH?q*C>!S+unpi(SzaTjUg(sO`zOd5L($?_PB}hJd z_AE}sw)0iuoo3Ad@yrh@ditUl+S;F2Cniovi6scX0JbLOG)2-!z>hO(YP_}+nI-Q7 zE)M3A7ssoSdgzcua5(Y({rxZ+jXq~)W*(Q3k!im@8xgG^8`Fnxwzs!CHr5mu4?di% z_p`oTQJO9>h$Pf#FAzmA!LF%3iP*^hTWa%l0|V0c|I`GA3VlQmB3K&*%FD}lz{>UpYg!lBH}Oh>eZ{C00|Z^An=MoKM%VXe*OTElbqq}p1slHuXu zrt|Z2C>J4hjESk2_u+yx^B97Xgv25~@*%(LxEVANg^TBCp!{zuEPgrheE-ib^!STH zpr;BZ=kC9q(VOO`rt>u2bP@m6b5G-#xVX4{-+?dB*#UeAy15;70#5^7-3})yIYpSY zjm`G{?wh3cTzlAM8o0lfXay&S7%cZ<7yoREA3)J=h$T``DV9+~;m zPQ=pEl51j6`Klp7)HetqY*Eicvo&vPW>!o>LE$mG3JgCcAKD@Z2>AN?KDcZTr-jP6 z&!Id%v@ei)&pV;M(Z0FF*d7q$?7CsM9&mqm9J$JlA8qM#j=_YnH6vm>3Fp9sd+#;; zt*jPDRqOlu)VzY&_W53XKCy#Bp}B@m`uh43t$WpK?CAngq`s;O?R(Z%GxQ$uZ#M@8 zVkz0$kIuG7E*o1~R)rKoFlf>1AFx+GV_!gcR<@Rw@?OKK0@JE_aI>05g!j%bW(#+B zcWUY(Yt$ZGZX(*v6q2sM1B39;Q%F#31+xe~*H;=D7$_pM`fy)2u?WQ|6G-Dwe3q7# zRWLO%x!s=B;?TQ4Xc$if)~z^4`6;7!b}sM_R2pWObbNq7!+mau03+WU0bi@Q-$BbgQ$VZgd7Z~a29+8rg z3gS{nkBS$*2SZl5hlYmajaJr){l=9p`B_+4aw^Np+TaK5P;xG+@4g-B>FKagY40^~ zBqr0UD=CTSJ>AoXviG!nk73}U-{FA8wS)Nvb3VPyMuO~JQ^oGAh3b}S#XsL

A%- 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 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png deleted file mode 100644 index ed50c345110f4a16d27b351cb5f704a089c98c40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9954 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 - - - - - - diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef88..0000000 --- a/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index b49116c..0000000 --- a/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index fcd3493..0000000 --- a/android/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.0.0' // Updated to support Gradle 8.0 - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" - afterEvaluate { - if (project.hasProperty("android")) { - android { - compileSdkVersion 35 // Updated to support newer AndroidX dependencies - defaultConfig { - minSdkVersion 23 - targetSdkVersion 35 // Matched with compileSdkVersion - } - buildTypes { - release { - minifyEnabled true - shrinkResources false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - } - } - } -} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index b7e75ff..0000000 --- a/android/gradle.properties +++ /dev/null @@ -1,26 +0,0 @@ -# Gradle performance optimizations -org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseG1GC -org.gradle.daemon=true -org.gradle.parallel=true -org.gradle.configureondemand=true -org.gradle.caching=true - -# Android build configurations -android.useAndroidX=true -android.enableJetifier=true -android.suppressUnsupportedCompileSdk=35 -android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=true -android.nonFinalResIds=true - -# Keep rules for R8 --keep class com.google.mlkit.vision.text.** { *; } --keep class com.google.android.gms.vision.** { *; } --keep class com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions { *; } --keep class com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions$Builder { *; } --keep class com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions { *; } --keep class com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions$Builder { *; } --keep class com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions { *; } --keep class com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions$Builder { *; } --keep class com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions { *; } --keep class com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions$Builder { *; } \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 41681a7..0000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 460bb43..0000000 --- a/android/settings.gradle +++ /dev/null @@ -1,25 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - // Use a version of AGP that supports compileSdkVersion 34 - id "com.android.application" version "8.1.0" apply false -} - -include ":app" diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 829af1d..70e3b88 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -63,7 +63,7 @@ class AuthService { try { final prefs = await SharedPreferences.getInstance(); _token = prefs.getString(_tokenKey); - + if (_token != null) { final userStr = prefs.getString(_userKey); return userStr != null; @@ -78,7 +78,7 @@ class AuthService { Future> register(UserInfo userInfo) async { try { log('Attempting registration for email: ${userInfo.email}'); - + final response = await _api.post( 'userInfo', { @@ -112,7 +112,7 @@ class AuthService { Future> login(String email, String password) async { try { log('Attempting login for email: $email'); - + final response = await _api.post( 'login', {'email': email, 'password': password}, @@ -126,28 +126,140 @@ class AuthService { log('Token received: $token'); await setToken(token); - final userInfo = UserInfo( - username: response['user']?['username'] ?? '', - email: email, - password: password, - height: response['user']?['height'] ?? 0, - weight: response['user']?['weight'] ?? 0, - ); + // Get complete user info after successful login + try { + final userResponse = await _api.get('userinfo'); + final userData = userResponse['user'] as Map; + final userInfo = UserInfo.fromJson({ + ...userData, + 'email': email, + 'password': password, // Store password temporarily for session + }); + + await saveUserInfo(userInfo); + + return { + 'statusCode': 200, + 'message': response['message'] ?? 'Login successful', + 'token': token, + 'user': userInfo, + }; + } catch (userError) { + log('Error fetching detailed user info: $userError'); + // Fallback to basic user info from login response + final userInfo = UserInfo( + username: response['user']?['username'] ?? '', + email: email, + password: password, + height: response['user']?['height'] ?? 0, + weight: response['user']?['weight'] ?? 0, + ); + + await saveUserInfo(userInfo); + + return { + 'statusCode': 200, + 'message': response['message'] ?? 'Login successful', + 'token': token, + 'user': userInfo, + }; + } + } + + throw Exception(response['message'] ?? 'Login failed'); + } catch (e) { + log('Login error: $e'); + await clearToken(); + rethrow; + } + } + + Future getUserInfo() async { + try { + // First try to get from local storage + final savedInfo = await loadUserInfo(); + if (savedInfo != null) { + // Verify if local data is still valid + try { + final response = await _api.get('userinfo'); + if (response['statusCode'] == 200) { + final userData = response['user'] as Map; + // Preserve sensitive data that might not come from API + final updatedInfo = UserInfo.fromJson({ + ...userData, + 'email': savedInfo.email, + 'password': savedInfo.password, + }); + await saveUserInfo(updatedInfo); + return updatedInfo; + } + } catch (e) { + log('Error refreshing user info: $e'); + // Return saved info if API call fails + return savedInfo; + } + } + // If no saved info, get from API + final response = await _api.get('userinfo'); + if (response['statusCode'] == 200) { + final userData = response['user'] as Map; + final userInfo = UserInfo.fromJson(userData); await saveUserInfo(userInfo); + return userInfo; + } + + throw Exception(response['message'] ?? 'Failed to fetch user info'); + } catch (e) { + log('Get user info error: $e'); + rethrow; + } + } + + Future> updateUserInfo(UserInfo userInfo) async { + try { + final response = await _api.post( + 'updateuser', + userInfo.toJson(), + requiresAuth: true, + ); + if (response['statusCode'] == 200) { + await saveUserInfo(userInfo); return { 'statusCode': 200, - 'message': response['message'] ?? 'Login successful', - 'token': token, + 'message': 'User info updated successfully', 'user': userInfo, }; } - throw Exception(response['message'] ?? 'Login failed'); + throw Exception(response['message'] ?? 'Failed to update user info'); } catch (e) { - log('Login error: $e'); - await clearToken(); + log('Update user info error: $e'); + rethrow; + } + } + + Future validateToken() async { + if (_token == null) return false; + + try { + final response = await _api.get('validate-token'); + return response['statusCode'] == 200; + } catch (e) { + log('Token validation error: $e'); + return false; + } + } + + Future refreshToken() async { + try { + final response = await _api.post('refresh-token', {}, requiresAuth: true); + if (response['token'] != null) { + await setToken(response['token']); + } + } catch (e) { + log('Token refresh error: $e'); rethrow; } } diff --git a/lib/services/situp_service.dart b/lib/services/situp_service.dart new file mode 100644 index 0000000..ca15b2f --- /dev/null +++ b/lib/services/situp_service.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'package:http/http.dart' as http; +import 'package:workout_ai/services/auth_service.dart'; + +class SitUpService { + static const String baseUrl = 'https://backend-workout-ai.vercel.app/api'; + + Future> submitSitUps({ + required int sitUps, + }) async { + try { + final token = AuthService.getToken(); + if (token == null) { + throw Exception('No authentication token found'); + } + + log('Submitting situps with token: $token'); + final response = await http.post( + Uri.parse('$baseUrl/situp'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + 'Cookie': 'token=$token', + }, + body: jsonEncode({ + 'sitUps': sitUps, + }), + ); + + final data = jsonDecode(response.body); + log('Situp submission response: $data'); + + if (response.statusCode == 200) { + return { + 'statusCode': response.statusCode, + 'Kalori_yang_terbakar_per_sit_up': + data['Kalori_yang_terbakar_per_sit_up'], + 'Total_kalori_yang_terbakar': data['Total_kalori_yang_terbakar'], + }; + } else { + log('Situp submission failed with status code: ${response.statusCode}'); + return { + 'statusCode': response.statusCode, + 'Kalori_yang_terbakar_per_sit_up': null, + 'Total_kalori_yang_terbakar': null, + }; + } + } catch (e) { + log('Situp submission error: $e'); + throw Exception('Failed to submit situps: $e'); + } + } + + Future testSubmitSitUps() async { + try { + final result = await submitSitUps(sitUps: 12); + log('Test Submit SitUps Result:'); + log(json.encode(result)); + } catch (e) { + log('Test Submit SitUps Error: $e'); + throw Exception('Failed to submit situps: $e'); + } + } +} diff --git a/lib/utils/sit_up_utils.dart b/lib/utils/sit_up_utils.dart index 765db16..f88397d 100644 --- a/lib/utils/sit_up_utils.dart +++ b/lib/utils/sit_up_utils.dart @@ -1,7 +1,7 @@ -// lib/utils/sit_up_utils.dart +import 'dart:math'; -import 'dart:math' as math; import 'package:google_mlkit_pose_detection/google_mlkit_pose_detection.dart'; +import 'package:vector_math/vector_math.dart' show Vector2, degrees; import '../models/sit_up_model.dart'; double calculateTorsoAngle( @@ -9,42 +9,49 @@ double calculateTorsoAngle( PoseLandmark hip, PoseLandmark knee, ) { - try { - final radians = math.atan2( - knee.y - hip.y, - knee.x - hip.x, - ) - - math.atan2( - shoulder.y - hip.y, - shoulder.x - hip.x, - ); - - double degrees = radians * 180.0 / math.pi; - degrees = degrees.abs(); - if (degrees > 180.0) { - degrees = 360.0 - degrees; - } - return degrees; - } catch (e) { - print('Error calculating torso angle: $e'); - return 0.0; + // Calculate vectors + final hipToShoulder = Vector2( + shoulder.x - hip.x, + shoulder.y - hip.y, + ); + + final hipToKnee = Vector2( + knee.x - hip.x, + knee.y - hip.y, + ); + + // Calculate angle between vectors + double dot = hipToShoulder.dot(hipToKnee); + double norm = hipToShoulder.length * hipToKnee.length; + + double angle = degrees(acos(dot / norm)); + + // Adjust angle based on relative positions + if (shoulder.y > hip.y) { + angle = 360 - angle; } + + return angle; } -SitUpState? isSitUp(double torsoAngle, SitUpState current) { - // More lenient angle thresholds - const minLyingAngle = 140.0; // Nearly flat - const maxRaisedAngle = 80.0; // Upper body raised - - try { - if (current == SitUpState.neutral && torsoAngle > minLyingAngle) { - return SitUpState.init; // Person is lying down - } else if (current == SitUpState.init && torsoAngle < maxRaisedAngle) { - return SitUpState.complete; // Person has lifted up - } - return null; - } catch (e) { - print('Error determining sit-up state: $e'); - return null; +SitUpState? isSitUp(double angle, SitUpState currentState) { + const double upThreshold = 75.0; // Angle when torso is upright + const double downThreshold = 30.0; // Angle when lying down + + switch (currentState) { + case SitUpState.neutral: + if (angle <= downThreshold) { + return SitUpState.init; + } + break; + case SitUpState.init: + if (angle >= upThreshold) { + return SitUpState.complete; + } + break; + case SitUpState.complete: + return SitUpState.neutral; } -} \ No newline at end of file + + return null; +} diff --git a/lib/views/auth/login_screen.dart b/lib/views/auth/login_screen.dart index 69630a4..c612408 100644 --- a/lib/views/auth/login_screen.dart +++ b/lib/views/auth/login_screen.dart @@ -113,7 +113,7 @@ class _LoginScreenState extends State { backgroundColor: Colors.green, ), ); - + setState(() { _isLoginView = true; _emailController.clear(); @@ -125,7 +125,7 @@ class _LoginScreenState extends State { } } catch (e) { if (!mounted) return; - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Registration failed: ${e.toString()}'), @@ -178,16 +178,15 @@ class _LoginScreenState extends State { ), ), Text( - _isLoginView - ? 'Enter your email and password to sign in' - : 'Create your account, it takes less than a minute.', + _isLoginView + ? 'Enter your email and password to sign in' + : 'Create your account, it takes less than a minute.', style: TextStyle( color: Colors.grey[600], fontSize: 16, ), ), const SizedBox(height: 32), - if (!_isLoginView) ...[ TextFormField( controller: _usernameController, @@ -273,7 +272,6 @@ class _LoginScreenState extends State { ], ), ], - const SizedBox(height: 16), TextFormField( controller: _emailController, @@ -313,7 +311,9 @@ class _LoginScreenState extends State { contentPadding: const EdgeInsets.all(16), suffixIcon: IconButton( icon: Icon( - _obscurePassword ? Icons.visibility : Icons.visibility_off, + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, color: Colors.grey[600], ), onPressed: () { @@ -330,7 +330,6 @@ class _LoginScreenState extends State { return null; }, ), - if (_isLoginView) ...[ Align( alignment: Alignment.centerRight, @@ -347,18 +346,19 @@ class _LoginScreenState extends State { ), ), ], - const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: _isLoading - ? null - : _isLoginView ? _login : _register, + onPressed: _isLoading + ? null + : _isLoginView + ? _login + : _register, style: ElevatedButton.styleFrom( - backgroundColor: _isLoginView - ? const Color(0xFFE8FE54) - : Colors.black, + backgroundColor: _isLoginView + ? const Color(0xFFE8FE54) + : Colors.black, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -366,24 +366,25 @@ class _LoginScreenState extends State { elevation: 0, ), child: _isLoading - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - _isLoginView ? Colors.black : Colors.white, + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + _isLoginView ? Colors.black : Colors.white, + ), + ), + ) + : Text( + _isLoginView ? 'Log in' : 'Create an Account', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: + _isLoginView ? Colors.black : Colors.white, ), ), - ) - : Text( - _isLoginView ? 'Log in' : 'Create an Account', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: _isLoginView ? Colors.black : Colors.white, - ), - ), ), ), const SizedBox(height: 24), @@ -391,13 +392,14 @@ class _LoginScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - _isLoginView - ? 'Don\'t have an account?' - : 'Already have an account?', + _isLoginView + ? 'Don\'t have an account?' + : 'Already have an account?', style: TextStyle(color: Colors.grey[600]), ), TextButton( - onPressed: () => setState(() => _isLoginView = !_isLoginView), + onPressed: () => + setState(() => _isLoginView = !_isLoginView), child: Text( _isLoginView ? 'Sign up' : 'Log in', style: const TextStyle( diff --git a/lib/views/camera_view.dart b/lib/views/camera_view.dart index bba3ff0..41d9353 100644 --- a/lib/views/camera_view.dart +++ b/lib/views/camera_view.dart @@ -11,6 +11,7 @@ import 'package:workout_ai/models/push_up_model.dart'; import 'package:workout_ai/models/sit_up_model.dart'; import 'package:workout_ai/painters/pose_painter.dart'; import 'package:workout_ai/services/pushup_service.dart'; +import 'package:workout_ai/utils/sit_up_utils.dart'; import 'package:workout_ai/utils/utils.dart' as utils; import 'package:workout_ai/widgets/workout_completion_dialog.dart'; import 'dart:developer' as developer; @@ -29,33 +30,33 @@ class ExerciseStatsWidget extends StatelessWidget { if (exerciseType == 'Push-up Counter') { try { final pushupService = PushupService(); - + // Test submission first await pushupService.testSubmitPushups(); - + final result = await pushupService.submitPushups( pushUps: reps, ); if (context.mounted) { context.read().updateStats( - exerciseType: exerciseType, - repCount: reps, - caloriesPerRep: double.tryParse( - result['Kalori_yang_terbakar_per_push_up'] ?? '0'), - totalCalories: double.tryParse( - result['Total_kalori_yang_terbakar'] ?? '0'), - ); + exerciseType: exerciseType, + repCount: reps, + caloriesPerRep: double.tryParse( + result['Kalori_yang_terbakar_per_push_up'] ?? '0'), + totalCalories: double.tryParse( + result['Total_kalori_yang_terbakar'] ?? '0'), + ); context.read().markExerciseComplete(exerciseType); - + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Workout submitted successfully!'), backgroundColor: Colors.green, ), ); - + Navigator.of(context).popUntil((route) => route.isFirst); } } catch (e) { @@ -209,27 +210,65 @@ class _CameraViewState extends State { if (!_isTimerRunning) { _startTimer(); } - final bloc = BlocProvider.of(context); - for (final pose in widget.posePainter!.poses) { - PoseLandmark getPoseLandmark(PoseLandmarkType type1) { - final PoseLandmark joint1 = pose.landmarks[type1]!; - return joint1; - } - p1 = getPoseLandmark(PoseLandmarkType.rightShoulder); - p2 = getPoseLandmark(PoseLandmarkType.rightElbow); - p3 = getPoseLandmark(PoseLandmarkType.rightWrist); + if (widget.exerciseTitle == 'Push-up Counter') { + _handlePushUpDetection(); + } else { + _handleSitUpDetection(); + } + } + } + + void _handlePushUpDetection() { + final bloc = BlocProvider.of(context); + for (final pose in widget.posePainter!.poses) { + PoseLandmark getPoseLandmark(PoseLandmarkType type1) { + final PoseLandmark joint1 = pose.landmarks[type1]!; + return joint1; } - if (p1 != null && p2 != null && p3 != null) { - final rtaAngle = utils.angle(p1!, p2!, p3!); - final rta = utils.isPushUp(rtaAngle, bloc.state); - developer.log("Angle: ${rtaAngle.toStringAsFixed(2)}"); - if (rta != null) { - if (rta == PushUpState.init) { - bloc.setPushUpState(rta); - } else if (rta == PushUpState.complete) { + + p1 = getPoseLandmark(PoseLandmarkType.rightShoulder); + p2 = getPoseLandmark(PoseLandmarkType.rightElbow); + p3 = getPoseLandmark(PoseLandmarkType.rightWrist); + } + if (p1 != null && p2 != null && p3 != null) { + final rtaAngle = utils.angle(p1!, p2!, p3!); + final rta = utils.isPushUp(rtaAngle, bloc.state); + developer.log("Angle: ${rtaAngle.toStringAsFixed(2)}"); + if (rta != null) { + if (rta == PushUpState.init) { + bloc.setPushUpState(rta); + } else if (rta == PushUpState.complete) { + bloc.incrementCounter(); + bloc.setPushUpState(PushUpState.neutral); + } + } + } + } + + void _handleSitUpDetection() { + final bloc = BlocProvider.of(context); + for (final pose in widget.posePainter!.poses) { + if (pose.landmarks.isEmpty) continue; + + final rightShoulder = pose.landmarks[PoseLandmarkType.rightShoulder]; + final rightHip = pose.landmarks[PoseLandmarkType.rightHip]; + final rightKnee = pose.landmarks[PoseLandmarkType.rightKnee]; + + if (rightShoulder != null && rightHip != null && rightKnee != null) { + final torsoAngle = calculateTorsoAngle( + rightShoulder, + rightHip, + rightKnee, + ); + + final sitUpState = isSitUp(torsoAngle, bloc.state); + if (sitUpState != null) { + if (sitUpState == SitUpState.init) { + bloc.setSitUpState(sitUpState); + } else if (sitUpState == SitUpState.complete) { bloc.incrementCounter(); - bloc.setPushUpState(PushUpState.neutral); + bloc.setSitUpState(SitUpState.neutral); } } } @@ -248,197 +287,197 @@ class _CameraViewState extends State { return Scaffold(body: _liveFeedBody()); } -Widget _liveFeedBody() { - if (_cameras.isEmpty) return Container(); - if (_controller == null) return Container(); - if (_controller?.value.isInitialized == false) return Container(); - - return Container( - color: Colors.black, - child: Stack( - fit: StackFit.expand, - children: [ - RepaintBoundary( - child: Center( - child: _changingCameraLens - ? const Center( - child: Text('Changing camera lens'), - ) - : CameraPreview( - _controller!, - child: RepaintBoundary(child: widget.customPaint), - ), + Widget _liveFeedBody() { + if (_cameras.isEmpty) return Container(); + if (_controller == null) return Container(); + if (_controller?.value.isInitialized == false) return Container(); + + return Container( + color: Colors.black, + child: Stack( + fit: StackFit.expand, + children: [ + RepaintBoundary( + child: Center( + child: _changingCameraLens + ? const Center( + child: Text('Changing camera lens'), + ) + : CameraPreview( + _controller!, + child: RepaintBoundary(child: widget.customPaint), + ), + ), ), - ), - Positioned( - top: 50, - right: 20, - child: RepaintBoundary( - child: Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.fitness_center, color: Colors.white), - const SizedBox(width: 8), - Text( - 'Reps: ${widget.exerciseTitle == 'Push-up Counter' - ? context.select((PushUpCounter c) => c.counter) - : context.select((SitUpCounter c) => c.counter)}', - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, + Positioned( + top: 50, + right: 20, + child: RepaintBoundary( + child: Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.fitness_center, color: Colors.white), + const SizedBox(width: 8), + Text( + 'Reps: ${widget.exerciseTitle == 'Push-up Counter' ? context.select((PushUpCounter c) => c.counter) : context.select((SitUpCounter c) => c.counter)}', + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), ), - ), - ], - ), - const SizedBox(height: 16), - SizedBox( - width: 120, - child: ElevatedButton( - onPressed: () { - final count = widget.exerciseTitle == 'Push-up Counter' - ? context.read().counter - : context.read().counter; - - if (count == 0) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Complete at least one repetition'), - backgroundColor: Colors.orange, + ], + ), + const SizedBox(height: 16), + SizedBox( + width: 120, + child: ElevatedButton( + onPressed: () { + final count = + widget.exerciseTitle == 'Push-up Counter' + ? context.read().counter + : context.read().counter; + + if (count == 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Complete at least one repetition'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Center( + child: WorkoutCompletionDialog( + exerciseType: widget.exerciseTitle, + reps: count, + ), ), ); - return; - } - - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Center( - child: WorkoutCompletionDialog( - exerciseType: widget.exerciseTitle, - reps: count, - ), + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), ), + child: const Text('Done'), ), - child: const Text('Done'), ), - ), - ], + ], + ), ), ), ), - ), - if (_isTimerRunning) - Positioned( - top: 50, - left: 20, - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _formatTime(_seconds), - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, + if (_isTimerRunning) + Positioned( + top: 50, + left: 20, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _formatTime(_seconds), + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - _backButton(), - _switchLiveCameraToggle(), - _detectionViewModeToggle(), - ], - ), - ); -} + _backButton(), + _switchLiveCameraToggle(), + _detectionViewModeToggle(), + ], + ), + ); + } Widget _backButton() => Positioned( - top: 40, - left: 8, - child: SizedBox( - height: 50.0, - width: 50.0, - child: FloatingActionButton( - heroTag: Object(), - onPressed: () { - if (widget.exerciseTitle == 'Push-up Counter') { - context.read().resetCounter(); - } else { - context.read().resetCounter(); - } - Navigator.of(context).pop(); - }, - backgroundColor: Colors.black54, - child: const Icon( - Icons.arrow_back_ios_outlined, - size: 20, + top: 40, + left: 8, + child: SizedBox( + height: 50.0, + width: 50.0, + child: FloatingActionButton( + heroTag: Object(), + onPressed: () { + if (widget.exerciseTitle == 'Push-up Counter') { + context.read().resetCounter(); + } else { + context.read().resetCounter(); + } + Navigator.of(context).pop(); + }, + backgroundColor: Colors.black54, + child: const Icon( + Icons.arrow_back_ios_outlined, + size: 20, + ), + ), ), - ), - ), - ); + ); Widget _detectionViewModeToggle() => Positioned( - bottom: 8, - left: 8, - child: SizedBox( - height: 50.0, - width: 50.0, - child: FloatingActionButton( - heroTag: Object(), - onPressed: widget.onDetectorViewModeChanged, - backgroundColor: Colors.black54, - child: const Icon( - Icons.photo_library_outlined, - size: 25, + bottom: 8, + left: 8, + child: SizedBox( + height: 50.0, + width: 50.0, + child: FloatingActionButton( + heroTag: Object(), + onPressed: widget.onDetectorViewModeChanged, + backgroundColor: Colors.black54, + child: const Icon( + Icons.photo_library_outlined, + size: 25, + ), + ), ), - ), - ), - ); + ); Widget _switchLiveCameraToggle() => Positioned( - bottom: 8, - right: 8, - child: SizedBox( - height: 50.0, - width: 50.0, - child: FloatingActionButton( - heroTag: Object(), - onPressed: _switchLiveCamera, - backgroundColor: Colors.black54, - child: Icon( - Platform.isIOS - ? Icons.flip_camera_ios_outlined - : Icons.flip_camera_android_outlined, - size: 25, + bottom: 8, + right: 8, + child: SizedBox( + height: 50.0, + width: 50.0, + child: FloatingActionButton( + heroTag: Object(), + onPressed: _switchLiveCamera, + backgroundColor: Colors.black54, + child: Icon( + Platform.isIOS + ? Icons.flip_camera_ios_outlined + : Icons.flip_camera_android_outlined, + size: 25, + ), + ), ), - ), - ), - ); + ); Future _startLiveFeed() async { final camera = _cameras[_cameraIndex]; @@ -538,4 +577,4 @@ Widget _liveFeedBody() { ), ); } -} \ No newline at end of file +} diff --git a/lib/views/detector_view.dart b/lib/views/detector_view.dart index fe2a29e..24e8a29 100644 --- a/lib/views/detector_view.dart +++ b/lib/views/detector_view.dart @@ -20,7 +20,7 @@ class DetectorView extends StatefulWidget { this.initialCameraLensDirection = CameraLensDirection.back, this.onCameraFeedReady, this.onDetectorViewModeChanged, - this.onCameraLensDirectionChanged, + this.onCameraLensDirectionChanged, required String exerciseTitle, }); diff --git a/lib/views/pose_detection_view.dart b/lib/views/pose_detection_view.dart index f952dc9..907e21c 100644 --- a/lib/views/pose_detection_view.dart +++ b/lib/views/pose_detection_view.dart @@ -43,6 +43,7 @@ class _PoseDetectorViewState extends State { posePainter: _posePainter, initialCameraLensDirection: _cameraLensDirection, onCameraLensDirectionChanged: (value) => _cameraLensDirection = value, + exerciseTitle: PoseDetectorView.exerciseTitle, ); } diff --git a/lib/views/sit_up_detector_view.dart b/lib/views/sit_up_detector_view.dart index 8a2d499..55570fc 100644 --- a/lib/views/sit_up_detector_view.dart +++ b/lib/views/sit_up_detector_view.dart @@ -1,18 +1,19 @@ // lib/views/sit_up_detector_view.dart - import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_mlkit_pose_detection/google_mlkit_pose_detection.dart'; import '../painters/pose_painter.dart'; import '../models/sit_up_model.dart'; import '../models/exercise_timer_model.dart'; import '../utils/sit_up_utils.dart'; +import '../widgets/situp_completion_dialog.dart'; import 'detector_view.dart'; class SitUpDetectorView extends StatefulWidget { - static const String title = 'Sit-up Counter'; // Add this constant - + static const String title = 'Sit-up Counter'; + const SitUpDetectorView({super.key}); @override @@ -21,7 +22,9 @@ class SitUpDetectorView extends StatefulWidget { class _SitUpDetectorViewState extends State { final PoseDetector _poseDetector = PoseDetector( - options: PoseDetectorOptions(), + options: PoseDetectorOptions( + mode: PoseDetectionMode.stream, + ), ); bool _canProcess = true; bool _isBusy = false; @@ -37,6 +40,29 @@ class _SitUpDetectorViewState extends State { super.dispose(); } + void _showCompletionDialog(BuildContext context, int reps) { + if (reps == 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Complete at least one sit-up'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Center( + child: SitUpCompletionDialog( + exerciseType: 'Sit-up', + reps: reps, + ), + ), + ); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -44,7 +70,8 @@ class _SitUpDetectorViewState extends State { child: BlocListener( listener: (context, state) { if (state.status == TimerStatus.completed) { - Navigator.of(context).popUntil((route) => route.isFirst); + final reps = context.read().counter; + _showCompletionDialog(context, reps); } }, child: DetectorView( @@ -55,26 +82,24 @@ class _SitUpDetectorViewState extends State { posePainter: _posePainter, initialCameraLensDirection: _cameraLensDirection, onCameraLensDirectionChanged: (value) => _cameraLensDirection = value, + exerciseTitle: SitUpDetectorView + .title, // Using the existing exerciseTitle parameter ), ), ); } Future _processImage(InputImage inputImage) async { - if (!_canProcess) return; - if (_isBusy) return; - + if (!_canProcess || _isBusy) return; + _isBusy = true; - setState(() { - _text = ''; - }); + setState(() => _text = ''); try { final poses = await _poseDetector.processImage(inputImage); - - if (inputImage.metadata?.size != null && + + if (inputImage.metadata?.size != null && inputImage.metadata?.rotation != null) { - final painter = PosePainter( poses, inputImage.metadata!.size, @@ -82,23 +107,41 @@ class _SitUpDetectorViewState extends State { _cameraLensDirection, ); - // Only process if we have valid poses if (poses.isNotEmpty) { final pose = poses.first; final rightShoulder = pose.landmarks[PoseLandmarkType.rightShoulder]; final rightHip = pose.landmarks[PoseLandmarkType.rightHip]; final rightKnee = pose.landmarks[PoseLandmarkType.rightKnee]; - if (rightShoulder != null && rightHip != null && rightKnee != null) { - final torsoAngle = calculateTorsoAngle( + // Additional landmarks for better accuracy + final leftShoulder = pose.landmarks[PoseLandmarkType.leftShoulder]; + final leftHip = pose.landmarks[PoseLandmarkType.leftHip]; + final leftKnee = pose.landmarks[PoseLandmarkType.leftKnee]; + + if (rightShoulder != null && + rightHip != null && + rightKnee != null && + leftShoulder != null && + leftHip != null && + leftKnee != null) { + // Calculate average angles from both sides for better accuracy + final rightTorsoAngle = calculateTorsoAngle( rightShoulder, rightHip, rightKnee, ); + final leftTorsoAngle = calculateTorsoAngle( + leftShoulder, + leftHip, + leftKnee, + ); + + final averageAngle = (rightTorsoAngle + leftTorsoAngle) / 2; + if (mounted) { final bloc = context.read(); - final sitUpState = isSitUp(torsoAngle, bloc.state); + final sitUpState = isSitUp(averageAngle, bloc.state); if (sitUpState != null) { if (sitUpState == SitUpState.init) { @@ -106,6 +149,9 @@ class _SitUpDetectorViewState extends State { } else if (sitUpState == SitUpState.complete) { bloc.incrementCounter(); bloc.setSitUpState(SitUpState.neutral); + + // Provide haptic feedback for completed rep + HapticFeedback.mediumImpact(); } } } @@ -120,12 +166,9 @@ class _SitUpDetectorViewState extends State { } } } catch (e) { - // Handle errors gracefully debugPrint('Error in pose detection: $e'); if (mounted) { - setState(() { - _text = 'Error processing image'; - }); + setState(() => _text = 'Error processing image'); } } finally { _isBusy = false; @@ -134,4 +177,4 @@ class _SitUpDetectorViewState extends State { } } } -} \ No newline at end of file +} diff --git a/lib/views/splash_screen.dart b/lib/views/splash_screen.dart index 5356829..693bb8c 100644 --- a/lib/views/splash_screen.dart +++ b/lib/views/splash_screen.dart @@ -20,30 +20,56 @@ class SplashScreen extends StatefulWidget { State createState() => _SplashScreenState(); } -class _SplashScreenState extends State { +class _SplashScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; final WorkoutInfoService _workoutInfoService = WorkoutInfoService(); final AuthService _authService = AuthService(); List _workoutInfo = []; bool _isLoading = true; String? _error; + DateTime? _lastLoadTime; @override void initState() { super.initState(); + _tabController = TabController(length: 2, vsync: this); + _tabController.addListener(_handleTabChange); _loadData(); _setPortraitOrientation(); } - Future _setPortraitOrientation() async { - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); + @override + void dispose() { + _tabController.removeListener(_handleTabChange); + _tabController.dispose(); + _setPortraitOrientation(); + super.dispose(); } - Future loadData() => _loadData(); + // Add tab change handler + void _handleTabChange() { + if (!_tabController.indexIsChanging) return; - Future _loadData() async { + // If switching to progress tab + if (_tabController.index == 1) { + // Check if we need to refresh + if (_shouldRefreshData()) { + _loadWorkoutInfo(); + } + } + } + + bool _shouldRefreshData() { + if (_lastLoadTime == null) return true; + + // Refresh if last load was more than 30 seconds ago + final now = DateTime.now(); + final difference = now.difference(_lastLoadTime!); + return difference.inSeconds > 30; + } + + Future _loadWorkoutInfo() async { if (!mounted) return; setState(() { @@ -51,43 +77,15 @@ class _SplashScreenState extends State { _error = null; }); - try { - if (!_authService.isAuthenticated()) { - _navigateToLogin(); - return; - } - - await _loadWorkoutInfo(); - - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } catch (e) { - log('Data loading error: $e'); - if (!mounted) return; - - if (e.toString().contains('Authentication') || - e.toString().contains('Unauthorized')) { - _navigateToLogin(); - } else { - setState(() { - _error = e.toString(); - _isLoading = false; - }); - } - } - } - - Future _loadWorkoutInfo() async { try { final workoutInfo = await _workoutInfoService.getWorkoutInfo(); if (!mounted) return; - + setState(() { _workoutInfo = workoutInfo; _error = null; + _lastLoadTime = DateTime.now(); + _isLoading = false; }); } catch (e) { log('Error loading workout info: $e'); @@ -99,9 +97,9 @@ class _SplashScreenState extends State { setState(() { _workoutInfo = []; _error = e.toString(); + _isLoading = false; }); } - rethrow; } } @@ -112,7 +110,7 @@ class _SplashScreenState extends State { context.read().clearUserInfo(); context.read().loggedOut(); AuthService.clearToken(); - + Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (context) => const LoginScreen()), (route) => false, @@ -132,43 +130,207 @@ class _SplashScreenState extends State { } } - Future _startExercise(BuildContext context, Widget exerciseScreen) async { - try { - // Set landscape orientation before starting exercise - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); + Future _startExercise( + BuildContext context, Widget exerciseScreen) async { + try { + // Set landscape orientation before starting exercise + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); - if (!mounted) return; - - // Navigate to exercise screen - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => exerciseScreen, + if (!mounted) return; + + // Navigate to exercise screen + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => exerciseScreen, + ), + ); + + // Reset orientation and reload data if needed + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + if (result == true && mounted) { + await _loadData(); + } + } catch (e) { + debugPrint('Exercise screen error: $e'); + } + } + + String get _userName { + final user = context.read().userInfo; + return user?.username ?? ''; + } + + Widget _buildHomeTab() { + return RefreshIndicator( + onRefresh: _loadData, + color: const Color(0xFFE8FE54), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildWelcomeSection(), + const SizedBox(height: 24), + _buildWorkoutCards(), + const SizedBox(height: 24), + // _buildInstructions(), + // const SizedBox(height: 24), + _buildQuickStats(), + const SizedBox(height: 24), + ], + ), ), ); + } - // Reset orientation and reload data if needed - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); + Widget _buildWelcomeSection() { + final now = DateTime.now(); + final hour = now.hour; + + // Get appropriate greeting based on time of day + String greeting; + if (hour < 12) { + greeting = 'Good morning'; + } else if (hour < 17) { + greeting = 'Good afternoon'; + } else { + greeting = 'Good evening'; + } + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFFE8FE54).withOpacity(0.2), + Colors.white, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$greeting,', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 4), + Text( + _userName, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFE8FE54).withOpacity(0.3), + shape: BoxShape.circle, + ), + child: Icon( + _getGreetingIcon(hour), + color: Colors.black, + size: 24, + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFE8FE54), + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.local_fire_department, + color: Colors.orange, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Ready for today\'s workout?', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[800], + ), + ), + ], + ), + ), + ], + ), + ); + } - if (result == true && mounted) { - await _loadData(); + IconData _getGreetingIcon(int hour) { + if (hour < 6) { + return Icons.bedtime; // Night time + } else if (hour < 12) { + return Icons.wb_sunny; // Morning + } else if (hour < 17) { + return Icons.wb_cloudy; // Afternoon + } else if (hour < 20) { + return Icons.wb_twilight; // Evening + } else { + return Icons.nightlight; // Night } - } catch (e) { - debugPrint('Exercise screen error: $e'); } -} - Widget _buildHeader() { + Widget _buildQuickStats() { + final totalWorkouts = _workoutInfo.length; + final totalCalories = _workoutInfo.fold( + 0, + (sum, workout) => sum + workout.totalCalories, + ); + return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), @@ -177,44 +339,213 @@ class _SplashScreenState extends State { ), ], ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Text( + 'Quick Stats', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), Row( children: [ - Hero( - tag: 'app_logo', - child: Image.asset( - 'assets/workout_logo.png', - height: 60, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.fitness_center, - size: 40, - color: Colors.black, - ); - }, - ), + _buildStatCard( + 'Total Workouts', + '$totalWorkouts', + Icons.fitness_center, + Colors.blue, + ), + const SizedBox(width: 16), + _buildStatCard( + 'Calories Burned', + '${totalCalories.toStringAsFixed(1)} cal', + Icons.local_fire_department, + Colors.orange, ), ], ), - TextButton.icon( - onPressed: _handleLogout, - icon: const Icon(Icons.logout), - label: const Text('Logout'), - style: TextButton.styleFrom( - foregroundColor: Colors.black54, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + ], + ), + ); + } + + Widget _buildStatCard( + String title, String value, IconData icon, Color color) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Icon(icon, color: color), + const SizedBox(height: 8), + Text( + title, + style: TextStyle( + color: color, + fontWeight: FontWeight.w500, ), + textAlign: TextAlign.center, ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + title: _buildHeader(), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(60), + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.withOpacity(0.2), + ), + ), + ), + child: TabBar( + controller: _tabController, + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + indicatorColor: const Color(0xFFE8FE54), + indicatorWeight: 3, + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + tabs: const [ + Tab( + icon: Icon(Icons.home_outlined), + text: 'Beranda', + ), + Tab( + icon: Icon(Icons.bar_chart), + text: 'Progress', + ), + ], ), + ), + ), + ); + } + + Widget _buildWorkoutsTab() { + return RefreshIndicator( + onRefresh: () async { + await _loadWorkoutInfo(); + _lastLoadTime = + DateTime.now(); // Update last load time after manual refresh + }, + color: const Color(0xFFE8FE54), + child: Stack( + children: [ + SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProgress(), + const SizedBox(height: 24), + ], + ), + ), + if (_isLoading) + Container( + color: Colors.black12, + child: const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFFE8FE54)), + ), + ), + ), ], ), ); } + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: BlocListener( + listener: (context, state) { + if (state.status != AuthStatus.authenticated) { + _navigateToLogin(); + } + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: _buildAppBar(), + body: SafeArea( + child: TabBarView( + controller: _tabController, + physics: const BouncingScrollPhysics(), + children: [ + _buildHomeTab(), + _buildWorkoutsTab(), + ], + ), + ), + ), + ), + ); + } + + Widget _buildHeader() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Hero( + tag: 'app_logo', + child: Image.asset( + 'assets/workout_logo.png', + height: 100, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.fitness_center, + size: 32, + color: Colors.black, + ); + }, + ), + ), + TextButton.icon( + onPressed: _handleLogout, + icon: const Icon(Icons.logout), + label: const Text('Logout'), + style: TextButton.styleFrom( + foregroundColor: Colors.black54, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ); + } + + // Update _buildWorkoutCards for better fit in tab view Widget _buildWorkoutCards() { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), @@ -267,154 +598,67 @@ class _SplashScreenState extends State { } Widget _buildProgress() { - if (_error != null || _workoutInfo.isEmpty) { - return ProgressTracker( - workouts: _workoutInfo, - isLoading: _isLoading, - error: _error, - onRetry: _loadData, - ); - } - return Container( margin: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Your Progress', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Colors.black, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Your Progress', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + if (_lastLoadTime != null) + Text( + 'Last updated: ${_formatLastUpdate(_lastLoadTime!)}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], ), const SizedBox(height: 16), ProgressTracker( workouts: _workoutInfo, - isLoading: _isLoading, + isLoading: false, // We handle loading state in tab view error: _error, - onRetry: _loadData, + onRetry: _loadWorkoutInfo, ), ], ), ); } - Widget _buildInstructions() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey[200]!), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'How to use', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - const SizedBox(height: 16), - _buildInstructionItem( - Icons.screen_rotation, - 'Position your device in landscape mode', - ), - _buildInstructionItem( - Icons.visibility, - 'Make sure your full body is visible', - ), - _buildInstructionItem( - Icons.fitness_center, - 'Maintain proper form for accurate counting', - ), - ], - ), - ); - } + String _formatLastUpdate(DateTime time) { + final now = DateTime.now(); + final difference = now.difference(time); - Widget _buildInstructionItem(IconData icon, String text) { - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: const Color(0xFFE8FE54), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, color: Colors.black, size: 20), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - text, - style: const TextStyle( - fontSize: 16, - color: Colors.black87, - ), - ), - ), - ], - ), - ); + if (difference.inSeconds < 60) { + return 'Just now'; + } else if (difference.inMinutes < 60) { + return '${difference.inMinutes}m ago'; + } else if (difference.inHours < 24) { + return '${difference.inHours}h ago'; + } else { + return '${difference.inDays}d ago'; + } } - @override - Widget build(BuildContext context) { - return PopScope( - canPop: false, - child: BlocListener( - listener: (context, state) { - if (state.status != AuthStatus.authenticated) { - _navigateToLogin(); - } - }, - child: Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: _isLoading - ? const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Color(0xFFE8FE54)), - ), - ) - : RefreshIndicator( - onRefresh: _loadData, - color: const Color(0xFFE8FE54), - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(), - const SizedBox(height: 24), - _buildWorkoutCards(), - const SizedBox(height: 24), - _buildProgress(), - const SizedBox(height: 24), - _buildInstructions(), - const SizedBox(height: 24), - ], - ), - ), - ), - ), - ), - ), - ); + Future _loadData() async { + await _loadWorkoutInfo(); } - @override - void dispose() { - _setPortraitOrientation(); - super.dispose(); + Future _setPortraitOrientation() async { + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); } -} \ No newline at end of file +} diff --git a/lib/widgets/progress_tracker.dart b/lib/widgets/progress_tracker.dart index 19a58e6..a46e0fd 100644 --- a/lib/widgets/progress_tracker.dart +++ b/lib/widgets/progress_tracker.dart @@ -1,6 +1,6 @@ -// lib/widgets/progress_tracker.dart import 'package:flutter/material.dart'; import 'package:workout_ai/services/workout_info_service.dart'; +import 'package:fl_chart/fl_chart.dart'; class ProgressTracker extends StatelessWidget { final List workouts; @@ -26,96 +26,315 @@ class ProgressTracker extends StatelessWidget { } if (error != null) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.red.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.error_outline, color: Colors.red.shade700), - const SizedBox(width: 8), - Expanded( - child: Text( - error!.contains('Authentication') - ? 'Please log in again to view your progress' - : 'Unable to load workout progress', + return _buildErrorWidget(); + } + + return ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + _buildWeightTracker(), + const SizedBox(height: 20), + if (workouts.isEmpty) + _buildEmptyState() + else + ...workouts.map((workout) { + final color = + workout.woName.contains('Push') ? Colors.blue : Colors.green; + return _buildWorkoutItem(workout, color); + }), + ], + ); + } + + Widget _buildWeightTracker() { + final weightData = [ + WeightData(DateTime(2024, 1, 1), 92.3), + WeightData(DateTime(2024, 2, 1), 80.4), + WeightData(DateTime(2024, 3, 1), 90.22), + ]; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Weight', style: TextStyle( - color: Colors.red.shade700, - fontWeight: FontWeight.bold, + color: Colors.grey.shade600, + fontSize: 14, ), ), + const SizedBox(height: 4), + Row( + children: [ + Text( + '90.22', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(width: 4), + Text( + 'Kg', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + ], + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), ), - ], - ), - const SizedBox(height: 8), - Text( - error!, - style: TextStyle(color: Colors.red.shade700), - ), - const SizedBox(height: 12), - Center( - child: ElevatedButton.icon( - onPressed: onRetry, - icon: const Icon(Icons.refresh), - label: const Text('Try Again'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.red.shade400, + child: Row( + children: [ + Icon(Icons.arrow_upward, + size: 16, color: Colors.green.shade600), + const SizedBox(width: 4), + Text( + '4.2 (+2.68%)', + style: TextStyle( + color: Colors.green.shade600, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 24), + SizedBox( + height: 200, + child: LineChart( + LineChartData( + gridData: const FlGridData(show: false), + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 22, + interval: 1, + getTitlesWidget: (value, meta) { + if (value.toInt() >= weightData.length) + return const Text(''); + return Text( + _getMonthName(weightData[value.toInt()].date.month), + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + ); + }, + ), + ), + leftTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData( + show: false, + border: Border.all(color: const Color(0xff37434d)), + ), + minX: 0, + maxX: weightData.length.toDouble() - 1, + minY: _getMinWeight(weightData) - 5, + maxY: _getMaxWeight(weightData) + 5, + lineBarsData: [ + LineChartBarData( + spots: weightData.asMap().entries.map((entry) { + return FlSpot(entry.key.toDouble(), entry.value.weight); + }).toList(), + isCurved: true, + color: Colors.grey.shade600, + barWidth: 2, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: Colors.white, + strokeWidth: 2, + strokeColor: Colors.grey.shade600, + ); + }, + ), + belowBarData: BarAreaData(show: false), + ), + ], + lineTouchData: LineTouchData( + enabled: true, + touchTooltipData: LineTouchTooltipData( + fitInsideHorizontally: true, + tooltipRoundedRadius: 8, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItems: (List touchedBarSpots) { + return touchedBarSpots.map((barSpot) { + return LineTooltipItem( + '${barSpot.y.toStringAsFixed(1)} kg', + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ); + }).toList(); + }, + ), ), ), ), - ], - ), - ); - } + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Calories Burned', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + '1280', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(width: 4), + Text( + 'Kcal', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + ], + ), + ], + ), + ], + ), + ], + ), + ); + } - if (workouts.isEmpty) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: const Column( - children: [ - Icon(Icons.fitness_center, size: 48, color: Colors.grey), - SizedBox(height: 16), - Text( - 'No workouts yet', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + Widget _buildErrorWidget() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.error_outline, color: Colors.red.shade700), + const SizedBox(width: 8), + Expanded( + child: Text( + error!.contains('Authentication') + ? 'Please log in again to view your progress' + : 'Unable to load workout progress', + style: TextStyle( + color: Colors.red.shade700, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + error!, + style: TextStyle(color: Colors.red.shade700), + ), + const SizedBox(height: 12), + Center( + child: ElevatedButton.icon( + onPressed: onRetry, + icon: const Icon(Icons.refresh), + label: const Text('Try Again'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.red.shade400, ), ), - SizedBox(height: 8), - Text( - 'Complete your first workout to see your progress here', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ], - ), - ); - } + ), + ], + ), + ); + } - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: workouts.length, - itemBuilder: (context, index) { - final workout = workouts[index]; - final color = workout.woName.contains('Push') ? Colors.blue : Colors.green; - return _buildWorkoutItem(workout, color); - }, + Widget _buildEmptyState() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: const Column( + children: [ + Icon(Icons.fitness_center, size: 48, color: Colors.grey), + SizedBox(height: 16), + Text( + 'No workouts yet', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + 'Complete your first workout to see your progress here', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + ], + ), ); } @@ -192,4 +411,37 @@ class ProgressTracker extends StatelessWidget { ), ); } -} \ No newline at end of file + + String _getMonthName(int month) { + const monthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' + ]; + return monthNames[month - 1]; + } + + double _getMinWeight(List data) { + return data.map((item) => item.weight).reduce((a, b) => a < b ? a : b); + } + + double _getMaxWeight(List data) { + return data.map((item) => item.weight).reduce((a, b) => a > b ? a : b); + } +} + +class WeightData { + final DateTime date; + final double weight; + + WeightData(this.date, this.weight); +} diff --git a/lib/widgets/situp_completion_dialog.dart b/lib/widgets/situp_completion_dialog.dart new file mode 100644 index 0000000..0256a1b --- /dev/null +++ b/lib/widgets/situp_completion_dialog.dart @@ -0,0 +1,193 @@ +// lib/widgets/situp_completion_dialog.dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:workout_ai/models/exercise_completion_model.dart'; +import 'package:workout_ai/models/exercise_stats_model.dart'; +import 'package:workout_ai/services/situp_service.dart'; +import 'package:workout_ai/views/splash_screen.dart'; + +class SitUpCompletionDialog extends StatefulWidget { + final String exerciseType; + final int reps; + + const SitUpCompletionDialog({ + super.key, + required this.exerciseType, + required this.reps, + }); + + @override + State createState() => _SitUpCompletionDialogState(); +} + +class _SitUpCompletionDialogState extends State { + final _formKey = GlobalKey(); + bool _isSubmitting = false; + + Future _submitWorkout() async { + if (!_formKey.currentState!.validate()) return; + setState(() => _isSubmitting = true); + + try { + final situpService = SitUpService(); + final result = await situpService.submitSitUps( + sitUps: widget.reps, + ); + + if (!mounted) return; + + if (result['statusCode'] == 200) { + context.read().updateStats( + exerciseType: widget.exerciseType, + repCount: widget.reps, + caloriesPerRep: double.tryParse( + result['Kalori_yang_terbakar_per_sit_up'] ?? '0', + ), + totalCalories: double.tryParse( + result['Total_kalori_yang_terbakar'] ?? '0', + ), + ); + + context + .read() + .markExerciseComplete(widget.exerciseType); + + Navigator.of(context).pop(); + Navigator.of(context).pop(); + + final navigatorContext = Navigator.of(context); + await Future.delayed(const Duration(milliseconds: 100)); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Workout completed! Calories burned: ${result['Total_kalori_yang_terbakar']}', + ), + backgroundColor: Colors.green, + ), + ); + + if (navigatorContext.context.mounted) { + final splashScreenState = navigatorContext.context + .findAncestorStateOfType>(); + if (splashScreenState != null) { + (splashScreenState as dynamic).loadData(); + } + } + } else { + throw Exception('Failed to submit workout'); + } + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to submit workout: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + if (mounted) { + setState(() => _isSubmitting = false); + } + } + } + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 40), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Complete Workout', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${widget.exerciseType}s completed:', + style: + const TextStyle(fontWeight: FontWeight.w500), + ), + Text( + '${widget.reps}', + style: const TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: + _isSubmitting ? null : () => Navigator.pop(context), + child: const Text('Cancel'), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: _isSubmitting ? null : _submitWorkout, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + child: _isSubmitting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : const Text('Submit'), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/workout_completion_dialog.dart b/lib/widgets/workout_completion_dialog.dart index 570ffd8..e2b10e1 100644 --- a/lib/widgets/workout_completion_dialog.dart +++ b/lib/widgets/workout_completion_dialog.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:workout_ai/models/exercise_completion_model.dart'; import 'package:workout_ai/models/exercise_stats_model.dart'; import 'package:workout_ai/services/pushup_service.dart'; +import 'package:workout_ai/services/situp_service.dart'; import 'package:workout_ai/views/splash_screen.dart'; class WorkoutCompletionDialog extends StatefulWidget { @@ -17,7 +18,8 @@ class WorkoutCompletionDialog extends StatefulWidget { }); @override - State createState() => _WorkoutCompletionDialogState(); + State createState() => + _WorkoutCompletionDialogState(); } class _WorkoutCompletionDialogState extends State { @@ -25,80 +27,85 @@ class _WorkoutCompletionDialogState extends State { bool _isSubmitting = false; Future _submitWorkout() async { - if (!_formKey.currentState!.validate()) return; + if (!_formKey.currentState!.validate()) return; + setState(() => _isSubmitting = true); - setState(() => _isSubmitting = true); + try { + Map result; - try { - final pushupService = PushupService(); - final result = await pushupService.submitPushups( - pushUps: widget.reps, - ); + if (widget.exerciseType == 'Push-up Counter') { + final pushupService = PushupService(); + result = await pushupService.submitPushups( + pushUps: widget.reps, + ); + } else { + final situpService = SitUpService(); + result = await situpService.submitSitUps( + sitUps: widget.reps, + ); + } - if (!mounted) return; + if (!mounted) return; - if (result['statusCode'] == 200) { - context.read().updateStats( - exerciseType: widget.exerciseType, - repCount: widget.reps, - caloriesPerRep: double.tryParse( - result['Kalori_yang_terbakar_per_push_up'] ?? '0', - ), - totalCalories: double.tryParse( - result['Total_kalori_yang_terbakar'] ?? '0', - ), - ); + if (result['statusCode'] == 200) { + context.read().updateStats( + exerciseType: widget.exerciseType, + repCount: widget.reps, + caloriesPerRep: double.tryParse( + widget.exerciseType == 'Push-up Counter' + ? result['Kalori_yang_terbakar_per_push_up'] ?? '0' + : result['Kalori_yang_terbakar_per_sit_up'] ?? '0', + ), + totalCalories: double.tryParse( + result['Total_kalori_yang_terbakar'] ?? '0', + ), + ); + + context + .read() + .markExerciseComplete(widget.exerciseType); + + Navigator.of(context).pop(); + Navigator.of(context).pop(); + + final navigatorContext = Navigator.of(context); + await Future.delayed(const Duration(milliseconds: 100)); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Workout completed! Calories burned: ${result['Total_kalori_yang_terbakar']}', + ), + backgroundColor: Colors.green, + ), + ); - context.read().markExerciseComplete(widget.exerciseType); - - // First pop the dialog - Navigator.of(context).pop(); - - // Then pop the exercise screen - Navigator.of(context).pop(); - - // Find the SplashScreen and reload data - final navigatorContext = Navigator.of(context); - await Future.delayed(const Duration(milliseconds: 100)); - + if (navigatorContext.context.mounted) { + final splashScreenState = navigatorContext.context + .findAncestorStateOfType>(); + if (splashScreenState != null) { + (splashScreenState as dynamic).loadData(); + } + } + } else { + throw Exception('Failed to submit workout'); + } + } catch (e) { if (!mounted) return; - - // Show success message on splash screen ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Workout completed! Calories burned: ${result['Total_kalori_yang_terbakar']}', - ), - backgroundColor: Colors.green, + content: Text('Failed to submit workout: $e'), + backgroundColor: Colors.red, ), ); - - // Trigger splash screen reload - if (navigatorContext.context.mounted) { - final splashScreenState = navigatorContext.context - .findAncestorStateOfType>(); - if (splashScreenState != null) { - (splashScreenState as dynamic).loadData(); - } + } finally { + if (mounted) { + setState(() => _isSubmitting = false); } - } else { - throw Exception('Failed to submit workout'); - } - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to submit workout: $e'), - backgroundColor: Colors.red, - ), - ); - } finally { - if (mounted) { - setState(() => _isSubmitting = false); } } -} - @override Widget build(BuildContext context) { @@ -139,7 +146,8 @@ class _WorkoutCompletionDialogState extends State { children: [ Text( '${widget.exerciseType}s completed:', - style: const TextStyle(fontWeight: FontWeight.w500), + style: + const TextStyle(fontWeight: FontWeight.w500), ), Text( '${widget.reps}', @@ -159,7 +167,8 @@ class _WorkoutCompletionDialogState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: _isSubmitting ? null : () => Navigator.pop(context), + onPressed: + _isSubmitting ? null : () => Navigator.pop(context), child: const Text('Cancel'), ), const SizedBox(width: 16), @@ -178,7 +187,8 @@ class _WorkoutCompletionDialogState extends State { height: 20, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), + valueColor: AlwaysStoppedAnimation( + Colors.white), ), ) : const Text('Submit'), @@ -193,4 +203,4 @@ class _WorkoutCompletionDialogState extends State { ), ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 1d7f1e0..b85ca74 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -145,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -201,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+3" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" + url: "https://pub.dev" + source: hosted + version: "0.69.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index c09d5b2..1f68d7a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: sdk: flutter http: ^1.1.0 - shared_preferences: ^2.2.2 + shared_preferences: ^2.3.2 cupertino_icons: ^1.0.8 flutter_bloc: ^8.1.3 google_ml_kit: ^0.19.0 @@ -21,6 +21,7 @@ dependencies: path: ^1.8.3 path_provider: ^2.0.15 lottie: ^2.6.0 + fl_chart: ^0.69.0 dev_dependencies: flutter_test: