Compare commits

..

No commits in common. "757dfad4e8c6ea2fe57614f16eb6718111c38c15" and "befbfb472782099d7ad7019abf7d803ed7000b40" have entirely different histories.

171 changed files with 1247 additions and 7446 deletions

View File

@ -1,12 +1,12 @@
plugins {
id("kotlin-android")
id("com.android.application")
id("com.google.gms.google-services")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.yimaru.lms.app"
namespace = "com.example.yimaru_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@ -15,24 +15,25 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.yimaru_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
applicationId = "com.yimaru.lms.app"
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
targetSdk = flutter.targetSdkVersion
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}

View File

@ -1,51 +0,0 @@
{
"project_info": {
"project_number": "574860813475",
"project_id": "yimaru-lms-e834e",
"storage_bucket": "yimaru-lms-e834e.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:574860813475:android:cd7fa6cf3a0527d97acb16",
"android_client_info": {
"package_name": "com.yimaru.lms.app"
}
},
"oauth_client": [
{
"client_id": "574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.yimaru.lms.app",
"certificate_hash": "fc91f52846d27c62bba3e16bc98982fb9953eca1"
}
},
{
"client_id": "574860813475-631s3mo8ha2qc2jeb5e2aosn0967niik.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.yimaru.lms.app",
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
}
}
],
"api_key": [
{
"current_key": "AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -1,13 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<application
android:label="Yimaru"
android:label="yimaru_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

View File

@ -1,4 +1,4 @@
package com.yimaru.lms.app
package com.example.yimaru_app
import io.flutter.embedding.android.FlutterActivity

View File

@ -19,10 +19,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.13.2" apply false
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
id("com.google.gms.google-services") version("4.4.4") apply false
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

View File

@ -1,5 +0,0 @@
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.9409C116.627 97.7416 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7416 8.37341 93.9409V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
<path d="M55.1865 5.94918C59.7122 3.33638 65.2878 3.33638 69.8135 5.94918L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94918Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
<path d="M40.0531 83.9074L50.2659 55.9819H57.4467L67.5797 83.9074H61.1569L59.0026 77.8037H48.3909L46.2366 83.9074H40.0531ZM49.9866 72.9766H57.367L53.6569 62.3649L49.9866 72.9766ZM79.9963 55.9819V83.9074H74.2516V66.075C73.9059 66.208 73.4006 66.2745 72.7357 66.2745H68.0681V61.5271H72.2569C72.9484 61.5271 73.467 61.2612 73.8128 60.7292C74.1851 60.1707 74.3713 59.4925 74.3713 58.6947V55.9819H79.9963Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,5 +0,0 @@
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.9409C116.627 97.7416 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7416 8.37341 93.9409V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
<path d="M55.1865 5.94918C59.7122 3.33638 65.2878 3.33638 69.8135 5.94918L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94918Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
<path d="M40.0531 83.9074L50.2659 55.9819H57.4467L67.5797 83.9074H61.1569L59.0026 77.8037H48.3909L46.2366 83.9074H40.0531ZM49.9866 72.9766H57.367L53.6569 62.3649L49.9866 72.9766ZM79.5575 70.7027C80.9139 69.6122 81.9112 68.6149 82.5495 67.7106C83.1878 66.8064 83.5069 65.8223 83.5069 64.7585C83.5069 63.4021 83.0947 62.3649 82.2702 61.6468C81.4724 60.9021 80.3952 60.5298 79.0389 60.5298C77.7357 60.5298 76.6718 60.9686 75.8474 61.8463C75.0495 62.7239 74.6506 63.9074 74.6506 65.3968V65.8356H68.866V65.0777C68.866 63.3223 69.2782 61.7266 70.1027 60.2904C70.9537 58.8542 72.1506 57.7372 73.6931 56.9394C75.2357 56.1149 77.0176 55.7027 79.0389 55.7027C82.3101 55.7027 84.8367 56.5005 86.6186 58.0963C88.4272 59.692 89.3314 61.833 89.3314 64.5191C89.3314 66.434 88.8527 68.0963 87.8952 69.5058C86.9644 70.8888 85.5149 72.3782 83.5468 73.9739L77.2835 79.0803H89.4511V83.9074H68.9857V79.4792L79.5575 70.7027Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

10
assets/icons/b1.svg Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="125px" height="138px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.808" fill="#9e2891" d="M 55.5,-0.5 C 59.8333,-0.5 64.1667,-0.5 68.5,-0.5C 84.7704,8.97332 101.104,18.4733 117.5,28C 120.738,30.9978 123.071,34.4978 124.5,38.5C 124.5,58.5 124.5,78.5 124.5,98.5C 122.507,103.008 119.507,106.842 115.5,110C 99.2743,118.941 83.2743,128.108 67.5,137.5C 64.1667,137.5 60.8333,137.5 57.5,137.5C 41.2609,128.375 24.9276,119.208 8.5,110C 4.49334,106.842 1.49334,103.008 -0.5,98.5C -0.5,78.5 -0.5,58.5 -0.5,38.5C 1.15508,34.654 3.48842,31.154 6.5,28C 22.8963,18.4733 39.2296,8.97332 55.5,-0.5 Z"/></g>
<g><path style="opacity:1" fill="#e9cee5" d="M 42.5,55.5 C 42.5,64.5 42.5,73.5 42.5,82.5C 47.1667,82.5 51.8333,82.5 56.5,82.5C 51.6946,83.4872 46.6946,83.8205 41.5,83.5C 41.1731,73.985 41.5064,64.6517 42.5,55.5 Z"/></g>
<g><path style="opacity:1" fill="#fdfafc" d="M 42.5,55.5 C 47.1785,55.3342 51.8452,55.5008 56.5,56C 61.2911,57.4119 63.1244,60.5786 62,65.5C 61.1667,67 60,68.1667 58.5,69C 63.8662,71.8444 64.8662,75.8444 61.5,81C 59.9751,82.0086 58.3084,82.5086 56.5,82.5C 51.8333,82.5 47.1667,82.5 42.5,82.5C 42.5,73.5 42.5,64.5 42.5,55.5 Z"/></g>
<g><path style="opacity:1" fill="#fbf7fb" d="M 71.5,55.5 C 73.5,55.5 75.5,55.5 77.5,55.5C 77.5,64.8333 77.5,74.1667 77.5,83.5C 75.5,83.5 73.5,83.5 71.5,83.5C 71.5,77.5 71.5,71.5 71.5,65.5C 69.5,65.5 67.5,65.5 65.5,65.5C 65.5,64.1667 65.5,62.8333 65.5,61.5C 70.5,62.5 72.5,60.5 71.5,55.5 Z"/></g>
<g><path style="opacity:1" fill="#a63d9a" d="M 47.5,60.5 C 50.1873,60.3359 52.854,60.5026 55.5,61C 57.1397,63.7758 56.473,65.7758 53.5,67C 51.5273,67.4955 49.5273,67.6621 47.5,67.5C 47.5,65.1667 47.5,62.8333 47.5,60.5 Z"/></g>
<g><path style="opacity:1" fill="#a33596" d="M 47.5,71.5 C 50.1873,71.3359 52.854,71.5026 55.5,72C 58.033,74.084 58.033,76.084 55.5,78C 52.854,78.4974 50.1873,78.6641 47.5,78.5C 47.5,76.1667 47.5,73.8333 47.5,71.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,5 +0,0 @@
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.941C116.627 97.7417 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7417 8.37341 93.941V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
<path d="M55.1865 5.94919C59.7122 3.33638 65.2878 3.33638 69.8135 5.94919L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94919Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
<path d="M52.8989 55.9819C56.1436 55.9819 58.6569 56.6202 60.4388 57.8968C62.2207 59.1734 63.1116 60.9819 63.1116 63.3223C63.1116 65.0511 62.6329 66.4473 61.6755 67.5112C60.718 68.575 59.3616 69.2665 57.6063 69.5856C59.6808 69.8516 61.2765 70.5165 62.3936 71.5803C63.5372 72.6441 64.109 74.1335 64.109 76.0484C64.109 78.4952 63.1781 80.4234 61.3164 81.833C59.4547 83.216 56.8217 83.9074 53.4175 83.9074H42.2872V55.9819H52.8989ZM48.2712 67.4713H53.0584C54.335 67.4713 55.3324 67.1654 56.0505 66.5537C56.7686 65.942 57.1276 65.1175 57.1276 64.0803C57.1276 63.0431 56.7686 62.2186 56.0505 61.6069C55.3324 60.9952 54.335 60.6894 53.0584 60.6894H48.2712V67.4713ZM48.2712 79.1601H53.4574C54.8936 79.1601 55.9973 78.8542 56.7686 78.2425C57.5664 77.6308 57.9654 76.7798 57.9654 75.6894C57.9654 74.5457 57.5664 73.6681 56.7686 73.0564C55.9707 72.4181 54.867 72.0989 53.4574 72.0989H48.2712V79.1601ZM77.9315 55.9819V83.9074H72.1868V66.075C71.8411 66.208 71.3358 66.2745 70.6709 66.2745H66.0033V61.5271H70.1921C70.8836 61.5271 71.4022 61.2612 71.748 60.7292C72.1203 60.1707 72.3065 59.4925 72.3065 58.6947V55.9819H77.9315Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,5 +0,0 @@
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.941C116.627 97.7417 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7417 8.37341 93.941V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
<path d="M55.1865 5.94919C59.7122 3.33638 65.2878 3.33638 69.8135 5.94919L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94919Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
<path d="M52.8989 55.9819C56.1436 55.9819 58.6569 56.6202 60.4388 57.8968C62.2207 59.1734 63.1116 60.9819 63.1116 63.3223C63.1116 65.0511 62.6329 66.4473 61.6755 67.5112C60.718 68.575 59.3616 69.2665 57.6063 69.5856C59.6808 69.8516 61.2765 70.5165 62.3936 71.5803C63.5372 72.6441 64.109 74.1335 64.109 76.0484C64.109 78.4952 63.1781 80.4234 61.3164 81.833C59.4547 83.216 56.8217 83.9074 53.4175 83.9074H42.2872V55.9819H52.8989ZM48.2712 67.4713H53.0584C54.335 67.4713 55.3324 67.1654 56.0505 66.5537C56.7686 65.942 57.1276 65.1175 57.1276 64.0803C57.1276 63.0431 56.7686 62.2186 56.0505 61.6069C55.3324 60.9952 54.335 60.6894 53.0584 60.6894H48.2712V67.4713ZM48.2712 79.1601H53.4574C54.8936 79.1601 55.9973 78.8542 56.7686 78.2425C57.5664 77.6308 57.9654 76.7798 57.9654 75.6894C57.9654 74.5457 57.5664 73.6681 56.7686 73.0564C55.9707 72.4181 54.867 72.0989 53.4574 72.0989H48.2712V79.1601ZM77.4927 70.7027C78.849 69.6122 79.8464 68.6149 80.4847 67.7106C81.123 66.8064 81.4421 65.8223 81.4421 64.7585C81.4421 63.4021 81.0299 62.3649 80.2054 61.6468C79.4076 60.9021 78.3304 60.5298 76.974 60.5298C75.6709 60.5298 74.607 60.9686 73.7826 61.8463C72.9847 62.7239 72.5858 63.9074 72.5858 65.3968V65.8356H66.8012V65.0777C66.8012 63.3223 67.2134 61.7266 68.0379 60.2904C68.8889 58.8542 70.0858 57.7372 71.6283 56.9394C73.1709 56.1149 74.9528 55.7027 76.974 55.7027C80.2453 55.7027 82.7719 56.5005 84.5538 58.0963C86.3623 59.692 87.2666 61.833 87.2666 64.5191C87.2666 66.434 86.7879 68.0963 85.8304 69.5058C84.8996 70.8888 83.4501 72.3782 81.482 73.9739L75.2187 79.0803H87.3863V83.9074H66.9209V79.4792L77.4927 70.7027Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1 +0,0 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-lms-e834e","appId":"1:574860813475:android:cd7fa6cf3a0527d97acb16","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-lms-e834e","configurations":{"android":"1:574860813475:android:cd7fa6cf3a0527d97acb16","ios":"1:574860813475:ios:3ac9f7c4ae1771287acb16"}}}}}}

View File

@ -368,7 +368,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -547,7 +547,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@ -30,14 +30,6 @@ import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/views/welcome/welcome_view.dart';
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import 'package:yimaru_app/services/permission_handler_service.dart';
import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/services/google_auth_service.dart';
import 'package:yimaru_app/services/image_downloader_service.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
// @stacked-import
@StackedApp(
@ -65,10 +57,6 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
MaterialRoute(page: WelcomeView),
MaterialRoute(page: AssessmentView),
MaterialRoute(page: LearnLessonView),
MaterialRoute(page: FailureView),
MaterialRoute(page: ForgetPasswordView),
MaterialRoute(page: LearnLessonDetailView),
MaterialRoute(page: LearnPracticeView),
// @stacked-route
],
dependencies: [
@ -80,10 +68,6 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
LazySingleton(classType: SecureStorageService),
LazySingleton(classType: DioService),
LazySingleton(classType: StatusCheckerService),
LazySingleton(classType: PermissionHandlerService),
LazySingleton(classType: ImagePickerService),
LazySingleton(classType: GoogleAuthService),
LazySingleton(classType: ImageDownloaderService),
// @stacked-service
],
bottomsheets: [

View File

@ -14,10 +14,6 @@ import 'package:stacked_shared/stacked_shared.dart';
import '../services/api_service.dart';
import '../services/authentication_service.dart';
import '../services/dio_service.dart';
import '../services/google_auth_service.dart';
import '../services/image_downloader_service.dart';
import '../services/image_picker_service.dart';
import '../services/permission_handler_service.dart';
import '../services/secure_storage_service.dart';
import '../services/status_checker_service.dart';
@ -40,8 +36,4 @@ Future<void> setupLocator({
locator.registerLazySingleton(() => SecureStorageService());
locator.registerLazySingleton(() => DioService());
locator.registerLazySingleton(() => StatusCheckerService());
locator.registerLazySingleton(() => PermissionHandlerService());
locator.registerLazySingleton(() => ImagePickerService());
locator.registerLazySingleton(() => GoogleAuthService());
locator.registerLazySingleton(() => ImageDownloaderService());
}

View File

@ -5,31 +5,24 @@
// **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i29;
import 'package:flutter/material.dart' as _i25;
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i30;
import 'package:stacked_services/stacked_services.dart' as _i26;
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
as _i10;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'
as _i13;
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7;
import 'package:yimaru_app/ui/views/failure/failure_view.dart' as _i25;
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'
as _i26;
import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2;
import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14;
import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19;
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'
as _i24;
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart'
as _i27;
import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20;
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
as _i21;
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'
as _i28;
import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18;
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3;
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart'
@ -96,14 +89,6 @@ class Routes {
static const learnLessonView = '/learn-lesson-view';
static const failureView = '/failure-view';
static const forgetPasswordView = '/forget-password-view';
static const learnLessonDetailView = '/learn-lesson-detail-view';
static const learnPracticeView = '/learn-practice-view';
static const all = <String>{
homeView,
onboardingView,
@ -128,10 +113,6 @@ class Routes {
welcomeView,
assessmentView,
learnLessonView,
failureView,
forgetPasswordView,
learnLessonDetailView,
learnPracticeView,
};
}
@ -229,33 +210,17 @@ class StackedRouter extends _i1.RouterBase {
Routes.learnLessonView,
page: _i24.LearnLessonView,
),
_i1.RouteDef(
Routes.failureView,
page: _i25.FailureView,
),
_i1.RouteDef(
Routes.forgetPasswordView,
page: _i26.ForgetPasswordView,
),
_i1.RouteDef(
Routes.learnLessonDetailView,
page: _i27.LearnLessonDetailView,
),
_i1.RouteDef(
Routes.learnPracticeView,
page: _i28.LearnPracticeView,
),
];
final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(),
settings: data,
);
},
_i3.OnboardingView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(),
settings: data,
);
@ -264,159 +229,133 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(),
);
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data,
);
},
_i5.ProfileView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(),
settings: data,
);
},
_i6.ProfileDetailView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(),
settings: data,
);
},
_i7.DownloadsView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(),
settings: data,
);
},
_i8.ProgressView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(),
settings: data,
);
},
_i9.OngoingProgressView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(),
settings: data,
);
},
_i10.AccountPrivacyView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(),
settings: data,
);
},
_i11.SupportView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(),
settings: data,
);
},
_i12.TelegramSupportView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(),
settings: data,
);
},
_i13.CallSupportView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(),
settings: data,
);
},
_i14.LanguageView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(),
settings: data,
);
},
_i15.PrivacyPolicyView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(),
settings: data,
);
},
_i16.TermsAndConditionsView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(),
settings: data,
);
},
_i17.RegisterView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(),
settings: data,
);
},
_i18.LoginView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(),
settings: data,
);
},
_i19.LearnView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(),
settings: data,
);
},
_i20.LearnLevelView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(),
settings: data,
);
},
_i21.LearnModuleView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(),
settings: data,
);
},
_i22.WelcomeView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i22.WelcomeView(),
settings: data,
);
},
_i23.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) =>
_i23.AssessmentView(key: args.key, data: args.data),
settings: data,
);
},
_i24.LearnLessonView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.LearnLessonView(),
settings: data,
);
},
_i25.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i29.MaterialPageRoute<dynamic>(
builder: (context) =>
_i25.FailureView(key: args.key, label: args.label),
settings: data,
);
},
_i26.ForgetPasswordView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i26.ForgetPasswordView(),
settings: data,
);
},
_i27.LearnLessonDetailView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i27.LearnLessonDetailView(),
settings: data,
);
},
_i28.LearnPracticeView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i28.LearnPracticeView(),
settings: data,
);
},
};
@override
@ -432,7 +371,7 @@ class StartupViewArguments {
this.label = 'Loading',
});
final _i29.Key? key;
final _i25.Key? key;
final String label;
@ -459,7 +398,7 @@ class AssessmentViewArguments {
required this.data,
});
final _i29.Key? key;
final _i25.Key? key;
final Map<String, dynamic> data;
@ -480,34 +419,7 @@ class AssessmentViewArguments {
}
}
class FailureViewArguments {
const FailureViewArguments({
this.key,
required this.label,
});
final _i29.Key? key;
final String label;
@override
String toString() {
return '{"key": "$key", "label": "$label"}';
}
@override
bool operator ==(covariant FailureViewArguments other) {
if (identical(this, other)) return true;
return other.key == key && other.label == label;
}
@override
int get hashCode {
return key.hashCode ^ label.hashCode;
}
}
extension NavigatorStateExtension on _i30.NavigationService {
extension NavigatorStateExtension on _i26.NavigationService {
Future<dynamic> navigateToHomeView([
int? routerId,
bool preventDuplicates = true,
@ -537,7 +449,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> navigateToStartupView({
_i29.Key? key,
_i25.Key? key,
String label = 'Loading',
int? routerId,
bool preventDuplicates = true,
@ -806,7 +718,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> navigateToAssessmentView({
_i29.Key? key,
_i25.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -836,65 +748,6 @@ extension NavigatorStateExtension on _i30.NavigationService {
transition: transition);
}
Future<dynamic> navigateToFailureView({
_i29.Key? key,
required String label,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
return navigateTo<dynamic>(Routes.failureView,
arguments: FailureViewArguments(key: key, label: label),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToForgetPasswordView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.forgetPasswordView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnLessonDetailView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnLessonDetailView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnPracticeView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnPracticeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([
int? routerId,
bool preventDuplicates = true,
@ -924,7 +777,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> replaceWithStartupView({
_i29.Key? key,
_i25.Key? key,
String label = 'Loading',
int? routerId,
bool preventDuplicates = true,
@ -1193,7 +1046,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> replaceWithAssessmentView({
_i29.Key? key,
_i25.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -1222,63 +1075,4 @@ extension NavigatorStateExtension on _i30.NavigationService {
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithFailureView({
_i29.Key? key,
required String label,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
return replaceWith<dynamic>(Routes.failureView,
arguments: FailureViewArguments(key: key, label: label),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithForgetPasswordView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.forgetPasswordView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnLessonDetailView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnLessonDetailView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnPracticeView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnPracticeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
}

View File

@ -1,70 +0,0 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk',
appId: '1:574860813475:android:cd7fa6cf3a0527d97acb16',
messagingSenderId: '574860813475',
projectId: 'yimaru-lms-e834e',
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyBBcQ17JB6RBTjD7G7mh6Xf_FMUGxP5cC8',
appId: '1:574860813475:ios:3ac9f7c4ae1771287acb16',
messagingSenderId: '574860813475',
projectId: 'yimaru-lms-e834e',
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
androidClientId:
'574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com',
iosBundleId: 'com.yimaru.lms.app',
);
}

View File

@ -1,35 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/option.dart';
import 'package:yimaru_app/models/question.dart';
part 'assessment.g.dart';
@JsonSerializable()
class Assessment {
final int? id;
final int? points;
final String? status;
@JsonKey(name: 'question_type')
final String? questionType;
@JsonKey(name: 'question_text')
final String? questionText;
@JsonKey(name: 'difficulty_level')
final String? difficultyLevel;
@JsonKey(name: 'Question')
final Question? question;
@JsonKey(name: 'Options')
final List<Option>? options;
const Assessment({
this.id,
this.points,
this.status,
this.options,
this.questionText,
this.questionType,
this.difficultyLevel,
});
const Assessment({this.options, this.question});
factory Assessment.fromJson(Map<String, dynamic> json) =>
_$AssessmentFromJson(json);

View File

@ -7,24 +7,16 @@ part of 'assessment.dart';
// **************************************************************************
Assessment _$AssessmentFromJson(Map<String, dynamic> json) => Assessment(
id: (json['id'] as num?)?.toInt(),
points: (json['points'] as num?)?.toInt(),
status: json['status'] as String?,
options: (json['options'] as List<dynamic>?)
options: (json['Options'] as List<dynamic>?)
?.map((e) => Option.fromJson(e as Map<String, dynamic>))
.toList(),
questionText: json['question_text'] as String?,
questionType: json['question_type'] as String?,
difficultyLevel: json['difficulty_level'] as String?,
question: json['Question'] == null
? null
: Question.fromJson(json['Question'] as Map<String, dynamic>),
);
Map<String, dynamic> _$AssessmentToJson(Assessment instance) =>
<String, dynamic>{
'id': instance.id,
'points': instance.points,
'status': instance.status,
'question_type': instance.questionType,
'question_text': instance.questionText,
'difficulty_level': instance.difficultyLevel,
'options': instance.options,
'Question': instance.question,
'Options': instance.options,
};

View File

@ -3,15 +3,13 @@ part 'option.g.dart';
@JsonSerializable()
class Option {
final int? id;
@JsonKey(name: 'question_id')
final int? questionId;
@JsonKey(name: 'option_text')
final String? optionText;
@JsonKey(name: 'is_correct')
final bool? isCorrect;
const Option({this.id, this.optionText, this.isCorrect});
const Option({this.optionText, this.questionId});
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);

View File

@ -7,13 +7,11 @@ part of 'option.dart';
// **************************************************************************
Option _$OptionFromJson(Map<String, dynamic> json) => Option(
id: (json['id'] as num?)?.toInt(),
optionText: json['option_text'] as String?,
isCorrect: json['is_correct'] as bool?,
questionId: (json['question_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
'id': instance.id,
'question_id': instance.questionId,
'option_text': instance.optionText,
'is_correct': instance.isCorrect,
};

36
lib/models/question.dart Normal file
View File

@ -0,0 +1,36 @@
import 'package:json_annotation/json_annotation.dart';
part 'question.g.dart';
@JsonSerializable()
class Question {
final int? id;
final int? points;
final String? title;
final String? description;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'question_type')
final String? questionType;
@JsonKey(name: 'difficulty_level')
final String? difficultyLevel;
const Question(
{this.id,
this.title,
this.points,
this.isActive,
this.description,
this.questionType,
this.difficultyLevel});
factory Question.fromJson(Map<String, dynamic> json) =>
_$QuestionFromJson(json);
Map<String, dynamic> toJson() => _$QuestionToJson(this);
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'question.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Question _$QuestionFromJson(Map<String, dynamic> json) => Question(
id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?,
points: (json['points'] as num?)?.toInt(),
isActive: json['is_active'] as bool?,
description: json['description'] as String?,
questionType: json['question_type'] as String?,
difficultyLevel: json['difficulty_level'] as String?,
);
Map<String, dynamic> _$QuestionToJson(Question instance) => <String, dynamic>{
'id': instance.id,
'points': instance.points,
'title': instance.title,
'description': instance.description,
'is_active': instance.isActive,
'question_type': instance.questionType,
'difficulty_level': instance.difficultyLevel,
};

View File

@ -4,31 +4,10 @@ part 'user_model.g.dart';
@JsonSerializable()
class UserModel {
final String? email;
final String? gender;
final String? region;
final String? country;
final String? occupation;
final bool? userInfoLoaded;
@JsonKey(name: 'user_id')
final int? userId;
@JsonKey(name: 'last_name')
final String? lastName;
@JsonKey(name: 'birth_day')
final String? birthday;
@JsonKey(name: 'first_name')
final String? firstName;
final bool? profileCompleted;
@JsonKey(name: 'access_token')
final String? accessToken;
@ -36,28 +15,11 @@ class UserModel {
@JsonKey(name: 'refresh_token')
final String? refreshToken;
@JsonKey(name: 'profile_completed')
final bool? profileCompleted;
@JsonKey(name: 'profile_picture_url')
final String? profilePicture;
const UserModel({
this.email,
this.region,
this.gender,
this.userId,
this.country,
this.lastName,
this.birthday,
this.firstName,
this.occupation,
const UserModel(
{this.userId,
this.accessToken,
this.refreshToken,
this.profilePicture,
this.userInfoLoaded ,
this.profileCompleted,
});
this.refreshToken});
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);

View File

@ -7,35 +7,15 @@ part of 'user_model.dart';
// **************************************************************************
UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
email: json['email'] as String?,
region: json['region'] as String?,
gender: json['gender'] as String?,
userId: (json['user_id'] as num?)?.toInt(),
country: json['country'] as String?,
lastName: json['last_name'] as String?,
birthday: json['birth_day'] as String?,
firstName: json['first_name'] as String?,
occupation: json['occupation'] as String?,
accessToken: json['access_token'] as String?,
profileCompleted: json['profileCompleted'] as bool?,
refreshToken: json['refresh_token'] as String?,
profilePicture: json['profile_picture_url'] as String?,
profileCompleted: json['profile_completed'] as bool?,
userInfoLoaded: json['userInfoLoaded'] as bool? ?? false,
);
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'email': instance.email,
'gender': instance.gender,
'region': instance.region,
'country': instance.country,
'occupation': instance.occupation,
'userInfoLoaded': instance.userInfoLoaded,
'user_id': instance.userId,
'last_name': instance.lastName,
'birth_day': instance.birthday,
'first_name': instance.firstName,
'profileCompleted': instance.profileCompleted,
'access_token': instance.accessToken,
'refresh_token': instance.refreshToken,
'profile_completed': instance.profileCompleted,
'profile_picture_url': instance.profilePicture,
};

View File

@ -11,10 +11,10 @@ class ApiService {
final _service = locator<DioService>();
// Register
Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic> data) async {
Future<Map<String, dynamic>> register(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kRegisterUrl',
'$baseUrl/$userUrl/$kRegisterUrl',
data: data,
);
@ -29,19 +29,19 @@ class ApiService {
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Email Login
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
// Login
Future<Map<String, dynamic>> login(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kLoginUrl',
'$baseUrl/$kLoginUrl',
data: data,
);
@ -57,38 +57,10 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Google login
Future<Map<String, dynamic>> googleAuth(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kGoogleAuthUrl',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Logged in successfully',
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
@ -97,14 +69,14 @@ class ApiService {
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kVerifyOtpUrl',
'$baseUrl/$userUrl/$kVerifyOtpUrl',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Otp verified successfully',
'data': UserModel.fromJson(response.data['data']),
//'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
@ -112,10 +84,10 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
@ -124,7 +96,7 @@ class ApiService {
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kResendOtpUrl',
'$baseUrl/$userUrl/$kResendOtpUrl',
data: data,
);
@ -139,65 +111,10 @@ class ApiService {
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Request reset code
Future<Map<String, dynamic>> requestResetCode(
Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kRequestResetCode',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Reset code sent successfully',
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Reset password
Future<Map<String, dynamic>> resetPassword(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kResetPassword',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Password reset successfully',
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
@ -206,7 +123,7 @@ class ApiService {
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
try {
Response response = await _service.dio.get(
'$kBaseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl',
'$baseUrl/$userUrl/${user?.userId}/$kProfileStatusUrl',
);
if (response.statusCode == 200) {
@ -221,101 +138,25 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
} catch (e) {
return {
'message': e.toString(),
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Get profile
Future<Map<String, dynamic>> getProfileData(int? userId) async {
try {
Response response = await _service.dio.get(
'$kBaseUrl/$kUserUrl/$kGetUserUrl/$userId',
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Profile fetched successfully',
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Complete profile
Future<Map<String, dynamic>> completeProfile(
Map<String, dynamic> data) async {
// Update profile
Future<Map<String, dynamic>> updateProfile(
{required UserModel? user, required Map<String, dynamic> data}) async {
try {
Response response = await _service.dio.put(
'$kBaseUrl/$kUserUrl',
'$baseUrl/$userUrl',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Profile updated successfully'
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Update profile image
Future<Map<String, dynamic>> updateProfileImage(
{required int? userId, required Map<String, dynamic> data}) async {
try {
late FormData formData;
if (data['profile_picture_url']
.toString()
.contains('com.example.yimaru_app/')) {
formData = FormData.fromMap({
'file': data['profile_picture_url'].toString().isNotEmpty
? MultipartFile.fromFileSync(
data['profile_picture_url'],
filename:
data['profile_picture_url'].toString().split('/').last,
)
: null,
});
} else {
formData = FormData.fromMap({
'file': data['profile_picture_url'].toString().isNotEmpty
? MultipartFile.fromFileSync(
data['profile_picture_url'],
filename:
data['profile_picture_url'].toString().split('/').last,
)
: null,
});
}
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$userId/$kUpdateProfileImage',
data: formData,
);
print(response.statusCode);
print(response.data);
if (response.statusCode == 200) {
return {
@ -328,10 +169,11 @@ class ApiService {
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
} catch (e) {
print('Exception ${e.toString()}');
return {
'message': e.toString(),
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
@ -342,7 +184,7 @@ class ApiService {
List<Assessment> assessments = [];
final Response response =
await _service.dio.get('$kBaseUrl/$kAssessmentsUrl');
await _service.dio.get('$baseUrl/$kAssessmentsUrl');
if (response.statusCode == 200) {
var data = response.data;

View File

@ -1,19 +1,10 @@
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
class AuthenticationService with ListenableServiceMixin {
class AuthenticationService {
final _secureService = locator<SecureStorageService>();
AuthenticationService() {
listenToReactiveValues([_user]);
}
UserModel? _user;
UserModel? get user => _user;
Future<bool> userLoggedIn() async {
if (await _secureService.getString('userId') != null) {
return true;
@ -37,122 +28,14 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('refreshToken', refresh);
}
Future<void> saveUserCredential(Map<String, dynamic> data) async {
Future<void> saveUserData(Map<String, dynamic> data) async {
await _secureService.setInt('userId', data['userId']);
await _secureService.setString('accessToken', data['accessToken']);
await _secureService.setString('refreshToken', data['refreshToken']);
_user = UserModel(
userId: await _secureService.getInt('userId'),
accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'),
);
}
Future<void> saveProfileStatus(bool value) async {
Future<void> saveProfileCompleted(bool value) async {
await _secureService.setBool('profileCompleted', value);
_user = UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
userId: _user?.userId,
country: _user?.country,
lastName: _user?.lastName,
birthday: _user?.birthday,
firstName: _user?.firstName,
occupation: _user?.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profilePicture: _user?.profilePicture,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profileCompleted: await _secureService.getBool('profileCompleted'));
notifyListeners();
}
Future<void> saveProfileImage(String image) async {
await _secureService.setString('profileImage', image);
_user = UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
userId: _user?.userId,
country: _user?.country,
lastName: _user?.lastName,
birthday: _user?.birthday,
firstName: _user?.firstName,
occupation: _user?.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profileImage'),
);
notifyListeners();
}
Future<void> saveUserData(
{required String image, required UserModel data}) async {
await _secureService.setBool('userInfoLoaded', true);
await _secureService.setBool(
'profileCompleted', data.profileCompleted ?? false);
await _secureService.setString('profilePicture', image);
await _secureService.setString('email', data.email ?? '');
await _secureService.setString('region', data.region ?? '');
await _secureService.setString('gender', data.gender ?? '');
await _secureService.setString('country', data.country ?? '');
await _secureService.setString('birthday', data.birthday ?? '');
await _secureService.setString('lastName', data.lastName ?? '');
await _secureService.setString('firstName', data.firstName ?? '');
await _secureService.setString('occupation', data.occupation ?? '');
_user = UserModel(
email: data.email,
gender: data.gender,
region: data.region,
userInfoLoaded: true,
profilePicture: image,
userId: _user?.userId,
country: data.country,
lastName: data.lastName,
birthday: data.birthday,
firstName: data.firstName,
occupation: data.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: data.profileCompleted,
);
notifyListeners();
}
Future<void> updateUserData(Map<String, dynamic> data) async {
await _secureService.setString('region', data['region']);
await _secureService.setString('gender', data['gender']);
await _secureService.setString('country', data['country']);
await _secureService.setString('lastName', data['last_name']);
await _secureService.setString('birthday', data['birth_day']);
await _secureService.setString('firstName', data['first_name']);
await _secureService.setString('occupation', data['occupation']);
_user = UserModel(
email: _user?.email,
userId: _user?.userId,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profilePicture: _user?.profilePicture,
profileCompleted: _user?.profileCompleted,
region: await _secureService.getString('region'),
gender: await _secureService.getString('gender'),
country: await _secureService.getString('country'),
lastName: await _secureService.getString('lastName'),
birthday: await _secureService.getString('birthday'),
firstName: await _secureService.getString('firstName'),
occupation: await _secureService.getString('occupation'),
);
notifyListeners();
}
Future<bool> isFirstTimeInstall() async =>
@ -162,29 +45,18 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setBool('firstTimeInstall', value);
}
Future<UserModel?> getUser() async {
_user = UserModel(
Future<UserModel> getUser() async {
UserModel user = UserModel(
userId: await _secureService.getInt('userId'),
email: await _secureService.getString('email'),
region: await _secureService.getString('region'),
gender: await _secureService.getString('gender'),
country: await _secureService.getString('country'),
lastName: await _secureService.getString('lastName'),
birthday: await _secureService.getString('birthday'),
firstName: await _secureService.getString('firstName'),
occupation: await _secureService.getString('occupation'),
accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'),
profilePicture: await _secureService.getString('profileImage'),
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
profileCompleted: await _secureService.getBool('profileCompleted'),
);
return _user;
return user;
}
Future<void> logOut() async {
bool firstTimeInstall = await isFirstTimeInstall();
_user = null;
await _secureService.clear();
await setFirstTimeInstall(firstTimeInstall);
}

View File

@ -4,6 +4,7 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
import '../app/app.locator.dart';
import '../ui/common/app_constants.dart';
@ -20,7 +21,7 @@ class DioService {
DioService() {
_dio.options
..baseUrl = kBaseUrl
..baseUrl = baseUrl
..connectTimeout = const Duration(seconds: 30)
..receiveTimeout = const Duration(seconds: 30);
@ -49,11 +50,10 @@ class DioService {
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final access = await _authenticationService.getAccessToken();
final refresh = await _authenticationService.getRefreshToken();
final token = await _authenticationService.getAccessToken();
if (access != null) {
options.headers['Authorization'] = 'Bearer $access';
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
options.headers['Accept'] = 'application/json';
@ -61,7 +61,6 @@ class DioService {
debugPrint('INITIALIZING REQUEST➡');
debugPrint('➡️ ${options.method} ${options.uri}');
debugPrint('➡️ REFRESH: $refresh');
debugPrint('➡️ HEADERS: ${options.headers}');
debugPrint('➡️ DATA: ${options.data}');
debugPrint('FINALIZING REQUEST➡');
@ -126,22 +125,33 @@ class DioService {
}
Future<bool> _refreshToken() async {
final UserModel? user = await _authenticationService.getUser();
final UserModel user = await _authenticationService.getUser();
if (user?.refreshToken == null) return false;
if (user.refreshToken == null) return false;
try {
Map<String, dynamic> data = {
'role': 'USER',
'user_id': user?.userId,
'access_token': user?.accessToken,
'refresh_token': user?.refreshToken
'role': 'STUDENT',
'user_id': user.userId,
'access_token': user.accessToken,
'refresh_token': user.refreshToken
};
print(data);
final response = await _refreshDio.post(
'$kBaseUrl/$kRefreshTokenUrl',
'$baseUrl/$kRefreshTokenUrl',
data: data,
options: Options(
followRedirects: false,
validateStatus: (status) => true,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
print('Refresh response');
print(response.data);
await _authenticationService.saveTokens(
access: response.data['access_token'],
refresh: response.data['refresh_token'],
@ -149,8 +159,10 @@ class DioService {
return true;
} catch (e) {
await _authenticationService.logOut();
await _navigationService.replaceWithLoginView();
print('Refresh response exception');
print(e.toString());
// await _authenticationService.logOut();
// await _navigationService.replaceWithLoginView();
return false;
}
}

View File

@ -1,21 +0,0 @@
import 'package:google_sign_in/google_sign_in.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
class GoogleAuthService {
final GoogleSignIn signIn = GoogleSignIn.instance;
Future<GoogleSignInAccount?> googleAuth() async {
try {
GoogleSignInAccount? googleUser;
await signIn.initialize(serverClientId: kServerClientId).then((_) async {
googleUser = await signIn.attemptLightweightAuthentication();
googleUser ??=
await signIn.authenticate(scopeHint: ['email', 'profile']);
});
return googleUser;
} catch (e) {
return null;
}
}
}

View File

@ -1,41 +0,0 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import '../app/app.locator.dart';
import '../ui/common/app_constants.dart';
import 'dio_service.dart';
class ImageDownloaderService {
final _service = locator<DioService>();
Future<String> downloader(String? networkImage) async {
late File image;
late String profileImage;
final Directory appDir = await getApplicationDocumentsDirectory();
if (networkImage != null) {
profileImage = networkImage.contains('https://lh3.googleusercontent.com')
? networkImage
: '$kBaseUrl$networkImage';
}
final Response profileImageResponse = await _service.dio.get(
profileImage,
options: Options(
followRedirects: false,
responseType: ResponseType.bytes,
),
);
final imageName = basename(networkImage ?? '');
final localImagePath = join(appDir.path, imageName);
image = File(localImagePath);
image.writeAsBytes(profileImageResponse.data);
return image.path;
}
}

View File

@ -1,56 +0,0 @@
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:yimaru_app/services/permission_handler_service.dart';
import '../app/app.locator.dart';
import '../ui/common/ui_helpers.dart';
class ImagePickerService {
final _permissionHandler = locator<PermissionHandlerService>();
final ImagePicker _picker = ImagePicker();
Future<String?> gallery() async {
try {
PermissionStatus status =
await _permissionHandler.requestPermission(Permission.mediaLibrary);
if (status == PermissionStatus.granted) {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.gallery, maxWidth: 600, maxHeight: 600);
if (pickedFile == null) {
showErrorToast('Please select a picture');
return null;
} else {
return pickedFile.path;
}
}
return null;
} catch (e) {
return null;
}
}
Future<String?> camera() async {
try {
PermissionStatus status =
await _permissionHandler.requestPermission(Permission.camera);
if (status == PermissionStatus.granted) {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.camera, maxWidth: 600, maxHeight: 600);
if (pickedFile == null) {
showErrorToast('Please take a picture');
return null;
} else {
return pickedFile.path;
}
}
return null;
} catch (e) {
return null;
}
}
}

View File

@ -1,31 +0,0 @@
import 'package:permission_handler/permission_handler.dart';
import '../ui/common/ui_helpers.dart';
class PermissionHandlerService {
Future<PermissionStatus> requestPermission(
Permission requestedPermission) async {
if (requestedPermission == Permission.camera) {
return await request(Permission.camera);
}
if (requestedPermission == Permission.storage) {
return await request(Permission.storage);
}
if (requestedPermission == Permission.mediaLibrary) {
return await request(Permission.mediaLibrary);
}
return PermissionStatus.denied;
}
Future<PermissionStatus> request(Permission permission) async {
if (await permission.isDenied) {
final PermissionStatus status = await permission.request();
if (status.isDenied || status.isPermanentlyDenied) {
showErrorToast('Permission Denied');
}
return status;
}
return PermissionStatus.granted;
}
}

View File

@ -5,7 +5,6 @@ const Color kcRed = Color(0xffFF4C4C);
const Color kcGreen = Color(0xFF1DE964);
const Color kcBackgroundColor = kcWhite;
const Color kcWhite = Color(0xFFFFFFFF);
const Color kcViolet = Color(0x336A1B9A);
const Color kcIndigo = Color(0xff6A1B9A);
const Color kcOrange = Color(0xFFF79400);
const Color kcSkyBlue = Color(0xFF28B4CD);
@ -14,6 +13,7 @@ const Color kcMediumGrey = Color(0xFF474A54);
const Color kcAquamarine = Color(0xFF1DE9B6);
const Color kcTransparent = Colors.transparent;
const Color kcPrimaryColor = Color(0xFF9E2891);
const Color kcPrimaryAccent = Color(0xFF6A1B9A);
const Color kcVeryLightGrey = Color(0xFFE3E3E3);
const Color kcPrimaryColorDark = Color(0xFF300151);
const Color kcPrimaryColorLight = Color(0x149E2891);

View File

@ -1,9 +1,7 @@
String kBaseUrl = 'http://195.35.29.82:8080';
String baseUrl = 'http://195.35.29.82:8080';
//String baseUrl = 'https://api.yimaru.yaltopia.com';
String kGetUserUrl = 'single';
String kUserUrl = 'api/v1/user';
String userUrl = 'api/v1/user';
String kRegisterUrl = 'register';
@ -11,24 +9,10 @@ String kVerifyOtpUrl = 'verify-otp';
String kResendOtpUrl = 'resend-otp';
String kResetPassword = 'resetPassword';
String kRequestResetCode = 'sendResetCode';
String kUpdateProfileImage = 'profile-picture';
String kRefreshTokenUrl = 'api/v1/auth/refresh';
String kLoginUrl = 'api/v1/auth/customer-login';
String kProfileStatusUrl = 'is-profile-completed';
String kGoogleAuthUrl = 'api/v1/auth/google/android';
String kAssessmentsUrl = 'api/v1/assessment/questions';
String kServerClientId =
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
String kSampleVideoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';

View File

@ -8,19 +8,3 @@ enum ProgressStatuses { pending, started, completed }
// Levels
enum ProficiencyLevels { a1, a2, b1, b2, none }
// State object
enum StateObjects {
verifyOtp,
resendOtp,
profileImage,
profileUpdate,
resetPassword,
loginWithEmail,
loginWithGoogle,
loadLessonVideo,
requestResetCode,
registerWithEmail,
profileCompletion,
registerWithGoogle,
}

View File

@ -1,5 +1,4 @@
import 'dart:math';
import 'package:chewie/chewie.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
@ -172,64 +171,30 @@ PinTheme errorPinTheme = defaultPin.copyBorderWith(
border: Border.all(color: Colors.red),
);
TextStyle style18P600 = const TextStyle(
fontSize: 18,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style18W600 = const TextStyle(
fontSize: 18,
color: kcWhite,
fontWeight: FontWeight.w600,
);
TextStyle style25W600 = const TextStyle(
fontSize: 25,
color: kcWhite,
fontWeight: FontWeight.w600,
);
TextStyle style12R700 = const TextStyle(
TextStyle validationStyle = const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
);
TextStyle style14P400 = const TextStyle(
color: kcPrimaryColor,
);
TextStyle style14P600 = const TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style25P600 = const TextStyle(
fontSize: 25,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style25DG600 = const TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
);
TextStyle style12R700 = const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
);
TextStyle style16DG600 = const TextStyle(
fontSize: 16,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
);
TextStyle style18DG500 = const TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w500,
);
TextStyle style18DG600 = const TextStyle(
fontSize: 18,
color: kcDarkGrey,
@ -241,29 +206,17 @@ TextStyle style16DG400 = const TextStyle(
color: kcDarkGrey,
);
TextStyle style14LG400 = const TextStyle(
color: kcLightGrey,
);
TextStyle style14MG400 = const TextStyle(
color: kcMediumGrey,
);
TextStyle style14DG500 =
const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500);
TextStyle style14DG400 = const TextStyle(
color: kcDarkGrey,
);
TextStyle style14DG600 = const TextStyle(
color: kcDarkGrey,
fontWeight: FontWeight.w600,
TextStyle style14P400 = const TextStyle(
color: kcPrimaryColor,
);
TextStyle validationStyle = const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
TextStyle style14P600 = const TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
@ -287,36 +240,27 @@ Map<String, Style> htmlStyle = {
),
};
ChewieProgressColors buildChewieProgressIndicator = ChewieProgressColors(
bufferedColor: kcIndigo,
playedColor: kcPrimaryColor,
backgroundColor: kcBackgroundColor,
);
Widget buildToastDescription(String message) => Text(
message,
maxLines: 4,
style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500),
style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500),
);
void showErrorToast(String message) {
toastification.show(
showIcon: true,
dragToClose: true,
primaryColor: kcRed,
showProgressBar: false,
applyBlurEffect: false,
alignment: Alignment.topCenter,
primaryColor: kcBackgroundColor,
icon: const Icon(Icons.check),
type: ToastificationType.success,
alignment: Alignment.bottomCenter,
style: ToastificationStyle.fillColored,
description: buildToastDescription(message),
autoCloseDuration: const Duration(seconds: 3),
borderSide: const BorderSide(color: kcWhite),
autoCloseDuration: const Duration(seconds: 5),
margin: const EdgeInsets.symmetric(horizontal: 15),
borderSide: const BorderSide(color: kcPrimaryColor),
icon: const Icon(
Icons.close,
color: kcPrimaryColor,
),
);
}
@ -326,17 +270,14 @@ void showSuccessToast(String message) {
dragToClose: true,
showProgressBar: false,
applyBlurEffect: false,
alignment: Alignment.topCenter,
primaryColor: kcBackgroundColor,
icon: const Icon(Icons.check),
primaryColor: kcPrimaryColor,
type: ToastificationType.success,
alignment: Alignment.bottomCenter,
style: ToastificationStyle.fillColored,
description: buildToastDescription(message),
autoCloseDuration: const Duration(seconds: 3),
borderSide: const BorderSide(color: kcWhite),
autoCloseDuration: const Duration(seconds: 5),
margin: const EdgeInsets.symmetric(horizontal: 15),
borderSide: const BorderSide(color: kcPrimaryColor),
icon: const Icon(
Icons.check,
color: kcPrimaryColor,
),
);
}

View File

@ -107,7 +107,11 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildHeader(String title) => Text(
title,
style: style18DG600,
style: const TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>

View File

@ -17,9 +17,9 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
const AssessmentView({Key? key, required this.data}) : super(key: key);
@override
void onViewModelReady(AssessmentViewModel viewModel) async {
void onViewModelReady(AssessmentViewModel viewModel) {
viewModel.getAssessments();
viewModel.initUserData(data);
await viewModel.getAssessments();
super.onViewModelReady(viewModel);
}
@ -39,12 +39,10 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
List<Widget> _buildScreens() => [
_buildAssessmentIntro(),
_buildAssessment(),
/*
_buildAssessmentFailure(),
_buildRetakeAssessment(),
_buildResultAnalysis(),
_buildAssessmentCompletion(),
*/
// _buildAssessmentFailure(),
// _buildRetakeAssessment(),
// _buildResultAnalysis(),
// _buildAssessmentCompletion(),
_buildAssessmentResult(),
_buildStartLesson(),
];

View File

@ -3,25 +3,21 @@ import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/models/option.dart';
import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/assessment.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../../services/authentication_service.dart';
import '../home/home_view.dart';
class AssessmentViewModel extends BaseViewModel {
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
// In-app navigation
int _currentPage = 0;
int get currentPage => _currentPage;
@ -35,6 +31,7 @@ class AssessmentViewModel extends BaseViewModel {
int get previousPage => _previousPage;
// Assessment
int _currentQuestion = 0;
int get currentQuestion => _currentQuestion;
@ -57,6 +54,7 @@ class AssessmentViewModel extends BaseViewModel {
Map<String, dynamic> get userData => _userData;
// Assessment
int countCorrectAnswersUntil(int untilQuestion) {
int count = 0;
@ -75,6 +73,9 @@ class AssessmentViewModel extends BaseViewModel {
if (_currentQuestion == 5) {
// A1
final correctCount = countCorrectAnswersUntil(5);
print('All : $_selectedAnswers');
print('Question page : $_currentQuestion');
print('Correct A1: $correctCount');
if (correctCount > 3) {
return {'continue': true, 'level': ProficiencyLevels.a1};
@ -85,6 +86,9 @@ class AssessmentViewModel extends BaseViewModel {
// A2
final correctCount = countCorrectAnswersUntil(10);
print('All : $_selectedAnswers');
print('Question page : $_currentQuestion');
print('Correct A2: $correctCount');
if (correctCount > 3) {
return {'continue': true, 'level': ProficiencyLevels.a2};
@ -94,6 +98,9 @@ class AssessmentViewModel extends BaseViewModel {
} else if (_currentQuestion == 16) {
// B1
final correctCount = countCorrectAnswersUntil(16);
print('All : $_selectedAnswers');
print('Question page : $_currentQuestion');
print('Correct B1: $correctCount');
if (correctCount > 4) {
return {'continue': true, 'level': ProficiencyLevels.b1};
@ -102,9 +109,12 @@ class AssessmentViewModel extends BaseViewModel {
}
} else if (_currentQuestion == 22) {
final correctCount = countCorrectAnswersUntil(16);
print('All : $_selectedAnswers');
print('Question page : $_currentQuestion');
print('Correct B2: $correctCount');
if (correctCount > 4) {
return {'continue': false, 'level': ProficiencyLevels.b2};
return {'continue': true, 'level': ProficiencyLevels.b2};
} else {
return {'continue': false, 'level': ProficiencyLevels.b2};
}
@ -113,20 +123,18 @@ class AssessmentViewModel extends BaseViewModel {
}
}
void setSelectedAnswer({required int question, required Option? option}) {
void setSelectedAnswer({required int question, required String option}) {
bool correct = false;
if (option?.isCorrect ?? false) {
final generator = Random();
int random = generator.nextInt(4);
if (option == _assessments[question - 1].options?[random].optionText) {
correct = true;
}
final data = {
question.toString(): {
'option': option,
'correct': correct,
'option': option?.optionText,
'answer': _assessments[question - 1]
.options
?.firstWhere((e) => e.isCorrect ?? false)
.optionText
'answer': _assessments[question - 1].options?[random].optionText
}
};
@ -139,6 +147,22 @@ class AssessmentViewModel extends BaseViewModel {
return _selectedAnswers[question.toString()]?['option'] == answer;
}
Future<void> getAssessments() async {
_assessments = await runBusyFuture<List<Assessment>>(_getAssessments());
}
Future<List<Assessment>> _getAssessments() async {
List<Assessment> response = await _apiService.getAssessments();
for (int i = 0; i < 6; i++) {
final generator = Random();
int random = generator.nextInt(15);
response.add(response[random]);
}
return response;
}
// Add user data
void initUserData(Map<String, dynamic> data) {
clearUserData();
@ -153,48 +177,38 @@ class AssessmentViewModel extends BaseViewModel {
_userData.clear();
}
// Dialog
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(
cancelTitle: 'No',
buttonTitle: 'Yes',
barrierDismissible: true,
title: 'Abort Assessment',
cancelTitleColor: kcDarkGrey,
buttonTitleColor: kcPrimaryColor,
description: 'Are you sure to abort the assessment ?',
);
return response?.confirmed;
// Complete profile
Future<void> completeProfile() async {
Map<String, dynamic> response =
await runBusyFuture<Map<String, dynamic>>(_completeProfile());
}
Future<void> abort() async {
bool? response = await showAbortDialog();
if (response != null && response) {
next(page: 3);
}
Future<Map<String, dynamic>> _completeProfile() async {
print(_userData);
UserModel user = await _authenticationService.getUser();
Map<String, dynamic> response =
await _apiService.updateProfile(data: _userData, user: user);
return response;
}
// Question navigation
// Navigation
void nextQuestion() {
_currentQuestion++;
Map<String, dynamic> response = evaluateAssessment();
if (_currentQuestion == _assessments.length) {
_proficiencyLevel = response['level'];
next();
} else {
if (response['level'] == ProficiencyLevels.none) {
_pageController.jumpToPage(_currentQuestion);
} else {
if (response['continue']) {
_pageController.jumpToPage(_currentQuestion);
} else {
}
{
_proficiencyLevel = response['level'];
next();
}
}
}
rebuildUi();
}
@ -204,6 +218,8 @@ class AssessmentViewModel extends BaseViewModel {
_pageController.previousPage(
duration: const Duration(microseconds: 100), curve: Curves.linear);
rebuildUi();
} else {
_navigationService.back();
}
}
@ -222,54 +238,15 @@ class AssessmentViewModel extends BaseViewModel {
}
void pop() {
if (_currentPage == 0 || _currentPage == 3 /*7*/) {
_navigationService.back();
} else if (_currentPage != 0 && _currentPage != 3) {
if (_currentPage != 0) {
_currentPage--;
rebuildUi();
}
}
// Navigation
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
// Remote api call
Future<void> getAssessments() async => await runBusyFuture(_getAssessments());
Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) {
List<Assessment> response = await _apiService.getAssessments();
/*
for (int i = 0; i < 6; i++) {
final generator = Random();
int random = generator.nextInt(15);
response.add(response[random]);
}
*/
_assessments = response;
}
}
// Complete profile
Future<void> completeProfile() async =>
await runBusyFuture(_completeProfile(),
busyObject: StateObjects.profileCompletion);
Future<void> _completeProfile() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.completeProfile(_userData);
if (response['status'] == ResponseStatus.success) {
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
}

View File

@ -61,23 +61,27 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
];
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/complete.svg',
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Assessment complete!',
style: style25DG600,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Were now analyzing your speaking skills',
textAlign: TextAlign.center,
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
@ -90,8 +94,8 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55,
borderRadius: 12,
text: 'View My Results',
foregroundColor: kcWhite,
onTap: () => viewModel.next(),
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
}

View File

@ -64,21 +64,25 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
];
Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg');
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'We didnt get enough from your assessment',
style: style25DG600,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
textAlign: TextAlign.center,
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
@ -113,9 +117,9 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55,
text: 'Skip',
borderRadius: 12,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
);
}

View File

@ -12,21 +12,18 @@ import 'assessment_loading_screen.dart';
class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
const AssessmentFormScreen({super.key});
//final PageController _pageController = PageController();
@override
Widget build(BuildContext context, AssessmentViewModel viewModel) =>
_buildAssessmentScreens(viewModel);
Widget _buildAssessmentScreens(AssessmentViewModel viewModel) =>
viewModel.isBusy || viewModel.assessments.isEmpty
? _buildPageLoadingIndicator(viewModel)
viewModel.isBusy
? _buildPageLoadingIndicator()
: _buildAssessmentScreensWrapper(viewModel);
Widget _buildPageLoadingIndicator(AssessmentViewModel viewModel) =>
AssessmentLoadingScreen(
isLoading: viewModel.isBusy,
isEmpty: viewModel.assessments.isEmpty,
onTap: () async => await viewModel.getAssessments(),
);
Widget _buildPageLoadingIndicator() => const AssessmentLoadingScreen();
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>
PopScope(
@ -48,10 +45,9 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
onClose: viewModel.abort,
showBackButton: true,
showLanguageSelection: false,
onPop: viewModel.previousQuestion,
showBackButton: viewModel.currentQuestion == 0 ? false : true,
);
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
@ -66,8 +62,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
controller: viewModel.pageController,
itemCount: viewModel.assessments.length,
itemBuilder: (cotext, index) =>
_buildBody(index: index, viewModel: viewModel),
);
_buildBody(index: index, viewModel: viewModel));
Widget _buildBody(
{required int index, required AssessmentViewModel viewModel}) =>
@ -110,7 +105,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildTitle(
{required int index, required AssessmentViewModel viewModel}) =>
Text(
'Q${index + 1}. ${viewModel.assessments[index].questionText} ',
'Q${index + 1}. ${viewModel.assessments[index].question?.title} ',
style: style16DG600,
);
@ -128,7 +123,8 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
''),
onTap: () => viewModel.setSelectedAnswer(
question: index + 1,
option: viewModel.assessments[index].options?[inner]),
option: viewModel.assessments[index].options?[inner].optionText ??
''),
),
);
@ -163,7 +159,11 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.selectedAnswers.containsKey(question.toString())
? () => viewModel.nextQuestion()
?
// viewModel.currentQuestion == viewModel.assessments.length - 1
// ? () => viewModel.next()
// :
() => viewModel.nextQuestion()
: null,
);
}

View File

@ -54,24 +54,27 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.pop,
showBackButton: false,
showLanguageSelection: true,
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Want a quick assessment to know your English level?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Answer a few quick questions to help us understand your English proficiency.',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
@ -91,8 +94,8 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
safe: false,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: () => viewModel.next(),
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);

View File

@ -3,14 +3,9 @@ import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
import '../../../common/app_colors.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/refresh_button.dart';
class AssessmentLoadingScreen extends StatelessWidget {
final bool isEmpty;
final bool isLoading;
final GestureTapCallback? onTap;
const AssessmentLoadingScreen(
{super.key, this.onTap, required this.isEmpty, required this.isLoading});
const AssessmentLoadingScreen({super.key});
@override
Widget build(BuildContext context) => _buildScaffoldWrapper();
@ -21,11 +16,7 @@ class AssessmentLoadingScreen extends StatelessWidget {
);
Widget _buildScaffold() => Stack(
children: [
_buildColumn(),
if (isEmpty) _buildRefreshButton(),
if (isLoading) _buildPageIndicator()
],
children: [_buildColumn(), _buildPageIndicator()],
);
Widget _buildColumn() => Column(
@ -43,6 +34,4 @@ class AssessmentLoadingScreen extends StatelessWidget {
Widget _buildBody() => Expanded(child: Container());
Widget _buildPageIndicator() => const PageLoadingIndicator();
Widget _buildRefreshButton() => RefreshButton(onTap: onTap);
}

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
@ -63,37 +62,35 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceLarge,
_buildTitle(viewModel),
verticalSpaceSmall,
_buildPrimarySubtitle(),
_buildPrimarySubTitle(),
verticalSpaceMedium,
_buildIconWrapper(viewModel),
_buildIcon(),
verticalSpaceMedium,
_buildSecondarySubtitle()
_buildSecondarySubTitle()
];
Widget _buildTitle(AssessmentViewModel viewModel) => Text(
'Youre likely a ${viewModel.proficiencyLevel.name.toUpperCase()} speaker!',
style: style25DG600,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 25,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
);
Widget _buildPrimarySubtitle() => Text(
Widget _buildPrimarySubTitle() => const Text(
'Great Job! Heres your next step to keep improving.',
textAlign: TextAlign.center,
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildIconWrapper(AssessmentViewModel viewModel) =>
viewModel.proficiencyLevel != ProficiencyLevels.none
? _buildIcon(viewModel)
: Container();
Widget _buildIcon() => SvgPicture.asset('assets/icons/b1.svg');
Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset(
'assets/icons/${viewModel.proficiencyLevel.name.substring(0, 1)}_${viewModel.proficiencyLevel.name.substring(1)}.svg');
Widget _buildSecondarySubtitle() => Text(
Widget _buildSecondarySubTitle() => const Text(
'Let\'s start your practice',
style: style14DG400,
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
@ -113,8 +110,8 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
safe: false,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: () => viewModel.next(),
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
@ -127,10 +124,10 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
text: 'Practice Speaking',
borderColor: kcPrimaryColor,
onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
);
}

View File

@ -48,7 +48,7 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
@ -61,15 +61,19 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
'assets/icons/progress_indicator.svg',
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Analyzing your results…',
style: style25DG600,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Were now analyzing your speaking skills',
style: style14MG400,
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
}

View File

@ -57,7 +57,7 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
@ -72,16 +72,20 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
color: kcPrimaryColor,
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'We didnt get enough from your assessment',
style: style25DG600,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
style: style14MG400,
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(

View File

@ -7,7 +7,6 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../../../common/enmus.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../assessment_viewmodel.dart';
class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
@ -16,6 +15,7 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
Future<void> _start(AssessmentViewModel viewModel) async {
if (viewModel.proficiencyLevel != ProficiencyLevels.none) {
Map<String, dynamic> data = {
'preferred_language': 'en',
'knowledge_level': viewModel.proficiencyLevel.name.toUpperCase()
};
@ -31,24 +31,20 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
body: _buildScaffold(viewModel),
);
Widget _buildScaffoldStack(AssessmentViewModel viewModel) =>
Stack(children: [_buildScaffold(viewModel), _buildState(viewModel)]);
Widget _buildScaffold(AssessmentViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
[_buildAppBar(), _buildExpandedBody(viewModel)];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true,
Widget _buildAppBar() => const LargeAppBar(
showBackButton: false,
showLanguageSelection: false,
);
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
@ -80,7 +76,7 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(viewModel),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
];
Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg');
@ -88,19 +84,26 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich(
TextSpan(
text: 'Welcome aboard',
style: style25DG600,
style: const TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
children: [
TextSpan(
style: style25DG600,
text: ', ${viewModel.userData['first_name']}!',
style: const TextStyle(
fontSize: 25,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
],
),
)
]),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Youre ready to explore your personalized lessons.',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
@ -111,15 +114,10 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildContinueButton(AssessmentViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Finish',
borderRadius: 12,
text: 'Go to My Lessons',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await _start(viewModel),
);
Widget _buildState(AssessmentViewModel viewModel) =>
viewModel.busy(StateObjects.profileCompletion)
? const PageLoadingIndicator()
: Container();
}

View File

@ -92,10 +92,14 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
Widget _buildIcon() =>
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Call our support team between 9 AM - 6 PM',
style: style25DG600,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitle(String title) => Text(

View File

@ -178,7 +178,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
verticalSpaceMedium,
_buildEmptyTitle(),
verticalSpaceSmall,
_buildEmptySubtitle(),
_buildEmptySubTitle(),
];
Widget _buildEmptyIcon() => const Icon(
@ -197,7 +197,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
),
);
Widget _buildEmptySubtitle() => const Text(
Widget _buildEmptySubTitle() => const Text(
'Start by exploring your learning materials and save them for offline access.',
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),

View File

@ -1,96 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import 'failure_viewmodel.dart';
class FailureView extends StackedView<FailureViewModel> {
final String label;
const FailureView({Key? key, required this.label}) : super(key: key);
@override
FailureViewModel viewModelBuilder(BuildContext context) => FailureViewModel();
@override
Widget builder(
BuildContext context,
FailureViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper();
Widget _buildScaffoldWrapper() => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(),
);
Widget _buildScaffold() => Stack(
children: _buildScaffoldChildren(),
);
List<Widget> _buildScaffoldChildren() => [
_buildBackground(),
_buildColumn(),
];
Widget _buildBackground() => Image.asset(
'assets/images/onboarding_1.png',
fit: BoxFit.fill,
width: double.maxFinite,
height: double.maxFinite,
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[_buildIconWrapper(), _buildSafeWrapper()];
Widget _buildSafeWrapper() => SafeArea(child: _buildLoadingTextContainer());
Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildLoadingTextWrapper(),
);
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
];
Widget _buildLoadingText() =>
Text('$label ...', style: const TextStyle(color: kcWhite, fontSize: 16));
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildIconWrapper() => Padding(
padding: const EdgeInsets.only(top: 100),
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
height: 50,
);
}

View File

@ -1,3 +0,0 @@
import 'package:stacked/stacked.dart';
class FailureViewModel extends BaseViewModel {}

View File

@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.form.dart';
import 'package:yimaru_app/ui/views/forget_password/screens/request_reset_code_screen.dart';
import 'package:yimaru_app/ui/views/forget_password/screens/reset_password_screen.dart';
import '../../common/app_colors.dart';
import '../../common/validators/form_validator.dart';
import '../../widgets/large_app_bar.dart';
import '../../widgets/page_loading_indicator.dart';
import 'forget_password_viewmodel.dart';
@FormView(fields: [
FormTextField(name: 'email', validator: FormValidator.validateEmail),
FormTextField(name: 'resetCode', validator: FormValidator.validateForm),
FormTextField(name: 'password', validator: FormValidator.validateForm),
FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm)
])
class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
with $ForgetPasswordView {
const ForgetPasswordView({Key? key}) : super(key: key);
void _initClearData() {
emailController.clear();
passwordController.clear();
resetCodeController.clear();
confirmPasswordController.clear();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
if (viewModel.currentPage == 0) {
emailController.clear();
viewModel.resetRequestResetCodeScreen();
} else {
passwordController.clear();
resetCodeController.clear();
confirmPasswordController.clear();
viewModel.resetResetPasswordScreen();
}
}
void _pop({required bool value, required ForgetPasswordViewModel viewModel}) {
{
if (!value) return;
_clearDataOnNavigation(viewModel);
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
}
}
@override
void onViewModelReady(ForgetPasswordViewModel viewModel) {
_initClearData();
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@override
ForgetPasswordViewModel viewModelBuilder(BuildContext context) =>
ForgetPasswordViewModel();
@override
Widget builder(
BuildContext context,
ForgetPasswordViewModel viewModel,
Widget? child,
) =>
_buildLoginScreensWrapper(viewModel);
Widget _buildLoginScreensWrapper(ForgetPasswordViewModel viewModel) =>
PopScope(
canPop: true,
onPopInvokedWithResult: (value, data) =>
_pop(value: value, viewModel: viewModel),
child: _buildBody(viewModel));
Widget _buildBody(ForgetPasswordViewModel viewModel) =>
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildRequestCodeScreen(),
_buildResetPasswordScreen(),
];
Widget _buildRequestCodeScreen() =>
RequestCodeScreen(emailController: emailController);
Widget _buildResetPasswordScreen() => ResetPasswordScreen(
passwordController: passwordController,
resetCodeController: resetCodeController,
confirmPasswordController: confirmPasswordController);
}

View File

@ -1,281 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// StackedFormGenerator
// **************************************************************************
// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String EmailValueKey = 'email';
const String ResetCodeValueKey = 'resetCode';
const String PasswordValueKey = 'password';
const String ConfirmPasswordValueKey = 'confirmPassword';
final Map<String, TextEditingController>
_ForgetPasswordViewTextEditingControllers = {};
final Map<String, FocusNode> _ForgetPasswordViewFocusNodes = {};
final Map<String, String? Function(String?)?>
_ForgetPasswordViewTextValidations = {
EmailValueKey: FormValidator.validateEmail,
ResetCodeValueKey: FormValidator.validateForm,
PasswordValueKey: FormValidator.validateForm,
ConfirmPasswordValueKey: FormValidator.validateForm,
};
mixin $ForgetPasswordView {
TextEditingController get emailController =>
_getFormTextEditingController(EmailValueKey);
TextEditingController get resetCodeController =>
_getFormTextEditingController(ResetCodeValueKey);
TextEditingController get passwordController =>
_getFormTextEditingController(PasswordValueKey);
TextEditingController get confirmPasswordController =>
_getFormTextEditingController(ConfirmPasswordValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get resetCodeFocusNode => _getFormFocusNode(ResetCodeValueKey);
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
FocusNode get confirmPasswordFocusNode =>
_getFormFocusNode(ConfirmPasswordValueKey);
TextEditingController _getFormTextEditingController(
String key, {
String? initialValue,
}) {
if (_ForgetPasswordViewTextEditingControllers.containsKey(key)) {
return _ForgetPasswordViewTextEditingControllers[key]!;
}
_ForgetPasswordViewTextEditingControllers[key] =
TextEditingController(text: initialValue);
return _ForgetPasswordViewTextEditingControllers[key]!;
}
FocusNode _getFormFocusNode(String key) {
if (_ForgetPasswordViewFocusNodes.containsKey(key)) {
return _ForgetPasswordViewFocusNodes[key]!;
}
_ForgetPasswordViewFocusNodes[key] = FocusNode();
return _ForgetPasswordViewFocusNodes[key]!;
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
emailController.addListener(() => _updateFormData(model));
resetCodeController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
confirmPasswordController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
@Deprecated(
'Use syncFormWithViewModel instead.'
'This feature was deprecated after 3.1.0.',
)
void listenToFormUpdated(FormViewModel model) {
emailController.addListener(() => _updateFormData(model));
resetCodeController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
confirmPasswordController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Updates the formData on the FormViewModel
void _updateFormData(FormStateHelper model, {bool forceValidate = false}) {
model.setData(
model.formValueMap
..addAll({
EmailValueKey: emailController.text,
ResetCodeValueKey: resetCodeController.text,
PasswordValueKey: passwordController.text,
ConfirmPasswordValueKey: confirmPasswordController.text,
}),
);
if (_autoTextFieldValidation || forceValidate) {
updateValidationData(model);
}
}
bool validateFormFields(FormViewModel model) {
_updateFormData(model, forceValidate: true);
return model.isFormValid;
}
/// Calls dispose on all the generated controllers and focus nodes
void disposeForm() {
// The dispose function for a TextEditingController sets all listeners to null
for (var controller in _ForgetPasswordViewTextEditingControllers.values) {
controller.dispose();
}
for (var focusNode in _ForgetPasswordViewFocusNodes.values) {
focusNode.dispose();
}
_ForgetPasswordViewTextEditingControllers.clear();
_ForgetPasswordViewFocusNodes.clear();
}
}
extension ValueProperties on FormStateHelper {
bool get hasAnyValidationMessage => this
.fieldsValidationMessages
.values
.any((validation) => validation != null);
bool get isFormValid {
if (!_autoTextFieldValidation) this.validateForm();
return !hasAnyValidationMessage;
}
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
String? get resetCodeValue => this.formValueMap[ResetCodeValueKey] as String?;
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
String? get confirmPasswordValue =>
this.formValueMap[ConfirmPasswordValueKey] as String?;
set emailValue(String? value) {
this.setData(
this.formValueMap..addAll({EmailValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(EmailValueKey)) {
_ForgetPasswordViewTextEditingControllers[EmailValueKey]?.text =
value ?? '';
}
}
set resetCodeValue(String? value) {
this.setData(
this.formValueMap..addAll({ResetCodeValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(
ResetCodeValueKey)) {
_ForgetPasswordViewTextEditingControllers[ResetCodeValueKey]?.text =
value ?? '';
}
}
set passwordValue(String? value) {
this.setData(
this.formValueMap..addAll({PasswordValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(
PasswordValueKey)) {
_ForgetPasswordViewTextEditingControllers[PasswordValueKey]?.text =
value ?? '';
}
}
set confirmPasswordValue(String? value) {
this.setData(
this.formValueMap..addAll({ConfirmPasswordValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(
ConfirmPasswordValueKey)) {
_ForgetPasswordViewTextEditingControllers[ConfirmPasswordValueKey]?.text =
value ?? '';
}
}
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
bool get hasResetCode =>
this.formValueMap.containsKey(ResetCodeValueKey) &&
(resetCodeValue?.isNotEmpty ?? false);
bool get hasPassword =>
this.formValueMap.containsKey(PasswordValueKey) &&
(passwordValue?.isNotEmpty ?? false);
bool get hasConfirmPassword =>
this.formValueMap.containsKey(ConfirmPasswordValueKey) &&
(confirmPasswordValue?.isNotEmpty ?? false);
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
bool get hasResetCodeValidationMessage =>
this.fieldsValidationMessages[ResetCodeValueKey]?.isNotEmpty ?? false;
bool get hasPasswordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
bool get hasConfirmPasswordValidationMessage =>
this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ??
false;
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
String? get resetCodeValidationMessage =>
this.fieldsValidationMessages[ResetCodeValueKey];
String? get passwordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey];
String? get confirmPasswordValidationMessage =>
this.fieldsValidationMessages[ConfirmPasswordValueKey];
}
extension Methods on FormStateHelper {
setEmailValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
setResetCodeValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ResetCodeValueKey] = validationMessage;
setPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
setConfirmPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ConfirmPasswordValueKey] =
validationMessage;
/// Clears text input fields on the Form
void clearForm() {
emailValue = '';
resetCodeValue = '';
passwordValue = '';
confirmPasswordValue = '';
}
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
EmailValueKey: getValidationMessage(EmailValueKey),
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
});
}
}
/// Returns the validation message for the given key
String? getValidationMessage(String key) {
final validatorForKey = _ForgetPasswordViewTextValidations[key];
if (validatorForKey == null) return null;
String? validationMessageForKey = validatorForKey(
_ForgetPasswordViewTextEditingControllers[key]!.text,
);
return validationMessageForKey;
}
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
EmailValueKey: getValidationMessage(EmailValueKey),
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
});

View File

@ -1,231 +0,0 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/ui/views/login/login_view.dart';
import '../../../app/app.locator.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
class ForgetPasswordViewModel extends FormViewModel {
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// User data
final Map<String, dynamic> _userData = {};
Map<String, dynamic> get userData => _userData;
// Navigation
int _currentPage = 0;
int get currentPage => _currentPage;
// Email
bool _focusEmail = false;
bool get focusEmail => _focusEmail;
// Reset code
bool _focusResetCode = false;
bool get focusResetCode => _focusResetCode;
// Password
bool _length = false;
bool get length => _length;
bool _number = false;
bool get number => _number;
bool _specialChar = false;
bool get specialChar => _specialChar;
bool _focusPassword = false;
bool get focusPassword => _focusPassword;
bool _obscurePassword = true;
bool get obscurePassword => _obscurePassword;
bool _passwordMatch = false;
bool get passwordMatch => _passwordMatch;
// Confirm password
bool _focusConfirmPassword = false;
bool get focusConfirmPassword => _focusConfirmPassword;
bool _obscureConfirmPassword = true;
bool get obscureConfirmPassword => _obscureConfirmPassword;
// Add user data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void clearUserData() {
_userData.clear();
}
// Email
void setEmailFocus() {
_focusEmail = true;
rebuildUi();
}
// Reset code
void setResetCodeFocus() {
_focusResetCode = true;
rebuildUi();
}
// Password
void setPasswordFocus() {
_focusPassword = true;
rebuildUi();
}
void validatePassword(
{required String password, required String confirmPassword}) {
if (password.length > 8) {
_length = true;
} else {
_length = false;
}
if (RegExp(r'\d').hasMatch(password)) {
_number = true;
} else {
_number = false;
}
if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
_specialChar = true;
} else {
_specialChar = false;
}
if (password == confirmPassword) {
_passwordMatch = true;
} else {
_passwordMatch = false;
}
rebuildUi();
}
double validationProgress() {
int completed = 0;
if (_length) completed++;
if (_number) completed++;
if (_specialChar) completed++;
if (_passwordMatch) completed++;
return completed / 4; // returns 0.0 1.0
}
void setObscurePassword() {
_obscurePassword = !_obscurePassword;
rebuildUi();
}
// Confirm password
void setConfirmPasswordFocus() {
_focusConfirmPassword = true;
rebuildUi();
}
void setObscureConfirmPassword() {
_obscureConfirmPassword = !_obscureConfirmPassword;
rebuildUi();
}
// Form reset
// Reset reset password screen
void resetResetPasswordScreen() {
_length = false;
_number = false;
_specialChar = false;
_passwordMatch = false;
_focusPassword = false;
_focusResetCode = false;
_focusConfirmPassword = false;
rebuildUi();
}
// Reset reset password screen
void resetRequestResetCodeScreen() {
_focusEmail = false;
rebuildUi();
}
// In-app navigation
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
void goBack() {
if (_currentPage == 1) {
_currentPage = 0;
rebuildUi();
} else {
_navigationService.back();
}
}
// Navigation
void pop() => _navigationService.back();
Future<void> replaceWithLogin() async =>
await _navigationService.clearStackAndShowView(const LoginView());
// Remote api calls
// Request reset code
Future<void> requestResetCode() async =>
await runBusyFuture(_requestResetCode(),
busyObject: StateObjects.requestResetCode);
Future<void> _requestResetCode() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.requestResetCode(_userData);
if (response['status'] == ResponseStatus.success) {
goTo(1);
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
// Request reset code
Future<void> resetPassword() async => await runBusyFuture(_resetPassword(),
busyObject: StateObjects.resetPassword);
Future<void> _resetPassword() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.resetPassword(_userData);
if (response['status'] == ResponseStatus.success) {
showSuccessToast(response['message']);
await replaceWithLogin();
} else {
showErrorToast(response['message']);
}
}
}
}

View File

@ -1,190 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/login_account.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/option_text_divider.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../forget_password_view.form.dart';
class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
final TextEditingController emailController;
const RequestCodeScreen({
super.key,
required this.emailController,
});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 375 - half,);
}
void _inAppPop(ForgetPasswordViewModel viewModel) {
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
emailController.clear();
viewModel.resetRequestResetCodeScreen();
}
Future<void> _addUserData(ForgetPasswordViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'email': emailController.text,
};
viewModel.addUserData(data);
await viewModel.requestResetCode();
}
@override
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Stack(
children: [
_buildScaffold(context: context,viewModel: viewModel),
_buildRequestResetCodeState(viewModel),
],
);
Widget _buildScaffold( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context, viewModel: viewModel)];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
Widget _buildExpandedBody( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context, viewModel: viewModel),
);
Widget _buildBodyWrapper( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context, viewModel: viewModel),
);
Widget _buildBody( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(ForgetPasswordViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(ForgetPasswordViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubtitle(),
verticalSpaceLarge,
_buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
verticalSpaceTiny,
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
_buildEmailValidatorWrapper(viewModel),
];
Widget _buildTitle() => Text(
'Reset password',
style: style25DG600,
);
Widget _buildSubtitle() => Text(
'Enter your email, we will send you a reset code.',
style: style14DG400,
);
Widget _buildEmailFormField(ForgetPasswordViewModel viewModel) =>
TextFormField(
controller: emailController,
onTap: viewModel.setEmailFocus,
keyboardType: TextInputType.emailAddress,
decoration: inputDecoration(
hint: 'Email',
focus: viewModel.focusEmail,
filled: emailController.text.isNotEmpty),
);
Widget _buildEmailValidatorWrapper(ForgetPasswordViewModel viewModel) =>
viewModel.hasEmailValidationMessage
? _buildEmailValidator(viewModel)
: Container();
Widget _buildEmailValidator(ForgetPasswordViewModel viewModel) => Text(
viewModel.emailValidationMessage!,
style: style12R700,
);
Widget _buildContinueButtonWrapper(ForgetPasswordViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(ForgetPasswordViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: emailController.text.isNotEmpty &&
!viewModel.hasEmailValidationMessage
? () => _addUserData(viewModel)
: null,
backgroundColor: emailController.text.isNotEmpty &&
!viewModel.hasEmailValidationMessage
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildRequestResetCodeState(ForgetPasswordViewModel viewModel) =>
viewModel.busy(StateObjects.requestResetCode)
? const PageLoadingIndicator()
: Container();
}

View File

@ -1,306 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/custom_form_label.dart';
import '../../../widgets/custom_linear_progress_indicator.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/obscure_password.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../../../widgets/validator_list_tile.dart';
import '../forget_password_viewmodel.dart';
import '../forget_password_view.form.dart';
class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
final TextEditingController resetCodeController;
final TextEditingController passwordController;
final TextEditingController confirmPasswordController;
const ResetPasswordScreen(
{super.key,
required this.resetCodeController,
required this.passwordController,
required this.confirmPasswordController});
void _inAppPop(ForgetPasswordViewModel viewModel) {
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
passwordController.clear();
resetCodeController.clear();
confirmPasswordController.clear();
viewModel.resetResetPasswordScreen();
}
Future<void> _reset(ForgetPasswordViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'otp': resetCodeController.text,
'password': passwordController.text,
};
viewModel.addUserData(data);
await viewModel.resetPassword();
}
@override
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Stack(
children: [
_buildScaffold(viewModel),
_buildResetPasswordState(viewModel),
],
);
Widget _buildScaffold( ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren( ForgetPasswordViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
Widget _buildExpandedBody( ForgetPasswordViewModel viewModel) =>
Expanded(child: _buildColumnScroller(viewModel));
Widget _buildColumnScroller( ForgetPasswordViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper( ForgetPasswordViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(ForgetPasswordViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildFormLabel('Reset code'),
verticalSpaceSmall,
_buildResetCodeFormField(viewModel),
if (viewModel.hasResetCodeValidationMessage && viewModel.focusResetCode)
verticalSpaceTiny,
if (viewModel.hasResetCodeValidationMessage && viewModel.focusResetCode)
_buildResetCodeValidationWrapper(viewModel),
verticalSpaceMedium,
_buildFormLabel('New Password'),
verticalSpaceSmall,
_buildPasswordFormField(viewModel),
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
verticalSpaceTiny,
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
_buildPasswordValidationWrapper(viewModel),
verticalSpaceMedium,
_buildFormLabel('Confirm Password'),
verticalSpaceSmall,
_buildConfirmPasswordFormField(viewModel),
if (viewModel.hasConfirmPasswordValidationMessage &&
viewModel.focusConfirmPassword)
verticalSpaceTiny,
if (viewModel.hasConfirmPasswordValidationMessage &&
viewModel.focusConfirmPassword)
_buildConfirmPasswordValidationWrapper(viewModel),
verticalSpaceMedium,
_buildLinearProgressIndicator(viewModel),
verticalSpaceSmall,
_buildCharLengthValidator(viewModel),
_buildNumberValidator(viewModel),
_buildSymbolValidator(viewModel),
_buildPasswordMatchValidator(viewModel),
verticalSpaceSmall,
_buildSignUpButton(viewModel),
verticalSpaceMedium
];
Widget _buildTitle() => Text(
'Reset password',
style: style25DG600,
);
Widget _buildFormLabel(String label) => CustomFormLabel(
label: label,
style: style14DG400,
);
Widget _buildResetCodeFormField(ForgetPasswordViewModel viewModel) =>
TextFormField(
controller: resetCodeController,
onTap: viewModel.setResetCodeFocus,
decoration: inputDecoration(
hint: 'Reset code',
focus: viewModel.focusResetCode,
filled: passwordController.text.isNotEmpty),
);
Widget _buildResetCodeValidationWrapper(ForgetPasswordViewModel viewModel) =>
viewModel.hasResetCodeValidationMessage
? _buildResetCodeValidator(viewModel)
: Container();
Widget _buildResetCodeValidator(ForgetPasswordViewModel viewModel) => Text(
viewModel.resetCodeValidationMessage!,
style: style12R700,
);
Widget _buildPasswordFormField(ForgetPasswordViewModel viewModel) =>
TextFormField(
controller: passwordController,
onTap: viewModel.setPasswordFocus,
obscureText: viewModel.obscurePassword,
decoration: inputDecoration(
hint: 'Password',
focus: viewModel.focusPassword,
suffix: _buildObscurePassword(viewModel),
filled: passwordController.text.isNotEmpty),
onChanged: (value) => viewModel.validatePassword(
password: passwordController.text,
confirmPassword: confirmPasswordController.text),
);
Widget _buildObscurePassword(ForgetPasswordViewModel viewModel) =>
ObscurePassword(
focus: viewModel.focusPassword,
obscure: viewModel.obscurePassword,
onTap: viewModel.setObscurePassword,
);
Widget _buildPasswordValidationWrapper(ForgetPasswordViewModel viewModel) =>
viewModel.hasPasswordValidationMessage
? _buildPasswordValidator(viewModel)
: Container();
Widget _buildPasswordValidator(ForgetPasswordViewModel viewModel) => Text(
viewModel.passwordValidationMessage!,
style: style12R700,
);
Widget _buildConfirmPasswordFormField(ForgetPasswordViewModel viewModel) =>
TextFormField(
controller: confirmPasswordController,
onTap: viewModel.setConfirmPasswordFocus,
obscureText: viewModel.obscureConfirmPassword,
onChanged: (value) => viewModel.validatePassword(
password: passwordController.text,
confirmPassword: confirmPasswordController.text),
decoration: inputDecoration(
hint: 'Confirm Password',
focus: viewModel.focusConfirmPassword,
suffix: _buildObscureConfirmPassword(viewModel),
filled: confirmPasswordController.text.isNotEmpty),
);
Widget _buildObscureConfirmPassword(ForgetPasswordViewModel viewModel) =>
ObscurePassword(
focus: viewModel.focusConfirmPassword,
obscure: viewModel.obscureConfirmPassword,
onTap: viewModel.setObscureConfirmPassword,
);
Widget _buildConfirmPasswordValidationWrapper(
ForgetPasswordViewModel viewModel) =>
viewModel.hasConfirmPasswordValidationMessage
? _buildConfirmPasswordValidator(viewModel)
: Container();
Widget _buildConfirmPasswordValidator(ForgetPasswordViewModel viewModel) =>
Text(
viewModel.confirmPasswordValidationMessage!,
style: style12R700,
);
Widget _buildLinearProgressIndicator(ForgetPasswordViewModel viewModel) =>
CustomLinearProgressIndicator(
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
progress: viewModel.validationProgress(),
);
Widget _buildCharLengthValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey,
label: '8 characters minimum');
Widget _buildNumberValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey,
label: 'a number');
Widget _buildSymbolValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey,
label: 'one symbol minimum');
Widget _buildPasswordMatchValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor:
viewModel.passwordMatch ? kcPrimaryColor : kcLightGrey,
label: 'password match');
Widget _buildSignUpButton(ForgetPasswordViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: passwordController.text.isNotEmpty &&
confirmPasswordController.text.isNotEmpty &&
resetCodeController.text.isNotEmpty &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch
? () async => await _reset(viewModel)
: null,
backgroundColor: passwordController.text.isNotEmpty &&
confirmPasswordController.text.isNotEmpty &&
resetCodeController.text.isNotEmpty &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildResetPasswordState(ForgetPasswordViewModel viewModel) =>
viewModel.busy(StateObjects.resetPassword)
? const PageLoadingIndicator()
: Container();
}

View File

@ -15,9 +15,8 @@ class HomeView extends StackedView<HomeViewModel> {
HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel();
@override
void onViewModelReady(HomeViewModel viewModel) async {
await viewModel.getProfileStatus();
await viewModel.getProfileData();
void onViewModelReady(HomeViewModel viewModel) {
viewModel.getProfileStatus();
super.onViewModelReady(viewModel);
}
@ -27,7 +26,9 @@ class HomeView extends StackedView<HomeViewModel> {
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy
? const StartupView(label: 'Checking user info')
? const StartupView(
label: 'Checking user info',
)
: _buildScaffold(viewModel);
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(

View File

@ -7,30 +7,18 @@ import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/common/app_strings.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/image_downloader_service.dart';
import '../../common/enmus.dart';
class HomeViewModel extends ReactiveViewModel {
class HomeViewModel extends BaseViewModel {
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _bottomSheetService = locator<BottomSheetService>();
final _authenticationService = locator<AuthenticationService>();
final _imageDownloaderService = locator<ImageDownloaderService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
// Bottom navigation
int _currentIndex = 0;
@ -58,77 +46,33 @@ class HomeViewModel extends ReactiveViewModel {
);
}
Future<void> saveProfileStatus(bool value) async =>
await _authenticationService.saveProfileStatus(value);
// Navigation
Future<void> replaceWithFailure() async =>
await _navigationService.clearStackAndShowView(
const FailureView(label: 'Check your internet connection to proceed'),
);
Future<void> replaceWithOnboarding() async =>
await _navigationService.replaceWithOnboardingView();
// Remote api calls
Future<void> getProfileStatus() async {
Map<String, dynamic> response =
await runBusyFuture<Map<String, dynamic>>(_getProfileStatus());
if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding();
}
}
// Profile data
Future<void> getProfileData() async => await runBusyFuture(_getProfileData());
Future<void> _getProfileData() async {
print('RESPONSE FOR USER DATA ${_user?.firstName}');
if (!(_user?.userInfoLoaded ?? false)) {
print('RESPONSE FOR USER DATA 1');
if (await _statusChecker.checkConnection()) {
Future<Map<String, dynamic>> _getProfileStatus() async {
Map<String, dynamic> response = {};
UserModel user = await _authenticationService.getUser();
if (_user?.profileCompleted != null &&
(_user?.profileCompleted ?? false)) {
if (user.profileCompleted == null) {
if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileData(_user?.userId);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
String image =
await _imageDownloaderService.downloader(user.profilePicture);
await _authenticationService.saveUserData(
image: image, data: user);
}
}
}
}
}
}
// Profile status
Future<void> getProfileStatus() async =>
await runBusyFuture(_getProfileStatus());
Future<void> _getProfileStatus() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = {};
if (_user?.profileCompleted == null) {
if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileStatus(_user);
response = await _apiService.getProfileStatus(user);
} else {
await replaceWithFailure();
}
} else if (!(_user?.profileCompleted ?? false)) {
response = {'data': false, 'status': ResponseStatus.success};
}
} else {
response = {'data': true, 'status': ResponseStatus.success};
}
if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding();
} else if (response['status'] == ResponseStatus.success &&
response['data']) {
await saveProfileStatus(response['data']);
}
}
return response;
}
}

View File

@ -61,7 +61,7 @@ class LanguageView extends StackedView<LanguageViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceMedium,
_buildLanguages(viewModel)
];
@ -72,18 +72,22 @@ class LanguageView extends StackedView<LanguageViewModel> {
);
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
title: 'Language Preference',
onTap: viewModel.pop,
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Choose your language',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'You can switch languages anytime',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder(

View File

@ -38,15 +38,12 @@ class LearnView extends StackedView<LearnViewModel> {
Widget _buildColumn(LearnViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildAppBar(),
_buildLevelsColumnWrapper(viewModel)
],
);
Widget _buildAppBar(LearnViewModel viewModel) => LearnAppBar(
name: viewModel.user?.firstName,
profileImage: viewModel.user?.profilePicture,
);
Widget _buildAppBar() => const LearnAppBar();
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));

View File

@ -1,22 +1,12 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
class LearnViewModel extends ReactiveViewModel {
class LearnViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get user => _authenticationService.user;
final List<Map<String, dynamic>> _learnLevels = [
{

View File

@ -4,6 +4,7 @@ import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/learn_lesson_tile.dart';
import 'package:yimaru_app/ui/widgets/module_progress.dart';
import 'package:yimaru_app/ui/widgets/motivation_card.dart';
import 'package:yimaru_app/ui/widgets/overall_module_progress.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
@ -119,22 +120,17 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
itemBuilder: (context, index) => _buildTile(
title: viewModel.lessons[index]['title'],
status: viewModel.lessons[index]['status'],
thumbnail: viewModel.lessons[index]['thumbnail'],
onLessonTap: () async =>
await viewModel.navigateToLearnLessonDetail(),
),
thumbnail: viewModel.lessons[index]['thumbnail']),
);
Widget _buildTile({
required String title,
required String thumbnail,
GestureTapCallback? onLessonTap,
required ProgressStatuses status,
}) =>
LearnLessonTile(
title: title,
status: status,
thumbnail: thumbnail,
onLessonTap: onLessonTap,
);
}

View File

@ -1,6 +1,5 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
@ -9,6 +8,7 @@ class LearnLessonViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Lessons
// Downloads
final List<Map<String, dynamic>> _lessons = [
{
'title': '1.1 Introducing Yourself',
@ -31,7 +31,4 @@ class LearnLessonViewModel extends BaseViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnLessonDetail() async =>
await _navigationService.navigateToLearnLessonDetailView();
}

View File

@ -1,178 +0,0 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/empty_video_player.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_lesson_detail_viewmodel.dart';
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
const LearnLessonDetailView({Key? key}) : super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel)async{
await viewModel.pause();
await viewModel.navigateToLearnPractice();
}
// @override
// void onDispose(LearnLessonDetailViewModel viewModel) {
// print('DISPOSED');
// viewModel.dispose();
// super.onDispose(viewModel);
// }
@override
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
await viewModel.initializePlayer();
super.onViewModelReady(viewModel);
}
@override
LearnLessonDetailViewModel viewModelBuilder(BuildContext context) =>
LearnLessonDetailViewModel();
@override
Widget builder(
BuildContext context,
LearnLessonDetailViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnLessonDetailViewModel viewModel) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnLessonDetailViewModel viewModel) =>
SafeArea(child: _buildColumn(viewModel));
Widget _buildColumn(LearnLessonDetailViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
_buildBodyColumnWrapper(viewModel),
],
);
Widget _buildAppBarWrapper(LearnLessonDetailViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppBar(viewModel));
Widget _buildAppBar(LearnLessonDetailViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
);
Widget _buildBodyColumnWrapper(LearnLessonDetailViewModel viewModel) =>
Expanded(
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(LearnLessonDetailViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnLessonDetailViewModel viewModel) =>
[
_buildLevelsColumnWrapper(viewModel),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildLevelsColumnWrapper(LearnLessonDetailViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(LearnLessonDetailViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(LearnLessonDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(
LearnLessonDetailViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitleWrapper(),
verticalSpaceLarge,
_buildVideoPlayerWrapper(viewModel),
verticalSpaceMedium,
_buildDescriptionWrapper(),
];
Widget _buildTitleWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildTitle(),
);
Widget _buildTitle() => Text(
'1.3 Common Greetings',
style: style16DG600,
);
Widget _buildVideoPlayerWrapper(LearnLessonDetailViewModel viewModel) =>
Container(
height: 200,
color: kcBlack,
width: double.maxFinite,
child: _buildVideoPlayerState(viewModel),
);
Widget _buildVideoPlayerState(LearnLessonDetailViewModel viewModel) =>
viewModel.chewieController != null &&
viewModel.videoPlayerController != null &&
!viewModel.busy(StateObjects.loadLessonVideo)
? _buildVideoPlayer(viewModel)
: _buildEmptyVideoPlayer();
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
_buildChewiePlayer(viewModel);
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
Chewie(controller: viewModel.chewieController!);
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
Widget _buildDescriptionWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildDescription(),
);
Widget _buildDescription() => Text(
'In this lesson, youll explore how to start simple conversations by greeting others in polite and friendly ways. Youll practice different greetings for morning, afternoon, and evening, as well as casual and formal situations. By the end, youll know how to confidently say hello, ask how someone is, and respond naturally.',
style: style14DG600,
);
Widget _buildContinueButtonWrapper(LearnLessonDetailViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(
left: 15,
right: 15,
bottom: 50,
),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LearnLessonDetailViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Practice',
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: ()async => await _navigate(viewModel),
);
}

View File

@ -1,72 +0,0 @@
import 'package:chewie/chewie.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:video_player/video_player.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../../../app/app.locator.dart';
import '../../../services/status_checker_service.dart';
class LearnLessonDetailViewModel extends BaseViewModel {
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Video player config
ChewieController? _chewieController;
ChewieController? get chewieController => _chewieController;
VideoPlayerController? _videoPlayerController;
VideoPlayerController? get videoPlayerController => _videoPlayerController;
// Video player
Future<void> initializePlayer() async =>
await runBusyFuture(_initializePlayer(),
busyObject: StateObjects.loadLessonVideo);
Future<void> _initializePlayer() async {
_videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl));
await _videoPlayerController?.initialize();
if (_videoPlayerController != null) {
print('Initialized');
_chewieController = ChewieController(
looping: true,
autoPlay: true,
showOptions: true,
showControls: true,
aspectRatio: 16 / 9,
autoInitialize: true,
allowedScreenSleep: false,
videoPlayerController: _videoPlayerController!,
materialProgressColors: buildChewieProgressIndicator);
}
// rebuildUi();
}
Future<void> pause()async{
await _chewieController?.pause();
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnPractice() async=>await _navigationService.navigateToLearnPracticeView();
}

View File

@ -73,14 +73,20 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
_buildListView(viewModel)
];
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'A1 - Beginner',
style: style18P600,
style: TextStyle(
fontSize: 18,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitle() => Text(
Widget _buildSubTitle() => const Text(
'Your Current Level',
style: style14DG400,
style: TextStyle(
color: kcDarkGrey,
),
);
Widget _buildOverallProgress() => const OverallLearnProgress();

View File

@ -37,6 +37,5 @@ class LearnModuleViewModel extends BaseViewModel {
void pop() => _navigationService.back();
Future<void> navigateToLearnLesson() async =>
await _navigationService.navigateToLearnLessonView();
Future<void> navigateToLearnLesson() async=> await _navigationService.navigateToLearnLessonView();
}

View File

@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/listen_speaker_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/practice_intro_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/start_practice_screen.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart';
import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_practice_viewmodel.dart';
class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
const LearnPracticeView({Key? key}) : super(key: key);
@override
LearnPracticeViewModel viewModelBuilder(BuildContext context) =>
LearnPracticeViewModel();
@override
Widget builder(
BuildContext context,
LearnPracticeViewModel viewModel,
Widget? child,
) =>
_buildPracticeScreensWrapper(viewModel);
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) => PopScope(
canPop: true,
onPopInvokedWithResult: (value, data) {
if (!value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(LearnPracticeViewModel viewModel) => Stack(children: [
_buildBody(viewModel),
//_buildLoginWithEmailState(viewModel),
//_buildLoginWithGoogleState(viewModel)
]);
Widget _buildBody(LearnPracticeViewModel viewModel) =>
IndexedStack(
index: viewModel.currentIndex, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildPracticeIntroScreen(),
_buildStartPracticeScreen(),
_buildListenSpeakerScreen()
];
Widget _buildPracticeIntroScreen() => const PracticeIntroScreen();
Widget _buildStartPracticeScreen() => const StartPracticeScreen();
Widget _buildListenSpeakerScreen() => const ListenSpeakerScreen();
}

View File

@ -1,34 +0,0 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class LearnPracticeViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// In-app navigation
int _currentIndex = 0;
int get currentIndex => _currentIndex;
// In-app navigation
void goTo(int page) {
_currentIndex = page;
rebuildUi();
}
void goBack() {
if(_currentIndex == 0){
pop();
}else{
_currentIndex--;
rebuildUi();
}
}
// Navigation
void pop() => _navigationService.back();
}

View File

@ -1,254 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_column_button.dart';
import '../../../widgets/small_app_bar.dart';
class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
const ListenSpeakerScreen({super.key});
Future<void> _showSheet(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SafeArea(
child:
_buildBodyColumnWrapper(context: context, viewModel: viewModel));
Widget _buildBodyColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyStack(context: context, viewModel: viewModel),
);
Widget _buildBodyStack(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Stack(
children: [
_buildBodyColumn(context: context, viewModel: viewModel),
_buildProgressIndicatorWrapper()
],
);
Widget _buildBodyColumn(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
_buildBodyColumnChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyColumnChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildAppBarWrapper(viewModel),
_buildSpeakingIndicatorWrapper(viewModel),
_buildLowerButtonsSectionWrapper(context: context, viewModel: viewModel)
];
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildSpeakingIndicatorChildren(),
);
List<Widget> _buildSpeakingIndicatorChildren() =>
[_buildSpeakerLabel(), verticalSpaceMedium, _buildSpeakingIndicator()];
Widget _buildSpeakerLabel() => Text(
'Daniel is speaking...',
style: style14P400,
textAlign: TextAlign.center,
);
Widget _buildSpeakingIndicator() => Container(
height: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
radius: 0.7,
stops: const [
0.2,
0.25,
0.45,
0.75,
1,
],
center: Alignment.center,
colors: [
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.15),
kcPrimaryColor.withOpacity(0.1),
kcPrimaryColor.withOpacity(0.05),
],
// quarterly spread
),
),
child: _buildSpinner(),
);
Widget _buildSpinner() => const SpinKitWave(
size: 20,
color: kcPrimaryColor,
type: SpinKitWaveType.center,
);
Widget _buildLowerButtonsSectionWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child:
_buildLowerButtonsSection(context: context, viewModel: viewModel),
);
Widget _buildLowerButtonsSection(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildActionLabel(),
verticalSpaceMedium,
_buildButtonsRowWrapper(context: context, viewModel: viewModel),
verticalSpaceMedium,
];
Widget _buildActionLabel() => Text(
'Tap the microphone to speak',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildButtonsRowWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
_buildButtonsRowChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildButtonsRowChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildReplyButtonWrapper(),
_buildMicButtonWrapper(),
_buildCancelButtonWrapper(context: context, viewModel: viewModel)
];
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
Widget _buildMicButton() => ElevatedButton(
onPressed: () {},
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
child: _buildMicIcon(),
);
Widget _buildMicIcon() => const Icon(
Icons.mic,
size: 35,
color: kcWhite,
);
Widget _buildCancelButtonWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Expanded(
child: _buildCancelButton(context: context, viewModel: viewModel));
Widget _buildCancelButton(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
CustomColumnButton(
color: kcRed,
label: 'Cancel',
icon: Icons.close,
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
);
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
CancelLearnPracticeSheet(
onTap: viewModel.pop,
);
Widget _buildProgressIndicatorWrapper() => Positioned(
top: 75,
left: 0,
right: 0,
child: _buildProgressIndicator(),
);
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
progress: 0.7,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
}

View File

@ -1,124 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/small_app_bar.dart';
import '../../../widgets/speaking_partner_image.dart';
class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
const PracticeIntroScreen({super.key});
@override
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
SafeArea(child: _buildColumnWrapper(viewModel));
Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildBodyColumnWrapper(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded(
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
_buildPracticeColumnWrapper(viewModel),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildPracticeColumnWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildPracticeColumnScrollView(viewModel));
Widget _buildPracticeColumnScrollView(LearnPracticeViewModel viewModel) =>
SingleChildScrollView(
child: _buildPracticeColumn(viewModel),
);
Widget _buildPracticeColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildPracticeColumnChildren(viewModel),
);
List<Widget> _buildPracticeColumnChildren(LearnPracticeViewModel viewModel) =>
[
verticalSpaceMassive,
_buildImage(),
verticalSpaceMedium,
_buildPartnerName(),
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubtitle()
];
Widget _buildImage() => const SpeakingPartnerImage(radius: 75,);
Widget _buildPartnerName() => Text.rich(
TextSpan(text: 'Daniel', style: style14DG600, children: [
TextSpan(
text: ' - Your Speaking Partner',
style: style14MG400,
)
]),
);
Widget _buildTitle() => Text(
'Let \'s practice what you just learnt!',
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Ill ask you a few questions, and you can respond naturally.',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Start Practice',
foregroundColor: kcWhite,
onTap: ()=> viewModel.goTo(1),
backgroundColor: kcPrimaryColor,
);
}

View File

@ -1,172 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_column_button.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/small_app_bar.dart';
class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
const StartPracticeScreen({super.key});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
SafeArea(child: _buildBodyColumnWrapper(viewModel));
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
_buildAppBarWrapper(viewModel),
_buildStartButtonWrapper(viewModel),
_buildLowerButtonsSectionWrapper(viewModel)
];
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildStartButtonWrapper(LearnPracticeViewModel viewModel) => Expanded(
child: _buildStartButtonContainer(viewModel),
);
Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) =>
GestureDetector(
onTap: () => viewModel.goTo(2),
child: _buildStartButton(),
);
Widget _buildStartButton() => Container(
width: 150,
height: 150,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: SweepGradient(
stops: const [
0.0,
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.8,
0.9,
1,
],
endAngle: 8,
startAngle: 0.0,
center: Alignment.center,
colors: [
kcPrimaryColor.withOpacity(0.3),
kcIndigo.withOpacity(0.2),
kcIndigo.withOpacity(0.3),
kcIndigo.withOpacity(0.4),
kcIndigo.withOpacity(0.5),
kcPrimaryColor.withOpacity(0.5),
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.3),
kcPrimaryColor.withOpacity(0.2),
kcPrimaryColor.withOpacity(0.5),
],
// quarterly spread
),
),
child: _buildStartText(),
);
Widget _buildStartText() => Text(
'Start',
style: style25W600,
);
Widget _buildLowerButtonsSectionWrapper(LearnPracticeViewModel viewMode) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: _buildLowerButtonsSection(viewMode),
);
Widget _buildLowerButtonsSection(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
LearnPracticeViewModel viewModel) =>
[
_buildActionLabel(),
verticalSpaceMedium,
_buildButtonsRowWrapper(),
verticalSpaceMedium,
];
Widget _buildActionLabel() => Text(
'Tap the microphone to speak',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildButtonsRowWrapper() => Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildButtonsRowChildren(),
);
List<Widget> _buildButtonsRowChildren() => [
_buildReplyButtonWrapper(),
_buildMicButtonWrapper(),
_buildEmptySpace()
];
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
Widget _buildMicButton() => ElevatedButton(
onPressed: () {},
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
child: _buildMicIcon(),
);
Widget _buildMicIcon() => const Icon(
Icons.mic,
size: 35,
color: kcWhite,
);
Widget _buildEmptySpace() => Expanded(child: Container());
}

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/login/screens/login_otp_screen.dart';
import 'package:yimaru_app/ui/views/login/screens/login_with_email_screen.dart';
import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart';
@ -25,18 +24,10 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
@override
void onViewModelReady(LoginViewModel viewModel) {
_clearData();
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
void _clearData() {
otpController.clear();
emailController.clear();
passwordController.clear();
phoneNumberController.clear();
}
@override
LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel();
@ -54,11 +45,35 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
if (!value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildBody(viewModel));
child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(LoginViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(LoginViewModel viewModel) =>
Stack(children: [_buildScaffold(viewModel), _buildBusyLogin(viewModel)]);
Widget _buildScaffold(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(LoginViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(LoginViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(LoginViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(LoginViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
@ -79,4 +94,6 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
otpController: otpController,
phoneNumberController: phoneNumberController);
Widget _buildBusyLogin(LoginViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container();
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.locator.dart';
@ -8,25 +7,16 @@ import 'package:yimaru_app/models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/image_downloader_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../home/home_view.dart';
class LoginViewModel extends FormViewModel {
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _googleAuthService = locator<GoogleAuthService>();
final _authenticationService = locator<AuthenticationService>();
// In-app navigation
// Navigation
int _currentIndex = 0;
int get currentIndex => _currentIndex;
@ -116,7 +106,36 @@ class LoginViewModel extends FormViewModel {
_userData.clear();
}
// In app navigation
// Remote api calls
Future<void> login() async {
Map<String, dynamic> response =
await runBusyFuture<Map<String, dynamic>>(_login());
if (response['status'] == ResponseStatus.success) {
await replaceWithHome();
}
}
Future<Map<String, dynamic>> _login() async {
Map<String, dynamic> response = await _apiService.login(_userData);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserData(data);
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
return response;
}
// Navigation
void goTo(int page) {
_currentIndex = page;
rebuildUi();
@ -134,70 +153,9 @@ class LoginViewModel extends FormViewModel {
}
}
// Navigation
Future<void> navigateToRegister() async =>
await _navigationService.navigateToRegisterView();
Future<void> navigateToForgetPassword() async =>
await _navigationService.navigateToForgetPasswordView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
// Remote api calls
// Login with email
Future<void> emailLogin() async => await runBusyFuture(_emailLogin(),
busyObject: StateObjects.loginWithEmail);
Future<void> _emailLogin() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = await _apiService.emailLogin(_userData);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserCredential(data);
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
Future<void> signInWithGoogle() async => await runBusyFuture(_signInWithGoogle(),
busyObject: StateObjects.loginWithGoogle);
Future<void> _signInWithGoogle() async {
if (await _statusChecker.checkConnection()) {
GoogleSignInAccount? googleUser = await _googleAuthService.googleAuth();
Map<String, dynamic> data = {
'id_token': googleUser?.authentication.idToken ?? '',
};
Map<String, dynamic> response = await _apiService.googleAuth(data);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserCredential(data);
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
}

View File

@ -8,7 +8,6 @@ import 'package:yimaru_app/ui/widgets/custom_cursor.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../login_viewmodel.dart';
import '../login_view.form.dart';
@ -21,61 +20,23 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
required this.otpController,
required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 325 - half,);
}
@override
Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildScaffoldWrapper(context: context,viewModel: viewModel);
_buildBody(viewModel);
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context,viewModel: viewModel),
);
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
Widget _buildBody(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context,viewModel: viewModel),
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButton(viewModel)];
Widget _buildColumnScroller(LoginViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
@ -83,7 +44,6 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),

View File

@ -4,12 +4,9 @@ import 'package:yimaru_app/ui/views/login/login_view.form.dart';
import 'package:yimaru_app/ui/widgets/obscure_password.dart';
import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/option_text_divider.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../../../widgets/register_for_account.dart';
import '../login_viewmodel.dart';
@ -22,12 +19,6 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
required this.emailController,
required this.passwordController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 25 - half,);
}
Future<void> _login(LoginViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -37,63 +28,27 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
};
viewModel.addUserData(data);
await viewModel.emailLogin();
await viewModel.login();
}
@override
Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildScaffoldWrapper(context: context,viewModel: viewModel);
_buildBody(viewModel);
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context,viewModel: viewModel),
);
Widget _buildScaffoldStack({required BuildContext context,required LoginViewModel viewModel}) => Stack(children: [
_buildScaffold(context: context,viewModel: viewModel),
_buildLoginWithEmailState(viewModel),
_buildLoginWithGoogleState(viewModel)
]);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
Widget _buildBody(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
Widget _buildColumnScroller(LoginViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context,viewModel: viewModel),
child: _buildUpperColumn(viewModel),
);
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@ -103,7 +58,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubtitleWrapper(viewModel),
_buildSubTitleWrapper(viewModel),
verticalSpaceLarge,
_buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
@ -116,15 +71,19 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
verticalSpaceTiny,
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
_buildPasswordValidationWrapper(viewModel),
_buildForgetPasswordTextButtonWrapper(viewModel),
_buildForgetPasswordTextButtonWrapper(),
];
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Welcome Back',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
onTap: () async => await viewModel.navigateToRegister(),
);
@ -145,7 +104,11 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
Widget _buildEmailValidator(LoginViewModel viewModel) => Text(
viewModel.emailValidationMessage!,
style: style12R700,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField(
@ -172,23 +135,26 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
Widget _buildPasswordValidator(LoginViewModel viewModel) => Text(
viewModel.passwordValidationMessage!,
style: style12R700,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildForgetPasswordTextButtonWrapper(LoginViewModel viewModel) =>
Align(
Widget _buildForgetPasswordTextButtonWrapper() => Align(
alignment: Alignment.centerRight,
child: _buildForgetPasswordTextButton(viewModel),
child: _buildForgetPasswordTextButton(),
);
Widget _buildForgetPasswordTextButton(LoginViewModel viewModel) => TextButton(
onPressed: () async => await viewModel.navigateToForgetPassword(),
Widget _buildForgetPasswordTextButton() => TextButton(
onPressed: () {},
child: _buildForgetPasswordText(),
);
Widget _buildForgetPasswordText() => Text(
Widget _buildForgetPasswordText() => const Text(
'Forget Password?',
style: style14P400,
style: TextStyle(color: kcPrimaryColor),
);
Widget _buildLowerColumn(LoginViewModel viewModel) => Column(
@ -198,15 +164,13 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
List<Widget> _buildLowerColumnChildren(LoginViewModel viewModel) => [
_buildContinueButton(viewModel),
_buildLoginWithGoogleButton(viewModel),
_buildOptionTextDivider(),
_buildLoginWithPhoneButton(viewModel),
_buildLoginWithEmailButton(viewModel),
verticalSpaceMedium
];
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
height: 55,
safe: false,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
@ -220,40 +184,17 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildLoginWithGoogleButton(LoginViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
text: 'Login with Google',
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
leadingImage: 'assets/icons/google.png',
onTap: () async => await viewModel.signInWithGoogle(),
);
Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildLoginWithPhoneButton(LoginViewModel viewModel) =>
Widget _buildLoginWithEmailButton(LoginViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
leadingIcon: Icons.phone,
borderColor: kcPrimaryColor,
onTap: () => viewModel.goTo(1),
foregroundColor: kcPrimaryColor,
text: 'Login with Phone Number',
onTap: () => viewModel.goTo(1),
);
Widget _buildLoginWithEmailState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithEmail)
? const PageLoadingIndicator()
: Container();
Widget _buildLoginWithGoogleState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithGoogle)
? const PageLoadingIndicator()
: Container();
}

View File

@ -8,7 +8,6 @@ import 'package:yimaru_app/ui/widgets/register_for_account.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/phone_number_prefix.dart';
import '../login_view.form.dart';
@ -18,75 +17,34 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
const LoginWithPhoneNumberScreen(
{super.key, required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 175 - half,);
}
@override
Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel);
_buildScaffoldWrapper(context: context,viewModel: viewModel);
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
Widget _buildBody(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
Widget _buildColumnScroller(LoginViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context,viewModel: viewModel),
child: _buildUpperColumn(viewModel),
);
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubtitleWrapper(viewModel),
_buildSubTitleWrapper(viewModel),
verticalSpaceMedium,
_buildSubtitle(),
verticalSpaceMedium,
@ -99,18 +57,22 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
_buildPhoneNumberValidatorWrapper(viewModel),
];
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Welcome Back',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
onTap: () async => await viewModel.navigateToRegister(),
);
Widget _buildSubtitle() => Text(
Widget _buildSubtitle() => const Text(
'Enter your phone number. We will send you a confirmation code there',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row(

View File

@ -19,6 +19,7 @@ import 'onboarding_viewmodel.dart';
import 'onboarding_view.form.dart';
@FormView(fields: [
FormTextField(name: 'answer', validator: FormValidator.validateForm),
FormTextField(name: 'fullName', validator: FormValidator.validateForm),
FormTextField(name: 'challenge', validator: FormValidator.validateForm),
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
@ -29,55 +30,13 @@ class OnboardingView extends StackedView<OnboardingViewModel>
with $OnboardingView {
const OnboardingView({Key? key}) : super(key: key);
void _initClearData() {
topicController.clear();
fullNameController.clear();
challengeController.clear();
occupationController.clear();
languageGoalController.clear();
}
void _clearDataOnNavigation(OnboardingViewModel viewModel) {
if (viewModel.currentPage == 0) {
fullNameController.clear();
viewModel.resetFullNameFormScreen();
} else if (viewModel.currentPage == 1) {
viewModel.resetGenderFormScreen();
} else if (viewModel.currentPage == 2) {
viewModel.resetBirthdayFormScreen();
} else if (viewModel.currentPage == 3) {
viewModel.resetAgeGroupFormScreen();
} else if (viewModel.currentPage == 4) {
viewModel.resetEducationalBackgroundFormScreen();
} else if (viewModel.currentPage == 5) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
} else if (viewModel.currentPage == 6) {
viewModel.resetCountryRegionFormScreen();
} else if (viewModel.currentPage == 7) {
viewModel.resetLearningGoalFormScreen();
} else if (viewModel.currentPage == 8) {
languageGoalController.clear();
viewModel.resetLanguageGoalFormScreen();
} else if (viewModel.currentPage == 9) {
challengeController.clear();
viewModel.resetChallengeFormScreen();
} else if (viewModel.currentPage == 10) {
topicController.clear();
viewModel.resetTopicFormScreen();
}
}
void _pop(OnboardingViewModel viewModel) {
{
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _initFormFields() {
answerController.text = 'Book';
}
@override
void onViewModelReady(OnboardingViewModel viewModel) {
_initClearData();
_initFormFields();
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@ -98,8 +57,8 @@ class OnboardingView extends StackedView<OnboardingViewModel>
Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) =>
PopScope(
canPop: viewModel.currentPage == 0 ? true : false,
onPopInvokedWithResult: (value, data) => _pop(viewModel),
canPop: false,
onPopInvokedWithResult: (value, data) => viewModel.pop(),
child: _buildOnboardingScreens(viewModel));
Widget _buildOnboardingScreens(OnboardingViewModel viewModel) => IndexedStack(

View File

@ -12,6 +12,7 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String AnswerValueKey = 'answer';
const String FullNameValueKey = 'fullName';
const String ChallengeValueKey = 'challenge';
const String OccupationValueKey = 'occupation';
@ -24,6 +25,7 @@ final Map<String, TextEditingController> _OnboardingViewTextEditingControllers =
final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
AnswerValueKey: FormValidator.validateForm,
FullNameValueKey: FormValidator.validateForm,
ChallengeValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm,
@ -32,6 +34,8 @@ final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
};
mixin $OnboardingView {
TextEditingController get answerController =>
_getFormTextEditingController(AnswerValueKey);
TextEditingController get fullNameController =>
_getFormTextEditingController(FullNameValueKey);
TextEditingController get challengeController =>
@ -43,6 +47,7 @@ mixin $OnboardingView {
TextEditingController get topicController =>
_getFormTextEditingController(TopicValueKey);
FocusNode get answerFocusNode => _getFormFocusNode(AnswerValueKey);
FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey);
FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey);
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
@ -74,6 +79,7 @@ mixin $OnboardingView {
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
answerController.addListener(() => _updateFormData(model));
fullNameController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
@ -90,6 +96,7 @@ mixin $OnboardingView {
'This feature was deprecated after 3.1.0.',
)
void listenToFormUpdated(FormViewModel model) {
answerController.addListener(() => _updateFormData(model));
fullNameController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
@ -104,6 +111,7 @@ mixin $OnboardingView {
model.setData(
model.formValueMap
..addAll({
AnswerValueKey: answerController.text,
FullNameValueKey: fullNameController.text,
ChallengeValueKey: challengeController.text,
OccupationValueKey: occupationController.text,
@ -150,6 +158,7 @@ extension ValueProperties on FormStateHelper {
return !hasAnyValidationMessage;
}
String? get answerValue => this.formValueMap[AnswerValueKey] as String?;
String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?;
String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?;
String? get occupationValue =>
@ -158,6 +167,16 @@ extension ValueProperties on FormStateHelper {
this.formValueMap[LanguageGoalValueKey] as String?;
String? get topicValue => this.formValueMap[TopicValueKey] as String?;
set answerValue(String? value) {
this.setData(
this.formValueMap..addAll({AnswerValueKey: value}),
);
if (_OnboardingViewTextEditingControllers.containsKey(AnswerValueKey)) {
_OnboardingViewTextEditingControllers[AnswerValueKey]?.text = value ?? '';
}
}
set fullNameValue(String? value) {
this.setData(
this.formValueMap..addAll({FullNameValueKey: value}),
@ -213,6 +232,9 @@ extension ValueProperties on FormStateHelper {
}
}
bool get hasAnswer =>
this.formValueMap.containsKey(AnswerValueKey) &&
(answerValue?.isNotEmpty ?? false);
bool get hasFullName =>
this.formValueMap.containsKey(FullNameValueKey) &&
(fullNameValue?.isNotEmpty ?? false);
@ -229,6 +251,8 @@ extension ValueProperties on FormStateHelper {
this.formValueMap.containsKey(TopicValueKey) &&
(topicValue?.isNotEmpty ?? false);
bool get hasAnswerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey]?.isNotEmpty ?? false;
bool get hasFullNameValidationMessage =>
this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false;
bool get hasChallengeValidationMessage =>
@ -240,6 +264,8 @@ extension ValueProperties on FormStateHelper {
bool get hasTopicValidationMessage =>
this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false;
String? get answerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey];
String? get fullNameValidationMessage =>
this.fieldsValidationMessages[FullNameValueKey];
String? get challengeValidationMessage =>
@ -253,6 +279,8 @@ extension ValueProperties on FormStateHelper {
}
extension Methods on FormStateHelper {
setAnswerValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[AnswerValueKey] = validationMessage;
setFullNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[FullNameValueKey] = validationMessage;
setChallengeValidationMessage(String? validationMessage) =>
@ -266,6 +294,7 @@ extension Methods on FormStateHelper {
/// Clears text input fields on the Form
void clearForm() {
answerValue = '';
fullNameValue = '';
challengeValue = '';
occupationValue = '';
@ -276,6 +305,7 @@ extension Methods on FormStateHelper {
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
FullNameValueKey: getValidationMessage(FullNameValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
@ -300,6 +330,7 @@ String? getValidationMessage(String key) {
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
FullNameValueKey: getValidationMessage(FullNameValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),

View File

@ -54,13 +54,10 @@ class OnboardingViewModel extends FormViewModel {
// Age group
final List<String> _ageGroups = [
'UNDER_13',
'13_17',
'18_24',
'25_34',
'35_44',
'45_54',
'55_PLUS'
'8-14',
'15-18',
'19-26',
'26+',
];
List<String> get ageGroups => _ageGroups;
@ -79,11 +76,30 @@ class OnboardingViewModel extends FormViewModel {
String get selectedCountry => _selectedCountry;
Future<List<String>> getCountries() async => ['Ethiopia'];
// Country
String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion;
Future<List<String>> getRegions(String country) async => [
'Afar',
'SNNPR',
'Amhara',
'Harari',
'Oromia',
'Sidama',
'Somali',
'Tigray',
'Gambela',
'Dire Dawa',
'Addis Ababa',
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
// Learning goal
String? _selectedLearningGoal;
@ -242,42 +258,12 @@ class OnboardingViewModel extends FormViewModel {
}
// Country
List<String> getCountries() => ['Ethiopia', 'Other'];
void setSelectedCountry(String value) {
_selectedCountry = value;
if (selectedCountry != 'Ethiopia') {
_selectedRegion = 'Other';
} else {
_selectedRegion = 'Addis Ababa';
}
rebuildUi();
}
// Region
List<String> getRegions(String country) {
if (country == 'Ethiopia') {
return [
'Afar',
'SNNPR',
'Amhara',
'Harari',
'Oromia',
'Sidama',
'Somali',
'Tigray',
'Gambela',
'Dire Dawa',
'Addis Ababa',
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
} else {
return ['Other'];
}
}
void setSelectedRegion(String value) {
_selectedRegion = value;
rebuildUi();
@ -366,89 +352,24 @@ class OnboardingViewModel extends FormViewModel {
// Add user data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
print('User data : $_userData');
}
void clearUserData() {
_userData.clear();
}
// Form reset
// Navigation
// Reset full name form screen
void resetFullNameFormScreen() {
_focusFullName = false;
rebuildUi();
}
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
// Reset gender form screen
void resetGenderFormScreen() {
_selectedGender = null;
rebuildUi();
}
Future<void> navigateToAssessment() async =>
await _navigationService.navigateToAssessmentView(data: _userData);
// Reset birthday form screen
void resetBirthdayFormScreen() {
_selectedBirthday = null;
rebuildUi();
}
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
// Reset age group form screen
void resetAgeGroupFormScreen() {
_selectedAgeGroup = null;
rebuildUi();
}
// Reset educational background form screen
void resetEducationalBackgroundFormScreen() {
_selectedEducationalBackground = null;
rebuildUi();
}
// Reset occupation form screen
void resetOccupationFormScreen() {
_focusOccupation = false;
rebuildUi();
}
// Reset country region form screen
void resetCountryRegionFormScreen() {
_selectedCountry = 'Ethiopia';
_selectedRegion = 'Addis Ababa';
rebuildUi();
}
// Reset learning goal form screen
void resetLearningGoalFormScreen() {
_selectedLearningGoal = null;
rebuildUi();
}
// Reset language goal form screen
void resetLanguageGoalFormScreen() {
_focusLanguageGoal = false;
_selectedLanguageGoal = null;
_showLanguageGoalTextBox = false;
rebuildUi();
}
// Reset challenge form screen
void resetChallengeFormScreen() {
_focusChallenge = false;
_selectedChallenge = null;
_showChallengeTextBox = false;
rebuildUi();
}
// Reset topic form screen
void resetTopicFormScreen() {
_focusTopic = false;
_selectedTopic = null;
_showTopicTextBox = false;
rebuildUi();
}
// In-app navigation
void next({int? page}) async {
if (page == null) {
if (_previousPage != 0) {
@ -463,8 +384,8 @@ class OnboardingViewModel extends FormViewModel {
rebuildUi();
}
void goBack() {
if (_currentPage == 0) {
void pop() {
if (_currentPage == 8) {
_navigationService.back();
} else {
_currentPage--;
@ -472,14 +393,4 @@ class OnboardingViewModel extends FormViewModel {
rebuildUi();
}
}
// Navigation
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> navigateToAssessment() async =>
await _navigationService.navigateToAssessmentView(data: _userData);
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
}

View File

@ -10,12 +10,6 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
const AgeGroupFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetAgeGroupFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -80,20 +74,24 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Which age range are you in?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubTitle() => Text(
Widget _buildSubTitle() => const Text(
'Well personalize your learning experience based on your age.',
style: style14DG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -12,16 +12,14 @@ import '../../../widgets/birthday_selector.dart';
class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
const BirthdayFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetBirthdayFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {'birth_day': viewModel.selectedBirthday};
Map<String, dynamic> data = {
'birth_day': DateFormat('yyyy-MM-dd')
.parseUTC(viewModel.selectedBirthday ?? DateTime.now().toString())
.toIso8601String()
};
viewModel.addUserData(data);
viewModel.next();
}
@ -70,26 +68,30 @@ class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceMedium,
_buildBirthdayFormField(viewModel)
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Pick your birthday?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Well personalize your learning experience based on your birthday.',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildBirthdayFormField(OnboardingViewModel viewModel) =>

View File

@ -13,12 +13,6 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
const ChallengeFormScreen({super.key, required this.challengeController});
void _pop(OnboardingViewModel viewModel) {
challengeController.clear();
viewModel.resetChallengeFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -80,7 +74,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceMedium,
_buildChallenges(viewModel),
if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel),
@ -96,20 +90,24 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'What challenge do you face most with English?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Everyone has struggles, lets start fixing yours 😊',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder(
@ -153,7 +151,11 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text(
viewModel.challengeValidationMessage!,
style: style12R700,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -10,11 +10,6 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
const CountryRegionFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetCountryRegionFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -76,7 +71,7 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceMedium,
_buildCountryDropDown(viewModel),
verticalSpaceMedium,
@ -85,27 +80,31 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Where are you from?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Select your country and region from the dropdown',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildCountryDropDown(OnboardingViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select country',
icon: _buildSearchIcon(),
selectedItem: viewModel.selectedCountry,
selectedItem: 'Ethiopia',
items: (value, props) => viewModel.getCountries(),
onChanged: (value) =>
viewModel.setSelectedCountry(value ?? 'Ethiopia'));
@ -114,11 +113,10 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
CustomDropdownPicker(
hint: 'Select region',
icon: _buildSearchIcon(),
selectedItem: viewModel.selectedRegion,
items: (value, props) =>
viewModel.getRegions(viewModel.selectedCountry),
selectedItem: 'Addis Ababa',
onChanged: (value) =>
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
items: (value, props) => viewModel.getRegions('Addis Ababa'),
);
Icon _buildSearchIcon() => const Icon(

View File

@ -11,11 +11,6 @@ class EducationalBackgroundFormScreen
extends ViewModelWidget<OnboardingViewModel> {
const EducationalBackgroundFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetEducationalBackgroundFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -82,9 +77,9 @@ class EducationalBackgroundFormScreen
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);

View File

@ -72,7 +72,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceLarge,
_buildFullNameFormField(viewModel),
if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName)
@ -96,7 +96,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
),
);
Widget _buildSubtitle() => const Text(
Widget _buildSubTitle() => const Text(
'Well use your name to personalize your learning journey.',
style: TextStyle(color: kcMediumGrey),
);

View File

@ -10,11 +10,6 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
const GenderFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetGenderFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -68,26 +63,30 @@ class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceMedium,
_buildAgeGroups(viewModel)
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Choose your gender?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Well personalize your learning experience based on your gender.',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -14,12 +14,6 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
const LanguageGoalFormScreen(
{super.key, required this.languageGoalController});
void _pop(OnboardingViewModel viewModel) {
languageGoalController.clear();
viewModel.resetLanguageGoalFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -81,7 +75,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceMedium,
_buildReasons(viewModel),
if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel),
@ -97,20 +91,26 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Whats your main goal for improving your English?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Your goal helps us tailor your learning journey.',
style: style14MG400,
style: TextStyle(
color: kcMediumGrey,
),
);
Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder(
@ -154,7 +154,11 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text(
viewModel.languageGoalValidationMessage!,
style: style12R700,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -23,11 +23,6 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
return Icons.book;
}
void _pop(OnboardingViewModel viewModel) {
viewModel.resetLearningGoalFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -92,22 +87,19 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle(OnboardingViewModel viewModel) => Text.rich(
TextSpan(
text: 'Hi ${viewModel.userData['first_name']},',
style: style18P600.copyWith(fontSize: 22),
children: [
TextSpan(
text: ' Choose your learning goal.',
style: style16DG600.copyWith(fontSize: 22),
)
]),
Widget _buildTitle(OnboardingViewModel viewModel) => Text(
'Hi ${viewModel.userData['first_name']}, Choose your learning goal.',
style: const TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildLearningGoals(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -13,12 +13,6 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
const OccupationFormScreen({super.key, required this.occupationController});
void _pop(OnboardingViewModel viewModel) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -77,7 +71,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceLarge,
_buildOccupationFormField(viewModel),
if (viewModel.hasOccupationValidationMessage &&
@ -90,19 +84,23 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Whats your occupation?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Well personalize your learning experience based on your occupation.',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildOccupationFormField(OnboardingViewModel viewModel) =>
@ -122,7 +120,11 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text(
viewModel.occupationValidationMessage!,
style: style12R700,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -13,18 +13,10 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
const TopicFormScreen({super.key, required this.topicController});
void _pop(OnboardingViewModel viewModel) {
topicController.clear();
viewModel.resetTopicFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'profile_completed': true,
'preferred_language': 'en',
'favoutite_topic': viewModel.selectedTopic ?? topicController.text,
};
viewModel.addUserData(data);
@ -51,8 +43,8 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
@ -88,7 +80,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
_buildSubTitle(),
verticalSpaceMedium,
_buildTopics(viewModel),
if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel),
@ -103,14 +95,18 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
];
Widget _buildTitle() => Text(
Widget _buildTitle() => const Text(
'Which topics interest you most?',
style: style25DG600,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => Text(
Widget _buildSubTitle() => const Text(
'Your favorite topics help us create fun, relatable lessons.',
style: style14MG400,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder(
@ -152,7 +148,11 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text(
viewModel.topicValidationMessage!,
style: style12R700,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -1,36 +1,17 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/profile_card.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart';
import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/image_picker_option.dart';
import 'profile_viewmodel.dart';
class ProfileView extends StackedView<ProfileViewModel> {
const ProfileView({Key? key}) : super(key: key);
Future<void> _showImagePicker(
{required BuildContext context,
required ProfileViewModel viewModel}) async =>
await showDialog(
context: context,
builder: (context) =>
_showImagePickerDialog(context: context, viewModel: viewModel),
);
AlertDialog _showImagePickerDialog(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
AlertDialog(
backgroundColor: Colors.transparent,
content: _buildImagePicker(context: context, viewModel: viewModel),
);
@override
ProfileViewModel viewModelBuilder(
BuildContext context,
@ -43,45 +24,30 @@ class ProfileView extends StackedView<ProfileViewModel> {
ProfileViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Scaffold(
Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context, viewModel: viewModel),
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
SafeArea(
child: _buildBodyWrapper(context: context, viewModel: viewModel));
Widget _buildScaffold(ProfileViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBody(context: context, viewModel: viewModel),
Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView(
child: _buildBody(viewModel),
);
Widget _buildBody(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Padding(
Widget _buildBody(ProfileViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(context: context, viewModel: viewModel),
child: _buildColumn(viewModel),
);
Widget _buildColumn(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Column(
Widget _buildColumn(ProfileViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildNotificationIconWrapper(),
_buildProfileSection(context: context, viewModel: viewModel),
_buildProfileSection(),
verticalSpaceSmall,
_buildViewProfileButton(viewModel),
verticalSpaceLarge,
@ -100,46 +66,27 @@ class ProfileView extends StackedView<ProfileViewModel> {
color: kcDarkGrey,
);
Widget _buildProfileSection(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Column(
Widget _buildProfileSection() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildProfileSectionChildren(
context: context, viewModel: viewModel),
children: _buildProfileSectionChildren(),
);
List<Widget> _buildProfileSectionChildren(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
[
_buildProfileImage(context: context, viewModel: viewModel),
List<Widget> _buildProfileSectionChildren() => [
_buildProfileImage(),
verticalSpaceSmall,
_buildProfileName(viewModel),
_buildProfileName(),
];
Widget _buildProfileImage(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
ProfileImage(
profileImage: viewModel.user?.profilePicture,
loading: viewModel.busy(StateObjects.profileImage) ? true : false,
onTap: () async =>
await _showImagePicker(context: context, viewModel: viewModel),
);
Widget _buildProfileImage() => const ProfileImage();
Widget _buildImagePicker(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
ImagePickerOption(
onCameraTap: () async => await viewModel.openCamera(),
onGalleryTap: () async => await viewModel.openGallery(),
);
Widget _buildProfileName(ProfileViewModel viewModel) => Text(
'Hi, ${viewModel.user?.firstName ?? ''} 👋',
style: style25DG600,
Widget _buildProfileName() => const Text(
'Hi, Bisrat 👋',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildViewProfileButton(ProfileViewModel viewModel) =>
@ -164,28 +111,28 @@ class ProfileView extends StackedView<ProfileViewModel> {
Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard(
icon: Icons.download,
title: 'My Downloads',
subtitle: 'Access offline lessons and saved videos',
subTitle: 'Access offline lessons and saved videos',
onTap: () async => await viewModel.navigateToDownloads(),
);
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
title: 'My Progress',
icon: Icons.stacked_bar_chart,
subtitle: 'Track your achievements and learning streak',
subTitle: 'Track your achievements and learning streak',
onTap: () async => await viewModel.navigateToProgress(),
);
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Account & Privacy',
icon: Icons.privacy_tip_outlined,
subtitle: 'Manage setting and app preference',
subTitle: 'Manage setting and app preference',
onTap: () async => await viewModel.navigateToAccountPrivacy(),
);
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Support',
icon: Icons.headphones,
subtitle: 'Get help through phone or Telegram',
subTitle: 'Get help through phone or Telegram',
onTap: () async => await viewModel.navigateToSupport(),
);
@ -194,7 +141,7 @@ class ProfileView extends StackedView<ProfileViewModel> {
text: 'Log Out',
borderRadius: 12,
foregroundColor: kcRed,
backgroundColor: kcRed.withOpacity(0.25),
onTap: () async => await viewModel.logOut(),
backgroundColor: kcRed.withOpacity(0.25),
);
}

View File

@ -1,93 +1,20 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/app_colors.dart';
class ProfileViewModel extends ReactiveViewModel {
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
class ProfileViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
// Image picker
Future<void> openCamera() async =>
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
Future<void> _openCamera() async {
String? image = await _imagePickerService.camera();
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
}
}
Future<void> openGallery() async =>
runBusyFuture(_openGallery(), busyObject: StateObjects.profileImage);
Future<void> _openGallery() async {
String? image = await _imagePickerService.gallery();
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
}
}
// Logout
Future<void> _logOut() async {
Future<void> logOut() async {
await _authenticationService.logOut();
await _navigationService.replaceWithLoginView();
}
// Dialog
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(
title: 'Logout',
cancelTitle: 'No',
buttonTitle: 'Yes',
barrierDismissible: true,
cancelTitleColor: kcDarkGrey,
buttonTitleColor: kcPrimaryColor,
description: 'Are you sure you want to quit?',
);
return response?.confirmed;
}
Future<void> logOut() async {
bool? response = await showAbortDialog();
if (response != null && response) {
await _logOut();
}
}
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToProfileDetail() async =>
await _navigationService.navigateToProfileDetailView();
@ -102,19 +29,4 @@ class ProfileViewModel extends ReactiveViewModel {
Future<void> navigateToSupport() async =>
await _navigationService.navigateToSupportView();
// Remote api call
// Update profile
Future<void> updateProfilePicture(String image) async =>
await runBusyFuture(_updateProfilePicture(image));
Future<void> _updateProfilePicture(String image) async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> data = {
'profile_picture_url': image,
};
await _apiService.updateProfileImage(data: data, userId: _user?.userId);
}
}
}

View File

@ -8,13 +8,10 @@ import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../common/validators/form_validator.dart';
import '../../widgets/custom_dropdown.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/image_picker_option.dart';
import '../../widgets/page_loading_indicator.dart';
import '../../widgets/profile_image.dart';
import 'profile_detail_viewmodel.dart';
@ -26,62 +23,21 @@ import 'profile_detail_view.form.dart';
name: 'phoneNumber', validator: FormValidator.validatePhoneNumber),
FormTextField(name: 'lastName', validator: FormValidator.validateForm),
FormTextField(name: 'firstName', validator: FormValidator.validateForm),
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
])
class ProfileDetailView extends StackedView<ProfileDetailViewModel>
with $ProfileDetailView {
const ProfileDetailView({Key? key}) : super(key: key);
Future<void> _update(ProfileDetailViewModel viewModel) async {
Map<String, dynamic> data = {
'region': viewModel.selectedRegion,
'gender': viewModel.selectedGender,
'last_name': lastNameController.text,
'country': viewModel.selectedCountry,
'first_name': firstNameController.text,
'occupation': occupationController.text,
'birth_day': viewModel.selectedBirthday,
};
viewModel.addUserData(data);
await viewModel.updateProfile();
}
Future<void> _showImagePicker(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) async =>
await showDialog(
context: context,
builder: (context) =>
_showImagePickerDialog(context: context, viewModel: viewModel),
);
AlertDialog _showImagePickerDialog(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
AlertDialog(
backgroundColor: Colors.transparent,
content: _buildImagePicker(context: context, viewModel: viewModel),
);
void _onModelReady(ProfileDetailViewModel viewModel) {
void _onModelReady() {
firstNameController.text = 'Abel';
lastNameController.text = 'Abebe';
phoneNumberController.text = '251900000000';
emailController.text = viewModel.user?.email ?? '';
lastNameController.text = viewModel.user?.lastName ?? '';
firstNameController.text = viewModel.user?.firstName ?? '';
occupationController.text = viewModel.user?.occupation ?? '';
viewModel.clearUserData();
viewModel.setGender(viewModel.user?.gender ?? '');
viewModel.setSelectedCountry(viewModel.user?.country ?? 'Ethiopia');
viewModel.setSelectedRegion(viewModel.user?.region ?? 'Addis Ababa');
viewModel.setBirthday(viewModel.user?.birthday ??
DateFormat('d MMM, yyyy').format(DateTime.now()));
emailController.text = 'email@test.com';
}
@override
void onViewModelReady(ProfileDetailViewModel viewModel) {
_onModelReady(viewModel);
_onModelReady();
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@ -96,55 +52,32 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
ProfileDetailViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Scaffold(
Widget _buildScaffoldWrapper(ProfileDetailViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
body: _buildScaffold(viewModel),
);
Widget _buildScaffoldStack(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Stack(children: [
_buildScaffold(context: context, viewModel: viewModel),
_buildState(viewModel)
]);
Widget _buildScaffold(ProfileDetailViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildScaffold(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
SafeArea(
child: _buildBodyWrapper(context: context, viewModel: viewModel));
Widget _buildBodyWrapper(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Padding(
Widget _buildBodyWrapper(ProfileDetailViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context, viewModel: viewModel),
child: _buildBody(viewModel),
);
Widget _buildBody(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Column(
Widget _buildBody(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context, viewModel: viewModel),
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
[
List<Widget> _buildBodyChildren(ProfileDetailViewModel viewModel) => [
verticalSpaceMedium,
_buildAppbar(viewModel),
verticalSpaceSmall,
_buildColumnWrapper(context: context, viewModel: viewModel)
_buildColumnWrapper(viewModel)
];
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
@ -152,33 +85,23 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
onTap: viewModel.pop,
);
Widget _buildColumnWrapper(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Expanded(child: _buildBodyColumn(context: context, viewModel: viewModel));
Widget _buildColumnWrapper(ProfileDetailViewModel viewModel) =>
Expanded(child: _buildBodyColumn(viewModel));
Widget _buildBodyColumn(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Widget _buildBodyColumn(ProfileDetailViewModel viewModel) =>
SingleChildScrollView(
child: _buildColumn(context: context, viewModel: viewModel),
child: _buildColumn(viewModel),
);
Widget _buildColumn(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Column(
Widget _buildColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(context: context, viewModel: viewModel),
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
[
List<Widget> _buildColumnChildren(ProfileDetailViewModel viewModel) => [
verticalSpaceMedium,
_buildProfileImageWrapper(context: context, viewModel: viewModel),
_buildProfileImage(),
verticalSpaceMedium,
_buildNameFormSection(viewModel),
verticalSpaceMedium,
@ -197,30 +120,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
_buildLowerColumn(viewModel)
];
Widget _buildProfileImageWrapper(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Align(
alignment: Alignment.center,
child: _buildProfileImage(context: context, viewModel: viewModel));
Widget _buildProfileImage(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
ProfileImage(
profileImage: viewModel.user?.profilePicture,
loading: viewModel.busy(StateObjects.profileImage) ? true : false,
onTap: () async =>
await _showImagePicker(context: context, viewModel: viewModel),
);
Widget _buildImagePicker(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
ImagePickerOption(
onCameraTap: () async => await viewModel.openCamera(),
onGalleryTap: () async => await viewModel.openGallery(),
);
Widget _buildProfileImage() =>
const Align(alignment: Alignment.center, child: ProfileImage());
Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
@ -467,7 +368,6 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
maxLength: 12,
enabled: false,
keyboardType: TextInputType.phone,
controller: phoneNumberController,
onTap: viewModel.setPhoneNumberFocus,
@ -513,7 +413,6 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
enabled: false,
controller: emailController,
onTap: viewModel.setPhoneNumberFocus,
keyboardType: TextInputType.emailAddress,
@ -572,10 +471,10 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
onChanged: (value) {},
hint: 'Select country',
selectedItem: viewModel.selectedCountry,
selectedItem: 'Ethiopia',
items: (value, props) => viewModel.getCountries(),
onChanged: (value) => viewModel.setSelectedCountry(value ?? 'Ethiopia'),
);
Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
@ -605,11 +504,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select region',
selectedItem: viewModel.selectedRegion,
items: (value, props) =>
viewModel.getRegions(viewModel.selectedCountry),
onChanged: (value) =>
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
onChanged: (value) {},
selectedItem: 'Addis Ababa',
items: (value, props) => viewModel.getRegions('Addis Ababa'),
);
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
@ -625,13 +522,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
[
_buildOccupationDropdownLabel(),
verticalSpaceSmall,
_buildOccupationFormField(viewModel),
if (viewModel.hasOccupationValidationMessage &&
viewModel.focusOccupation)
verticalSpaceTiny,
if (viewModel.hasOccupationValidationMessage &&
viewModel.focusOccupation)
_buildOccupationValidatorWrapper(viewModel)
_buildOccupationDropdown(viewModel)
];
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
@ -639,29 +530,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
style: style16DG600,
);
Widget _buildOccupationFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
controller: occupationController,
onTap: viewModel.setOccupationFocus,
decoration: inputDecoration(
hint: 'Enter Your Occupation',
focus: viewModel.focusOccupation,
filled: occupationController.text.isNotEmpty),
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select occupation',
onChanged: (value) {},
selectedItem: 'Student',
items: (value, props) => viewModel.getOccupations('Student'),
);
Widget _buildOccupationValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasOccupationValidationMessage
? _buildOccupationValidator(viewModel)
: Container();
Widget _buildOccupationValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.occupationValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),
@ -669,18 +545,17 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
List<Widget> _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [
_buildSaveButton(viewModel),
verticalSpaceMedium,
verticalSpaceSmall,
_buildCancelButtonWrapper(viewModel)
];
Widget _buildSaveButton(ProfileDetailViewModel viewModel) =>
CustomElevatedButton(
const CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Save Changes',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await _update(viewModel),
);
Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding(
@ -689,18 +564,12 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
);
Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
CustomElevatedButton(
const CustomElevatedButton(
height: 55,
text: 'Cancel',
borderRadius: 12,
onTap: viewModel.pop,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
);
Widget _buildState(ProfileDetailViewModel viewModel) =>
viewModel.busy(StateObjects.profileUpdate)
? const PageLoadingIndicator()
: Container();
}

View File

@ -16,7 +16,6 @@ const String EmailValueKey = 'email';
const String PhoneNumberValueKey = 'phoneNumber';
const String LastNameValueKey = 'lastName';
const String FirstNameValueKey = 'firstName';
const String OccupationValueKey = 'occupation';
final Map<String, TextEditingController>
_ProfileDetailViewTextEditingControllers = {};
@ -29,7 +28,6 @@ final Map<String, String? Function(String?)?>
PhoneNumberValueKey: FormValidator.validatePhoneNumber,
LastNameValueKey: FormValidator.validateForm,
FirstNameValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm,
};
mixin $ProfileDetailView {
@ -41,14 +39,11 @@ mixin $ProfileDetailView {
_getFormTextEditingController(LastNameValueKey);
TextEditingController get firstNameController =>
_getFormTextEditingController(FirstNameValueKey);
TextEditingController get occupationController =>
_getFormTextEditingController(OccupationValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey);
FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey);
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
TextEditingController _getFormTextEditingController(
String key, {
@ -78,7 +73,6 @@ mixin $ProfileDetailView {
phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
@ -94,7 +88,6 @@ mixin $ProfileDetailView {
phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
@ -108,7 +101,6 @@ mixin $ProfileDetailView {
PhoneNumberValueKey: phoneNumberController.text,
LastNameValueKey: lastNameController.text,
FirstNameValueKey: firstNameController.text,
OccupationValueKey: occupationController.text,
}),
);
@ -155,8 +147,6 @@ extension ValueProperties on FormStateHelper {
this.formValueMap[PhoneNumberValueKey] as String?;
String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?;
String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?;
String? get occupationValue =>
this.formValueMap[OccupationValueKey] as String?;
set emailValue(String? value) {
this.setData(
@ -205,18 +195,6 @@ extension ValueProperties on FormStateHelper {
}
}
set occupationValue(String? value) {
this.setData(
this.formValueMap..addAll({OccupationValueKey: value}),
);
if (_ProfileDetailViewTextEditingControllers.containsKey(
OccupationValueKey)) {
_ProfileDetailViewTextEditingControllers[OccupationValueKey]?.text =
value ?? '';
}
}
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
@ -229,9 +207,6 @@ extension ValueProperties on FormStateHelper {
bool get hasFirstName =>
this.formValueMap.containsKey(FirstNameValueKey) &&
(firstNameValue?.isNotEmpty ?? false);
bool get hasOccupation =>
this.formValueMap.containsKey(OccupationValueKey) &&
(occupationValue?.isNotEmpty ?? false);
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
@ -241,8 +216,6 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false;
bool get hasFirstNameValidationMessage =>
this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false;
bool get hasOccupationValidationMessage =>
this.fieldsValidationMessages[OccupationValueKey]?.isNotEmpty ?? false;
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
@ -252,8 +225,6 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey];
String? get firstNameValidationMessage =>
this.fieldsValidationMessages[FirstNameValueKey];
String? get occupationValidationMessage =>
this.fieldsValidationMessages[OccupationValueKey];
}
extension Methods on FormStateHelper {
@ -265,8 +236,6 @@ extension Methods on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey] = validationMessage;
setFirstNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[FirstNameValueKey] = validationMessage;
setOccupationValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[OccupationValueKey] = validationMessage;
/// Clears text input fields on the Form
void clearForm() {
@ -274,7 +243,6 @@ extension Methods on FormStateHelper {
phoneNumberValue = '';
lastNameValue = '';
firstNameValue = '';
occupationValue = '';
}
/// Validates text input fields on the Form
@ -284,7 +252,6 @@ extension Methods on FormStateHelper {
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
});
}
}
@ -308,5 +275,4 @@ void updateValidationData(FormStateHelper model) =>
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
});

View File

@ -2,36 +2,9 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/image_picker_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
class ProfileDetailViewModel extends ReactiveViewModel
with FormStateHelper
implements FormViewModel {
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
class ProfileDetailViewModel extends FormViewModel {
final _navigationService = locator<NavigationService>();
final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
// First name
bool _focusFirstName = false;
@ -62,26 +35,6 @@ class ProfileDetailViewModel extends ReactiveViewModel
bool get focusEmail => _focusEmail;
// Country
String _selectedCountry = 'Ethiopia';
String get selectedCountry => _selectedCountry;
// Region
String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion;
// Occupation
bool _focusOccupation = false;
bool get focusOccupation => _focusOccupation;
// User data
final Map<String, dynamic> _userData = {};
Map<String, dynamic> get userData => _userData;
// First name
void setFirstNameFocus() {
_focusFirstName = true;
@ -119,132 +72,15 @@ class ProfileDetailViewModel extends ReactiveViewModel
}
// Country
List<String> getCountries() => ['Ethiopia', 'Other'];
void setSelectedCountry(String value) {
_selectedCountry = value;
if (selectedCountry != 'Ethiopia') {
_selectedRegion = 'Other';
} else {
_selectedRegion = 'Addis Ababa';
}
rebuildUi();
}
Future<List<String>> getCountries() async => ['Ethiopia', 'Djibouti'];
// Region
List<String> getRegions(String country) {
if (country == 'Ethiopia') {
return [
'Afar',
'SNNPR',
'Amhara',
'Harari',
'Oromia',
'Sidama',
'Somali',
'Tigray',
'Gambela',
'Dire Dawa',
'Addis Ababa',
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
} else {
return ['Other'];
}
}
void setSelectedRegion(String value) {
_selectedRegion = value;
rebuildUi();
}
Future<List<String>> getRegions(String country) async =>
['Addis Ababa', 'Oromia'];
// Occupation
void setOccupationFocus() {
_focusOccupation = true;
rebuildUi();
}
Future<List<String>> getOccupations(String country) async =>
['Student', 'Worker'];
// User data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void clearUserData() {
_userData.clear();
}
// Image picker
Future<void> openCamera() async =>
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
Future<void> _openCamera() async {
String? image = await _imagePickerService.camera();
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
}
}
Future<void> openGallery() async =>
runBusyFuture(_openGallery(), busyObject: StateObjects.profileImage);
Future<void> _openGallery() async {
String? image = await _imagePickerService.gallery();
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
}
}
// Navigation
void pop() => _navigationService.back();
// Remote api call
// Get profile
Future<void> getProfile() async => await runBusyFuture(_getProfile());
Future<void> _getProfile() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.getProfileData(_user?.userId);
if (response['status'] == ResponseStatus.success) {
addUserData(response['data']);
}
}
}
// Update profile
Future<void> updateProfile() async => await runBusyFuture(_updateProfile(),
busyObject: StateObjects.profileUpdate);
Future<void> _updateProfile() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.completeProfile(_userData);
if (response['status'] == ResponseStatus.success) {
await _authenticationService.updateUserData(_userData);
pop();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
// Update profile picture
Future<void> updateProfilePicture(String image) async =>
await runBusyFuture(_updateProfilePicture(image));
Future<void> _updateProfilePicture(String image) async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> data = {'profile_picture_url': image};
await _apiService.updateProfileImage(data: data, userId: _user?.userId);
}
}
}

View File

@ -101,7 +101,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
title: viewModel.progresses[index]['title'],
color: viewModel.progresses[index]['color'],
status: viewModel.progresses[index]['status'],
subtitle: viewModel.progresses[index]['subtitle'],
subTitle: viewModel.progresses[index]['subTitle'],
isCompleted: viewModel.progresses[index]['isCompleted'],
),
);
@ -111,7 +111,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
required String title,
required String icon,
required String status,
required String subtitle,
required String subTitle,
required bool isCompleted,
required ProgressViewModel viewModel}) =>
CourseLevelCard(
@ -119,7 +119,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
title: title,
color: color,
status: status,
subtitle: subtitle,
subTitle: subTitle,
isCompleted: isCompleted,
onTap: viewModel.navigateToOngoingProgress,
);

View File

@ -14,24 +14,24 @@ class ProgressViewModel extends BaseViewModel {
'title': 'Beginner',
'isCompleted': true,
'status': 'Completed',
'icon': 'assets/icons/b_1.svg',
'subtitle': 'Youve mastered everyday English basics!',
'icon': 'assets/icons/b1.svg',
'subTitle': 'Youve mastered everyday English basics!',
},
{
'title': 'Elementary',
'isCompleted': false,
'status': 'In Progress',
'color': kcPrimaryColor,
'icon': 'assets/icons/b_1.svg',
'subtitle': 'Continue improving your conversations and fluency.',
'icon': 'assets/icons/b1.svg',
'subTitle': 'Continue improving your conversations and fluency.',
},
{
'title': 'Beginner',
'isCompleted': true,
'status': 'In Progress',
'color': kcPrimaryColor,
'icon': 'assets/icons/b_1.svg',
'subtitle': 'Youve mastered everyday English basics!',
'icon': 'assets/icons/b1.svg',
'subTitle': 'Youve mastered everyday English basics!',
},
];

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/register/screens/create_password_screen.dart';
import 'package:yimaru_app/ui/views/register/screens/register_with_email_screen.dart';
import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart';
@ -25,46 +24,8 @@ import 'register_view.form.dart';
class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
const RegisterView({Key? key}) : super(key: key);
void _initClearData() {
otpController.clear();
emailController.clear();
passwordController.clear();
phoneNumberController.clear();
confirmPasswordController.clear();
}
void _inAppPop(RegisterViewModel viewModel) {
print('OnPop');
print(viewModel.currentPage);
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(RegisterViewModel viewModel) {
if (viewModel.currentPage == 0) {
emailController.clear();
viewModel.resetRegisterWithEmailScreen();
} else if (viewModel.currentPage == 2) {
passwordController.clear();
confirmPasswordController.clear();
viewModel.resetCreatePasswordScreen();
} else if (viewModel.currentPage == 3) {
otpController.clear();
viewModel.resetRegistrationOtpScreen();
}
}
void _pop({required bool value, required RegisterViewModel viewModel}) {
{
if (!value) return;
_clearDataOnNavigation(viewModel);
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
}
}
@override
void onViewModelReady(RegisterViewModel viewModel) {
_initClearData();
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@ -83,14 +44,44 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope(
canPop: false,
onPopInvokedWithResult: (value, data) =>
_pop(value: value, viewModel: viewModel),
child: _buildBody(viewModel));
onPopInvokedWithResult: (value, data) {
if (value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack(
children: [_buildScaffold(viewModel), _buildBusyRegistration(viewModel)]);
Widget _buildScaffold(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(RegisterViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
);
Widget _buildExpandedBody(RegisterViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(RegisterViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(RegisterViewModel viewModel) =>
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildRegisterWithEmailScreen(),
@ -115,4 +106,6 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
passwordController: passwordController,
confirmPasswordController: confirmPasswordController);
Widget _buildBusyRegistration(RegisterViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container();
}

Some files were not shown because too many files have changed in this diff Show More