feat(Course): Finalize course integration
|
|
@ -1,43 +1,79 @@
|
|||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("kotlin-android")
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.gms.google-services")
|
||||
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 {
|
||||
ndkVersion = flutter.ndkVersion
|
||||
namespace = "com.yimaru.lms.app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
minSdk = flutter.minSdkVersion
|
||||
applicationId = "com.yimaru.lms.app"
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
applicationId = "com.yimaru.lms.app"
|
||||
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 {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
getByName("release") {
|
||||
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 {
|
||||
source = "../.."
|
||||
}
|
||||
|
|
@ -14,19 +14,19 @@
|
|||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com",
|
||||
"client_id": "574860813475-glgnkruic7dflaomb59el8994b7hhfga.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.yimaru.lms.app",
|
||||
"certificate_hash": "fc91f52846d27c62bba3e16bc98982fb9953eca1"
|
||||
"certificate_hash": "378836a3aa9f36958b6c6c69bc67e3195352f68d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "574860813475-631s3mo8ha2qc2jeb5e2aosn0967niik.apps.googleusercontent.com",
|
||||
"client_id": "574860813475-m90u87plqaac4tb8oug32k41usossiod.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.yimaru.lms.app",
|
||||
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
|
||||
"certificate_hash": "29797902ad6a24212b9d9fad71562907956f6a6c"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
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.READ_MEDIA_IMAGES"/>
|
||||
<application
|
||||
android:label="Yimaru"
|
||||
android:label="Yimaru Academy"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<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"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</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"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</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
|
||||
the Flutter engine draws its first frame -->
|
||||
<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>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
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
|
||||
the Flutter engine draws its first frame -->
|
||||
<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>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
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
|
||||
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
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pluginManagement {
|
|||
|
||||
plugins {
|
||||
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.0.1" 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
|
||||
|
||||
|
|
|
|||
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" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"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">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||
<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>
|
||||
</view>
|
||||
</viewController>
|
||||
|
|
@ -32,6 +38,7 @@
|
|||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LaunchImage" width="1024" height="1024"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
|||
|
|
@ -45,5 +45,7 @@
|
|||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</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/downloads/downloads_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/support/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/assessment/assessment_view.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
|
||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
||||
import 'package:yimaru_app/services/permission_handler_service.dart';
|
||||
import 'package:yimaru_app/services/image_picker_service.dart';
|
||||
import 'package:yimaru_app/services/google_auth_service.dart';
|
||||
|
|
@ -38,10 +36,18 @@ import 'package:yimaru_app/services/image_downloader_service.dart';
|
|||
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course/course_view.dart';
|
||||
import 'package:yimaru_app/ui/views/course_module/course_module_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';
|
||||
// @stacked-import
|
||||
|
||||
@StackedApp(
|
||||
|
|
@ -53,7 +59,6 @@ import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
|||
MaterialRoute(page: ProfileDetailView),
|
||||
MaterialRoute(page: DownloadsView),
|
||||
MaterialRoute(page: ProgressView),
|
||||
MaterialRoute(page: OngoingProgressView),
|
||||
MaterialRoute(page: AccountPrivacyView),
|
||||
MaterialRoute(page: SupportView),
|
||||
MaterialRoute(page: TelegramSupportView),
|
||||
|
|
@ -69,14 +74,18 @@ import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
|||
MaterialRoute(page: WelcomeView),
|
||||
MaterialRoute(page: AssessmentView),
|
||||
MaterialRoute(page: LearnLessonView),
|
||||
MaterialRoute(page: FailureView),
|
||||
MaterialRoute(page: ForgetPasswordView),
|
||||
MaterialRoute(page: LearnLessonDetailView),
|
||||
MaterialRoute(page: LearnPracticeView),
|
||||
MaterialRoute(page: CourseView),
|
||||
MaterialRoute(page: CourseModuleView),
|
||||
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
|
||||
],
|
||||
dependencies: [
|
||||
|
|
@ -92,6 +101,9 @@ import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
|||
LazySingleton(classType: ImagePickerService),
|
||||
LazySingleton(classType: GoogleAuthService),
|
||||
LazySingleton(classType: ImageDownloaderService),
|
||||
LazySingleton(classType: NotificationService),
|
||||
LazySingleton(classType: SmartAuthService),
|
||||
LazySingleton(classType: CourseService),
|
||||
// @stacked-service
|
||||
],
|
||||
bottomsheets: [
|
||||
|
|
|
|||
|
|
@ -13,12 +13,15 @@ import 'package:stacked_shared/stacked_shared.dart';
|
|||
|
||||
import '../services/api_service.dart';
|
||||
import '../services/authentication_service.dart';
|
||||
import '../services/course_service.dart';
|
||||
import '../services/dio_service.dart';
|
||||
import '../services/google_auth_service.dart';
|
||||
import '../services/image_downloader_service.dart';
|
||||
import '../services/image_picker_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/permission_handler_service.dart';
|
||||
import '../services/secure_storage_service.dart';
|
||||
import '../services/smart_auth_service.dart';
|
||||
import '../services/status_checker_service.dart';
|
||||
|
||||
final locator = StackedLocator.instance;
|
||||
|
|
@ -44,4 +47,7 @@ Future<void> setupLocator({
|
|||
locator.registerLazySingleton(() => ImagePickerService());
|
||||
locator.registerLazySingleton(() => GoogleAuthService());
|
||||
locator.registerLazySingleton(() => ImageDownloaderService());
|
||||
locator.registerLazySingleton(() => NotificationService());
|
||||
locator.registerLazySingleton(() => SmartAuthService());
|
||||
locator.registerLazySingleton(() => CourseService());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class DefaultFirebaseOptions {
|
|||
projectId: 'yimaru-lms-e834e',
|
||||
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
|
||||
androidClientId:
|
||||
'574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com',
|
||||
'574860813475-glgnkruic7dflaomb59el8994b7hhfga.apps.googleusercontent.com',
|
||||
iosBundleId: 'com.yimaru.lms.app',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toastification/toastification.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.router.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/services/notification_service.dart';
|
||||
|
||||
import 'firebase_options.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await setupLocator();
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
await locator<NotificationService>().initialize();
|
||||
setupDialogUi();
|
||||
setupBottomSheetUi();
|
||||
runApp(const MainApp());
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ class Assessment {
|
|||
|
||||
final List<Option>? options;
|
||||
|
||||
|
||||
@JsonKey(name: 'question_type')
|
||||
final String? questionType;
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ class Assessment {
|
|||
@JsonKey(name: 'difficulty_level')
|
||||
final String? difficultyLevel;
|
||||
|
||||
|
||||
const Assessment({
|
||||
this.id,
|
||||
this.points,
|
||||
|
|
|
|||
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,
|
||||
};
|
||||
39
lib/models/practice.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
import 'package:dio/dio.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/services/dio_service.dart';
|
||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||
|
|
@ -16,7 +23,7 @@ class ApiService {
|
|||
Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kRegisterUrl',
|
||||
'$kBaseUrl/$kUserBaseUrl/$kRegisterUrl',
|
||||
data: data,
|
||||
);
|
||||
|
||||
|
|
@ -40,7 +47,7 @@ class ApiService {
|
|||
}
|
||||
|
||||
// Email login
|
||||
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
|
||||
Future<Map<String, dynamic>> login(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kLoginUrl',
|
||||
|
|
@ -99,7 +106,7 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kVerifyOtpUrl',
|
||||
'$kBaseUrl/$kUserBaseUrl/$kVerifyOtpUrl',
|
||||
data: data,
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -126,7 +133,7 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kResendOtpUrl',
|
||||
'$kBaseUrl/$kUserBaseUrl/$kResendOtpUrl',
|
||||
data: data,
|
||||
);
|
||||
|
||||
|
|
@ -154,7 +161,7 @@ class ApiService {
|
|||
Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kRequestResetCode',
|
||||
'$kBaseUrl/$kUserBaseUrl/$kRequestResetCode',
|
||||
data: data,
|
||||
);
|
||||
|
||||
|
|
@ -181,7 +188,7 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> resetPassword(Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$kResetPassword',
|
||||
'$kBaseUrl/$kUserBaseUrl/$kResetPassword',
|
||||
data: data,
|
||||
);
|
||||
|
||||
|
|
@ -208,7 +215,7 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
|
||||
try {
|
||||
Response response = await _service.dio.get(
|
||||
'$kBaseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl',
|
||||
'$kBaseUrl/$kUserBaseUrl/${user?.userId}/$kProfileStatusUrl',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -235,7 +242,7 @@ class ApiService {
|
|||
Future<Map<String, dynamic>> getProfileData(int? userId) async {
|
||||
try {
|
||||
Response response = await _service.dio.get(
|
||||
'$kBaseUrl/$kUserUrl/$kGetUserUrl/$userId',
|
||||
'$kBaseUrl/$kUserBaseUrl/$kGetUserUrl',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -263,7 +270,7 @@ class ApiService {
|
|||
Map<String, dynamic> data) async {
|
||||
try {
|
||||
Response response = await _service.dio.put(
|
||||
'$kBaseUrl/$kUserUrl',
|
||||
'$kBaseUrl/$kUserBaseUrl',
|
||||
data: data,
|
||||
);
|
||||
|
||||
|
|
@ -293,7 +300,7 @@ class ApiService {
|
|||
late FormData formData;
|
||||
if (data['profile_picture_url']
|
||||
.toString()
|
||||
.contains('com.example.yimaru_app/')) {
|
||||
.contains('com.yimaru.lms.app/')) {
|
||||
formData = FormData.fromMap({
|
||||
'file': data['profile_picture_url'].toString().isNotEmpty
|
||||
? MultipartFile.fromFileSync(
|
||||
|
|
@ -315,7 +322,7 @@ class ApiService {
|
|||
});
|
||||
}
|
||||
Response response = await _service.dio.post(
|
||||
'$kBaseUrl/$kUserUrl/$userId/$kUpdateProfileImage',
|
||||
'$kBaseUrl/$kUserBaseUrl/$userId/$kUpdateProfileImage',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
|
|
@ -361,4 +368,197 @@ class ApiService {
|
|||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,12 +113,10 @@ class AuthenticationService with ListenableServiceMixin {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> saveUserData(
|
||||
{required String image, required UserModel data}) async {
|
||||
Future<void> saveUserData(UserModel data) async {
|
||||
await _secureService.setBool('userInfoLoaded', true);
|
||||
await _secureService.setBool(
|
||||
'profileCompleted', data.profileCompleted ?? false);
|
||||
await _secureService.setString('profilePicture', image);
|
||||
await _secureService.setString('email', data.email ?? '');
|
||||
await _secureService.setString('region', data.region ?? '');
|
||||
await _secureService.setString('gender', data.gender ?? '');
|
||||
|
|
@ -133,7 +131,6 @@ class AuthenticationService with ListenableServiceMixin {
|
|||
gender: data.gender,
|
||||
region: data.region,
|
||||
userInfoLoaded: true,
|
||||
profilePicture: image,
|
||||
userId: _user?.userId,
|
||||
country: data.country,
|
||||
lastName: data.lastName,
|
||||
|
|
@ -212,7 +209,7 @@ class AuthenticationService with ListenableServiceMixin {
|
|||
return _user;
|
||||
}
|
||||
|
||||
Future<void> logOut() async {
|
||||
Future<void> logout() async {
|
||||
bool firstTimeInstall = await isFirstTimeInstall();
|
||||
_user = null;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ class DioService {
|
|||
|
||||
return true;
|
||||
} catch (e) {
|
||||
await _authenticationService.logOut();
|
||||
await _authenticationService.logout();
|
||||
await _navigationService.replaceWithLoginView();
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,39 @@
|
|||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||
|
||||
class GoogleAuthService {
|
||||
class GoogleAuthService with ListenableServiceMixin {
|
||||
// Initialization
|
||||
final GoogleSignIn signIn = GoogleSignIn.instance;
|
||||
final GoogleSignIn _signIn = GoogleSignIn.instance;
|
||||
|
||||
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<GoogleSignInAccount?> googleAuth() async {
|
||||
Future<void> googleAuth() async {
|
||||
try {
|
||||
GoogleSignInAccount? googleUser;
|
||||
await signIn.initialize(serverClientId: kServerClientId).then((_) async {
|
||||
googleUser = await signIn.attemptLightweightAuthentication();
|
||||
await _signIn.initialize(serverClientId: kServerClientId).then((_) async {
|
||||
_googleUser = await _signIn.attemptLightweightAuthentication();
|
||||
|
||||
googleUser ??=
|
||||
await signIn.authenticate(scopeHint: ['email', 'profile']);
|
||||
_googleUser ??=
|
||||
await _signIn.authenticate(scopeHint: ['email', 'profile']);
|
||||
});
|
||||
return googleUser;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
return null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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,7 +3,6 @@ import 'package:permission_handler/permission_handler.dart';
|
|||
import '../ui/common/ui_helpers.dart';
|
||||
|
||||
class PermissionHandlerService {
|
||||
|
||||
// Check permission category
|
||||
Future<PermissionStatus> requestPermission(
|
||||
Permission requestedPermission) async {
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ const Color kcIndigo = Color(0xff6A1B9A);
|
|||
const Color kcOrange = Color(0xFFF79400);
|
||||
const Color kcSkyBlue = Color(0xFF28B4CD);
|
||||
const Color kcDarkGrey = Color(0xFF1A1B1E);
|
||||
const Color kcDeepGreen = Color(0xFF078E37);
|
||||
const Color kcMediumGrey = Color(0xFF474A54);
|
||||
const Color kcAquamarine = Color(0xFF1DE9B6);
|
||||
const Color kcTransparent = Colors.transparent;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,56 @@
|
|||
String kBaseUrl = 'http://195.35.29.82:8080';
|
||||
String kBaseUrl = 'https://api.yimaruacademy.com';
|
||||
//String baseUrl = 'https://api.yimaru.yaltopia.com';
|
||||
|
||||
String kGetUserUrl = 'single';
|
||||
|
||||
String kUserUrl = 'api/v1/user';
|
||||
String kCoursesUrl = 'courses';
|
||||
|
||||
String kRegisterUrl = 'register';
|
||||
|
||||
String kCoursePractice = 'by-owner';
|
||||
|
||||
String kUserBaseUrl = 'api/v1/user';
|
||||
|
||||
String kVerifyOtpUrl = 'verify-otp';
|
||||
|
||||
String kResendOtpUrl = 'resend-otp';
|
||||
|
||||
String kGetUserUrl = 'user-profile';
|
||||
|
||||
String kSubcoursesUrl = 'sub-courses';
|
||||
|
||||
String kCompleteLessonUrl = 'complete';
|
||||
|
||||
|
||||
String kResetPassword = 'resetPassword';
|
||||
|
||||
String kCourseCategoryUrl = 'categories';
|
||||
|
||||
String kRequestResetCode = 'sendResetCode';
|
||||
|
||||
String kPublishedVideos = 'videos/published';
|
||||
|
||||
String kCoursePracticeQuestions = 'questions';
|
||||
|
||||
String kUpdateProfileImage = 'profile-picture';
|
||||
|
||||
String kRefreshTokenUrl = 'api/v1/auth/refresh';
|
||||
|
||||
String kLoginUrl = 'api/v1/auth/customer-login';
|
||||
|
||||
String kPracticeBaseUrl = 'api/v1/question-sets';
|
||||
|
||||
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 kCourseProgressUrl = 'api/v1/progress/courses';
|
||||
|
||||
String kAssessmentsUrl = 'api/v1/assessment/questions';
|
||||
|
||||
String kEmptyImagePath = '/data/user/0/com.yimaru.lms.app/app_flutter';
|
||||
|
||||
String kSampleVideoUrl =
|
||||
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@ const String ksHomeBottomSheetTitle = 'Build Great Apps!';
|
|||
|
||||
const String ksSuggestion =
|
||||
"15 minutes a day can make you 3x more fluent in 3 month";
|
||||
|
||||
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';
|
||||
|
||||
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 = """
|
||||
<p style="color:#9C2C91;font-size:13px;">
|
||||
Last updated: October 26, 2025
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
// Registration type
|
||||
enum RegistrationType { phone, email }
|
||||
// Login method
|
||||
enum LoginMethod { phone, email, google }
|
||||
|
||||
// Sign-up method
|
||||
enum SignUpMethod { phone, email, google }
|
||||
|
||||
// Response status
|
||||
enum ResponseStatus { success, failure }
|
||||
|
|
@ -10,19 +13,29 @@ 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
|
||||
enum StateObjects {
|
||||
courses,
|
||||
homeView,
|
||||
register,
|
||||
verifyOtp,
|
||||
resendOtp,
|
||||
profileImage,
|
||||
courseLessons,
|
||||
profileUpdate,
|
||||
resetPassword,
|
||||
subcategories,
|
||||
loginWithEmail,
|
||||
coursePractice,
|
||||
loginWithGoogle,
|
||||
loadLessonVideo,
|
||||
loadCourseVideo,
|
||||
requestResetCode,
|
||||
registerWithEmail,
|
||||
courseCategories,
|
||||
profileCompletion,
|
||||
registerWithGoogle,
|
||||
loginWithPhoneNumber,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
|
||||
// Split full name
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'app_colors.dart';
|
||||
|
||||
Map<String, String> splitFullName(String fullName) {
|
||||
final parts = fullName.trim().split(RegExp(r'\s+'));
|
||||
|
||||
|
|
@ -17,3 +21,25 @@ Map<String, String> splitFullName(String fullName) {
|
|||
'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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||
|
|
@ -208,6 +209,11 @@ TextStyle style12R700 = const TextStyle(
|
|||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
|
||||
TextStyle style12P400 = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
TextStyle style12DG400 = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: kcDarkGrey,
|
||||
|
|
@ -267,6 +273,18 @@ TextStyle style18DG500 = const TextStyle(
|
|||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
|
||||
TextStyle style18DG600 = const TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcDarkGrey,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle style18G700 = const TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcDeepGreen,
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
|
||||
TextStyle style18DG700 = const TextStyle(
|
||||
fontSize: 18,
|
||||
color: kcDarkGrey,
|
||||
|
|
@ -309,6 +327,8 @@ TextStyle validationStyle = const TextStyle(
|
|||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
|
||||
Duration kDuration = const Duration(seconds: 1);
|
||||
|
||||
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
|
||||
|
||||
Map<String, Style> htmlStyle = {
|
||||
|
|
@ -339,13 +359,19 @@ ChewieProgressColors buildChewieProgressIndicator = ChewieProgressColors(
|
|||
Widget buildToastDescription(String message) => Text(
|
||||
message,
|
||||
maxLines: 4,
|
||||
style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500),
|
||||
style: style14DG500,
|
||||
);
|
||||
|
||||
Icon buildCloseIcon() => const Icon(
|
||||
Icons.close,
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
void showErrorToast(String message) {
|
||||
toastification.show(
|
||||
showIcon: true,
|
||||
dragToClose: true,
|
||||
icon: buildCloseIcon(),
|
||||
showProgressBar: false,
|
||||
applyBlurEffect: false,
|
||||
alignment: Alignment.topCenter,
|
||||
|
|
@ -356,19 +382,21 @@ void showErrorToast(String message) {
|
|||
autoCloseDuration: const Duration(seconds: 3),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||
borderSide: const BorderSide(color: kcPrimaryColor),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: kcPrimaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Icon buildCheckIcon() => const Icon(
|
||||
Icons.check,
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
void showSuccessToast(String message) {
|
||||
toastification.show(
|
||||
showIcon: true,
|
||||
dragToClose: true,
|
||||
showProgressBar: false,
|
||||
applyBlurEffect: false,
|
||||
icon: buildCheckIcon(),
|
||||
alignment: Alignment.topCenter,
|
||||
primaryColor: kcBackgroundColor,
|
||||
type: ToastificationType.success,
|
||||
|
|
@ -377,9 +405,5 @@ void showSuccessToast(String message) {
|
|||
autoCloseDuration: const Duration(seconds: 3),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15),
|
||||
borderSide: const BorderSide(color: kcPrimaryColor),
|
||||
icon: const Icon(
|
||||
Icons.check,
|
||||
color: kcPrimaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,29 @@ class FormValidator {
|
|||
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? validateEmail(String? value) {
|
||||
static String? validateEmailForm(String? value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -32,7 +49,7 @@ class FormValidator {
|
|||
}
|
||||
|
||||
// Password validator
|
||||
static String? validatePassword(String? value) {
|
||||
static String? validatePasswordForm(String? value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -44,7 +61,7 @@ class FormValidator {
|
|||
}
|
||||
|
||||
// Phone number validator
|
||||
static String? validatePhoneNumber(String? value) {
|
||||
static String? validatePhoneNumberForm(String? value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -66,5 +83,4 @@ class FormValidator {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import 'package:flutter/material.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_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_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 'assessment_viewmodel.dart';
|
||||
|
|
@ -24,7 +20,8 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
|||
}
|
||||
|
||||
@override
|
||||
AssessmentViewModel viewModelBuilder(BuildContext context) => AssessmentViewModel();
|
||||
AssessmentViewModel viewModelBuilder(BuildContext context) =>
|
||||
AssessmentViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
|
|
@ -32,7 +29,13 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
|||
AssessmentViewModel viewModel,
|
||||
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(
|
||||
index: viewModel.currentPage,
|
||||
|
|
@ -56,17 +59,7 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
|
|||
|
||||
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 _buildStartLesson() => const StartLessonScreen();
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
|
@ -13,7 +11,6 @@ import '../../../models/assessment.dart';
|
|||
import '../../../services/api_service.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../home/home_view.dart';
|
||||
|
||||
class AssessmentViewModel extends BaseViewModel {
|
||||
// Dependency injection
|
||||
|
|
@ -223,11 +220,18 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
rebuildUi();
|
||||
}
|
||||
|
||||
void pop() {
|
||||
if (_currentPage == 0 || _currentPage == 3 /*7*/) {
|
||||
void goBack() {
|
||||
if (_currentPage == 0) {
|
||||
_navigationService.back();
|
||||
} else if (_currentPage != 0 && _currentPage != 3) {
|
||||
} else if (_currentPage == 2) {
|
||||
_currentPage = 0;
|
||||
rebuildUi();
|
||||
} else if (_currentPage == 3) {
|
||||
if (_proficiencyLevel != ProficiencyLevels.none) {
|
||||
_currentPage--;
|
||||
} else {
|
||||
_currentPage = 0;
|
||||
}
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
|
|
@ -244,7 +248,7 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
|
||||
Future<void> _getAssessments() async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
List<Assessment> response = await _apiService.getAssessments();
|
||||
_assessments = await _apiService.getAssessments();
|
||||
/*
|
||||
for (int i = 0; i < 6; i++) {
|
||||
final generator = Random();
|
||||
|
|
@ -252,7 +256,6 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
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',
|
||||
style: style14MG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
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,122 +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 ',
|
||||
style: style14MG400,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
foregroundColor: kcWhite,
|
||||
text: 'Continue Assessment',
|
||||
onTap: () => viewModel.next(),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -65,46 +65,33 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
Widget _buildAssessment(AssessmentViewModel viewModel) => PageView.builder(
|
||||
controller: viewModel.pageController,
|
||||
itemCount: viewModel.assessments.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (cotext, index) =>
|
||||
_buildBody(index: index, viewModel: viewModel),
|
||||
_buildBodyScroller(index: index, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBodyScroller(
|
||||
{required int index, required AssessmentViewModel viewModel}) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildBody(index: index, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildBody(
|
||||
{required int index, required AssessmentViewModel viewModel}) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: _buildBodyChildren(viewModel: viewModel, index: index),
|
||||
);
|
||||
|
||||
List<Widget> _buildBodyChildren(
|
||||
{required int index, required AssessmentViewModel viewModel}) =>
|
||||
[
|
||||
_buildUpperColumnWrapper(viewModel: viewModel, index: index),
|
||||
_buildContinueButtonWrapper(viewModel: viewModel, question: index + 1)
|
||||
];
|
||||
|
||||
Widget _buildUpperColumnWrapper(
|
||||
{required int index, required AssessmentViewModel viewModel}) =>
|
||||
Expanded(
|
||||
child: _buildUpperColumn(index: index, viewModel: viewModel),
|
||||
);
|
||||
|
||||
Widget _buildUpperColumn(
|
||||
{required int index, required AssessmentViewModel viewModel}) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildUpperColumnChildren(index: index, viewModel: viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildUpperColumnChildren(
|
||||
{required int index, required AssessmentViewModel viewModel}) =>
|
||||
[
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(index: index, viewModel: viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildAnswers(index: index, viewModel: viewModel)
|
||||
_buildAnswers(index: index, viewModel: viewModel),
|
||||
_buildContinueButtonWrapper(viewModel: viewModel, question: index + 1)
|
||||
];
|
||||
|
||||
Widget _buildTitle(
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
children: _buildScaffoldChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => [
|
||||
_buildAppBar(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildExpandedBody(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
|
||||
Expanded(child: _buildBodyWrapper(viewModel));
|
||||
|
|
@ -59,7 +62,7 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.pop,
|
||||
onPop: viewModel.goBack,
|
||||
showLanguageSelection: true,
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,11 +26,15 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
children: _buildScaffoldChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => [
|
||||
_buildAppBar(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildExpandedBody(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: false,
|
||||
showBackButton: true,
|
||||
onPop: viewModel.goBack,
|
||||
showLanguageSelection: true,
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
|
@ -50,7 +54,7 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
);
|
||||
|
||||
List<Widget> _buildBodyChildren(AssessmentViewModel viewModel) =>
|
||||
[_buildUpperColumn(viewModel), _buildLowerColumn(viewModel)];
|
||||
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)];
|
||||
|
||||
Widget _buildUpperColumn(AssessmentViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -96,41 +100,18 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _buildLowerColumnChildren(viewModel),
|
||||
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50),
|
||||
child: _buildContinueButton(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildLowerColumnChildren(AssessmentViewModel viewModel) => [
|
||||
_buildContinueButton(viewModel),
|
||||
verticalSpaceSmall,
|
||||
_buildSkipButtonWrapper(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildContinueButton(AssessmentViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
safe: false,
|
||||
text: 'Continue',
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
onTap: () => viewModel.next(),
|
||||
backgroundColor: kcPrimaryColor,
|
||||
);
|
||||
|
||||
Widget _buildSkipButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50),
|
||||
child: _buildSkipButton(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildSkipButton(AssessmentViewModel viewModel) =>
|
||||
CustomElevatedButton(
|
||||
height: 55,
|
||||
borderRadius: 12,
|
||||
backgroundColor: kcWhite,
|
||||
text: 'Practice Speaking',
|
||||
borderColor: kcPrimaryColor,
|
||||
onTap: () => viewModel.next(),
|
||||
foregroundColor: kcPrimaryColor,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +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/large_app_bar.dart';
|
||||
|
||||
import '../assessment_viewmodel.dart';
|
||||
|
||||
class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||
const ResultAnalysisScreen({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 _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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _buildUpperColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildUpperColumnChildren(AssessmentViewModel viewModel) => [
|
||||
verticalSpaceMassive,
|
||||
_buildIcon(),
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
verticalSpaceSmall,
|
||||
_buildSubtitle(),
|
||||
];
|
||||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: false,
|
||||
showLanguageSelection: true,
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildIcon() => SvgPicture.asset(
|
||||
'assets/icons/progress_indicator.svg',
|
||||
);
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Analyzing your results…',
|
||||
style: style25DG600,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'We’re now analyzing your speaking skills',
|
||||
style: style14MG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||
import 'package:yimaru_app/ui/common/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 RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||
const RetakeAssessmentScreen({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 _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 _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: false,
|
||||
showLanguageSelection: true,
|
||||
onLanguage: () async => await viewModel.navigateToLanguage(),
|
||||
);
|
||||
|
||||
Widget _buildIcon() => const Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
size: 100,
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
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 ',
|
||||
style: style14MG400,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
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,
|
||||
foregroundColor: kcWhite,
|
||||
text: 'Retake Assessment',
|
||||
onTap: () => viewModel.next(),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
@ -42,12 +42,15 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
children: _buildScaffoldChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
|
||||
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
|
||||
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => [
|
||||
_buildAppBar(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildExpandedBody(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
|
||||
showBackButton: true,
|
||||
onPop: viewModel.pop,
|
||||
onPop: viewModel.goBack,
|
||||
showLanguageSelection: true,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,26 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/ui/widgets/course_card.dart';
|
||||
|
||||
import '../../../models/course_detail.dart';
|
||||
import '../../../models/course_subcategory.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/learn_app_bar.dart';
|
||||
import '../../widgets/course_tile.dart';
|
||||
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||
import '../../widgets/small_app_bar.dart';
|
||||
import 'course_viewmodel.dart';
|
||||
|
||||
class CourseView extends StackedView<CourseViewModel> {
|
||||
const CourseView({Key? key}) : super(key: key);
|
||||
final CourseSubcategory subcategory;
|
||||
|
||||
const CourseView({Key? key, required this.subcategory}) : super(key: key);
|
||||
|
||||
@override
|
||||
void onViewModelReady(CourseViewModel viewModel) async {
|
||||
await viewModel.getCourseDetails(subcategory.id ?? 0);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
CourseViewModel viewModelBuilder(BuildContext context) => CourseViewModel();
|
||||
|
|
@ -38,68 +50,79 @@ class CourseView extends StackedView<CourseViewModel> {
|
|||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
_buildCourseColumnWrapper(viewModel)
|
||||
verticalSpaceMedium,
|
||||
_buildCoursesColumnWrapper(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(CourseViewModel viewModel) => LearnAppBar(
|
||||
name: viewModel.user?.firstName,
|
||||
profileImage: viewModel.user?.profilePicture,
|
||||
Widget _buildAppBar(CourseViewModel viewModel) => SmallAppBar(
|
||||
onTap: viewModel.pop,
|
||||
showBackButton: true,
|
||||
);
|
||||
|
||||
Widget _buildCourseColumnWrapper(CourseViewModel viewModel) =>
|
||||
Expanded(child: _buildCourseColumnScrollView(viewModel));
|
||||
Widget _buildCoursesColumnWrapper(CourseViewModel viewModel) =>
|
||||
Expanded(child: _buildCoursesColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildCourseColumnScrollView(CourseViewModel viewModel) =>
|
||||
Widget _buildCoursesColumnScrollView(CourseViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildCourseColumn(viewModel),
|
||||
child: _buildCoursesColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildCourseColumn(CourseViewModel viewModel) => Column(
|
||||
Widget _buildCoursesColumn(CourseViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildLevelsColumnChildren(viewModel),
|
||||
children: _buildCoursesColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildLevelsColumnChildren(CourseViewModel viewModel) => [
|
||||
verticalSpaceLarge,
|
||||
List<Widget> _buildCoursesColumnChildren(CourseViewModel viewModel) => [
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
_buildSubtitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildListView(viewModel)
|
||||
_buildListViewBuilder(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Courses',
|
||||
style: style20DG700,
|
||||
'${subcategory.title ?? ''} courses',
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Choose a course to improve your professional or exam skills.',
|
||||
'Explore variety of courses on ${subcategory.title ?? ''}.',
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildListViewBuilder(CourseViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.courses)
|
||||
? _buildProgressIndicator()
|
||||
: _buildListView(viewModel);
|
||||
|
||||
Widget _buildProgressIndicator() => const Center(
|
||||
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||
);
|
||||
|
||||
Widget _buildListView(CourseViewModel viewModel) => ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: viewModel.courses.length,
|
||||
itemCount: viewModel.courseDetail.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
title: viewModel.courses[index]['title'],
|
||||
subtitle: viewModel.courses[index]['subtitle'],
|
||||
onTap: () async => await viewModel.navigateToCourseModule(),
|
||||
courseDetail: viewModel.courseDetail[index],
|
||||
onCourseTap: () async => await viewModel
|
||||
.navigateToCoursePayment(viewModel.courseDetail[index].course!),
|
||||
onPracticeTap: () async => await viewModel.navigateToCoursePractice(
|
||||
viewModel.courseDetail[index].course?.id ?? 0),
|
||||
),
|
||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||
);
|
||||
|
||||
//
|
||||
Widget _buildTile({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required GestureTapCallback onTap,
|
||||
GestureTapCallback? onCourseTap,
|
||||
GestureTapCallback? onPracticeTap,
|
||||
required CourseDetail courseDetail,
|
||||
}) =>
|
||||
CourseCard(
|
||||
title: title,
|
||||
onTap: onTap,
|
||||
subtitle: subtitle,
|
||||
CourseTile(
|
||||
onCourseTap: onCourseTap,
|
||||
courseDetail: courseDetail,
|
||||
onPracticeTap: onPracticeTap,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,50 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:yimaru_app/app/app.router.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../models/user_model.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../app/app.router.dart';
|
||||
import '../../../models/course.dart';
|
||||
import '../../../models/course_detail.dart';
|
||||
import '../../../services/course_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class CourseViewModel extends ReactiveViewModel {
|
||||
class CourseViewModel extends BaseViewModel {
|
||||
// Dependency injection
|
||||
|
||||
final _courseService = locator<CourseService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
// Subcourse with progress
|
||||
List<CourseDetail> _courseDetail = [];
|
||||
|
||||
// Current user
|
||||
UserModel? get _user => _authenticationService.user;
|
||||
|
||||
UserModel? get user => _user;
|
||||
|
||||
// Courses
|
||||
final List<Map<String, dynamic>> _courses = [
|
||||
{
|
||||
'title': 'English Proficiency Exams',
|
||||
'subtitle':
|
||||
'Prepare for IELTS, TOEFL, or Duolingo with structured practice.',
|
||||
},
|
||||
{
|
||||
'title': 'Skill-Based Courses',
|
||||
'subtitle':
|
||||
'Learn English for the workplace, travel, and real-life communication.',
|
||||
},
|
||||
];
|
||||
|
||||
List<Map<String, dynamic>> get courses => _courses;
|
||||
List<CourseDetail> get courseDetail => _courseDetail;
|
||||
|
||||
// Navigation
|
||||
Future<void> navigateToCourseModule() async =>
|
||||
_navigationService.navigateToCourseModuleView();
|
||||
void pop() => _navigationService.back();
|
||||
|
||||
Future<void> navigateToCoursePayment(Course course) async =>
|
||||
_navigationService.navigateToCoursePaymentView(course: course);
|
||||
|
||||
Future<void> navigateToCoursePractice(int id) =>
|
||||
_navigationService.navigateToCoursePracticeView(id: id);
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Sub course
|
||||
Future<void> getCourseDetails(int id) async =>
|
||||
await runBusyFuture(_getCourseDetails(id),
|
||||
busyObject: StateObjects.courses);
|
||||
|
||||
Future<void> _getCourseDetails(int id) async {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_courseDetail = await _courseService.getCoursesDetail(id);
|
||||
_courseDetail.sort((a, b) =>
|
||||
(a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0));
|
||||
rebuildUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
124
lib/ui/views/course_category/course_category_view.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../../models/course_category.dart';
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/course_category_card.dart';
|
||||
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||
import '../../widgets/profile_app_bar.dart';
|
||||
import 'course_category_viewmodel.dart';
|
||||
|
||||
class CourseCategoryView extends StackedView<CourseCategoryViewModel> {
|
||||
const CourseCategoryView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
void onViewModelReady(CourseCategoryViewModel viewModel) async {
|
||||
await viewModel.getCourseCategories();
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
CourseCategoryViewModel viewModelBuilder(BuildContext context) =>
|
||||
CourseCategoryViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
CourseCategoryViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(CourseCategoryViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(CourseCategoryViewModel viewModel) =>
|
||||
SafeArea(child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(CourseCategoryViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(CourseCategoryViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildCategoryColumnWrapper(viewModel)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(CourseCategoryViewModel viewModel) => ProfileAppBar(
|
||||
name: viewModel.user?.firstName,
|
||||
profileImage: viewModel.user?.profilePicture,
|
||||
);
|
||||
|
||||
Widget _buildCategoryColumnWrapper(CourseCategoryViewModel viewModel) =>
|
||||
Expanded(child: _buildCourseColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildCourseColumnScrollView(CourseCategoryViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildCourseColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildCourseColumn(CourseCategoryViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildLevelsColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildLevelsColumnChildren(CourseCategoryViewModel viewModel) =>
|
||||
[
|
||||
_buildTitle(),
|
||||
_buildSubtitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildListViewBuilder(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'Courses',
|
||||
style: style18DG700,
|
||||
);
|
||||
|
||||
Widget _buildSubtitle() => Text(
|
||||
'Choose a course to improve your professional or exam skills.',
|
||||
style: style14DG400,
|
||||
);
|
||||
|
||||
Widget _buildListViewBuilder(CourseCategoryViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.courseCategories)
|
||||
? _buildProgressIndicator()
|
||||
: _buildListView(viewModel);
|
||||
|
||||
Widget _buildProgressIndicator() => const Center(
|
||||
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||
);
|
||||
|
||||
Widget _buildListView(CourseCategoryViewModel viewModel) =>
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: viewModel.categories.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
category: viewModel.categories[index],
|
||||
onTap: () async => await viewModel
|
||||
.navigateToCourseCategory(viewModel.categories[index]),
|
||||
),
|
||||
separatorBuilder: (context, index) => verticalSpaceSmall,
|
||||
);
|
||||
|
||||
//
|
||||
Widget _buildTile({
|
||||
required CourseCategory category,
|
||||
required GestureTapCallback onTap,
|
||||
}) =>
|
||||
CourseCategoryCard(
|
||||
onTap: onTap,
|
||||
category: category,
|
||||
);
|
||||
}
|
||||
58
lib/ui/views/course_category/course_category_viewmodel.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../app/app.router.dart';
|
||||
import '../../../models/course_category.dart';
|
||||
import '../../../models/user_model.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
import '../../../services/authentication_service.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../common/enmus.dart';
|
||||
|
||||
class CourseCategoryViewModel extends ReactiveViewModel {
|
||||
// Dependency injection
|
||||
final _apiService = locator<ApiService>();
|
||||
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
final _authenticationService = locator<AuthenticationService>();
|
||||
|
||||
@override
|
||||
List<ListenableServiceMixin> get listenableServices =>
|
||||
[_authenticationService];
|
||||
|
||||
// Current user
|
||||
UserModel? get _user => _authenticationService.user;
|
||||
|
||||
UserModel? get user => _user;
|
||||
|
||||
// Course categories
|
||||
List<CourseCategory> _categories = [];
|
||||
|
||||
List<CourseCategory> get categories => _categories;
|
||||
|
||||
// Navigation
|
||||
Future<void> navigateToCourseCategory(CourseCategory category) async =>
|
||||
_navigationService.navigateToCourseSubcategoryView(category: category);
|
||||
|
||||
// Remote api call
|
||||
|
||||
// Course categories
|
||||
Future<void> getCourseCategories() async =>
|
||||
await runBusyFuture(_getCourseCategories(),
|
||||
busyObject: StateObjects.courseCategories);
|
||||
|
||||
Future<void> _getCourseCategories() async {
|
||||
if (categories.isEmpty) {
|
||||
if (await _statusChecker.checkConnection()) {
|
||||
_categories = await _apiService.getCourseCategories();
|
||||
|
||||
rebuildUi();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
lib/ui/views/course_lesson/course_lesson_view.dart
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:yimaru_app/models/course.dart';
|
||||
import 'package:yimaru_app/models/course_lesson.dart';
|
||||
import 'package:yimaru_app/ui/widgets/course_lesson_tile.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/custom_circular_progress_indicator.dart';
|
||||
import '../../widgets/small_app_bar.dart';
|
||||
import 'course_lesson_viewmodel.dart';
|
||||
|
||||
class CourseLessonView extends StackedView<CourseLessonViewModel> {
|
||||
final Course course;
|
||||
|
||||
const CourseLessonView({Key? key, required this.course}) : super(key: key);
|
||||
|
||||
@override
|
||||
void onViewModelReady(CourseLessonViewModel viewModel) async {
|
||||
await viewModel.getCourseLessons(course.id ?? 0);
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
CourseLessonViewModel viewModelBuilder(BuildContext context) =>
|
||||
CourseLessonViewModel();
|
||||
|
||||
@override
|
||||
Widget builder(
|
||||
BuildContext context,
|
||||
CourseLessonViewModel viewModel,
|
||||
Widget? child,
|
||||
) =>
|
||||
_buildScaffoldWrapper(viewModel);
|
||||
|
||||
Widget _buildScaffoldWrapper(CourseLessonViewModel viewModel) => Scaffold(
|
||||
backgroundColor: kcBackgroundColor,
|
||||
body: _buildScaffold(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildScaffold(CourseLessonViewModel viewModel) =>
|
||||
SafeArea(child: _buildBody(viewModel));
|
||||
|
||||
Widget _buildBody(CourseLessonViewModel viewModel) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: _buildColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildColumn(CourseLessonViewModel viewModel) => Column(
|
||||
children: [
|
||||
verticalSpaceMedium,
|
||||
_buildAppBar(viewModel),
|
||||
verticalSpaceMedium,
|
||||
_buildLessonColumnWrapper(viewModel),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildAppBar(CourseLessonViewModel viewModel) => SmallAppBar(
|
||||
onTap: viewModel.pop,
|
||||
showBackButton: true,
|
||||
title: 'Course Detail',
|
||||
);
|
||||
|
||||
Widget _buildLessonColumnWrapper(CourseLessonViewModel viewModel) =>
|
||||
Expanded(child: _buildLessonColumnScrollView(viewModel));
|
||||
|
||||
Widget _buildLessonColumnScrollView(CourseLessonViewModel viewModel) =>
|
||||
SingleChildScrollView(
|
||||
child: _buildLessonColumn(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildLessonColumn(CourseLessonViewModel viewModel) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _buildLessonColumnChildren(viewModel),
|
||||
);
|
||||
|
||||
List<Widget> _buildLessonColumnChildren(CourseLessonViewModel viewModel) => [
|
||||
// verticalSpaceLarge,
|
||||
_buildTitle(),
|
||||
verticalSpaceMedium,
|
||||
_buildListViewBuilder(viewModel)
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
'${course.title} course lessons',
|
||||
style: style18DG700,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
|
||||
Widget _buildListViewBuilder(CourseLessonViewModel viewModel) =>
|
||||
viewModel.busy(StateObjects.courseLessons)
|
||||
? _buildProgressIndicator()
|
||||
: _buildListView(viewModel);
|
||||
|
||||
Widget _buildProgressIndicator() => const Center(
|
||||
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||
);
|
||||
|
||||
Widget _buildListView(CourseLessonViewModel viewModel) => ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: viewModel.courseLessons.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildTile(
|
||||
lesson: viewModel.courseLessons[index],
|
||||
onPracticeTap: () async =>
|
||||
await viewModel.navigateToCoursePractice(course.id ?? 0),
|
||||
onVideoTap: () async => await viewModel
|
||||
.navigateToCourseLessonDetail(viewModel.courseLessons[index])),
|
||||
);
|
||||
|
||||
Widget _buildTile({
|
||||
required CourseLesson lesson,
|
||||
required GestureTapCallback onVideoTap,
|
||||
required GestureTapCallback onPracticeTap,
|
||||
}) =>
|
||||
CourseLessonTile(
|
||||
lesson: lesson,
|
||||
onVideoTap: onVideoTap,
|
||||
onPracticeTap: onPracticeTap,
|
||||
);
|
||||
}
|
||||