Compare commits
No commits in common. "757dfad4e8c6ea2fe57614f16eb6718111c38c15" and "befbfb472782099d7ad7019abf7d803ed7000b40" have entirely different histories.
757dfad4e8
...
befbfb4727
|
|
@ -1,12 +1,12 @@
|
|||
plugins {
|
||||
id("kotlin-android")
|
||||
id("com.android.application")
|
||||
id("com.google.gms.google-services")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.yimaru.lms.app"
|
||||
namespace = "com.example.yimaru_app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
|
|
@ -15,24 +15,25 @@ android {
|
|||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.yimaru_app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
applicationId = "com.yimaru.lms.app"
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "574860813475",
|
||||
"project_id": "yimaru-lms-e834e",
|
||||
"storage_bucket": "yimaru-lms-e834e.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:574860813475:android:cd7fa6cf3a0527d97acb16",
|
||||
"android_client_info": {
|
||||
"package_name": "com.yimaru.lms.app"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.yimaru.lms.app",
|
||||
"certificate_hash": "fc91f52846d27c62bba3e16bc98982fb9953eca1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "574860813475-631s3mo8ha2qc2jeb5e2aosn0967niik.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.yimaru.lms.app",
|
||||
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
|
||||
}
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
|
|
@ -1,13 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<application
|
||||
android:label="Yimaru"
|
||||
android:label="yimaru_app"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.yimaru.lms.app
|
||||
package com.example.yimaru_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
|
@ -19,10 +19,8 @@ pluginManagement {
|
|||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.13.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
|
||||
id("com.google.gms.google-services") version("4.4.4") apply false
|
||||
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.9409C116.627 97.7416 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7416 8.37341 93.9409V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
|
||||
<path d="M55.1865 5.94918C59.7122 3.33638 65.2878 3.33638 69.8135 5.94918L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94918Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
|
||||
<path d="M40.0531 83.9074L50.2659 55.9819H57.4467L67.5797 83.9074H61.1569L59.0026 77.8037H48.3909L46.2366 83.9074H40.0531ZM49.9866 72.9766H57.367L53.6569 62.3649L49.9866 72.9766ZM79.9963 55.9819V83.9074H74.2516V66.075C73.9059 66.208 73.4006 66.2745 72.7357 66.2745H68.0681V61.5271H72.2569C72.9484 61.5271 73.467 61.2612 73.8128 60.7292C74.1851 60.1707 74.3713 59.4925 74.3713 58.6947V55.9819H79.9963Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,5 +0,0 @@
|
|||
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.9409C116.627 97.7416 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7416 8.37341 93.9409V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
|
||||
<path d="M55.1865 5.94918C59.7122 3.33638 65.2878 3.33638 69.8135 5.94918L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94918Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
|
||||
<path d="M40.0531 83.9074L50.2659 55.9819H57.4467L67.5797 83.9074H61.1569L59.0026 77.8037H48.3909L46.2366 83.9074H40.0531ZM49.9866 72.9766H57.367L53.6569 62.3649L49.9866 72.9766ZM79.5575 70.7027C80.9139 69.6122 81.9112 68.6149 82.5495 67.7106C83.1878 66.8064 83.5069 65.8223 83.5069 64.7585C83.5069 63.4021 83.0947 62.3649 82.2702 61.6468C81.4724 60.9021 80.3952 60.5298 79.0389 60.5298C77.7357 60.5298 76.6718 60.9686 75.8474 61.8463C75.0495 62.7239 74.6506 63.9074 74.6506 65.3968V65.8356H68.866V65.0777C68.866 63.3223 69.2782 61.7266 70.1027 60.2904C70.9537 58.8542 72.1506 57.7372 73.6931 56.9394C75.2357 56.1149 77.0176 55.7027 79.0389 55.7027C82.3101 55.7027 84.8367 56.5005 86.6186 58.0963C88.4272 59.692 89.3314 61.833 89.3314 64.5191C89.3314 66.434 88.8527 68.0963 87.8952 69.5058C86.9644 70.8888 85.5149 72.3782 83.5468 73.9739L77.2835 79.0803H89.4511V83.9074H68.9857V79.4792L79.5575 70.7027Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
10
assets/icons/b1.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="125px" height="138px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g><path style="opacity:0.808" fill="#9e2891" d="M 55.5,-0.5 C 59.8333,-0.5 64.1667,-0.5 68.5,-0.5C 84.7704,8.97332 101.104,18.4733 117.5,28C 120.738,30.9978 123.071,34.4978 124.5,38.5C 124.5,58.5 124.5,78.5 124.5,98.5C 122.507,103.008 119.507,106.842 115.5,110C 99.2743,118.941 83.2743,128.108 67.5,137.5C 64.1667,137.5 60.8333,137.5 57.5,137.5C 41.2609,128.375 24.9276,119.208 8.5,110C 4.49334,106.842 1.49334,103.008 -0.5,98.5C -0.5,78.5 -0.5,58.5 -0.5,38.5C 1.15508,34.654 3.48842,31.154 6.5,28C 22.8963,18.4733 39.2296,8.97332 55.5,-0.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#e9cee5" d="M 42.5,55.5 C 42.5,64.5 42.5,73.5 42.5,82.5C 47.1667,82.5 51.8333,82.5 56.5,82.5C 51.6946,83.4872 46.6946,83.8205 41.5,83.5C 41.1731,73.985 41.5064,64.6517 42.5,55.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#fdfafc" d="M 42.5,55.5 C 47.1785,55.3342 51.8452,55.5008 56.5,56C 61.2911,57.4119 63.1244,60.5786 62,65.5C 61.1667,67 60,68.1667 58.5,69C 63.8662,71.8444 64.8662,75.8444 61.5,81C 59.9751,82.0086 58.3084,82.5086 56.5,82.5C 51.8333,82.5 47.1667,82.5 42.5,82.5C 42.5,73.5 42.5,64.5 42.5,55.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#fbf7fb" d="M 71.5,55.5 C 73.5,55.5 75.5,55.5 77.5,55.5C 77.5,64.8333 77.5,74.1667 77.5,83.5C 75.5,83.5 73.5,83.5 71.5,83.5C 71.5,77.5 71.5,71.5 71.5,65.5C 69.5,65.5 67.5,65.5 65.5,65.5C 65.5,64.1667 65.5,62.8333 65.5,61.5C 70.5,62.5 72.5,60.5 71.5,55.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a63d9a" d="M 47.5,60.5 C 50.1873,60.3359 52.854,60.5026 55.5,61C 57.1397,63.7758 56.473,65.7758 53.5,67C 51.5273,67.4955 49.5273,67.6621 47.5,67.5C 47.5,65.1667 47.5,62.8333 47.5,60.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#a33596" d="M 47.5,71.5 C 50.1873,71.3359 52.854,71.5026 55.5,72C 58.033,74.084 58.033,76.084 55.5,78C 52.854,78.4974 50.1873,78.6641 47.5,78.5C 47.5,76.1667 47.5,73.8333 47.5,71.5 Z"/></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -1,5 +0,0 @@
|
|||
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.941C116.627 97.7417 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7417 8.37341 93.941V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
|
||||
<path d="M55.1865 5.94919C59.7122 3.33638 65.2878 3.33638 69.8135 5.94919L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94919Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
|
||||
<path d="M52.8989 55.9819C56.1436 55.9819 58.6569 56.6202 60.4388 57.8968C62.2207 59.1734 63.1116 60.9819 63.1116 63.3223C63.1116 65.0511 62.6329 66.4473 61.6755 67.5112C60.718 68.575 59.3616 69.2665 57.6063 69.5856C59.6808 69.8516 61.2765 70.5165 62.3936 71.5803C63.5372 72.6441 64.109 74.1335 64.109 76.0484C64.109 78.4952 63.1781 80.4234 61.3164 81.833C59.4547 83.216 56.8217 83.9074 53.4175 83.9074H42.2872V55.9819H52.8989ZM48.2712 67.4713H53.0584C54.335 67.4713 55.3324 67.1654 56.0505 66.5537C56.7686 65.942 57.1276 65.1175 57.1276 64.0803C57.1276 63.0431 56.7686 62.2186 56.0505 61.6069C55.3324 60.9952 54.335 60.6894 53.0584 60.6894H48.2712V67.4713ZM48.2712 79.1601H53.4574C54.8936 79.1601 55.9973 78.8542 56.7686 78.2425C57.5664 77.6308 57.9654 76.7798 57.9654 75.6894C57.9654 74.5457 57.5664 73.6681 56.7686 73.0564C55.9707 72.4181 54.867 72.0989 53.4574 72.0989H48.2712V79.1601ZM77.9315 55.9819V83.9074H72.1868V66.075C71.8411 66.208 71.3358 66.2745 70.6709 66.2745H66.0033V61.5271H70.1921C70.8836 61.5271 71.4022 61.2612 71.748 60.7292C72.1203 60.1707 72.3065 59.4925 72.3065 58.6947V55.9819H77.9315Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
|
@ -1,5 +0,0 @@
|
|||
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.941C116.627 97.7417 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7417 8.37341 93.941V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
|
||||
<path d="M55.1865 5.94919C59.7122 3.33638 65.2878 3.33638 69.8135 5.94919L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94919Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
|
||||
<path d="M52.8989 55.9819C56.1436 55.9819 58.6569 56.6202 60.4388 57.8968C62.2207 59.1734 63.1116 60.9819 63.1116 63.3223C63.1116 65.0511 62.6329 66.4473 61.6755 67.5112C60.718 68.575 59.3616 69.2665 57.6063 69.5856C59.6808 69.8516 61.2765 70.5165 62.3936 71.5803C63.5372 72.6441 64.109 74.1335 64.109 76.0484C64.109 78.4952 63.1781 80.4234 61.3164 81.833C59.4547 83.216 56.8217 83.9074 53.4175 83.9074H42.2872V55.9819H52.8989ZM48.2712 67.4713H53.0584C54.335 67.4713 55.3324 67.1654 56.0505 66.5537C56.7686 65.942 57.1276 65.1175 57.1276 64.0803C57.1276 63.0431 56.7686 62.2186 56.0505 61.6069C55.3324 60.9952 54.335 60.6894 53.0584 60.6894H48.2712V67.4713ZM48.2712 79.1601H53.4574C54.8936 79.1601 55.9973 78.8542 56.7686 78.2425C57.5664 77.6308 57.9654 76.7798 57.9654 75.6894C57.9654 74.5457 57.5664 73.6681 56.7686 73.0564C55.9707 72.4181 54.867 72.0989 53.4574 72.0989H48.2712V79.1601ZM77.4927 70.7027C78.849 69.6122 79.8464 68.6149 80.4847 67.7106C81.123 66.8064 81.4421 65.8223 81.4421 64.7585C81.4421 63.4021 81.0299 62.3649 80.2054 61.6468C79.4076 60.9021 78.3304 60.5298 76.974 60.5298C75.6709 60.5298 74.607 60.9686 73.7826 61.8463C72.9847 62.7239 72.5858 63.9074 72.5858 65.3968V65.8356H66.8012V65.0777C66.8012 63.3223 67.2134 61.7266 68.0379 60.2904C68.8889 58.8542 70.0858 57.7372 71.6283 56.9394C73.1709 56.1149 74.9528 55.7027 76.974 55.7027C80.2453 55.7027 82.7719 56.5005 84.5538 58.0963C86.3623 59.692 87.2666 61.833 87.2666 64.5191C87.2666 66.434 86.7879 68.0963 85.8304 69.5058C84.8996 70.8888 83.4501 72.3782 81.482 73.9739L75.2187 79.0803H87.3863V83.9074H66.9209V79.4792L77.4927 70.7027Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 9.6 KiB |
|
|
@ -1 +0,0 @@
|
|||
{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-lms-e834e","appId":"1:574860813475:android:cd7fa6cf3a0527d97acb16","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-lms-e834e","configurations":{"android":"1:574860813475:android:cd7fa6cf3a0527d97acb16","ios":"1:574860813475:ios:3ac9f7c4ae1771287acb16"}}}}}}
|
||||
|
|
@ -368,7 +368,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -384,7 +384,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
|
|
@ -401,7 +401,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
|
|
@ -416,7 +416,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
|
|
@ -547,7 +547,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
|
|
@ -569,7 +569,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
|
|||
|
|
@ -30,14 +30,6 @@ import 'package:yimaru_app/services/status_checker_service.dart';
|
|||
import 'package:yimaru_app/ui/views/welcome/welcome_view.dart';
|
||||
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
|
||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
||||
import 'package:yimaru_app/services/permission_handler_service.dart';
|
||||
import 'package:yimaru_app/services/image_picker_service.dart';
|
||||
import 'package:yimaru_app/services/google_auth_service.dart';
|
||||
import 'package:yimaru_app/services/image_downloader_service.dart';
|
||||
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||
// @stacked-import
|
||||
|
||||
@StackedApp(
|
||||
|
|
@ -65,10 +57,6 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
|||
MaterialRoute(page: WelcomeView),
|
||||
MaterialRoute(page: AssessmentView),
|
||||
MaterialRoute(page: LearnLessonView),
|
||||
MaterialRoute(page: FailureView),
|
||||
MaterialRoute(page: ForgetPasswordView),
|
||||
MaterialRoute(page: LearnLessonDetailView),
|
||||
MaterialRoute(page: LearnPracticeView),
|
||||
// @stacked-route
|
||||
],
|
||||
dependencies: [
|
||||
|
|
@ -80,10 +68,6 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
|||
LazySingleton(classType: SecureStorageService),
|
||||
LazySingleton(classType: DioService),
|
||||
LazySingleton(classType: StatusCheckerService),
|
||||
LazySingleton(classType: PermissionHandlerService),
|
||||
LazySingleton(classType: ImagePickerService),
|
||||
LazySingleton(classType: GoogleAuthService),
|
||||
LazySingleton(classType: ImageDownloaderService),
|
||||
// @stacked-service
|
||||
],
|
||||
bottomsheets: [
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ import 'package:stacked_shared/stacked_shared.dart';
|
|||
import '../services/api_service.dart';
|
||||
import '../services/authentication_service.dart';
|
||||
import '../services/dio_service.dart';
|
||||
import '../services/google_auth_service.dart';
|
||||
import '../services/image_downloader_service.dart';
|
||||
import '../services/image_picker_service.dart';
|
||||
import '../services/permission_handler_service.dart';
|
||||
import '../services/secure_storage_service.dart';
|
||||
import '../services/status_checker_service.dart';
|
||||
|
||||
|
|
@ -40,8 +36,4 @@ Future<void> setupLocator({
|
|||
locator.registerLazySingleton(() => SecureStorageService());
|
||||
locator.registerLazySingleton(() => DioService());
|
||||
locator.registerLazySingleton(() => StatusCheckerService());
|
||||
locator.registerLazySingleton(() => PermissionHandlerService());
|
||||
locator.registerLazySingleton(() => ImagePickerService());
|
||||
locator.registerLazySingleton(() => GoogleAuthService());
|
||||
locator.registerLazySingleton(() => ImageDownloaderService());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,31 +5,24 @@
|
|||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:flutter/material.dart' as _i29;
|
||||
import 'package:flutter/material.dart' as _i25;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart' as _i1;
|
||||
import 'package:stacked_services/stacked_services.dart' as _i30;
|
||||
import 'package:stacked_services/stacked_services.dart' as _i26;
|
||||
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
|
||||
as _i10;
|
||||
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
|
||||
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'
|
||||
as _i13;
|
||||
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7;
|
||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart' as _i25;
|
||||
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'
|
||||
as _i26;
|
||||
import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2;
|
||||
import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14;
|
||||
import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19;
|
||||
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'
|
||||
as _i24;
|
||||
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart'
|
||||
as _i27;
|
||||
import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20;
|
||||
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
|
||||
as _i21;
|
||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'
|
||||
as _i28;
|
||||
import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18;
|
||||
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3;
|
||||
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart'
|
||||
|
|
@ -96,14 +89,6 @@ class Routes {
|
|||
|
||||
static const learnLessonView = '/learn-lesson-view';
|
||||
|
||||
static const failureView = '/failure-view';
|
||||
|
||||
static const forgetPasswordView = '/forget-password-view';
|
||||
|
||||
static const learnLessonDetailView = '/learn-lesson-detail-view';
|
||||
|
||||
static const learnPracticeView = '/learn-practice-view';
|
||||
|
||||
static const all = <String>{
|
||||
homeView,
|
||||
onboardingView,
|
||||
|
|
@ -128,10 +113,6 @@ class Routes {
|
|||
welcomeView,
|
||||
assessmentView,
|
||||
learnLessonView,
|
||||
failureView,
|
||||
forgetPasswordView,
|
||||
learnLessonDetailView,
|
||||
learnPracticeView,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -229,33 +210,17 @@ class StackedRouter extends _i1.RouterBase {
|
|||
Routes.learnLessonView,
|
||||
page: _i24.LearnLessonView,
|
||||
),
|
||||
_i1.RouteDef(
|
||||
Routes.failureView,
|
||||
page: _i25.FailureView,
|
||||
),
|
||||
_i1.RouteDef(
|
||||
Routes.forgetPasswordView,
|
||||
page: _i26.ForgetPasswordView,
|
||||
),
|
||||
_i1.RouteDef(
|
||||
Routes.learnLessonDetailView,
|
||||
page: _i27.LearnLessonDetailView,
|
||||
),
|
||||
_i1.RouteDef(
|
||||
Routes.learnPracticeView,
|
||||
page: _i28.LearnPracticeView,
|
||||
),
|
||||
];
|
||||
|
||||
final _pagesMap = <Type, _i1.StackedRouteFactory>{
|
||||
_i2.HomeView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i2.HomeView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i3.OnboardingView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i3.OnboardingView(),
|
||||
settings: data,
|
||||
);
|
||||
|
|
@ -264,159 +229,133 @@ class StackedRouter extends _i1.RouterBase {
|
|||
final args = data.getArgs<StartupViewArguments>(
|
||||
orElse: () => const StartupViewArguments(),
|
||||
);
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => _i4.StartupView(key: args.key, label: args.label),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i5.ProfileView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i5.ProfileView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i6.ProfileDetailView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i6.ProfileDetailView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i7.DownloadsView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i7.DownloadsView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i8.ProgressView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i8.ProgressView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i9.OngoingProgressView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i9.OngoingProgressView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i10.AccountPrivacyView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i10.AccountPrivacyView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i11.SupportView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i11.SupportView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i12.TelegramSupportView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i12.TelegramSupportView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i13.CallSupportView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i13.CallSupportView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i14.LanguageView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i14.LanguageView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i15.PrivacyPolicyView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i15.PrivacyPolicyView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i16.TermsAndConditionsView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i16.TermsAndConditionsView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i17.RegisterView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i17.RegisterView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i18.LoginView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i18.LoginView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i19.LearnView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i19.LearnView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i20.LearnLevelView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i20.LearnLevelView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i21.LearnModuleView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i21.LearnModuleView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i22.WelcomeView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i22.WelcomeView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i23.AssessmentView: (data) {
|
||||
final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) =>
|
||||
_i23.AssessmentView(key: args.key, data: args.data),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i24.LearnLessonView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
return _i25.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i24.LearnLessonView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i25.FailureView: (data) {
|
||||
final args = data.getArgs<FailureViewArguments>(nullOk: false);
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
builder: (context) =>
|
||||
_i25.FailureView(key: args.key, label: args.label),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i26.ForgetPasswordView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i26.ForgetPasswordView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i27.LearnLessonDetailView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i27.LearnLessonDetailView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
_i28.LearnPracticeView: (data) {
|
||||
return _i29.MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const _i28.LearnPracticeView(),
|
||||
settings: data,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
@ -432,7 +371,7 @@ class StartupViewArguments {
|
|||
this.label = 'Loading',
|
||||
});
|
||||
|
||||
final _i29.Key? key;
|
||||
final _i25.Key? key;
|
||||
|
||||
final String label;
|
||||
|
||||
|
|
@ -459,7 +398,7 @@ class AssessmentViewArguments {
|
|||
required this.data,
|
||||
});
|
||||
|
||||
final _i29.Key? key;
|
||||
final _i25.Key? key;
|
||||
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
|
|
@ -480,34 +419,7 @@ class AssessmentViewArguments {
|
|||
}
|
||||
}
|
||||
|
||||
class FailureViewArguments {
|
||||
const FailureViewArguments({
|
||||
this.key,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final _i29.Key? key;
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '{"key": "$key", "label": "$label"}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant FailureViewArguments other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other.key == key && other.label == label;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return key.hashCode ^ label.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
extension NavigatorStateExtension on _i30.NavigationService {
|
||||
extension NavigatorStateExtension on _i26.NavigationService {
|
||||
Future<dynamic> navigateToHomeView([
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
|
|
@ -537,7 +449,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
|
|||
}
|
||||
|
||||
Future<dynamic> navigateToStartupView({
|
||||
_i29.Key? key,
|
||||
_i25.Key? key,
|
||||
String label = 'Loading',
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
|
|
@ -806,7 +718,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
|
|||
}
|
||||
|
||||
Future<dynamic> navigateToAssessmentView({
|
||||
_i29.Key? key,
|
||||
_i25.Key? key,
|
||||
required Map<String, dynamic> data,
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
|
|
@ -836,65 +748,6 @@ extension NavigatorStateExtension on _i30.NavigationService {
|
|||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> navigateToFailureView({
|
||||
_i29.Key? key,
|
||||
required String label,
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
}) async {
|
||||
return navigateTo<dynamic>(Routes.failureView,
|
||||
arguments: FailureViewArguments(key: key, label: label),
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> navigateToForgetPasswordView([
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
return navigateTo<dynamic>(Routes.forgetPasswordView,
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> navigateToLearnLessonDetailView([
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
return navigateTo<dynamic>(Routes.learnLessonDetailView,
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> navigateToLearnPracticeView([
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
return navigateTo<dynamic>(Routes.learnPracticeView,
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> replaceWithHomeView([
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
|
|
@ -924,7 +777,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
|
|||
}
|
||||
|
||||
Future<dynamic> replaceWithStartupView({
|
||||
_i29.Key? key,
|
||||
_i25.Key? key,
|
||||
String label = 'Loading',
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
|
|
@ -1193,7 +1046,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
|
|||
}
|
||||
|
||||
Future<dynamic> replaceWithAssessmentView({
|
||||
_i29.Key? key,
|
||||
_i25.Key? key,
|
||||
required Map<String, dynamic> data,
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
|
|
@ -1222,63 +1075,4 @@ extension NavigatorStateExtension on _i30.NavigationService {
|
|||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> replaceWithFailureView({
|
||||
_i29.Key? key,
|
||||
required String label,
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
}) async {
|
||||
return replaceWith<dynamic>(Routes.failureView,
|
||||
arguments: FailureViewArguments(key: key, label: label),
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> replaceWithForgetPasswordView([
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
return replaceWith<dynamic>(Routes.forgetPasswordView,
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> replaceWithLearnLessonDetailView([
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
return replaceWith<dynamic>(Routes.learnLessonDetailView,
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
|
||||
Future<dynamic> replaceWithLearnPracticeView([
|
||||
int? routerId,
|
||||
bool preventDuplicates = true,
|
||||
Map<String, String>? parameters,
|
||||
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
|
||||
transition,
|
||||
]) async {
|
||||
return replaceWith<dynamic>(Routes.learnPracticeView,
|
||||
id: routerId,
|
||||
preventDuplicates: preventDuplicates,
|
||||
parameters: parameters,
|
||||
transition: transition);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
// File generated by FlutterFire CLI.
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||
import 'package:flutter/foundation.dart'
|
||||
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||
|
||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'firebase_options.dart';
|
||||
/// // ...
|
||||
/// await Firebase.initializeApp(
|
||||
/// options: DefaultFirebaseOptions.currentPlatform,
|
||||
/// );
|
||||
/// ```
|
||||
class DefaultFirebaseOptions {
|
||||
static FirebaseOptions get currentPlatform {
|
||||
if (kIsWeb) {
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for web - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
}
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return android;
|
||||
case TargetPlatform.iOS:
|
||||
return ios;
|
||||
case TargetPlatform.macOS:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for macos - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.windows:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for windows - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.linux:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for linux - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions are not supported for this platform.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static const FirebaseOptions android = FirebaseOptions(
|
||||
apiKey: 'AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk',
|
||||
appId: '1:574860813475:android:cd7fa6cf3a0527d97acb16',
|
||||
messagingSenderId: '574860813475',
|
||||
projectId: 'yimaru-lms-e834e',
|
||||
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
|
||||
);
|
||||
|
||||
static const FirebaseOptions ios = FirebaseOptions(
|
||||
apiKey: 'AIzaSyBBcQ17JB6RBTjD7G7mh6Xf_FMUGxP5cC8',
|
||||
appId: '1:574860813475:ios:3ac9f7c4ae1771287acb16',
|
||||
messagingSenderId: '574860813475',
|
||||
projectId: 'yimaru-lms-e834e',
|
||||
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
|
||||
androidClientId:
|
||||
'574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com',
|
||||
iosBundleId: 'com.yimaru.lms.app',
|
||||
);
|
||||
}
|
||||
|
|
@ -1,35 +1,17 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:yimaru_app/models/option.dart';
|
||||
import 'package:yimaru_app/models/question.dart';
|
||||
part 'assessment.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Assessment {
|
||||
final int? id;
|
||||
|
||||
final int? points;
|
||||
|
||||
final String? status;
|
||||
|
||||
@JsonKey(name: 'question_type')
|
||||
final String? questionType;
|
||||
|
||||
@JsonKey(name: 'question_text')
|
||||
final String? questionText;
|
||||
|
||||
@JsonKey(name: 'difficulty_level')
|
||||
final String? difficultyLevel;
|
||||
@JsonKey(name: 'Question')
|
||||
final Question? question;
|
||||
|
||||
@JsonKey(name: 'Options')
|
||||
final List<Option>? options;
|
||||
|
||||
const Assessment({
|
||||
this.id,
|
||||
this.points,
|
||||
this.status,
|
||||
this.options,
|
||||
this.questionText,
|
||||
this.questionType,
|
||||
this.difficultyLevel,
|
||||
});
|
||||
const Assessment({this.options, this.question});
|
||||
|
||||
factory Assessment.fromJson(Map<String, dynamic> json) =>
|
||||
_$AssessmentFromJson(json);
|
||||
|
|
|
|||
|
|
@ -7,24 +7,16 @@ part of 'assessment.dart';
|
|||
// **************************************************************************
|
||||
|
||||
Assessment _$AssessmentFromJson(Map<String, dynamic> json) => Assessment(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
points: (json['points'] as num?)?.toInt(),
|
||||
status: json['status'] as String?,
|
||||
options: (json['options'] as List<dynamic>?)
|
||||
options: (json['Options'] as List<dynamic>?)
|
||||
?.map((e) => Option.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
questionText: json['question_text'] as String?,
|
||||
questionType: json['question_type'] as String?,
|
||||
difficultyLevel: json['difficulty_level'] as String?,
|
||||
question: json['Question'] == null
|
||||
? null
|
||||
: Question.fromJson(json['Question'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AssessmentToJson(Assessment instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'points': instance.points,
|
||||
'status': instance.status,
|
||||
'question_type': instance.questionType,
|
||||
'question_text': instance.questionText,
|
||||
'difficulty_level': instance.difficultyLevel,
|
||||
'options': instance.options,
|
||||
'Question': instance.question,
|
||||
'Options': instance.options,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@ part 'option.g.dart';
|
|||
|
||||
@JsonSerializable()
|
||||
class Option {
|
||||
final int? id;
|
||||
@JsonKey(name: 'question_id')
|
||||
final int? questionId;
|
||||
|
||||
@JsonKey(name: 'option_text')
|
||||
final String? optionText;
|
||||
|
||||
@JsonKey(name: 'is_correct')
|
||||
final bool? isCorrect;
|
||||
|
||||
const Option({this.id, this.optionText, this.isCorrect});
|
||||
const Option({this.optionText, this.questionId});
|
||||
|
||||
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,11 @@ part of 'option.dart';
|
|||
// **************************************************************************
|
||||
|
||||
Option _$OptionFromJson(Map<String, dynamic> json) => Option(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
optionText: json['option_text'] as String?,
|
||||
isCorrect: json['is_correct'] as bool?,
|
||||
questionId: (json['question_id'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'question_id': instance.questionId,
|
||||
'option_text': instance.optionText,
|
||||
'is_correct': instance.isCorrect,
|
||||
};
|
||||
|
|
|
|||
36
lib/models/question.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
part 'question.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Question {
|
||||
final int? id;
|
||||
|
||||
final int? points;
|
||||
|
||||
final String? title;
|
||||
|
||||
final String? description;
|
||||
|
||||
@JsonKey(name: 'is_active')
|
||||
final bool? isActive;
|
||||
|
||||
@JsonKey(name: 'question_type')
|
||||
final String? questionType;
|
||||
|
||||
@JsonKey(name: 'difficulty_level')
|
||||
final String? difficultyLevel;
|
||||
|
||||
const Question(
|
||||
{this.id,
|
||||
this.title,
|
||||
this.points,
|
||||
this.isActive,
|
||||
this.description,
|
||||
this.questionType,
|
||||
this.difficultyLevel});
|
||||
|
||||
factory Question.fromJson(Map<String, dynamic> json) =>
|
||||
_$QuestionFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$QuestionToJson(this);
|
||||
}
|
||||
27
lib/models/question.g.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'question.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Question _$QuestionFromJson(Map<String, dynamic> json) => Question(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
title: json['title'] as String?,
|
||||
points: (json['points'] as num?)?.toInt(),
|
||||
isActive: json['is_active'] as bool?,
|
||||
description: json['description'] as String?,
|
||||
questionType: json['question_type'] as String?,
|
||||
difficultyLevel: json['difficulty_level'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$QuestionToJson(Question instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'points': instance.points,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'is_active': instance.isActive,
|
||||
'question_type': instance.questionType,
|
||||
'difficulty_level': instance.difficultyLevel,
|
||||
};
|
||||
|
|
@ -4,31 +4,10 @@ part 'user_model.g.dart';
|
|||
|
||||
@JsonSerializable()
|
||||
class UserModel {
|
||||
final String? email;
|
||||
|
||||
final String? gender;
|
||||
|
||||
final String? region;
|
||||
|
||||
final String? country;
|
||||
|
||||
|
||||
final String? occupation;
|
||||
|
||||
final bool? userInfoLoaded;
|
||||
|
||||
|
||||
@JsonKey(name: 'user_id')
|
||||
final int? userId;
|
||||
|
||||
@JsonKey(name: 'last_name')
|
||||
final String? lastName;
|
||||
|
||||
@JsonKey(name: 'birth_day')
|
||||
final String? birthday;
|
||||
|
||||
@JsonKey(name: 'first_name')
|
||||
final String? firstName;
|
||||
final bool? profileCompleted;
|
||||
|
||||
@JsonKey(name: 'access_token')
|
||||
final String? accessToken;
|
||||
|
|
@ -36,28 +15,11 @@ class UserModel {
|
|||
@JsonKey(name: 'refresh_token')
|
||||
final String? refreshToken;
|
||||
|
||||
@JsonKey(name: 'profile_completed')
|
||||
final bool? profileCompleted;
|
||||
|
||||
@JsonKey(name: 'profile_picture_url')
|
||||
final String? profilePicture;
|
||||
|
||||
const UserModel({
|
||||
this.email,
|
||||
this.region,
|
||||
this.gender,
|
||||
this.userId,
|
||||
this.country,
|
||||
this.lastName,
|
||||
this.birthday,
|
||||
this.firstName,
|
||||
this.occupation,
|
||||
this.accessToken,
|
||||
this.refreshToken,
|
||||
this.profilePicture,
|
||||
this.userInfoLoaded ,
|
||||
this.profileCompleted,
|
||||
});
|
||||
const UserModel(
|
||||
{this.userId,
|
||||
this.accessToken,
|
||||
this.profileCompleted,
|
||||
this.refreshToken});
|
||||
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserModelFromJson(json);
|
||||
|
|
|
|||
|
|
@ -7,35 +7,15 @@ part of 'user_model.dart';
|
|||
// **************************************************************************
|
||||
|
||||
UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
|
||||
email: json['email'] as String?,
|
||||
region: json['region'] as String?,
|
||||
gender: json['gender'] as String?,
|
||||
userId: (json['user_id'] as num?)?.toInt(),
|
||||
country: json['country'] as String?,
|
||||
lastName: json['last_name'] as String?,
|
||||
birthday: json['birth_day'] as String?,
|
||||
firstName: json['first_name'] as String?,
|
||||
occupation: json['occupation'] as String?,
|
||||
accessToken: json['access_token'] as String?,
|
||||
profileCompleted: json['profileCompleted'] as bool?,
|
||||
refreshToken: json['refresh_token'] as String?,
|
||||
profilePicture: json['profile_picture_url'] as String?,
|
||||
profileCompleted: json['profile_completed'] as bool?,
|
||||
userInfoLoaded: json['userInfoLoaded'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
|
||||
'email': instance.email,
|
||||
'gender': instance.gender,
|
||||
'region': instance.region,
|
||||
'country': instance.country,
|
||||
'occupation': instance.occupation,
|
||||
'userInfoLoaded': instance.userInfoLoaded,
|
||||
'user_id': instance.userId,
|
||||
'last_name': instance.lastName,
|
||||
'birth_day': instance.birthday,
|
||||
'first_name': instance.firstName,
|
||||
'profileCompleted': instance.profileCompleted,
|
||||
'access_token': instance.accessToken,
|
||||
'refresh_token': instance.refreshToken,
|
||||
'profile_completed': instance.profileCompleted,
|
||||
'profile_picture_url': instance.profilePicture,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ class ApiService {
|
|||
final _service = locator<DioService>();
|
||||
|
||||
// Register
|
||||
Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic> data) async {
|
||||
Future<Map<String, dynamic>> register(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kRegisterUrl',
|
||||
'$baseUrl/$userUrl/$kRegisterUrl',
|
||||
data: data,
|
||||
);
|
||||
|
||||
|
|
@ -29,19 +29,19 @@ class ApiService {
|
|||
'message': 'Unknown Error Occurred'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
} catch (e) {
|
||||
return {
|
||||
'message': e.toString(),
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Email Login
|
||||
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
|
||||
// Login
|
||||
Future<Map<String, dynamic>> login(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kLoginUrl',
|
||||
'$baseUrl/$kLoginUrl',
|
||||
data: data,
|
||||
);
|
||||
|
||||
|
|
@ -57,38 +57,10 @@ class ApiService {
|
|||
'message': '${response.data['message']}, ${response.data['error']}'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
} catch (e) {
|
||||
return {
|
||||
'message': e.toString(),
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Google login
|
||||
Future<Map<String, dynamic>> googleAuth(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kGoogleAuthUrl',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'status': ResponseStatus.success,
|
||||
'message': 'Logged in successfully',
|
||||
'data': UserModel.fromJson(response.data['data']),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': '${response.data['message']}, ${response.data['error']}'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -97,14 +69,14 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kVerifyOtpUrl',
|
||||
'$baseUrl/$userUrl/$kVerifyOtpUrl',
|
||||
data: data,
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'status': ResponseStatus.success,
|
||||
'message': 'Otp verified successfully',
|
||||
'data': UserModel.fromJson(response.data['data']),
|
||||
//'data': UserModel.fromJson(response.data['data']),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
|
@ -112,10 +84,10 @@ class ApiService {
|
|||
'message': '${response.data['message']}, ${response.data['error']}'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
} catch (e) {
|
||||
return {
|
||||
'message': e.toString(),
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -124,7 +96,7 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kResendOtpUrl',
|
||||
'$baseUrl/$userUrl/$kResendOtpUrl',
|
||||
data: data,
|
||||
);
|
||||
|
||||
|
|
@ -139,65 +111,10 @@ class ApiService {
|
|||
'message': 'Unknown Error Occurred'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
} catch (e) {
|
||||
return {
|
||||
'message': e.toString(),
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Request reset code
|
||||
Future<Map<String, dynamic>> requestResetCode(
|
||||
Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kRequestResetCode',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'status': ResponseStatus.success,
|
||||
'message': 'Reset code sent successfully',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': '${response.data['message']}, ${response.data['error']}'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Reset password
|
||||
Future<Map<String, dynamic>> resetPassword(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kResetPassword',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'status': ResponseStatus.success,
|
||||
'message': 'Password reset successfully',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': '${response.data['message']}, ${response.data['error']}'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -206,7 +123,7 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
|
||||
try {
|
||||
Response response = await _service.dio.get(
|
||||
'$kBaseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl',
|
||||
'$baseUrl/$userUrl/${user?.userId}/$kProfileStatusUrl',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -221,101 +138,25 @@ class ApiService {
|
|||
'message': '${response.data['message']}, ${response.data['error']}'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
} catch (e) {
|
||||
return {
|
||||
'message': e.toString(),
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get profile
|
||||
Future<Map<String, dynamic>> getProfileData(int? userId) async {
|
||||
try {
|
||||
Response response = await _service.dio.get(
|
||||
'$kBaseUrl/$kUserUrl/$kGetUserUrl/$userId',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'status': ResponseStatus.success,
|
||||
'message': 'Profile fetched successfully',
|
||||
'data': UserModel.fromJson(response.data['data']),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': 'Unknown Error Occurred'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Complete profile
|
||||
Future<Map<String, dynamic>> completeProfile(
|
||||
Map<String, dynamic> data) async {
|
||||
// Update profile
|
||||
Future<Map<String, dynamic>> updateProfile(
|
||||
{required UserModel? user, required Map<String, dynamic> data}) async {
|
||||
try {
|
||||
Response response = await _service.dio.put(
|
||||
'$kBaseUrl/$kUserUrl',
|
||||
'$baseUrl/$userUrl',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'status': ResponseStatus.success,
|
||||
'message': 'Profile updated successfully'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': 'Unknown Error Occurred'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile image
|
||||
Future<Map<String, dynamic>> updateProfileImage(
|
||||
{required int? userId, required Map<String, dynamic> data}) async {
|
||||
try {
|
||||
late FormData formData;
|
||||
if (data['profile_picture_url']
|
||||
.toString()
|
||||
.contains('com.example.yimaru_app/')) {
|
||||
formData = FormData.fromMap({
|
||||
'file': data['profile_picture_url'].toString().isNotEmpty
|
||||
? MultipartFile.fromFileSync(
|
||||
data['profile_picture_url'],
|
||||
filename:
|
||||
data['profile_picture_url'].toString().split('/').last,
|
||||
)
|
||||
: null,
|
||||
});
|
||||
} else {
|
||||
formData = FormData.fromMap({
|
||||
'file': data['profile_picture_url'].toString().isNotEmpty
|
||||
? MultipartFile.fromFileSync(
|
||||
data['profile_picture_url'],
|
||||
filename:
|
||||
data['profile_picture_url'].toString().split('/').last,
|
||||
)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$userId/$kUpdateProfileImage',
|
||||
data: formData,
|
||||
);
|
||||
print(response.statusCode);
|
||||
print(response.data);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
|
|
@ -328,10 +169,11 @@ class ApiService {
|
|||
'message': 'Unknown Error Occurred'
|
||||
};
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
} catch (e) {
|
||||
print('Exception ${e.toString()}');
|
||||
return {
|
||||
'message': e.toString(),
|
||||
'status': ResponseStatus.failure,
|
||||
'message': e.response?.data.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -342,7 +184,7 @@ class ApiService {
|
|||
List<Assessment> assessments = [];
|
||||
|
||||
final Response response =
|
||||
await _service.dio.get('$kBaseUrl/$kAssessmentsUrl');
|
||||
await _service.dio.get('$baseUrl/$kAssessmentsUrl');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var data = response.data;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,10 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/app/app.locator.dart';
|
||||
import 'package:yimaru_app/models/user_model.dart';
|
||||
import 'package:yimaru_app/services/secure_storage_service.dart';
|
||||
|
||||
class AuthenticationService with ListenableServiceMixin {
|
||||
class AuthenticationService {
|
||||
final _secureService = locator<SecureStorageService>();
|
||||
|
||||
AuthenticationService() {
|
||||
listenToReactiveValues([_user]);
|
||||
}
|
||||
|
||||
UserModel? _user;
|
||||
|
||||
UserModel? get user => _user;
|
||||
|
||||
Future<bool> userLoggedIn() async {
|
||||
if (await _secureService.getString('userId') != null) {
|
||||
return true;
|
||||
|
|
@ -37,122 +28,14 @@ class AuthenticationService with ListenableServiceMixin {
|
|||
await _secureService.setString('refreshToken', refresh);
|
||||
}
|
||||
|
||||
|
||||
Future<void> saveUserCredential(Map<String, dynamic> data) async {
|
||||
Future<void> saveUserData(Map<String, dynamic> data) async {
|
||||
await _secureService.setInt('userId', data['userId']);
|
||||
await _secureService.setString('accessToken', data['accessToken']);
|
||||
await _secureService.setString('refreshToken', data['refreshToken']);
|
||||
|
||||
_user = UserModel(
|
||||
userId: await _secureService.getInt('userId'),
|
||||
accessToken: await _secureService.getString('accessToken'),
|
||||
refreshToken: await _secureService.getString('refreshToken'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveProfileStatus(bool value) async {
|
||||
Future<void> saveProfileCompleted(bool value) async {
|
||||
await _secureService.setBool('profileCompleted', value);
|
||||
|
||||
_user = UserModel(
|
||||
email: _user?.email,
|
||||
gender: _user?.gender,
|
||||
region: _user?.region,
|
||||
userId: _user?.userId,
|
||||
country: _user?.country,
|
||||
lastName: _user?.lastName,
|
||||
birthday: _user?.birthday,
|
||||
firstName: _user?.firstName,
|
||||
occupation: _user?.occupation,
|
||||
accessToken: _user?.accessToken,
|
||||
refreshToken: _user?.refreshToken,
|
||||
profilePicture: _user?.profilePicture,
|
||||
userInfoLoaded: _user?.userInfoLoaded ?? false,
|
||||
profileCompleted: await _secureService.getBool('profileCompleted'));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> saveProfileImage(String image) async {
|
||||
await _secureService.setString('profileImage', image);
|
||||
_user = UserModel(
|
||||
email: _user?.email,
|
||||
gender: _user?.gender,
|
||||
region: _user?.region,
|
||||
userId: _user?.userId,
|
||||
country: _user?.country,
|
||||
lastName: _user?.lastName,
|
||||
birthday: _user?.birthday,
|
||||
firstName: _user?.firstName,
|
||||
occupation: _user?.occupation,
|
||||
accessToken: _user?.accessToken,
|
||||
refreshToken: _user?.refreshToken,
|
||||
profileCompleted: _user?.profileCompleted,
|
||||
userInfoLoaded: _user?.userInfoLoaded ?? false,
|
||||
profilePicture: await _secureService.getString('profileImage'),
|
||||
);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> saveUserData(
|
||||
{required String image, required UserModel data}) async {
|
||||
await _secureService.setBool('userInfoLoaded', true);
|
||||
await _secureService.setBool(
|
||||
'profileCompleted', data.profileCompleted ?? false);
|
||||
await _secureService.setString('profilePicture', image);
|
||||
await _secureService.setString('email', data.email ?? '');
|
||||
await _secureService.setString('region', data.region ?? '');
|
||||
await _secureService.setString('gender', data.gender ?? '');
|
||||
await _secureService.setString('country', data.country ?? '');
|
||||
await _secureService.setString('birthday', data.birthday ?? '');
|
||||
await _secureService.setString('lastName', data.lastName ?? '');
|
||||
await _secureService.setString('firstName', data.firstName ?? '');
|
||||
await _secureService.setString('occupation', data.occupation ?? '');
|
||||
|
||||
_user = UserModel(
|
||||
email: data.email,
|
||||
gender: data.gender,
|
||||
region: data.region,
|
||||
userInfoLoaded: true,
|
||||
profilePicture: image,
|
||||
userId: _user?.userId,
|
||||
country: data.country,
|
||||
lastName: data.lastName,
|
||||
birthday: data.birthday,
|
||||
firstName: data.firstName,
|
||||
occupation: data.occupation,
|
||||
accessToken: _user?.accessToken,
|
||||
refreshToken: _user?.refreshToken,
|
||||
profileCompleted: data.profileCompleted,
|
||||
);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateUserData(Map<String, dynamic> data) async {
|
||||
await _secureService.setString('region', data['region']);
|
||||
await _secureService.setString('gender', data['gender']);
|
||||
await _secureService.setString('country', data['country']);
|
||||
await _secureService.setString('lastName', data['last_name']);
|
||||
await _secureService.setString('birthday', data['birth_day']);
|
||||
await _secureService.setString('firstName', data['first_name']);
|
||||
await _secureService.setString('occupation', data['occupation']);
|
||||
|
||||
_user = UserModel(
|
||||
email: _user?.email,
|
||||
userId: _user?.userId,
|
||||
accessToken: _user?.accessToken,
|
||||
refreshToken: _user?.refreshToken,
|
||||
profilePicture: _user?.profilePicture,
|
||||
profileCompleted: _user?.profileCompleted,
|
||||
region: await _secureService.getString('region'),
|
||||
gender: await _secureService.getString('gender'),
|
||||
country: await _secureService.getString('country'),
|
||||
lastName: await _secureService.getString('lastName'),
|
||||
birthday: await _secureService.getString('birthday'),
|
||||
firstName: await _secureService.getString('firstName'),
|
||||
occupation: await _secureService.getString('occupation'),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> isFirstTimeInstall() async =>
|
||||
|
|
@ -162,29 +45,18 @@ class AuthenticationService with ListenableServiceMixin {
|
|||
await _secureService.setBool('firstTimeInstall', value);
|
||||
}
|
||||
|
||||
Future<UserModel?> getUser() async {
|
||||
_user = UserModel(
|
||||
Future<UserModel> getUser() async {
|
||||
UserModel user = UserModel(
|
||||
userId: await _secureService.getInt('userId'),
|
||||
email: await _secureService.getString('email'),
|
||||
region: await _secureService.getString('region'),
|
||||
gender: await _secureService.getString('gender'),
|
||||
country: await _secureService.getString('country'),
|
||||
lastName: await _secureService.getString('lastName'),
|
||||
birthday: await _secureService.getString('birthday'),
|
||||
firstName: await _secureService.getString('firstName'),
|
||||
occupation: await _secureService.getString('occupation'),
|
||||
accessToken: await _secureService.getString('accessToken'),
|
||||
refreshToken: await _secureService.getString('refreshToken'),
|
||||
profilePicture: await _secureService.getString('profileImage'),
|
||||
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
|
||||
profileCompleted: await _secureService.getBool('profileCompleted'),
|
||||
);
|
||||
return _user;
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<void> logOut() async {
|
||||
bool firstTimeInstall = await isFirstTimeInstall();
|
||||
_user = null;
|
||||
await _secureService.clear();
|
||||
await setFirstTimeInstall(firstTimeInstall);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:stacked_services/stacked_services.dart';
|
|||
import 'package:yimaru_app/app/app.router.dart';
|
||||
import 'package:yimaru_app/models/user_model.dart';
|
||||
import 'package:yimaru_app/services/authentication_service.dart';
|
||||
import 'package:yimaru_app/services/secure_storage_service.dart';
|
||||
|
||||
import '../app/app.locator.dart';
|
||||
import '../ui/common/app_constants.dart';
|
||||
|
|
@ -20,7 +21,7 @@ class DioService {
|
|||
|
||||
DioService() {
|
||||
_dio.options
|
||||
..baseUrl = kBaseUrl
|
||||
..baseUrl = baseUrl
|
||||
..connectTimeout = const Duration(seconds: 30)
|
||||
..receiveTimeout = const Duration(seconds: 30);
|
||||
|
||||
|
|
@ -49,11 +50,10 @@ class DioService {
|
|||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
final access = await _authenticationService.getAccessToken();
|
||||
final refresh = await _authenticationService.getRefreshToken();
|
||||
final token = await _authenticationService.getAccessToken();
|
||||
|
||||
if (access != null) {
|
||||
options.headers['Authorization'] = 'Bearer $access';
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
options.headers['Accept'] = 'application/json';
|
||||
|
|
@ -61,7 +61,6 @@ class DioService {
|
|||
|
||||
debugPrint('️️➡️➡️➡️➡️INITIALIZING REQUEST➡️➡️➡️➡️');
|
||||
debugPrint('➡️ ${options.method} ${options.uri}');
|
||||
debugPrint('➡️ REFRESH: $refresh');
|
||||
debugPrint('➡️ HEADERS: ${options.headers}');
|
||||
debugPrint('➡️ DATA: ${options.data}');
|
||||
debugPrint('️️➡️➡️➡️➡️FINALIZING REQUEST➡️➡️➡️➡️');
|
||||
|
|
@ -126,22 +125,33 @@ class DioService {
|
|||
}
|
||||
|
||||
Future<bool> _refreshToken() async {
|
||||
final UserModel? user = await _authenticationService.getUser();
|
||||
final UserModel user = await _authenticationService.getUser();
|
||||
|
||||
if (user?.refreshToken == null) return false;
|
||||
if (user.refreshToken == null) return false;
|
||||
|
||||
try {
|
||||
Map<String, dynamic> data = {
|
||||
'role': 'USER',
|
||||
'user_id': user?.userId,
|
||||
'access_token': user?.accessToken,
|
||||
'refresh_token': user?.refreshToken
|
||||
'role': 'STUDENT',
|
||||
'user_id': user.userId,
|
||||
'access_token': user.accessToken,
|
||||
'refresh_token': user.refreshToken
|
||||
};
|
||||
print(data);
|
||||
final response = await _refreshDio.post(
|
||||
'$kBaseUrl/$kRefreshTokenUrl',
|
||||
'$baseUrl/$kRefreshTokenUrl',
|
||||
data: data,
|
||||
options: Options(
|
||||
followRedirects: false,
|
||||
validateStatus: (status) => true,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
print('Refresh response');
|
||||
print(response.data);
|
||||
await _authenticationService.saveTokens(
|
||||
access: response.data['access_token'],
|
||||
refresh: response.data['refresh_token'],
|
||||
|
|
@ -149,8 +159,10 @@ class DioService {
|
|||
|
||||
return true;
|
||||
} catch (e) {
|
||||
await _authenticationService.logOut();
|
||||
await _navigationService.replaceWithLoginView();
|
||||
print('Refresh response exception');
|
||||
print(e.toString());
|
||||
// await _authenticationService.logOut();
|
||||
// await _navigationService.replaceWithLoginView();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||
|
||||
class GoogleAuthService {
|
||||
final GoogleSignIn signIn = GoogleSignIn.instance;
|
||||
|
||||
Future<GoogleSignInAccount?> googleAuth() async {
|
||||
try {
|
||||
GoogleSignInAccount? googleUser;
|
||||
await signIn.initialize(serverClientId: kServerClientId).then((_) async {
|
||||
googleUser = await signIn.attemptLightweightAuthentication();
|
||||
|
||||
googleUser ??=
|
||||
await signIn.authenticate(scopeHint: ['email', 'profile']);
|
||||
});
|
||||
return googleUser;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../app/app.locator.dart';
|
||||
import '../ui/common/app_constants.dart';
|
||||
import 'dio_service.dart';
|
||||
|
||||
class ImageDownloaderService {
|
||||
final _service = locator<DioService>();
|
||||
|
||||
Future<String> downloader(String? networkImage) async {
|
||||
late File image;
|
||||
|
||||
late String profileImage;
|
||||
|
||||
final Directory appDir = await getApplicationDocumentsDirectory();
|
||||
|
||||
if (networkImage != null) {
|
||||
profileImage = networkImage.contains('https://lh3.googleusercontent.com')
|
||||
? networkImage
|
||||
: '$kBaseUrl$networkImage';
|
||||
}
|
||||
|
||||
final Response profileImageResponse = await _service.dio.get(
|
||||
profileImage,
|
||||
options: Options(
|
||||
followRedirects: false,
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
);
|
||||
final imageName = basename(networkImage ?? '');
|
||||
final localImagePath = join(appDir.path, imageName);
|
||||
image = File(localImagePath);
|
||||
image.writeAsBytes(profileImageResponse.data);
|
||||
|
||||
return image.path;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:yimaru_app/services/permission_handler_service.dart';
|
||||
|
||||
import '../app/app.locator.dart';
|
||||
import '../ui/common/ui_helpers.dart';
|
||||
|
||||
class ImagePickerService {
|
||||
final _permissionHandler = locator<PermissionHandlerService>();
|
||||
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
Future<String?> gallery() async {
|
||||
try {
|
||||
PermissionStatus status =
|
||||
await _permissionHandler.requestPermission(Permission.mediaLibrary);
|
||||
|
||||
if (status == PermissionStatus.granted) {
|
||||
final XFile? pickedFile = await _picker.pickImage(
|
||||
source: ImageSource.gallery, maxWidth: 600, maxHeight: 600);
|
||||
|
||||
if (pickedFile == null) {
|
||||
showErrorToast('Please select a picture');
|
||||
return null;
|
||||
} else {
|
||||
return pickedFile.path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> camera() async {
|
||||
try {
|
||||
PermissionStatus status =
|
||||
await _permissionHandler.requestPermission(Permission.camera);
|
||||
|
||||
if (status == PermissionStatus.granted) {
|
||||
final XFile? pickedFile = await _picker.pickImage(
|
||||
source: ImageSource.camera, maxWidth: 600, maxHeight: 600);
|
||||
|
||||
if (pickedFile == null) {
|
||||
showErrorToast('Please take a picture');
|
||||
return null;
|
||||
} else {
|
||||
return pickedFile.path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../ui/common/ui_helpers.dart';
|
||||
|
||||
class PermissionHandlerService {
|
||||
Future<PermissionStatus> requestPermission(
|
||||
Permission requestedPermission) async {
|
||||
if (requestedPermission == Permission.camera) {
|
||||
return await request(Permission.camera);
|
||||
}
|
||||
if (requestedPermission == Permission.storage) {
|
||||
return await request(Permission.storage);
|
||||
}
|
||||
if (requestedPermission == Permission.mediaLibrary) {
|
||||
return await request(Permission.mediaLibrary);
|
||||
}
|
||||
return PermissionStatus.denied;
|
||||
}
|
||||
|
||||
Future<PermissionStatus> request(Permission permission) async {
|
||||
if (await permission.isDenied) {
|
||||
final PermissionStatus status = await permission.request();
|
||||
|
||||
if (status.isDenied || status.isPermanentlyDenied) {
|
||||
showErrorToast('Permission Denied');
|
||||
}
|
||||
return status;
|
||||
}
|
||||
return PermissionStatus.granted;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ const Color kcRed = Color(0xffFF4C4C);
|
|||
const Color kcGreen = Color(0xFF1DE964);
|
||||
const Color kcBackgroundColor = kcWhite;
|
||||
const Color kcWhite = Color(0xFFFFFFFF);
|
||||
const Color kcViolet = Color(0x336A1B9A);
|
||||
const Color kcIndigo = Color(0xff6A1B9A);
|
||||
const Color kcOrange = Color(0xFFF79400);
|
||||
const Color kcSkyBlue = Color(0xFF28B4CD);
|
||||
|
|
@ -14,6 +13,7 @@ const Color kcMediumGrey = Color(0xFF474A54);
|
|||
const Color kcAquamarine = Color(0xFF1DE9B6);
|
||||
const Color kcTransparent = Colors.transparent;
|
||||
const Color kcPrimaryColor = Color(0xFF9E2891);
|
||||
const Color kcPrimaryAccent = Color(0xFF6A1B9A);
|
||||
const Color kcVeryLightGrey = Color(0xFFE3E3E3);
|
||||
const Color kcPrimaryColorDark = Color(0xFF300151);
|
||||
const Color kcPrimaryColorLight = Color(0x149E2891);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
String kBaseUrl = 'http://195.35.29.82:8080';
|
||||
String baseUrl = 'http://195.35.29.82:8080';
|
||||
//String baseUrl = 'https://api.yimaru.yaltopia.com';
|
||||
|
||||
String kGetUserUrl = 'single';
|
||||
|
||||
String kUserUrl = 'api/v1/user';
|
||||
String userUrl = 'api/v1/user';
|
||||
|
||||
String kRegisterUrl = 'register';
|
||||
|
||||
|
|
@ -11,24 +9,10 @@ String kVerifyOtpUrl = 'verify-otp';
|
|||
|
||||
String kResendOtpUrl = 'resend-otp';
|
||||
|
||||
String kResetPassword = 'resetPassword';
|
||||
|
||||
String kRequestResetCode = 'sendResetCode';
|
||||
|
||||
String kUpdateProfileImage = 'profile-picture';
|
||||
|
||||
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
||||
|
||||
String kLoginUrl = 'api/v1/auth/customer-login';
|
||||
|
||||
String kProfileStatusUrl = 'is-profile-completed';
|
||||
|
||||
String kGoogleAuthUrl = 'api/v1/auth/google/android';
|
||||
|
||||
String kAssessmentsUrl = 'api/v1/assessment/questions';
|
||||
|
||||
String kServerClientId =
|
||||
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
|
||||
|
||||
String kSampleVideoUrl =
|
||||
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
|
||||
|
|
|
|||
|
|
@ -8,19 +8,3 @@ enum ProgressStatuses { pending, started, completed }
|
|||
|
||||
// Levels
|
||||
enum ProficiencyLevels { a1, a2, b1, b2, none }
|
||||
|
||||
// State object
|
||||
enum StateObjects {
|
||||
verifyOtp,
|
||||
resendOtp,
|
||||
profileImage,
|
||||
profileUpdate,
|
||||
resetPassword,
|
||||
loginWithEmail,
|
||||
loginWithGoogle,
|
||||
loadLessonVideo,
|
||||
requestResetCode,
|
||||
registerWithEmail,
|
||||
profileCompletion,
|
||||
registerWithGoogle,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:math';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -172,64 +171,30 @@ PinTheme errorPinTheme = defaultPin.copyBorderWith(
|
|||
border: Border.all(color: Colors.red),
|
||||
);
|
||||
|
||||
TextStyle style18P600 = const TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle style18W600 = const TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcWhite,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle style25W600 = const TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcWhite,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
|
||||
TextStyle style12R700 = const TextStyle(
|
||||
TextStyle validationStyle = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
|
||||
TextStyle style14P400 = const TextStyle(
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
TextStyle style14P600 = const TextStyle(
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle style25P600 = const TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle style25DG600 = const TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle style12R700 = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
|
||||
TextStyle style16DG600 = const TextStyle(
|
||||
fontSize: 16,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle style18DG500 = const TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
|
||||
TextStyle style18DG600 = const TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcDarkGrey,
|
||||
|
|
@ -241,29 +206,17 @@ TextStyle style16DG400 = const TextStyle(
|
|||
color: kcDarkGrey,
|
||||
);
|
||||
|
||||
TextStyle style14LG400 = const TextStyle(
|
||||
color: kcLightGrey,
|
||||
);
|
||||
|
||||
TextStyle style14MG400 = const TextStyle(
|
||||
color: kcMediumGrey,
|
||||
);
|
||||
TextStyle style14DG500 =
|
||||
const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500);
|
||||
|
||||
TextStyle style14DG400 = const TextStyle(
|
||||
color: kcDarkGrey,
|
||||
);
|
||||
|
||||
TextStyle style14DG600 = const TextStyle(
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
TextStyle style14P400 = const TextStyle(
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
TextStyle validationStyle = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
TextStyle style14P600 = const TextStyle(
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
|
||||
|
|
@ -287,36 +240,27 @@ Map<String, Style> htmlStyle = {
|
|||
),
|
||||
};
|
||||
|
||||
ChewieProgressColors buildChewieProgressIndicator = ChewieProgressColors(
|
||||
bufferedColor: kcIndigo,
|
||||
playedColor: kcPrimaryColor,
|
||||
backgroundColor: kcBackgroundColor,
|
||||
);
|
||||
|
||||
Widget buildToastDescription(String message) => Text(
|
||||
message,
|
||||
maxLines: 4,
|
||||
style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500),
|
||||
);
|
||||
|
||||
void showErrorToast(String message) {
|
||||
toastification.show(
|
||||
showIcon: true,
|
||||
dragToClose: true,
|
||||
primaryColor: kcRed,
|
||||
showProgressBar: false,
|
||||
applyBlurEffect: false,
|
||||
alignment: Alignment.topCenter,
|
||||
primaryColor: kcBackgroundColor,
|
||||
icon: const Icon(Icons.check),
|
||||
type: ToastificationType.success,
|
||||
alignment: Alignment.bottomCenter,
|
||||
style: ToastificationStyle.fillColored,
|
||||
description: buildToastDescription(message),
|
||||
autoCloseDuration: const Duration(seconds: 3),
|
||||
borderSide: const BorderSide(color: kcWhite),
|
||||
autoCloseDuration: const Duration(seconds: 5),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||
borderSide: const BorderSide(color: kcPrimaryColor),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: kcPrimaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -326,17 +270,14 @@ void showSuccessToast(String message) {
|
|||
dragToClose: true,
|
||||
showProgressBar: false,
|
||||
applyBlurEffect: false,
|
||||
alignment: Alignment.topCenter,
|
||||
primaryColor: kcBackgroundColor,
|
||||
icon: const Icon(Icons.check),
|
||||
primaryColor: kcPrimaryColor,
|
||||
type: ToastificationType.success,
|
||||
alignment: Alignment.bottomCenter,
|
||||
style: ToastificationStyle.fillColored,
|
||||
description: buildToastDescription(message),
|
||||
autoCloseDuration: const Duration(seconds: 3),
|
||||
borderSide: const BorderSide(color: kcWhite),
|
||||
autoCloseDuration: const Duration(seconds: 5),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||
borderSide: const BorderSide(color: kcPrimaryColor),
|
||||
icon: const Icon(
|
||||
Icons.check,
|
||||
color: kcPrimaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,11 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
|||
|
||||
Widget _buildHeader(String title) => Text(
|
||||
title,
|
||||
style: style18DG600,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
|||
const AssessmentView({Key? key, required this.data}) : super(key: key);
|
||||
|
||||
@override
|
||||
void onViewModelReady(AssessmentViewModel viewModel) async {
|
||||
void onViewModelReady(AssessmentViewModel viewModel) {
|
||||
viewModel.getAssessments();
|
||||
viewModel.initUserData(data);
|
||||
await viewModel.getAssessments();
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
|
|
@ -39,12 +39,10 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
|||
List<Widget> _buildScreens() => [
|
||||
_buildAssessmentIntro(),
|
||||
_buildAssessment(),
|
||||
/*
|
||||
_buildAssessmentFailure(),
|
||||
_buildRetakeAssessment(),
|
||||
_buildResultAnalysis(),
|
||||
_buildAssessmentCompletion(),
|
||||
*/
|
||||
// _buildAssessmentFailure(),
|
||||
// _buildRetakeAssessment(),
|
||||
// _buildResultAnalysis(),
|
||||
// _buildAssessmentCompletion(),
|
||||
_buildAssessmentResult(),
|
||||
_buildStartLesson(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,25 +3,21 @@ import 'dart:math';
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/models/option.dart';
|
||||
import 'package:yimaru_app/services/status_checker_service.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../app/app.router.dart';
|
||||
import '../../../models/assessment.dart';
|
||||
import '../../../models/user_model.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../home/home_view.dart';
|
||||
|
||||
class AssessmentViewModel extends BaseViewModel {
|
||||
final _apiService = locator<ApiService>();
|
||||
final _dialogService = locator<DialogService>();
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
final _navigationService = locator<NavigationService>();
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
// In-app navigation
|
||||
int _currentPage = 0;
|
||||
|
||||
int get currentPage => _currentPage;
|
||||
|
|
@ -35,6 +31,7 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
int get previousPage => _previousPage;
|
||||
|
||||
// Assessment
|
||||
|
||||
int _currentQuestion = 0;
|
||||
|
||||
int get currentQuestion => _currentQuestion;
|
||||
|
|
@ -57,6 +54,7 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
Map<String, dynamic> get userData => _userData;
|
||||
|
||||
// Assessment
|
||||
|
||||
int countCorrectAnswersUntil(int untilQuestion) {
|
||||
int count = 0;
|
||||
|
||||
|
|
@ -75,6 +73,9 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
if (_currentQuestion == 5) {
|
||||
// A1
|
||||
final correctCount = countCorrectAnswersUntil(5);
|
||||
print('All : $_selectedAnswers');
|
||||
print('Question page : $_currentQuestion');
|
||||
print('Correct A1: $correctCount');
|
||||
|
||||
if (correctCount > 3) {
|
||||
return {'continue': true, 'level': ProficiencyLevels.a1};
|
||||
|
|
@ -85,6 +86,9 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
// A2
|
||||
|
||||
final correctCount = countCorrectAnswersUntil(10);
|
||||
print('All : $_selectedAnswers');
|
||||
print('Question page : $_currentQuestion');
|
||||
print('Correct A2: $correctCount');
|
||||
|
||||
if (correctCount > 3) {
|
||||
return {'continue': true, 'level': ProficiencyLevels.a2};
|
||||
|
|
@ -94,6 +98,9 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
} else if (_currentQuestion == 16) {
|
||||
// B1
|
||||
final correctCount = countCorrectAnswersUntil(16);
|
||||
print('All : $_selectedAnswers');
|
||||
print('Question page : $_currentQuestion');
|
||||
print('Correct B1: $correctCount');
|
||||
|
||||
if (correctCount > 4) {
|
||||
return {'continue': true, 'level': ProficiencyLevels.b1};
|
||||
|
|
@ -102,9 +109,12 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
}
|
||||
} else if (_currentQuestion == 22) {
|
||||
final correctCount = countCorrectAnswersUntil(16);
|
||||
print('All : $_selectedAnswers');
|
||||
print('Question page : $_currentQuestion');
|
||||
print('Correct B2: $correctCount');
|
||||
|
||||
if (correctCount > 4) {
|
||||
return {'continue': false, 'level': ProficiencyLevels.b2};
|
||||
return {'continue': true, 'level': ProficiencyLevels.b2};
|
||||
} else {
|
||||
return {'continue': false, 'level': ProficiencyLevels.b2};
|
||||
}
|
||||
|
|
@ -113,20 +123,18 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
void setSelectedAnswer({required int question, required Option? option}) {
|
||||
void setSelectedAnswer({required int question, required String option}) {
|
||||
bool correct = false;
|
||||
if (option?.isCorrect ?? false) {
|
||||
final generator = Random();
|
||||
int random = generator.nextInt(4);
|
||||
if (option == _assessments[question - 1].options?[random].optionText) {
|
||||
correct = true;
|
||||
}
|
||||
|
||||
final data = {
|
||||
question.toString(): {
|
||||
'option': option,
|
||||
'correct': correct,
|
||||
'option': option?.optionText,
|
||||
'answer': _assessments[question - 1]
|
||||
.options
|
||||
?.firstWhere((e) => e.isCorrect ?? false)
|
||||
.optionText
|
||||
'answer': _assessments[question - 1].options?[random].optionText
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -139,6 +147,22 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
return _selectedAnswers[question.toString()]?['option'] == answer;
|
||||
}
|
||||
|
||||
Future<void> getAssessments() async {
|
||||
_assessments = await runBusyFuture<List<Assessment>>(_getAssessments());
|
||||
}
|
||||
|
||||
Future<List<Assessment>> _getAssessments() async {
|
||||
List<Assessment> response = await _apiService.getAssessments();
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
final generator = Random();
|
||||
int random = generator.nextInt(15);
|
||||
response.add(response[random]);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Add user data
|
||||
void initUserData(Map<String, dynamic> data) {
|
||||
clearUserData();
|
||||
|
|
@ -153,48 +177,38 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
_userData.clear();
|
||||
}
|
||||
|
||||
// Dialog
|
||||
Future<bool?> showAbortDialog() async {
|
||||
DialogResponse? response = await _dialogService.showDialog(
|
||||
cancelTitle: 'No',
|
||||
buttonTitle: 'Yes',
|
||||
barrierDismissible: true,
|
||||
title: 'Abort Assessment',
|
||||
cancelTitleColor: kcDarkGrey,
|
||||
buttonTitleColor: kcPrimaryColor,
|
||||
description: 'Are you sure to abort the assessment ?',
|
||||
);
|
||||
return response?.confirmed;
|
||||
// Complete profile
|
||||
Future<void> completeProfile() async {
|
||||
Map<String, dynamic> response =
|
||||
await runBusyFuture<Map<String, dynamic>>(_completeProfile());
|
||||
}
|
||||
|
||||
Future<void> abort() async {
|
||||
bool? response = await showAbortDialog();
|
||||
if (response != null && response) {
|
||||
next(page: 3);
|
||||
}
|
||||
Future<Map<String, dynamic>> _completeProfile() async {
|
||||
print(_userData);
|
||||
UserModel user = await _authenticationService.getUser();
|
||||
Map<String, dynamic> response =
|
||||
await _apiService.updateProfile(data: _userData, user: user);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Question navigation
|
||||
// Navigation
|
||||
|
||||
void nextQuestion() {
|
||||
_currentQuestion++;
|
||||
Map<String, dynamic> response = evaluateAssessment();
|
||||
|
||||
if (_currentQuestion == _assessments.length) {
|
||||
_proficiencyLevel = response['level'];
|
||||
next();
|
||||
if (response['level'] == ProficiencyLevels.none) {
|
||||
_pageController.jumpToPage(_currentQuestion);
|
||||
} else {
|
||||
if (response['level'] == ProficiencyLevels.none) {
|
||||
if (response['continue']) {
|
||||
_pageController.jumpToPage(_currentQuestion);
|
||||
} else {
|
||||
if (response['continue']) {
|
||||
_pageController.jumpToPage(_currentQuestion);
|
||||
} else {
|
||||
_proficiencyLevel = response['level'];
|
||||
next();
|
||||
}
|
||||
}
|
||||
{
|
||||
_proficiencyLevel = response['level'];
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
|
|
@ -204,6 +218,8 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
_pageController.previousPage(
|
||||
duration: const Duration(microseconds: 100), curve: Curves.linear);
|
||||
rebuildUi();
|
||||
} else {
|
||||
_navigationService.back();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,54 +238,15 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
}
|
||||
|
||||
void pop() {
|
||||
if (_currentPage == 0 || _currentPage == 3 /*7*/) {
|
||||
_navigationService.back();
|
||||
} else if (_currentPage != 0 && _currentPage != 3) {
|
||||
if (_currentPage != 0) {
|
||||
_currentPage--;
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
Future<void> navigateToLanguage() async =>
|
||||
await _navigationService.navigateToLanguageView();
|
||||
|
||||
Future<void> replaceWithHome() async =>
|
||||
await _navigationService.clearStackAndShowView(const HomeView());
|
||||
|
||||
// Remote api call
|
||||
Future<void> getAssessments() async => await runBusyFuture(_getAssessments());
|
||||
|
||||
Future<void> _getAssessments() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
List<Assessment> response = await _apiService.getAssessments();
|
||||
/*
|
||||
for (int i = 0; i < 6; i++) {
|
||||
final generator = Random();
|
||||
int random = generator.nextInt(15);
|
||||
response.add(response[random]);
|
||||
}
|
||||
*/
|
||||
_assessments = response;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete profile
|
||||
Future<void> completeProfile() async =>
|
||||
await runBusyFuture(_completeProfile(),
|
||||
busyObject: StateObjects.profileCompletion);
|
||||
|
||||
Future<void> _completeProfile() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> response =
|
||||
await _apiService.completeProfile(_userData);
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
clearUserData();
|
||||
await replaceWithHome();
|
||||
showSuccessToast(response['message']);
|
||||
} else {
|
||||
showErrorToast(response['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,23 +61,27 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
];
|
||||
|
||||
Widget _buildIcon() => SvgPicture.asset(
|
||||
'assets/icons/complete.svg',
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Assessment complete!',
|
||||
style: style25DG600,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'We’re now analyzing your speaking skills',
|
||||
textAlign: TextAlign.center,
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
||||
|
|
@ -90,8 +94,8 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
height: 55,
|
||||
borderRadius: 12,
|
||||
text: 'View My Results',
|
||||
foregroundColor: kcWhite,
|
||||
onTap: () => viewModel.next(),
|
||||
foregroundColor: kcWhite,
|
||||
backgroundColor: kcPrimaryColor,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,21 +64,25 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
];
|
||||
|
||||
Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg');
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'We didn’t get enough from your assessment',
|
||||
style: style25DG600,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'Your assessment wasn’t long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
|
||||
textAlign: TextAlign.center,
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||
|
|
@ -113,9 +117,9 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
height: 55,
|
||||
text: 'Skip',
|
||||
borderRadius: 12,
|
||||
backgroundColor: kcWhite,
|
||||
borderColor: kcPrimaryColor,
|
||||
onTap: () => viewModel.next(),
|
||||
backgroundColor: kcWhite,
|
||||
foregroundColor: kcPrimaryColor,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,21 +12,18 @@ import 'assessment_loading_screen.dart';
|
|||
class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||
const AssessmentFormScreen({super.key});
|
||||
|
||||
//final PageController _pageController = PageController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, AssessmentViewModel viewModel) =>
|
||||
_buildAssessmentScreens(viewModel);
|
||||
|
||||
Widget _buildAssessmentScreens(AssessmentViewModel viewModel) =>
|
||||
viewModel.isBusy || viewModel.assessments.isEmpty
|
||||
? _buildPageLoadingIndicator(viewModel)
|
||||
viewModel.isBusy
|
||||
? _buildPageLoadingIndicator()
|
||||
: _buildAssessmentScreensWrapper(viewModel);
|
||||
|
||||
Widget _buildPageLoadingIndicator(AssessmentViewModel viewModel) =>
|
||||
AssessmentLoadingScreen(
|
||||
isLoading: viewModel.isBusy,
|
||||
isEmpty: viewModel.assessments.isEmpty,
|
||||
onTap: () async => await viewModel.getAssessments(),
|
||||
);
|
||||
Widget _buildPageLoadingIndicator() => const AssessmentLoadingScreen();
|
||||
|
||||
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>
|
||||
PopScope(
|
||||
|
|
@ -48,10 +45,9 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
onClose: viewModel.abort,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: false,
|
||||
onPop: viewModel.previousQuestion,
|
||||
showBackButton: viewModel.currentQuestion == 0 ? false : true,
|
||||
);
|
||||
|
||||
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
||||
|
|
@ -63,11 +59,10 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildAssessment(AssessmentViewModel viewModel) => PageView.builder(
|
||||
controller: viewModel.pageController,
|
||||
itemCount: viewModel.assessments.length,
|
||||
itemBuilder: (cotext, index) =>
|
||||
_buildBody(index: index, viewModel: viewModel),
|
||||
);
|
||||
controller: viewModel.pageController,
|
||||
itemCount: viewModel.assessments.length,
|
||||
itemBuilder: (cotext, index) =>
|
||||
_buildBody(index: index, viewModel: viewModel));
|
||||
|
||||
Widget _buildBody(
|
||||
{required int index, required AssessmentViewModel viewModel}) =>
|
||||
|
|
@ -110,7 +105,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
Widget _buildTitle(
|
||||
{required int index, required AssessmentViewModel viewModel}) =>
|
||||
Text(
|
||||
'Q${index + 1}. ${viewModel.assessments[index].questionText} ',
|
||||
'Q${index + 1}. ${viewModel.assessments[index].question?.title} ',
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
|
|
@ -128,7 +123,8 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
''),
|
||||
onTap: () => viewModel.setSelectedAnswer(
|
||||
question: index + 1,
|
||||
option: viewModel.assessments[index].options?[inner]),
|
||||
option: viewModel.assessments[index].options?[inner].optionText ??
|
||||
''),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -163,7 +159,11 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
? kcPrimaryColor
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
onTap: viewModel.selectedAnswers.containsKey(question.toString())
|
||||
? () => viewModel.nextQuestion()
|
||||
?
|
||||
// viewModel.currentQuestion == viewModel.assessments.length - 1
|
||||
// ? () => viewModel.next()
|
||||
// :
|
||||
() => viewModel.nextQuestion()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,24 +54,27 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
];
|
||||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: false,
|
||||
showLanguageSelection: true,
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Want a quick assessment to know your English level?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'Answer a few quick questions to help us understand your English proficiency.',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||
|
|
@ -91,8 +94,8 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
safe: false,
|
||||
text: 'Continue',
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
onTap: () => viewModel.next(),
|
||||
foregroundColor: kcWhite,
|
||||
backgroundColor: kcPrimaryColor,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,9 @@ import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
|
|||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../widgets/large_app_bar.dart';
|
||||
import '../../../widgets/refresh_button.dart';
|
||||
|
||||
class AssessmentLoadingScreen extends StatelessWidget {
|
||||
final bool isEmpty;
|
||||
final bool isLoading;
|
||||
final GestureTapCallback? onTap;
|
||||
const AssessmentLoadingScreen(
|
||||
{super.key, this.onTap, required this.isEmpty, required this.isLoading});
|
||||
const AssessmentLoadingScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildScaffoldWrapper();
|
||||
|
|
@ -21,11 +16,7 @@ class AssessmentLoadingScreen extends StatelessWidget {
|
|||
);
|
||||
|
||||
Widget _buildScaffold() => Stack(
|
||||
children: [
|
||||
_buildColumn(),
|
||||
if (isEmpty) _buildRefreshButton(),
|
||||
if (isLoading) _buildPageIndicator()
|
||||
],
|
||||
children: [_buildColumn(), _buildPageIndicator()],
|
||||
);
|
||||
|
||||
Widget _buildColumn() => Column(
|
||||
|
|
@ -43,6 +34,4 @@ class AssessmentLoadingScreen extends StatelessWidget {
|
|||
Widget _buildBody() => Expanded(child: Container());
|
||||
|
||||
Widget _buildPageIndicator() => const PageLoadingIndicator();
|
||||
|
||||
Widget _buildRefreshButton() => RefreshButton(onTap: onTap);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
||||
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||
|
|
@ -63,37 +62,35 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
verticalSpaceLarge,
|
||||
_buildTitle(viewModel),
|
||||
verticalSpaceSmall,
|
||||
_buildPrimarySubtitle(),
|
||||
_buildPrimarySubTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildIconWrapper(viewModel),
|
||||
_buildIcon(),
|
||||
verticalSpaceMedium,
|
||||
_buildSecondarySubtitle()
|
||||
_buildSecondarySubTitle()
|
||||
];
|
||||
|
||||
Widget _buildTitle(AssessmentViewModel viewModel) => Text(
|
||||
'You’re likely a ${viewModel.proficiencyLevel.name.toUpperCase()} speaker!',
|
||||
style: style25DG600,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildPrimarySubtitle() => Text(
|
||||
Widget _buildPrimarySubTitle() => const Text(
|
||||
'Great Job! Here’s your next step to keep improving.',
|
||||
textAlign: TextAlign.center,
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildIconWrapper(AssessmentViewModel viewModel) =>
|
||||
viewModel.proficiencyLevel != ProficiencyLevels.none
|
||||
? _buildIcon(viewModel)
|
||||
: Container();
|
||||
Widget _buildIcon() => SvgPicture.asset('assets/icons/b1.svg');
|
||||
|
||||
Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset(
|
||||
'assets/icons/${viewModel.proficiencyLevel.name.substring(0, 1)}_${viewModel.proficiencyLevel.name.substring(1)}.svg');
|
||||
|
||||
Widget _buildSecondarySubtitle() => Text(
|
||||
Widget _buildSecondarySubTitle() => const Text(
|
||||
'Let\'s start your practice',
|
||||
style: style14DG400,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||
|
|
@ -113,8 +110,8 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
safe: false,
|
||||
text: 'Continue',
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
onTap: () => viewModel.next(),
|
||||
foregroundColor: kcWhite,
|
||||
backgroundColor: kcPrimaryColor,
|
||||
);
|
||||
|
||||
|
|
@ -127,10 +124,10 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
backgroundColor: kcWhite,
|
||||
text: 'Practice Speaking',
|
||||
borderColor: kcPrimaryColor,
|
||||
onTap: () => viewModel.next(),
|
||||
backgroundColor: kcWhite,
|
||||
foregroundColor: kcPrimaryColor,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
];
|
||||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
|
|
@ -61,15 +61,19 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
'assets/icons/progress_indicator.svg',
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Analyzing your results…',
|
||||
style: style25DG600,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'We’re now analyzing your speaking skills',
|
||||
style: style14MG400,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
];
|
||||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
|
|
@ -72,16 +72,20 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'We didn’t get enough from your assessment',
|
||||
style: style25DG600,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'Your assessment 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,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
|||
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
||||
|
||||
import '../../../common/enmus.dart';
|
||||
import '../../../widgets/page_loading_indicator.dart';
|
||||
import '../assessment_viewmodel.dart';
|
||||
|
||||
class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||
|
|
@ -16,6 +15,7 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
Future<void> _start(AssessmentViewModel viewModel) async {
|
||||
if (viewModel.proficiencyLevel != ProficiencyLevels.none) {
|
||||
Map<String, dynamic> data = {
|
||||
'preferred_language': 'en',
|
||||
'knowledge_level': viewModel.proficiencyLevel.name.toUpperCase()
|
||||
};
|
||||
|
||||
|
|
@ -31,24 +31,20 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
|
||||
Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldStack(viewModel),
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldStack(AssessmentViewModel viewModel) =>
|
||||
Stack(children: [_buildScaffold(viewModel), _buildState(viewModel)]);
|
||||
|
||||
Widget _buildScaffold(AssessmentViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildScaffoldChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||
[_buildAppBar(), _buildExpandedBody(viewModel)];
|
||||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.pop,
|
||||
showLanguageSelection: true,
|
||||
Widget _buildAppBar() => const LargeAppBar(
|
||||
showBackButton: false,
|
||||
showLanguageSelection: false,
|
||||
);
|
||||
|
||||
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
||||
|
|
@ -80,27 +76,34 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(viewModel),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
];
|
||||
|
||||
Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg');
|
||||
|
||||
Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich(
|
||||
TextSpan(
|
||||
text: 'Welcome aboard',
|
||||
style: style25DG600,
|
||||
children: [
|
||||
TextSpan(
|
||||
style: style25DG600,
|
||||
text: ', ${viewModel.userData['first_name']}!',
|
||||
text: 'Welcome aboard',
|
||||
style: const TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ', ${viewModel.userData['first_name']}!',
|
||||
style: const TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
)
|
||||
]),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'You’re ready to explore your personalized lessons.',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
||||
|
|
@ -111,15 +114,10 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
Widget _buildContinueButton(AssessmentViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
text: 'Finish',
|
||||
borderRadius: 12,
|
||||
text: 'Go to My Lessons',
|
||||
foregroundColor: kcWhite,
|
||||
backgroundColor: kcPrimaryColor,
|
||||
onTap: () async => await _start(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildState(AssessmentViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.profileCompletion)
|
||||
? const PageLoadingIndicator()
|
||||
: Container();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,10 +92,14 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
|
|||
Widget _buildIcon() =>
|
||||
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Call our support team between 9 AM - 6 PM',
|
||||
style: style25DG600,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubTitle(String title) => Text(
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildEmptyTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildEmptySubtitle(),
|
||||
_buildEmptySubTitle(),
|
||||
];
|
||||
|
||||
Widget _buildEmptyIcon() => const Icon(
|
||||
|
|
@ -197,7 +197,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
|
|||
),
|
||||
);
|
||||
|
||||
Widget _buildEmptySubtitle() => const Text(
|
||||
Widget _buildEmptySubTitle() => const Text(
|
||||
'Start by exploring your learning materials and save them for offline access.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||
import 'failure_viewmodel.dart';
|
||||
|
||||
class FailureView extends StackedView<FailureViewModel> {
|
||||
final String label;
|
||||
const FailureView({Key? key, required this.label}) : super(key: key);
|
||||
|
||||
@override
|
||||
FailureViewModel viewModelBuilder(BuildContext context) => FailureViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
FailureViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper();
|
||||
|
||||
Widget _buildScaffoldWrapper() => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(),
|
||||
);
|
||||
|
||||
Widget _buildScaffold() => Stack(
|
||||
children: _buildScaffoldChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren() => [
|
||||
_buildBackground(),
|
||||
_buildColumn(),
|
||||
];
|
||||
|
||||
Widget _buildBackground() => Image.asset(
|
||||
'assets/images/onboarding_1.png',
|
||||
fit: BoxFit.fill,
|
||||
width: double.maxFinite,
|
||||
height: double.maxFinite,
|
||||
);
|
||||
|
||||
Widget _buildColumn() => Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _buildUpperColumnChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildUpperColumnChildren() =>
|
||||
[_buildIconWrapper(), _buildSafeWrapper()];
|
||||
|
||||
Widget _buildSafeWrapper() => SafeArea(child: _buildLoadingTextContainer());
|
||||
|
||||
Widget _buildLoadingTextContainer() => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50),
|
||||
child: _buildLoadingTextWrapper(),
|
||||
);
|
||||
|
||||
Widget _buildLoadingTextWrapper() => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _buildLoadingTextChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildLoadingTextChildren() => [
|
||||
_buildLoadingText(),
|
||||
horizontalSpaceSmall,
|
||||
_buildIndicatorWrapper(),
|
||||
];
|
||||
|
||||
Widget _buildLoadingText() =>
|
||||
Text('$label ...', style: const TextStyle(color: kcWhite, fontSize: 16));
|
||||
|
||||
Widget _buildIndicatorWrapper() => SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: _buildIndicator(),
|
||||
);
|
||||
|
||||
Widget _buildIndicator() =>
|
||||
const CustomCircularProgressIndicator(color: kcWhite);
|
||||
|
||||
Widget _buildIconWrapper() => Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
child: _buildIcon(),
|
||||
);
|
||||
|
||||
Widget _buildIcon() => SvgPicture.asset(
|
||||
'assets/icons/logo.svg',
|
||||
height: 50,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
|
||||
class FailureViewModel extends BaseViewModel {}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked/stacked_annotations.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.form.dart';
|
||||
import 'package:yimaru_app/ui/views/forget_password/screens/request_reset_code_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/forget_password/screens/reset_password_screen.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/validators/form_validator.dart';
|
||||
import '../../widgets/large_app_bar.dart';
|
||||
import '../../widgets/page_loading_indicator.dart';
|
||||
import 'forget_password_viewmodel.dart';
|
||||
|
||||
@FormView(fields: [
|
||||
FormTextField(name: 'email', validator: FormValidator.validateEmail),
|
||||
FormTextField(name: 'resetCode', validator: FormValidator.validateForm),
|
||||
FormTextField(name: 'password', validator: FormValidator.validateForm),
|
||||
FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm)
|
||||
])
|
||||
class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
|
||||
with $ForgetPasswordView {
|
||||
const ForgetPasswordView({Key? key}) : super(key: key);
|
||||
|
||||
void _initClearData() {
|
||||
emailController.clear();
|
||||
passwordController.clear();
|
||||
resetCodeController.clear();
|
||||
confirmPasswordController.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
|
||||
if (viewModel.currentPage == 0) {
|
||||
emailController.clear();
|
||||
viewModel.resetRequestResetCodeScreen();
|
||||
} else {
|
||||
passwordController.clear();
|
||||
resetCodeController.clear();
|
||||
confirmPasswordController.clear();
|
||||
viewModel.resetResetPasswordScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void _pop({required bool value, required ForgetPasswordViewModel viewModel}) {
|
||||
{
|
||||
if (!value) return;
|
||||
_clearDataOnNavigation(viewModel);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onViewModelReady(ForgetPasswordViewModel viewModel) {
|
||||
_initClearData();
|
||||
syncFormWithViewModel(viewModel);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
ForgetPasswordViewModel viewModelBuilder(BuildContext context) =>
|
||||
ForgetPasswordViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
ForgetPasswordViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildLoginScreensWrapper(viewModel);
|
||||
|
||||
Widget _buildLoginScreensWrapper(ForgetPasswordViewModel viewModel) =>
|
||||
PopScope(
|
||||
canPop: true,
|
||||
onPopInvokedWithResult: (value, data) =>
|
||||
_pop(value: value, viewModel: viewModel),
|
||||
child: _buildBody(viewModel));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Widget _buildBody(ForgetPasswordViewModel viewModel) =>
|
||||
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
|
||||
|
||||
List<Widget> _buildScreens() => [
|
||||
_buildRequestCodeScreen(),
|
||||
_buildResetPasswordScreen(),
|
||||
];
|
||||
|
||||
Widget _buildRequestCodeScreen() =>
|
||||
RequestCodeScreen(emailController: emailController);
|
||||
|
||||
Widget _buildResetPasswordScreen() => ResetPasswordScreen(
|
||||
passwordController: passwordController,
|
||||
resetCodeController: resetCodeController,
|
||||
confirmPasswordController: confirmPasswordController);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
// **************************************************************************
|
||||
// StackedFormGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/validators/form_validator.dart';
|
||||
|
||||
const bool _autoTextFieldValidation = true;
|
||||
|
||||
const String EmailValueKey = 'email';
|
||||
const String ResetCodeValueKey = 'resetCode';
|
||||
const String PasswordValueKey = 'password';
|
||||
const String ConfirmPasswordValueKey = 'confirmPassword';
|
||||
|
||||
final Map<String, TextEditingController>
|
||||
_ForgetPasswordViewTextEditingControllers = {};
|
||||
|
||||
final Map<String, FocusNode> _ForgetPasswordViewFocusNodes = {};
|
||||
|
||||
final Map<String, String? Function(String?)?>
|
||||
_ForgetPasswordViewTextValidations = {
|
||||
EmailValueKey: FormValidator.validateEmail,
|
||||
ResetCodeValueKey: FormValidator.validateForm,
|
||||
PasswordValueKey: FormValidator.validateForm,
|
||||
ConfirmPasswordValueKey: FormValidator.validateForm,
|
||||
};
|
||||
|
||||
mixin $ForgetPasswordView {
|
||||
TextEditingController get emailController =>
|
||||
_getFormTextEditingController(EmailValueKey);
|
||||
TextEditingController get resetCodeController =>
|
||||
_getFormTextEditingController(ResetCodeValueKey);
|
||||
TextEditingController get passwordController =>
|
||||
_getFormTextEditingController(PasswordValueKey);
|
||||
TextEditingController get confirmPasswordController =>
|
||||
_getFormTextEditingController(ConfirmPasswordValueKey);
|
||||
|
||||
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
|
||||
FocusNode get resetCodeFocusNode => _getFormFocusNode(ResetCodeValueKey);
|
||||
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
|
||||
FocusNode get confirmPasswordFocusNode =>
|
||||
_getFormFocusNode(ConfirmPasswordValueKey);
|
||||
|
||||
TextEditingController _getFormTextEditingController(
|
||||
String key, {
|
||||
String? initialValue,
|
||||
}) {
|
||||
if (_ForgetPasswordViewTextEditingControllers.containsKey(key)) {
|
||||
return _ForgetPasswordViewTextEditingControllers[key]!;
|
||||
}
|
||||
|
||||
_ForgetPasswordViewTextEditingControllers[key] =
|
||||
TextEditingController(text: initialValue);
|
||||
return _ForgetPasswordViewTextEditingControllers[key]!;
|
||||
}
|
||||
|
||||
FocusNode _getFormFocusNode(String key) {
|
||||
if (_ForgetPasswordViewFocusNodes.containsKey(key)) {
|
||||
return _ForgetPasswordViewFocusNodes[key]!;
|
||||
}
|
||||
_ForgetPasswordViewFocusNodes[key] = FocusNode();
|
||||
return _ForgetPasswordViewFocusNodes[key]!;
|
||||
}
|
||||
|
||||
/// Registers a listener on every generated controller that calls [model.setData()]
|
||||
/// with the latest textController values
|
||||
void syncFormWithViewModel(FormStateHelper model) {
|
||||
emailController.addListener(() => _updateFormData(model));
|
||||
resetCodeController.addListener(() => _updateFormData(model));
|
||||
passwordController.addListener(() => _updateFormData(model));
|
||||
confirmPasswordController.addListener(() => _updateFormData(model));
|
||||
|
||||
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
||||
}
|
||||
|
||||
/// Registers a listener on every generated controller that calls [model.setData()]
|
||||
/// with the latest textController values
|
||||
@Deprecated(
|
||||
'Use syncFormWithViewModel instead.'
|
||||
'This feature was deprecated after 3.1.0.',
|
||||
)
|
||||
void listenToFormUpdated(FormViewModel model) {
|
||||
emailController.addListener(() => _updateFormData(model));
|
||||
resetCodeController.addListener(() => _updateFormData(model));
|
||||
passwordController.addListener(() => _updateFormData(model));
|
||||
confirmPasswordController.addListener(() => _updateFormData(model));
|
||||
|
||||
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
||||
}
|
||||
|
||||
/// Updates the formData on the FormViewModel
|
||||
void _updateFormData(FormStateHelper model, {bool forceValidate = false}) {
|
||||
model.setData(
|
||||
model.formValueMap
|
||||
..addAll({
|
||||
EmailValueKey: emailController.text,
|
||||
ResetCodeValueKey: resetCodeController.text,
|
||||
PasswordValueKey: passwordController.text,
|
||||
ConfirmPasswordValueKey: confirmPasswordController.text,
|
||||
}),
|
||||
);
|
||||
|
||||
if (_autoTextFieldValidation || forceValidate) {
|
||||
updateValidationData(model);
|
||||
}
|
||||
}
|
||||
|
||||
bool validateFormFields(FormViewModel model) {
|
||||
_updateFormData(model, forceValidate: true);
|
||||
return model.isFormValid;
|
||||
}
|
||||
|
||||
/// Calls dispose on all the generated controllers and focus nodes
|
||||
void disposeForm() {
|
||||
// The dispose function for a TextEditingController sets all listeners to null
|
||||
|
||||
for (var controller in _ForgetPasswordViewTextEditingControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (var focusNode in _ForgetPasswordViewFocusNodes.values) {
|
||||
focusNode.dispose();
|
||||
}
|
||||
|
||||
_ForgetPasswordViewTextEditingControllers.clear();
|
||||
_ForgetPasswordViewFocusNodes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
extension ValueProperties on FormStateHelper {
|
||||
bool get hasAnyValidationMessage => this
|
||||
.fieldsValidationMessages
|
||||
.values
|
||||
.any((validation) => validation != null);
|
||||
|
||||
bool get isFormValid {
|
||||
if (!_autoTextFieldValidation) this.validateForm();
|
||||
|
||||
return !hasAnyValidationMessage;
|
||||
}
|
||||
|
||||
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
|
||||
String? get resetCodeValue => this.formValueMap[ResetCodeValueKey] as String?;
|
||||
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
|
||||
String? get confirmPasswordValue =>
|
||||
this.formValueMap[ConfirmPasswordValueKey] as String?;
|
||||
|
||||
set emailValue(String? value) {
|
||||
this.setData(
|
||||
this.formValueMap..addAll({EmailValueKey: value}),
|
||||
);
|
||||
|
||||
if (_ForgetPasswordViewTextEditingControllers.containsKey(EmailValueKey)) {
|
||||
_ForgetPasswordViewTextEditingControllers[EmailValueKey]?.text =
|
||||
value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
set resetCodeValue(String? value) {
|
||||
this.setData(
|
||||
this.formValueMap..addAll({ResetCodeValueKey: value}),
|
||||
);
|
||||
|
||||
if (_ForgetPasswordViewTextEditingControllers.containsKey(
|
||||
ResetCodeValueKey)) {
|
||||
_ForgetPasswordViewTextEditingControllers[ResetCodeValueKey]?.text =
|
||||
value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
set passwordValue(String? value) {
|
||||
this.setData(
|
||||
this.formValueMap..addAll({PasswordValueKey: value}),
|
||||
);
|
||||
|
||||
if (_ForgetPasswordViewTextEditingControllers.containsKey(
|
||||
PasswordValueKey)) {
|
||||
_ForgetPasswordViewTextEditingControllers[PasswordValueKey]?.text =
|
||||
value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
set confirmPasswordValue(String? value) {
|
||||
this.setData(
|
||||
this.formValueMap..addAll({ConfirmPasswordValueKey: value}),
|
||||
);
|
||||
|
||||
if (_ForgetPasswordViewTextEditingControllers.containsKey(
|
||||
ConfirmPasswordValueKey)) {
|
||||
_ForgetPasswordViewTextEditingControllers[ConfirmPasswordValueKey]?.text =
|
||||
value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
bool get hasEmail =>
|
||||
this.formValueMap.containsKey(EmailValueKey) &&
|
||||
(emailValue?.isNotEmpty ?? false);
|
||||
bool get hasResetCode =>
|
||||
this.formValueMap.containsKey(ResetCodeValueKey) &&
|
||||
(resetCodeValue?.isNotEmpty ?? false);
|
||||
bool get hasPassword =>
|
||||
this.formValueMap.containsKey(PasswordValueKey) &&
|
||||
(passwordValue?.isNotEmpty ?? false);
|
||||
bool get hasConfirmPassword =>
|
||||
this.formValueMap.containsKey(ConfirmPasswordValueKey) &&
|
||||
(confirmPasswordValue?.isNotEmpty ?? false);
|
||||
|
||||
bool get hasEmailValidationMessage =>
|
||||
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
|
||||
bool get hasResetCodeValidationMessage =>
|
||||
this.fieldsValidationMessages[ResetCodeValueKey]?.isNotEmpty ?? false;
|
||||
bool get hasPasswordValidationMessage =>
|
||||
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
|
||||
bool get hasConfirmPasswordValidationMessage =>
|
||||
this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ??
|
||||
false;
|
||||
|
||||
String? get emailValidationMessage =>
|
||||
this.fieldsValidationMessages[EmailValueKey];
|
||||
String? get resetCodeValidationMessage =>
|
||||
this.fieldsValidationMessages[ResetCodeValueKey];
|
||||
String? get passwordValidationMessage =>
|
||||
this.fieldsValidationMessages[PasswordValueKey];
|
||||
String? get confirmPasswordValidationMessage =>
|
||||
this.fieldsValidationMessages[ConfirmPasswordValueKey];
|
||||
}
|
||||
|
||||
extension Methods on FormStateHelper {
|
||||
setEmailValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
|
||||
setResetCodeValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[ResetCodeValueKey] = validationMessage;
|
||||
setPasswordValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
|
||||
setConfirmPasswordValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[ConfirmPasswordValueKey] =
|
||||
validationMessage;
|
||||
|
||||
/// Clears text input fields on the Form
|
||||
void clearForm() {
|
||||
emailValue = '';
|
||||
resetCodeValue = '';
|
||||
passwordValue = '';
|
||||
confirmPasswordValue = '';
|
||||
}
|
||||
|
||||
/// Validates text input fields on the Form
|
||||
void validateForm() {
|
||||
this.setValidationMessages({
|
||||
EmailValueKey: getValidationMessage(EmailValueKey),
|
||||
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
|
||||
PasswordValueKey: getValidationMessage(PasswordValueKey),
|
||||
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the validation message for the given key
|
||||
String? getValidationMessage(String key) {
|
||||
final validatorForKey = _ForgetPasswordViewTextValidations[key];
|
||||
if (validatorForKey == null) return null;
|
||||
|
||||
String? validationMessageForKey = validatorForKey(
|
||||
_ForgetPasswordViewTextEditingControllers[key]!.text,
|
||||
);
|
||||
|
||||
return validationMessageForKey;
|
||||
}
|
||||
|
||||
/// Updates the fieldsValidationMessages on the FormViewModel
|
||||
void updateValidationData(FormStateHelper model) =>
|
||||
model.setValidationMessages({
|
||||
EmailValueKey: getValidationMessage(EmailValueKey),
|
||||
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
|
||||
PasswordValueKey: getValidationMessage(PasswordValueKey),
|
||||
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
|
||||
});
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/ui/views/login/login_view.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
|
||||
class ForgetPasswordViewModel extends FormViewModel {
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// User data
|
||||
final Map<String, dynamic> _userData = {};
|
||||
|
||||
Map<String, dynamic> get userData => _userData;
|
||||
|
||||
// Navigation
|
||||
int _currentPage = 0;
|
||||
|
||||
int get currentPage => _currentPage;
|
||||
|
||||
// Email
|
||||
bool _focusEmail = false;
|
||||
|
||||
bool get focusEmail => _focusEmail;
|
||||
|
||||
// Reset code
|
||||
bool _focusResetCode = false;
|
||||
|
||||
bool get focusResetCode => _focusResetCode;
|
||||
|
||||
// Password
|
||||
bool _length = false;
|
||||
|
||||
bool get length => _length;
|
||||
|
||||
bool _number = false;
|
||||
|
||||
bool get number => _number;
|
||||
|
||||
bool _specialChar = false;
|
||||
|
||||
bool get specialChar => _specialChar;
|
||||
|
||||
bool _focusPassword = false;
|
||||
|
||||
bool get focusPassword => _focusPassword;
|
||||
|
||||
bool _obscurePassword = true;
|
||||
|
||||
bool get obscurePassword => _obscurePassword;
|
||||
|
||||
bool _passwordMatch = false;
|
||||
|
||||
bool get passwordMatch => _passwordMatch;
|
||||
|
||||
// Confirm password
|
||||
bool _focusConfirmPassword = false;
|
||||
|
||||
bool get focusConfirmPassword => _focusConfirmPassword;
|
||||
|
||||
bool _obscureConfirmPassword = true;
|
||||
|
||||
bool get obscureConfirmPassword => _obscureConfirmPassword;
|
||||
|
||||
// Add user data
|
||||
void addUserData(Map<String, dynamic> data) {
|
||||
_userData.addAll(data);
|
||||
}
|
||||
|
||||
void clearUserData() {
|
||||
_userData.clear();
|
||||
}
|
||||
|
||||
// Email
|
||||
void setEmailFocus() {
|
||||
_focusEmail = true;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset code
|
||||
void setResetCodeFocus() {
|
||||
_focusResetCode = true;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Password
|
||||
void setPasswordFocus() {
|
||||
_focusPassword = true;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
void validatePassword(
|
||||
{required String password, required String confirmPassword}) {
|
||||
if (password.length > 8) {
|
||||
_length = true;
|
||||
} else {
|
||||
_length = false;
|
||||
}
|
||||
|
||||
if (RegExp(r'\d').hasMatch(password)) {
|
||||
_number = true;
|
||||
} else {
|
||||
_number = false;
|
||||
}
|
||||
|
||||
if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
|
||||
_specialChar = true;
|
||||
} else {
|
||||
_specialChar = false;
|
||||
}
|
||||
|
||||
if (password == confirmPassword) {
|
||||
_passwordMatch = true;
|
||||
} else {
|
||||
_passwordMatch = false;
|
||||
}
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
double validationProgress() {
|
||||
int completed = 0;
|
||||
|
||||
if (_length) completed++;
|
||||
if (_number) completed++;
|
||||
if (_specialChar) completed++;
|
||||
if (_passwordMatch) completed++;
|
||||
|
||||
return completed / 4; // returns 0.0 → 1.0
|
||||
}
|
||||
|
||||
void setObscurePassword() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Confirm password
|
||||
void setConfirmPasswordFocus() {
|
||||
_focusConfirmPassword = true;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
void setObscureConfirmPassword() {
|
||||
_obscureConfirmPassword = !_obscureConfirmPassword;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Form reset
|
||||
|
||||
// Reset reset password screen
|
||||
void resetResetPasswordScreen() {
|
||||
_length = false;
|
||||
_number = false;
|
||||
_specialChar = false;
|
||||
_passwordMatch = false;
|
||||
_focusPassword = false;
|
||||
_focusResetCode = false;
|
||||
_focusConfirmPassword = false;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset reset password screen
|
||||
void resetRequestResetCodeScreen() {
|
||||
_focusEmail = false;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// In-app navigation
|
||||
void goTo(int page) {
|
||||
_currentPage = page;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
void goBack() {
|
||||
if (_currentPage == 1) {
|
||||
_currentPage = 0;
|
||||
rebuildUi();
|
||||
} else {
|
||||
_navigationService.back();
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> replaceWithLogin() async =>
|
||||
await _navigationService.clearStackAndShowView(const LoginView());
|
||||
|
||||
// Remote api calls
|
||||
|
||||
// Request reset code
|
||||
Future<void> requestResetCode() async =>
|
||||
await runBusyFuture(_requestResetCode(),
|
||||
busyObject: StateObjects.requestResetCode);
|
||||
|
||||
Future<void> _requestResetCode() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> response =
|
||||
await _apiService.requestResetCode(_userData);
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
goTo(1);
|
||||
showSuccessToast(response['message']);
|
||||
} else {
|
||||
showErrorToast(response['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request reset code
|
||||
Future<void> resetPassword() async => await runBusyFuture(_resetPassword(),
|
||||
busyObject: StateObjects.resetPassword);
|
||||
|
||||
Future<void> _resetPassword() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> response =
|
||||
await _apiService.resetPassword(_userData);
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
showSuccessToast(response['message']);
|
||||
await replaceWithLogin();
|
||||
} else {
|
||||
showErrorToast(response['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/views/forget_password/forget_password_viewmodel.dart';
|
||||
import 'package:yimaru_app/ui/widgets/login_account.dart';
|
||||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/large_app_bar.dart';
|
||||
import '../../../widgets/option_text_divider.dart';
|
||||
import '../../../widgets/page_loading_indicator.dart';
|
||||
import '../forget_password_view.form.dart';
|
||||
|
||||
class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
|
||||
final TextEditingController emailController;
|
||||
|
||||
const RequestCodeScreen({
|
||||
super.key,
|
||||
required this.emailController,
|
||||
});
|
||||
|
||||
|
||||
Widget getPadding(context){
|
||||
double half = screenHeight(context)/2;
|
||||
return SizedBox(height: half + 375 - half,);
|
||||
}
|
||||
|
||||
void _inAppPop(ForgetPasswordViewModel viewModel) {
|
||||
_clearDataOnNavigation(viewModel);
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
|
||||
emailController.clear();
|
||||
viewModel.resetRequestResetCodeScreen();
|
||||
|
||||
}
|
||||
|
||||
Future<void> _addUserData(ForgetPasswordViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
Map<String, dynamic> data = {
|
||||
'email': emailController.text,
|
||||
};
|
||||
viewModel.addUserData(data);
|
||||
|
||||
await viewModel.requestResetCode();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(context: context, viewModel: viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldStack(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldStack( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) => Stack(
|
||||
children: [
|
||||
_buildScaffold(context: context,viewModel: viewModel),
|
||||
_buildRequestResetCodeState(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildScaffold( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildScaffoldChildren(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(context: context, viewModel: viewModel)];
|
||||
|
||||
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _inAppPop(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildExpandedBody( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) =>
|
||||
Expanded(child: _buildColumnScroller(context: context, viewModel: viewModel));
|
||||
|
||||
Widget _buildColumnScroller( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildBodyWrapper(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBodyWrapper( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildBody(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyChildren(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildBodyChildren( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) =>
|
||||
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButtonWrapper(viewModel)];
|
||||
|
||||
|
||||
|
||||
Widget _buildUpperColumn(ForgetPasswordViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildUpperColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildUpperColumnChildren(ForgetPasswordViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
_buildSubtitle(),
|
||||
verticalSpaceLarge,
|
||||
_buildEmailFormField(viewModel),
|
||||
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
|
||||
verticalSpaceTiny,
|
||||
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
|
||||
_buildEmailValidatorWrapper(viewModel),
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Reset password',
|
||||
style: style25DG600,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Enter your email, we will send you a reset code.',
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildEmailFormField(ForgetPasswordViewModel viewModel) =>
|
||||
TextFormField(
|
||||
controller: emailController,
|
||||
onTap: viewModel.setEmailFocus,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: inputDecoration(
|
||||
hint: 'Email',
|
||||
focus: viewModel.focusEmail,
|
||||
filled: emailController.text.isNotEmpty),
|
||||
);
|
||||
|
||||
Widget _buildEmailValidatorWrapper(ForgetPasswordViewModel viewModel) =>
|
||||
viewModel.hasEmailValidationMessage
|
||||
? _buildEmailValidator(viewModel)
|
||||
: Container();
|
||||
|
||||
Widget _buildEmailValidator(ForgetPasswordViewModel viewModel) => Text(
|
||||
viewModel.emailValidationMessage!,
|
||||
style: style12R700,
|
||||
);
|
||||
|
||||
Widget _buildContinueButtonWrapper(ForgetPasswordViewModel viewModel) =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50),
|
||||
child: _buildContinueButton(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildContinueButton(ForgetPasswordViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
text: 'Continue',
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
onTap: emailController.text.isNotEmpty &&
|
||||
!viewModel.hasEmailValidationMessage
|
||||
? () => _addUserData(viewModel)
|
||||
: null,
|
||||
backgroundColor: emailController.text.isNotEmpty &&
|
||||
!viewModel.hasEmailValidationMessage
|
||||
? kcPrimaryColor
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
);
|
||||
|
||||
Widget _buildRequestResetCodeState(ForgetPasswordViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.requestResetCode)
|
||||
? const PageLoadingIndicator()
|
||||
: Container();
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/enmus.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/custom_form_label.dart';
|
||||
import '../../../widgets/custom_linear_progress_indicator.dart';
|
||||
import '../../../widgets/large_app_bar.dart';
|
||||
import '../../../widgets/obscure_password.dart';
|
||||
import '../../../widgets/page_loading_indicator.dart';
|
||||
import '../../../widgets/validator_list_tile.dart';
|
||||
import '../forget_password_viewmodel.dart';
|
||||
import '../forget_password_view.form.dart';
|
||||
|
||||
class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
|
||||
final TextEditingController resetCodeController;
|
||||
final TextEditingController passwordController;
|
||||
final TextEditingController confirmPasswordController;
|
||||
|
||||
const ResetPasswordScreen(
|
||||
{super.key,
|
||||
required this.resetCodeController,
|
||||
required this.passwordController,
|
||||
required this.confirmPasswordController});
|
||||
|
||||
void _inAppPop(ForgetPasswordViewModel viewModel) {
|
||||
_clearDataOnNavigation(viewModel);
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
|
||||
|
||||
passwordController.clear();
|
||||
resetCodeController.clear();
|
||||
confirmPasswordController.clear();
|
||||
viewModel.resetResetPasswordScreen();
|
||||
|
||||
}
|
||||
|
||||
Future<void> _reset(ForgetPasswordViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
Map<String, dynamic> data = {
|
||||
'otp': resetCodeController.text,
|
||||
'password': passwordController.text,
|
||||
};
|
||||
viewModel.addUserData(data);
|
||||
|
||||
await viewModel.resetPassword();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(context: context, viewModel: viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldStack(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldStack( {required BuildContext context,
|
||||
required ForgetPasswordViewModel viewModel}) => Stack(
|
||||
children: [
|
||||
_buildScaffold(viewModel),
|
||||
_buildResetPasswordState(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildScaffold( ForgetPasswordViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildScaffoldChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren( ForgetPasswordViewModel viewModel) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||
|
||||
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _inAppPop(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildExpandedBody( ForgetPasswordViewModel viewModel) =>
|
||||
Expanded(child: _buildColumnScroller(viewModel));
|
||||
|
||||
Widget _buildColumnScroller( ForgetPasswordViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildBodyWrapper(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBodyWrapper( ForgetPasswordViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildBody(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody(ForgetPasswordViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
List<Widget> _buildBodyColumnChildren(ForgetPasswordViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildFormLabel('Reset code'),
|
||||
verticalSpaceSmall,
|
||||
_buildResetCodeFormField(viewModel),
|
||||
if (viewModel.hasResetCodeValidationMessage && viewModel.focusResetCode)
|
||||
verticalSpaceTiny,
|
||||
if (viewModel.hasResetCodeValidationMessage && viewModel.focusResetCode)
|
||||
_buildResetCodeValidationWrapper(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildFormLabel('New Password'),
|
||||
verticalSpaceSmall,
|
||||
_buildPasswordFormField(viewModel),
|
||||
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
|
||||
verticalSpaceTiny,
|
||||
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
|
||||
_buildPasswordValidationWrapper(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildFormLabel('Confirm Password'),
|
||||
verticalSpaceSmall,
|
||||
_buildConfirmPasswordFormField(viewModel),
|
||||
if (viewModel.hasConfirmPasswordValidationMessage &&
|
||||
viewModel.focusConfirmPassword)
|
||||
verticalSpaceTiny,
|
||||
if (viewModel.hasConfirmPasswordValidationMessage &&
|
||||
viewModel.focusConfirmPassword)
|
||||
_buildConfirmPasswordValidationWrapper(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildLinearProgressIndicator(viewModel),
|
||||
verticalSpaceSmall,
|
||||
_buildCharLengthValidator(viewModel),
|
||||
_buildNumberValidator(viewModel),
|
||||
_buildSymbolValidator(viewModel),
|
||||
_buildPasswordMatchValidator(viewModel),
|
||||
verticalSpaceSmall,
|
||||
_buildSignUpButton(viewModel),
|
||||
verticalSpaceMedium
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Reset password',
|
||||
style: style25DG600,
|
||||
);
|
||||
|
||||
Widget _buildFormLabel(String label) => CustomFormLabel(
|
||||
label: label,
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildResetCodeFormField(ForgetPasswordViewModel viewModel) =>
|
||||
TextFormField(
|
||||
controller: resetCodeController,
|
||||
onTap: viewModel.setResetCodeFocus,
|
||||
decoration: inputDecoration(
|
||||
hint: 'Reset code',
|
||||
focus: viewModel.focusResetCode,
|
||||
filled: passwordController.text.isNotEmpty),
|
||||
);
|
||||
|
||||
Widget _buildResetCodeValidationWrapper(ForgetPasswordViewModel viewModel) =>
|
||||
viewModel.hasResetCodeValidationMessage
|
||||
? _buildResetCodeValidator(viewModel)
|
||||
: Container();
|
||||
|
||||
Widget _buildResetCodeValidator(ForgetPasswordViewModel viewModel) => Text(
|
||||
viewModel.resetCodeValidationMessage!,
|
||||
style: style12R700,
|
||||
);
|
||||
|
||||
Widget _buildPasswordFormField(ForgetPasswordViewModel viewModel) =>
|
||||
TextFormField(
|
||||
controller: passwordController,
|
||||
onTap: viewModel.setPasswordFocus,
|
||||
obscureText: viewModel.obscurePassword,
|
||||
decoration: inputDecoration(
|
||||
hint: 'Password',
|
||||
focus: viewModel.focusPassword,
|
||||
suffix: _buildObscurePassword(viewModel),
|
||||
filled: passwordController.text.isNotEmpty),
|
||||
onChanged: (value) => viewModel.validatePassword(
|
||||
password: passwordController.text,
|
||||
confirmPassword: confirmPasswordController.text),
|
||||
);
|
||||
|
||||
Widget _buildObscurePassword(ForgetPasswordViewModel viewModel) =>
|
||||
ObscurePassword(
|
||||
focus: viewModel.focusPassword,
|
||||
obscure: viewModel.obscurePassword,
|
||||
onTap: viewModel.setObscurePassword,
|
||||
);
|
||||
|
||||
Widget _buildPasswordValidationWrapper(ForgetPasswordViewModel viewModel) =>
|
||||
viewModel.hasPasswordValidationMessage
|
||||
? _buildPasswordValidator(viewModel)
|
||||
: Container();
|
||||
|
||||
Widget _buildPasswordValidator(ForgetPasswordViewModel viewModel) => Text(
|
||||
viewModel.passwordValidationMessage!,
|
||||
style: style12R700,
|
||||
);
|
||||
|
||||
Widget _buildConfirmPasswordFormField(ForgetPasswordViewModel viewModel) =>
|
||||
TextFormField(
|
||||
controller: confirmPasswordController,
|
||||
onTap: viewModel.setConfirmPasswordFocus,
|
||||
obscureText: viewModel.obscureConfirmPassword,
|
||||
onChanged: (value) => viewModel.validatePassword(
|
||||
password: passwordController.text,
|
||||
confirmPassword: confirmPasswordController.text),
|
||||
decoration: inputDecoration(
|
||||
hint: 'Confirm Password',
|
||||
focus: viewModel.focusConfirmPassword,
|
||||
suffix: _buildObscureConfirmPassword(viewModel),
|
||||
filled: confirmPasswordController.text.isNotEmpty),
|
||||
);
|
||||
|
||||
Widget _buildObscureConfirmPassword(ForgetPasswordViewModel viewModel) =>
|
||||
ObscurePassword(
|
||||
focus: viewModel.focusConfirmPassword,
|
||||
obscure: viewModel.obscureConfirmPassword,
|
||||
onTap: viewModel.setObscureConfirmPassword,
|
||||
);
|
||||
|
||||
Widget _buildConfirmPasswordValidationWrapper(
|
||||
ForgetPasswordViewModel viewModel) =>
|
||||
viewModel.hasConfirmPasswordValidationMessage
|
||||
? _buildConfirmPasswordValidator(viewModel)
|
||||
: Container();
|
||||
|
||||
Widget _buildConfirmPasswordValidator(ForgetPasswordViewModel viewModel) =>
|
||||
Text(
|
||||
viewModel.confirmPasswordValidationMessage!,
|
||||
style: style12R700,
|
||||
);
|
||||
|
||||
Widget _buildLinearProgressIndicator(ForgetPasswordViewModel viewModel) =>
|
||||
CustomLinearProgressIndicator(
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcVeryLightGrey,
|
||||
progress: viewModel.validationProgress(),
|
||||
);
|
||||
|
||||
Widget _buildCharLengthValidator(ForgetPasswordViewModel viewModel) =>
|
||||
ValidatorListTile(
|
||||
backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey,
|
||||
label: '8 characters minimum');
|
||||
|
||||
Widget _buildNumberValidator(ForgetPasswordViewModel viewModel) =>
|
||||
ValidatorListTile(
|
||||
backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey,
|
||||
label: 'a number');
|
||||
|
||||
Widget _buildSymbolValidator(ForgetPasswordViewModel viewModel) =>
|
||||
ValidatorListTile(
|
||||
backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey,
|
||||
label: 'one symbol minimum');
|
||||
|
||||
Widget _buildPasswordMatchValidator(ForgetPasswordViewModel viewModel) =>
|
||||
ValidatorListTile(
|
||||
backgroundColor:
|
||||
viewModel.passwordMatch ? kcPrimaryColor : kcLightGrey,
|
||||
label: 'password match');
|
||||
|
||||
Widget _buildSignUpButton(ForgetPasswordViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
text: 'Continue',
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
onTap: passwordController.text.isNotEmpty &&
|
||||
confirmPasswordController.text.isNotEmpty &&
|
||||
resetCodeController.text.isNotEmpty &&
|
||||
viewModel.number &&
|
||||
viewModel.length &&
|
||||
viewModel.specialChar &&
|
||||
viewModel.specialChar &&
|
||||
viewModel.passwordMatch
|
||||
? () async => await _reset(viewModel)
|
||||
: null,
|
||||
backgroundColor: passwordController.text.isNotEmpty &&
|
||||
confirmPasswordController.text.isNotEmpty &&
|
||||
resetCodeController.text.isNotEmpty &&
|
||||
viewModel.number &&
|
||||
viewModel.length &&
|
||||
viewModel.specialChar &&
|
||||
viewModel.specialChar &&
|
||||
viewModel.passwordMatch
|
||||
? kcPrimaryColor
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
);
|
||||
|
||||
Widget _buildResetPasswordState(ForgetPasswordViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.resetPassword)
|
||||
? const PageLoadingIndicator()
|
||||
: Container();
|
||||
}
|
||||
|
|
@ -15,9 +15,8 @@ class HomeView extends StackedView<HomeViewModel> {
|
|||
HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel();
|
||||
|
||||
@override
|
||||
void onViewModelReady(HomeViewModel viewModel) async {
|
||||
await viewModel.getProfileStatus();
|
||||
await viewModel.getProfileData();
|
||||
void onViewModelReady(HomeViewModel viewModel) {
|
||||
viewModel.getProfileStatus();
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +26,9 @@ class HomeView extends StackedView<HomeViewModel> {
|
|||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy
|
||||
? const StartupView(label: 'Checking user info')
|
||||
? const StartupView(
|
||||
label: 'Checking user info',
|
||||
)
|
||||
: _buildScaffold(viewModel);
|
||||
|
||||
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(
|
||||
|
|
|
|||
|
|
@ -7,30 +7,18 @@ import 'package:yimaru_app/services/status_checker_service.dart';
|
|||
import 'package:yimaru_app/ui/common/app_strings.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
||||
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/image_downloader_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class HomeViewModel extends ReactiveViewModel {
|
||||
class HomeViewModel extends BaseViewModel {
|
||||
final _apiService = locator<ApiService>();
|
||||
final _dialogService = locator<DialogService>();
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
final _navigationService = locator<NavigationService>();
|
||||
final _bottomSheetService = locator<BottomSheetService>();
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
final _imageDownloaderService = locator<ImageDownloaderService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
|
||||
// Current user
|
||||
UserModel? get _user => _authenticationService.user;
|
||||
|
||||
UserModel? get user => _user;
|
||||
|
||||
// Bottom navigation
|
||||
int _currentIndex = 0;
|
||||
|
|
@ -58,77 +46,33 @@ class HomeViewModel extends ReactiveViewModel {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> saveProfileStatus(bool value) async =>
|
||||
await _authenticationService.saveProfileStatus(value);
|
||||
|
||||
// Navigation
|
||||
Future<void> replaceWithFailure() async =>
|
||||
await _navigationService.clearStackAndShowView(
|
||||
const FailureView(label: 'Check your internet connection to proceed'),
|
||||
);
|
||||
|
||||
Future<void> replaceWithOnboarding() async =>
|
||||
await _navigationService.replaceWithOnboardingView();
|
||||
|
||||
// Remote api calls
|
||||
|
||||
// Profile data
|
||||
Future<void> getProfileData() async => await runBusyFuture(_getProfileData());
|
||||
|
||||
Future<void> _getProfileData() async {
|
||||
print('RESPONSE FOR USER DATA ${_user?.firstName}');
|
||||
|
||||
if (!(_user?.userInfoLoaded ?? false)) {
|
||||
print('RESPONSE FOR USER DATA 1');
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> response = {};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Future<void> getProfileStatus() async {
|
||||
Map<String, dynamic> response =
|
||||
await runBusyFuture<Map<String, dynamic>>(_getProfileStatus());
|
||||
if (response['status'] == ResponseStatus.success && !response['data']) {
|
||||
await replaceWithOnboarding();
|
||||
}
|
||||
}
|
||||
|
||||
// Profile status
|
||||
Future<void> getProfileStatus() async =>
|
||||
await runBusyFuture(_getProfileStatus());
|
||||
Future<Map<String, dynamic>> _getProfileStatus() async {
|
||||
Map<String, dynamic> response = {};
|
||||
UserModel user = await _authenticationService.getUser();
|
||||
|
||||
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};
|
||||
if (user.profileCompleted == null) {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
response = await _apiService.getProfileStatus(user);
|
||||
} else {
|
||||
response = {'data': true, 'status': ResponseStatus.success};
|
||||
}
|
||||
|
||||
if (response['status'] == ResponseStatus.success && !response['data']) {
|
||||
await replaceWithOnboarding();
|
||||
} else if (response['status'] == ResponseStatus.success &&
|
||||
response['data']) {
|
||||
await saveProfileStatus(response['data']);
|
||||
response = {'data': false, 'status': ResponseStatus.success};
|
||||
}
|
||||
} else {
|
||||
response = {'data': true, 'status': ResponseStatus.success};
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class LanguageView extends StackedView<LanguageViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildLanguages(viewModel)
|
||||
];
|
||||
|
|
@ -72,18 +72,22 @@ class LanguageView extends StackedView<LanguageViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
|
||||
onTap: viewModel.pop,
|
||||
title: 'Language Preference',
|
||||
onTap: viewModel.pop,
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Choose your language',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'You can switch languages anytime',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder(
|
||||
|
|
|
|||
|
|
@ -38,15 +38,12 @@ class LearnView extends StackedView<LearnViewModel> {
|
|||
Widget _buildColumn(LearnViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
_buildAppBar(),
|
||||
_buildLevelsColumnWrapper(viewModel)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(LearnViewModel viewModel) => LearnAppBar(
|
||||
name: viewModel.user?.firstName,
|
||||
profileImage: viewModel.user?.profilePicture,
|
||||
);
|
||||
Widget _buildAppBar() => const LearnAppBar();
|
||||
|
||||
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
|
||||
Expanded(child: _buildLevelsColumnScrollView(viewModel));
|
||||
|
|
|
|||
|
|
@ -1,22 +1,12 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/app/app.router.dart';
|
||||
import 'package:yimaru_app/models/user_model.dart';
|
||||
import 'package:yimaru_app/services/authentication_service.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
|
||||
class LearnViewModel extends ReactiveViewModel {
|
||||
class LearnViewModel extends BaseViewModel {
|
||||
final _navigationService = locator<NavigationService>();
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
|
||||
// Current user
|
||||
UserModel? get user => _authenticationService.user;
|
||||
|
||||
final List<Map<String, dynamic>> _learnLevels = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:yimaru_app/ui/common/enmus.dart';
|
|||
import 'package:yimaru_app/ui/widgets/learn_lesson_tile.dart';
|
||||
import 'package:yimaru_app/ui/widgets/module_progress.dart';
|
||||
import 'package:yimaru_app/ui/widgets/motivation_card.dart';
|
||||
import 'package:yimaru_app/ui/widgets/overall_module_progress.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
|
|
@ -117,24 +118,19 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
|
|||
itemCount: viewModel.lessons.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
title: viewModel.lessons[index]['title'],
|
||||
status: viewModel.lessons[index]['status'],
|
||||
thumbnail: viewModel.lessons[index]['thumbnail'],
|
||||
onLessonTap: () async =>
|
||||
await viewModel.navigateToLearnLessonDetail(),
|
||||
),
|
||||
title: viewModel.lessons[index]['title'],
|
||||
status: viewModel.lessons[index]['status'],
|
||||
thumbnail: viewModel.lessons[index]['thumbnail']),
|
||||
);
|
||||
|
||||
Widget _buildTile({
|
||||
required String title,
|
||||
required String thumbnail,
|
||||
GestureTapCallback? onLessonTap,
|
||||
required ProgressStatuses status,
|
||||
}) =>
|
||||
LearnLessonTile(
|
||||
title: title,
|
||||
status: status,
|
||||
thumbnail: thumbnail,
|
||||
onLessonTap: onLessonTap,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/app/app.router.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
|
|
@ -9,6 +8,7 @@ class LearnLessonViewModel extends BaseViewModel {
|
|||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// Lessons
|
||||
// Downloads
|
||||
final List<Map<String, dynamic>> _lessons = [
|
||||
{
|
||||
'title': '1.1 Introducing Yourself',
|
||||
|
|
@ -31,7 +31,4 @@ class LearnLessonViewModel extends BaseViewModel {
|
|||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToLearnLessonDetail() async =>
|
||||
await _navigationService.navigateToLearnLessonDetailView();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_viewmodel.dart';
|
||||
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
|
||||
import 'package:yimaru_app/ui/widgets/empty_video_player.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/custom_elevated_button.dart';
|
||||
import '../../widgets/small_app_bar.dart';
|
||||
import 'learn_lesson_detail_viewmodel.dart';
|
||||
|
||||
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
||||
const LearnLessonDetailView({Key? key}) : super(key: key);
|
||||
|
||||
|
||||
Future<void> _navigate(LearnLessonDetailViewModel viewModel)async{
|
||||
await viewModel.pause();
|
||||
await viewModel.navigateToLearnPractice();
|
||||
}
|
||||
|
||||
|
||||
// @override
|
||||
// void onDispose(LearnLessonDetailViewModel viewModel) {
|
||||
// print('DISPOSED');
|
||||
// viewModel.dispose();
|
||||
// super.onDispose(viewModel);
|
||||
// }
|
||||
|
||||
@override
|
||||
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
|
||||
await viewModel.initializePlayer();
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
LearnLessonDetailViewModel viewModelBuilder(BuildContext context) =>
|
||||
LearnLessonDetailViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
LearnLessonDetailViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(LearnLessonDetailViewModel viewModel) =>
|
||||
Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(LearnLessonDetailViewModel viewModel) =>
|
||||
SafeArea(child: _buildColumn(viewModel));
|
||||
|
||||
Widget _buildColumn(LearnLessonDetailViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBarWrapper(viewModel),
|
||||
_buildBodyColumnWrapper(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBarWrapper(LearnLessonDetailViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildAppBar(viewModel));
|
||||
|
||||
Widget _buildAppBar(LearnLessonDetailViewModel viewModel) => SmallAppBar(
|
||||
onTap: viewModel.pop,
|
||||
);
|
||||
|
||||
Widget _buildBodyColumnWrapper(LearnLessonDetailViewModel viewModel) =>
|
||||
Expanded(
|
||||
child: _buildBodyColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBodyColumn(LearnLessonDetailViewModel viewModel) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildBodyColumnChildren(LearnLessonDetailViewModel viewModel) =>
|
||||
[
|
||||
_buildLevelsColumnWrapper(viewModel),
|
||||
_buildContinueButtonWrapper(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildLevelsColumnWrapper(LearnLessonDetailViewModel viewModel) =>
|
||||
Expanded(child: _buildLevelsColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildLevelsColumnScrollView(LearnLessonDetailViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildLevelsColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildLevelsColumn(LearnLessonDetailViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildLevelsColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildLevelsColumnChildren(
|
||||
LearnLessonDetailViewModel viewModel) =>
|
||||
[
|
||||
verticalSpaceMedium,
|
||||
_buildTitleWrapper(),
|
||||
verticalSpaceLarge,
|
||||
_buildVideoPlayerWrapper(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildDescriptionWrapper(),
|
||||
];
|
||||
|
||||
Widget _buildTitleWrapper() => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildTitle(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'1.3 Common Greetings',
|
||||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildVideoPlayerWrapper(LearnLessonDetailViewModel viewModel) =>
|
||||
Container(
|
||||
height: 200,
|
||||
color: kcBlack,
|
||||
width: double.maxFinite,
|
||||
child: _buildVideoPlayerState(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildVideoPlayerState(LearnLessonDetailViewModel viewModel) =>
|
||||
viewModel.chewieController != null &&
|
||||
viewModel.videoPlayerController != null &&
|
||||
!viewModel.busy(StateObjects.loadLessonVideo)
|
||||
? _buildVideoPlayer(viewModel)
|
||||
: _buildEmptyVideoPlayer();
|
||||
|
||||
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
|
||||
_buildChewiePlayer(viewModel);
|
||||
|
||||
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
|
||||
Chewie(controller: viewModel.chewieController!);
|
||||
|
||||
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
|
||||
|
||||
Widget _buildDescriptionWrapper() => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildDescription(),
|
||||
);
|
||||
|
||||
Widget _buildDescription() => Text(
|
||||
'In this lesson, 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),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import 'package:chewie/chewie.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:yimaru_app/app/app.router.dart';
|
||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
|
||||
class LearnLessonDetailViewModel extends BaseViewModel {
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// Video player config
|
||||
ChewieController? _chewieController;
|
||||
|
||||
ChewieController? get chewieController => _chewieController;
|
||||
|
||||
VideoPlayerController? _videoPlayerController;
|
||||
|
||||
VideoPlayerController? get videoPlayerController => _videoPlayerController;
|
||||
|
||||
// Video player
|
||||
Future<void> initializePlayer() async =>
|
||||
await runBusyFuture(_initializePlayer(),
|
||||
busyObject: StateObjects.loadLessonVideo);
|
||||
|
||||
Future<void> _initializePlayer() async {
|
||||
_videoPlayerController =
|
||||
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl));
|
||||
|
||||
await _videoPlayerController?.initialize();
|
||||
|
||||
if (_videoPlayerController != null) {
|
||||
print('Initialized');
|
||||
_chewieController = ChewieController(
|
||||
looping: true,
|
||||
autoPlay: true,
|
||||
showOptions: true,
|
||||
showControls: true,
|
||||
aspectRatio: 16 / 9,
|
||||
autoInitialize: true,
|
||||
allowedScreenSleep: false,
|
||||
videoPlayerController: _videoPlayerController!,
|
||||
materialProgressColors: buildChewieProgressIndicator);
|
||||
}
|
||||
|
||||
// rebuildUi();
|
||||
}
|
||||
|
||||
Future<void> pause()async{
|
||||
await _chewieController?.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_videoPlayerController?.dispose();
|
||||
_chewieController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToLearnPractice() async=>await _navigationService.navigateToLearnPracticeView();
|
||||
|
||||
}
|
||||
|
|
@ -73,14 +73,20 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
|||
_buildListView(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'A1 - Beginner',
|
||||
style: style18P600,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubTitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'Your Current Level',
|
||||
style: style14DG400,
|
||||
style: TextStyle(
|
||||
color: kcDarkGrey,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildOverallProgress() => const OverallLearnProgress();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,5 @@ class LearnModuleViewModel extends BaseViewModel {
|
|||
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToLearnLesson() async =>
|
||||
await _navigationService.navigateToLearnLessonView();
|
||||
Future<void> navigateToLearnLesson() async=> await _navigationService.navigateToLearnLessonView();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/screens/listen_speaker_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/screens/practice_intro_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/screens/start_practice_screen.dart';
|
||||
import 'package:yimaru_app/ui/widgets/profile_image.dart';
|
||||
import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/custom_elevated_button.dart';
|
||||
import '../../widgets/small_app_bar.dart';
|
||||
import 'learn_practice_viewmodel.dart';
|
||||
|
||||
class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
||||
const LearnPracticeView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
LearnPracticeViewModel viewModelBuilder(BuildContext context) =>
|
||||
LearnPracticeViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
LearnPracticeViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildPracticeScreensWrapper(viewModel);
|
||||
|
||||
|
||||
|
||||
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) => PopScope(
|
||||
canPop: true,
|
||||
onPopInvokedWithResult: (value, data) {
|
||||
if (!value) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
|
||||
},
|
||||
child: _buildScaffoldWrapper(viewModel));
|
||||
|
||||
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldStack(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldStack(LearnPracticeViewModel viewModel) => Stack(children: [
|
||||
_buildBody(viewModel),
|
||||
//_buildLoginWithEmailState(viewModel),
|
||||
//_buildLoginWithGoogleState(viewModel)
|
||||
]);
|
||||
|
||||
|
||||
|
||||
|
||||
Widget _buildBody(LearnPracticeViewModel viewModel) =>
|
||||
IndexedStack(
|
||||
|
||||
index: viewModel.currentIndex, children: _buildScreens());
|
||||
|
||||
List<Widget> _buildScreens() => [
|
||||
_buildPracticeIntroScreen(),
|
||||
_buildStartPracticeScreen(),
|
||||
_buildListenSpeakerScreen()
|
||||
];
|
||||
|
||||
Widget _buildPracticeIntroScreen() => const PracticeIntroScreen();
|
||||
|
||||
Widget _buildStartPracticeScreen() => const StartPracticeScreen();
|
||||
|
||||
Widget _buildListenSpeakerScreen() => const ListenSpeakerScreen();
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
|
||||
class LearnPracticeViewModel extends BaseViewModel {
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// In-app navigation
|
||||
int _currentIndex = 0;
|
||||
|
||||
int get currentIndex => _currentIndex;
|
||||
|
||||
// In-app navigation
|
||||
void goTo(int page) {
|
||||
|
||||
_currentIndex = page;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
void goBack() {
|
||||
if(_currentIndex == 0){
|
||||
pop();
|
||||
}else{
|
||||
_currentIndex--;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
}
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
|
||||
import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart';
|
||||
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
|
||||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_column_button.dart';
|
||||
import '../../../widgets/small_app_bar.dart';
|
||||
|
||||
class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
||||
const ListenSpeakerScreen({super.key});
|
||||
|
||||
Future<void> _showSheet(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) async =>
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: kcTransparent,
|
||||
builder: (_) => _buildSheet(viewModel),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(context: context, viewModel: viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
SafeArea(
|
||||
child:
|
||||
_buildBodyColumnWrapper(context: context, viewModel: viewModel));
|
||||
|
||||
Widget _buildBodyColumnWrapper(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildBodyStack(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBodyStack(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Stack(
|
||||
children: [
|
||||
_buildBodyColumn(context: context, viewModel: viewModel),
|
||||
_buildProgressIndicatorWrapper()
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildBodyColumn(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children:
|
||||
_buildBodyColumnChildren(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildBodyColumnChildren(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
[
|
||||
_buildAppBarWrapper(viewModel),
|
||||
_buildSpeakingIndicatorWrapper(viewModel),
|
||||
_buildLowerButtonsSectionWrapper(context: context, viewModel: viewModel)
|
||||
];
|
||||
|
||||
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
|
||||
onTap: viewModel.goBack,
|
||||
title: 'Practice Speaking',
|
||||
);
|
||||
|
||||
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: _buildSpeakingIndicatorChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildSpeakingIndicatorChildren() =>
|
||||
[_buildSpeakerLabel(), verticalSpaceMedium, _buildSpeakingIndicator()];
|
||||
|
||||
Widget _buildSpeakerLabel() => Text(
|
||||
'Daniel is speaking...',
|
||||
style: style14P400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildSpeakingIndicator() => Container(
|
||||
height: 200,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
radius: 0.7,
|
||||
stops: const [
|
||||
0.2,
|
||||
0.25,
|
||||
0.45,
|
||||
0.75,
|
||||
1,
|
||||
],
|
||||
center: Alignment.center,
|
||||
colors: [
|
||||
kcPrimaryColor.withOpacity(0.4),
|
||||
kcPrimaryColor.withOpacity(0.4),
|
||||
kcPrimaryColor.withOpacity(0.15),
|
||||
kcPrimaryColor.withOpacity(0.1),
|
||||
kcPrimaryColor.withOpacity(0.05),
|
||||
],
|
||||
// quarterly spread
|
||||
),
|
||||
),
|
||||
child: _buildSpinner(),
|
||||
);
|
||||
|
||||
Widget _buildSpinner() => const SpinKitWave(
|
||||
size: 20,
|
||||
color: kcPrimaryColor,
|
||||
type: SpinKitWaveType.center,
|
||||
);
|
||||
|
||||
Widget _buildLowerButtonsSectionWrapper(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child:
|
||||
_buildLowerButtonsSection(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildLowerButtonsSection(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _buildLowerButtonsSectionChildren(
|
||||
context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildLowerButtonsSectionChildren(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
[
|
||||
_buildActionLabel(),
|
||||
verticalSpaceMedium,
|
||||
_buildButtonsRowWrapper(context: context, viewModel: viewModel),
|
||||
verticalSpaceMedium,
|
||||
];
|
||||
|
||||
Widget _buildActionLabel() => Text(
|
||||
'Tap the microphone to speak',
|
||||
style: style14DG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildButtonsRowWrapper(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children:
|
||||
_buildButtonsRowChildren(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildButtonsRowChildren(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
[
|
||||
_buildReplyButtonWrapper(),
|
||||
_buildMicButtonWrapper(),
|
||||
_buildCancelButtonWrapper(context: context, viewModel: viewModel)
|
||||
];
|
||||
|
||||
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
|
||||
|
||||
Widget _buildReplyButton() => const CustomColumnButton(
|
||||
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
|
||||
|
||||
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
|
||||
|
||||
Widget _buildMicButton() => ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: const ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(CircleBorder()),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
|
||||
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
),
|
||||
child: _buildMicIcon(),
|
||||
);
|
||||
|
||||
Widget _buildMicIcon() => const Icon(
|
||||
Icons.mic,
|
||||
size: 35,
|
||||
color: kcWhite,
|
||||
);
|
||||
|
||||
Widget _buildCancelButtonWrapper(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
Expanded(
|
||||
child: _buildCancelButton(context: context, viewModel: viewModel));
|
||||
|
||||
Widget _buildCancelButton(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) =>
|
||||
CustomColumnButton(
|
||||
color: kcRed,
|
||||
label: 'Cancel',
|
||||
icon: Icons.close,
|
||||
onTap: () async =>
|
||||
await _showSheet(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
|
||||
CancelLearnPracticeSheet(
|
||||
onTap: viewModel.pop,
|
||||
);
|
||||
|
||||
Widget _buildProgressIndicatorWrapper() => Positioned(
|
||||
top: 75,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _buildProgressIndicator(),
|
||||
);
|
||||
|
||||
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
|
||||
progress: 0.7,
|
||||
activeColor: kcPrimaryColor,
|
||||
backgroundColor: kcVeryLightGrey);
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
|
||||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/small_app_bar.dart';
|
||||
import '../../../widgets/speaking_partner_image.dart';
|
||||
|
||||
class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
||||
const PracticeIntroScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildScaffoldWrapper(viewModel);
|
||||
|
||||
|
||||
|
||||
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
|
||||
SafeArea(child: _buildColumnWrapper(viewModel));
|
||||
|
||||
Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
_buildBodyColumnWrapper(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
|
||||
onTap: viewModel.goBack,
|
||||
title: 'Practice Speaking',
|
||||
);
|
||||
|
||||
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded(
|
||||
child: _buildBodyColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
|
||||
_buildPracticeColumnWrapper(viewModel),
|
||||
_buildContinueButtonWrapper(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildPracticeColumnWrapper(LearnPracticeViewModel viewModel) =>
|
||||
Expanded(child: _buildPracticeColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildPracticeColumnScrollView(LearnPracticeViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildPracticeColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildPracticeColumn(LearnPracticeViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _buildPracticeColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildPracticeColumnChildren(LearnPracticeViewModel viewModel) =>
|
||||
[
|
||||
verticalSpaceMassive,
|
||||
_buildImage(),
|
||||
verticalSpaceMedium,
|
||||
_buildPartnerName(),
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildSubtitle()
|
||||
];
|
||||
|
||||
Widget _buildImage() => const SpeakingPartnerImage(radius: 75,);
|
||||
|
||||
Widget _buildPartnerName() => Text.rich(
|
||||
TextSpan(text: 'Daniel', style: style14DG600, children: [
|
||||
TextSpan(
|
||||
text: ' - Your Speaking Partner',
|
||||
style: style14MG400,
|
||||
)
|
||||
]),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Let \'s practice what you just learnt!',
|
||||
style: style25DG600,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'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,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
|
||||
import 'package:yimaru_app/ui/widgets/custom_column_button.dart';
|
||||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/small_app_bar.dart';
|
||||
|
||||
class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
||||
const StartPracticeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
|
||||
SafeArea(child: _buildBodyColumnWrapper(viewModel));
|
||||
|
||||
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildBodyColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
|
||||
_buildAppBarWrapper(viewModel),
|
||||
_buildStartButtonWrapper(viewModel),
|
||||
_buildLowerButtonsSectionWrapper(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
|
||||
onTap: viewModel.goBack,
|
||||
title: 'Practice Speaking',
|
||||
);
|
||||
|
||||
Widget _buildStartButtonWrapper(LearnPracticeViewModel viewModel) => Expanded(
|
||||
child: _buildStartButtonContainer(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) =>
|
||||
GestureDetector(
|
||||
onTap: () => viewModel.goTo(2),
|
||||
child: _buildStartButton(),
|
||||
);
|
||||
|
||||
Widget _buildStartButton() => Container(
|
||||
width: 150,
|
||||
height: 150,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: SweepGradient(
|
||||
stops: const [
|
||||
0.0,
|
||||
0.1,
|
||||
0.2,
|
||||
0.3,
|
||||
0.4,
|
||||
0.5,
|
||||
0.6,
|
||||
0.8,
|
||||
0.9,
|
||||
1,
|
||||
],
|
||||
endAngle: 8,
|
||||
startAngle: 0.0,
|
||||
center: Alignment.center,
|
||||
colors: [
|
||||
kcPrimaryColor.withOpacity(0.3),
|
||||
kcIndigo.withOpacity(0.2),
|
||||
kcIndigo.withOpacity(0.3),
|
||||
kcIndigo.withOpacity(0.4),
|
||||
kcIndigo.withOpacity(0.5),
|
||||
kcPrimaryColor.withOpacity(0.5),
|
||||
kcPrimaryColor.withOpacity(0.4),
|
||||
kcPrimaryColor.withOpacity(0.3),
|
||||
kcPrimaryColor.withOpacity(0.2),
|
||||
kcPrimaryColor.withOpacity(0.5),
|
||||
],
|
||||
// quarterly spread
|
||||
),
|
||||
),
|
||||
child: _buildStartText(),
|
||||
);
|
||||
|
||||
Widget _buildStartText() => Text(
|
||||
'Start',
|
||||
style: style25W600,
|
||||
);
|
||||
|
||||
Widget _buildLowerButtonsSectionWrapper(LearnPracticeViewModel viewMode) =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: _buildLowerButtonsSection(viewMode),
|
||||
);
|
||||
|
||||
Widget _buildLowerButtonsSection(LearnPracticeViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _buildLowerButtonsSectionChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildLowerButtonsSectionChildren(
|
||||
LearnPracticeViewModel viewModel) =>
|
||||
[
|
||||
_buildActionLabel(),
|
||||
verticalSpaceMedium,
|
||||
_buildButtonsRowWrapper(),
|
||||
verticalSpaceMedium,
|
||||
];
|
||||
|
||||
Widget _buildActionLabel() => Text(
|
||||
'Tap the microphone to speak',
|
||||
style: style14DG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildButtonsRowWrapper() => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildButtonsRowChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildButtonsRowChildren() => [
|
||||
_buildReplyButtonWrapper(),
|
||||
_buildMicButtonWrapper(),
|
||||
_buildEmptySpace()
|
||||
];
|
||||
|
||||
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
|
||||
|
||||
Widget _buildReplyButton() => const CustomColumnButton(
|
||||
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
|
||||
|
||||
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
|
||||
|
||||
Widget _buildMicButton() => ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: const ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(CircleBorder()),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
|
||||
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
|
||||
),
|
||||
child: _buildMicIcon(),
|
||||
);
|
||||
|
||||
Widget _buildMicIcon() => const Icon(
|
||||
Icons.mic,
|
||||
size: 35,
|
||||
color: kcWhite,
|
||||
);
|
||||
|
||||
Widget _buildEmptySpace() => Expanded(child: Container());
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked/stacked_annotations.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/views/login/screens/login_otp_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/login/screens/login_with_email_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart';
|
||||
|
|
@ -25,18 +24,10 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
|
|||
|
||||
@override
|
||||
void onViewModelReady(LoginViewModel viewModel) {
|
||||
_clearData();
|
||||
syncFormWithViewModel(viewModel);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
void _clearData() {
|
||||
otpController.clear();
|
||||
emailController.clear();
|
||||
passwordController.clear();
|
||||
phoneNumberController.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel();
|
||||
|
||||
|
|
@ -54,11 +45,35 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
|
|||
if (!value) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
|
||||
},
|
||||
child: _buildBody(viewModel));
|
||||
child: _buildScaffoldWrapper(viewModel));
|
||||
|
||||
Widget _buildScaffoldWrapper(LoginViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldStack(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldStack(LoginViewModel viewModel) =>
|
||||
Stack(children: [_buildScaffold(viewModel), _buildBusyLogin(viewModel)]);
|
||||
|
||||
Widget _buildScaffold(LoginViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildScaffoldChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren(LoginViewModel viewModel) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||
|
||||
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
|
||||
showBackButton: false,
|
||||
showLanguageSelection: true,
|
||||
);
|
||||
Widget _buildExpandedBody(LoginViewModel viewModel) =>
|
||||
Expanded(child: _buildBodyWrapper(viewModel));
|
||||
|
||||
Widget _buildBodyWrapper(LoginViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildBody(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody(LoginViewModel viewModel) =>
|
||||
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
|
||||
|
|
@ -79,4 +94,6 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
|
|||
otpController: otpController,
|
||||
phoneNumberController: phoneNumberController);
|
||||
|
||||
Widget _buildBusyLogin(LoginViewModel viewModel) =>
|
||||
viewModel.isBusy ? const PageLoadingIndicator() : Container();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/app/app.locator.dart';
|
||||
|
|
@ -8,25 +7,16 @@ import 'package:yimaru_app/models/user_model.dart';
|
|||
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/google_auth_service.dart';
|
||||
import '../../../services/image_downloader_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../home/home_view.dart';
|
||||
|
||||
class LoginViewModel extends FormViewModel {
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
final _googleAuthService = locator<GoogleAuthService>();
|
||||
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
// In-app navigation
|
||||
// Navigation
|
||||
int _currentIndex = 0;
|
||||
|
||||
int get currentIndex => _currentIndex;
|
||||
|
|
@ -116,7 +106,36 @@ class LoginViewModel extends FormViewModel {
|
|||
_userData.clear();
|
||||
}
|
||||
|
||||
// In app navigation
|
||||
// Remote api calls
|
||||
Future<void> login() async {
|
||||
Map<String, dynamic> response =
|
||||
await runBusyFuture<Map<String, dynamic>>(_login());
|
||||
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
await replaceWithHome();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _login() async {
|
||||
Map<String, dynamic> response = await _apiService.login(_userData);
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
UserModel user = response['data'] as UserModel;
|
||||
Map<String, dynamic> data = {
|
||||
'userId': user.userId,
|
||||
'accessToken': user.accessToken,
|
||||
'refreshToken': user.refreshToken
|
||||
};
|
||||
|
||||
await _authenticationService.saveUserData(data);
|
||||
showSuccessToast(response['message']);
|
||||
} else {
|
||||
showErrorToast(response['message']);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void goTo(int page) {
|
||||
_currentIndex = page;
|
||||
rebuildUi();
|
||||
|
|
@ -134,70 +153,9 @@ class LoginViewModel extends FormViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
Future<void> navigateToRegister() async =>
|
||||
await _navigationService.navigateToRegisterView();
|
||||
|
||||
Future<void> navigateToForgetPassword() async =>
|
||||
await _navigationService.navigateToForgetPasswordView();
|
||||
|
||||
Future<void> replaceWithHome() async =>
|
||||
await _navigationService.clearStackAndShowView(const HomeView());
|
||||
|
||||
// Remote api calls
|
||||
|
||||
// Login with email
|
||||
Future<void> emailLogin() async => await runBusyFuture(_emailLogin(),
|
||||
busyObject: StateObjects.loginWithEmail);
|
||||
|
||||
Future<void> _emailLogin() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> response = await _apiService.emailLogin(_userData);
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
UserModel user = response['data'] as UserModel;
|
||||
Map<String, dynamic> data = {
|
||||
'userId': user.userId,
|
||||
'accessToken': user.accessToken,
|
||||
'refreshToken': user.refreshToken
|
||||
};
|
||||
|
||||
await _authenticationService.saveUserCredential(data);
|
||||
clearUserData();
|
||||
await replaceWithHome();
|
||||
showSuccessToast(response['message']);
|
||||
} else {
|
||||
showErrorToast(response['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signInWithGoogle() async => await runBusyFuture(_signInWithGoogle(),
|
||||
busyObject: StateObjects.loginWithGoogle);
|
||||
|
||||
Future<void> _signInWithGoogle() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
GoogleSignInAccount? googleUser = await _googleAuthService.googleAuth();
|
||||
|
||||
Map<String, dynamic> data = {
|
||||
'id_token': googleUser?.authentication.idToken ?? '',
|
||||
};
|
||||
|
||||
Map<String, dynamic> response = await _apiService.googleAuth(data);
|
||||
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
UserModel user = response['data'] as UserModel;
|
||||
Map<String, dynamic> data = {
|
||||
'userId': user.userId,
|
||||
'accessToken': user.accessToken,
|
||||
'refreshToken': user.refreshToken
|
||||
};
|
||||
await _authenticationService.saveUserCredential(data);
|
||||
clearUserData();
|
||||
await replaceWithHome();
|
||||
showSuccessToast(response['message']);
|
||||
} else {
|
||||
showErrorToast(response['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import 'package:yimaru_app/ui/widgets/custom_cursor.dart';
|
|||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/large_app_bar.dart';
|
||||
import '../login_viewmodel.dart';
|
||||
import '../login_view.form.dart';
|
||||
|
||||
|
|
@ -21,68 +20,29 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
|
|||
required this.otpController,
|
||||
required this.phoneNumberController});
|
||||
|
||||
Widget getPadding(context){
|
||||
double half = screenHeight(context)/2;
|
||||
return SizedBox(height: half + 325 - half,);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(context: context,viewModel: viewModel);
|
||||
_buildBody(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(context: context,viewModel: viewModel),
|
||||
);
|
||||
|
||||
|
||||
|
||||
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
|
||||
|
||||
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
|
||||
showBackButton: false,
|
||||
showLanguageSelection: true,
|
||||
);
|
||||
|
||||
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
|
||||
|
||||
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildBodyWrapper(context: context,viewModel: viewModel),
|
||||
Widget _buildBody(LoginViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyChildren(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,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyChildren(context: context,viewModel: viewModel),
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButton(viewModel)];
|
||||
|
||||
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
|
||||
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
|
||||
|
||||
Widget _buildColumnScroller(LoginViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildUpperColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildUpperColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildUpperColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import 'package:yimaru_app/ui/views/login/login_view.form.dart';
|
|||
import 'package:yimaru_app/ui/widgets/obscure_password.dart';
|
||||
|
||||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/enmus.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/large_app_bar.dart';
|
||||
import '../../../widgets/option_text_divider.dart';
|
||||
import '../../../widgets/page_loading_indicator.dart';
|
||||
import '../../../widgets/register_for_account.dart';
|
||||
import '../login_viewmodel.dart';
|
||||
|
||||
|
|
@ -22,12 +19,6 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
|||
required this.emailController,
|
||||
required this.passwordController});
|
||||
|
||||
|
||||
Widget getPadding(context){
|
||||
double half = screenHeight(context)/2;
|
||||
return SizedBox(height: half + 25 - half,);
|
||||
}
|
||||
|
||||
Future<void> _login(LoginViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -37,62 +28,26 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
|||
};
|
||||
viewModel.addUserData(data);
|
||||
|
||||
await viewModel.emailLogin();
|
||||
await viewModel.login();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
||||
_buildScaffoldWrapper(context: context,viewModel: viewModel);
|
||||
_buildBody(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldStack(context: context,viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldStack({required BuildContext context,required LoginViewModel viewModel}) => Stack(children: [
|
||||
_buildScaffold(context: context,viewModel: viewModel),
|
||||
_buildLoginWithEmailState(viewModel),
|
||||
_buildLoginWithGoogleState(viewModel)
|
||||
]);
|
||||
|
||||
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
|
||||
|
||||
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
|
||||
showBackButton: false,
|
||||
showLanguageSelection: true,
|
||||
);
|
||||
|
||||
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
|
||||
|
||||
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildBodyWrapper(context: context,viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildBody(context: context,viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
|
||||
Widget _buildBody(LoginViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildBodyChildren(context: context,viewModel: viewModel),
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
|
||||
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
|
||||
|
||||
|
||||
|
||||
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
|
||||
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)];
|
||||
|
||||
|
||||
Widget _buildColumnScroller(LoginViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildUpperColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -103,7 +58,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
|||
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
_buildSubtitleWrapper(viewModel),
|
||||
_buildSubTitleWrapper(viewModel),
|
||||
verticalSpaceLarge,
|
||||
_buildEmailFormField(viewModel),
|
||||
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
|
||||
|
|
@ -116,15 +71,19 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
|||
verticalSpaceTiny,
|
||||
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
|
||||
_buildPasswordValidationWrapper(viewModel),
|
||||
_buildForgetPasswordTextButtonWrapper(viewModel),
|
||||
_buildForgetPasswordTextButtonWrapper(),
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Welcome Back',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
|
||||
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
|
||||
onTap: () async => await viewModel.navigateToRegister(),
|
||||
);
|
||||
|
||||
|
|
@ -145,7 +104,11 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
|||
|
||||
Widget _buildEmailValidator(LoginViewModel viewModel) => Text(
|
||||
viewModel.emailValidationMessage!,
|
||||
style: style12R700,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField(
|
||||
|
|
@ -172,23 +135,26 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
|||
|
||||
Widget _buildPasswordValidator(LoginViewModel viewModel) => Text(
|
||||
viewModel.passwordValidationMessage!,
|
||||
style: style12R700,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildForgetPasswordTextButtonWrapper(LoginViewModel viewModel) =>
|
||||
Align(
|
||||
Widget _buildForgetPasswordTextButtonWrapper() => Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _buildForgetPasswordTextButton(viewModel),
|
||||
child: _buildForgetPasswordTextButton(),
|
||||
);
|
||||
|
||||
Widget _buildForgetPasswordTextButton(LoginViewModel viewModel) => TextButton(
|
||||
onPressed: () async => await viewModel.navigateToForgetPassword(),
|
||||
Widget _buildForgetPasswordTextButton() => TextButton(
|
||||
onPressed: () {},
|
||||
child: _buildForgetPasswordText(),
|
||||
);
|
||||
|
||||
Widget _buildForgetPasswordText() => Text(
|
||||
Widget _buildForgetPasswordText() => const Text(
|
||||
'Forget Password?',
|
||||
style: style14P400,
|
||||
style: TextStyle(color: kcPrimaryColor),
|
||||
);
|
||||
|
||||
Widget _buildLowerColumn(LoginViewModel viewModel) => Column(
|
||||
|
|
@ -198,15 +164,13 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
|||
|
||||
List<Widget> _buildLowerColumnChildren(LoginViewModel viewModel) => [
|
||||
_buildContinueButton(viewModel),
|
||||
_buildLoginWithGoogleButton(viewModel),
|
||||
_buildOptionTextDivider(),
|
||||
_buildLoginWithPhoneButton(viewModel),
|
||||
_buildLoginWithEmailButton(viewModel),
|
||||
verticalSpaceMedium
|
||||
];
|
||||
|
||||
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
|
||||
height: 55,
|
||||
safe: false,
|
||||
text: 'Continue',
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
|
|
@ -220,40 +184,17 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
|||
: kcPrimaryColor.withOpacity(0.1),
|
||||
);
|
||||
|
||||
Widget _buildLoginWithGoogleButton(LoginViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
backgroundColor: kcWhite,
|
||||
text: 'Login with Google',
|
||||
borderColor: kcPrimaryColor,
|
||||
foregroundColor: kcPrimaryColor,
|
||||
leadingImage: 'assets/icons/google.png',
|
||||
onTap: () async => await viewModel.signInWithGoogle(),
|
||||
);
|
||||
|
||||
Widget _buildOptionTextDivider() => const OptionTextDivider();
|
||||
|
||||
Widget _buildLoginWithPhoneButton(LoginViewModel viewModel) =>
|
||||
Widget _buildLoginWithEmailButton(LoginViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
backgroundColor: kcWhite,
|
||||
leadingIcon: Icons.phone,
|
||||
borderColor: kcPrimaryColor,
|
||||
onTap: () => viewModel.goTo(1),
|
||||
foregroundColor: kcPrimaryColor,
|
||||
text: 'Login with Phone Number',
|
||||
onTap: () => viewModel.goTo(1),
|
||||
);
|
||||
|
||||
|
||||
Widget _buildLoginWithEmailState(LoginViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.loginWithEmail)
|
||||
? const PageLoadingIndicator()
|
||||
: Container();
|
||||
|
||||
Widget _buildLoginWithGoogleState(LoginViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.loginWithGoogle)
|
||||
? const PageLoadingIndicator()
|
||||
: Container();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import 'package:yimaru_app/ui/widgets/register_for_account.dart';
|
|||
import '../../../common/app_colors.dart';
|
||||
import '../../../common/ui_helpers.dart';
|
||||
import '../../../widgets/custom_elevated_button.dart';
|
||||
import '../../../widgets/large_app_bar.dart';
|
||||
import '../../../widgets/phone_number_prefix.dart';
|
||||
import '../login_view.form.dart';
|
||||
|
||||
|
|
@ -18,75 +17,34 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
|
|||
const LoginWithPhoneNumberScreen(
|
||||
{super.key, required this.phoneNumberController});
|
||||
|
||||
Widget getPadding(context){
|
||||
double half = screenHeight(context)/2;
|
||||
return SizedBox(height: half + 175 - half,);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, LoginViewModel viewModel) =>
|
||||
_buildBody(viewModel);
|
||||
|
||||
_buildScaffoldWrapper(context: context,viewModel: viewModel);
|
||||
|
||||
|
||||
|
||||
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(context: context,viewModel: viewModel),
|
||||
);
|
||||
|
||||
|
||||
|
||||
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
|
||||
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 _buildBody(LoginViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyChildren(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)];
|
||||
|
||||
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
|
||||
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
|
||||
|
||||
Widget _buildColumnScroller(LoginViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildUpperColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildUpperColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildUpperColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
_buildSubtitleWrapper(viewModel),
|
||||
_buildSubTitleWrapper(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildSubtitle(),
|
||||
verticalSpaceMedium,
|
||||
|
|
@ -99,18 +57,22 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
|
|||
_buildPhoneNumberValidatorWrapper(viewModel),
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Welcome Back',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
|
||||
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
|
||||
onTap: () async => await viewModel.navigateToRegister(),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubtitle() => const Text(
|
||||
'Enter your phone number. We will send you a confirmation code there',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import 'onboarding_viewmodel.dart';
|
|||
import 'onboarding_view.form.dart';
|
||||
|
||||
@FormView(fields: [
|
||||
FormTextField(name: 'answer', validator: FormValidator.validateForm),
|
||||
FormTextField(name: 'fullName', validator: FormValidator.validateForm),
|
||||
FormTextField(name: 'challenge', validator: FormValidator.validateForm),
|
||||
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
|
||||
|
|
@ -29,55 +30,13 @@ class OnboardingView extends StackedView<OnboardingViewModel>
|
|||
with $OnboardingView {
|
||||
const OnboardingView({Key? key}) : super(key: key);
|
||||
|
||||
void _initClearData() {
|
||||
topicController.clear();
|
||||
fullNameController.clear();
|
||||
challengeController.clear();
|
||||
occupationController.clear();
|
||||
languageGoalController.clear();
|
||||
}
|
||||
|
||||
void _clearDataOnNavigation(OnboardingViewModel viewModel) {
|
||||
if (viewModel.currentPage == 0) {
|
||||
fullNameController.clear();
|
||||
viewModel.resetFullNameFormScreen();
|
||||
} else if (viewModel.currentPage == 1) {
|
||||
viewModel.resetGenderFormScreen();
|
||||
} else if (viewModel.currentPage == 2) {
|
||||
viewModel.resetBirthdayFormScreen();
|
||||
} else if (viewModel.currentPage == 3) {
|
||||
viewModel.resetAgeGroupFormScreen();
|
||||
} else if (viewModel.currentPage == 4) {
|
||||
viewModel.resetEducationalBackgroundFormScreen();
|
||||
} else if (viewModel.currentPage == 5) {
|
||||
occupationController.clear();
|
||||
viewModel.resetOccupationFormScreen();
|
||||
} else if (viewModel.currentPage == 6) {
|
||||
viewModel.resetCountryRegionFormScreen();
|
||||
} else if (viewModel.currentPage == 7) {
|
||||
viewModel.resetLearningGoalFormScreen();
|
||||
} else if (viewModel.currentPage == 8) {
|
||||
languageGoalController.clear();
|
||||
viewModel.resetLanguageGoalFormScreen();
|
||||
} else if (viewModel.currentPage == 9) {
|
||||
challengeController.clear();
|
||||
viewModel.resetChallengeFormScreen();
|
||||
} else if (viewModel.currentPage == 10) {
|
||||
topicController.clear();
|
||||
viewModel.resetTopicFormScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
{
|
||||
_clearDataOnNavigation(viewModel);
|
||||
viewModel.goBack();
|
||||
}
|
||||
void _initFormFields() {
|
||||
answerController.text = 'Book';
|
||||
}
|
||||
|
||||
@override
|
||||
void onViewModelReady(OnboardingViewModel viewModel) {
|
||||
_initClearData();
|
||||
_initFormFields();
|
||||
syncFormWithViewModel(viewModel);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
|
@ -98,8 +57,8 @@ class OnboardingView extends StackedView<OnboardingViewModel>
|
|||
|
||||
Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) =>
|
||||
PopScope(
|
||||
canPop: viewModel.currentPage == 0 ? true : false,
|
||||
onPopInvokedWithResult: (value, data) => _pop(viewModel),
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (value, data) => viewModel.pop(),
|
||||
child: _buildOnboardingScreens(viewModel));
|
||||
|
||||
Widget _buildOnboardingScreens(OnboardingViewModel viewModel) => IndexedStack(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart';
|
|||
|
||||
const bool _autoTextFieldValidation = true;
|
||||
|
||||
const String AnswerValueKey = 'answer';
|
||||
const String FullNameValueKey = 'fullName';
|
||||
const String ChallengeValueKey = 'challenge';
|
||||
const String OccupationValueKey = 'occupation';
|
||||
|
|
@ -24,6 +25,7 @@ final Map<String, TextEditingController> _OnboardingViewTextEditingControllers =
|
|||
final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
|
||||
|
||||
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
|
||||
AnswerValueKey: FormValidator.validateForm,
|
||||
FullNameValueKey: FormValidator.validateForm,
|
||||
ChallengeValueKey: FormValidator.validateForm,
|
||||
OccupationValueKey: FormValidator.validateForm,
|
||||
|
|
@ -32,6 +34,8 @@ final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
|
|||
};
|
||||
|
||||
mixin $OnboardingView {
|
||||
TextEditingController get answerController =>
|
||||
_getFormTextEditingController(AnswerValueKey);
|
||||
TextEditingController get fullNameController =>
|
||||
_getFormTextEditingController(FullNameValueKey);
|
||||
TextEditingController get challengeController =>
|
||||
|
|
@ -43,6 +47,7 @@ mixin $OnboardingView {
|
|||
TextEditingController get topicController =>
|
||||
_getFormTextEditingController(TopicValueKey);
|
||||
|
||||
FocusNode get answerFocusNode => _getFormFocusNode(AnswerValueKey);
|
||||
FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey);
|
||||
FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey);
|
||||
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
|
||||
|
|
@ -74,6 +79,7 @@ mixin $OnboardingView {
|
|||
/// Registers a listener on every generated controller that calls [model.setData()]
|
||||
/// with the latest textController values
|
||||
void syncFormWithViewModel(FormStateHelper model) {
|
||||
answerController.addListener(() => _updateFormData(model));
|
||||
fullNameController.addListener(() => _updateFormData(model));
|
||||
challengeController.addListener(() => _updateFormData(model));
|
||||
occupationController.addListener(() => _updateFormData(model));
|
||||
|
|
@ -90,6 +96,7 @@ mixin $OnboardingView {
|
|||
'This feature was deprecated after 3.1.0.',
|
||||
)
|
||||
void listenToFormUpdated(FormViewModel model) {
|
||||
answerController.addListener(() => _updateFormData(model));
|
||||
fullNameController.addListener(() => _updateFormData(model));
|
||||
challengeController.addListener(() => _updateFormData(model));
|
||||
occupationController.addListener(() => _updateFormData(model));
|
||||
|
|
@ -104,6 +111,7 @@ mixin $OnboardingView {
|
|||
model.setData(
|
||||
model.formValueMap
|
||||
..addAll({
|
||||
AnswerValueKey: answerController.text,
|
||||
FullNameValueKey: fullNameController.text,
|
||||
ChallengeValueKey: challengeController.text,
|
||||
OccupationValueKey: occupationController.text,
|
||||
|
|
@ -150,6 +158,7 @@ extension ValueProperties on FormStateHelper {
|
|||
return !hasAnyValidationMessage;
|
||||
}
|
||||
|
||||
String? get answerValue => this.formValueMap[AnswerValueKey] as String?;
|
||||
String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?;
|
||||
String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?;
|
||||
String? get occupationValue =>
|
||||
|
|
@ -158,6 +167,16 @@ extension ValueProperties on FormStateHelper {
|
|||
this.formValueMap[LanguageGoalValueKey] as String?;
|
||||
String? get topicValue => this.formValueMap[TopicValueKey] as String?;
|
||||
|
||||
set answerValue(String? value) {
|
||||
this.setData(
|
||||
this.formValueMap..addAll({AnswerValueKey: value}),
|
||||
);
|
||||
|
||||
if (_OnboardingViewTextEditingControllers.containsKey(AnswerValueKey)) {
|
||||
_OnboardingViewTextEditingControllers[AnswerValueKey]?.text = value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
set fullNameValue(String? value) {
|
||||
this.setData(
|
||||
this.formValueMap..addAll({FullNameValueKey: value}),
|
||||
|
|
@ -213,6 +232,9 @@ extension ValueProperties on FormStateHelper {
|
|||
}
|
||||
}
|
||||
|
||||
bool get hasAnswer =>
|
||||
this.formValueMap.containsKey(AnswerValueKey) &&
|
||||
(answerValue?.isNotEmpty ?? false);
|
||||
bool get hasFullName =>
|
||||
this.formValueMap.containsKey(FullNameValueKey) &&
|
||||
(fullNameValue?.isNotEmpty ?? false);
|
||||
|
|
@ -229,6 +251,8 @@ extension ValueProperties on FormStateHelper {
|
|||
this.formValueMap.containsKey(TopicValueKey) &&
|
||||
(topicValue?.isNotEmpty ?? false);
|
||||
|
||||
bool get hasAnswerValidationMessage =>
|
||||
this.fieldsValidationMessages[AnswerValueKey]?.isNotEmpty ?? false;
|
||||
bool get hasFullNameValidationMessage =>
|
||||
this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false;
|
||||
bool get hasChallengeValidationMessage =>
|
||||
|
|
@ -240,6 +264,8 @@ extension ValueProperties on FormStateHelper {
|
|||
bool get hasTopicValidationMessage =>
|
||||
this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false;
|
||||
|
||||
String? get answerValidationMessage =>
|
||||
this.fieldsValidationMessages[AnswerValueKey];
|
||||
String? get fullNameValidationMessage =>
|
||||
this.fieldsValidationMessages[FullNameValueKey];
|
||||
String? get challengeValidationMessage =>
|
||||
|
|
@ -253,6 +279,8 @@ extension ValueProperties on FormStateHelper {
|
|||
}
|
||||
|
||||
extension Methods on FormStateHelper {
|
||||
setAnswerValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[AnswerValueKey] = validationMessage;
|
||||
setFullNameValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[FullNameValueKey] = validationMessage;
|
||||
setChallengeValidationMessage(String? validationMessage) =>
|
||||
|
|
@ -266,6 +294,7 @@ extension Methods on FormStateHelper {
|
|||
|
||||
/// Clears text input fields on the Form
|
||||
void clearForm() {
|
||||
answerValue = '';
|
||||
fullNameValue = '';
|
||||
challengeValue = '';
|
||||
occupationValue = '';
|
||||
|
|
@ -276,6 +305,7 @@ extension Methods on FormStateHelper {
|
|||
/// Validates text input fields on the Form
|
||||
void validateForm() {
|
||||
this.setValidationMessages({
|
||||
AnswerValueKey: getValidationMessage(AnswerValueKey),
|
||||
FullNameValueKey: getValidationMessage(FullNameValueKey),
|
||||
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
|
||||
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
||||
|
|
@ -300,6 +330,7 @@ String? getValidationMessage(String key) {
|
|||
/// Updates the fieldsValidationMessages on the FormViewModel
|
||||
void updateValidationData(FormStateHelper model) =>
|
||||
model.setValidationMessages({
|
||||
AnswerValueKey: getValidationMessage(AnswerValueKey),
|
||||
FullNameValueKey: getValidationMessage(FullNameValueKey),
|
||||
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
|
||||
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
||||
|
|
|
|||
|
|
@ -54,13 +54,10 @@ class OnboardingViewModel extends FormViewModel {
|
|||
|
||||
// Age group
|
||||
final List<String> _ageGroups = [
|
||||
'UNDER_13',
|
||||
'13_17',
|
||||
'18_24',
|
||||
'25_34',
|
||||
'35_44',
|
||||
'45_54',
|
||||
'55_PLUS'
|
||||
'8-14',
|
||||
'15-18',
|
||||
'19-26',
|
||||
'26+',
|
||||
];
|
||||
|
||||
List<String> get ageGroups => _ageGroups;
|
||||
|
|
@ -79,11 +76,30 @@ class OnboardingViewModel extends FormViewModel {
|
|||
|
||||
String get selectedCountry => _selectedCountry;
|
||||
|
||||
Future<List<String>> getCountries() async => ['Ethiopia'];
|
||||
|
||||
// Country
|
||||
String _selectedRegion = 'Addis Ababa';
|
||||
|
||||
String get selectedRegion => _selectedRegion;
|
||||
|
||||
Future<List<String>> getRegions(String country) async => [
|
||||
'Afar',
|
||||
'SNNPR',
|
||||
'Amhara',
|
||||
'Harari',
|
||||
'Oromia',
|
||||
'Sidama',
|
||||
'Somali',
|
||||
'Tigray',
|
||||
'Gambela',
|
||||
'Dire Dawa',
|
||||
'Addis Ababa',
|
||||
'Central Ethiopia',
|
||||
'Benishangul-Gumuz',
|
||||
'South West Ethiopia',
|
||||
];
|
||||
|
||||
// Learning goal
|
||||
String? _selectedLearningGoal;
|
||||
|
||||
|
|
@ -242,42 +258,12 @@ class OnboardingViewModel extends FormViewModel {
|
|||
}
|
||||
|
||||
// Country
|
||||
List<String> getCountries() => ['Ethiopia', 'Other'];
|
||||
|
||||
void setSelectedCountry(String value) {
|
||||
_selectedCountry = value;
|
||||
if (selectedCountry != 'Ethiopia') {
|
||||
_selectedRegion = 'Other';
|
||||
} else {
|
||||
_selectedRegion = 'Addis Ababa';
|
||||
}
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Region
|
||||
List<String> getRegions(String country) {
|
||||
if (country == 'Ethiopia') {
|
||||
return [
|
||||
'Afar',
|
||||
'SNNPR',
|
||||
'Amhara',
|
||||
'Harari',
|
||||
'Oromia',
|
||||
'Sidama',
|
||||
'Somali',
|
||||
'Tigray',
|
||||
'Gambela',
|
||||
'Dire Dawa',
|
||||
'Addis Ababa',
|
||||
'Central Ethiopia',
|
||||
'Benishangul-Gumuz',
|
||||
'South West Ethiopia',
|
||||
];
|
||||
} else {
|
||||
return ['Other'];
|
||||
}
|
||||
}
|
||||
|
||||
void setSelectedRegion(String value) {
|
||||
_selectedRegion = value;
|
||||
rebuildUi();
|
||||
|
|
@ -366,89 +352,24 @@ class OnboardingViewModel extends FormViewModel {
|
|||
// Add user data
|
||||
void addUserData(Map<String, dynamic> data) {
|
||||
_userData.addAll(data);
|
||||
print('User data : $_userData');
|
||||
}
|
||||
|
||||
void clearUserData() {
|
||||
_userData.clear();
|
||||
}
|
||||
|
||||
// Form reset
|
||||
// Navigation
|
||||
|
||||
// Reset full name form screen
|
||||
void resetFullNameFormScreen() {
|
||||
_focusFullName = false;
|
||||
rebuildUi();
|
||||
}
|
||||
Future<void> navigateToLanguage() async =>
|
||||
await _navigationService.navigateToLanguageView();
|
||||
|
||||
// Reset gender form screen
|
||||
void resetGenderFormScreen() {
|
||||
_selectedGender = null;
|
||||
rebuildUi();
|
||||
}
|
||||
Future<void> navigateToAssessment() async =>
|
||||
await _navigationService.navigateToAssessmentView(data: _userData);
|
||||
|
||||
// Reset birthday form screen
|
||||
void resetBirthdayFormScreen() {
|
||||
_selectedBirthday = null;
|
||||
rebuildUi();
|
||||
}
|
||||
Future<void> replaceWithHome() async =>
|
||||
await _navigationService.clearStackAndShowView(const HomeView());
|
||||
|
||||
// Reset age group form screen
|
||||
void resetAgeGroupFormScreen() {
|
||||
_selectedAgeGroup = null;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset educational background form screen
|
||||
void resetEducationalBackgroundFormScreen() {
|
||||
_selectedEducationalBackground = null;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset occupation form screen
|
||||
void resetOccupationFormScreen() {
|
||||
_focusOccupation = false;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset country region form screen
|
||||
void resetCountryRegionFormScreen() {
|
||||
_selectedCountry = 'Ethiopia';
|
||||
_selectedRegion = 'Addis Ababa';
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset learning goal form screen
|
||||
void resetLearningGoalFormScreen() {
|
||||
_selectedLearningGoal = null;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset language goal form screen
|
||||
void resetLanguageGoalFormScreen() {
|
||||
_focusLanguageGoal = false;
|
||||
_selectedLanguageGoal = null;
|
||||
_showLanguageGoalTextBox = false;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset challenge form screen
|
||||
void resetChallengeFormScreen() {
|
||||
_focusChallenge = false;
|
||||
_selectedChallenge = null;
|
||||
_showChallengeTextBox = false;
|
||||
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// Reset topic form screen
|
||||
void resetTopicFormScreen() {
|
||||
_focusTopic = false;
|
||||
_selectedTopic = null;
|
||||
_showTopicTextBox = false;
|
||||
rebuildUi();
|
||||
}
|
||||
|
||||
// In-app navigation
|
||||
void next({int? page}) async {
|
||||
if (page == null) {
|
||||
if (_previousPage != 0) {
|
||||
|
|
@ -463,8 +384,8 @@ class OnboardingViewModel extends FormViewModel {
|
|||
rebuildUi();
|
||||
}
|
||||
|
||||
void goBack() {
|
||||
if (_currentPage == 0) {
|
||||
void pop() {
|
||||
if (_currentPage == 8) {
|
||||
_navigationService.back();
|
||||
} else {
|
||||
_currentPage--;
|
||||
|
|
@ -472,14 +393,4 @@ class OnboardingViewModel extends FormViewModel {
|
|||
rebuildUi();
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
Future<void> navigateToLanguage() async =>
|
||||
await _navigationService.navigateToLanguageView();
|
||||
|
||||
Future<void> navigateToAssessment() async =>
|
||||
await _navigationService.navigateToAssessmentView(data: _userData);
|
||||
|
||||
Future<void> replaceWithHome() async =>
|
||||
await _navigationService.clearStackAndShowView(const HomeView());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
|||
class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||
const AgeGroupFormScreen({super.key});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
viewModel.resetAgeGroupFormScreen();
|
||||
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -80,20 +74,24 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
];
|
||||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Which age range are you in?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubTitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'We’ll personalize your learning experience based on your age.',
|
||||
style: style14DG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(
|
||||
|
|
|
|||
|
|
@ -12,16 +12,14 @@ import '../../../widgets/birthday_selector.dart';
|
|||
class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||
const BirthdayFormScreen({super.key});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
viewModel.resetBirthdayFormScreen();
|
||||
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
Map<String, dynamic> data = {'birth_day': viewModel.selectedBirthday};
|
||||
Map<String, dynamic> data = {
|
||||
'birth_day': DateFormat('yyyy-MM-dd')
|
||||
.parseUTC(viewModel.selectedBirthday ?? DateTime.now().toString())
|
||||
.toIso8601String()
|
||||
};
|
||||
viewModel.addUserData(data);
|
||||
viewModel.next();
|
||||
}
|
||||
|
|
@ -70,26 +68,30 @@ class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildBirthdayFormField(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Pick your birthday?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'We’ll personalize your learning experience based on your birthday.',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildBirthdayFormField(OnboardingViewModel viewModel) =>
|
||||
|
|
|
|||
|
|
@ -13,12 +13,6 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
const ChallengeFormScreen({super.key, required this.challengeController});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
challengeController.clear();
|
||||
viewModel.resetChallengeFormScreen();
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -80,7 +74,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildChallenges(viewModel),
|
||||
if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel),
|
||||
|
|
@ -96,20 +90,24 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
];
|
||||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'What challenge do you face most with English?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'Everyone has struggles, let’s start fixing yours 😊',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder(
|
||||
|
|
@ -153,7 +151,11 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text(
|
||||
viewModel.challengeValidationMessage!,
|
||||
style: style12R700,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
||||
|
|
|
|||
|
|
@ -10,11 +10,6 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
|||
class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||
const CountryRegionFormScreen({super.key});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
viewModel.resetCountryRegionFormScreen();
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -76,7 +71,7 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildCountryDropDown(viewModel),
|
||||
verticalSpaceMedium,
|
||||
|
|
@ -85,27 +80,31 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
];
|
||||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Where are you from?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'Select your country and region from the dropdown',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildCountryDropDown(OnboardingViewModel viewModel) =>
|
||||
CustomDropdownPicker(
|
||||
hint: 'Select country',
|
||||
icon: _buildSearchIcon(),
|
||||
selectedItem: viewModel.selectedCountry,
|
||||
selectedItem: 'Ethiopia',
|
||||
items: (value, props) => viewModel.getCountries(),
|
||||
onChanged: (value) =>
|
||||
viewModel.setSelectedCountry(value ?? 'Ethiopia'));
|
||||
|
|
@ -114,11 +113,10 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
CustomDropdownPicker(
|
||||
hint: 'Select region',
|
||||
icon: _buildSearchIcon(),
|
||||
selectedItem: viewModel.selectedRegion,
|
||||
items: (value, props) =>
|
||||
viewModel.getRegions(viewModel.selectedCountry),
|
||||
selectedItem: 'Addis Ababa',
|
||||
onChanged: (value) =>
|
||||
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
|
||||
items: (value, props) => viewModel.getRegions('Addis Ababa'),
|
||||
);
|
||||
|
||||
Icon _buildSearchIcon() => const Icon(
|
||||
|
|
|
|||
|
|
@ -11,11 +11,6 @@ class EducationalBackgroundFormScreen
|
|||
extends ViewModelWidget<OnboardingViewModel> {
|
||||
const EducationalBackgroundFormScreen({super.key});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
viewModel.resetEducationalBackgroundFormScreen();
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -82,9 +77,9 @@ class EducationalBackgroundFormScreen
|
|||
];
|
||||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceLarge,
|
||||
_buildFullNameFormField(viewModel),
|
||||
if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName)
|
||||
|
|
@ -96,7 +96,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => const Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'We’ll use your name to personalize your learning journey.',
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,11 +10,6 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
|||
class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||
const GenderFormScreen({super.key});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
viewModel.resetGenderFormScreen();
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -68,26 +63,30 @@ class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildAgeGroups(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Choose your gender?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'We’ll personalize your learning experience based on your gender.',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(
|
||||
|
|
|
|||
|
|
@ -14,12 +14,6 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
const LanguageGoalFormScreen(
|
||||
{super.key, required this.languageGoalController});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
languageGoalController.clear();
|
||||
viewModel.resetLanguageGoalFormScreen();
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -81,7 +75,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildReasons(viewModel),
|
||||
if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel),
|
||||
|
|
@ -97,20 +91,26 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
];
|
||||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'What’s your main goal for improving your English?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'Your goal helps us tailor your learning journey.',
|
||||
style: style14MG400,
|
||||
style: TextStyle(
|
||||
color: kcMediumGrey,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder(
|
||||
|
|
@ -154,7 +154,11 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text(
|
||||
viewModel.languageGoalValidationMessage!,
|
||||
style: style12R700,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
||||
|
|
|
|||
|
|
@ -23,11 +23,6 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
return Icons.book;
|
||||
}
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
viewModel.resetLearningGoalFormScreen();
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -92,22 +87,19 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
];
|
||||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
onPop: viewModel.pop,
|
||||
showBackButton: true,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle(OnboardingViewModel viewModel) => Text.rich(
|
||||
TextSpan(
|
||||
text: 'Hi ${viewModel.userData['first_name']},',
|
||||
style: style18P600.copyWith(fontSize: 22),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' Choose your learning goal.',
|
||||
style: style16DG600.copyWith(fontSize: 22),
|
||||
)
|
||||
]),
|
||||
Widget _buildTitle(OnboardingViewModel viewModel) => Text(
|
||||
'Hi ${viewModel.userData['first_name']}, Choose your learning goal.',
|
||||
style: const TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildLearningGoals(OnboardingViewModel viewModel) => ListView.builder(
|
||||
|
|
|
|||
|
|
@ -13,12 +13,6 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
const OccupationFormScreen({super.key, required this.occupationController});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
occupationController.clear();
|
||||
viewModel.resetOccupationFormScreen();
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
|
|
@ -77,7 +71,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceLarge,
|
||||
_buildOccupationFormField(viewModel),
|
||||
if (viewModel.hasOccupationValidationMessage &&
|
||||
|
|
@ -90,19 +84,23 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.pop,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'What’s your occupation?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'We’ll personalize your learning experience based on your occupation.',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildOccupationFormField(OnboardingViewModel viewModel) =>
|
||||
|
|
@ -122,7 +120,11 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text(
|
||||
viewModel.occupationValidationMessage!,
|
||||
style: style12R700,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
||||
|
|
|
|||
|
|
@ -13,18 +13,10 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
const TopicFormScreen({super.key, required this.topicController});
|
||||
|
||||
void _pop(OnboardingViewModel viewModel) {
|
||||
topicController.clear();
|
||||
viewModel.resetTopicFormScreen();
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
Future<void> _next(OnboardingViewModel viewModel) async {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
Map<String, dynamic> data = {
|
||||
'profile_completed': true,
|
||||
'preferred_language': 'en',
|
||||
'favoutite_topic': viewModel.selectedTopic ?? topicController.text,
|
||||
};
|
||||
viewModel.addUserData(data);
|
||||
|
|
@ -51,8 +43,8 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.pop,
|
||||
showLanguageSelection: true,
|
||||
onPop: () => _pop(viewModel),
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
|
|
@ -88,7 +80,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
_buildSubTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildTopics(viewModel),
|
||||
if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel),
|
||||
|
|
@ -103,14 +95,18 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
verticalSpaceMedium,
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
Widget _buildTitle() => const Text(
|
||||
'Which topics interest you most?',
|
||||
style: style25DG600,
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
Widget _buildSubTitle() => const Text(
|
||||
'Your favorite topics help us create fun, relatable lessons.',
|
||||
style: style14MG400,
|
||||
style: TextStyle(color: kcMediumGrey),
|
||||
);
|
||||
|
||||
Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder(
|
||||
|
|
@ -152,7 +148,11 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
|
||||
Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text(
|
||||
viewModel.topicValidationMessage!,
|
||||
style: style12R700,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
|
||||
|
|
|
|||
|
|
@ -1,36 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||
import 'package:yimaru_app/ui/widgets/profile_card.dart';
|
||||
import 'package:yimaru_app/ui/widgets/profile_image.dart';
|
||||
import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
|
||||
|
||||
import '../../widgets/custom_elevated_button.dart';
|
||||
import '../../widgets/image_picker_option.dart';
|
||||
import 'profile_viewmodel.dart';
|
||||
|
||||
class ProfileView extends StackedView<ProfileViewModel> {
|
||||
const ProfileView({Key? key}) : super(key: key);
|
||||
|
||||
Future<void> _showImagePicker(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) async =>
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
_showImagePickerDialog(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
AlertDialog _showImagePickerDialog(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
AlertDialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
content: _buildImagePicker(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
@override
|
||||
ProfileViewModel viewModelBuilder(
|
||||
BuildContext context,
|
||||
|
|
@ -43,45 +24,30 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
|||
ProfileViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper(context: context, viewModel: viewModel);
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
Scaffold(
|
||||
Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(context: context, viewModel: viewModel),
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
SafeArea(
|
||||
child: _buildBodyWrapper(context: context, viewModel: viewModel));
|
||||
Widget _buildScaffold(ProfileViewModel viewModel) =>
|
||||
SafeArea(child: _buildBodyWrapper(viewModel));
|
||||
|
||||
Widget _buildBodyWrapper(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildBody(context: context, viewModel: viewModel),
|
||||
Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView(
|
||||
child: _buildBody(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
Padding(
|
||||
Widget _buildBody(ProfileViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(context: context, viewModel: viewModel),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
Column(
|
||||
Widget _buildColumn(ProfileViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildNotificationIconWrapper(),
|
||||
_buildProfileSection(context: context, viewModel: viewModel),
|
||||
_buildProfileSection(),
|
||||
verticalSpaceSmall,
|
||||
_buildViewProfileButton(viewModel),
|
||||
verticalSpaceLarge,
|
||||
|
|
@ -100,46 +66,27 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
|||
color: kcDarkGrey,
|
||||
);
|
||||
|
||||
Widget _buildProfileSection(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
Column(
|
||||
Widget _buildProfileSection() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _buildProfileSectionChildren(
|
||||
context: context, viewModel: viewModel),
|
||||
children: _buildProfileSectionChildren(),
|
||||
);
|
||||
|
||||
List<Widget> _buildProfileSectionChildren(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
[
|
||||
_buildProfileImage(context: context, viewModel: viewModel),
|
||||
List<Widget> _buildProfileSectionChildren() => [
|
||||
_buildProfileImage(),
|
||||
verticalSpaceSmall,
|
||||
_buildProfileName(viewModel),
|
||||
_buildProfileName(),
|
||||
];
|
||||
|
||||
Widget _buildProfileImage(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
ProfileImage(
|
||||
profileImage: viewModel.user?.profilePicture,
|
||||
loading: viewModel.busy(StateObjects.profileImage) ? true : false,
|
||||
onTap: () async =>
|
||||
await _showImagePicker(context: context, viewModel: viewModel),
|
||||
);
|
||||
Widget _buildProfileImage() => const ProfileImage();
|
||||
|
||||
Widget _buildImagePicker(
|
||||
{required BuildContext context,
|
||||
required ProfileViewModel viewModel}) =>
|
||||
ImagePickerOption(
|
||||
onCameraTap: () async => await viewModel.openCamera(),
|
||||
onGalleryTap: () async => await viewModel.openGallery(),
|
||||
);
|
||||
|
||||
Widget _buildProfileName(ProfileViewModel viewModel) => Text(
|
||||
'Hi, ${viewModel.user?.firstName ?? ''} 👋',
|
||||
style: style25DG600,
|
||||
Widget _buildProfileName() => const Text(
|
||||
'Hi, Bisrat 👋',
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildViewProfileButton(ProfileViewModel viewModel) =>
|
||||
|
|
@ -164,28 +111,28 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
|||
Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard(
|
||||
icon: Icons.download,
|
||||
title: 'My Downloads',
|
||||
subtitle: 'Access offline lessons and saved videos',
|
||||
subTitle: 'Access offline lessons and saved videos',
|
||||
onTap: () async => await viewModel.navigateToDownloads(),
|
||||
);
|
||||
|
||||
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
|
||||
title: 'My Progress',
|
||||
icon: Icons.stacked_bar_chart,
|
||||
subtitle: 'Track your achievements and learning streak',
|
||||
subTitle: 'Track your achievements and learning streak',
|
||||
onTap: () async => await viewModel.navigateToProgress(),
|
||||
);
|
||||
|
||||
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
|
||||
title: 'Account & Privacy',
|
||||
icon: Icons.privacy_tip_outlined,
|
||||
subtitle: 'Manage setting and app preference',
|
||||
subTitle: 'Manage setting and app preference',
|
||||
onTap: () async => await viewModel.navigateToAccountPrivacy(),
|
||||
);
|
||||
|
||||
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
|
||||
title: 'Support',
|
||||
icon: Icons.headphones,
|
||||
subtitle: 'Get help through phone or Telegram',
|
||||
subTitle: 'Get help through phone or Telegram',
|
||||
onTap: () async => await viewModel.navigateToSupport(),
|
||||
);
|
||||
|
||||
|
|
@ -194,7 +141,7 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
|||
text: 'Log Out',
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcRed,
|
||||
backgroundColor: kcRed.withOpacity(0.25),
|
||||
onTap: () async => await viewModel.logOut(),
|
||||
backgroundColor: kcRed.withOpacity(0.25),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +1,20 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/app/app.router.dart';
|
||||
import 'package:yimaru_app/services/image_picker_service.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../models/user_model.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
|
||||
class ProfileViewModel extends ReactiveViewModel {
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _dialogService = locator<DialogService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
class ProfileViewModel extends BaseViewModel {
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
final _imagePickerService = locator<ImagePickerService>();
|
||||
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
|
||||
// Current user
|
||||
UserModel? get _user => _authenticationService.user;
|
||||
|
||||
UserModel? get user => _user;
|
||||
|
||||
// Image picker
|
||||
Future<void> openCamera() async =>
|
||||
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
|
||||
|
||||
Future<void> _openCamera() async {
|
||||
String? image = await _imagePickerService.camera();
|
||||
pop();
|
||||
if (image != null) {
|
||||
await updateProfilePicture(image);
|
||||
await _authenticationService.saveProfileImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openGallery() async =>
|
||||
runBusyFuture(_openGallery(), busyObject: StateObjects.profileImage);
|
||||
|
||||
Future<void> _openGallery() async {
|
||||
String? image = await _imagePickerService.gallery();
|
||||
pop();
|
||||
if (image != null) {
|
||||
await updateProfilePicture(image);
|
||||
await _authenticationService.saveProfileImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
// Logout
|
||||
Future<void> _logOut() async {
|
||||
Future<void> logOut() async {
|
||||
await _authenticationService.logOut();
|
||||
await _navigationService.replaceWithLoginView();
|
||||
}
|
||||
|
||||
// Dialog
|
||||
Future<bool?> showAbortDialog() async {
|
||||
DialogResponse? response = await _dialogService.showDialog(
|
||||
title: 'Logout',
|
||||
cancelTitle: 'No',
|
||||
buttonTitle: 'Yes',
|
||||
barrierDismissible: true,
|
||||
cancelTitleColor: kcDarkGrey,
|
||||
buttonTitleColor: kcPrimaryColor,
|
||||
description: 'Are you sure you want to quit?',
|
||||
);
|
||||
return response?.confirmed;
|
||||
}
|
||||
|
||||
Future<void> logOut() async {
|
||||
bool? response = await showAbortDialog();
|
||||
if (response != null && response) {
|
||||
await _logOut();
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToProfileDetail() async =>
|
||||
await _navigationService.navigateToProfileDetailView();
|
||||
|
||||
|
|
@ -102,19 +29,4 @@ class ProfileViewModel extends ReactiveViewModel {
|
|||
|
||||
Future<void> navigateToSupport() async =>
|
||||
await _navigationService.navigateToSupportView();
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Update profile
|
||||
Future<void> updateProfilePicture(String image) async =>
|
||||
await runBusyFuture(_updateProfilePicture(image));
|
||||
|
||||
Future<void> _updateProfilePicture(String image) async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> data = {
|
||||
'profile_picture_url': image,
|
||||
};
|
||||
await _apiService.updateProfileImage(data: data, userId: _user?.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,10 @@ import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
|
|||
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../common/validators/form_validator.dart';
|
||||
import '../../widgets/custom_dropdown.dart';
|
||||
import '../../widgets/custom_elevated_button.dart';
|
||||
import '../../widgets/image_picker_option.dart';
|
||||
import '../../widgets/page_loading_indicator.dart';
|
||||
import '../../widgets/profile_image.dart';
|
||||
import 'profile_detail_viewmodel.dart';
|
||||
|
||||
|
|
@ -26,62 +23,21 @@ import 'profile_detail_view.form.dart';
|
|||
name: 'phoneNumber', validator: FormValidator.validatePhoneNumber),
|
||||
FormTextField(name: 'lastName', validator: FormValidator.validateForm),
|
||||
FormTextField(name: 'firstName', validator: FormValidator.validateForm),
|
||||
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
|
||||
])
|
||||
class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||
with $ProfileDetailView {
|
||||
const ProfileDetailView({Key? key}) : super(key: key);
|
||||
|
||||
Future<void> _update(ProfileDetailViewModel viewModel) async {
|
||||
Map<String, dynamic> data = {
|
||||
'region': viewModel.selectedRegion,
|
||||
'gender': viewModel.selectedGender,
|
||||
'last_name': lastNameController.text,
|
||||
'country': viewModel.selectedCountry,
|
||||
'first_name': firstNameController.text,
|
||||
'occupation': occupationController.text,
|
||||
'birth_day': viewModel.selectedBirthday,
|
||||
};
|
||||
|
||||
viewModel.addUserData(data);
|
||||
|
||||
await viewModel.updateProfile();
|
||||
}
|
||||
|
||||
Future<void> _showImagePicker(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) async =>
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
_showImagePickerDialog(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
AlertDialog _showImagePickerDialog(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
AlertDialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
content: _buildImagePicker(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
void _onModelReady(ProfileDetailViewModel viewModel) {
|
||||
void _onModelReady() {
|
||||
firstNameController.text = 'Abel';
|
||||
lastNameController.text = 'Abebe';
|
||||
phoneNumberController.text = '251900000000';
|
||||
emailController.text = viewModel.user?.email ?? '';
|
||||
lastNameController.text = viewModel.user?.lastName ?? '';
|
||||
firstNameController.text = viewModel.user?.firstName ?? '';
|
||||
occupationController.text = viewModel.user?.occupation ?? '';
|
||||
viewModel.clearUserData();
|
||||
viewModel.setGender(viewModel.user?.gender ?? '');
|
||||
viewModel.setSelectedCountry(viewModel.user?.country ?? 'Ethiopia');
|
||||
viewModel.setSelectedRegion(viewModel.user?.region ?? 'Addis Ababa');
|
||||
viewModel.setBirthday(viewModel.user?.birthday ??
|
||||
DateFormat('d MMM, yyyy').format(DateTime.now()));
|
||||
emailController.text = 'email@test.com';
|
||||
}
|
||||
|
||||
@override
|
||||
void onViewModelReady(ProfileDetailViewModel viewModel) {
|
||||
_onModelReady(viewModel);
|
||||
_onModelReady();
|
||||
syncFormWithViewModel(viewModel);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
|
@ -96,55 +52,32 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
ProfileDetailViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper(context: context, viewModel: viewModel);
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
Scaffold(
|
||||
Widget _buildScaffoldWrapper(ProfileDetailViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldStack(context: context, viewModel: viewModel),
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldStack(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
Stack(children: [
|
||||
_buildScaffold(context: context, viewModel: viewModel),
|
||||
_buildState(viewModel)
|
||||
]);
|
||||
Widget _buildScaffold(ProfileDetailViewModel viewModel) =>
|
||||
SafeArea(child: _buildBodyWrapper(viewModel));
|
||||
|
||||
Widget _buildScaffold(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
SafeArea(
|
||||
child: _buildBodyWrapper(context: context, viewModel: viewModel));
|
||||
|
||||
Widget _buildBodyWrapper(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
Padding(
|
||||
Widget _buildBodyWrapper(ProfileDetailViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildBody(context: context, viewModel: viewModel),
|
||||
child: _buildBody(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
Column(
|
||||
Widget _buildBody(ProfileDetailViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildBodyChildren(context: context, viewModel: viewModel),
|
||||
children: _buildBodyChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildBodyChildren(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
[
|
||||
List<Widget> _buildBodyChildren(ProfileDetailViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
_buildAppbar(viewModel),
|
||||
verticalSpaceSmall,
|
||||
_buildColumnWrapper(context: context, viewModel: viewModel)
|
||||
_buildColumnWrapper(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
|
||||
|
|
@ -152,33 +85,23 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
onTap: viewModel.pop,
|
||||
);
|
||||
|
||||
Widget _buildColumnWrapper(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
Expanded(child: _buildBodyColumn(context: context, viewModel: viewModel));
|
||||
Widget _buildColumnWrapper(ProfileDetailViewModel viewModel) =>
|
||||
Expanded(child: _buildBodyColumn(viewModel));
|
||||
|
||||
Widget _buildBodyColumn(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
Widget _buildBodyColumn(ProfileDetailViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildColumn(context: context, viewModel: viewModel),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
Column(
|
||||
Widget _buildColumn(ProfileDetailViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildColumnChildren(context: context, viewModel: viewModel),
|
||||
children: _buildColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildColumnChildren(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
[
|
||||
List<Widget> _buildColumnChildren(ProfileDetailViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
_buildProfileImageWrapper(context: context, viewModel: viewModel),
|
||||
_buildProfileImage(),
|
||||
verticalSpaceMedium,
|
||||
_buildNameFormSection(viewModel),
|
||||
verticalSpaceMedium,
|
||||
|
|
@ -197,30 +120,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
_buildLowerColumn(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildProfileImageWrapper(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: _buildProfileImage(context: context, viewModel: viewModel));
|
||||
|
||||
Widget _buildProfileImage(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
ProfileImage(
|
||||
profileImage: viewModel.user?.profilePicture,
|
||||
loading: viewModel.busy(StateObjects.profileImage) ? true : false,
|
||||
onTap: () async =>
|
||||
await _showImagePicker(context: context, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildImagePicker(
|
||||
{required BuildContext context,
|
||||
required ProfileDetailViewModel viewModel}) =>
|
||||
ImagePickerOption(
|
||||
onCameraTap: () async => await viewModel.openCamera(),
|
||||
onGalleryTap: () async => await viewModel.openGallery(),
|
||||
);
|
||||
Widget _buildProfileImage() =>
|
||||
const Align(alignment: Alignment.center, child: ProfileImage());
|
||||
|
||||
Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -467,7 +368,6 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
|
||||
TextFormField(
|
||||
maxLength: 12,
|
||||
enabled: false,
|
||||
keyboardType: TextInputType.phone,
|
||||
controller: phoneNumberController,
|
||||
onTap: viewModel.setPhoneNumberFocus,
|
||||
|
|
@ -513,7 +413,6 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
|
||||
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
|
||||
TextFormField(
|
||||
enabled: false,
|
||||
controller: emailController,
|
||||
onTap: viewModel.setPhoneNumberFocus,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
|
|
@ -572,10 +471,10 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
|
||||
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
|
||||
CustomDropdownPicker(
|
||||
onChanged: (value) {},
|
||||
hint: 'Select country',
|
||||
selectedItem: viewModel.selectedCountry,
|
||||
selectedItem: 'Ethiopia',
|
||||
items: (value, props) => viewModel.getCountries(),
|
||||
onChanged: (value) => viewModel.setSelectedCountry(value ?? 'Ethiopia'),
|
||||
);
|
||||
|
||||
Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
|
||||
|
|
@ -605,11 +504,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) =>
|
||||
CustomDropdownPicker(
|
||||
hint: 'Select region',
|
||||
selectedItem: viewModel.selectedRegion,
|
||||
items: (value, props) =>
|
||||
viewModel.getRegions(viewModel.selectedCountry),
|
||||
onChanged: (value) =>
|
||||
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
|
||||
onChanged: (value) {},
|
||||
selectedItem: 'Addis Ababa',
|
||||
items: (value, props) => viewModel.getRegions('Addis Ababa'),
|
||||
);
|
||||
|
||||
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
|
||||
|
|
@ -625,13 +522,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
[
|
||||
_buildOccupationDropdownLabel(),
|
||||
verticalSpaceSmall,
|
||||
_buildOccupationFormField(viewModel),
|
||||
if (viewModel.hasOccupationValidationMessage &&
|
||||
viewModel.focusOccupation)
|
||||
verticalSpaceTiny,
|
||||
if (viewModel.hasOccupationValidationMessage &&
|
||||
viewModel.focusOccupation)
|
||||
_buildOccupationValidatorWrapper(viewModel)
|
||||
_buildOccupationDropdown(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
|
||||
|
|
@ -639,29 +530,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildOccupationFormField(ProfileDetailViewModel viewModel) =>
|
||||
TextFormField(
|
||||
controller: occupationController,
|
||||
onTap: viewModel.setOccupationFocus,
|
||||
decoration: inputDecoration(
|
||||
hint: 'Enter Your Occupation',
|
||||
focus: viewModel.focusOccupation,
|
||||
filled: occupationController.text.isNotEmpty),
|
||||
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
|
||||
CustomDropdownPicker(
|
||||
hint: 'Select occupation',
|
||||
onChanged: (value) {},
|
||||
selectedItem: 'Student',
|
||||
items: (value, props) => viewModel.getOccupations('Student'),
|
||||
);
|
||||
|
||||
Widget _buildOccupationValidatorWrapper(ProfileDetailViewModel viewModel) =>
|
||||
viewModel.hasOccupationValidationMessage
|
||||
? _buildOccupationValidator(viewModel)
|
||||
: Container();
|
||||
|
||||
Widget _buildOccupationValidator(ProfileDetailViewModel viewModel) => Text(
|
||||
viewModel.occupationValidationMessage!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildLowerColumnChildren(viewModel),
|
||||
|
|
@ -669,18 +545,17 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
|
||||
List<Widget> _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [
|
||||
_buildSaveButton(viewModel),
|
||||
verticalSpaceMedium,
|
||||
verticalSpaceSmall,
|
||||
_buildCancelButtonWrapper(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildSaveButton(ProfileDetailViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
const CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
text: 'Save Changes',
|
||||
foregroundColor: kcWhite,
|
||||
backgroundColor: kcPrimaryColor,
|
||||
onTap: () async => await _update(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding(
|
||||
|
|
@ -689,18 +564,12 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
);
|
||||
|
||||
Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
const CustomElevatedButton(
|
||||
height: 55,
|
||||
text: 'Cancel',
|
||||
borderRadius: 12,
|
||||
onTap: viewModel.pop,
|
||||
backgroundColor: kcWhite,
|
||||
borderColor: kcPrimaryColor,
|
||||
backgroundColor: kcWhite,
|
||||
foregroundColor: kcPrimaryColor,
|
||||
);
|
||||
|
||||
Widget _buildState(ProfileDetailViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.profileUpdate)
|
||||
? const PageLoadingIndicator()
|
||||
: Container();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ const String EmailValueKey = 'email';
|
|||
const String PhoneNumberValueKey = 'phoneNumber';
|
||||
const String LastNameValueKey = 'lastName';
|
||||
const String FirstNameValueKey = 'firstName';
|
||||
const String OccupationValueKey = 'occupation';
|
||||
|
||||
final Map<String, TextEditingController>
|
||||
_ProfileDetailViewTextEditingControllers = {};
|
||||
|
|
@ -29,7 +28,6 @@ final Map<String, String? Function(String?)?>
|
|||
PhoneNumberValueKey: FormValidator.validatePhoneNumber,
|
||||
LastNameValueKey: FormValidator.validateForm,
|
||||
FirstNameValueKey: FormValidator.validateForm,
|
||||
OccupationValueKey: FormValidator.validateForm,
|
||||
};
|
||||
|
||||
mixin $ProfileDetailView {
|
||||
|
|
@ -41,14 +39,11 @@ mixin $ProfileDetailView {
|
|||
_getFormTextEditingController(LastNameValueKey);
|
||||
TextEditingController get firstNameController =>
|
||||
_getFormTextEditingController(FirstNameValueKey);
|
||||
TextEditingController get occupationController =>
|
||||
_getFormTextEditingController(OccupationValueKey);
|
||||
|
||||
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
|
||||
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
|
||||
FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey);
|
||||
FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey);
|
||||
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
|
||||
|
||||
TextEditingController _getFormTextEditingController(
|
||||
String key, {
|
||||
|
|
@ -78,7 +73,6 @@ mixin $ProfileDetailView {
|
|||
phoneNumberController.addListener(() => _updateFormData(model));
|
||||
lastNameController.addListener(() => _updateFormData(model));
|
||||
firstNameController.addListener(() => _updateFormData(model));
|
||||
occupationController.addListener(() => _updateFormData(model));
|
||||
|
||||
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
||||
}
|
||||
|
|
@ -94,7 +88,6 @@ mixin $ProfileDetailView {
|
|||
phoneNumberController.addListener(() => _updateFormData(model));
|
||||
lastNameController.addListener(() => _updateFormData(model));
|
||||
firstNameController.addListener(() => _updateFormData(model));
|
||||
occupationController.addListener(() => _updateFormData(model));
|
||||
|
||||
_updateFormData(model, forceValidate: _autoTextFieldValidation);
|
||||
}
|
||||
|
|
@ -108,7 +101,6 @@ mixin $ProfileDetailView {
|
|||
PhoneNumberValueKey: phoneNumberController.text,
|
||||
LastNameValueKey: lastNameController.text,
|
||||
FirstNameValueKey: firstNameController.text,
|
||||
OccupationValueKey: occupationController.text,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -155,8 +147,6 @@ extension ValueProperties on FormStateHelper {
|
|||
this.formValueMap[PhoneNumberValueKey] as String?;
|
||||
String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?;
|
||||
String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?;
|
||||
String? get occupationValue =>
|
||||
this.formValueMap[OccupationValueKey] as String?;
|
||||
|
||||
set emailValue(String? value) {
|
||||
this.setData(
|
||||
|
|
@ -205,18 +195,6 @@ extension ValueProperties on FormStateHelper {
|
|||
}
|
||||
}
|
||||
|
||||
set occupationValue(String? value) {
|
||||
this.setData(
|
||||
this.formValueMap..addAll({OccupationValueKey: value}),
|
||||
);
|
||||
|
||||
if (_ProfileDetailViewTextEditingControllers.containsKey(
|
||||
OccupationValueKey)) {
|
||||
_ProfileDetailViewTextEditingControllers[OccupationValueKey]?.text =
|
||||
value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
bool get hasEmail =>
|
||||
this.formValueMap.containsKey(EmailValueKey) &&
|
||||
(emailValue?.isNotEmpty ?? false);
|
||||
|
|
@ -229,9 +207,6 @@ extension ValueProperties on FormStateHelper {
|
|||
bool get hasFirstName =>
|
||||
this.formValueMap.containsKey(FirstNameValueKey) &&
|
||||
(firstNameValue?.isNotEmpty ?? false);
|
||||
bool get hasOccupation =>
|
||||
this.formValueMap.containsKey(OccupationValueKey) &&
|
||||
(occupationValue?.isNotEmpty ?? false);
|
||||
|
||||
bool get hasEmailValidationMessage =>
|
||||
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
|
||||
|
|
@ -241,8 +216,6 @@ extension ValueProperties on FormStateHelper {
|
|||
this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false;
|
||||
bool get hasFirstNameValidationMessage =>
|
||||
this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false;
|
||||
bool get hasOccupationValidationMessage =>
|
||||
this.fieldsValidationMessages[OccupationValueKey]?.isNotEmpty ?? false;
|
||||
|
||||
String? get emailValidationMessage =>
|
||||
this.fieldsValidationMessages[EmailValueKey];
|
||||
|
|
@ -252,8 +225,6 @@ extension ValueProperties on FormStateHelper {
|
|||
this.fieldsValidationMessages[LastNameValueKey];
|
||||
String? get firstNameValidationMessage =>
|
||||
this.fieldsValidationMessages[FirstNameValueKey];
|
||||
String? get occupationValidationMessage =>
|
||||
this.fieldsValidationMessages[OccupationValueKey];
|
||||
}
|
||||
|
||||
extension Methods on FormStateHelper {
|
||||
|
|
@ -265,8 +236,6 @@ extension Methods on FormStateHelper {
|
|||
this.fieldsValidationMessages[LastNameValueKey] = validationMessage;
|
||||
setFirstNameValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[FirstNameValueKey] = validationMessage;
|
||||
setOccupationValidationMessage(String? validationMessage) =>
|
||||
this.fieldsValidationMessages[OccupationValueKey] = validationMessage;
|
||||
|
||||
/// Clears text input fields on the Form
|
||||
void clearForm() {
|
||||
|
|
@ -274,7 +243,6 @@ extension Methods on FormStateHelper {
|
|||
phoneNumberValue = '';
|
||||
lastNameValue = '';
|
||||
firstNameValue = '';
|
||||
occupationValue = '';
|
||||
}
|
||||
|
||||
/// Validates text input fields on the Form
|
||||
|
|
@ -284,7 +252,6 @@ extension Methods on FormStateHelper {
|
|||
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
|
||||
LastNameValueKey: getValidationMessage(LastNameValueKey),
|
||||
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
|
||||
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -308,5 +275,4 @@ void updateValidationData(FormStateHelper model) =>
|
|||
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
|
||||
LastNameValueKey: getValidationMessage(LastNameValueKey),
|
||||
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
|
||||
OccupationValueKey: getValidationMessage(OccupationValueKey),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,36 +2,9 @@ import 'package:stacked/stacked.dart';
|
|||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../models/user_model.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/image_picker_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
|
||||
class ProfileDetailViewModel extends ReactiveViewModel
|
||||
with FormStateHelper
|
||||
implements FormViewModel {
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
class ProfileDetailViewModel extends FormViewModel {
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
final _imagePickerService = locator<ImagePickerService>();
|
||||
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
|
||||
// Current user
|
||||
UserModel? get _user => _authenticationService.user;
|
||||
|
||||
UserModel? get user => _user;
|
||||
|
||||
// First name
|
||||
bool _focusFirstName = false;
|
||||
|
||||
|
|
@ -62,26 +35,6 @@ class ProfileDetailViewModel extends ReactiveViewModel
|
|||
|
||||
bool get focusEmail => _focusEmail;
|
||||
|
||||
// Country
|
||||
String _selectedCountry = 'Ethiopia';
|
||||
|
||||
String get selectedCountry => _selectedCountry;
|
||||
|
||||
// Region
|
||||
String _selectedRegion = 'Addis Ababa';
|
||||
|
||||
String get selectedRegion => _selectedRegion;
|
||||
|
||||
// Occupation
|
||||
bool _focusOccupation = false;
|
||||
|
||||
bool get focusOccupation => _focusOccupation;
|
||||
|
||||
// User data
|
||||
final Map<String, dynamic> _userData = {};
|
||||
|
||||
Map<String, dynamic> get userData => _userData;
|
||||
|
||||
// First name
|
||||
void setFirstNameFocus() {
|
||||
_focusFirstName = true;
|
||||
|
|
@ -119,132 +72,15 @@ class ProfileDetailViewModel extends ReactiveViewModel
|
|||
}
|
||||
|
||||
// Country
|
||||
List<String> getCountries() => ['Ethiopia', 'Other'];
|
||||
|
||||
void setSelectedCountry(String value) {
|
||||
_selectedCountry = value;
|
||||
if (selectedCountry != 'Ethiopia') {
|
||||
_selectedRegion = 'Other';
|
||||
} else {
|
||||
_selectedRegion = 'Addis Ababa';
|
||||
}
|
||||
|
||||
rebuildUi();
|
||||
}
|
||||
Future<List<String>> getCountries() async => ['Ethiopia', 'Djibouti'];
|
||||
|
||||
// Region
|
||||
List<String> getRegions(String country) {
|
||||
if (country == 'Ethiopia') {
|
||||
return [
|
||||
'Afar',
|
||||
'SNNPR',
|
||||
'Amhara',
|
||||
'Harari',
|
||||
'Oromia',
|
||||
'Sidama',
|
||||
'Somali',
|
||||
'Tigray',
|
||||
'Gambela',
|
||||
'Dire Dawa',
|
||||
'Addis Ababa',
|
||||
'Central Ethiopia',
|
||||
'Benishangul-Gumuz',
|
||||
'South West Ethiopia',
|
||||
];
|
||||
} else {
|
||||
return ['Other'];
|
||||
}
|
||||
}
|
||||
|
||||
void setSelectedRegion(String value) {
|
||||
_selectedRegion = value;
|
||||
rebuildUi();
|
||||
}
|
||||
Future<List<String>> getRegions(String country) async =>
|
||||
['Addis Ababa', 'Oromia'];
|
||||
|
||||
// Occupation
|
||||
void setOccupationFocus() {
|
||||
_focusOccupation = true;
|
||||
rebuildUi();
|
||||
}
|
||||
Future<List<String>> getOccupations(String country) async =>
|
||||
['Student', 'Worker'];
|
||||
|
||||
// User data
|
||||
void addUserData(Map<String, dynamic> data) {
|
||||
_userData.addAll(data);
|
||||
}
|
||||
|
||||
void clearUserData() {
|
||||
_userData.clear();
|
||||
}
|
||||
|
||||
// Image picker
|
||||
Future<void> openCamera() async =>
|
||||
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
|
||||
|
||||
Future<void> _openCamera() async {
|
||||
String? image = await _imagePickerService.camera();
|
||||
pop();
|
||||
if (image != null) {
|
||||
await updateProfilePicture(image);
|
||||
await _authenticationService.saveProfileImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openGallery() async =>
|
||||
runBusyFuture(_openGallery(), busyObject: StateObjects.profileImage);
|
||||
|
||||
Future<void> _openGallery() async {
|
||||
String? image = await _imagePickerService.gallery();
|
||||
pop();
|
||||
if (image != null) {
|
||||
await updateProfilePicture(image);
|
||||
await _authenticationService.saveProfileImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Get profile
|
||||
Future<void> getProfile() async => await runBusyFuture(_getProfile());
|
||||
|
||||
Future<void> _getProfile() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> response =
|
||||
await _apiService.getProfileData(_user?.userId);
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
addUserData(response['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile
|
||||
Future<void> updateProfile() async => await runBusyFuture(_updateProfile(),
|
||||
busyObject: StateObjects.profileUpdate);
|
||||
|
||||
Future<void> _updateProfile() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> response =
|
||||
await _apiService.completeProfile(_userData);
|
||||
if (response['status'] == ResponseStatus.success) {
|
||||
await _authenticationService.updateUserData(_userData);
|
||||
pop();
|
||||
showSuccessToast(response['message']);
|
||||
} else {
|
||||
showErrorToast(response['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile picture
|
||||
Future<void> updateProfilePicture(String image) async =>
|
||||
await runBusyFuture(_updateProfilePicture(image));
|
||||
|
||||
Future<void> _updateProfilePicture(String image) async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
Map<String, dynamic> data = {'profile_picture_url': image};
|
||||
await _apiService.updateProfileImage(data: data, userId: _user?.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
|
|||
title: viewModel.progresses[index]['title'],
|
||||
color: viewModel.progresses[index]['color'],
|
||||
status: viewModel.progresses[index]['status'],
|
||||
subtitle: viewModel.progresses[index]['subtitle'],
|
||||
subTitle: viewModel.progresses[index]['subTitle'],
|
||||
isCompleted: viewModel.progresses[index]['isCompleted'],
|
||||
),
|
||||
);
|
||||
|
|
@ -111,7 +111,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
|
|||
required String title,
|
||||
required String icon,
|
||||
required String status,
|
||||
required String subtitle,
|
||||
required String subTitle,
|
||||
required bool isCompleted,
|
||||
required ProgressViewModel viewModel}) =>
|
||||
CourseLevelCard(
|
||||
|
|
@ -119,7 +119,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
|
|||
title: title,
|
||||
color: color,
|
||||
status: status,
|
||||
subtitle: subtitle,
|
||||
subTitle: subTitle,
|
||||
isCompleted: isCompleted,
|
||||
onTap: viewModel.navigateToOngoingProgress,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,24 +14,24 @@ class ProgressViewModel extends BaseViewModel {
|
|||
'title': 'Beginner',
|
||||
'isCompleted': true,
|
||||
'status': 'Completed',
|
||||
'icon': 'assets/icons/b_1.svg',
|
||||
'subtitle': 'You’ve mastered everyday English basics!',
|
||||
'icon': 'assets/icons/b1.svg',
|
||||
'subTitle': 'You’ve mastered everyday English basics!',
|
||||
},
|
||||
{
|
||||
'title': 'Elementary',
|
||||
'isCompleted': false,
|
||||
'status': 'In Progress',
|
||||
'color': kcPrimaryColor,
|
||||
'icon': 'assets/icons/b_1.svg',
|
||||
'subtitle': 'Continue improving your conversations and fluency.',
|
||||
'icon': 'assets/icons/b1.svg',
|
||||
'subTitle': 'Continue improving your conversations and fluency.',
|
||||
},
|
||||
{
|
||||
'title': 'Beginner',
|
||||
'isCompleted': true,
|
||||
'status': 'In Progress',
|
||||
'color': kcPrimaryColor,
|
||||
'icon': 'assets/icons/b_1.svg',
|
||||
'subtitle': 'You’ve mastered everyday English basics!',
|
||||
'icon': 'assets/icons/b1.svg',
|
||||
'subTitle': 'You’ve mastered everyday English basics!',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked/stacked_annotations.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
import 'package:yimaru_app/ui/views/register/screens/create_password_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/register/screens/register_with_email_screen.dart';
|
||||
import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart';
|
||||
|
|
@ -25,46 +24,8 @@ import 'register_view.form.dart';
|
|||
class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
|
||||
const RegisterView({Key? key}) : super(key: key);
|
||||
|
||||
void _initClearData() {
|
||||
otpController.clear();
|
||||
emailController.clear();
|
||||
passwordController.clear();
|
||||
phoneNumberController.clear();
|
||||
confirmPasswordController.clear();
|
||||
}
|
||||
|
||||
void _inAppPop(RegisterViewModel viewModel) {
|
||||
print('OnPop');
|
||||
print(viewModel.currentPage);
|
||||
_clearDataOnNavigation(viewModel);
|
||||
viewModel.goBack();
|
||||
}
|
||||
|
||||
void _clearDataOnNavigation(RegisterViewModel viewModel) {
|
||||
if (viewModel.currentPage == 0) {
|
||||
emailController.clear();
|
||||
viewModel.resetRegisterWithEmailScreen();
|
||||
} else if (viewModel.currentPage == 2) {
|
||||
passwordController.clear();
|
||||
confirmPasswordController.clear();
|
||||
viewModel.resetCreatePasswordScreen();
|
||||
} else if (viewModel.currentPage == 3) {
|
||||
otpController.clear();
|
||||
viewModel.resetRegistrationOtpScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void _pop({required bool value, required RegisterViewModel viewModel}) {
|
||||
{
|
||||
if (!value) return;
|
||||
_clearDataOnNavigation(viewModel);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onViewModelReady(RegisterViewModel viewModel) {
|
||||
_initClearData();
|
||||
syncFormWithViewModel(viewModel);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
|
@ -83,14 +44,44 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
|
|||
|
||||
Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (value, data) =>
|
||||
_pop(value: value, viewModel: viewModel),
|
||||
child: _buildBody(viewModel));
|
||||
onPopInvokedWithResult: (value, data) {
|
||||
if (value) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
|
||||
},
|
||||
child: _buildScaffoldWrapper(viewModel));
|
||||
|
||||
Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffoldStack(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack(
|
||||
children: [_buildScaffold(viewModel), _buildBusyRegistration(viewModel)]);
|
||||
|
||||
Widget _buildScaffold(RegisterViewModel viewModel) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildScaffoldChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren(RegisterViewModel viewModel) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||
|
||||
Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.goBack,
|
||||
showLanguageSelection: true,
|
||||
);
|
||||
|
||||
Widget _buildExpandedBody(RegisterViewModel viewModel) =>
|
||||
Expanded(child: _buildBodyWrapper(viewModel));
|
||||
|
||||
Widget _buildBodyWrapper(RegisterViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildBody(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody(RegisterViewModel viewModel) =>
|
||||
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
|
||||
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
|
||||
|
||||
List<Widget> _buildScreens() => [
|
||||
_buildRegisterWithEmailScreen(),
|
||||
|
|
@ -115,4 +106,6 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
|
|||
passwordController: passwordController,
|
||||
confirmPasswordController: confirmPasswordController);
|
||||
|
||||
Widget _buildBusyRegistration(RegisterViewModel viewModel) =>
|
||||
viewModel.isBusy ? const PageLoadingIndicator() : Container();
|
||||
}
|
||||
|
|
|
|||