- feat(learn): Integrate learn practice with local data
Merge branch 'release/0.1.0-internal.v2'
|
|
@ -1,43 +1,79 @@
|
||||||
|
import java.util.Properties
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("kotlin-android")
|
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
FileInputStream(keystorePropertiesFile).use {
|
||||||
|
keystoreProperties.load(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
namespace = "com.yimaru.lms.app"
|
namespace = "com.yimaru.lms.app"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
applicationId = "com.yimaru.lms.app"
|
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
applicationId = "com.yimaru.lms.app"
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
keyAlias = keystoreProperties["keyAlias"] as String
|
||||||
|
keyPassword = keystoreProperties["keyPassword"] as String
|
||||||
|
storePassword = keystoreProperties["storePassword"] as String
|
||||||
|
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
getByName("release") {
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
|
ndk { debugSymbolLevel = "FULL" }
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||||
|
implementation(platform("com.google.firebase:firebase-bom:34.10.0"))
|
||||||
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
@ -14,20 +14,20 @@
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
{
|
{
|
||||||
"client_id": "574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com",
|
"client_id": "574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.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,
|
"client_type": 1,
|
||||||
"android_info": {
|
"android_info": {
|
||||||
"package_name": "com.yimaru.lms.app",
|
"package_name": "com.yimaru.lms.app",
|
||||||
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
|
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": "574860813475-m90u87plqaac4tb8oug32k41usossiod.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.yimaru.lms.app",
|
||||||
|
"certificate_hash": "29797902ad6a24212b9d9fad71562907956f6a6c"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"api_key": [
|
"api_key": [
|
||||||
|
|
|
||||||
41
android/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
############################################
|
||||||
|
# Flutter
|
||||||
|
############################################
|
||||||
|
-keep class io.flutter.** { *; }
|
||||||
|
-keep class io.flutter.plugins.** { *; }
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Firebase (General Safe Rules)
|
||||||
|
############################################
|
||||||
|
-keep class com.google.firebase.** { *; }
|
||||||
|
-dontwarn com.google.firebase.**
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Google Sign-In
|
||||||
|
############################################
|
||||||
|
-keep class com.google.android.gms.auth.api.signin.** { *; }
|
||||||
|
-keep class com.google.android.gms.common.api.** { *; }
|
||||||
|
-dontwarn com.google.android.gms.**
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Play Services
|
||||||
|
############################################
|
||||||
|
-keep class com.google.android.gms.** { *; }
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# flutter_inappwebview
|
||||||
|
############################################
|
||||||
|
-keep class com.pichillilorenzo.flutter_inappwebview.** { *; }
|
||||||
|
-dontwarn com.pichillilorenzo.flutter_inappwebview.**
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Keep annotations
|
||||||
|
############################################
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Google Play Core
|
||||||
|
############################################
|
||||||
|
-keep class com.google.android.play.core.** { *; }
|
||||||
|
-dontwarn com.google.android.play.core.**
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
<application
|
<application
|
||||||
android:label="Yimaru"
|
android:label="Yimaru Academy"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
BIN
android/app/src/main/res/drawable-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
android/app/src/main/res/drawable-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-night-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
android/app/src/main/res/drawable-night-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 84 KiB |
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
|
|
@ -1,12 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="?android:colorBackground" />
|
<item>
|
||||||
|
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||||
<!-- You can insert your own image assets here -->
|
</item>
|
||||||
<!-- <item>
|
<item>
|
||||||
<bitmap
|
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||||
android:gravity="center"
|
</item>
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|
|
||||||
BIN
android/app/src/main/res/drawable-xhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
android/app/src/main/res/drawable/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
|
|
@ -1,12 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@android:color/white" />
|
<item>
|
||||||
|
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||||
<!-- You can insert your own image assets here -->
|
</item>
|
||||||
<!-- <item>
|
<item>
|
||||||
<bitmap
|
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||||
android:gravity="center"
|
</item>
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.9 KiB |
21
android/app/src/main/res/values-night-v31/styles.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:forceDarkAllowed">false</item>
|
||||||
|
<item name="android:windowFullscreen">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">#9E2891</item>
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:forceDarkAllowed">false</item>
|
||||||
|
<item name="android:windowFullscreen">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|
|
||||||
21
android/app/src/main/res/values-v31/styles.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:forceDarkAllowed">false</item>
|
||||||
|
<item name="android:windowFullscreen">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">#9E2891</item>
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:forceDarkAllowed">false</item>
|
||||||
|
<item name="android:windowFullscreen">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|
|
||||||
659
android/build/reports/problems/problems-report.html
Normal file
|
|
@ -1,2 +1,12 @@
|
||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
android.defaults.buildfeatures.resvalues=true
|
||||||
|
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
||||||
|
android.enableAppCompileTimeRClass=false
|
||||||
|
android.usesSdkInManifest.disallowed=false
|
||||||
|
android.uniquePackageNames=false
|
||||||
|
android.dependency.useConstraints=true
|
||||||
|
android.r8.strictFullModeForKeepRules=false
|
||||||
|
android.r8.optimizedResourceShrinking=false
|
||||||
|
android.builtInKotlin=false
|
||||||
|
android.newDsl=false
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,10 @@ pluginManagement {
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.13.2" apply false
|
id("com.android.application") version "9.1.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.3.0" 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.google.gms.google-services") version("4.4.4") apply false
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
BIN
assets/icons/duolingo.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/icons/dwarf.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/icons/splash_logo.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
3
devtools_options.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
21
ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "background.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
|
|
@ -1,23 +1,23 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "LaunchImage.png",
|
"filename" : "LaunchImage.png",
|
||||||
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "LaunchImage@2x.png",
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "LaunchImage@3x.png",
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 57 KiB |
|
|
@ -16,13 +16,19 @@
|
||||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||||
</imageView>
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
|
||||||
|
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
|
||||||
|
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
</viewController>
|
</viewController>
|
||||||
|
|
@ -32,6 +38,7 @@
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="LaunchImage" width="168" height="185"/>
|
<image name="LaunchImage" width="1024" height="1024"/>
|
||||||
|
<image name="LaunchBackground" width="1" height="1"/>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|
|
||||||
|
|
@ -45,5 +45,7 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import 'package:yimaru_app/ui/views/profile/profile_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart';
|
import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart';
|
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/progress/progress_view.dart';
|
import 'package:yimaru_app/ui/views/progress/progress_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart';
|
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/support/support_view.dart';
|
import 'package:yimaru_app/ui/views/support/support_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart';
|
import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart';
|
||||||
|
|
@ -30,7 +29,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/welcome/welcome_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
|
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
|
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
|
||||||
import 'package:yimaru_app/services/permission_handler_service.dart';
|
import 'package:yimaru_app/services/permission_handler_service.dart';
|
||||||
import 'package:yimaru_app/services/image_picker_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/google_auth_service.dart';
|
||||||
|
|
@ -38,6 +36,20 @@ 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/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_lesson_detail/learn_lesson_detail_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_category/course_category_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_lesson/course_lesson_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
|
||||||
|
import 'package:yimaru_app/services/notification_service.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
|
||||||
|
import 'package:yimaru_app/services/smart_auth_service.dart';
|
||||||
|
import 'package:yimaru_app/services/course_service.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_subcategory/course_subcategory_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course/course_view.dart';
|
||||||
|
import 'package:yimaru_app/services/audio_player_service.dart';
|
||||||
|
import 'package:yimaru_app/services/voice_recorder_service.dart';
|
||||||
// @stacked-import
|
// @stacked-import
|
||||||
|
|
||||||
@StackedApp(
|
@StackedApp(
|
||||||
|
|
@ -49,7 +61,6 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||||
MaterialRoute(page: ProfileDetailView),
|
MaterialRoute(page: ProfileDetailView),
|
||||||
MaterialRoute(page: DownloadsView),
|
MaterialRoute(page: DownloadsView),
|
||||||
MaterialRoute(page: ProgressView),
|
MaterialRoute(page: ProgressView),
|
||||||
MaterialRoute(page: OngoingProgressView),
|
|
||||||
MaterialRoute(page: AccountPrivacyView),
|
MaterialRoute(page: AccountPrivacyView),
|
||||||
MaterialRoute(page: SupportView),
|
MaterialRoute(page: SupportView),
|
||||||
MaterialRoute(page: TelegramSupportView),
|
MaterialRoute(page: TelegramSupportView),
|
||||||
|
|
@ -65,10 +76,18 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||||
MaterialRoute(page: WelcomeView),
|
MaterialRoute(page: WelcomeView),
|
||||||
MaterialRoute(page: AssessmentView),
|
MaterialRoute(page: AssessmentView),
|
||||||
MaterialRoute(page: LearnLessonView),
|
MaterialRoute(page: LearnLessonView),
|
||||||
MaterialRoute(page: FailureView),
|
|
||||||
MaterialRoute(page: ForgetPasswordView),
|
MaterialRoute(page: ForgetPasswordView),
|
||||||
MaterialRoute(page: LearnLessonDetailView),
|
MaterialRoute(page: LearnLessonDetailView),
|
||||||
MaterialRoute(page: LearnPracticeView),
|
MaterialRoute(page: LearnPracticeView),
|
||||||
|
MaterialRoute(page: CoursePracticeView),
|
||||||
|
MaterialRoute(page: CoursePaymentView),
|
||||||
|
MaterialRoute(page: CourseCategoryView),
|
||||||
|
MaterialRoute(page: FailureView),
|
||||||
|
MaterialRoute(page: CourseLessonView),
|
||||||
|
MaterialRoute(page: CourseLessonDetailView),
|
||||||
|
MaterialRoute(page: DuolingoView),
|
||||||
|
MaterialRoute(page: CourseSubcategoryView),
|
||||||
|
MaterialRoute(page: CourseView),
|
||||||
// @stacked-route
|
// @stacked-route
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|
@ -84,6 +103,11 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||||
LazySingleton(classType: ImagePickerService),
|
LazySingleton(classType: ImagePickerService),
|
||||||
LazySingleton(classType: GoogleAuthService),
|
LazySingleton(classType: GoogleAuthService),
|
||||||
LazySingleton(classType: ImageDownloaderService),
|
LazySingleton(classType: ImageDownloaderService),
|
||||||
|
LazySingleton(classType: NotificationService),
|
||||||
|
LazySingleton(classType: SmartAuthService),
|
||||||
|
LazySingleton(classType: CourseService),
|
||||||
|
LazySingleton(classType: AudioPlayerService),
|
||||||
|
LazySingleton(classType: VoiceRecorderService),
|
||||||
// @stacked-service
|
// @stacked-service
|
||||||
],
|
],
|
||||||
bottomsheets: [
|
bottomsheets: [
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,19 @@ import 'package:stacked_services/src/navigation/navigation_service.dart';
|
||||||
import 'package:stacked_shared/stacked_shared.dart';
|
import 'package:stacked_shared/stacked_shared.dart';
|
||||||
|
|
||||||
import '../services/api_service.dart';
|
import '../services/api_service.dart';
|
||||||
|
import '../services/audio_player_service.dart';
|
||||||
import '../services/authentication_service.dart';
|
import '../services/authentication_service.dart';
|
||||||
|
import '../services/course_service.dart';
|
||||||
import '../services/dio_service.dart';
|
import '../services/dio_service.dart';
|
||||||
import '../services/google_auth_service.dart';
|
import '../services/google_auth_service.dart';
|
||||||
import '../services/image_downloader_service.dart';
|
import '../services/image_downloader_service.dart';
|
||||||
import '../services/image_picker_service.dart';
|
import '../services/image_picker_service.dart';
|
||||||
|
import '../services/notification_service.dart';
|
||||||
import '../services/permission_handler_service.dart';
|
import '../services/permission_handler_service.dart';
|
||||||
import '../services/secure_storage_service.dart';
|
import '../services/secure_storage_service.dart';
|
||||||
|
import '../services/smart_auth_service.dart';
|
||||||
import '../services/status_checker_service.dart';
|
import '../services/status_checker_service.dart';
|
||||||
|
import '../services/voice_recorder_service.dart';
|
||||||
|
|
||||||
final locator = StackedLocator.instance;
|
final locator = StackedLocator.instance;
|
||||||
|
|
||||||
|
|
@ -44,4 +49,9 @@ Future<void> setupLocator({
|
||||||
locator.registerLazySingleton(() => ImagePickerService());
|
locator.registerLazySingleton(() => ImagePickerService());
|
||||||
locator.registerLazySingleton(() => GoogleAuthService());
|
locator.registerLazySingleton(() => GoogleAuthService());
|
||||||
locator.registerLazySingleton(() => ImageDownloaderService());
|
locator.registerLazySingleton(() => ImageDownloaderService());
|
||||||
|
locator.registerLazySingleton(() => NotificationService());
|
||||||
|
locator.registerLazySingleton(() => SmartAuthService());
|
||||||
|
locator.registerLazySingleton(() => CourseService());
|
||||||
|
locator.registerLazySingleton(() => AudioPlayerService());
|
||||||
|
locator.registerLazySingleton(() => VoiceRecorderService());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class DefaultFirebaseOptions {
|
||||||
projectId: 'yimaru-lms-e834e',
|
projectId: 'yimaru-lms-e834e',
|
||||||
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
|
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
|
||||||
androidClientId:
|
androidClientId:
|
||||||
'574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com',
|
'574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com',
|
||||||
iosBundleId: 'com.yimaru.lms.app',
|
iosBundleId: 'com.yimaru.lms.app',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:toastification/toastification.dart';
|
import 'package:toastification/toastification.dart';
|
||||||
import 'package:yimaru_app/app/app.bottomsheets.dart';
|
import 'package:yimaru_app/app/app.bottomsheets.dart';
|
||||||
|
|
@ -5,10 +6,15 @@ import 'package:yimaru_app/app/app.dialogs.dart';
|
||||||
import 'package:yimaru_app/app/app.locator.dart';
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
import 'package:yimaru_app/app/app.router.dart';
|
import 'package:yimaru_app/app/app.router.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
import 'package:yimaru_app/services/notification_service.dart';
|
||||||
|
|
||||||
|
import 'firebase_options.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await setupLocator();
|
await setupLocator();
|
||||||
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
await locator<NotificationService>().initialize();
|
||||||
setupDialogUi();
|
setupDialogUi();
|
||||||
setupBottomSheetUi();
|
setupBottomSheetUi();
|
||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ class Assessment {
|
||||||
|
|
||||||
final String? status;
|
final String? status;
|
||||||
|
|
||||||
|
final List<Option>? options;
|
||||||
|
|
||||||
@JsonKey(name: 'question_type')
|
@JsonKey(name: 'question_type')
|
||||||
final String? questionType;
|
final String? questionType;
|
||||||
|
|
||||||
|
|
@ -19,8 +21,6 @@ class Assessment {
|
||||||
@JsonKey(name: 'difficulty_level')
|
@JsonKey(name: 'difficulty_level')
|
||||||
final String? difficultyLevel;
|
final String? difficultyLevel;
|
||||||
|
|
||||||
final List<Option>? options;
|
|
||||||
|
|
||||||
const Assessment({
|
const Assessment({
|
||||||
this.id,
|
this.id,
|
||||||
this.points,
|
this.points,
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ Map<String, dynamic> _$AssessmentToJson(Assessment instance) =>
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'points': instance.points,
|
'points': instance.points,
|
||||||
'status': instance.status,
|
'status': instance.status,
|
||||||
|
'options': instance.options,
|
||||||
'question_type': instance.questionType,
|
'question_type': instance.questionType,
|
||||||
'question_text': instance.questionText,
|
'question_text': instance.questionText,
|
||||||
'difficulty_level': instance.difficultyLevel,
|
'difficulty_level': instance.difficultyLevel,
|
||||||
'options': instance.options,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
39
lib/models/course.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'course.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Course {
|
||||||
|
final int? id;
|
||||||
|
|
||||||
|
final String? level;
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
final String? thumbnail;
|
||||||
|
|
||||||
|
final String? description;
|
||||||
|
|
||||||
|
@JsonKey(name: 'course_id')
|
||||||
|
final int? courseId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'is_active')
|
||||||
|
final bool? isActive;
|
||||||
|
|
||||||
|
@JsonKey(name: 'display_order')
|
||||||
|
final int? displayOrder;
|
||||||
|
|
||||||
|
const Course(
|
||||||
|
{this.id,
|
||||||
|
this.level,
|
||||||
|
this.title,
|
||||||
|
this.isActive,
|
||||||
|
this.courseId,
|
||||||
|
this.thumbnail,
|
||||||
|
this.description,
|
||||||
|
this.displayOrder});
|
||||||
|
|
||||||
|
factory Course.fromJson(Map<String, dynamic> json) => _$CourseFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CourseToJson(this);
|
||||||
|
}
|
||||||
29
lib/models/course.g.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'course.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Course _$CourseFromJson(Map<String, dynamic> json) => Course(
|
||||||
|
id: (json['id'] as num?)?.toInt(),
|
||||||
|
level: json['level'] as String?,
|
||||||
|
title: json['title'] as String?,
|
||||||
|
isActive: json['is_active'] as bool?,
|
||||||
|
courseId: (json['course_id'] as num?)?.toInt(),
|
||||||
|
thumbnail: json['thumbnail'] as String?,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
displayOrder: (json['display_order'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CourseToJson(Course instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'level': instance.level,
|
||||||
|
'title': instance.title,
|
||||||
|
'thumbnail': instance.thumbnail,
|
||||||
|
'description': instance.description,
|
||||||
|
'course_id': instance.courseId,
|
||||||
|
'is_active': instance.isActive,
|
||||||
|
'display_order': instance.displayOrder,
|
||||||
|
};
|
||||||
20
lib/models/course_category.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'course_category.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CourseCategory {
|
||||||
|
final int? id;
|
||||||
|
|
||||||
|
final String? name;
|
||||||
|
|
||||||
|
@JsonKey(name: 'is_active')
|
||||||
|
final bool? isActive;
|
||||||
|
|
||||||
|
const CourseCategory({this.id, this.name, this.isActive});
|
||||||
|
|
||||||
|
factory CourseCategory.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CourseCategoryFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CourseCategoryToJson(this);
|
||||||
|
}
|
||||||
21
lib/models/course_category.g.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'course_category.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CourseCategory _$CourseCategoryFromJson(Map<String, dynamic> json) =>
|
||||||
|
CourseCategory(
|
||||||
|
id: (json['id'] as num?)?.toInt(),
|
||||||
|
name: json['name'] as String?,
|
||||||
|
isActive: json['is_active'] as bool?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CourseCategoryToJson(CourseCategory instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
|
'is_active': instance.isActive,
|
||||||
|
};
|
||||||
19
lib/models/course_detail.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:yimaru_app/models/course_progress.dart';
|
||||||
|
import 'package:yimaru_app/models/course.dart';
|
||||||
|
|
||||||
|
part 'course_detail.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CourseDetail {
|
||||||
|
final Course? course;
|
||||||
|
|
||||||
|
final CourseProgress? courseProgress;
|
||||||
|
|
||||||
|
const CourseDetail({this.course, this.courseProgress});
|
||||||
|
|
||||||
|
factory CourseDetail.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CourseDetailFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CourseDetailToJson(this);
|
||||||
|
}
|
||||||
23
lib/models/course_detail.g.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'course_detail.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CourseDetail _$CourseDetailFromJson(Map<String, dynamic> json) => CourseDetail(
|
||||||
|
course: json['course'] == null
|
||||||
|
? null
|
||||||
|
: Course.fromJson(json['course'] as Map<String, dynamic>),
|
||||||
|
courseProgress: json['courseProgress'] == null
|
||||||
|
? null
|
||||||
|
: CourseProgress.fromJson(
|
||||||
|
json['courseProgress'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CourseDetailToJson(CourseDetail instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'course': instance.course,
|
||||||
|
'courseProgress': instance.courseProgress,
|
||||||
|
};
|
||||||
57
lib/models/course_lesson.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'course_lesson.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CourseLesson {
|
||||||
|
int? id;
|
||||||
|
|
||||||
|
String? title;
|
||||||
|
|
||||||
|
int? duration;
|
||||||
|
|
||||||
|
String? status;
|
||||||
|
|
||||||
|
String? thumbnail;
|
||||||
|
|
||||||
|
String? resolution;
|
||||||
|
|
||||||
|
String? visibility;
|
||||||
|
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
@JsonKey(name: 'video_url')
|
||||||
|
String? videoUrl;
|
||||||
|
|
||||||
|
@JsonKey(name: 'instructor_id')
|
||||||
|
int? instructorId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'sub_course_id')
|
||||||
|
int? courseId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'vimeo_status')
|
||||||
|
String? vimeoStatus;
|
||||||
|
|
||||||
|
@JsonKey(name: 'display_order')
|
||||||
|
int? displayOrder;
|
||||||
|
|
||||||
|
CourseLesson(
|
||||||
|
{this.id,
|
||||||
|
this.title,
|
||||||
|
this.status,
|
||||||
|
this.courseId,
|
||||||
|
this.videoUrl,
|
||||||
|
this.duration,
|
||||||
|
this.thumbnail,
|
||||||
|
this.visibility,
|
||||||
|
this.resolution,
|
||||||
|
this.vimeoStatus,
|
||||||
|
this.description,
|
||||||
|
this.displayOrder,
|
||||||
|
this.instructorId});
|
||||||
|
|
||||||
|
factory CourseLesson.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CourseLessonFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CourseLessonToJson(this);
|
||||||
|
}
|
||||||
40
lib/models/course_lesson.g.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'course_lesson.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CourseLesson _$CourseLessonFromJson(Map<String, dynamic> json) => CourseLesson(
|
||||||
|
id: (json['id'] as num?)?.toInt(),
|
||||||
|
title: json['title'] as String?,
|
||||||
|
status: json['status'] as String?,
|
||||||
|
courseId: (json['sub_course_id'] as num?)?.toInt(),
|
||||||
|
videoUrl: json['video_url'] as String?,
|
||||||
|
duration: (json['duration'] as num?)?.toInt(),
|
||||||
|
thumbnail: json['thumbnail'] as String?,
|
||||||
|
visibility: json['visibility'] as String?,
|
||||||
|
resolution: json['resolution'] as String?,
|
||||||
|
vimeoStatus: json['vimeo_status'] as String?,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
displayOrder: (json['display_order'] as num?)?.toInt(),
|
||||||
|
instructorId: (json['instructor_id'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CourseLessonToJson(CourseLesson instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'title': instance.title,
|
||||||
|
'duration': instance.duration,
|
||||||
|
'status': instance.status,
|
||||||
|
'thumbnail': instance.thumbnail,
|
||||||
|
'resolution': instance.resolution,
|
||||||
|
'visibility': instance.visibility,
|
||||||
|
'description': instance.description,
|
||||||
|
'video_url': instance.videoUrl,
|
||||||
|
'instructor_id': instance.instructorId,
|
||||||
|
'sub_course_id': instance.courseId,
|
||||||
|
'vimeo_status': instance.vimeoStatus,
|
||||||
|
'display_order': instance.displayOrder,
|
||||||
|
};
|
||||||
42
lib/models/course_progress.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'course_progress.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CourseProgress {
|
||||||
|
final String? level;
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
final String? description;
|
||||||
|
|
||||||
|
@JsonKey(name: 'is_locked')
|
||||||
|
final bool? isLocked;
|
||||||
|
|
||||||
|
@JsonKey(name: 'sub_course_id')
|
||||||
|
final int? courseId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'display_order')
|
||||||
|
final int? displayOrder;
|
||||||
|
|
||||||
|
@JsonKey(name: 'progress_status')
|
||||||
|
final String? progressStatus;
|
||||||
|
|
||||||
|
@JsonKey(name: 'progress_percentage')
|
||||||
|
final double? progressPercentage;
|
||||||
|
|
||||||
|
const CourseProgress(
|
||||||
|
{this.level,
|
||||||
|
this.title,
|
||||||
|
this.isLocked,
|
||||||
|
this.courseId,
|
||||||
|
this.description,
|
||||||
|
this.displayOrder,
|
||||||
|
this.progressStatus,
|
||||||
|
this.progressPercentage});
|
||||||
|
|
||||||
|
factory CourseProgress.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CourseProgressFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CourseProgressToJson(this);
|
||||||
|
}
|
||||||
31
lib/models/course_progress.g.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'course_progress.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CourseProgress _$CourseProgressFromJson(Map<String, dynamic> json) =>
|
||||||
|
CourseProgress(
|
||||||
|
level: json['level'] as String?,
|
||||||
|
title: json['title'] as String?,
|
||||||
|
isLocked: json['is_locked'] as bool?,
|
||||||
|
courseId: (json['sub_course_id'] as num?)?.toInt(),
|
||||||
|
description: json['description'] as String?,
|
||||||
|
displayOrder: (json['display_order'] as num?)?.toInt(),
|
||||||
|
progressStatus: json['progress_status'] as String?,
|
||||||
|
progressPercentage: (json['progress_percentage'] as num?)?.toDouble(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CourseProgressToJson(CourseProgress instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'level': instance.level,
|
||||||
|
'title': instance.title,
|
||||||
|
'description': instance.description,
|
||||||
|
'is_locked': instance.isLocked,
|
||||||
|
'sub_course_id': instance.courseId,
|
||||||
|
'display_order': instance.displayOrder,
|
||||||
|
'progress_status': instance.progressStatus,
|
||||||
|
'progress_percentage': instance.progressPercentage,
|
||||||
|
};
|
||||||
33
lib/models/course_subcategory.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'course_subcategory.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CourseSubcategory {
|
||||||
|
final int? id;
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
final String? thumbnail;
|
||||||
|
|
||||||
|
final String? description;
|
||||||
|
|
||||||
|
@JsonKey(name: 'is_active')
|
||||||
|
final bool? isActive;
|
||||||
|
|
||||||
|
@JsonKey(name: 'category_id')
|
||||||
|
final int? categoryId;
|
||||||
|
|
||||||
|
const CourseSubcategory(
|
||||||
|
{this.id,
|
||||||
|
this.title,
|
||||||
|
this.isActive,
|
||||||
|
this.thumbnail,
|
||||||
|
this.categoryId,
|
||||||
|
this.description});
|
||||||
|
|
||||||
|
factory CourseSubcategory.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CourseSubcategoryFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CourseSubcategoryToJson(this);
|
||||||
|
}
|
||||||
27
lib/models/course_subcategory.g.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'course_subcategory.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CourseSubcategory _$CourseSubcategoryFromJson(Map<String, dynamic> json) =>
|
||||||
|
CourseSubcategory(
|
||||||
|
id: (json['id'] as num?)?.toInt(),
|
||||||
|
title: json['title'] as String?,
|
||||||
|
isActive: json['is_active'] as bool?,
|
||||||
|
thumbnail: json['thumbnail'] as String?,
|
||||||
|
categoryId: (json['category_id'] as num?)?.toInt(),
|
||||||
|
description: json['description'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CourseSubcategoryToJson(CourseSubcategory instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'title': instance.title,
|
||||||
|
'thumbnail': instance.thumbnail,
|
||||||
|
'description': instance.description,
|
||||||
|
'is_active': instance.isActive,
|
||||||
|
'category_id': instance.categoryId,
|
||||||
|
};
|
||||||
43
lib/models/practice.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
part 'practice.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Practice {
|
||||||
|
final int? id;
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
final String? status;
|
||||||
|
|
||||||
|
final String? persona;
|
||||||
|
|
||||||
|
final String? description;
|
||||||
|
|
||||||
|
@JsonKey(name: 'owner_id')
|
||||||
|
final int? ownerId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'set_type')
|
||||||
|
final String? setType;
|
||||||
|
|
||||||
|
@JsonKey(name: 'owner_type')
|
||||||
|
final String? ownerType;
|
||||||
|
|
||||||
|
@JsonKey(name: 'shuffle_questions')
|
||||||
|
final bool? shuffleQuestions;
|
||||||
|
|
||||||
|
const Practice(
|
||||||
|
{this.id,
|
||||||
|
this.title,
|
||||||
|
this.status,
|
||||||
|
this.setType,
|
||||||
|
this.persona,
|
||||||
|
this.ownerId,
|
||||||
|
this.ownerType,
|
||||||
|
this.description,
|
||||||
|
this.shuffleQuestions});
|
||||||
|
|
||||||
|
factory Practice.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PracticeFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$PracticeToJson(this);
|
||||||
|
}
|
||||||
31
lib/models/practice.g.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'practice.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Practice _$PracticeFromJson(Map<String, dynamic> json) => Practice(
|
||||||
|
id: (json['id'] as num?)?.toInt(),
|
||||||
|
title: json['title'] as String?,
|
||||||
|
status: json['status'] as String?,
|
||||||
|
setType: json['set_type'] as String?,
|
||||||
|
persona: json['persona'] as String?,
|
||||||
|
ownerId: (json['owner_id'] as num?)?.toInt(),
|
||||||
|
ownerType: json['owner_type'] as String?,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
shuffleQuestions: json['shuffle_questions'] as bool?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$PracticeToJson(Practice instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'title': instance.title,
|
||||||
|
'status': instance.status,
|
||||||
|
'persona': instance.persona,
|
||||||
|
'description': instance.description,
|
||||||
|
'owner_id': instance.ownerId,
|
||||||
|
'set_type': instance.setType,
|
||||||
|
'owner_type': instance.ownerType,
|
||||||
|
'shuffle_questions': instance.shuffleQuestions,
|
||||||
|
};
|
||||||
46
lib/models/practice_question.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'practice_question.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class PracticeQuestion {
|
||||||
|
final int? id;
|
||||||
|
|
||||||
|
final int? points;
|
||||||
|
|
||||||
|
final String? tips;
|
||||||
|
|
||||||
|
@JsonKey(name: 'set_id')
|
||||||
|
final int? setId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'question_id')
|
||||||
|
final int? questionId;
|
||||||
|
|
||||||
|
@JsonKey(name: 'display_order')
|
||||||
|
final int? displayOrder;
|
||||||
|
|
||||||
|
@JsonKey(name: 'question_text')
|
||||||
|
final String? questionText;
|
||||||
|
|
||||||
|
@JsonKey(name: 'question_type')
|
||||||
|
final String? questionType;
|
||||||
|
|
||||||
|
@JsonKey(name: 'question_status')
|
||||||
|
final String? questionStatus;
|
||||||
|
|
||||||
|
const PracticeQuestion(
|
||||||
|
{this.id,
|
||||||
|
this.tips,
|
||||||
|
this.setId,
|
||||||
|
this.points,
|
||||||
|
this.questionId,
|
||||||
|
this.questionText,
|
||||||
|
this.questionType,
|
||||||
|
this.displayOrder,
|
||||||
|
this.questionStatus});
|
||||||
|
|
||||||
|
factory PracticeQuestion.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PracticeQuestionFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$PracticeQuestionToJson(this);
|
||||||
|
}
|
||||||
33
lib/models/practice_question.g.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'practice_question.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
PracticeQuestion _$PracticeQuestionFromJson(Map<String, dynamic> json) =>
|
||||||
|
PracticeQuestion(
|
||||||
|
id: (json['id'] as num?)?.toInt(),
|
||||||
|
tips: json['tips'] as String?,
|
||||||
|
setId: (json['set_id'] as num?)?.toInt(),
|
||||||
|
points: (json['points'] as num?)?.toInt(),
|
||||||
|
questionId: (json['question_id'] as num?)?.toInt(),
|
||||||
|
questionText: json['question_text'] as String?,
|
||||||
|
questionType: json['question_type'] as String?,
|
||||||
|
displayOrder: (json['display_order'] as num?)?.toInt(),
|
||||||
|
questionStatus: json['question_status'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$PracticeQuestionToJson(PracticeQuestion instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'points': instance.points,
|
||||||
|
'tips': instance.tips,
|
||||||
|
'set_id': instance.setId,
|
||||||
|
'question_id': instance.questionId,
|
||||||
|
'display_order': instance.displayOrder,
|
||||||
|
'question_text': instance.questionText,
|
||||||
|
'question_type': instance.questionType,
|
||||||
|
'question_status': instance.questionStatus,
|
||||||
|
};
|
||||||
|
|
@ -12,12 +12,10 @@ class UserModel {
|
||||||
|
|
||||||
final String? country;
|
final String? country;
|
||||||
|
|
||||||
|
|
||||||
final String? occupation;
|
final String? occupation;
|
||||||
|
|
||||||
final bool? userInfoLoaded;
|
final bool? userInfoLoaded;
|
||||||
|
|
||||||
|
|
||||||
@JsonKey(name: 'user_id')
|
@JsonKey(name: 'user_id')
|
||||||
final int? userId;
|
final int? userId;
|
||||||
|
|
||||||
|
|
@ -59,6 +57,37 @@ class UserModel {
|
||||||
this.profileCompleted,
|
this.profileCompleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserModel copyWith(
|
||||||
|
{int? userId,
|
||||||
|
String? email,
|
||||||
|
String? gender,
|
||||||
|
String? region,
|
||||||
|
String? country,
|
||||||
|
String? lastName,
|
||||||
|
String? birthday,
|
||||||
|
String? firstName,
|
||||||
|
String? occupation,
|
||||||
|
String? accessToken,
|
||||||
|
String? refreshToken,
|
||||||
|
bool? userInfoLoaded,
|
||||||
|
bool? profileCompleted,
|
||||||
|
String? profilePicture}) =>
|
||||||
|
UserModel(
|
||||||
|
email: email ?? this.email,
|
||||||
|
userId: userId ?? this.userId,
|
||||||
|
gender: gender ?? this.gender,
|
||||||
|
region: region ?? this.region,
|
||||||
|
country: country ?? this.country,
|
||||||
|
lastName: lastName ?? this.lastName,
|
||||||
|
birthday: birthday ?? this.birthday,
|
||||||
|
firstName: firstName ?? this.firstName,
|
||||||
|
occupation: occupation ?? this.occupation,
|
||||||
|
accessToken: accessToken ?? this.accessToken,
|
||||||
|
refreshToken: refreshToken ?? this.refreshToken,
|
||||||
|
userInfoLoaded: userInfoLoaded ?? this.userInfoLoaded,
|
||||||
|
profilePicture: profilePicture ?? this.profilePicture,
|
||||||
|
profileCompleted: profileCompleted ?? this.profileCompleted);
|
||||||
|
|
||||||
factory UserModel.fromJson(Map<String, dynamic> json) =>
|
factory UserModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$UserModelFromJson(json);
|
_$UserModelFromJson(json);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
|
||||||
accessToken: json['access_token'] as String?,
|
accessToken: json['access_token'] as String?,
|
||||||
refreshToken: json['refresh_token'] as String?,
|
refreshToken: json['refresh_token'] as String?,
|
||||||
profilePicture: json['profile_picture_url'] as String?,
|
profilePicture: json['profile_picture_url'] as String?,
|
||||||
|
userInfoLoaded: json['userInfoLoaded'] as bool?,
|
||||||
profileCompleted: json['profile_completed'] as bool?,
|
profileCompleted: json['profile_completed'] as bool?,
|
||||||
userInfoLoaded: json['userInfoLoaded'] as bool? ?? false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
|
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:yimaru_app/models/assessment.dart';
|
import 'package:yimaru_app/models/assessment.dart';
|
||||||
|
import 'package:yimaru_app/models/course_subcategory.dart';
|
||||||
|
import 'package:yimaru_app/models/course_category.dart';
|
||||||
|
import 'package:yimaru_app/models/course_lesson.dart';
|
||||||
|
import 'package:yimaru_app/models/course_progress.dart';
|
||||||
|
import 'package:yimaru_app/models/course.dart';
|
||||||
|
import 'package:yimaru_app/models/practice.dart';
|
||||||
|
import 'package:yimaru_app/models/practice_question.dart';
|
||||||
import 'package:yimaru_app/models/user_model.dart';
|
import 'package:yimaru_app/models/user_model.dart';
|
||||||
import 'package:yimaru_app/services/dio_service.dart';
|
import 'package:yimaru_app/services/dio_service.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||||
|
|
@ -8,13 +15,15 @@ import '../app/app.locator.dart';
|
||||||
import '../ui/common/enmus.dart';
|
import '../ui/common/enmus.dart';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
|
// Dependency injection
|
||||||
final _service = locator<DioService>();
|
final _service = locator<DioService>();
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> registerWithEmail(
|
||||||
|
Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$kBaseUrl/$kUserUrl/$kRegisterUrl',
|
'$kBaseUrl/$kUserBaseUrl/$kRegisterUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -37,8 +46,8 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Email Login
|
// Email login
|
||||||
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> login(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$kBaseUrl/$kLoginUrl',
|
'$kBaseUrl/$kLoginUrl',
|
||||||
|
|
@ -97,7 +106,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$kBaseUrl/$kUserUrl/$kVerifyOtpUrl',
|
'$kBaseUrl/$kUserBaseUrl/$kVerifyOtpUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
@ -124,7 +133,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$kBaseUrl/$kUserUrl/$kResendOtpUrl',
|
'$kBaseUrl/$kUserBaseUrl/$kResendOtpUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -152,7 +161,7 @@ class ApiService {
|
||||||
Map<String, dynamic> data) async {
|
Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$kBaseUrl/$kUserUrl/$kRequestResetCode',
|
'$kBaseUrl/$kUserBaseUrl/$kRequestResetCode',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -179,7 +188,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> resetPassword(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> resetPassword(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$kBaseUrl/$kUserUrl/$kResetPassword',
|
'$kBaseUrl/$kUserBaseUrl/$kResetPassword',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -206,7 +215,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
|
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.get(
|
Response response = await _service.dio.get(
|
||||||
'$kBaseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl',
|
'$kBaseUrl/$kUserBaseUrl/${user?.userId}/$kProfileStatusUrl',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
@ -233,7 +242,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> getProfileData(int? userId) async {
|
Future<Map<String, dynamic>> getProfileData(int? userId) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.get(
|
Response response = await _service.dio.get(
|
||||||
'$kBaseUrl/$kUserUrl/$kGetUserUrl/$userId',
|
'$kBaseUrl/$kUserBaseUrl/$kGetUserUrl',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
@ -261,7 +270,7 @@ class ApiService {
|
||||||
Map<String, dynamic> data) async {
|
Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.put(
|
Response response = await _service.dio.put(
|
||||||
'$kBaseUrl/$kUserUrl',
|
'$kBaseUrl/$kUserBaseUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -291,7 +300,7 @@ class ApiService {
|
||||||
late FormData formData;
|
late FormData formData;
|
||||||
if (data['profile_picture_url']
|
if (data['profile_picture_url']
|
||||||
.toString()
|
.toString()
|
||||||
.contains('com.example.yimaru_app/')) {
|
.contains('com.yimaru.lms.app/')) {
|
||||||
formData = FormData.fromMap({
|
formData = FormData.fromMap({
|
||||||
'file': data['profile_picture_url'].toString().isNotEmpty
|
'file': data['profile_picture_url'].toString().isNotEmpty
|
||||||
? MultipartFile.fromFileSync(
|
? MultipartFile.fromFileSync(
|
||||||
|
|
@ -313,7 +322,7 @@ class ApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$kBaseUrl/$kUserUrl/$userId/$kUpdateProfileImage',
|
'$kBaseUrl/$kUserBaseUrl/$userId/$kUpdateProfileImage',
|
||||||
data: formData,
|
data: formData,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -359,4 +368,195 @@ class ApiService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Course categories
|
||||||
|
Future<List<CourseCategory>> getCourseCategories() async {
|
||||||
|
try {
|
||||||
|
List<CourseCategory> categories = [];
|
||||||
|
|
||||||
|
final Response response = await _service.dio
|
||||||
|
.get('$kBaseUrl/$kCourseBaseUrl/$kCourseCategoryUrl');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var data = response.data;
|
||||||
|
var decodedData = data['data']['categories'] as List;
|
||||||
|
categories = decodedData.map(
|
||||||
|
(e) {
|
||||||
|
return CourseCategory.fromJson(e);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course subcategory
|
||||||
|
Future<List<CourseSubcategory>> getCourseSubcategories(int id) async {
|
||||||
|
try {
|
||||||
|
List<CourseSubcategory> subcategories = [];
|
||||||
|
|
||||||
|
final Response response = await _service.dio.get(
|
||||||
|
'$kBaseUrl/$kCourseBaseUrl/$kCourseCategoryUrl/$id/$kCoursesUrl');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var data = response.data;
|
||||||
|
var decodedData = data['data']['courses'] as List;
|
||||||
|
subcategories = decodedData.map(
|
||||||
|
(e) {
|
||||||
|
return CourseSubcategory.fromJson(e);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return subcategories;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub-courses
|
||||||
|
Future<List<Course>> getCourses(int id) async {
|
||||||
|
try {
|
||||||
|
List<Course> courses = [];
|
||||||
|
|
||||||
|
final Response response = await _service.dio
|
||||||
|
.get('$kBaseUrl/$kCourseBaseUrl/$kCoursesUrl/$id/$kSubcoursesUrl');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var data = response.data;
|
||||||
|
var decodedData = data['data']['sub_courses'] as List;
|
||||||
|
courses = decodedData.map(
|
||||||
|
(e) {
|
||||||
|
return Course.fromJson(e);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return courses;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course progress
|
||||||
|
Future<List<CourseProgress>> getCourseProgress(int id) async {
|
||||||
|
try {
|
||||||
|
List<CourseProgress> courseProgress = [];
|
||||||
|
|
||||||
|
final Response response =
|
||||||
|
await _service.dio.get('$kBaseUrl/$kCourseProgressUrl/$id');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var data = response.data;
|
||||||
|
var decodedData = data['data'] as List;
|
||||||
|
courseProgress = decodedData.map(
|
||||||
|
(e) {
|
||||||
|
return CourseProgress.fromJson(e);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return courseProgress;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course videos
|
||||||
|
Future<List<CourseLesson>> getCourseLessons(int id) async {
|
||||||
|
try {
|
||||||
|
List<CourseLesson> courseLessons = [];
|
||||||
|
|
||||||
|
final Response response = await _service.dio.get(
|
||||||
|
'$kBaseUrl/$kCourseBaseUrl/$kSubcoursesUrl/$id/$kPublishedVideos');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var data = response.data;
|
||||||
|
var decodedData = data['data'] as List;
|
||||||
|
courseLessons = decodedData.map(
|
||||||
|
(e) {
|
||||||
|
return CourseLesson.fromJson(e);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return courseLessons;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete lesson
|
||||||
|
Future<Map<String, dynamic>> completeLesson(int id) async {
|
||||||
|
try {
|
||||||
|
Response response = await _service.dio.post(
|
||||||
|
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteLessonUrl',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return {'status': ResponseStatus.success, 'message': 'Video completed'};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': ResponseStatus.failure,
|
||||||
|
'message': 'Unknown Error Occurred'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
return {
|
||||||
|
'status': ResponseStatus.failure,
|
||||||
|
'message': e.response?.data.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course practices
|
||||||
|
Future<List<Practice>> getCoursePractices(Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
List<Practice> coursePractices = [];
|
||||||
|
|
||||||
|
final Response response = await _service.dio
|
||||||
|
.get('$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice', data: data);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var data = response.data;
|
||||||
|
var decodedData = data['data'] as List;
|
||||||
|
coursePractices = decodedData.map(
|
||||||
|
(e) {
|
||||||
|
return Practice.fromJson(e);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return coursePractices;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course practic questions
|
||||||
|
Future<List<PracticeQuestion>> getCoursePracticeQuestions(int id) async {
|
||||||
|
try {
|
||||||
|
List<PracticeQuestion> coursePracticeQuestions = [];
|
||||||
|
|
||||||
|
final Response response = await _service.dio
|
||||||
|
.get('$kBaseUrl/$kPracticeBaseUrl/$id/$kCoursePracticeQuestions');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var data = response.data;
|
||||||
|
var decodedData = data['data'] as List;
|
||||||
|
coursePracticeQuestions = decodedData.map(
|
||||||
|
(e) {
|
||||||
|
return PracticeQuestion.fromJson(e);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return coursePracticeQuestions;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
lib/services/audio_player_service.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
|
import '../ui/common/helper_functions.dart';
|
||||||
|
|
||||||
|
class AudioPlayerService with ListenableServiceMixin {
|
||||||
|
final AudioPlayer _player = AudioPlayer();
|
||||||
|
|
||||||
|
AudioPlayer get player => _player;
|
||||||
|
|
||||||
|
AudioPlayerService() {
|
||||||
|
_player.setReleaseMode(ReleaseMode.stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streams
|
||||||
|
Stream<Duration> get positionStream => _player.onPositionChanged;
|
||||||
|
Stream<Duration> get durationStream => _player.onDurationChanged;
|
||||||
|
|
||||||
|
// Optional: player state
|
||||||
|
Stream<PlayerState> get stateStream => _player.onPlayerStateChanged;
|
||||||
|
|
||||||
|
Future<void> playUrl(String url) async {
|
||||||
|
final playableUrl = getPlayableUrl(url);
|
||||||
|
|
||||||
|
if (playableUrl == null) {
|
||||||
|
throw Exception("Invalid audio URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _player.play(UrlSource(playableUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> playLocal(String url) async {
|
||||||
|
|
||||||
|
|
||||||
|
await _player.play(UrlSource(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pause() async => await _player.pause();
|
||||||
|
|
||||||
|
Future<void> seek(Duration position) async => await _player.seek(position);
|
||||||
|
}
|
||||||
|
|
@ -4,16 +4,20 @@ import 'package:yimaru_app/models/user_model.dart';
|
||||||
import 'package:yimaru_app/services/secure_storage_service.dart';
|
import 'package:yimaru_app/services/secure_storage_service.dart';
|
||||||
|
|
||||||
class AuthenticationService with ListenableServiceMixin {
|
class AuthenticationService with ListenableServiceMixin {
|
||||||
|
// Dependency injection
|
||||||
final _secureService = locator<SecureStorageService>();
|
final _secureService = locator<SecureStorageService>();
|
||||||
|
|
||||||
AuthenticationService() {
|
// User data
|
||||||
listenToReactiveValues([_user]);
|
|
||||||
}
|
|
||||||
|
|
||||||
UserModel? _user;
|
UserModel? _user;
|
||||||
|
|
||||||
UserModel? get user => _user;
|
UserModel? get user => _user;
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
AuthenticationService() {
|
||||||
|
listenToReactiveValues([_user]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user logged in
|
||||||
Future<bool> userLoggedIn() async {
|
Future<bool> userLoggedIn() async {
|
||||||
if (await _secureService.getString('userId') != null) {
|
if (await _secureService.getString('userId') != null) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -21,14 +25,18 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get access token
|
||||||
Future<String?> getAccessToken() async =>
|
Future<String?> getAccessToken() async =>
|
||||||
await _secureService.getString('accessToken');
|
await _secureService.getString('accessToken');
|
||||||
|
|
||||||
|
// Get refresh token
|
||||||
Future<String?> getRefreshToken() async =>
|
Future<String?> getRefreshToken() async =>
|
||||||
await _secureService.getString('refreshToken');
|
await _secureService.getString('refreshToken');
|
||||||
|
|
||||||
|
// Get user id
|
||||||
Future<int?> getUserId() async => await _secureService.getInt('userId');
|
Future<int?> getUserId() async => await _secureService.getInt('userId');
|
||||||
|
|
||||||
|
// Save tokens
|
||||||
Future<void> saveTokens({
|
Future<void> saveTokens({
|
||||||
required String access,
|
required String access,
|
||||||
required String refresh,
|
required String refresh,
|
||||||
|
|
@ -37,7 +45,7 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
await _secureService.setString('refreshToken', refresh);
|
await _secureService.setString('refreshToken', refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save user credential
|
||||||
Future<void> saveUserCredential(Map<String, dynamic> data) async {
|
Future<void> saveUserCredential(Map<String, dynamic> data) async {
|
||||||
await _secureService.setInt('userId', data['userId']);
|
await _secureService.setInt('userId', data['userId']);
|
||||||
await _secureService.setString('accessToken', data['accessToken']);
|
await _secureService.setString('accessToken', data['accessToken']);
|
||||||
|
|
@ -50,10 +58,16 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save profile status
|
||||||
Future<void> saveProfileStatus(bool value) async {
|
Future<void> saveProfileStatus(bool value) async {
|
||||||
await _secureService.setBool('profileCompleted', value);
|
await _secureService.setBool('profileCompleted', value);
|
||||||
|
|
||||||
_user = UserModel(
|
_user = _user?.copyWith(
|
||||||
|
userInfoLoaded: _user?.userInfoLoaded ?? false,
|
||||||
|
profileCompleted: await _secureService.getBool('profileCompleted'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/* UserModel(
|
||||||
email: _user?.email,
|
email: _user?.email,
|
||||||
gender: _user?.gender,
|
gender: _user?.gender,
|
||||||
region: _user?.region,
|
region: _user?.region,
|
||||||
|
|
@ -68,12 +82,18 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
profilePicture: _user?.profilePicture,
|
profilePicture: _user?.profilePicture,
|
||||||
userInfoLoaded: _user?.userInfoLoaded ?? false,
|
userInfoLoaded: _user?.userInfoLoaded ?? false,
|
||||||
profileCompleted: await _secureService.getBool('profileCompleted'));
|
profileCompleted: await _secureService.getBool('profileCompleted'));
|
||||||
|
*/
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveProfileImage(String image) async {
|
Future<void> saveProfilePicture(String image) async {
|
||||||
await _secureService.setString('profileImage', image);
|
await _secureService.setString('profilePicture', image);
|
||||||
_user = UserModel(
|
_user = _user?.copyWith(
|
||||||
|
userInfoLoaded: _user?.userInfoLoaded ?? false,
|
||||||
|
profilePicture: await _secureService.getString('profilePicture'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/*UserModel(
|
||||||
email: _user?.email,
|
email: _user?.email,
|
||||||
gender: _user?.gender,
|
gender: _user?.gender,
|
||||||
region: _user?.region,
|
region: _user?.region,
|
||||||
|
|
@ -87,18 +107,16 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
refreshToken: _user?.refreshToken,
|
refreshToken: _user?.refreshToken,
|
||||||
profileCompleted: _user?.profileCompleted,
|
profileCompleted: _user?.profileCompleted,
|
||||||
userInfoLoaded: _user?.userInfoLoaded ?? false,
|
userInfoLoaded: _user?.userInfoLoaded ?? false,
|
||||||
profilePicture: await _secureService.getString('profileImage'),
|
profilePicture: await _secureService.getString('profilePicture'),
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveUserData(
|
Future<void> saveUserData(UserModel data) async {
|
||||||
{required String image, required UserModel data}) async {
|
|
||||||
await _secureService.setBool('userInfoLoaded', true);
|
await _secureService.setBool('userInfoLoaded', true);
|
||||||
await _secureService.setBool(
|
await _secureService.setBool(
|
||||||
'profileCompleted', data.profileCompleted ?? false);
|
'profileCompleted', data.profileCompleted ?? false);
|
||||||
await _secureService.setString('profilePicture', image);
|
|
||||||
await _secureService.setString('email', data.email ?? '');
|
await _secureService.setString('email', data.email ?? '');
|
||||||
await _secureService.setString('region', data.region ?? '');
|
await _secureService.setString('region', data.region ?? '');
|
||||||
await _secureService.setString('gender', data.gender ?? '');
|
await _secureService.setString('gender', data.gender ?? '');
|
||||||
|
|
@ -113,7 +131,6 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
gender: data.gender,
|
gender: data.gender,
|
||||||
region: data.region,
|
region: data.region,
|
||||||
userInfoLoaded: true,
|
userInfoLoaded: true,
|
||||||
profilePicture: image,
|
|
||||||
userId: _user?.userId,
|
userId: _user?.userId,
|
||||||
country: data.country,
|
country: data.country,
|
||||||
lastName: data.lastName,
|
lastName: data.lastName,
|
||||||
|
|
@ -137,7 +154,17 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
await _secureService.setString('firstName', data['first_name']);
|
await _secureService.setString('firstName', data['first_name']);
|
||||||
await _secureService.setString('occupation', data['occupation']);
|
await _secureService.setString('occupation', data['occupation']);
|
||||||
|
|
||||||
_user = UserModel(
|
_user = _user?.copyWith(
|
||||||
|
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'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/*UserModel(
|
||||||
email: _user?.email,
|
email: _user?.email,
|
||||||
userId: _user?.userId,
|
userId: _user?.userId,
|
||||||
accessToken: _user?.accessToken,
|
accessToken: _user?.accessToken,
|
||||||
|
|
@ -151,7 +178,7 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
birthday: await _secureService.getString('birthday'),
|
birthday: await _secureService.getString('birthday'),
|
||||||
firstName: await _secureService.getString('firstName'),
|
firstName: await _secureService.getString('firstName'),
|
||||||
occupation: await _secureService.getString('occupation'),
|
occupation: await _secureService.getString('occupation'),
|
||||||
);
|
);*/
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,14 +202,14 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
occupation: await _secureService.getString('occupation'),
|
occupation: await _secureService.getString('occupation'),
|
||||||
accessToken: await _secureService.getString('accessToken'),
|
accessToken: await _secureService.getString('accessToken'),
|
||||||
refreshToken: await _secureService.getString('refreshToken'),
|
refreshToken: await _secureService.getString('refreshToken'),
|
||||||
profilePicture: await _secureService.getString('profileImage'),
|
|
||||||
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
|
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
|
||||||
|
profilePicture: await _secureService.getString('profilePicture'),
|
||||||
profileCompleted: await _secureService.getBool('profileCompleted'),
|
profileCompleted: await _secureService.getBool('profileCompleted'),
|
||||||
);
|
);
|
||||||
return _user;
|
return _user;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logOut() async {
|
Future<void> logout() async {
|
||||||
bool firstTimeInstall = await isFirstTimeInstall();
|
bool firstTimeInstall = await isFirstTimeInstall();
|
||||||
_user = null;
|
_user = null;
|
||||||
await _secureService.clear();
|
await _secureService.clear();
|
||||||
|
|
|
||||||
25
lib/services/course_service.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
|
import 'package:yimaru_app/models/course_progress.dart';
|
||||||
|
import 'package:yimaru_app/services/api_service.dart';
|
||||||
|
|
||||||
|
import '../models/course_detail.dart';
|
||||||
|
|
||||||
|
class CourseService {
|
||||||
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
Future<List<CourseDetail>> getCoursesDetail(int id) async {
|
||||||
|
final courses = await _apiService.getCourses(id);
|
||||||
|
final progress = await _apiService.getCourseProgress(id);
|
||||||
|
|
||||||
|
final progressMap = {
|
||||||
|
for (var p in progress.whereType<CourseProgress>()) p.courseId: p
|
||||||
|
};
|
||||||
|
|
||||||
|
return courses.map((course) {
|
||||||
|
return CourseDetail(
|
||||||
|
course: course,
|
||||||
|
courseProgress: progressMap[course.id],
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,15 +9,21 @@ import '../app/app.locator.dart';
|
||||||
import '../ui/common/app_constants.dart';
|
import '../ui/common/app_constants.dart';
|
||||||
|
|
||||||
class DioService {
|
class DioService {
|
||||||
|
// Dependency injection
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
|
// Initialization
|
||||||
final Dio _dio = Dio();
|
final Dio _dio = Dio();
|
||||||
|
|
||||||
|
Dio get dio => _dio;
|
||||||
|
|
||||||
final Dio _refreshDio = Dio(); // separate instance
|
final Dio _refreshDio = Dio(); // separate instance
|
||||||
|
|
||||||
bool _isRefreshing = false;
|
bool _isRefreshing = false;
|
||||||
final List<void Function()> _retryQueue = [];
|
final List<void Function()> _retryQueue = [];
|
||||||
|
|
||||||
|
// Initialization
|
||||||
DioService() {
|
DioService() {
|
||||||
_dio.options
|
_dio.options
|
||||||
..baseUrl = kBaseUrl
|
..baseUrl = kBaseUrl
|
||||||
|
|
@ -33,6 +39,7 @@ class DioService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Response logger
|
||||||
void _onResponse(
|
void _onResponse(
|
||||||
Response response,
|
Response response,
|
||||||
ResponseInterceptorHandler handler,
|
ResponseInterceptorHandler handler,
|
||||||
|
|
@ -69,6 +76,7 @@ class DioService {
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error logger
|
||||||
Future<void> _onError(
|
Future<void> _onError(
|
||||||
DioException error,
|
DioException error,
|
||||||
ErrorInterceptorHandler handler,
|
ErrorInterceptorHandler handler,
|
||||||
|
|
@ -125,6 +133,7 @@ class DioService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh token
|
||||||
Future<bool> _refreshToken() async {
|
Future<bool> _refreshToken() async {
|
||||||
final UserModel? user = await _authenticationService.getUser();
|
final UserModel? user = await _authenticationService.getUser();
|
||||||
|
|
||||||
|
|
@ -149,15 +158,14 @@ class DioService {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await _authenticationService.logOut();
|
await _authenticationService.logout();
|
||||||
await _navigationService.replaceWithLoginView();
|
await _navigationService.replaceWithLoginView();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check request if immediately after token refreshed
|
||||||
bool _isRefreshRequest(RequestOptions options) {
|
bool _isRefreshRequest(RequestOptions options) {
|
||||||
return options.path.contains(kRefreshTokenUrl);
|
return options.path.contains(kRefreshTokenUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
Dio get dio => _dio;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,39 @@
|
||||||
import 'package:google_sign_in/google_sign_in.dart';
|
import 'package:google_sign_in/google_sign_in.dart';
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||||
|
|
||||||
class GoogleAuthService {
|
class GoogleAuthService with ListenableServiceMixin {
|
||||||
final GoogleSignIn signIn = GoogleSignIn.instance;
|
// Initialization
|
||||||
|
final GoogleSignIn _signIn = GoogleSignIn.instance;
|
||||||
|
|
||||||
Future<GoogleSignInAccount?> googleAuth() async {
|
GoogleSignInAccount? _googleUser;
|
||||||
|
|
||||||
|
GoogleSignInAccount? get googleUser => _googleUser;
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
GoogleAuthService() {
|
||||||
|
listenToReactiveValues([_googleUser]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google logout
|
||||||
|
Future<void> logout() async {
|
||||||
|
await _signIn.signOut();
|
||||||
|
_googleUser = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google authentication
|
||||||
|
Future<void> googleAuth() async {
|
||||||
try {
|
try {
|
||||||
GoogleSignInAccount? googleUser;
|
await _signIn.initialize(serverClientId: kServerClientId).then((_) async {
|
||||||
await signIn.initialize(serverClientId: kServerClientId).then((_) async {
|
_googleUser = await _signIn.attemptLightweightAuthentication();
|
||||||
googleUser = await signIn.attemptLightweightAuthentication();
|
|
||||||
|
|
||||||
googleUser ??=
|
_googleUser ??=
|
||||||
await signIn.authenticate(scopeHint: ['email', 'profile']);
|
await _signIn.authenticate(scopeHint: ['email', 'profile']);
|
||||||
});
|
});
|
||||||
return googleUser;
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ import '../ui/common/app_constants.dart';
|
||||||
import 'dio_service.dart';
|
import 'dio_service.dart';
|
||||||
|
|
||||||
class ImageDownloaderService {
|
class ImageDownloaderService {
|
||||||
|
// Dependency injection
|
||||||
final _service = locator<DioService>();
|
final _service = locator<DioService>();
|
||||||
|
|
||||||
|
// Image downloader
|
||||||
Future<String> downloader(String? networkImage) async {
|
Future<String> downloader(String? networkImage) async {
|
||||||
late File image;
|
late File image;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,13 @@ import '../app/app.locator.dart';
|
||||||
import '../ui/common/ui_helpers.dart';
|
import '../ui/common/ui_helpers.dart';
|
||||||
|
|
||||||
class ImagePickerService {
|
class ImagePickerService {
|
||||||
|
// Dependency injection
|
||||||
final _permissionHandler = locator<PermissionHandlerService>();
|
final _permissionHandler = locator<PermissionHandlerService>();
|
||||||
|
|
||||||
|
// Initialization
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
|
// Pick image from gallery
|
||||||
Future<String?> gallery() async {
|
Future<String?> gallery() async {
|
||||||
try {
|
try {
|
||||||
PermissionStatus status =
|
PermissionStatus status =
|
||||||
|
|
@ -32,6 +35,7 @@ class ImagePickerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pick image from camera
|
||||||
Future<String?> camera() async {
|
Future<String?> camera() async {
|
||||||
try {
|
try {
|
||||||
PermissionStatus status =
|
PermissionStatus status =
|
||||||
|
|
|
||||||
138
lib/services/notification_service.dart
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
await locator<NotificationService>().setupFlutterNotifications();
|
||||||
|
await locator<NotificationService>().showNotification(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationService {
|
||||||
|
final _messaging = FirebaseMessaging.instance;
|
||||||
|
|
||||||
|
bool _isFlutterLocalNotificationInitialized = false;
|
||||||
|
|
||||||
|
final _localNotifications = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
// Initialize FCM token
|
||||||
|
await updateFCMToken();
|
||||||
|
|
||||||
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
|
|
||||||
|
// Request permission
|
||||||
|
await _requestPermission();
|
||||||
|
|
||||||
|
// setup message handle
|
||||||
|
await _setupMessageHandler();
|
||||||
|
|
||||||
|
// Subscribe to all devices
|
||||||
|
subscribeToTopic('yimaru');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestPermission() async {
|
||||||
|
await _messaging.requestPermission(
|
||||||
|
alert: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true,
|
||||||
|
carPlay: false,
|
||||||
|
provisional: false,
|
||||||
|
announcement: false,
|
||||||
|
criticalAlert: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setupFlutterNotifications() async {
|
||||||
|
if (_isFlutterLocalNotificationInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android setup
|
||||||
|
const channel = AndroidNotificationChannel(
|
||||||
|
'yimaru', // id
|
||||||
|
'Yimaru', // title
|
||||||
|
importance: Importance.high,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _localNotifications
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(channel);
|
||||||
|
|
||||||
|
const initializationSettingsAndroid =
|
||||||
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
|
||||||
|
// IOS setup
|
||||||
|
const initializationSettingsDarwin = DarwinInitializationSettings();
|
||||||
|
|
||||||
|
const initializationSettings = InitializationSettings(
|
||||||
|
android: initializationSettingsAndroid,
|
||||||
|
iOS: initializationSettingsDarwin);
|
||||||
|
|
||||||
|
// Flutter notification setup
|
||||||
|
await _localNotifications.initialize(
|
||||||
|
settings: initializationSettings,
|
||||||
|
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||||||
|
if (response.payload == 'Page') {
|
||||||
|
// navigatorKey.currentState?.pushNamed('RouteName');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_isFlutterLocalNotificationInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showNotification(RemoteMessage message) async {
|
||||||
|
RemoteNotification? notification = message.notification;
|
||||||
|
AndroidNotification? android = message.notification?.android;
|
||||||
|
|
||||||
|
if (notification != null && android != null) {
|
||||||
|
await _localNotifications.show(
|
||||||
|
id: notification.hashCode,
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.body,
|
||||||
|
notificationDetails: const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails('yimaru', 'Yimaru',
|
||||||
|
enableVibration: true,
|
||||||
|
priority: Priority.high,
|
||||||
|
icon: '@mipmap/ic_launcher',
|
||||||
|
importance: Importance.high),
|
||||||
|
iOS: DarwinNotificationDetails(
|
||||||
|
presentAlert: true, presentBadge: true, presentSound: true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupMessageHandler() async {
|
||||||
|
// Foreground message
|
||||||
|
FirebaseMessaging.onMessage
|
||||||
|
.listen((RemoteMessage message) => showNotification(message));
|
||||||
|
|
||||||
|
// Background message
|
||||||
|
FirebaseMessaging.onMessageOpenedApp.listen(_handleBackgroundMessage);
|
||||||
|
|
||||||
|
// Opened app
|
||||||
|
final initialMessage = await _messaging.getInitialMessage();
|
||||||
|
|
||||||
|
if (initialMessage != null) {
|
||||||
|
_handleBackgroundMessage(initialMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBackgroundMessage(RemoteMessage message) {
|
||||||
|
if (message.data['type'] == 'Page') {
|
||||||
|
// navigatorKey.currentState?.pushNamed('RouteName');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> subscribeToTopic(String topic) async {
|
||||||
|
await FirebaseMessaging.instance.subscribeToTopic(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateFCMToken() async {
|
||||||
|
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
|
||||||
|
_messaging.onTokenRefresh.listen((newToken) {
|
||||||
|
// updateTokenOnServer(newToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:permission_handler/permission_handler.dart';
|
||||||
import '../ui/common/ui_helpers.dart';
|
import '../ui/common/ui_helpers.dart';
|
||||||
|
|
||||||
class PermissionHandlerService {
|
class PermissionHandlerService {
|
||||||
|
// Check permission category
|
||||||
Future<PermissionStatus> requestPermission(
|
Future<PermissionStatus> requestPermission(
|
||||||
Permission requestedPermission) async {
|
Permission requestedPermission) async {
|
||||||
if (requestedPermission == Permission.camera) {
|
if (requestedPermission == Permission.camera) {
|
||||||
|
|
@ -17,6 +18,7 @@ class PermissionHandlerService {
|
||||||
return PermissionStatus.denied;
|
return PermissionStatus.denied;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request permission
|
||||||
Future<PermissionStatus> request(Permission permission) async {
|
Future<PermissionStatus> request(Permission permission) async {
|
||||||
if (await permission.isDenied) {
|
if (await permission.isDenied) {
|
||||||
final PermissionStatus status = await permission.request();
|
final PermissionStatus status = await permission.request();
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ extension BoolParsing on String {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SecureStorageService {
|
class SecureStorageService {
|
||||||
// Create storage
|
// Initialization
|
||||||
|
|
||||||
late final FlutterSecureStorage _storage;
|
late final FlutterSecureStorage _storage;
|
||||||
|
|
||||||
SecureStorageService() {
|
SecureStorageService() {
|
||||||
|
|
@ -31,33 +30,40 @@ class SecureStorageService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear storage data
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
_storage.deleteAll();
|
_storage.deleteAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get boolean data from storage
|
||||||
Future<bool?> getBool(String key) async {
|
Future<bool?> getBool(String key) async {
|
||||||
String? result = await _storage.read(key: key);
|
String? result = await _storage.read(key: key);
|
||||||
return result?.parseBool();
|
return result?.parseBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get string data from storage
|
||||||
Future<String?> getString(String key) async {
|
Future<String?> getString(String key) async {
|
||||||
return await _storage.read(key: key);
|
return await _storage.read(key: key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get integer data from storage
|
||||||
Future<int?> getInt(String key) async {
|
Future<int?> getInt(String key) async {
|
||||||
return await _storage.read(key: key) == null
|
return await _storage.read(key: key) == null
|
||||||
? null
|
? null
|
||||||
: int.parse(await _storage.read(key: key) ?? '0');
|
: int.parse(await _storage.read(key: key) ?? '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save string data to storage
|
||||||
Future<void> setString(String key, String value) async {
|
Future<void> setString(String key, String value) async {
|
||||||
await _storage.write(key: key, value: value);
|
await _storage.write(key: key, value: value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save integer data to storage
|
||||||
Future<void> setInt(String key, int value) async {
|
Future<void> setInt(String key, int value) async {
|
||||||
await _storage.write(key: key, value: value.toString());
|
await _storage.write(key: key, value: value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save boolean data to storage
|
||||||
Future<void> setBool(String key, bool value) async {
|
Future<void> setBool(String key, bool value) async {
|
||||||
await _storage.write(key: key, value: value.toString());
|
await _storage.write(key: key, value: value.toString());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
lib/services/smart_auth_service.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import 'package:pinput/pinput.dart';
|
||||||
|
import 'package:smart_auth/smart_auth.dart';
|
||||||
|
|
||||||
|
class SmartAuthService implements SmsRetriever {
|
||||||
|
final SmartAuth _smartAuth = SmartAuth.instance;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() => _smartAuth.removeUserConsentApiListener();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getSmsCode() async {
|
||||||
|
final res = await _smartAuth.getSmsWithUserConsentApi();
|
||||||
|
if (res.hasData) {
|
||||||
|
final code = res.requireData.code;
|
||||||
|
|
||||||
|
return code;
|
||||||
|
} else if (res.isCanceled) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get listenForMultipleSms => true;
|
||||||
|
}
|
||||||
|
|
@ -8,34 +8,28 @@ import 'package:yimaru_app/services/secure_storage_service.dart';
|
||||||
import '../app/app.locator.dart';
|
import '../app/app.locator.dart';
|
||||||
|
|
||||||
class StatusCheckerService {
|
class StatusCheckerService {
|
||||||
|
// Dependency injection
|
||||||
final storage = locator<SecureStorageService>();
|
final storage = locator<SecureStorageService>();
|
||||||
|
|
||||||
|
// Initialization
|
||||||
bool _previousConnection = true;
|
bool _previousConnection = true;
|
||||||
|
|
||||||
bool get previousConnection => _previousConnection;
|
bool get previousConnection => _previousConnection;
|
||||||
|
|
||||||
|
// Get phone battery level
|
||||||
Future<int> getBatteryLevel() async {
|
Future<int> getBatteryLevel() async {
|
||||||
final battery = Battery();
|
final battery = Battery();
|
||||||
final batteryLevel = await battery.batteryLevel;
|
final batteryLevel = await battery.batteryLevel;
|
||||||
return batteryLevel;
|
return batteryLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> userAuthenticated() async {
|
// Check internet connection
|
||||||
await checkAndUpdate();
|
|
||||||
|
|
||||||
if (await storage.getString('authenticated') != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> checkConnection() async {
|
Future<bool> checkConnection() async {
|
||||||
if (await InternetConnection().hasInternetAccess) {
|
if (await InternetConnection().hasInternetAccess) {
|
||||||
_previousConnection = true;
|
_previousConnection = true;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (_previousConnection) {
|
if (_previousConnection) {
|
||||||
// showErrorToast('Check your internet connection');
|
|
||||||
_previousConnection = false;
|
_previousConnection = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +37,7 @@ class StatusCheckerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check phone available storage
|
||||||
Future<int> getAvailableStorage() async {
|
Future<int> getAvailableStorage() async {
|
||||||
try {
|
try {
|
||||||
final availableStorage =
|
final availableStorage =
|
||||||
|
|
@ -53,6 +48,7 @@ class StatusCheckerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for latest update
|
||||||
Future<void> checkAndUpdate() async {
|
Future<void> checkAndUpdate() async {
|
||||||
const requiredStorage = 500 * 1024 * 1024;
|
const requiredStorage = 500 * 1024 * 1024;
|
||||||
|
|
||||||
|
|
@ -62,16 +58,12 @@ class StatusCheckerService {
|
||||||
await getAvailableStorage(); // Implement getAvailableStorage
|
await getAvailableStorage(); // Implement getAvailableStorage
|
||||||
if (batteryLevel < 20 || storageAvailable < requiredStorage) {
|
if (batteryLevel < 20 || storageAvailable < requiredStorage) {
|
||||||
if (batteryLevel < 20 || storageAvailable < requiredStorage) {
|
if (batteryLevel < 20 || storageAvailable < requiredStorage) {
|
||||||
// KewedeConst().showErrorToast(
|
|
||||||
// 'Unable to update app, please charge your phone & free up space.');
|
// 'Unable to update app, please charge your phone & free up space.');
|
||||||
} else if (batteryLevel < 20) {
|
} else if (batteryLevel < 20) {
|
||||||
// KewedeConst()
|
|
||||||
// .showErrorToast('Unable to update app, please charge your phone.');
|
// .showErrorToast('Unable to update app, please charge your phone.');
|
||||||
} else if (storageAvailable < requiredStorage) {
|
} else if (storageAvailable < requiredStorage) {
|
||||||
// KewedeConst()
|
|
||||||
// .showErrorToast('Unable to update app, please free up space.');
|
// .showErrorToast('Unable to update app, please free up space.');
|
||||||
}
|
}
|
||||||
// Show user-friendly message explaining why update failed and suggesting solutions (e.g., charge device, free up space)
|
|
||||||
return; // Prevent update from starting
|
return; // Prevent update from starting
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
35
lib/services/voice_recorder_service.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:waveform_recorder/waveform_recorder.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
|
class VoiceRecorderService with ListenableServiceMixin {
|
||||||
|
VoiceRecordingState _recordingState = VoiceRecordingState.pending;
|
||||||
|
|
||||||
|
VoiceRecordingState get recordingState => _recordingState;
|
||||||
|
|
||||||
|
final WaveformRecorderController _waveController =
|
||||||
|
WaveformRecorderController();
|
||||||
|
|
||||||
|
WaveformRecorderController get waveController => _waveController;
|
||||||
|
|
||||||
|
|
||||||
|
Future<void> startRecording() async {
|
||||||
|
|
||||||
|
await _waveController.startRecording();
|
||||||
|
_recordingState = VoiceRecordingState.recording;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopRecording() async {
|
||||||
|
await _waveController.stopRecording();
|
||||||
|
_recordingState = VoiceRecordingState.pending;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getRecordedAudio() async {
|
||||||
|
final file = _waveController.file;
|
||||||
|
print('RECORDED $file');
|
||||||
|
if (file == null) return null;
|
||||||
|
return file.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const Color kcBlack = Colors.black;
|
const Color kcBlack = Colors.black;
|
||||||
const Color kcRed = Color(0xffFF4C4C);
|
const Color kcRed = Color(0xffFF4C4C);
|
||||||
|
const Color kcBlue = Color(0xff135BEC);
|
||||||
const Color kcGreen = Color(0xFF1DE964);
|
const Color kcGreen = Color(0xFF1DE964);
|
||||||
const Color kcBackgroundColor = kcWhite;
|
const Color kcBackgroundColor = kcWhite;
|
||||||
const Color kcWhite = Color(0xFFFFFFFF);
|
const Color kcWhite = Color(0xFFFFFFFF);
|
||||||
|
|
@ -10,6 +11,7 @@ const Color kcIndigo = Color(0xff6A1B9A);
|
||||||
const Color kcOrange = Color(0xFFF79400);
|
const Color kcOrange = Color(0xFFF79400);
|
||||||
const Color kcSkyBlue = Color(0xFF28B4CD);
|
const Color kcSkyBlue = Color(0xFF28B4CD);
|
||||||
const Color kcDarkGrey = Color(0xFF1A1B1E);
|
const Color kcDarkGrey = Color(0xFF1A1B1E);
|
||||||
|
const Color kcDeepGreen = Color(0xFF078E37);
|
||||||
const Color kcMediumGrey = Color(0xFF474A54);
|
const Color kcMediumGrey = Color(0xFF474A54);
|
||||||
const Color kcAquamarine = Color(0xFF1DE9B6);
|
const Color kcAquamarine = Color(0xFF1DE9B6);
|
||||||
const Color kcTransparent = Colors.transparent;
|
const Color kcTransparent = Colors.transparent;
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,58 @@
|
||||||
String kBaseUrl = 'http://195.35.29.82:8080';
|
String kBaseUrl = 'https://api.yimaruacademy.com';
|
||||||
//String baseUrl = 'https://api.yimaru.yaltopia.com';
|
//String baseUrl = 'https://api.yimaru.yaltopia.com';
|
||||||
|
|
||||||
String kGetUserUrl = 'single';
|
String kCoursesUrl = 'courses';
|
||||||
|
|
||||||
String kUserUrl = 'api/v1/user';
|
|
||||||
|
|
||||||
String kRegisterUrl = 'register';
|
String kRegisterUrl = 'register';
|
||||||
|
|
||||||
|
String kCoursePractice = 'by-owner';
|
||||||
|
|
||||||
|
String kUserBaseUrl = 'api/v1/user';
|
||||||
|
|
||||||
String kVerifyOtpUrl = 'verify-otp';
|
String kVerifyOtpUrl = 'verify-otp';
|
||||||
|
|
||||||
String kResendOtpUrl = 'resend-otp';
|
String kResendOtpUrl = 'resend-otp';
|
||||||
|
|
||||||
|
String kGetUserUrl = 'user-profile';
|
||||||
|
|
||||||
|
String kSubcoursesUrl = 'sub-courses';
|
||||||
|
|
||||||
|
String kCompleteLessonUrl = 'complete';
|
||||||
|
|
||||||
String kResetPassword = 'resetPassword';
|
String kResetPassword = 'resetPassword';
|
||||||
|
|
||||||
|
String kCourseCategoryUrl = 'categories';
|
||||||
|
|
||||||
String kRequestResetCode = 'sendResetCode';
|
String kRequestResetCode = 'sendResetCode';
|
||||||
|
|
||||||
|
String kPublishedVideos = 'videos/published';
|
||||||
|
|
||||||
|
String kCoursePracticeQuestions = 'questions';
|
||||||
|
|
||||||
String kUpdateProfileImage = 'profile-picture';
|
String kUpdateProfileImage = 'profile-picture';
|
||||||
|
|
||||||
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
||||||
|
|
||||||
String kLoginUrl = 'api/v1/auth/customer-login';
|
String kLoginUrl = 'api/v1/auth/customer-login';
|
||||||
|
|
||||||
|
String kPracticeBaseUrl = 'api/v1/question-sets';
|
||||||
|
|
||||||
String kProfileStatusUrl = 'is-profile-completed';
|
String kProfileStatusUrl = 'is-profile-completed';
|
||||||
|
|
||||||
|
String kCourseBaseUrl = 'api/v1/course-management';
|
||||||
|
|
||||||
|
String kLessonProgressUrl = 'api/v1/progress/videos';
|
||||||
|
|
||||||
String kGoogleAuthUrl = 'api/v1/auth/google/android';
|
String kGoogleAuthUrl = 'api/v1/auth/google/android';
|
||||||
|
|
||||||
|
String kCourseProgressUrl = 'api/v1/progress/courses';
|
||||||
|
|
||||||
String kAssessmentsUrl = 'api/v1/assessment/questions';
|
String kAssessmentsUrl = 'api/v1/assessment/questions';
|
||||||
|
|
||||||
String kServerClientId =
|
String kEmptyImagePath = '/data/user/0/com.yimaru.lms.app/app_flutter';
|
||||||
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
|
|
||||||
|
|
||||||
String kSampleVideoUrl =
|
String kSampleVideoUrl =
|
||||||
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
|
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
|
||||||
|
|
||||||
|
String kServerClientId =
|
||||||
|
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
|
const String ksHomeBottomSheetTitle = 'Build Great Apps!';
|
||||||
|
|
||||||
const String ksSuggestion =
|
const String ksSuggestion =
|
||||||
"15 minutes a day can make you 3x more fluent in 3 month";
|
"15 minutes a day can make you 3x more fluent in 3 month";
|
||||||
const String ksHomeBottomSheetTitle = 'Build Great Apps!';
|
|
||||||
const String ksPrivacyPolicy =
|
|
||||||
'A brief, simple overview of Yimaru’s commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.';
|
|
||||||
const String ksHomeBottomSheetDescription =
|
const String ksHomeBottomSheetDescription =
|
||||||
'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more';
|
'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more';
|
||||||
|
|
||||||
|
const String ksPrivacyPolicy =
|
||||||
|
'A brief, simple overview of Yimaru’s commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.';
|
||||||
|
|
||||||
|
const String ksCategorySubtitle =
|
||||||
|
'Watch expert-led videos and reinforce your knowledge through guided practice activities.';
|
||||||
|
|
||||||
const String ksTerms = """
|
const String ksTerms = """
|
||||||
<p style="color:#9C2C91;font-size:13px;">
|
<p style="color:#9C2C91;font-size:13px;">
|
||||||
Last updated: October 26, 2025
|
Last updated: October 26, 2025
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,50 @@
|
||||||
// Registration type
|
// Login method
|
||||||
enum RegistrationType { phone, email }
|
enum LoginMethod { phone, email, google }
|
||||||
|
|
||||||
// Report status
|
// Response status
|
||||||
enum ResponseStatus { success, failure }
|
enum ResponseStatus { success, failure }
|
||||||
|
|
||||||
enum ProgressStatuses { pending, started, completed }
|
// Sign-up method
|
||||||
|
enum SignUpMethod { phone, email, google }
|
||||||
|
|
||||||
|
// Voice recording state
|
||||||
|
enum VoiceRecordingState { pending, recording }
|
||||||
|
|
||||||
// Levels
|
// Levels
|
||||||
enum ProficiencyLevels { a1, a2, b1, b2, none }
|
enum ProficiencyLevels { a1, a2, b1, b2, none }
|
||||||
|
|
||||||
|
// Progress status
|
||||||
|
enum ProgressStatuses { pending, started, completed }
|
||||||
|
|
||||||
|
// Duolingo assessment types
|
||||||
|
enum DuolingoAssessmentType { speaking, reading, writing, listening }
|
||||||
|
|
||||||
// State object
|
// State object
|
||||||
enum StateObjects {
|
enum StateObjects {
|
||||||
|
none,
|
||||||
|
courses,
|
||||||
|
homeView,
|
||||||
|
register,
|
||||||
verifyOtp,
|
verifyOtp,
|
||||||
resendOtp,
|
resendOtp,
|
||||||
profileImage,
|
profileImage,
|
||||||
|
courseLessons,
|
||||||
profileUpdate,
|
profileUpdate,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
|
subcategories,
|
||||||
loginWithEmail,
|
loginWithEmail,
|
||||||
|
coursePractice,
|
||||||
loginWithGoogle,
|
loginWithGoogle,
|
||||||
loadLessonVideo,
|
loadLessonVideo,
|
||||||
|
loadCourseVideo,
|
||||||
requestResetCode,
|
requestResetCode,
|
||||||
registerWithEmail,
|
courseCategories,
|
||||||
profileCompletion,
|
profileCompletion,
|
||||||
registerWithGoogle,
|
registerWithGoogle,
|
||||||
|
learnPracticeSample,
|
||||||
|
learnPracticeAnswer,
|
||||||
|
loginWithPhoneNumber,
|
||||||
|
learnPracticeQuestion,
|
||||||
|
recordLearnPracticeAnswer,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
// Split full name
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'app_colors.dart';
|
||||||
|
|
||||||
Map<String, String> splitFullName(String fullName) {
|
Map<String, String> splitFullName(String fullName) {
|
||||||
final parts = fullName.trim().split(RegExp(r'\s+'));
|
final parts = fullName.trim().split(RegExp(r'\s+'));
|
||||||
|
|
||||||
|
|
@ -15,3 +21,50 @@ Map<String, String> splitFullName(String fullName) {
|
||||||
'last_name': parts.sublist(1).join(' '),
|
'last_name': parts.sublist(1).join(' '),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color getColor() {
|
||||||
|
final generator = Random();
|
||||||
|
int random = generator.nextInt(8);
|
||||||
|
if (random == 1) {
|
||||||
|
return kcRed.withValues(alpha: 0.2);
|
||||||
|
} else if (random == 2) {
|
||||||
|
return kcPrimaryColor.withValues(alpha: 0.2);
|
||||||
|
} else if (random == 3) {
|
||||||
|
return kcOrange.withValues(alpha: 0.2);
|
||||||
|
} else if (random == 4) {
|
||||||
|
return kcGreen.withValues(alpha: 0.2);
|
||||||
|
} else if (random == 5) {
|
||||||
|
return kcBlue.withValues(alpha: 0.2);
|
||||||
|
} else if (random == 6) {
|
||||||
|
return kcSkyBlue.withValues(alpha: 0.2);
|
||||||
|
} else if (random == 7) {
|
||||||
|
return kcIndigo.withValues(alpha: 0.2);
|
||||||
|
} else {
|
||||||
|
return kcAquamarine.withValues(alpha: 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getPlayableUrl(String url) {
|
||||||
|
try {
|
||||||
|
// Case 1: /file/d/FILE_ID/view
|
||||||
|
final fileIdRegex = RegExp(r'/file/d/([a-zA-Z0-9_-]+)');
|
||||||
|
final match1 = fileIdRegex.firstMatch(url);
|
||||||
|
|
||||||
|
if (match1 != null) {
|
||||||
|
final fileId = match1.group(1);
|
||||||
|
return "https://drive.google.com/uc?export=download&id=$fileId";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: open?id=FILE_ID
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (uri.queryParameters.containsKey('id')) {
|
||||||
|
final fileId = uri.queryParameters['id'];
|
||||||
|
return "https://drive.google.com/uc?export=download&id=$fileId";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already converted or normal URL
|
||||||
|
return url;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:pinput/pinput.dart';
|
import 'package:pinput/pinput.dart';
|
||||||
import 'package:toastification/toastification.dart';
|
import 'package:toastification/toastification.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
|
|
@ -178,6 +179,12 @@ TextStyle style18P600 = const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style16W600 = const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: kcWhite,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style18W600 = const TextStyle(
|
TextStyle style18W600 = const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: kcWhite,
|
color: kcWhite,
|
||||||
|
|
@ -190,6 +197,11 @@ TextStyle style25W600 = const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style12RP600 = const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: kcPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style12R700 = const TextStyle(
|
TextStyle style12R700 = const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -197,10 +209,24 @@ TextStyle style12R700 = const TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style12P400 = const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: kcPrimaryColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
TextStyle style12DG400 = const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: kcDarkGrey,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style14P400 = const TextStyle(
|
TextStyle style14P400 = const TextStyle(
|
||||||
color: kcPrimaryColor,
|
color: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style14B400 = const TextStyle(
|
||||||
|
color: kcBlue,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style14P600 = const TextStyle(
|
TextStyle style14P600 = const TextStyle(
|
||||||
color: kcPrimaryColor,
|
color: kcPrimaryColor,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -218,12 +244,29 @@ TextStyle style25DG600 = const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style16P600 = const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: kcPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
);
|
||||||
|
|
||||||
|
TextStyle style16DG500 = const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: kcDarkGrey,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style16DG600 = const TextStyle(
|
TextStyle style16DG600 = const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style16B600 = const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: kcBlue,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style18DG500 = const TextStyle(
|
TextStyle style18DG500 = const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
|
|
@ -236,6 +279,24 @@ TextStyle style18DG600 = const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style18G700 = const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: kcDeepGreen,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
);
|
||||||
|
|
||||||
|
TextStyle style18DG700 = const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: kcDarkGrey,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
);
|
||||||
|
|
||||||
|
TextStyle style20DG700 = const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
color: kcDarkGrey,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style16DG400 = const TextStyle(
|
TextStyle style16DG400 = const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
|
|
@ -266,6 +327,8 @@ TextStyle validationStyle = const TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Duration kDuration = const Duration(seconds: 1);
|
||||||
|
|
||||||
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
|
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
|
||||||
|
|
||||||
Map<String, Style> htmlStyle = {
|
Map<String, Style> htmlStyle = {
|
||||||
|
|
@ -296,13 +359,19 @@ ChewieProgressColors buildChewieProgressIndicator = ChewieProgressColors(
|
||||||
Widget buildToastDescription(String message) => Text(
|
Widget buildToastDescription(String message) => Text(
|
||||||
message,
|
message,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500),
|
style: style14DG500,
|
||||||
|
);
|
||||||
|
|
||||||
|
Icon buildCloseIcon() => const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
void showErrorToast(String message) {
|
void showErrorToast(String message) {
|
||||||
toastification.show(
|
toastification.show(
|
||||||
showIcon: true,
|
showIcon: true,
|
||||||
dragToClose: true,
|
dragToClose: true,
|
||||||
|
icon: buildCloseIcon(),
|
||||||
showProgressBar: false,
|
showProgressBar: false,
|
||||||
applyBlurEffect: false,
|
applyBlurEffect: false,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
|
|
@ -313,19 +382,21 @@ void showErrorToast(String message) {
|
||||||
autoCloseDuration: const Duration(seconds: 3),
|
autoCloseDuration: const Duration(seconds: 3),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 15),
|
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
borderSide: const BorderSide(color: kcPrimaryColor),
|
borderSide: const BorderSide(color: kcPrimaryColor),
|
||||||
icon: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: kcPrimaryColor,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Icon buildCheckIcon() => const Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: kcPrimaryColor,
|
||||||
|
);
|
||||||
|
|
||||||
void showSuccessToast(String message) {
|
void showSuccessToast(String message) {
|
||||||
toastification.show(
|
toastification.show(
|
||||||
showIcon: true,
|
showIcon: true,
|
||||||
dragToClose: true,
|
dragToClose: true,
|
||||||
showProgressBar: false,
|
showProgressBar: false,
|
||||||
applyBlurEffect: false,
|
applyBlurEffect: false,
|
||||||
|
icon: buildCheckIcon(),
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
primaryColor: kcBackgroundColor,
|
primaryColor: kcBackgroundColor,
|
||||||
type: ToastificationType.success,
|
type: ToastificationType.success,
|
||||||
|
|
@ -334,9 +405,5 @@ void showSuccessToast(String message) {
|
||||||
autoCloseDuration: const Duration(seconds: 3),
|
autoCloseDuration: const Duration(seconds: 3),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 15),
|
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
borderSide: const BorderSide(color: kcPrimaryColor),
|
borderSide: const BorderSide(color: kcPrimaryColor),
|
||||||
icon: const Icon(
|
|
||||||
Icons.check,
|
|
||||||
color: kcPrimaryColor,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,67 @@
|
||||||
import 'package:email_validator/email_validator.dart';
|
import 'package:email_validator/email_validator.dart';
|
||||||
|
|
||||||
class FormValidator {
|
class FormValidator {
|
||||||
|
// Form validator
|
||||||
static String? validateForm(String? value) {
|
static String? validateForm(String? value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return 'The field is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form validator
|
||||||
|
static String? validateFullNameForm(String? value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return 'The field is required';
|
||||||
|
}
|
||||||
|
final regex = RegExp(r'^\S+\s+\S+.*$');
|
||||||
|
|
||||||
|
if (!regex.hasMatch(value.trim())) {
|
||||||
|
return "Enter your full name";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validator
|
||||||
|
static String? validateEmailForm(String? value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return 'The field is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EmailValidator.validate(value)) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password validator
|
||||||
|
static String? validatePasswordForm(String? value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (value.isEmpty) {
|
if (value.isEmpty) {
|
||||||
return 'The field is required';
|
return 'The field is required';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? validatePhoneNumber(String? value) {
|
// Phone number validator
|
||||||
|
static String? validatePhoneNumberForm(String? value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -34,31 +83,4 @@ class FormValidator {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? validateEmail(String? value) {
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.isEmpty) {
|
|
||||||
return 'The field is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!EmailValidator.validate(value)) {
|
|
||||||
return 'Invalid email format';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String? validatePassword(String? value) {
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.isEmpty) {
|
|
||||||
return 'The field is required';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
const AccountPrivacyView({Key? key}) : super(key: key);
|
const AccountPrivacyView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AccountPrivacyViewModel viewModelBuilder(
|
AccountPrivacyViewModel viewModelBuilder(BuildContext context) =>
|
||||||
BuildContext context,
|
|
||||||
) =>
|
|
||||||
AccountPrivacyViewModel();
|
AccountPrivacyViewModel();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -57,8 +55,9 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar(
|
||||||
title: 'Account Privacy',
|
showBackButton: true,
|
||||||
onTap: viewModel.pop,
|
onTap: viewModel.pop,
|
||||||
|
title: 'Account Privacy',
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) =>
|
Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) =>
|
||||||
|
|
@ -107,7 +106,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
|
|
||||||
Widget _buildHeader(String title) => Text(
|
Widget _buildHeader(String title) => Text(
|
||||||
title,
|
title,
|
||||||
style: style18DG600,
|
style: style18DG700,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
|
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
|
||||||
|
|
@ -146,8 +145,8 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
);
|
);
|
||||||
Widget _buildDeleteButton() => CustomElevatedButton(
|
Widget _buildDeleteButton() => CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Delete Account',
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
text: 'Delete Account',
|
||||||
foregroundColor: kcRed,
|
foregroundColor: kcRed,
|
||||||
backgroundColor: kcRed.withOpacity(0.25),
|
backgroundColor: kcRed.withOpacity(0.25),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:yimaru_app/app/app.router.dart';
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
|
||||||
class AccountPrivacyViewModel extends BaseViewModel {
|
class AccountPrivacyViewModel extends BaseViewModel {
|
||||||
|
// Dependency injection
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/views/assessment/screens/Assessment_form_screen.dart';
|
import 'package:yimaru_app/ui/views/assessment/screens/Assessment_form_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/assessment/screens/assessment_completion_screen.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/assessment/screens/assessment_failure_screen.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/assessment/screens/assessment_intro_screen.dart';
|
import 'package:yimaru_app/ui/views/assessment/screens/assessment_intro_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/assessment/screens/assessment_result_screen.dart';
|
import 'package:yimaru_app/ui/views/assessment/screens/assessment_result_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/assessment/screens/result_analysis_screen.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/assessment/screens/retake_assessment_screen.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/assessment/screens/start_lesson_screen.dart';
|
import 'package:yimaru_app/ui/views/assessment/screens/start_lesson_screen.dart';
|
||||||
|
|
||||||
import 'assessment_viewmodel.dart';
|
import 'assessment_viewmodel.dart';
|
||||||
|
|
@ -23,13 +19,23 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AssessmentViewModel viewModelBuilder(BuildContext context) =>
|
||||||
|
AssessmentViewModel();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget builder(
|
Widget builder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
AssessmentViewModel viewModel,
|
AssessmentViewModel viewModel,
|
||||||
Widget? child,
|
Widget? child,
|
||||||
) =>
|
) =>
|
||||||
_buildAssessmentScreens(viewModel);
|
_buildAssessmentScreensWrapper(viewModel);
|
||||||
|
|
||||||
|
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>
|
||||||
|
PopScope(
|
||||||
|
canPop: viewModel.currentPage == 0 ? true : false,
|
||||||
|
onPopInvokedWithResult: (value, data) => viewModel.goBack(),
|
||||||
|
child: _buildAssessmentScreens(viewModel));
|
||||||
|
|
||||||
Widget _buildAssessmentScreens(AssessmentViewModel viewModel) => IndexedStack(
|
Widget _buildAssessmentScreens(AssessmentViewModel viewModel) => IndexedStack(
|
||||||
index: viewModel.currentPage,
|
index: viewModel.currentPage,
|
||||||
|
|
@ -53,21 +59,7 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
||||||
|
|
||||||
Widget _buildAssessment() => const AssessmentFormScreen();
|
Widget _buildAssessment() => const AssessmentFormScreen();
|
||||||
|
|
||||||
Widget _buildAssessmentFailure() => const AssessmentFailureScreen();
|
|
||||||
|
|
||||||
Widget _buildRetakeAssessment() => const RetakeAssessmentScreen();
|
|
||||||
|
|
||||||
Widget _buildResultAnalysis() => const ResultAnalysisScreen();
|
|
||||||
|
|
||||||
Widget _buildAssessmentCompletion() => const AssessmentCompletionScreen();
|
|
||||||
|
|
||||||
Widget _buildAssessmentResult() => const AssessmentResultScreen();
|
Widget _buildAssessmentResult() => const AssessmentResultScreen();
|
||||||
|
|
||||||
Widget _buildStartLesson() => const StartLessonScreen();
|
Widget _buildStartLesson() => const StartLessonScreen();
|
||||||
|
|
||||||
@override
|
|
||||||
AssessmentViewModel viewModelBuilder(
|
|
||||||
BuildContext context,
|
|
||||||
) =>
|
|
||||||
AssessmentViewModel();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
@ -13,9 +11,9 @@ import '../../../models/assessment.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
import '../home/home_view.dart';
|
|
||||||
|
|
||||||
class AssessmentViewModel extends BaseViewModel {
|
class AssessmentViewModel extends BaseViewModel {
|
||||||
|
// Dependency injection
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
final _dialogService = locator<DialogService>();
|
final _dialogService = locator<DialogService>();
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
@ -207,6 +205,7 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In-app navigation
|
||||||
void next({int? page}) async {
|
void next({int? page}) async {
|
||||||
if (page == null) {
|
if (page == null) {
|
||||||
if (_previousPage != 0) {
|
if (_previousPage != 0) {
|
||||||
|
|
@ -221,28 +220,37 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop() {
|
void goBack() {
|
||||||
if (_currentPage == 0 || _currentPage == 3 /*7*/) {
|
if (_currentPage == 0) {
|
||||||
_navigationService.back();
|
_navigationService.back();
|
||||||
} else if (_currentPage != 0 && _currentPage != 3) {
|
} else if (_currentPage == 2) {
|
||||||
|
_currentPage = 0;
|
||||||
|
rebuildUi();
|
||||||
|
} else if (_currentPage == 3) {
|
||||||
|
if (_proficiencyLevel != ProficiencyLevels.none) {
|
||||||
_currentPage--;
|
_currentPage--;
|
||||||
|
} else {
|
||||||
|
_currentPage = 0;
|
||||||
|
}
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToLanguage() async =>
|
Future<void> navigateToLanguage() async =>
|
||||||
await _navigationService.navigateToLanguageView();
|
await _navigationService.navigateToLanguageView();
|
||||||
|
|
||||||
Future<void> replaceWithHome() async =>
|
Future<void> replaceWithHome() async =>
|
||||||
await _navigationService.clearStackAndShowView(const HomeView());
|
await _navigationService.clearStackAndShow(Routes.homeView);
|
||||||
|
|
||||||
// Remote api call
|
// Remote api call
|
||||||
Future<void> getAssessments() async => await runBusyFuture(_getAssessments());
|
Future<void> getAssessments() async => await runBusyFuture(_getAssessments());
|
||||||
|
|
||||||
Future<void> _getAssessments() async {
|
Future<void> _getAssessments() async {
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
List<Assessment> response = await _apiService.getAssessments();
|
_assessments = await _apiService.getAssessments();
|
||||||
/*
|
/*
|
||||||
for (int i = 0; i < 6; i++) {
|
for (int i = 0; i < 6; i++) {
|
||||||
final generator = Random();
|
final generator = Random();
|
||||||
|
|
@ -250,7 +258,6 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
response.add(response[random]);
|
response.add(response[random]);
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
_assessments = response;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
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/ui_helpers.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/assessment/assessment_viewmodel.dart';
|
|
||||||
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
|
||||||
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
|
||||||
|
|
||||||
class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
|
|
||||||
const AssessmentCompletionScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, AssessmentViewModel viewModel) =>
|
|
||||||
_buildScaffoldWrapper(viewModel);
|
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold(
|
|
||||||
backgroundColor: kcBackgroundColor,
|
|
||||||
body: _buildScaffold(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildScaffold(AssessmentViewModel viewModel) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: _buildScaffoldChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
|
|
||||||
[_buildAppBar(), _buildExpandedBody(viewModel)];
|
|
||||||
|
|
||||||
Widget _buildAppBar() => const LargeAppBar(
|
|
||||||
showBackButton: false,
|
|
||||||
showLanguageSelection: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
|
||||||
Expanded(child: _buildBodyWrapper(viewModel));
|
|
||||||
|
|
||||||
Widget _buildBodyWrapper(AssessmentViewModel viewModel) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
||||||
child: _buildBody(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildBody(AssessmentViewModel viewModel) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: _buildBodyChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildBodyChildren(AssessmentViewModel viewModel) =>
|
|
||||||
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)];
|
|
||||||
|
|
||||||
Widget _buildUpperColumn(AssessmentViewModel viewModel) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: _buildUpperColumnChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren(AssessmentViewModel viewModel) => [
|
|
||||||
verticalSpaceLarge,
|
|
||||||
_buildIcon(),
|
|
||||||
verticalSpaceMedium,
|
|
||||||
_buildTitle(),
|
|
||||||
verticalSpaceSmall,
|
|
||||||
_buildSubtitle(),
|
|
||||||
];
|
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
|
||||||
'assets/icons/complete.svg',
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
|
||||||
'Assessment complete!',
|
|
||||||
style: style25DG600,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSubtitle() => Text(
|
|
||||||
'We’re now analyzing your speaking skills',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: style14MG400,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 50),
|
|
||||||
child: _buildContinueButton(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildContinueButton(AssessmentViewModel viewModel) =>
|
|
||||||
CustomElevatedButton(
|
|
||||||
height: 55,
|
|
||||||
borderRadius: 12,
|
|
||||||
text: 'View My Results',
|
|
||||||
foregroundColor: kcWhite,
|
|
||||||
onTap: () => viewModel.next(),
|
|
||||||
backgroundColor: kcPrimaryColor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
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/ui_helpers.dart';
|
|
||||||
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
|
|
||||||
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
|
|
||||||
|
|
||||||
import '../assessment_viewmodel.dart';
|
|
||||||
|
|
||||||
class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
|
|
||||||
const AssessmentFailureScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, AssessmentViewModel viewModel) =>
|
|
||||||
_buildScaffoldWrapper(viewModel);
|
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold(
|
|
||||||
backgroundColor: kcBackgroundColor,
|
|
||||||
body: _buildScaffold(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildScaffold(AssessmentViewModel viewModel) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: _buildScaffoldChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
|
|
||||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
|
||||||
|
|
||||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
|
||||||
showBackButton: false,
|
|
||||||
showLanguageSelection: true,
|
|
||||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
|
||||||
Expanded(child: _buildBodyWrapper(viewModel));
|
|
||||||
|
|
||||||
Widget _buildBodyWrapper(AssessmentViewModel viewModel) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
||||||
child: _buildBody(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildBody(AssessmentViewModel viewModel) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: _buildBodyChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildBodyChildren(AssessmentViewModel viewModel) =>
|
|
||||||
[_buildUpperColumn(viewModel), _buildLowerColumn(viewModel)];
|
|
||||||
|
|
||||||
Widget _buildUpperColumn(AssessmentViewModel viewModel) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: _buildUpperColumnChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren(AssessmentViewModel viewModel) => [
|
|
||||||
verticalSpaceLarge,
|
|
||||||
_buildIcon(),
|
|
||||||
verticalSpaceMedium,
|
|
||||||
_buildTitle(),
|
|
||||||
verticalSpaceSmall,
|
|
||||||
_buildSubtitle(),
|
|
||||||
];
|
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg');
|
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
|
||||||
'We didn’t get enough from your assessment',
|
|
||||||
style: style25DG600,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSubtitle() => Text(
|
|
||||||
'Your assessment wasn’t long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: style14MG400,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: _buildLowerColumnChildren(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> _buildLowerColumnChildren(AssessmentViewModel viewModel) => [
|
|
||||||
_buildContinueButton(viewModel),
|
|
||||||
verticalSpaceSmall,
|
|
||||||
_buildSkipButtonWrapper(viewModel)
|
|
||||||
];
|
|
||||||
|
|
||||||
Widget _buildContinueButton(AssessmentViewModel viewModel) =>
|
|
||||||
CustomElevatedButton(
|
|
||||||
height: 55,
|
|
||||||
safe: false,
|
|
||||||
borderRadius: 12,
|
|
||||||
text: 'Continue Assessment',
|
|
||||||
onTap: () => viewModel.next(),
|
|
||||||
foregroundColor: kcWhite,
|
|
||||||
backgroundColor: kcPrimaryColor,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSkipButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 50),
|
|
||||||
child: _buildSkipButton(viewModel),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSkipButton(AssessmentViewModel viewModel) =>
|
|
||||||
CustomElevatedButton(
|
|
||||||
height: 55,
|
|
||||||
text: 'Skip',
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: kcWhite,
|
|
||||||
borderColor: kcPrimaryColor,
|
|
||||||
onTap: () => viewModel.next(),
|
|
||||||
foregroundColor: kcPrimaryColor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||