Merge branch 'release/0.1.0-internal.v1'
- refactor(auth): Apply code refactor for login, register and forget password functionalities
|
|
@ -1,12 +1,12 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
id("com.android.application")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.yimaru_app"
|
namespace = "com.yimaru.lms.app"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
|
@ -15,25 +15,24 @@ android {
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlin {
|
||||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
defaultConfig {
|
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
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
applicationId = "com.yimaru.lms.app"
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
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")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
android/app/google-services.json
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<application
|
||||||
android:label="yimaru_app"
|
android:label="Yimaru"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.example.yimaru_app
|
package com.yimaru.lms.app
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
|
@ -19,8 +19,10 @@ pluginManagement {
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "8.13.2" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.20" 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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
|
||||||
5
assets/icons/a_1.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
5
assets/icons/a_2.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,10 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
5
assets/icons/b_1.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
5
assets/icons/b_2.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 1.4 MiB |
1
firebase.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"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"}}}}}}
|
||||||
|
|
@ -368,7 +368,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -384,7 +384,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
|
@ -401,7 +401,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
|
@ -416,7 +416,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
|
@ -547,7 +547,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
|
@ -569,7 +569,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,14 @@ 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/welcome/welcome_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/assessment/assessment_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/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
|
// @stacked-import
|
||||||
|
|
||||||
@StackedApp(
|
@StackedApp(
|
||||||
|
|
@ -57,6 +65,10 @@ import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
|
||||||
MaterialRoute(page: WelcomeView),
|
MaterialRoute(page: WelcomeView),
|
||||||
MaterialRoute(page: AssessmentView),
|
MaterialRoute(page: AssessmentView),
|
||||||
MaterialRoute(page: LearnLessonView),
|
MaterialRoute(page: LearnLessonView),
|
||||||
|
MaterialRoute(page: FailureView),
|
||||||
|
MaterialRoute(page: ForgetPasswordView),
|
||||||
|
MaterialRoute(page: LearnLessonDetailView),
|
||||||
|
MaterialRoute(page: LearnPracticeView),
|
||||||
// @stacked-route
|
// @stacked-route
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|
@ -68,6 +80,10 @@ import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
|
||||||
LazySingleton(classType: SecureStorageService),
|
LazySingleton(classType: SecureStorageService),
|
||||||
LazySingleton(classType: DioService),
|
LazySingleton(classType: DioService),
|
||||||
LazySingleton(classType: StatusCheckerService),
|
LazySingleton(classType: StatusCheckerService),
|
||||||
|
LazySingleton(classType: PermissionHandlerService),
|
||||||
|
LazySingleton(classType: ImagePickerService),
|
||||||
|
LazySingleton(classType: GoogleAuthService),
|
||||||
|
LazySingleton(classType: ImageDownloaderService),
|
||||||
// @stacked-service
|
// @stacked-service
|
||||||
],
|
],
|
||||||
bottomsheets: [
|
bottomsheets: [
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ import 'package:stacked_shared/stacked_shared.dart';
|
||||||
import '../services/api_service.dart';
|
import '../services/api_service.dart';
|
||||||
import '../services/authentication_service.dart';
|
import '../services/authentication_service.dart';
|
||||||
import '../services/dio_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/secure_storage_service.dart';
|
||||||
import '../services/status_checker_service.dart';
|
import '../services/status_checker_service.dart';
|
||||||
|
|
||||||
|
|
@ -36,4 +40,8 @@ Future<void> setupLocator({
|
||||||
locator.registerLazySingleton(() => SecureStorageService());
|
locator.registerLazySingleton(() => SecureStorageService());
|
||||||
locator.registerLazySingleton(() => DioService());
|
locator.registerLazySingleton(() => DioService());
|
||||||
locator.registerLazySingleton(() => StatusCheckerService());
|
locator.registerLazySingleton(() => StatusCheckerService());
|
||||||
|
locator.registerLazySingleton(() => PermissionHandlerService());
|
||||||
|
locator.registerLazySingleton(() => ImagePickerService());
|
||||||
|
locator.registerLazySingleton(() => GoogleAuthService());
|
||||||
|
locator.registerLazySingleton(() => ImageDownloaderService());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,31 @@
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
import 'package:flutter/material.dart' as _i25;
|
import 'package:flutter/material.dart' as _i29;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart' as _i1;
|
import 'package:stacked/stacked.dart' as _i1;
|
||||||
import 'package:stacked_services/stacked_services.dart' as _i26;
|
import 'package:stacked_services/stacked_services.dart' as _i30;
|
||||||
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
|
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
|
||||||
as _i10;
|
as _i10;
|
||||||
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
|
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
|
||||||
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'
|
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'
|
||||||
as _i13;
|
as _i13;
|
||||||
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7;
|
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/home/home_view.dart' as _i2;
|
||||||
import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14;
|
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/learn_view.dart' as _i19;
|
||||||
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'
|
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'
|
||||||
as _i24;
|
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_level/learn_level_view.dart' as _i20;
|
||||||
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
|
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
|
||||||
as _i21;
|
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/login/login_view.dart' as _i18;
|
||||||
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3;
|
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3;
|
||||||
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart'
|
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart'
|
||||||
|
|
@ -89,6 +96,14 @@ class Routes {
|
||||||
|
|
||||||
static const learnLessonView = '/learn-lesson-view';
|
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>{
|
static const all = <String>{
|
||||||
homeView,
|
homeView,
|
||||||
onboardingView,
|
onboardingView,
|
||||||
|
|
@ -113,6 +128,10 @@ class Routes {
|
||||||
welcomeView,
|
welcomeView,
|
||||||
assessmentView,
|
assessmentView,
|
||||||
learnLessonView,
|
learnLessonView,
|
||||||
|
failureView,
|
||||||
|
forgetPasswordView,
|
||||||
|
learnLessonDetailView,
|
||||||
|
learnPracticeView,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,17 +229,33 @@ class StackedRouter extends _i1.RouterBase {
|
||||||
Routes.learnLessonView,
|
Routes.learnLessonView,
|
||||||
page: _i24.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>{
|
final _pagesMap = <Type, _i1.StackedRouteFactory>{
|
||||||
_i2.HomeView: (data) {
|
_i2.HomeView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i2.HomeView(),
|
builder: (context) => const _i2.HomeView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i3.OnboardingView: (data) {
|
_i3.OnboardingView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i3.OnboardingView(),
|
builder: (context) => const _i3.OnboardingView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
|
|
@ -229,133 +264,159 @@ class StackedRouter extends _i1.RouterBase {
|
||||||
final args = data.getArgs<StartupViewArguments>(
|
final args = data.getArgs<StartupViewArguments>(
|
||||||
orElse: () => const StartupViewArguments(),
|
orElse: () => const StartupViewArguments(),
|
||||||
);
|
);
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => _i4.StartupView(key: args.key, label: args.label),
|
builder: (context) => _i4.StartupView(key: args.key, label: args.label),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i5.ProfileView: (data) {
|
_i5.ProfileView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i5.ProfileView(),
|
builder: (context) => const _i5.ProfileView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i6.ProfileDetailView: (data) {
|
_i6.ProfileDetailView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i6.ProfileDetailView(),
|
builder: (context) => const _i6.ProfileDetailView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i7.DownloadsView: (data) {
|
_i7.DownloadsView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i7.DownloadsView(),
|
builder: (context) => const _i7.DownloadsView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i8.ProgressView: (data) {
|
_i8.ProgressView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i8.ProgressView(),
|
builder: (context) => const _i8.ProgressView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i9.OngoingProgressView: (data) {
|
_i9.OngoingProgressView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i9.OngoingProgressView(),
|
builder: (context) => const _i9.OngoingProgressView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i10.AccountPrivacyView: (data) {
|
_i10.AccountPrivacyView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i10.AccountPrivacyView(),
|
builder: (context) => const _i10.AccountPrivacyView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i11.SupportView: (data) {
|
_i11.SupportView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i11.SupportView(),
|
builder: (context) => const _i11.SupportView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i12.TelegramSupportView: (data) {
|
_i12.TelegramSupportView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i12.TelegramSupportView(),
|
builder: (context) => const _i12.TelegramSupportView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i13.CallSupportView: (data) {
|
_i13.CallSupportView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i13.CallSupportView(),
|
builder: (context) => const _i13.CallSupportView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i14.LanguageView: (data) {
|
_i14.LanguageView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i14.LanguageView(),
|
builder: (context) => const _i14.LanguageView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i15.PrivacyPolicyView: (data) {
|
_i15.PrivacyPolicyView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i15.PrivacyPolicyView(),
|
builder: (context) => const _i15.PrivacyPolicyView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i16.TermsAndConditionsView: (data) {
|
_i16.TermsAndConditionsView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i16.TermsAndConditionsView(),
|
builder: (context) => const _i16.TermsAndConditionsView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i17.RegisterView: (data) {
|
_i17.RegisterView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i17.RegisterView(),
|
builder: (context) => const _i17.RegisterView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i18.LoginView: (data) {
|
_i18.LoginView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i18.LoginView(),
|
builder: (context) => const _i18.LoginView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i19.LearnView: (data) {
|
_i19.LearnView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i19.LearnView(),
|
builder: (context) => const _i19.LearnView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i20.LearnLevelView: (data) {
|
_i20.LearnLevelView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i20.LearnLevelView(),
|
builder: (context) => const _i20.LearnLevelView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i21.LearnModuleView: (data) {
|
_i21.LearnModuleView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i21.LearnModuleView(),
|
builder: (context) => const _i21.LearnModuleView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i22.WelcomeView: (data) {
|
_i22.WelcomeView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i22.WelcomeView(),
|
builder: (context) => const _i22.WelcomeView(),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i23.AssessmentView: (data) {
|
_i23.AssessmentView: (data) {
|
||||||
final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
|
final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
_i23.AssessmentView(key: args.key, data: args.data),
|
_i23.AssessmentView(key: args.key, data: args.data),
|
||||||
settings: data,
|
settings: data,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
_i24.LearnLessonView: (data) {
|
_i24.LearnLessonView: (data) {
|
||||||
return _i25.MaterialPageRoute<dynamic>(
|
return _i29.MaterialPageRoute<dynamic>(
|
||||||
builder: (context) => const _i24.LearnLessonView(),
|
builder: (context) => const _i24.LearnLessonView(),
|
||||||
settings: data,
|
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
|
@override
|
||||||
|
|
@ -371,7 +432,7 @@ class StartupViewArguments {
|
||||||
this.label = 'Loading',
|
this.label = 'Loading',
|
||||||
});
|
});
|
||||||
|
|
||||||
final _i25.Key? key;
|
final _i29.Key? key;
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
|
@ -398,7 +459,7 @@ class AssessmentViewArguments {
|
||||||
required this.data,
|
required this.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
final _i25.Key? key;
|
final _i29.Key? key;
|
||||||
|
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
|
|
||||||
|
|
@ -419,7 +480,34 @@ class AssessmentViewArguments {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NavigatorStateExtension on _i26.NavigationService {
|
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 {
|
||||||
Future<dynamic> navigateToHomeView([
|
Future<dynamic> navigateToHomeView([
|
||||||
int? routerId,
|
int? routerId,
|
||||||
bool preventDuplicates = true,
|
bool preventDuplicates = true,
|
||||||
|
|
@ -449,7 +537,7 @@ extension NavigatorStateExtension on _i26.NavigationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> navigateToStartupView({
|
Future<dynamic> navigateToStartupView({
|
||||||
_i25.Key? key,
|
_i29.Key? key,
|
||||||
String label = 'Loading',
|
String label = 'Loading',
|
||||||
int? routerId,
|
int? routerId,
|
||||||
bool preventDuplicates = true,
|
bool preventDuplicates = true,
|
||||||
|
|
@ -718,7 +806,7 @@ extension NavigatorStateExtension on _i26.NavigationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> navigateToAssessmentView({
|
Future<dynamic> navigateToAssessmentView({
|
||||||
_i25.Key? key,
|
_i29.Key? key,
|
||||||
required Map<String, dynamic> data,
|
required Map<String, dynamic> data,
|
||||||
int? routerId,
|
int? routerId,
|
||||||
bool preventDuplicates = true,
|
bool preventDuplicates = true,
|
||||||
|
|
@ -748,6 +836,65 @@ extension NavigatorStateExtension on _i26.NavigationService {
|
||||||
transition: transition);
|
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([
|
Future<dynamic> replaceWithHomeView([
|
||||||
int? routerId,
|
int? routerId,
|
||||||
bool preventDuplicates = true,
|
bool preventDuplicates = true,
|
||||||
|
|
@ -777,7 +924,7 @@ extension NavigatorStateExtension on _i26.NavigationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> replaceWithStartupView({
|
Future<dynamic> replaceWithStartupView({
|
||||||
_i25.Key? key,
|
_i29.Key? key,
|
||||||
String label = 'Loading',
|
String label = 'Loading',
|
||||||
int? routerId,
|
int? routerId,
|
||||||
bool preventDuplicates = true,
|
bool preventDuplicates = true,
|
||||||
|
|
@ -1046,7 +1193,7 @@ extension NavigatorStateExtension on _i26.NavigationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> replaceWithAssessmentView({
|
Future<dynamic> replaceWithAssessmentView({
|
||||||
_i25.Key? key,
|
_i29.Key? key,
|
||||||
required Map<String, dynamic> data,
|
required Map<String, dynamic> data,
|
||||||
int? routerId,
|
int? routerId,
|
||||||
bool preventDuplicates = true,
|
bool preventDuplicates = true,
|
||||||
|
|
@ -1075,4 +1222,63 @@ extension NavigatorStateExtension on _i26.NavigationService {
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
transition: transition);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
lib/firebase_options.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
// 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,35 @@
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:yimaru_app/models/option.dart';
|
import 'package:yimaru_app/models/option.dart';
|
||||||
import 'package:yimaru_app/models/question.dart';
|
|
||||||
part 'assessment.g.dart';
|
part 'assessment.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Assessment {
|
class Assessment {
|
||||||
@JsonKey(name: 'Question')
|
final int? id;
|
||||||
final Question? question;
|
|
||||||
|
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: 'Options')
|
|
||||||
final List<Option>? options;
|
final List<Option>? options;
|
||||||
|
|
||||||
const Assessment({this.options, this.question});
|
const Assessment({
|
||||||
|
this.id,
|
||||||
|
this.points,
|
||||||
|
this.status,
|
||||||
|
this.options,
|
||||||
|
this.questionText,
|
||||||
|
this.questionType,
|
||||||
|
this.difficultyLevel,
|
||||||
|
});
|
||||||
|
|
||||||
factory Assessment.fromJson(Map<String, dynamic> json) =>
|
factory Assessment.fromJson(Map<String, dynamic> json) =>
|
||||||
_$AssessmentFromJson(json);
|
_$AssessmentFromJson(json);
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,24 @@ part of 'assessment.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
Assessment _$AssessmentFromJson(Map<String, dynamic> json) => Assessment(
|
Assessment _$AssessmentFromJson(Map<String, dynamic> json) => Assessment(
|
||||||
options: (json['Options'] as List<dynamic>?)
|
id: (json['id'] as num?)?.toInt(),
|
||||||
|
points: (json['points'] as num?)?.toInt(),
|
||||||
|
status: json['status'] as String?,
|
||||||
|
options: (json['options'] as List<dynamic>?)
|
||||||
?.map((e) => Option.fromJson(e as Map<String, dynamic>))
|
?.map((e) => Option.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
question: json['Question'] == null
|
questionText: json['question_text'] as String?,
|
||||||
? null
|
questionType: json['question_type'] as String?,
|
||||||
: Question.fromJson(json['Question'] as Map<String, dynamic>),
|
difficultyLevel: json['difficulty_level'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$AssessmentToJson(Assessment instance) =>
|
Map<String, dynamic> _$AssessmentToJson(Assessment instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'Question': instance.question,
|
'id': instance.id,
|
||||||
'Options': instance.options,
|
'points': instance.points,
|
||||||
|
'status': instance.status,
|
||||||
|
'question_type': instance.questionType,
|
||||||
|
'question_text': instance.questionText,
|
||||||
|
'difficulty_level': instance.difficultyLevel,
|
||||||
|
'options': instance.options,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@ part 'option.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Option {
|
class Option {
|
||||||
@JsonKey(name: 'question_id')
|
final int? id;
|
||||||
final int? questionId;
|
|
||||||
|
|
||||||
@JsonKey(name: 'option_text')
|
@JsonKey(name: 'option_text')
|
||||||
final String? optionText;
|
final String? optionText;
|
||||||
|
|
||||||
const Option({this.optionText, this.questionId});
|
@JsonKey(name: 'is_correct')
|
||||||
|
final bool? isCorrect;
|
||||||
|
|
||||||
|
const Option({this.id, this.optionText, this.isCorrect});
|
||||||
|
|
||||||
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
|
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ part of 'option.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
Option _$OptionFromJson(Map<String, dynamic> json) => Option(
|
Option _$OptionFromJson(Map<String, dynamic> json) => Option(
|
||||||
|
id: (json['id'] as num?)?.toInt(),
|
||||||
optionText: json['option_text'] as String?,
|
optionText: json['option_text'] as String?,
|
||||||
questionId: (json['question_id'] as num?)?.toInt(),
|
isCorrect: json['is_correct'] as bool?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
|
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
|
||||||
'question_id': instance.questionId,
|
'id': instance.id,
|
||||||
'option_text': instance.optionText,
|
'option_text': instance.optionText,
|
||||||
|
'is_correct': instance.isCorrect,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
|
|
@ -4,10 +4,31 @@ part 'user_model.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class UserModel {
|
class UserModel {
|
||||||
|
final String? email;
|
||||||
|
|
||||||
|
final String? gender;
|
||||||
|
|
||||||
|
final String? region;
|
||||||
|
|
||||||
|
final String? country;
|
||||||
|
|
||||||
|
|
||||||
|
final String? occupation;
|
||||||
|
|
||||||
|
final bool? userInfoLoaded;
|
||||||
|
|
||||||
|
|
||||||
@JsonKey(name: 'user_id')
|
@JsonKey(name: 'user_id')
|
||||||
final int? userId;
|
final int? userId;
|
||||||
|
|
||||||
final bool? profileCompleted;
|
@JsonKey(name: 'last_name')
|
||||||
|
final String? lastName;
|
||||||
|
|
||||||
|
@JsonKey(name: 'birth_day')
|
||||||
|
final String? birthday;
|
||||||
|
|
||||||
|
@JsonKey(name: 'first_name')
|
||||||
|
final String? firstName;
|
||||||
|
|
||||||
@JsonKey(name: 'access_token')
|
@JsonKey(name: 'access_token')
|
||||||
final String? accessToken;
|
final String? accessToken;
|
||||||
|
|
@ -15,11 +36,28 @@ class UserModel {
|
||||||
@JsonKey(name: 'refresh_token')
|
@JsonKey(name: 'refresh_token')
|
||||||
final String? refreshToken;
|
final String? refreshToken;
|
||||||
|
|
||||||
const UserModel(
|
@JsonKey(name: 'profile_completed')
|
||||||
{this.userId,
|
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,
|
||||||
this.accessToken,
|
this.accessToken,
|
||||||
|
this.refreshToken,
|
||||||
|
this.profilePicture,
|
||||||
|
this.userInfoLoaded ,
|
||||||
this.profileCompleted,
|
this.profileCompleted,
|
||||||
this.refreshToken});
|
});
|
||||||
|
|
||||||
factory UserModel.fromJson(Map<String, dynamic> json) =>
|
factory UserModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$UserModelFromJson(json);
|
_$UserModelFromJson(json);
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,35 @@ part of 'user_model.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
|
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(),
|
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?,
|
accessToken: json['access_token'] as String?,
|
||||||
profileCompleted: json['profileCompleted'] as bool?,
|
|
||||||
refreshToken: json['refresh_token'] as String?,
|
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>{
|
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,
|
'user_id': instance.userId,
|
||||||
'profileCompleted': instance.profileCompleted,
|
'last_name': instance.lastName,
|
||||||
|
'birth_day': instance.birthday,
|
||||||
|
'first_name': instance.firstName,
|
||||||
'access_token': instance.accessToken,
|
'access_token': instance.accessToken,
|
||||||
'refresh_token': instance.refreshToken,
|
'refresh_token': instance.refreshToken,
|
||||||
|
'profile_completed': instance.profileCompleted,
|
||||||
|
'profile_picture_url': instance.profilePicture,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ class ApiService {
|
||||||
final _service = locator<DioService>();
|
final _service = locator<DioService>();
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
Future<Map<String, dynamic>> register(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$baseUrl/$userUrl/$kRegisterUrl',
|
'$kBaseUrl/$kUserUrl/$kRegisterUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -29,19 +29,19 @@ class ApiService {
|
||||||
'message': 'Unknown Error Occurred'
|
'message': 'Unknown Error Occurred'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} on DioException catch (e) {
|
||||||
return {
|
return {
|
||||||
'message': e.toString(),
|
|
||||||
'status': ResponseStatus.failure,
|
'status': ResponseStatus.failure,
|
||||||
|
'message': e.response?.data.toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login
|
// Email Login
|
||||||
Future<Map<String, dynamic>> login(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$baseUrl/$kLoginUrl',
|
'$kBaseUrl/$kLoginUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -57,10 +57,38 @@ class ApiService {
|
||||||
'message': '${response.data['message']}, ${response.data['error']}'
|
'message': '${response.data['message']}, ${response.data['error']}'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} on DioException catch (e) {
|
||||||
return {
|
return {
|
||||||
'message': e.toString(),
|
|
||||||
'status': ResponseStatus.failure,
|
'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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,14 +97,14 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$baseUrl/$userUrl/$kVerifyOtpUrl',
|
'$kBaseUrl/$kUserUrl/$kVerifyOtpUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return {
|
return {
|
||||||
'status': ResponseStatus.success,
|
'status': ResponseStatus.success,
|
||||||
'message': 'Otp verified successfully',
|
'message': 'Otp verified successfully',
|
||||||
//'data': UserModel.fromJson(response.data['data']),
|
'data': UserModel.fromJson(response.data['data']),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
|
@ -84,10 +112,10 @@ class ApiService {
|
||||||
'message': '${response.data['message']}, ${response.data['error']}'
|
'message': '${response.data['message']}, ${response.data['error']}'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} on DioException catch (e) {
|
||||||
return {
|
return {
|
||||||
'message': e.toString(),
|
|
||||||
'status': ResponseStatus.failure,
|
'status': ResponseStatus.failure,
|
||||||
|
'message': e.response?.data.toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +124,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$baseUrl/$userUrl/$kResendOtpUrl',
|
'$kBaseUrl/$kUserUrl/$kResendOtpUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -111,10 +139,65 @@ class ApiService {
|
||||||
'message': 'Unknown Error Occurred'
|
'message': 'Unknown Error Occurred'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} on DioException catch (e) {
|
||||||
return {
|
return {
|
||||||
'message': e.toString(),
|
|
||||||
'status': ResponseStatus.failure,
|
'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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +206,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
|
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.get(
|
Response response = await _service.dio.get(
|
||||||
'$baseUrl/$userUrl/${user?.userId}/$kProfileStatusUrl',
|
'$kBaseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
@ -138,25 +221,49 @@ class ApiService {
|
||||||
'message': '${response.data['message']}, ${response.data['error']}'
|
'message': '${response.data['message']}, ${response.data['error']}'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} on DioException catch (e) {
|
||||||
return {
|
return {
|
||||||
'message': e.toString(),
|
|
||||||
'status': ResponseStatus.failure,
|
'status': ResponseStatus.failure,
|
||||||
|
'message': e.response?.data.toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update profile
|
// Get profile
|
||||||
Future<Map<String, dynamic>> updateProfile(
|
Future<Map<String, dynamic>> getProfileData(int? userId) async {
|
||||||
{required UserModel? user, required Map<String, dynamic> data}) async {
|
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.put(
|
Response response = await _service.dio.get(
|
||||||
'$baseUrl/$userUrl',
|
'$kBaseUrl/$kUserUrl/$kGetUserUrl/$userId',
|
||||||
data: data,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
print(response.statusCode);
|
if (response.statusCode == 200) {
|
||||||
print(response.data);
|
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 {
|
||||||
|
try {
|
||||||
|
Response response = await _service.dio.put(
|
||||||
|
'$kBaseUrl/$kUserUrl',
|
||||||
|
data: data,
|
||||||
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -169,11 +276,62 @@ class ApiService {
|
||||||
'message': 'Unknown Error Occurred'
|
'message': 'Unknown Error Occurred'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} on DioException catch (e) {
|
||||||
print('Exception ${e.toString()}');
|
|
||||||
return {
|
return {
|
||||||
'message': e.toString(),
|
|
||||||
'status': ResponseStatus.failure,
|
'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,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +342,7 @@ class ApiService {
|
||||||
List<Assessment> assessments = [];
|
List<Assessment> assessments = [];
|
||||||
|
|
||||||
final Response response =
|
final Response response =
|
||||||
await _service.dio.get('$baseUrl/$kAssessmentsUrl');
|
await _service.dio.get('$kBaseUrl/$kAssessmentsUrl');
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
var data = response.data;
|
var data = response.data;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/app/app.locator.dart';
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
import 'package:yimaru_app/models/user_model.dart';
|
import 'package:yimaru_app/models/user_model.dart';
|
||||||
import 'package:yimaru_app/services/secure_storage_service.dart';
|
import 'package:yimaru_app/services/secure_storage_service.dart';
|
||||||
|
|
||||||
class AuthenticationService {
|
class AuthenticationService with ListenableServiceMixin {
|
||||||
final _secureService = locator<SecureStorageService>();
|
final _secureService = locator<SecureStorageService>();
|
||||||
|
|
||||||
|
AuthenticationService() {
|
||||||
|
listenToReactiveValues([_user]);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserModel? _user;
|
||||||
|
|
||||||
|
UserModel? get user => _user;
|
||||||
|
|
||||||
Future<bool> userLoggedIn() async {
|
Future<bool> userLoggedIn() async {
|
||||||
if (await _secureService.getString('userId') != null) {
|
if (await _secureService.getString('userId') != null) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -28,14 +37,122 @@ class AuthenticationService {
|
||||||
await _secureService.setString('refreshToken', refresh);
|
await _secureService.setString('refreshToken', refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveUserData(Map<String, dynamic> data) async {
|
|
||||||
|
Future<void> saveUserCredential(Map<String, dynamic> data) async {
|
||||||
await _secureService.setInt('userId', data['userId']);
|
await _secureService.setInt('userId', data['userId']);
|
||||||
await _secureService.setString('accessToken', data['accessToken']);
|
await _secureService.setString('accessToken', data['accessToken']);
|
||||||
await _secureService.setString('refreshToken', data['refreshToken']);
|
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> saveProfileCompleted(bool value) async {
|
Future<void> saveProfileStatus(bool value) async {
|
||||||
await _secureService.setBool('profileCompleted', value);
|
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 =>
|
Future<bool> isFirstTimeInstall() async =>
|
||||||
|
|
@ -45,18 +162,29 @@ class AuthenticationService {
|
||||||
await _secureService.setBool('firstTimeInstall', value);
|
await _secureService.setBool('firstTimeInstall', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UserModel> getUser() async {
|
Future<UserModel?> getUser() async {
|
||||||
UserModel user = UserModel(
|
_user = UserModel(
|
||||||
userId: await _secureService.getInt('userId'),
|
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'),
|
accessToken: await _secureService.getString('accessToken'),
|
||||||
refreshToken: await _secureService.getString('refreshToken'),
|
refreshToken: await _secureService.getString('refreshToken'),
|
||||||
|
profilePicture: await _secureService.getString('profileImage'),
|
||||||
|
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
|
||||||
profileCompleted: await _secureService.getBool('profileCompleted'),
|
profileCompleted: await _secureService.getBool('profileCompleted'),
|
||||||
);
|
);
|
||||||
return user;
|
return _user;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logOut() async {
|
Future<void> logOut() async {
|
||||||
bool firstTimeInstall = await isFirstTimeInstall();
|
bool firstTimeInstall = await isFirstTimeInstall();
|
||||||
|
_user = null;
|
||||||
await _secureService.clear();
|
await _secureService.clear();
|
||||||
await setFirstTimeInstall(firstTimeInstall);
|
await setFirstTimeInstall(firstTimeInstall);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/app/app.router.dart';
|
import 'package:yimaru_app/app/app.router.dart';
|
||||||
import 'package:yimaru_app/models/user_model.dart';
|
import 'package:yimaru_app/models/user_model.dart';
|
||||||
import 'package:yimaru_app/services/authentication_service.dart';
|
import 'package:yimaru_app/services/authentication_service.dart';
|
||||||
import 'package:yimaru_app/services/secure_storage_service.dart';
|
|
||||||
|
|
||||||
import '../app/app.locator.dart';
|
import '../app/app.locator.dart';
|
||||||
import '../ui/common/app_constants.dart';
|
import '../ui/common/app_constants.dart';
|
||||||
|
|
@ -21,7 +20,7 @@ class DioService {
|
||||||
|
|
||||||
DioService() {
|
DioService() {
|
||||||
_dio.options
|
_dio.options
|
||||||
..baseUrl = baseUrl
|
..baseUrl = kBaseUrl
|
||||||
..connectTimeout = const Duration(seconds: 30)
|
..connectTimeout = const Duration(seconds: 30)
|
||||||
..receiveTimeout = const Duration(seconds: 30);
|
..receiveTimeout = const Duration(seconds: 30);
|
||||||
|
|
||||||
|
|
@ -50,10 +49,11 @@ class DioService {
|
||||||
RequestOptions options,
|
RequestOptions options,
|
||||||
RequestInterceptorHandler handler,
|
RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
final token = await _authenticationService.getAccessToken();
|
final access = await _authenticationService.getAccessToken();
|
||||||
|
final refresh = await _authenticationService.getRefreshToken();
|
||||||
|
|
||||||
if (token != null) {
|
if (access != null) {
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
options.headers['Authorization'] = 'Bearer $access';
|
||||||
}
|
}
|
||||||
|
|
||||||
options.headers['Accept'] = 'application/json';
|
options.headers['Accept'] = 'application/json';
|
||||||
|
|
@ -61,6 +61,7 @@ class DioService {
|
||||||
|
|
||||||
debugPrint('️️➡️➡️➡️➡️INITIALIZING REQUEST➡️➡️➡️➡️');
|
debugPrint('️️➡️➡️➡️➡️INITIALIZING REQUEST➡️➡️➡️➡️');
|
||||||
debugPrint('➡️ ${options.method} ${options.uri}');
|
debugPrint('➡️ ${options.method} ${options.uri}');
|
||||||
|
debugPrint('➡️ REFRESH: $refresh');
|
||||||
debugPrint('➡️ HEADERS: ${options.headers}');
|
debugPrint('➡️ HEADERS: ${options.headers}');
|
||||||
debugPrint('➡️ DATA: ${options.data}');
|
debugPrint('➡️ DATA: ${options.data}');
|
||||||
debugPrint('️️➡️➡️➡️➡️FINALIZING REQUEST➡️➡️➡️➡️');
|
debugPrint('️️➡️➡️➡️➡️FINALIZING REQUEST➡️➡️➡️➡️');
|
||||||
|
|
@ -125,33 +126,22 @@ class DioService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _refreshToken() async {
|
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 {
|
try {
|
||||||
Map<String, dynamic> data = {
|
Map<String, dynamic> data = {
|
||||||
'role': 'STUDENT',
|
'role': 'USER',
|
||||||
'user_id': user.userId,
|
'user_id': user?.userId,
|
||||||
'access_token': user.accessToken,
|
'access_token': user?.accessToken,
|
||||||
'refresh_token': user.refreshToken
|
'refresh_token': user?.refreshToken
|
||||||
};
|
};
|
||||||
print(data);
|
|
||||||
final response = await _refreshDio.post(
|
final response = await _refreshDio.post(
|
||||||
'$baseUrl/$kRefreshTokenUrl',
|
'$kBaseUrl/$kRefreshTokenUrl',
|
||||||
data: data,
|
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(
|
await _authenticationService.saveTokens(
|
||||||
access: response.data['access_token'],
|
access: response.data['access_token'],
|
||||||
refresh: response.data['refresh_token'],
|
refresh: response.data['refresh_token'],
|
||||||
|
|
@ -159,10 +149,8 @@ class DioService {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Refresh response exception');
|
await _authenticationService.logOut();
|
||||||
print(e.toString());
|
await _navigationService.replaceWithLoginView();
|
||||||
// await _authenticationService.logOut();
|
|
||||||
// await _navigationService.replaceWithLoginView();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
lib/services/google_auth_service.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
lib/services/image_downloader_service.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/services/image_picker_service.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/services/permission_handler_service.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ const Color kcRed = Color(0xffFF4C4C);
|
||||||
const Color kcGreen = Color(0xFF1DE964);
|
const Color kcGreen = Color(0xFF1DE964);
|
||||||
const Color kcBackgroundColor = kcWhite;
|
const Color kcBackgroundColor = kcWhite;
|
||||||
const Color kcWhite = Color(0xFFFFFFFF);
|
const Color kcWhite = Color(0xFFFFFFFF);
|
||||||
|
const Color kcViolet = Color(0x336A1B9A);
|
||||||
const Color kcIndigo = Color(0xff6A1B9A);
|
const Color kcIndigo = Color(0xff6A1B9A);
|
||||||
const Color kcOrange = Color(0xFFF79400);
|
const Color kcOrange = Color(0xFFF79400);
|
||||||
const Color kcSkyBlue = Color(0xFF28B4CD);
|
const Color kcSkyBlue = Color(0xFF28B4CD);
|
||||||
|
|
@ -13,7 +14,6 @@ const Color kcMediumGrey = Color(0xFF474A54);
|
||||||
const Color kcAquamarine = Color(0xFF1DE9B6);
|
const Color kcAquamarine = Color(0xFF1DE9B6);
|
||||||
const Color kcTransparent = Colors.transparent;
|
const Color kcTransparent = Colors.transparent;
|
||||||
const Color kcPrimaryColor = Color(0xFF9E2891);
|
const Color kcPrimaryColor = Color(0xFF9E2891);
|
||||||
const Color kcPrimaryAccent = Color(0xFF6A1B9A);
|
|
||||||
const Color kcVeryLightGrey = Color(0xFFE3E3E3);
|
const Color kcVeryLightGrey = Color(0xFFE3E3E3);
|
||||||
const Color kcPrimaryColorDark = Color(0xFF300151);
|
const Color kcPrimaryColorDark = Color(0xFF300151);
|
||||||
const Color kcPrimaryColorLight = Color(0x149E2891);
|
const Color kcPrimaryColorLight = Color(0x149E2891);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
String baseUrl = 'http://195.35.29.82:8080';
|
String kBaseUrl = 'http://195.35.29.82:8080';
|
||||||
//String baseUrl = 'https://api.yimaru.yaltopia.com';
|
//String baseUrl = 'https://api.yimaru.yaltopia.com';
|
||||||
|
|
||||||
String userUrl = 'api/v1/user';
|
String kGetUserUrl = 'single';
|
||||||
|
|
||||||
|
String kUserUrl = 'api/v1/user';
|
||||||
|
|
||||||
String kRegisterUrl = 'register';
|
String kRegisterUrl = 'register';
|
||||||
|
|
||||||
|
|
@ -9,10 +11,24 @@ String kVerifyOtpUrl = 'verify-otp';
|
||||||
|
|
||||||
String kResendOtpUrl = 'resend-otp';
|
String kResendOtpUrl = 'resend-otp';
|
||||||
|
|
||||||
|
String kResetPassword = 'resetPassword';
|
||||||
|
|
||||||
|
String kRequestResetCode = 'sendResetCode';
|
||||||
|
|
||||||
|
String kUpdateProfileImage = 'profile-picture';
|
||||||
|
|
||||||
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
||||||
|
|
||||||
String kLoginUrl = 'api/v1/auth/customer-login';
|
String kLoginUrl = 'api/v1/auth/customer-login';
|
||||||
|
|
||||||
String kProfileStatusUrl = 'is-profile-completed';
|
String kProfileStatusUrl = 'is-profile-completed';
|
||||||
|
|
||||||
|
String kGoogleAuthUrl = 'api/v1/auth/google/android';
|
||||||
|
|
||||||
String kAssessmentsUrl = 'api/v1/assessment/questions';
|
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';
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,19 @@ enum ProgressStatuses { pending, started, completed }
|
||||||
|
|
||||||
// Levels
|
// Levels
|
||||||
enum ProficiencyLevels { a1, a2, b1, b2, none }
|
enum ProficiencyLevels { a1, a2, b1, b2, none }
|
||||||
|
|
||||||
|
// State object
|
||||||
|
enum StateObjects {
|
||||||
|
verifyOtp,
|
||||||
|
resendOtp,
|
||||||
|
profileImage,
|
||||||
|
profileUpdate,
|
||||||
|
resetPassword,
|
||||||
|
loginWithEmail,
|
||||||
|
loginWithGoogle,
|
||||||
|
loadLessonVideo,
|
||||||
|
requestResetCode,
|
||||||
|
registerWithEmail,
|
||||||
|
profileCompletion,
|
||||||
|
registerWithGoogle,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -171,30 +172,64 @@ PinTheme errorPinTheme = defaultPin.copyBorderWith(
|
||||||
border: Border.all(color: Colors.red),
|
border: Border.all(color: Colors.red),
|
||||||
);
|
);
|
||||||
|
|
||||||
TextStyle validationStyle = const TextStyle(
|
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(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontWeight: FontWeight.w700,
|
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(
|
TextStyle style25DG600 = const TextStyle(
|
||||||
fontSize: 25,
|
fontSize: 25,
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
TextStyle style12R700 = const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
);
|
|
||||||
|
|
||||||
TextStyle style16DG600 = const TextStyle(
|
TextStyle style16DG600 = const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style18DG500 = const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: kcDarkGrey,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style18DG600 = const TextStyle(
|
TextStyle style18DG600 = const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
|
|
@ -206,17 +241,29 @@ TextStyle style16DG400 = const TextStyle(
|
||||||
color: kcDarkGrey,
|
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(
|
TextStyle style14DG400 = const TextStyle(
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
);
|
);
|
||||||
|
|
||||||
TextStyle style14P400 = const TextStyle(
|
TextStyle style14DG600 = const TextStyle(
|
||||||
color: kcPrimaryColor,
|
color: kcDarkGrey,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
TextStyle style14P600 = const TextStyle(
|
TextStyle validationStyle = const TextStyle(
|
||||||
color: kcPrimaryColor,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
);
|
);
|
||||||
|
|
||||||
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
|
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
|
||||||
|
|
@ -240,27 +287,36 @@ Map<String, Style> htmlStyle = {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ChewieProgressColors buildChewieProgressIndicator = ChewieProgressColors(
|
||||||
|
bufferedColor: kcIndigo,
|
||||||
|
playedColor: kcPrimaryColor,
|
||||||
|
backgroundColor: kcBackgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
Widget buildToastDescription(String message) => Text(
|
Widget buildToastDescription(String message) => Text(
|
||||||
message,
|
message,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500),
|
style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500),
|
||||||
);
|
);
|
||||||
|
|
||||||
void showErrorToast(String message) {
|
void showErrorToast(String message) {
|
||||||
toastification.show(
|
toastification.show(
|
||||||
showIcon: true,
|
showIcon: true,
|
||||||
dragToClose: true,
|
dragToClose: true,
|
||||||
primaryColor: kcRed,
|
|
||||||
showProgressBar: false,
|
showProgressBar: false,
|
||||||
applyBlurEffect: false,
|
applyBlurEffect: false,
|
||||||
icon: const Icon(Icons.check),
|
alignment: Alignment.topCenter,
|
||||||
|
primaryColor: kcBackgroundColor,
|
||||||
type: ToastificationType.success,
|
type: ToastificationType.success,
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
style: ToastificationStyle.fillColored,
|
style: ToastificationStyle.fillColored,
|
||||||
description: buildToastDescription(message),
|
description: buildToastDescription(message),
|
||||||
borderSide: const BorderSide(color: kcWhite),
|
autoCloseDuration: const Duration(seconds: 3),
|
||||||
autoCloseDuration: const Duration(seconds: 5),
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 15),
|
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
borderSide: const BorderSide(color: kcPrimaryColor),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: kcPrimaryColor,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,14 +326,17 @@ void showSuccessToast(String message) {
|
||||||
dragToClose: true,
|
dragToClose: true,
|
||||||
showProgressBar: false,
|
showProgressBar: false,
|
||||||
applyBlurEffect: false,
|
applyBlurEffect: false,
|
||||||
icon: const Icon(Icons.check),
|
alignment: Alignment.topCenter,
|
||||||
primaryColor: kcPrimaryColor,
|
primaryColor: kcBackgroundColor,
|
||||||
type: ToastificationType.success,
|
type: ToastificationType.success,
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
style: ToastificationStyle.fillColored,
|
style: ToastificationStyle.fillColored,
|
||||||
description: buildToastDescription(message),
|
description: buildToastDescription(message),
|
||||||
borderSide: const BorderSide(color: kcWhite),
|
autoCloseDuration: const Duration(seconds: 3),
|
||||||
autoCloseDuration: const Duration(seconds: 5),
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 15),
|
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
borderSide: const BorderSide(color: kcPrimaryColor),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: kcPrimaryColor,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,11 +107,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
|
|
||||||
Widget _buildHeader(String title) => Text(
|
Widget _buildHeader(String title) => Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: style18DG600,
|
||||||
fontSize: 18,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
|
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
||||||
const AssessmentView({Key? key, required this.data}) : super(key: key);
|
const AssessmentView({Key? key, required this.data}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(AssessmentViewModel viewModel) {
|
void onViewModelReady(AssessmentViewModel viewModel) async {
|
||||||
viewModel.getAssessments();
|
|
||||||
viewModel.initUserData(data);
|
viewModel.initUserData(data);
|
||||||
|
await viewModel.getAssessments();
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,10 +39,12 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
||||||
List<Widget> _buildScreens() => [
|
List<Widget> _buildScreens() => [
|
||||||
_buildAssessmentIntro(),
|
_buildAssessmentIntro(),
|
||||||
_buildAssessment(),
|
_buildAssessment(),
|
||||||
// _buildAssessmentFailure(),
|
/*
|
||||||
// _buildRetakeAssessment(),
|
_buildAssessmentFailure(),
|
||||||
// _buildResultAnalysis(),
|
_buildRetakeAssessment(),
|
||||||
// _buildAssessmentCompletion(),
|
_buildResultAnalysis(),
|
||||||
|
_buildAssessmentCompletion(),
|
||||||
|
*/
|
||||||
_buildAssessmentResult(),
|
_buildAssessmentResult(),
|
||||||
_buildStartLesson(),
|
_buildStartLesson(),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,25 @@ import 'dart:math';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.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 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../app/app.router.dart';
|
import '../../../app/app.router.dart';
|
||||||
import '../../../models/assessment.dart';
|
import '../../../models/assessment.dart';
|
||||||
import '../../../models/user_model.dart';
|
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/authentication_service.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
import '../../common/ui_helpers.dart';
|
||||||
import '../home/home_view.dart';
|
import '../home/home_view.dart';
|
||||||
|
|
||||||
class AssessmentViewModel extends BaseViewModel {
|
class AssessmentViewModel extends BaseViewModel {
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
|
final _dialogService = locator<DialogService>();
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
|
||||||
|
|
||||||
|
// In-app navigation
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
|
|
||||||
int get currentPage => _currentPage;
|
int get currentPage => _currentPage;
|
||||||
|
|
@ -31,7 +35,6 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
int get previousPage => _previousPage;
|
int get previousPage => _previousPage;
|
||||||
|
|
||||||
// Assessment
|
// Assessment
|
||||||
|
|
||||||
int _currentQuestion = 0;
|
int _currentQuestion = 0;
|
||||||
|
|
||||||
int get currentQuestion => _currentQuestion;
|
int get currentQuestion => _currentQuestion;
|
||||||
|
|
@ -54,7 +57,6 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
Map<String, dynamic> get userData => _userData;
|
Map<String, dynamic> get userData => _userData;
|
||||||
|
|
||||||
// Assessment
|
// Assessment
|
||||||
|
|
||||||
int countCorrectAnswersUntil(int untilQuestion) {
|
int countCorrectAnswersUntil(int untilQuestion) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
||||||
|
|
@ -73,9 +75,6 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
if (_currentQuestion == 5) {
|
if (_currentQuestion == 5) {
|
||||||
// A1
|
// A1
|
||||||
final correctCount = countCorrectAnswersUntil(5);
|
final correctCount = countCorrectAnswersUntil(5);
|
||||||
print('All : $_selectedAnswers');
|
|
||||||
print('Question page : $_currentQuestion');
|
|
||||||
print('Correct A1: $correctCount');
|
|
||||||
|
|
||||||
if (correctCount > 3) {
|
if (correctCount > 3) {
|
||||||
return {'continue': true, 'level': ProficiencyLevels.a1};
|
return {'continue': true, 'level': ProficiencyLevels.a1};
|
||||||
|
|
@ -86,9 +85,6 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
// A2
|
// A2
|
||||||
|
|
||||||
final correctCount = countCorrectAnswersUntil(10);
|
final correctCount = countCorrectAnswersUntil(10);
|
||||||
print('All : $_selectedAnswers');
|
|
||||||
print('Question page : $_currentQuestion');
|
|
||||||
print('Correct A2: $correctCount');
|
|
||||||
|
|
||||||
if (correctCount > 3) {
|
if (correctCount > 3) {
|
||||||
return {'continue': true, 'level': ProficiencyLevels.a2};
|
return {'continue': true, 'level': ProficiencyLevels.a2};
|
||||||
|
|
@ -98,9 +94,6 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
} else if (_currentQuestion == 16) {
|
} else if (_currentQuestion == 16) {
|
||||||
// B1
|
// B1
|
||||||
final correctCount = countCorrectAnswersUntil(16);
|
final correctCount = countCorrectAnswersUntil(16);
|
||||||
print('All : $_selectedAnswers');
|
|
||||||
print('Question page : $_currentQuestion');
|
|
||||||
print('Correct B1: $correctCount');
|
|
||||||
|
|
||||||
if (correctCount > 4) {
|
if (correctCount > 4) {
|
||||||
return {'continue': true, 'level': ProficiencyLevels.b1};
|
return {'continue': true, 'level': ProficiencyLevels.b1};
|
||||||
|
|
@ -109,12 +102,9 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
}
|
}
|
||||||
} else if (_currentQuestion == 22) {
|
} else if (_currentQuestion == 22) {
|
||||||
final correctCount = countCorrectAnswersUntil(16);
|
final correctCount = countCorrectAnswersUntil(16);
|
||||||
print('All : $_selectedAnswers');
|
|
||||||
print('Question page : $_currentQuestion');
|
|
||||||
print('Correct B2: $correctCount');
|
|
||||||
|
|
||||||
if (correctCount > 4) {
|
if (correctCount > 4) {
|
||||||
return {'continue': true, 'level': ProficiencyLevels.b2};
|
return {'continue': false, 'level': ProficiencyLevels.b2};
|
||||||
} else {
|
} else {
|
||||||
return {'continue': false, 'level': ProficiencyLevels.b2};
|
return {'continue': false, 'level': ProficiencyLevels.b2};
|
||||||
}
|
}
|
||||||
|
|
@ -123,18 +113,20 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSelectedAnswer({required int question, required String option}) {
|
void setSelectedAnswer({required int question, required Option? option}) {
|
||||||
bool correct = false;
|
bool correct = false;
|
||||||
final generator = Random();
|
if (option?.isCorrect ?? false) {
|
||||||
int random = generator.nextInt(4);
|
|
||||||
if (option == _assessments[question - 1].options?[random].optionText) {
|
|
||||||
correct = true;
|
correct = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = {
|
final data = {
|
||||||
question.toString(): {
|
question.toString(): {
|
||||||
'option': option,
|
|
||||||
'correct': correct,
|
'correct': correct,
|
||||||
'answer': _assessments[question - 1].options?[random].optionText
|
'option': option?.optionText,
|
||||||
|
'answer': _assessments[question - 1]
|
||||||
|
.options
|
||||||
|
?.firstWhere((e) => e.isCorrect ?? false)
|
||||||
|
.optionText
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -147,22 +139,6 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
return _selectedAnswers[question.toString()]?['option'] == answer;
|
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
|
// Add user data
|
||||||
void initUserData(Map<String, dynamic> data) {
|
void initUserData(Map<String, dynamic> data) {
|
||||||
clearUserData();
|
clearUserData();
|
||||||
|
|
@ -177,38 +153,48 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
_userData.clear();
|
_userData.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete profile
|
// Dialog
|
||||||
Future<void> completeProfile() async {
|
Future<bool?> showAbortDialog() async {
|
||||||
Map<String, dynamic> response =
|
DialogResponse? response = await _dialogService.showDialog(
|
||||||
await runBusyFuture<Map<String, dynamic>>(_completeProfile());
|
cancelTitle: 'No',
|
||||||
|
buttonTitle: 'Yes',
|
||||||
|
barrierDismissible: true,
|
||||||
|
title: 'Abort Assessment',
|
||||||
|
cancelTitleColor: kcDarkGrey,
|
||||||
|
buttonTitleColor: kcPrimaryColor,
|
||||||
|
description: 'Are you sure to abort the assessment ?',
|
||||||
|
);
|
||||||
|
return response?.confirmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _completeProfile() async {
|
Future<void> abort() async {
|
||||||
print(_userData);
|
bool? response = await showAbortDialog();
|
||||||
UserModel user = await _authenticationService.getUser();
|
if (response != null && response) {
|
||||||
Map<String, dynamic> response =
|
next(page: 3);
|
||||||
await _apiService.updateProfile(data: _userData, user: user);
|
}
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation
|
// Question navigation
|
||||||
|
|
||||||
void nextQuestion() {
|
void nextQuestion() {
|
||||||
_currentQuestion++;
|
_currentQuestion++;
|
||||||
Map<String, dynamic> response = evaluateAssessment();
|
Map<String, dynamic> response = evaluateAssessment();
|
||||||
|
|
||||||
|
if (_currentQuestion == _assessments.length) {
|
||||||
|
_proficiencyLevel = response['level'];
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
if (response['level'] == ProficiencyLevels.none) {
|
if (response['level'] == ProficiencyLevels.none) {
|
||||||
_pageController.jumpToPage(_currentQuestion);
|
_pageController.jumpToPage(_currentQuestion);
|
||||||
} else {
|
} else {
|
||||||
if (response['continue']) {
|
if (response['continue']) {
|
||||||
_pageController.jumpToPage(_currentQuestion);
|
_pageController.jumpToPage(_currentQuestion);
|
||||||
}
|
} else {
|
||||||
{
|
|
||||||
_proficiencyLevel = response['level'];
|
_proficiencyLevel = response['level'];
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,8 +204,6 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
_pageController.previousPage(
|
_pageController.previousPage(
|
||||||
duration: const Duration(microseconds: 100), curve: Curves.linear);
|
duration: const Duration(microseconds: 100), curve: Curves.linear);
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
} else {
|
|
||||||
_navigationService.back();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,15 +222,54 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop() {
|
void pop() {
|
||||||
if (_currentPage != 0) {
|
if (_currentPage == 0 || _currentPage == 3 /*7*/) {
|
||||||
|
_navigationService.back();
|
||||||
|
} else if (_currentPage != 0 && _currentPage != 3) {
|
||||||
_currentPage--;
|
_currentPage--;
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
Future<void> navigateToLanguage() async =>
|
Future<void> navigateToLanguage() async =>
|
||||||
await _navigationService.navigateToLanguageView();
|
await _navigationService.navigateToLanguageView();
|
||||||
|
|
||||||
Future<void> replaceWithHome() async =>
|
Future<void> replaceWithHome() async =>
|
||||||
await _navigationService.clearStackAndShowView(const HomeView());
|
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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,27 +61,23 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
'assets/icons/complete.svg',
|
'assets/icons/complete.svg',
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Assessment complete!',
|
'Assessment complete!',
|
||||||
|
style: style25DG600,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'We’re now analyzing your speaking skills',
|
'We’re now analyzing your speaking skills',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
||||||
|
|
@ -94,8 +90,8 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
height: 55,
|
height: 55,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
text: 'View My Results',
|
text: 'View My Results',
|
||||||
onTap: () => viewModel.next(),
|
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
|
onTap: () => viewModel.next(),
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,25 +64,21 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg');
|
Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg');
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'We didn’t get enough from your assessment',
|
'We didn’t get enough from your assessment',
|
||||||
|
style: style25DG600,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Your assessment wasn’t long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
|
'Your assessment wasn’t long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||||
|
|
@ -117,9 +113,9 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Skip',
|
text: 'Skip',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
backgroundColor: kcWhite,
|
||||||
borderColor: kcPrimaryColor,
|
borderColor: kcPrimaryColor,
|
||||||
onTap: () => viewModel.next(),
|
onTap: () => viewModel.next(),
|
||||||
backgroundColor: kcWhite,
|
|
||||||
foregroundColor: kcPrimaryColor,
|
foregroundColor: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,21 @@ import 'assessment_loading_screen.dart';
|
||||||
class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
const AssessmentFormScreen({super.key});
|
const AssessmentFormScreen({super.key});
|
||||||
|
|
||||||
//final PageController _pageController = PageController();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, AssessmentViewModel viewModel) =>
|
Widget build(BuildContext context, AssessmentViewModel viewModel) =>
|
||||||
_buildAssessmentScreens(viewModel);
|
_buildAssessmentScreens(viewModel);
|
||||||
|
|
||||||
Widget _buildAssessmentScreens(AssessmentViewModel viewModel) =>
|
Widget _buildAssessmentScreens(AssessmentViewModel viewModel) =>
|
||||||
viewModel.isBusy
|
viewModel.isBusy || viewModel.assessments.isEmpty
|
||||||
? _buildPageLoadingIndicator()
|
? _buildPageLoadingIndicator(viewModel)
|
||||||
: _buildAssessmentScreensWrapper(viewModel);
|
: _buildAssessmentScreensWrapper(viewModel);
|
||||||
|
|
||||||
Widget _buildPageLoadingIndicator() => const AssessmentLoadingScreen();
|
Widget _buildPageLoadingIndicator(AssessmentViewModel viewModel) =>
|
||||||
|
AssessmentLoadingScreen(
|
||||||
|
isLoading: viewModel.isBusy,
|
||||||
|
isEmpty: viewModel.assessments.isEmpty,
|
||||||
|
onTap: () async => await viewModel.getAssessments(),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>
|
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>
|
||||||
PopScope(
|
PopScope(
|
||||||
|
|
@ -45,9 +48,10 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||||
|
|
||||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||||
showBackButton: true,
|
onClose: viewModel.abort,
|
||||||
showLanguageSelection: false,
|
showLanguageSelection: false,
|
||||||
onPop: viewModel.previousQuestion,
|
onPop: viewModel.previousQuestion,
|
||||||
|
showBackButton: viewModel.currentQuestion == 0 ? false : true,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
||||||
|
|
@ -62,7 +66,8 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
controller: viewModel.pageController,
|
controller: viewModel.pageController,
|
||||||
itemCount: viewModel.assessments.length,
|
itemCount: viewModel.assessments.length,
|
||||||
itemBuilder: (cotext, index) =>
|
itemBuilder: (cotext, index) =>
|
||||||
_buildBody(index: index, viewModel: viewModel));
|
_buildBody(index: index, viewModel: viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildBody(
|
Widget _buildBody(
|
||||||
{required int index, required AssessmentViewModel viewModel}) =>
|
{required int index, required AssessmentViewModel viewModel}) =>
|
||||||
|
|
@ -105,7 +110,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
Widget _buildTitle(
|
Widget _buildTitle(
|
||||||
{required int index, required AssessmentViewModel viewModel}) =>
|
{required int index, required AssessmentViewModel viewModel}) =>
|
||||||
Text(
|
Text(
|
||||||
'Q${index + 1}. ${viewModel.assessments[index].question?.title} ',
|
'Q${index + 1}. ${viewModel.assessments[index].questionText} ',
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -123,8 +128,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
''),
|
''),
|
||||||
onTap: () => viewModel.setSelectedAnswer(
|
onTap: () => viewModel.setSelectedAnswer(
|
||||||
question: index + 1,
|
question: index + 1,
|
||||||
option: viewModel.assessments[index].options?[inner].optionText ??
|
option: viewModel.assessments[index].options?[inner]),
|
||||||
''),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -159,11 +163,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
? kcPrimaryColor
|
? kcPrimaryColor
|
||||||
: kcPrimaryColor.withOpacity(0.1),
|
: kcPrimaryColor.withOpacity(0.1),
|
||||||
onTap: viewModel.selectedAnswers.containsKey(question.toString())
|
onTap: viewModel.selectedAnswers.containsKey(question.toString())
|
||||||
?
|
? () => viewModel.nextQuestion()
|
||||||
// viewModel.currentQuestion == viewModel.assessments.length - 1
|
|
||||||
// ? () => viewModel.next()
|
|
||||||
// :
|
|
||||||
() => viewModel.nextQuestion()
|
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,27 +54,24 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||||
showBackButton: false,
|
showBackButton: true,
|
||||||
|
onPop: viewModel.pop,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Want a quick assessment to know your English level?',
|
'Want a quick assessment to know your English level?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Answer a few quick questions to help us understand your English proficiency.',
|
'Answer a few quick questions to help us understand your English proficiency.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||||
|
|
@ -94,8 +91,8 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
safe: false,
|
safe: false,
|
||||||
text: 'Continue',
|
text: 'Continue',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
onTap: () => viewModel.next(),
|
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
|
onTap: () => viewModel.next(),
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,14 @@ import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
|
||||||
|
|
||||||
import '../../../common/app_colors.dart';
|
import '../../../common/app_colors.dart';
|
||||||
import '../../../widgets/large_app_bar.dart';
|
import '../../../widgets/large_app_bar.dart';
|
||||||
|
import '../../../widgets/refresh_button.dart';
|
||||||
|
|
||||||
class AssessmentLoadingScreen extends StatelessWidget {
|
class AssessmentLoadingScreen extends StatelessWidget {
|
||||||
const AssessmentLoadingScreen({super.key});
|
final bool isEmpty;
|
||||||
|
final bool isLoading;
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
const AssessmentLoadingScreen(
|
||||||
|
{super.key, this.onTap, required this.isEmpty, required this.isLoading});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => _buildScaffoldWrapper();
|
Widget build(BuildContext context) => _buildScaffoldWrapper();
|
||||||
|
|
@ -16,7 +21,11 @@ class AssessmentLoadingScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold() => Stack(
|
Widget _buildScaffold() => Stack(
|
||||||
children: [_buildColumn(), _buildPageIndicator()],
|
children: [
|
||||||
|
_buildColumn(),
|
||||||
|
if (isEmpty) _buildRefreshButton(),
|
||||||
|
if (isLoading) _buildPageIndicator()
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildColumn() => Column(
|
Widget _buildColumn() => Column(
|
||||||
|
|
@ -34,4 +43,6 @@ class AssessmentLoadingScreen extends StatelessWidget {
|
||||||
Widget _buildBody() => Expanded(child: Container());
|
Widget _buildBody() => Expanded(child: Container());
|
||||||
|
|
||||||
Widget _buildPageIndicator() => const PageLoadingIndicator();
|
Widget _buildPageIndicator() => const PageLoadingIndicator();
|
||||||
|
|
||||||
|
Widget _buildRefreshButton() => RefreshButton(onTap: onTap);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_colors.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/common/ui_helpers.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||||
|
|
@ -62,35 +63,37 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
verticalSpaceLarge,
|
verticalSpaceLarge,
|
||||||
_buildTitle(viewModel),
|
_buildTitle(viewModel),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildPrimarySubTitle(),
|
_buildPrimarySubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildIcon(),
|
_buildIconWrapper(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildSecondarySubTitle()
|
_buildSecondarySubtitle()
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle(AssessmentViewModel viewModel) => Text(
|
Widget _buildTitle(AssessmentViewModel viewModel) => Text(
|
||||||
'You’re likely a ${viewModel.proficiencyLevel.name.toUpperCase()} speaker!',
|
'You’re likely a ${viewModel.proficiencyLevel.name.toUpperCase()} speaker!',
|
||||||
|
style: style25DG600,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: kcPrimaryColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPrimarySubTitle() => const Text(
|
Widget _buildPrimarySubtitle() => Text(
|
||||||
'Great Job! Here’s your next step to keep improving.',
|
'Great Job! Here’s your next step to keep improving.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset('assets/icons/b1.svg');
|
Widget _buildIconWrapper(AssessmentViewModel viewModel) =>
|
||||||
|
viewModel.proficiencyLevel != ProficiencyLevels.none
|
||||||
|
? _buildIcon(viewModel)
|
||||||
|
: Container();
|
||||||
|
|
||||||
Widget _buildSecondarySubTitle() => const Text(
|
Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset(
|
||||||
|
'assets/icons/${viewModel.proficiencyLevel.name.substring(0, 1)}_${viewModel.proficiencyLevel.name.substring(1)}.svg');
|
||||||
|
|
||||||
|
Widget _buildSecondarySubtitle() => Text(
|
||||||
'Let\'s start your practice',
|
'Let\'s start your practice',
|
||||||
|
style: style14DG400,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: kcMediumGrey),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||||
|
|
@ -110,8 +113,8 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
safe: false,
|
safe: false,
|
||||||
text: 'Continue',
|
text: 'Continue',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
onTap: () => viewModel.next(),
|
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
|
onTap: () => viewModel.next(),
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -124,10 +127,10 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
backgroundColor: kcWhite,
|
||||||
text: 'Practice Speaking',
|
text: 'Practice Speaking',
|
||||||
borderColor: kcPrimaryColor,
|
borderColor: kcPrimaryColor,
|
||||||
onTap: () => viewModel.next(),
|
onTap: () => viewModel.next(),
|
||||||
backgroundColor: kcWhite,
|
|
||||||
foregroundColor: kcPrimaryColor,
|
foregroundColor: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||||
|
|
@ -61,19 +61,15 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
'assets/icons/progress_indicator.svg',
|
'assets/icons/progress_indicator.svg',
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Analyzing your results…',
|
'Analyzing your results…',
|
||||||
|
style: style25DG600,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'We’re now analyzing your speaking skills',
|
'We’re now analyzing your speaking skills',
|
||||||
|
style: style14MG400,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: kcMediumGrey),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||||
|
|
@ -72,20 +72,16 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
color: kcPrimaryColor,
|
color: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'We didn’t get enough from your assessment',
|
'We didn’t get enough from your assessment',
|
||||||
|
style: style25DG600,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Your assessment wasn’t long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
|
'Your assessment wasn’t long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
|
||||||
|
style: style14MG400,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: kcMediumGrey),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||||
|
|
||||||
import '../../../common/enmus.dart';
|
import '../../../common/enmus.dart';
|
||||||
|
import '../../../widgets/page_loading_indicator.dart';
|
||||||
import '../assessment_viewmodel.dart';
|
import '../assessment_viewmodel.dart';
|
||||||
|
|
||||||
class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
|
|
@ -15,7 +16,6 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
Future<void> _start(AssessmentViewModel viewModel) async {
|
Future<void> _start(AssessmentViewModel viewModel) async {
|
||||||
if (viewModel.proficiencyLevel != ProficiencyLevels.none) {
|
if (viewModel.proficiencyLevel != ProficiencyLevels.none) {
|
||||||
Map<String, dynamic> data = {
|
Map<String, dynamic> data = {
|
||||||
'preferred_language': 'en',
|
|
||||||
'knowledge_level': viewModel.proficiencyLevel.name.toUpperCase()
|
'knowledge_level': viewModel.proficiencyLevel.name.toUpperCase()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -31,20 +31,24 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold(
|
Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
body: _buildScaffold(viewModel),
|
body: _buildScaffoldStack(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffoldStack(AssessmentViewModel viewModel) =>
|
||||||
|
Stack(children: [_buildScaffold(viewModel), _buildState(viewModel)]);
|
||||||
|
|
||||||
Widget _buildScaffold(AssessmentViewModel viewModel) => Column(
|
Widget _buildScaffold(AssessmentViewModel viewModel) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _buildScaffoldChildren(viewModel),
|
children: _buildScaffoldChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
|
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
|
||||||
[_buildAppBar(), _buildExpandedBody(viewModel)];
|
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||||
|
|
||||||
Widget _buildAppBar() => const LargeAppBar(
|
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||||
showBackButton: false,
|
showBackButton: true,
|
||||||
showLanguageSelection: false,
|
onPop: viewModel.pop,
|
||||||
|
showLanguageSelection: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
||||||
|
|
@ -76,7 +80,7 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(viewModel),
|
_buildTitle(viewModel),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg');
|
Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg');
|
||||||
|
|
@ -84,26 +88,19 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich(
|
Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'Welcome aboard',
|
text: 'Welcome aboard',
|
||||||
style: const TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
|
style: style25DG600,
|
||||||
text: ', ${viewModel.userData['first_name']}!',
|
text: ', ${viewModel.userData['first_name']}!',
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: kcPrimaryColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
]),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'You’re ready to explore your personalized lessons.',
|
'You’re ready to explore your personalized lessons.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
||||||
|
|
@ -114,10 +111,15 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
Widget _buildContinueButton(AssessmentViewModel viewModel) =>
|
Widget _buildContinueButton(AssessmentViewModel viewModel) =>
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
|
text: 'Finish',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
text: 'Go to My Lessons',
|
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
onTap: () async => await _start(viewModel),
|
onTap: () async => await _start(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget _buildState(AssessmentViewModel viewModel) =>
|
||||||
|
viewModel.busy(StateObjects.profileCompletion)
|
||||||
|
? const PageLoadingIndicator()
|
||||||
|
: Container();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,10 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
|
||||||
Widget _buildIcon() =>
|
Widget _buildIcon() =>
|
||||||
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
|
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Call our support team between 9 AM - 6 PM',
|
'Call our support team between 9 AM - 6 PM',
|
||||||
|
style: style25DG600,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle(String title) => Text(
|
Widget _buildSubTitle(String title) => Text(
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildEmptyTitle(),
|
_buildEmptyTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildEmptySubTitle(),
|
_buildEmptySubtitle(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildEmptyIcon() => const Icon(
|
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.',
|
'Start by exploring your learning materials and save them for offline access.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: TextStyle(color: kcMediumGrey),
|
||||||
|
|
|
||||||
96
lib/ui/views/failure/failure_view.dart
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
3
lib/ui/views/failure/failure_viewmodel.dart
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
class FailureViewModel extends BaseViewModel {}
|
||||||
101
lib/ui/views/forget_password/forget_password_view.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
281
lib/ui/views/forget_password/forget_password_view.form.dart
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
// 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),
|
||||||
|
});
|
||||||
231
lib/ui/views/forget_password/forget_password_viewmodel.dart
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
306
lib/ui/views/forget_password/screens/reset_password_screen.dart
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
@ -15,8 +15,9 @@ class HomeView extends StackedView<HomeViewModel> {
|
||||||
HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel();
|
HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(HomeViewModel viewModel) {
|
void onViewModelReady(HomeViewModel viewModel) async {
|
||||||
viewModel.getProfileStatus();
|
await viewModel.getProfileStatus();
|
||||||
|
await viewModel.getProfileData();
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,9 +27,7 @@ class HomeView extends StackedView<HomeViewModel> {
|
||||||
_buildScaffoldWrapper(viewModel);
|
_buildScaffoldWrapper(viewModel);
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy
|
Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy
|
||||||
? const StartupView(
|
? const StartupView(label: 'Checking user info')
|
||||||
label: 'Checking user info',
|
|
||||||
)
|
|
||||||
: _buildScaffold(viewModel);
|
: _buildScaffold(viewModel);
|
||||||
|
|
||||||
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(
|
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,30 @@ import 'package:yimaru_app/services/status_checker_service.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_strings.dart';
|
import 'package:yimaru_app/ui/common/app_strings.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.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/api_service.dart';
|
||||||
import '../../../services/authentication_service.dart';
|
import '../../../services/authentication_service.dart';
|
||||||
|
import '../../../services/image_downloader_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
class HomeViewModel extends BaseViewModel {
|
class HomeViewModel extends ReactiveViewModel {
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
final _dialogService = locator<DialogService>();
|
final _dialogService = locator<DialogService>();
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
final _bottomSheetService = locator<BottomSheetService>();
|
final _bottomSheetService = locator<BottomSheetService>();
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
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
|
// Bottom navigation
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
|
@ -46,33 +58,77 @@ class HomeViewModel extends BaseViewModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveProfileStatus(bool value) async =>
|
||||||
|
await _authenticationService.saveProfileStatus(value);
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
|
Future<void> replaceWithFailure() async =>
|
||||||
|
await _navigationService.clearStackAndShowView(
|
||||||
|
const FailureView(label: 'Check your internet connection to proceed'),
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> replaceWithOnboarding() async =>
|
Future<void> replaceWithOnboarding() async =>
|
||||||
await _navigationService.replaceWithOnboardingView();
|
await _navigationService.replaceWithOnboardingView();
|
||||||
|
|
||||||
// Remote api calls
|
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _getProfileStatus() async {
|
// Profile data
|
||||||
Map<String, dynamic> response = {};
|
Future<void> getProfileData() async => await runBusyFuture(_getProfileData());
|
||||||
UserModel user = await _authenticationService.getUser();
|
|
||||||
|
|
||||||
if (user.profileCompleted == null) {
|
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()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
response = await _apiService.getProfileStatus(user);
|
Map<String, dynamic> response = {};
|
||||||
} else {
|
|
||||||
response = {'data': false, 'status': ResponseStatus.success};
|
if (_user?.profileCompleted != null &&
|
||||||
|
(_user?.profileCompleted ?? false)) {
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
await replaceWithFailure();
|
||||||
|
}
|
||||||
|
} else if (!(_user?.profileCompleted ?? false)) {
|
||||||
|
response = {'data': false, 'status': ResponseStatus.success};
|
||||||
} else {
|
} else {
|
||||||
response = {'data': true, 'status': ResponseStatus.success};
|
response = {'data': true, 'status': ResponseStatus.success};
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
if (response['status'] == ResponseStatus.success && !response['data']) {
|
||||||
|
await replaceWithOnboarding();
|
||||||
|
} else if (response['status'] == ResponseStatus.success &&
|
||||||
|
response['data']) {
|
||||||
|
await saveProfileStatus(response['data']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class LanguageView extends StackedView<LanguageViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildLanguages(viewModel)
|
_buildLanguages(viewModel)
|
||||||
];
|
];
|
||||||
|
|
@ -72,22 +72,18 @@ class LanguageView extends StackedView<LanguageViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
|
||||||
title: 'Language Preference',
|
|
||||||
onTap: viewModel.pop,
|
onTap: viewModel.pop,
|
||||||
|
title: 'Language Preference',
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Choose your language',
|
'Choose your language',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'You can switch languages anytime',
|
'You can switch languages anytime',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder(
|
Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder(
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,15 @@ class LearnView extends StackedView<LearnViewModel> {
|
||||||
Widget _buildColumn(LearnViewModel viewModel) => Column(
|
Widget _buildColumn(LearnViewModel viewModel) => Column(
|
||||||
children: [
|
children: [
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildAppBar(),
|
_buildAppBar(viewModel),
|
||||||
_buildLevelsColumnWrapper(viewModel)
|
_buildLevelsColumnWrapper(viewModel)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppBar() => const LearnAppBar();
|
Widget _buildAppBar(LearnViewModel viewModel) => LearnAppBar(
|
||||||
|
name: viewModel.user?.firstName,
|
||||||
|
profileImage: viewModel.user?.profilePicture,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
|
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
|
||||||
Expanded(child: _buildLevelsColumnScrollView(viewModel));
|
Expanded(child: _buildLevelsColumnScrollView(viewModel));
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/app/app.router.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 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
|
||||||
class LearnViewModel extends BaseViewModel {
|
class LearnViewModel extends ReactiveViewModel {
|
||||||
final _navigationService = locator<NavigationService>();
|
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 = [
|
final List<Map<String, dynamic>> _learnLevels = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/learn_lesson_tile.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/module_progress.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/motivation_card.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/app_colors.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
|
|
@ -120,17 +119,22 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
||||||
itemBuilder: (context, index) => _buildTile(
|
itemBuilder: (context, index) => _buildTile(
|
||||||
title: viewModel.lessons[index]['title'],
|
title: viewModel.lessons[index]['title'],
|
||||||
status: viewModel.lessons[index]['status'],
|
status: viewModel.lessons[index]['status'],
|
||||||
thumbnail: viewModel.lessons[index]['thumbnail']),
|
thumbnail: viewModel.lessons[index]['thumbnail'],
|
||||||
|
onLessonTap: () async =>
|
||||||
|
await viewModel.navigateToLearnLessonDetail(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTile({
|
Widget _buildTile({
|
||||||
required String title,
|
required String title,
|
||||||
required String thumbnail,
|
required String thumbnail,
|
||||||
|
GestureTapCallback? onLessonTap,
|
||||||
required ProgressStatuses status,
|
required ProgressStatuses status,
|
||||||
}) =>
|
}) =>
|
||||||
LearnLessonTile(
|
LearnLessonTile(
|
||||||
title: title,
|
title: title,
|
||||||
status: status,
|
status: status,
|
||||||
thumbnail: thumbnail,
|
thumbnail: thumbnail,
|
||||||
|
onLessonTap: onLessonTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.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 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
|
@ -8,7 +9,6 @@ class LearnLessonViewModel extends BaseViewModel {
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
// Lessons
|
// Lessons
|
||||||
// Downloads
|
|
||||||
final List<Map<String, dynamic>> _lessons = [
|
final List<Map<String, dynamic>> _lessons = [
|
||||||
{
|
{
|
||||||
'title': '1.1 Introducing Yourself',
|
'title': '1.1 Introducing Yourself',
|
||||||
|
|
@ -31,4 +31,7 @@ class LearnLessonViewModel extends BaseViewModel {
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
|
Future<void> navigateToLearnLessonDetail() async =>
|
||||||
|
await _navigationService.navigateToLearnLessonDetailView();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
178
lib/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
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, you’ll explore how to start simple conversations by greeting others in polite and friendly ways. You’ll practice different greetings for morning, afternoon, and evening, as well as casual and formal situations. By the end, you’ll 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -73,20 +73,14 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
||||||
_buildListView(viewModel)
|
_buildListView(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'A1 - Beginner',
|
'A1 - Beginner',
|
||||||
style: TextStyle(
|
style: style18P600,
|
||||||
fontSize: 18,
|
|
||||||
color: kcPrimaryColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubTitle() => Text(
|
||||||
'Your Current Level',
|
'Your Current Level',
|
||||||
style: TextStyle(
|
style: style14DG400,
|
||||||
color: kcDarkGrey,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildOverallProgress() => const OverallLearnProgress();
|
Widget _buildOverallProgress() => const OverallLearnProgress();
|
||||||
|
|
|
||||||
|
|
@ -37,5 +37,6 @@ class LearnModuleViewModel extends BaseViewModel {
|
||||||
|
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToLearnLesson() async=> await _navigationService.navigateToLearnLessonView();
|
Future<void> navigateToLearnLesson() async =>
|
||||||
|
await _navigationService.navigateToLearnLessonView();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
72
lib/ui/views/learn_practice/learn_practice_view.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
34
lib/ui/views/learn_practice/learn_practice_viewmodel.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
}
|
||||||
254
lib/ui/views/learn_practice/screens/listen_speaker_screen.dart
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
124
lib/ui/views/learn_practice/screens/practice_intro_screen.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
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(
|
||||||
|
'I’ll 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
172
lib/ui/views/learn_practice/screens/start_practice_screen.dart
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked/stacked_annotations.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_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_email_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart';
|
import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart';
|
||||||
|
|
@ -24,10 +25,18 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(LoginViewModel viewModel) {
|
void onViewModelReady(LoginViewModel viewModel) {
|
||||||
|
_clearData();
|
||||||
syncFormWithViewModel(viewModel);
|
syncFormWithViewModel(viewModel);
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearData() {
|
||||||
|
otpController.clear();
|
||||||
|
emailController.clear();
|
||||||
|
passwordController.clear();
|
||||||
|
phoneNumberController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel();
|
LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel();
|
||||||
|
|
||||||
|
|
@ -45,35 +54,11 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
|
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
|
||||||
},
|
},
|
||||||
child: _buildScaffoldWrapper(viewModel));
|
child: _buildBody(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) =>
|
Widget _buildBody(LoginViewModel viewModel) =>
|
||||||
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
|
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
|
||||||
|
|
@ -94,6 +79,4 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
|
||||||
otpController: otpController,
|
otpController: otpController,
|
||||||
phoneNumberController: phoneNumberController);
|
phoneNumberController: phoneNumberController);
|
||||||
|
|
||||||
Widget _buildBusyLogin(LoginViewModel viewModel) =>
|
|
||||||
viewModel.isBusy ? const PageLoadingIndicator() : Container();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:google_sign_in/google_sign_in.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/app/app.locator.dart';
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
|
|
@ -7,16 +8,25 @@ import 'package:yimaru_app/models/user_model.dart';
|
||||||
|
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/authentication_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/enmus.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
import '../home/home_view.dart';
|
import '../home/home_view.dart';
|
||||||
|
|
||||||
class LoginViewModel extends FormViewModel {
|
class LoginViewModel extends FormViewModel {
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
|
final _googleAuthService = locator<GoogleAuthService>();
|
||||||
|
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
// Navigation
|
// In-app navigation
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
|
||||||
int get currentIndex => _currentIndex;
|
int get currentIndex => _currentIndex;
|
||||||
|
|
@ -106,36 +116,7 @@ class LoginViewModel extends FormViewModel {
|
||||||
_userData.clear();
|
_userData.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remote api calls
|
// In app navigation
|
||||||
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) {
|
void goTo(int page) {
|
||||||
_currentIndex = page;
|
_currentIndex = page;
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
|
|
@ -153,9 +134,70 @@ class LoginViewModel extends FormViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
Future<void> navigateToRegister() async =>
|
Future<void> navigateToRegister() async =>
|
||||||
await _navigationService.navigateToRegisterView();
|
await _navigationService.navigateToRegisterView();
|
||||||
|
|
||||||
|
Future<void> navigateToForgetPassword() async =>
|
||||||
|
await _navigationService.navigateToForgetPasswordView();
|
||||||
|
|
||||||
Future<void> replaceWithHome() async =>
|
Future<void> replaceWithHome() async =>
|
||||||
await _navigationService.clearStackAndShowView(const HomeView());
|
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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:yimaru_app/ui/widgets/custom_cursor.dart';
|
||||||
import '../../../common/app_colors.dart';
|
import '../../../common/app_colors.dart';
|
||||||
import '../../../common/ui_helpers.dart';
|
import '../../../common/ui_helpers.dart';
|
||||||
import '../../../widgets/custom_elevated_button.dart';
|
import '../../../widgets/custom_elevated_button.dart';
|
||||||
|
import '../../../widgets/large_app_bar.dart';
|
||||||
import '../login_viewmodel.dart';
|
import '../login_viewmodel.dart';
|
||||||
import '../login_view.form.dart';
|
import '../login_view.form.dart';
|
||||||
|
|
||||||
|
|
@ -20,23 +21,61 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
required this.otpController,
|
required this.otpController,
|
||||||
required this.phoneNumberController});
|
required this.phoneNumberController});
|
||||||
|
|
||||||
|
Widget getPadding(context){
|
||||||
|
double half = screenHeight(context)/2;
|
||||||
|
return SizedBox(height: half + 325 - half,);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
||||||
_buildBody(viewModel);
|
_buildScaffoldWrapper(context: context,viewModel: viewModel);
|
||||||
|
|
||||||
Widget _buildBody(LoginViewModel viewModel) => Column(
|
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(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: _buildBodyChildren(viewModel),
|
children: _buildBodyChildren(context: context,viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
|
|
||||||
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
|
|
||||||
|
|
||||||
Widget _buildColumnScroller(LoginViewModel viewModel) =>
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: _buildUpperColumn(viewModel),
|
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||||
);
|
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButton(viewModel)];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
|
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -44,6 +83,7 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
children: _buildUpperColumnChildren(viewModel),
|
children: _buildUpperColumnChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@ import 'package:yimaru_app/ui/views/login/login_view.form.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/obscure_password.dart';
|
import 'package:yimaru_app/ui/widgets/obscure_password.dart';
|
||||||
|
|
||||||
import '../../../common/app_colors.dart';
|
import '../../../common/app_colors.dart';
|
||||||
|
import '../../../common/enmus.dart';
|
||||||
import '../../../common/ui_helpers.dart';
|
import '../../../common/ui_helpers.dart';
|
||||||
import '../../../widgets/custom_elevated_button.dart';
|
import '../../../widgets/custom_elevated_button.dart';
|
||||||
|
import '../../../widgets/large_app_bar.dart';
|
||||||
import '../../../widgets/option_text_divider.dart';
|
import '../../../widgets/option_text_divider.dart';
|
||||||
|
import '../../../widgets/page_loading_indicator.dart';
|
||||||
import '../../../widgets/register_for_account.dart';
|
import '../../../widgets/register_for_account.dart';
|
||||||
import '../login_viewmodel.dart';
|
import '../login_viewmodel.dart';
|
||||||
|
|
||||||
|
|
@ -19,6 +22,12 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
required this.emailController,
|
required this.emailController,
|
||||||
required this.passwordController});
|
required this.passwordController});
|
||||||
|
|
||||||
|
|
||||||
|
Widget getPadding(context){
|
||||||
|
double half = screenHeight(context)/2;
|
||||||
|
return SizedBox(height: half + 25 - half,);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _login(LoginViewModel viewModel) async {
|
Future<void> _login(LoginViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -28,27 +37,63 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
};
|
};
|
||||||
viewModel.addUserData(data);
|
viewModel.addUserData(data);
|
||||||
|
|
||||||
await viewModel.login();
|
await viewModel.emailLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
||||||
_buildBody(viewModel);
|
_buildScaffoldWrapper(context: context,viewModel: viewModel);
|
||||||
|
|
||||||
Widget _buildBody(LoginViewModel viewModel) => Column(
|
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(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
|
||||||
children: _buildBodyChildren(viewModel),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
|
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||||
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
|
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
|
||||||
|
|
||||||
Widget _buildColumnScroller(LoginViewModel 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(
|
SingleChildScrollView(
|
||||||
child: _buildUpperColumn(viewModel),
|
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(
|
||||||
|
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(
|
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -58,7 +103,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
_buildSubTitleWrapper(viewModel),
|
_buildSubtitleWrapper(viewModel),
|
||||||
verticalSpaceLarge,
|
verticalSpaceLarge,
|
||||||
_buildEmailFormField(viewModel),
|
_buildEmailFormField(viewModel),
|
||||||
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
|
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
|
||||||
|
|
@ -71,19 +116,15 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
verticalSpaceTiny,
|
verticalSpaceTiny,
|
||||||
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
|
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
|
||||||
_buildPasswordValidationWrapper(viewModel),
|
_buildPasswordValidationWrapper(viewModel),
|
||||||
_buildForgetPasswordTextButtonWrapper(),
|
_buildForgetPasswordTextButtonWrapper(viewModel),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Welcome Back',
|
'Welcome Back',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
|
Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
|
||||||
onTap: () async => await viewModel.navigateToRegister(),
|
onTap: () async => await viewModel.navigateToRegister(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -104,11 +145,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
|
|
||||||
Widget _buildEmailValidator(LoginViewModel viewModel) => Text(
|
Widget _buildEmailValidator(LoginViewModel viewModel) => Text(
|
||||||
viewModel.emailValidationMessage!,
|
viewModel.emailValidationMessage!,
|
||||||
style: const TextStyle(
|
style: style12R700,
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField(
|
Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField(
|
||||||
|
|
@ -135,26 +172,23 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
|
|
||||||
Widget _buildPasswordValidator(LoginViewModel viewModel) => Text(
|
Widget _buildPasswordValidator(LoginViewModel viewModel) => Text(
|
||||||
viewModel.passwordValidationMessage!,
|
viewModel.passwordValidationMessage!,
|
||||||
style: const TextStyle(
|
style: style12R700,
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildForgetPasswordTextButtonWrapper() => Align(
|
Widget _buildForgetPasswordTextButtonWrapper(LoginViewModel viewModel) =>
|
||||||
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: _buildForgetPasswordTextButton(),
|
child: _buildForgetPasswordTextButton(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildForgetPasswordTextButton() => TextButton(
|
Widget _buildForgetPasswordTextButton(LoginViewModel viewModel) => TextButton(
|
||||||
onPressed: () {},
|
onPressed: () async => await viewModel.navigateToForgetPassword(),
|
||||||
child: _buildForgetPasswordText(),
|
child: _buildForgetPasswordText(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildForgetPasswordText() => const Text(
|
Widget _buildForgetPasswordText() => Text(
|
||||||
'Forget Password?',
|
'Forget Password?',
|
||||||
style: TextStyle(color: kcPrimaryColor),
|
style: style14P400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLowerColumn(LoginViewModel viewModel) => Column(
|
Widget _buildLowerColumn(LoginViewModel viewModel) => Column(
|
||||||
|
|
@ -164,13 +198,15 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
|
|
||||||
List<Widget> _buildLowerColumnChildren(LoginViewModel viewModel) => [
|
List<Widget> _buildLowerColumnChildren(LoginViewModel viewModel) => [
|
||||||
_buildContinueButton(viewModel),
|
_buildContinueButton(viewModel),
|
||||||
|
_buildLoginWithGoogleButton(viewModel),
|
||||||
_buildOptionTextDivider(),
|
_buildOptionTextDivider(),
|
||||||
_buildLoginWithEmailButton(viewModel),
|
_buildLoginWithPhoneButton(viewModel),
|
||||||
verticalSpaceMedium
|
verticalSpaceMedium
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
|
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
|
safe: false,
|
||||||
text: 'Continue',
|
text: 'Continue',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
|
|
@ -184,17 +220,40 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
: kcPrimaryColor.withOpacity(0.1),
|
: 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 _buildOptionTextDivider() => const OptionTextDivider();
|
||||||
|
|
||||||
Widget _buildLoginWithEmailButton(LoginViewModel viewModel) =>
|
Widget _buildLoginWithPhoneButton(LoginViewModel viewModel) =>
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
backgroundColor: kcWhite,
|
backgroundColor: kcWhite,
|
||||||
leadingIcon: Icons.phone,
|
leadingIcon: Icons.phone,
|
||||||
borderColor: kcPrimaryColor,
|
borderColor: kcPrimaryColor,
|
||||||
|
onTap: () => viewModel.goTo(1),
|
||||||
foregroundColor: kcPrimaryColor,
|
foregroundColor: kcPrimaryColor,
|
||||||
text: 'Login with Phone Number',
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:yimaru_app/ui/widgets/register_for_account.dart';
|
||||||
import '../../../common/app_colors.dart';
|
import '../../../common/app_colors.dart';
|
||||||
import '../../../common/ui_helpers.dart';
|
import '../../../common/ui_helpers.dart';
|
||||||
import '../../../widgets/custom_elevated_button.dart';
|
import '../../../widgets/custom_elevated_button.dart';
|
||||||
|
import '../../../widgets/large_app_bar.dart';
|
||||||
import '../../../widgets/phone_number_prefix.dart';
|
import '../../../widgets/phone_number_prefix.dart';
|
||||||
import '../login_view.form.dart';
|
import '../login_view.form.dart';
|
||||||
|
|
||||||
|
|
@ -17,34 +18,75 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
const LoginWithPhoneNumberScreen(
|
const LoginWithPhoneNumberScreen(
|
||||||
{super.key, required this.phoneNumberController});
|
{super.key, required this.phoneNumberController});
|
||||||
|
|
||||||
|
Widget getPadding(context){
|
||||||
|
double half = screenHeight(context)/2;
|
||||||
|
return SizedBox(height: half + 175 - half,);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
||||||
_buildBody(viewModel);
|
|
||||||
|
|
||||||
Widget _buildBody(LoginViewModel viewModel) => Column(
|
_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(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
|
||||||
children: _buildBodyChildren(viewModel),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
|
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||||
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
|
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
|
||||||
|
|
||||||
Widget _buildColumnScroller(LoginViewModel 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(
|
SingleChildScrollView(
|
||||||
child: _buildUpperColumn(viewModel),
|
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(
|
||||||
|
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(
|
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _buildUpperColumnChildren(viewModel),
|
children: _buildUpperColumnChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
_buildSubTitleWrapper(viewModel),
|
_buildSubtitleWrapper(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildSubtitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
|
|
@ -57,22 +99,18 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
_buildPhoneNumberValidatorWrapper(viewModel),
|
_buildPhoneNumberValidatorWrapper(viewModel),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Welcome Back',
|
'Welcome Back',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
|
Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
|
||||||
onTap: () async => await viewModel.navigateToRegister(),
|
onTap: () async => await viewModel.navigateToRegister(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubtitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Enter your phone number. We will send you a confirmation code there',
|
'Enter your phone number. We will send you a confirmation code there',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row(
|
Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row(
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import 'onboarding_viewmodel.dart';
|
||||||
import 'onboarding_view.form.dart';
|
import 'onboarding_view.form.dart';
|
||||||
|
|
||||||
@FormView(fields: [
|
@FormView(fields: [
|
||||||
FormTextField(name: 'answer', validator: FormValidator.validateForm),
|
|
||||||
FormTextField(name: 'fullName', validator: FormValidator.validateForm),
|
FormTextField(name: 'fullName', validator: FormValidator.validateForm),
|
||||||
FormTextField(name: 'challenge', validator: FormValidator.validateForm),
|
FormTextField(name: 'challenge', validator: FormValidator.validateForm),
|
||||||
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
|
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
|
||||||
|
|
@ -30,13 +29,55 @@ class OnboardingView extends StackedView<OnboardingViewModel>
|
||||||
with $OnboardingView {
|
with $OnboardingView {
|
||||||
const OnboardingView({Key? key}) : super(key: key);
|
const OnboardingView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
void _initFormFields() {
|
void _initClearData() {
|
||||||
answerController.text = 'Book';
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(OnboardingViewModel viewModel) {
|
void onViewModelReady(OnboardingViewModel viewModel) {
|
||||||
_initFormFields();
|
_initClearData();
|
||||||
syncFormWithViewModel(viewModel);
|
syncFormWithViewModel(viewModel);
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
@ -57,8 +98,8 @@ class OnboardingView extends StackedView<OnboardingViewModel>
|
||||||
|
|
||||||
Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) =>
|
Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) =>
|
||||||
PopScope(
|
PopScope(
|
||||||
canPop: false,
|
canPop: viewModel.currentPage == 0 ? true : false,
|
||||||
onPopInvokedWithResult: (value, data) => viewModel.pop(),
|
onPopInvokedWithResult: (value, data) => _pop(viewModel),
|
||||||
child: _buildOnboardingScreens(viewModel));
|
child: _buildOnboardingScreens(viewModel));
|
||||||
|
|
||||||
Widget _buildOnboardingScreens(OnboardingViewModel viewModel) => IndexedStack(
|
Widget _buildOnboardingScreens(OnboardingViewModel viewModel) => IndexedStack(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart';
|
||||||
|
|
||||||
const bool _autoTextFieldValidation = true;
|
const bool _autoTextFieldValidation = true;
|
||||||
|
|
||||||
const String AnswerValueKey = 'answer';
|
|
||||||
const String FullNameValueKey = 'fullName';
|
const String FullNameValueKey = 'fullName';
|
||||||
const String ChallengeValueKey = 'challenge';
|
const String ChallengeValueKey = 'challenge';
|
||||||
const String OccupationValueKey = 'occupation';
|
const String OccupationValueKey = 'occupation';
|
||||||
|
|
@ -25,7 +24,6 @@ final Map<String, TextEditingController> _OnboardingViewTextEditingControllers =
|
||||||
final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
|
final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
|
||||||
|
|
||||||
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
|
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
|
||||||
AnswerValueKey: FormValidator.validateForm,
|
|
||||||
FullNameValueKey: FormValidator.validateForm,
|
FullNameValueKey: FormValidator.validateForm,
|
||||||
ChallengeValueKey: FormValidator.validateForm,
|
ChallengeValueKey: FormValidator.validateForm,
|
||||||
OccupationValueKey: FormValidator.validateForm,
|
OccupationValueKey: FormValidator.validateForm,
|
||||||
|
|
@ -34,8 +32,6 @@ final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
|
||||||
};
|
};
|
||||||
|
|
||||||
mixin $OnboardingView {
|
mixin $OnboardingView {
|
||||||
TextEditingController get answerController =>
|
|
||||||
_getFormTextEditingController(AnswerValueKey);
|
|
||||||
TextEditingController get fullNameController =>
|
TextEditingController get fullNameController =>
|
||||||
_getFormTextEditingController(FullNameValueKey);
|
_getFormTextEditingController(FullNameValueKey);
|
||||||
TextEditingController get challengeController =>
|
TextEditingController get challengeController =>
|
||||||
|
|
@ -47,7 +43,6 @@ mixin $OnboardingView {
|
||||||
TextEditingController get topicController =>
|
TextEditingController get topicController =>
|
||||||
_getFormTextEditingController(TopicValueKey);
|
_getFormTextEditingController(TopicValueKey);
|
||||||
|
|
||||||
FocusNode get answerFocusNode => _getFormFocusNode(AnswerValueKey);
|
|
||||||
FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey);
|
FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey);
|
||||||
FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey);
|
FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey);
|
||||||
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
|
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
|
||||||
|
|
@ -79,7 +74,6 @@ mixin $OnboardingView {
|
||||||
/// Registers a listener on every generated controller that calls [model.setData()]
|
/// Registers a listener on every generated controller that calls [model.setData()]
|
||||||
/// with the latest textController values
|
/// with the latest textController values
|
||||||
void syncFormWithViewModel(FormStateHelper model) {
|
void syncFormWithViewModel(FormStateHelper model) {
|
||||||
answerController.addListener(() => _updateFormData(model));
|
|
||||||
fullNameController.addListener(() => _updateFormData(model));
|
fullNameController.addListener(() => _updateFormData(model));
|
||||||
challengeController.addListener(() => _updateFormData(model));
|
challengeController.addListener(() => _updateFormData(model));
|
||||||
occupationController.addListener(() => _updateFormData(model));
|
occupationController.addListener(() => _updateFormData(model));
|
||||||
|
|
@ -96,7 +90,6 @@ mixin $OnboardingView {
|
||||||
'This feature was deprecated after 3.1.0.',
|
'This feature was deprecated after 3.1.0.',
|
||||||
)
|
)
|
||||||
void listenToFormUpdated(FormViewModel model) {
|
void listenToFormUpdated(FormViewModel model) {
|
||||||
answerController.addListener(() => _updateFormData(model));
|
|
||||||
fullNameController.addListener(() => _updateFormData(model));
|
fullNameController.addListener(() => _updateFormData(model));
|
||||||
challengeController.addListener(() => _updateFormData(model));
|
challengeController.addListener(() => _updateFormData(model));
|
||||||
occupationController.addListener(() => _updateFormData(model));
|
occupationController.addListener(() => _updateFormData(model));
|
||||||
|
|
@ -111,7 +104,6 @@ mixin $OnboardingView {
|
||||||
model.setData(
|
model.setData(
|
||||||
model.formValueMap
|
model.formValueMap
|
||||||
..addAll({
|
..addAll({
|
||||||
AnswerValueKey: answerController.text,
|
|
||||||
FullNameValueKey: fullNameController.text,
|
FullNameValueKey: fullNameController.text,
|
||||||
ChallengeValueKey: challengeController.text,
|
ChallengeValueKey: challengeController.text,
|
||||||
OccupationValueKey: occupationController.text,
|
OccupationValueKey: occupationController.text,
|
||||||
|
|
@ -158,7 +150,6 @@ extension ValueProperties on FormStateHelper {
|
||||||
return !hasAnyValidationMessage;
|
return !hasAnyValidationMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? get answerValue => this.formValueMap[AnswerValueKey] as String?;
|
|
||||||
String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?;
|
String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?;
|
||||||
String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?;
|
String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?;
|
||||||
String? get occupationValue =>
|
String? get occupationValue =>
|
||||||
|
|
@ -167,16 +158,6 @@ extension ValueProperties on FormStateHelper {
|
||||||
this.formValueMap[LanguageGoalValueKey] as String?;
|
this.formValueMap[LanguageGoalValueKey] as String?;
|
||||||
String? get topicValue => this.formValueMap[TopicValueKey] 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) {
|
set fullNameValue(String? value) {
|
||||||
this.setData(
|
this.setData(
|
||||||
this.formValueMap..addAll({FullNameValueKey: value}),
|
this.formValueMap..addAll({FullNameValueKey: value}),
|
||||||
|
|
@ -232,9 +213,6 @@ extension ValueProperties on FormStateHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasAnswer =>
|
|
||||||
this.formValueMap.containsKey(AnswerValueKey) &&
|
|
||||||
(answerValue?.isNotEmpty ?? false);
|
|
||||||
bool get hasFullName =>
|
bool get hasFullName =>
|
||||||
this.formValueMap.containsKey(FullNameValueKey) &&
|
this.formValueMap.containsKey(FullNameValueKey) &&
|
||||||
(fullNameValue?.isNotEmpty ?? false);
|
(fullNameValue?.isNotEmpty ?? false);
|
||||||
|
|
@ -251,8 +229,6 @@ extension ValueProperties on FormStateHelper {
|
||||||
this.formValueMap.containsKey(TopicValueKey) &&
|
this.formValueMap.containsKey(TopicValueKey) &&
|
||||||
(topicValue?.isNotEmpty ?? false);
|
(topicValue?.isNotEmpty ?? false);
|
||||||
|
|
||||||
bool get hasAnswerValidationMessage =>
|
|
||||||
this.fieldsValidationMessages[AnswerValueKey]?.isNotEmpty ?? false;
|
|
||||||
bool get hasFullNameValidationMessage =>
|
bool get hasFullNameValidationMessage =>
|
||||||
this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false;
|
this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false;
|
||||||
bool get hasChallengeValidationMessage =>
|
bool get hasChallengeValidationMessage =>
|
||||||
|
|
@ -264,8 +240,6 @@ extension ValueProperties on FormStateHelper {
|
||||||
bool get hasTopicValidationMessage =>
|
bool get hasTopicValidationMessage =>
|
||||||
this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false;
|
this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false;
|
||||||
|
|
||||||
String? get answerValidationMessage =>
|
|
||||||
this.fieldsValidationMessages[AnswerValueKey];
|
|
||||||
String? get fullNameValidationMessage =>
|
String? get fullNameValidationMessage =>
|
||||||
this.fieldsValidationMessages[FullNameValueKey];
|
this.fieldsValidationMessages[FullNameValueKey];
|
||||||
String? get challengeValidationMessage =>
|
String? get challengeValidationMessage =>
|
||||||
|
|
@ -279,8 +253,6 @@ extension ValueProperties on FormStateHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Methods on FormStateHelper {
|
extension Methods on FormStateHelper {
|
||||||
setAnswerValidationMessage(String? validationMessage) =>
|
|
||||||
this.fieldsValidationMessages[AnswerValueKey] = validationMessage;
|
|
||||||
setFullNameValidationMessage(String? validationMessage) =>
|
setFullNameValidationMessage(String? validationMessage) =>
|
||||||
this.fieldsValidationMessages[FullNameValueKey] = validationMessage;
|
this.fieldsValidationMessages[FullNameValueKey] = validationMessage;
|
||||||
setChallengeValidationMessage(String? validationMessage) =>
|
setChallengeValidationMessage(String? validationMessage) =>
|
||||||
|
|
@ -294,7 +266,6 @@ extension Methods on FormStateHelper {
|
||||||
|
|
||||||
/// Clears text input fields on the Form
|
/// Clears text input fields on the Form
|
||||||
void clearForm() {
|
void clearForm() {
|
||||||
answerValue = '';
|
|
||||||
fullNameValue = '';
|
fullNameValue = '';
|
||||||
challengeValue = '';
|
challengeValue = '';
|
||||||
occupationValue = '';
|
occupationValue = '';
|
||||||
|
|
@ -305,7 +276,6 @@ extension Methods on FormStateHelper {
|
||||||
/// Validates text input fields on the Form
|
/// Validates text input fields on the Form
|
||||||
void validateForm() {
|
void validateForm() {
|
||||||
this.setValidationMessages({
|
this.setValidationMessages({
|
||||||
AnswerValueKey: getValidationMessage(AnswerValueKey),
|
|
||||||
FullNameValueKey: getValidationMessage(FullNameValueKey),
|
FullNameValueKey: getValidationMessage(FullNameValueKey),
|
||||||
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
|
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
|
||||||
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
||||||
|
|
@ -330,7 +300,6 @@ String? getValidationMessage(String key) {
|
||||||
/// Updates the fieldsValidationMessages on the FormViewModel
|
/// Updates the fieldsValidationMessages on the FormViewModel
|
||||||
void updateValidationData(FormStateHelper model) =>
|
void updateValidationData(FormStateHelper model) =>
|
||||||
model.setValidationMessages({
|
model.setValidationMessages({
|
||||||
AnswerValueKey: getValidationMessage(AnswerValueKey),
|
|
||||||
FullNameValueKey: getValidationMessage(FullNameValueKey),
|
FullNameValueKey: getValidationMessage(FullNameValueKey),
|
||||||
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
|
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
|
||||||
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,13 @@ class OnboardingViewModel extends FormViewModel {
|
||||||
|
|
||||||
// Age group
|
// Age group
|
||||||
final List<String> _ageGroups = [
|
final List<String> _ageGroups = [
|
||||||
'8-14',
|
'UNDER_13',
|
||||||
'15-18',
|
'13_17',
|
||||||
'19-26',
|
'18_24',
|
||||||
'26+',
|
'25_34',
|
||||||
|
'35_44',
|
||||||
|
'45_54',
|
||||||
|
'55_PLUS'
|
||||||
];
|
];
|
||||||
|
|
||||||
List<String> get ageGroups => _ageGroups;
|
List<String> get ageGroups => _ageGroups;
|
||||||
|
|
@ -76,30 +79,11 @@ class OnboardingViewModel extends FormViewModel {
|
||||||
|
|
||||||
String get selectedCountry => _selectedCountry;
|
String get selectedCountry => _selectedCountry;
|
||||||
|
|
||||||
Future<List<String>> getCountries() async => ['Ethiopia'];
|
|
||||||
|
|
||||||
// Country
|
// Country
|
||||||
String _selectedRegion = 'Addis Ababa';
|
String _selectedRegion = 'Addis Ababa';
|
||||||
|
|
||||||
String get selectedRegion => _selectedRegion;
|
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
|
// Learning goal
|
||||||
String? _selectedLearningGoal;
|
String? _selectedLearningGoal;
|
||||||
|
|
||||||
|
|
@ -258,12 +242,42 @@ class OnboardingViewModel extends FormViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Country
|
// Country
|
||||||
|
List<String> getCountries() => ['Ethiopia', 'Other'];
|
||||||
|
|
||||||
void setSelectedCountry(String value) {
|
void setSelectedCountry(String value) {
|
||||||
_selectedCountry = value;
|
_selectedCountry = value;
|
||||||
|
if (selectedCountry != 'Ethiopia') {
|
||||||
|
_selectedRegion = 'Other';
|
||||||
|
} else {
|
||||||
|
_selectedRegion = 'Addis Ababa';
|
||||||
|
}
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Region
|
// 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) {
|
void setSelectedRegion(String value) {
|
||||||
_selectedRegion = value;
|
_selectedRegion = value;
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
|
|
@ -352,24 +366,89 @@ class OnboardingViewModel extends FormViewModel {
|
||||||
// Add user data
|
// Add user data
|
||||||
void addUserData(Map<String, dynamic> data) {
|
void addUserData(Map<String, dynamic> data) {
|
||||||
_userData.addAll(data);
|
_userData.addAll(data);
|
||||||
print('User data : $_userData');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearUserData() {
|
void clearUserData() {
|
||||||
_userData.clear();
|
_userData.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation
|
// Form reset
|
||||||
|
|
||||||
Future<void> navigateToLanguage() async =>
|
// Reset full name form screen
|
||||||
await _navigationService.navigateToLanguageView();
|
void resetFullNameFormScreen() {
|
||||||
|
_focusFullName = false;
|
||||||
|
rebuildUi();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> navigateToAssessment() async =>
|
// Reset gender form screen
|
||||||
await _navigationService.navigateToAssessmentView(data: _userData);
|
void resetGenderFormScreen() {
|
||||||
|
_selectedGender = null;
|
||||||
|
rebuildUi();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> replaceWithHome() async =>
|
// Reset birthday form screen
|
||||||
await _navigationService.clearStackAndShowView(const HomeView());
|
void resetBirthdayFormScreen() {
|
||||||
|
_selectedBirthday = null;
|
||||||
|
rebuildUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
void next({int? page}) async {
|
||||||
if (page == null) {
|
if (page == null) {
|
||||||
if (_previousPage != 0) {
|
if (_previousPage != 0) {
|
||||||
|
|
@ -384,8 +463,8 @@ class OnboardingViewModel extends FormViewModel {
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop() {
|
void goBack() {
|
||||||
if (_currentPage == 8) {
|
if (_currentPage == 0) {
|
||||||
_navigationService.back();
|
_navigationService.back();
|
||||||
} else {
|
} else {
|
||||||
_currentPage--;
|
_currentPage--;
|
||||||
|
|
@ -393,4 +472,14 @@ class OnboardingViewModel extends FormViewModel {
|
||||||
rebuildUi();
|
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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||||
class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
const AgeGroupFormScreen({super.key});
|
const AgeGroupFormScreen({super.key});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
viewModel.resetAgeGroupFormScreen();
|
||||||
|
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -74,24 +80,20 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
onPop: viewModel.pop,
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Which age range are you in?',
|
'Which age range are you in?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubTitle() => Text(
|
||||||
'We’ll personalize your learning experience based on your age.',
|
'We’ll personalize your learning experience based on your age.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14DG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(
|
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,16 @@ import '../../../widgets/birthday_selector.dart';
|
||||||
class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
const BirthdayFormScreen({super.key});
|
const BirthdayFormScreen({super.key});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
viewModel.resetBirthdayFormScreen();
|
||||||
|
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
Map<String, dynamic> data = {
|
Map<String, dynamic> data = {'birth_day': viewModel.selectedBirthday};
|
||||||
'birth_day': DateFormat('yyyy-MM-dd')
|
|
||||||
.parseUTC(viewModel.selectedBirthday ?? DateTime.now().toString())
|
|
||||||
.toIso8601String()
|
|
||||||
};
|
|
||||||
viewModel.addUserData(data);
|
viewModel.addUserData(data);
|
||||||
viewModel.next();
|
viewModel.next();
|
||||||
}
|
}
|
||||||
|
|
@ -68,30 +70,26 @@ class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildBirthdayFormField(viewModel)
|
_buildBirthdayFormField(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
onPop: viewModel.pop,
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Pick your birthday?',
|
'Pick your birthday?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'We’ll personalize your learning experience based on your birthday.',
|
'We’ll personalize your learning experience based on your birthday.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildBirthdayFormField(OnboardingViewModel viewModel) =>
|
Widget _buildBirthdayFormField(OnboardingViewModel viewModel) =>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
const ChallengeFormScreen({super.key, required this.challengeController});
|
const ChallengeFormScreen({super.key, required this.challengeController});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
challengeController.clear();
|
||||||
|
viewModel.resetChallengeFormScreen();
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -74,7 +80,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildChallenges(viewModel),
|
_buildChallenges(viewModel),
|
||||||
if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel),
|
if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel),
|
||||||
|
|
@ -90,24 +96,20 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
onPop: viewModel.pop,
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'What challenge do you face most with English?',
|
'What challenge do you face most with English?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Everyone has struggles, let’s start fixing yours 😊',
|
'Everyone has struggles, let’s start fixing yours 😊',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder(
|
Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder(
|
||||||
|
|
@ -151,11 +153,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text(
|
Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text(
|
||||||
viewModel.challengeValidationMessage!,
|
viewModel.challengeValidationMessage!,
|
||||||
style: const TextStyle(
|
style: style12R700,
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||||
class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
const CountryRegionFormScreen({super.key});
|
const CountryRegionFormScreen({super.key});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
viewModel.resetCountryRegionFormScreen();
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -71,7 +76,7 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildCountryDropDown(viewModel),
|
_buildCountryDropDown(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
|
|
@ -80,31 +85,27 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
onPop: viewModel.pop,
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Where are you from?',
|
'Where are you from?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Select your country and region from the dropdown',
|
'Select your country and region from the dropdown',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCountryDropDown(OnboardingViewModel viewModel) =>
|
Widget _buildCountryDropDown(OnboardingViewModel viewModel) =>
|
||||||
CustomDropdownPicker(
|
CustomDropdownPicker(
|
||||||
hint: 'Select country',
|
hint: 'Select country',
|
||||||
icon: _buildSearchIcon(),
|
icon: _buildSearchIcon(),
|
||||||
selectedItem: 'Ethiopia',
|
selectedItem: viewModel.selectedCountry,
|
||||||
items: (value, props) => viewModel.getCountries(),
|
items: (value, props) => viewModel.getCountries(),
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
viewModel.setSelectedCountry(value ?? 'Ethiopia'));
|
viewModel.setSelectedCountry(value ?? 'Ethiopia'));
|
||||||
|
|
@ -113,10 +114,11 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
CustomDropdownPicker(
|
CustomDropdownPicker(
|
||||||
hint: 'Select region',
|
hint: 'Select region',
|
||||||
icon: _buildSearchIcon(),
|
icon: _buildSearchIcon(),
|
||||||
selectedItem: 'Addis Ababa',
|
selectedItem: viewModel.selectedRegion,
|
||||||
|
items: (value, props) =>
|
||||||
|
viewModel.getRegions(viewModel.selectedCountry),
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
|
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
|
||||||
items: (value, props) => viewModel.getRegions('Addis Ababa'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Icon _buildSearchIcon() => const Icon(
|
Icon _buildSearchIcon() => const Icon(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ class EducationalBackgroundFormScreen
|
||||||
extends ViewModelWidget<OnboardingViewModel> {
|
extends ViewModelWidget<OnboardingViewModel> {
|
||||||
const EducationalBackgroundFormScreen({super.key});
|
const EducationalBackgroundFormScreen({super.key});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
viewModel.resetEducationalBackgroundFormScreen();
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -77,9 +82,9 @@ class EducationalBackgroundFormScreen
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
onPop: viewModel.pop,
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceLarge,
|
verticalSpaceLarge,
|
||||||
_buildFullNameFormField(viewModel),
|
_buildFullNameFormField(viewModel),
|
||||||
if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName)
|
if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName)
|
||||||
|
|
@ -96,7 +96,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => const Text(
|
||||||
'We’ll use your name to personalize your learning journey.',
|
'We’ll use your name to personalize your learning journey.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: TextStyle(color: kcMediumGrey),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||||
class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
const GenderFormScreen({super.key});
|
const GenderFormScreen({super.key});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
viewModel.resetGenderFormScreen();
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -63,30 +68,26 @@ class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildAgeGroups(viewModel)
|
_buildAgeGroups(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
onPop: viewModel.pop,
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Choose your gender?',
|
'Choose your gender?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'We’ll personalize your learning experience based on your gender.',
|
'We’ll personalize your learning experience based on your gender.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(
|
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
const LanguageGoalFormScreen(
|
const LanguageGoalFormScreen(
|
||||||
{super.key, required this.languageGoalController});
|
{super.key, required this.languageGoalController});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
languageGoalController.clear();
|
||||||
|
viewModel.resetLanguageGoalFormScreen();
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -75,7 +81,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildReasons(viewModel),
|
_buildReasons(viewModel),
|
||||||
if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel),
|
if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel),
|
||||||
|
|
@ -91,26 +97,20 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
onPop: viewModel.pop,
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'What’s your main goal for improving your English?',
|
'What’s your main goal for improving your English?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Your goal helps us tailor your learning journey.',
|
'Your goal helps us tailor your learning journey.',
|
||||||
style: TextStyle(
|
style: style14MG400,
|
||||||
color: kcMediumGrey,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder(
|
Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder(
|
||||||
|
|
@ -154,11 +154,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text(
|
Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text(
|
||||||
viewModel.languageGoalValidationMessage!,
|
viewModel.languageGoalValidationMessage!,
|
||||||
style: const TextStyle(
|
style: style12R700,
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
return Icons.book;
|
return Icons.book;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
viewModel.resetLearningGoalFormScreen();
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -87,19 +92,22 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
onPop: viewModel.pop,
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle(OnboardingViewModel viewModel) => Text(
|
Widget _buildTitle(OnboardingViewModel viewModel) => Text.rich(
|
||||||
'Hi ${viewModel.userData['first_name']}, Choose your learning goal.',
|
TextSpan(
|
||||||
style: const TextStyle(
|
text: 'Hi ${viewModel.userData['first_name']},',
|
||||||
fontSize: 25,
|
style: style18P600.copyWith(fontSize: 22),
|
||||||
color: kcDarkGrey,
|
children: [
|
||||||
fontWeight: FontWeight.w600,
|
TextSpan(
|
||||||
),
|
text: ' Choose your learning goal.',
|
||||||
|
style: style16DG600.copyWith(fontSize: 22),
|
||||||
|
)
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLearningGoals(OnboardingViewModel viewModel) => ListView.builder(
|
Widget _buildLearningGoals(OnboardingViewModel viewModel) => ListView.builder(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
const OccupationFormScreen({super.key, required this.occupationController});
|
const OccupationFormScreen({super.key, required this.occupationController});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
occupationController.clear();
|
||||||
|
viewModel.resetOccupationFormScreen();
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
|
|
@ -71,7 +77,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceLarge,
|
verticalSpaceLarge,
|
||||||
_buildOccupationFormField(viewModel),
|
_buildOccupationFormField(viewModel),
|
||||||
if (viewModel.hasOccupationValidationMessage &&
|
if (viewModel.hasOccupationValidationMessage &&
|
||||||
|
|
@ -84,23 +90,19 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onPop: viewModel.pop,
|
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'What’s your occupation?',
|
'What’s your occupation?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'We’ll personalize your learning experience based on your occupation.',
|
'We’ll personalize your learning experience based on your occupation.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildOccupationFormField(OnboardingViewModel viewModel) =>
|
Widget _buildOccupationFormField(OnboardingViewModel viewModel) =>
|
||||||
|
|
@ -120,11 +122,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text(
|
Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text(
|
||||||
viewModel.occupationValidationMessage!,
|
viewModel.occupationValidationMessage!,
|
||||||
style: const TextStyle(
|
style: style12R700,
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,18 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
const TopicFormScreen({super.key, required this.topicController});
|
const TopicFormScreen({super.key, required this.topicController});
|
||||||
|
|
||||||
|
void _pop(OnboardingViewModel viewModel) {
|
||||||
|
topicController.clear();
|
||||||
|
viewModel.resetTopicFormScreen();
|
||||||
|
viewModel.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
|
||||||
Map<String, dynamic> data = {
|
Map<String, dynamic> data = {
|
||||||
|
'profile_completed': true,
|
||||||
|
'preferred_language': 'en',
|
||||||
'favoutite_topic': viewModel.selectedTopic ?? topicController.text,
|
'favoutite_topic': viewModel.selectedTopic ?? topicController.text,
|
||||||
};
|
};
|
||||||
viewModel.addUserData(data);
|
viewModel.addUserData(data);
|
||||||
|
|
@ -43,8 +51,8 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onPop: viewModel.pop,
|
|
||||||
showLanguageSelection: true,
|
showLanguageSelection: true,
|
||||||
|
onPop: () => _pop(viewModel),
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -80,7 +88,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTitle(),
|
_buildTitle(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildSubTitle(),
|
_buildSubtitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTopics(viewModel),
|
_buildTopics(viewModel),
|
||||||
if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel),
|
if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel),
|
||||||
|
|
@ -95,18 +103,14 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() => const Text(
|
Widget _buildTitle() => Text(
|
||||||
'Which topics interest you most?',
|
'Which topics interest you most?',
|
||||||
style: TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Your favorite topics help us create fun, relatable lessons.',
|
'Your favorite topics help us create fun, relatable lessons.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder(
|
Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder(
|
||||||
|
|
@ -148,11 +152,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
|
|
||||||
Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text(
|
Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text(
|
||||||
viewModel.topicValidationMessage!,
|
viewModel.topicValidationMessage!,
|
||||||
style: const TextStyle(
|
style: style12R700,
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,36 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_colors.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/common/ui_helpers.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/profile_card.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/profile_image.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
|
import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
|
||||||
|
|
||||||
import '../../widgets/custom_elevated_button.dart';
|
import '../../widgets/custom_elevated_button.dart';
|
||||||
|
import '../../widgets/image_picker_option.dart';
|
||||||
import 'profile_viewmodel.dart';
|
import 'profile_viewmodel.dart';
|
||||||
|
|
||||||
class ProfileView extends StackedView<ProfileViewModel> {
|
class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
const ProfileView({Key? key}) : super(key: key);
|
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
|
@override
|
||||||
ProfileViewModel viewModelBuilder(
|
ProfileViewModel viewModelBuilder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
|
@ -24,30 +43,45 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
ProfileViewModel viewModel,
|
ProfileViewModel viewModel,
|
||||||
Widget? child,
|
Widget? child,
|
||||||
) =>
|
) =>
|
||||||
_buildScaffoldWrapper(viewModel);
|
_buildScaffoldWrapper(context: context, viewModel: viewModel);
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold(
|
Widget _buildScaffoldWrapper(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileViewModel viewModel}) =>
|
||||||
|
Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
body: _buildScaffold(viewModel),
|
body: _buildScaffold(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(ProfileViewModel viewModel) =>
|
Widget _buildScaffold(
|
||||||
SafeArea(child: _buildBodyWrapper(viewModel));
|
{required BuildContext context,
|
||||||
|
required ProfileViewModel viewModel}) =>
|
||||||
|
SafeArea(
|
||||||
|
child: _buildBodyWrapper(context: context, viewModel: viewModel));
|
||||||
|
|
||||||
Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView(
|
Widget _buildBodyWrapper(
|
||||||
child: _buildBody(viewModel),
|
{required BuildContext context,
|
||||||
|
required ProfileViewModel viewModel}) =>
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: _buildBody(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildBody(ProfileViewModel viewModel) => Padding(
|
Widget _buildBody(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileViewModel viewModel}) =>
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
child: _buildColumn(viewModel),
|
child: _buildColumn(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildColumn(ProfileViewModel viewModel) => Column(
|
Widget _buildColumn(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileViewModel viewModel}) =>
|
||||||
|
Column(
|
||||||
children: [
|
children: [
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildNotificationIconWrapper(),
|
_buildNotificationIconWrapper(),
|
||||||
_buildProfileSection(),
|
_buildProfileSection(context: context, viewModel: viewModel),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildViewProfileButton(viewModel),
|
_buildViewProfileButton(viewModel),
|
||||||
verticalSpaceLarge,
|
verticalSpaceLarge,
|
||||||
|
|
@ -66,27 +100,46 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProfileSection() => Column(
|
Widget _buildProfileSection(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileViewModel viewModel}) =>
|
||||||
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: _buildProfileSectionChildren(),
|
children: _buildProfileSectionChildren(
|
||||||
|
context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildProfileSectionChildren() => [
|
List<Widget> _buildProfileSectionChildren(
|
||||||
_buildProfileImage(),
|
{required BuildContext context,
|
||||||
|
required ProfileViewModel viewModel}) =>
|
||||||
|
[
|
||||||
|
_buildProfileImage(context: context, viewModel: viewModel),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildProfileName(),
|
_buildProfileName(viewModel),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildProfileImage() => const ProfileImage();
|
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 _buildProfileName() => const Text(
|
Widget _buildImagePicker(
|
||||||
'Hi, Bisrat 👋',
|
{required BuildContext context,
|
||||||
style: TextStyle(
|
required ProfileViewModel viewModel}) =>
|
||||||
fontSize: 25,
|
ImagePickerOption(
|
||||||
color: kcDarkGrey,
|
onCameraTap: () async => await viewModel.openCamera(),
|
||||||
fontWeight: FontWeight.w600,
|
onGalleryTap: () async => await viewModel.openGallery(),
|
||||||
),
|
);
|
||||||
|
|
||||||
|
Widget _buildProfileName(ProfileViewModel viewModel) => Text(
|
||||||
|
'Hi, ${viewModel.user?.firstName ?? ''} 👋',
|
||||||
|
style: style25DG600,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildViewProfileButton(ProfileViewModel viewModel) =>
|
Widget _buildViewProfileButton(ProfileViewModel viewModel) =>
|
||||||
|
|
@ -111,28 +164,28 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard(
|
Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard(
|
||||||
icon: Icons.download,
|
icon: Icons.download,
|
||||||
title: 'My Downloads',
|
title: 'My Downloads',
|
||||||
subTitle: 'Access offline lessons and saved videos',
|
subtitle: 'Access offline lessons and saved videos',
|
||||||
onTap: () async => await viewModel.navigateToDownloads(),
|
onTap: () async => await viewModel.navigateToDownloads(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
|
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
|
||||||
title: 'My Progress',
|
title: 'My Progress',
|
||||||
icon: Icons.stacked_bar_chart,
|
icon: Icons.stacked_bar_chart,
|
||||||
subTitle: 'Track your achievements and learning streak',
|
subtitle: 'Track your achievements and learning streak',
|
||||||
onTap: () async => await viewModel.navigateToProgress(),
|
onTap: () async => await viewModel.navigateToProgress(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
|
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
|
||||||
title: 'Account & Privacy',
|
title: 'Account & Privacy',
|
||||||
icon: Icons.privacy_tip_outlined,
|
icon: Icons.privacy_tip_outlined,
|
||||||
subTitle: 'Manage setting and app preference',
|
subtitle: 'Manage setting and app preference',
|
||||||
onTap: () async => await viewModel.navigateToAccountPrivacy(),
|
onTap: () async => await viewModel.navigateToAccountPrivacy(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
|
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
|
||||||
title: 'Support',
|
title: 'Support',
|
||||||
icon: Icons.headphones,
|
icon: Icons.headphones,
|
||||||
subTitle: 'Get help through phone or Telegram',
|
subtitle: 'Get help through phone or Telegram',
|
||||||
onTap: () async => await viewModel.navigateToSupport(),
|
onTap: () async => await viewModel.navigateToSupport(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -141,7 +194,7 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
text: 'Log Out',
|
text: 'Log Out',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
foregroundColor: kcRed,
|
foregroundColor: kcRed,
|
||||||
onTap: () async => await viewModel.logOut(),
|
|
||||||
backgroundColor: kcRed.withOpacity(0.25),
|
backgroundColor: kcRed.withOpacity(0.25),
|
||||||
|
onTap: () async => await viewModel.logOut(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,93 @@
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/app/app.router.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 '../../../app/app.locator.dart';
|
||||||
|
import '../../../models/user_model.dart';
|
||||||
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/authentication_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 _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
|
final _imagePickerService = locator<ImagePickerService>();
|
||||||
|
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
Future<void> logOut() async {
|
@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 {
|
||||||
await _authenticationService.logOut();
|
await _authenticationService.logOut();
|
||||||
await _navigationService.replaceWithLoginView();
|
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 =>
|
Future<void> navigateToProfileDetail() async =>
|
||||||
await _navigationService.navigateToProfileDetailView();
|
await _navigationService.navigateToProfileDetailView();
|
||||||
|
|
||||||
|
|
@ -29,4 +102,19 @@ class ProfileViewModel extends BaseViewModel {
|
||||||
|
|
||||||
Future<void> navigateToSupport() async =>
|
Future<void> navigateToSupport() async =>
|
||||||
await _navigationService.navigateToSupportView();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@ import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
|
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
import '../../common/enmus.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
import '../../common/validators/form_validator.dart';
|
import '../../common/validators/form_validator.dart';
|
||||||
import '../../widgets/custom_dropdown.dart';
|
import '../../widgets/custom_dropdown.dart';
|
||||||
import '../../widgets/custom_elevated_button.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 '../../widgets/profile_image.dart';
|
||||||
import 'profile_detail_viewmodel.dart';
|
import 'profile_detail_viewmodel.dart';
|
||||||
|
|
||||||
|
|
@ -23,21 +26,62 @@ import 'profile_detail_view.form.dart';
|
||||||
name: 'phoneNumber', validator: FormValidator.validatePhoneNumber),
|
name: 'phoneNumber', validator: FormValidator.validatePhoneNumber),
|
||||||
FormTextField(name: 'lastName', validator: FormValidator.validateForm),
|
FormTextField(name: 'lastName', validator: FormValidator.validateForm),
|
||||||
FormTextField(name: 'firstName', validator: FormValidator.validateForm),
|
FormTextField(name: 'firstName', validator: FormValidator.validateForm),
|
||||||
|
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
|
||||||
])
|
])
|
||||||
class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
with $ProfileDetailView {
|
with $ProfileDetailView {
|
||||||
const ProfileDetailView({Key? key}) : super(key: key);
|
const ProfileDetailView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
void _onModelReady() {
|
Future<void> _update(ProfileDetailViewModel viewModel) async {
|
||||||
firstNameController.text = 'Abel';
|
Map<String, dynamic> data = {
|
||||||
lastNameController.text = 'Abebe';
|
'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) {
|
||||||
phoneNumberController.text = '251900000000';
|
phoneNumberController.text = '251900000000';
|
||||||
emailController.text = 'email@test.com';
|
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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(ProfileDetailViewModel viewModel) {
|
void onViewModelReady(ProfileDetailViewModel viewModel) {
|
||||||
_onModelReady();
|
_onModelReady(viewModel);
|
||||||
syncFormWithViewModel(viewModel);
|
syncFormWithViewModel(viewModel);
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
@ -52,32 +96,55 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
ProfileDetailViewModel viewModel,
|
ProfileDetailViewModel viewModel,
|
||||||
Widget? child,
|
Widget? child,
|
||||||
) =>
|
) =>
|
||||||
_buildScaffoldWrapper(viewModel);
|
_buildScaffoldWrapper(context: context, viewModel: viewModel);
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(ProfileDetailViewModel viewModel) => Scaffold(
|
Widget _buildScaffoldWrapper(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
body: _buildScaffold(viewModel),
|
body: _buildScaffoldStack(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(ProfileDetailViewModel viewModel) =>
|
Widget _buildScaffoldStack(
|
||||||
SafeArea(child: _buildBodyWrapper(viewModel));
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
Stack(children: [
|
||||||
|
_buildScaffold(context: context, viewModel: viewModel),
|
||||||
|
_buildState(viewModel)
|
||||||
|
]);
|
||||||
|
|
||||||
Widget _buildBodyWrapper(ProfileDetailViewModel viewModel) => Padding(
|
Widget _buildScaffold(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
SafeArea(
|
||||||
|
child: _buildBodyWrapper(context: context, viewModel: viewModel));
|
||||||
|
|
||||||
|
Widget _buildBodyWrapper(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
child: _buildBody(viewModel),
|
child: _buildBody(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildBody(ProfileDetailViewModel viewModel) => Column(
|
Widget _buildBody(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _buildBodyChildren(viewModel),
|
children: _buildBodyChildren(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildBodyChildren(ProfileDetailViewModel viewModel) => [
|
List<Widget> _buildBodyChildren(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
[
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildAppbar(viewModel),
|
_buildAppbar(viewModel),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildColumnWrapper(viewModel)
|
_buildColumnWrapper(context: context, viewModel: viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
|
||||||
|
|
@ -85,23 +152,33 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
onTap: viewModel.pop,
|
onTap: viewModel.pop,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildColumnWrapper(ProfileDetailViewModel viewModel) =>
|
Widget _buildColumnWrapper(
|
||||||
Expanded(child: _buildBodyColumn(viewModel));
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
Expanded(child: _buildBodyColumn(context: context, viewModel: viewModel));
|
||||||
|
|
||||||
Widget _buildBodyColumn(ProfileDetailViewModel viewModel) =>
|
Widget _buildBodyColumn(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: _buildColumn(viewModel),
|
child: _buildColumn(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildColumn(ProfileDetailViewModel viewModel) => Column(
|
Widget _buildColumn(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _buildColumnChildren(viewModel),
|
children: _buildColumnChildren(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildColumnChildren(ProfileDetailViewModel viewModel) => [
|
List<Widget> _buildColumnChildren(
|
||||||
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
[
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildProfileImage(),
|
_buildProfileImageWrapper(context: context, viewModel: viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildNameFormSection(viewModel),
|
_buildNameFormSection(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
|
|
@ -120,8 +197,30 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
_buildLowerColumn(viewModel)
|
_buildLowerColumn(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildProfileImage() =>
|
Widget _buildProfileImageWrapper(
|
||||||
const Align(alignment: Alignment.center, child: ProfileImage());
|
{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 _buildNameFormSection(ProfileDetailViewModel viewModel) => Row(
|
Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -368,6 +467,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
|
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
|
||||||
TextFormField(
|
TextFormField(
|
||||||
maxLength: 12,
|
maxLength: 12,
|
||||||
|
enabled: false,
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
controller: phoneNumberController,
|
controller: phoneNumberController,
|
||||||
onTap: viewModel.setPhoneNumberFocus,
|
onTap: viewModel.setPhoneNumberFocus,
|
||||||
|
|
@ -413,6 +513,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
|
|
||||||
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
|
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
enabled: false,
|
||||||
controller: emailController,
|
controller: emailController,
|
||||||
onTap: viewModel.setPhoneNumberFocus,
|
onTap: viewModel.setPhoneNumberFocus,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
|
@ -471,10 +572,10 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
|
|
||||||
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
|
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
|
||||||
CustomDropdownPicker(
|
CustomDropdownPicker(
|
||||||
onChanged: (value) {},
|
|
||||||
hint: 'Select country',
|
hint: 'Select country',
|
||||||
selectedItem: 'Ethiopia',
|
selectedItem: viewModel.selectedCountry,
|
||||||
items: (value, props) => viewModel.getCountries(),
|
items: (value, props) => viewModel.getCountries(),
|
||||||
|
onChanged: (value) => viewModel.setSelectedCountry(value ?? 'Ethiopia'),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
|
Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
|
||||||
|
|
@ -504,9 +605,11 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) =>
|
Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) =>
|
||||||
CustomDropdownPicker(
|
CustomDropdownPicker(
|
||||||
hint: 'Select region',
|
hint: 'Select region',
|
||||||
onChanged: (value) {},
|
selectedItem: viewModel.selectedRegion,
|
||||||
selectedItem: 'Addis Ababa',
|
items: (value, props) =>
|
||||||
items: (value, props) => viewModel.getRegions('Addis Ababa'),
|
viewModel.getRegions(viewModel.selectedCountry),
|
||||||
|
onChanged: (value) =>
|
||||||
|
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
|
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
|
||||||
|
|
@ -522,7 +625,13 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
[
|
[
|
||||||
_buildOccupationDropdownLabel(),
|
_buildOccupationDropdownLabel(),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildOccupationDropdown(viewModel)
|
_buildOccupationFormField(viewModel),
|
||||||
|
if (viewModel.hasOccupationValidationMessage &&
|
||||||
|
viewModel.focusOccupation)
|
||||||
|
verticalSpaceTiny,
|
||||||
|
if (viewModel.hasOccupationValidationMessage &&
|
||||||
|
viewModel.focusOccupation)
|
||||||
|
_buildOccupationValidatorWrapper(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
|
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
|
||||||
|
|
@ -530,14 +639,29 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
|
Widget _buildOccupationFormField(ProfileDetailViewModel viewModel) =>
|
||||||
CustomDropdownPicker(
|
TextFormField(
|
||||||
hint: 'Select occupation',
|
controller: occupationController,
|
||||||
onChanged: (value) {},
|
onTap: viewModel.setOccupationFocus,
|
||||||
selectedItem: 'Student',
|
decoration: inputDecoration(
|
||||||
items: (value, props) => viewModel.getOccupations('Student'),
|
hint: 'Enter Your Occupation',
|
||||||
|
focus: viewModel.focusOccupation,
|
||||||
|
filled: occupationController.text.isNotEmpty),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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(
|
Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: _buildLowerColumnChildren(viewModel),
|
children: _buildLowerColumnChildren(viewModel),
|
||||||
|
|
@ -545,17 +669,18 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
|
|
||||||
List<Widget> _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [
|
List<Widget> _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [
|
||||||
_buildSaveButton(viewModel),
|
_buildSaveButton(viewModel),
|
||||||
verticalSpaceSmall,
|
verticalSpaceMedium,
|
||||||
_buildCancelButtonWrapper(viewModel)
|
_buildCancelButtonWrapper(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildSaveButton(ProfileDetailViewModel viewModel) =>
|
Widget _buildSaveButton(ProfileDetailViewModel viewModel) =>
|
||||||
const CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
text: 'Save Changes',
|
text: 'Save Changes',
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
|
onTap: () async => await _update(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding(
|
Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding(
|
||||||
|
|
@ -564,12 +689,18 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
|
Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
|
||||||
const CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Cancel',
|
text: 'Cancel',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderColor: kcPrimaryColor,
|
onTap: viewModel.pop,
|
||||||
backgroundColor: kcWhite,
|
backgroundColor: kcWhite,
|
||||||
|
borderColor: kcPrimaryColor,
|
||||||
foregroundColor: kcPrimaryColor,
|
foregroundColor: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget _buildState(ProfileDetailViewModel viewModel) =>
|
||||||
|
viewModel.busy(StateObjects.profileUpdate)
|
||||||
|
? const PageLoadingIndicator()
|
||||||
|
: Container();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const String EmailValueKey = 'email';
|
||||||
const String PhoneNumberValueKey = 'phoneNumber';
|
const String PhoneNumberValueKey = 'phoneNumber';
|
||||||
const String LastNameValueKey = 'lastName';
|
const String LastNameValueKey = 'lastName';
|
||||||
const String FirstNameValueKey = 'firstName';
|
const String FirstNameValueKey = 'firstName';
|
||||||
|
const String OccupationValueKey = 'occupation';
|
||||||
|
|
||||||
final Map<String, TextEditingController>
|
final Map<String, TextEditingController>
|
||||||
_ProfileDetailViewTextEditingControllers = {};
|
_ProfileDetailViewTextEditingControllers = {};
|
||||||
|
|
@ -28,6 +29,7 @@ final Map<String, String? Function(String?)?>
|
||||||
PhoneNumberValueKey: FormValidator.validatePhoneNumber,
|
PhoneNumberValueKey: FormValidator.validatePhoneNumber,
|
||||||
LastNameValueKey: FormValidator.validateForm,
|
LastNameValueKey: FormValidator.validateForm,
|
||||||
FirstNameValueKey: FormValidator.validateForm,
|
FirstNameValueKey: FormValidator.validateForm,
|
||||||
|
OccupationValueKey: FormValidator.validateForm,
|
||||||
};
|
};
|
||||||
|
|
||||||
mixin $ProfileDetailView {
|
mixin $ProfileDetailView {
|
||||||
|
|
@ -39,11 +41,14 @@ mixin $ProfileDetailView {
|
||||||
_getFormTextEditingController(LastNameValueKey);
|
_getFormTextEditingController(LastNameValueKey);
|
||||||
TextEditingController get firstNameController =>
|
TextEditingController get firstNameController =>
|
||||||
_getFormTextEditingController(FirstNameValueKey);
|
_getFormTextEditingController(FirstNameValueKey);
|
||||||
|
TextEditingController get occupationController =>
|
||||||
|
_getFormTextEditingController(OccupationValueKey);
|
||||||
|
|
||||||
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
|
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
|
||||||
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
|
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
|
||||||
FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey);
|
FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey);
|
||||||
FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey);
|
FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey);
|
||||||
|
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
|
||||||
|
|
||||||
TextEditingController _getFormTextEditingController(
|
TextEditingController _getFormTextEditingController(
|
||||||
String key, {
|
String key, {
|
||||||
|
|
@ -73,6 +78,7 @@ mixin $ProfileDetailView {
|
||||||
phoneNumberController.addListener(() => _updateFormData(model));
|
phoneNumberController.addListener(() => _updateFormData(model));
|
||||||
lastNameController.addListener(() => _updateFormData(model));
|
lastNameController.addListener(() => _updateFormData(model));
|
||||||
firstNameController.addListener(() => _updateFormData(model));
|
firstNameController.addListener(() => _updateFormData(model));
|
||||||
|
occupationController.addListener(() => _updateFormData(model));
|
||||||
|
|
||||||
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +94,7 @@ mixin $ProfileDetailView {
|
||||||
phoneNumberController.addListener(() => _updateFormData(model));
|
phoneNumberController.addListener(() => _updateFormData(model));
|
||||||
lastNameController.addListener(() => _updateFormData(model));
|
lastNameController.addListener(() => _updateFormData(model));
|
||||||
firstNameController.addListener(() => _updateFormData(model));
|
firstNameController.addListener(() => _updateFormData(model));
|
||||||
|
occupationController.addListener(() => _updateFormData(model));
|
||||||
|
|
||||||
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +108,7 @@ mixin $ProfileDetailView {
|
||||||
PhoneNumberValueKey: phoneNumberController.text,
|
PhoneNumberValueKey: phoneNumberController.text,
|
||||||
LastNameValueKey: lastNameController.text,
|
LastNameValueKey: lastNameController.text,
|
||||||
FirstNameValueKey: firstNameController.text,
|
FirstNameValueKey: firstNameController.text,
|
||||||
|
OccupationValueKey: occupationController.text,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -147,6 +155,8 @@ extension ValueProperties on FormStateHelper {
|
||||||
this.formValueMap[PhoneNumberValueKey] as String?;
|
this.formValueMap[PhoneNumberValueKey] as String?;
|
||||||
String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?;
|
String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?;
|
||||||
String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?;
|
String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?;
|
||||||
|
String? get occupationValue =>
|
||||||
|
this.formValueMap[OccupationValueKey] as String?;
|
||||||
|
|
||||||
set emailValue(String? value) {
|
set emailValue(String? value) {
|
||||||
this.setData(
|
this.setData(
|
||||||
|
|
@ -195,6 +205,18 @@ 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 =>
|
bool get hasEmail =>
|
||||||
this.formValueMap.containsKey(EmailValueKey) &&
|
this.formValueMap.containsKey(EmailValueKey) &&
|
||||||
(emailValue?.isNotEmpty ?? false);
|
(emailValue?.isNotEmpty ?? false);
|
||||||
|
|
@ -207,6 +229,9 @@ extension ValueProperties on FormStateHelper {
|
||||||
bool get hasFirstName =>
|
bool get hasFirstName =>
|
||||||
this.formValueMap.containsKey(FirstNameValueKey) &&
|
this.formValueMap.containsKey(FirstNameValueKey) &&
|
||||||
(firstNameValue?.isNotEmpty ?? false);
|
(firstNameValue?.isNotEmpty ?? false);
|
||||||
|
bool get hasOccupation =>
|
||||||
|
this.formValueMap.containsKey(OccupationValueKey) &&
|
||||||
|
(occupationValue?.isNotEmpty ?? false);
|
||||||
|
|
||||||
bool get hasEmailValidationMessage =>
|
bool get hasEmailValidationMessage =>
|
||||||
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
|
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
|
||||||
|
|
@ -216,6 +241,8 @@ extension ValueProperties on FormStateHelper {
|
||||||
this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false;
|
this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false;
|
||||||
bool get hasFirstNameValidationMessage =>
|
bool get hasFirstNameValidationMessage =>
|
||||||
this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false;
|
this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false;
|
||||||
|
bool get hasOccupationValidationMessage =>
|
||||||
|
this.fieldsValidationMessages[OccupationValueKey]?.isNotEmpty ?? false;
|
||||||
|
|
||||||
String? get emailValidationMessage =>
|
String? get emailValidationMessage =>
|
||||||
this.fieldsValidationMessages[EmailValueKey];
|
this.fieldsValidationMessages[EmailValueKey];
|
||||||
|
|
@ -225,6 +252,8 @@ extension ValueProperties on FormStateHelper {
|
||||||
this.fieldsValidationMessages[LastNameValueKey];
|
this.fieldsValidationMessages[LastNameValueKey];
|
||||||
String? get firstNameValidationMessage =>
|
String? get firstNameValidationMessage =>
|
||||||
this.fieldsValidationMessages[FirstNameValueKey];
|
this.fieldsValidationMessages[FirstNameValueKey];
|
||||||
|
String? get occupationValidationMessage =>
|
||||||
|
this.fieldsValidationMessages[OccupationValueKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Methods on FormStateHelper {
|
extension Methods on FormStateHelper {
|
||||||
|
|
@ -236,6 +265,8 @@ extension Methods on FormStateHelper {
|
||||||
this.fieldsValidationMessages[LastNameValueKey] = validationMessage;
|
this.fieldsValidationMessages[LastNameValueKey] = validationMessage;
|
||||||
setFirstNameValidationMessage(String? validationMessage) =>
|
setFirstNameValidationMessage(String? validationMessage) =>
|
||||||
this.fieldsValidationMessages[FirstNameValueKey] = validationMessage;
|
this.fieldsValidationMessages[FirstNameValueKey] = validationMessage;
|
||||||
|
setOccupationValidationMessage(String? validationMessage) =>
|
||||||
|
this.fieldsValidationMessages[OccupationValueKey] = validationMessage;
|
||||||
|
|
||||||
/// Clears text input fields on the Form
|
/// Clears text input fields on the Form
|
||||||
void clearForm() {
|
void clearForm() {
|
||||||
|
|
@ -243,6 +274,7 @@ extension Methods on FormStateHelper {
|
||||||
phoneNumberValue = '';
|
phoneNumberValue = '';
|
||||||
lastNameValue = '';
|
lastNameValue = '';
|
||||||
firstNameValue = '';
|
firstNameValue = '';
|
||||||
|
occupationValue = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates text input fields on the Form
|
/// Validates text input fields on the Form
|
||||||
|
|
@ -252,6 +284,7 @@ extension Methods on FormStateHelper {
|
||||||
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
|
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
|
||||||
LastNameValueKey: getValidationMessage(LastNameValueKey),
|
LastNameValueKey: getValidationMessage(LastNameValueKey),
|
||||||
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
|
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
|
||||||
|
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -275,4 +308,5 @@ void updateValidationData(FormStateHelper model) =>
|
||||||
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
|
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
|
||||||
LastNameValueKey: getValidationMessage(LastNameValueKey),
|
LastNameValueKey: getValidationMessage(LastNameValueKey),
|
||||||
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
|
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
|
||||||
|
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,36 @@ import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.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 _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
|
// First name
|
||||||
bool _focusFirstName = false;
|
bool _focusFirstName = false;
|
||||||
|
|
||||||
|
|
@ -35,6 +62,26 @@ class ProfileDetailViewModel extends FormViewModel {
|
||||||
|
|
||||||
bool get focusEmail => _focusEmail;
|
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
|
// First name
|
||||||
void setFirstNameFocus() {
|
void setFirstNameFocus() {
|
||||||
_focusFirstName = true;
|
_focusFirstName = true;
|
||||||
|
|
@ -72,15 +119,132 @@ class ProfileDetailViewModel extends FormViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Country
|
// Country
|
||||||
Future<List<String>> getCountries() async => ['Ethiopia', 'Djibouti'];
|
List<String> getCountries() => ['Ethiopia', 'Other'];
|
||||||
|
|
||||||
|
void setSelectedCountry(String value) {
|
||||||
|
_selectedCountry = value;
|
||||||
|
if (selectedCountry != 'Ethiopia') {
|
||||||
|
_selectedRegion = 'Other';
|
||||||
|
} else {
|
||||||
|
_selectedRegion = 'Addis Ababa';
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildUi();
|
||||||
|
}
|
||||||
|
|
||||||
// Region
|
// Region
|
||||||
Future<List<String>> getRegions(String country) async =>
|
List<String> getRegions(String country) {
|
||||||
['Addis Ababa', 'Oromia'];
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
// Occupation
|
// Occupation
|
||||||
Future<List<String>> getOccupations(String country) async =>
|
void setOccupationFocus() {
|
||||||
['Student', 'Worker'];
|
_focusOccupation = true;
|
||||||
|
rebuildUi();
|
||||||
void pop() => _navigationService.back();
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
|
||||||
title: viewModel.progresses[index]['title'],
|
title: viewModel.progresses[index]['title'],
|
||||||
color: viewModel.progresses[index]['color'],
|
color: viewModel.progresses[index]['color'],
|
||||||
status: viewModel.progresses[index]['status'],
|
status: viewModel.progresses[index]['status'],
|
||||||
subTitle: viewModel.progresses[index]['subTitle'],
|
subtitle: viewModel.progresses[index]['subtitle'],
|
||||||
isCompleted: viewModel.progresses[index]['isCompleted'],
|
isCompleted: viewModel.progresses[index]['isCompleted'],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -111,7 +111,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
|
||||||
required String title,
|
required String title,
|
||||||
required String icon,
|
required String icon,
|
||||||
required String status,
|
required String status,
|
||||||
required String subTitle,
|
required String subtitle,
|
||||||
required bool isCompleted,
|
required bool isCompleted,
|
||||||
required ProgressViewModel viewModel}) =>
|
required ProgressViewModel viewModel}) =>
|
||||||
CourseLevelCard(
|
CourseLevelCard(
|
||||||
|
|
@ -119,7 +119,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
|
||||||
title: title,
|
title: title,
|
||||||
color: color,
|
color: color,
|
||||||
status: status,
|
status: status,
|
||||||
subTitle: subTitle,
|
subtitle: subtitle,
|
||||||
isCompleted: isCompleted,
|
isCompleted: isCompleted,
|
||||||
onTap: viewModel.navigateToOngoingProgress,
|
onTap: viewModel.navigateToOngoingProgress,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,24 @@ class ProgressViewModel extends BaseViewModel {
|
||||||
'title': 'Beginner',
|
'title': 'Beginner',
|
||||||
'isCompleted': true,
|
'isCompleted': true,
|
||||||
'status': 'Completed',
|
'status': 'Completed',
|
||||||
'icon': 'assets/icons/b1.svg',
|
'icon': 'assets/icons/b_1.svg',
|
||||||
'subTitle': 'You’ve mastered everyday English basics!',
|
'subtitle': 'You’ve mastered everyday English basics!',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Elementary',
|
'title': 'Elementary',
|
||||||
'isCompleted': false,
|
'isCompleted': false,
|
||||||
'status': 'In Progress',
|
'status': 'In Progress',
|
||||||
'color': kcPrimaryColor,
|
'color': kcPrimaryColor,
|
||||||
'icon': 'assets/icons/b1.svg',
|
'icon': 'assets/icons/b_1.svg',
|
||||||
'subTitle': 'Continue improving your conversations and fluency.',
|
'subtitle': 'Continue improving your conversations and fluency.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Beginner',
|
'title': 'Beginner',
|
||||||
'isCompleted': true,
|
'isCompleted': true,
|
||||||
'status': 'In Progress',
|
'status': 'In Progress',
|
||||||
'color': kcPrimaryColor,
|
'color': kcPrimaryColor,
|
||||||
'icon': 'assets/icons/b1.svg',
|
'icon': 'assets/icons/b_1.svg',
|
||||||
'subTitle': 'You’ve mastered everyday English basics!',
|
'subtitle': 'You’ve mastered everyday English basics!',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked/stacked_annotations.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/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_email_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart';
|
import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart';
|
||||||
|
|
@ -24,8 +25,46 @@ import 'register_view.form.dart';
|
||||||
class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
|
class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
|
||||||
const RegisterView({Key? key}) : super(key: key);
|
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
|
@override
|
||||||
void onViewModelReady(RegisterViewModel viewModel) {
|
void onViewModelReady(RegisterViewModel viewModel) {
|
||||||
|
_initClearData();
|
||||||
syncFormWithViewModel(viewModel);
|
syncFormWithViewModel(viewModel);
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
@ -44,44 +83,14 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
|
||||||
|
|
||||||
Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope(
|
Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope(
|
||||||
canPop: false,
|
canPop: false,
|
||||||
onPopInvokedWithResult: (value, data) {
|
onPopInvokedWithResult: (value, data) =>
|
||||||
if (value) return;
|
_pop(value: value, viewModel: viewModel),
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
|
child: _buildBody(viewModel));
|
||||||
},
|
|
||||||
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) =>
|
Widget _buildBody(RegisterViewModel viewModel) =>
|
||||||
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
|
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
|
||||||
|
|
||||||
List<Widget> _buildScreens() => [
|
List<Widget> _buildScreens() => [
|
||||||
_buildRegisterWithEmailScreen(),
|
_buildRegisterWithEmailScreen(),
|
||||||
|
|
@ -106,6 +115,4 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
|
||||||
passwordController: passwordController,
|
passwordController: passwordController,
|
||||||
confirmPasswordController: confirmPasswordController);
|
confirmPasswordController: confirmPasswordController);
|
||||||
|
|
||||||
Widget _buildBusyRegistration(RegisterViewModel viewModel) =>
|
|
||||||
viewModel.isBusy ? const PageLoadingIndicator() : Container();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||