feat(Course): Finalize course integration

This commit is contained in:
BisratHailu 2026-03-25 11:21:59 +03:00
parent 4eb6e9d6c3
commit c368404a83
267 changed files with 10670 additions and 2398 deletions

View File

@ -1,43 +1,79 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("kotlin-android")
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.gms.google-services") id("com.google.gms.google-services")
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
FileInputStream(keystorePropertiesFile).use {
keystoreProperties.load(it)
}
}
android { android {
ndkVersion = flutter.ndkVersion
namespace = "com.yimaru.lms.app" namespace = "com.yimaru.lms.app"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
defaultConfig { defaultConfig {
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
applicationId = "com.yimaru.lms.app"
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
applicationId = "com.yimaru.lms.app"
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
} }
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storePassword = keystoreProperties["storePassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
}
}
buildTypes { buildTypes {
release { getByName("release") {
signingConfig = signingConfigs.getByName("debug") isMinifyEnabled = false
isShrinkResources = false
ndk { debugSymbolLevel = "FULL" }
signingConfig = signingConfigs.getByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
} }
} }
} }
kotlin {
jvmToolchain(17)
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
implementation(platform("com.google.firebase:firebase-bom:34.10.0"))
}
flutter { flutter {
source = "../.." source = "../.."
} }

View File

@ -14,19 +14,19 @@
}, },
"oauth_client": [ "oauth_client": [
{ {
"client_id": "574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com", "client_id": "574860813475-glgnkruic7dflaomb59el8994b7hhfga.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
"android_info": { "android_info": {
"package_name": "com.yimaru.lms.app", "package_name": "com.yimaru.lms.app",
"certificate_hash": "fc91f52846d27c62bba3e16bc98982fb9953eca1" "certificate_hash": "378836a3aa9f36958b6c6c69bc67e3195352f68d"
} }
}, },
{ {
"client_id": "574860813475-631s3mo8ha2qc2jeb5e2aosn0967niik.apps.googleusercontent.com", "client_id": "574860813475-m90u87plqaac4tb8oug32k41usossiod.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
"android_info": { "android_info": {
"package_name": "com.yimaru.lms.app", "package_name": "com.yimaru.lms.app",
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a" "certificate_hash": "29797902ad6a24212b9d9fad71562907956f6a6c"
} }
} }
], ],

41
android/app/proguard-rules.pro vendored Normal file
View 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.**

View File

@ -7,7 +7,7 @@
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<application <application
android:label="Yimaru" android:label="Yimaru Academy"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item> <item>
<bitmap <bitmap android:gravity="center" android:src="@drawable/splash"/>
android:gravity="center" </item>
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item> <item>
<bitmap <bitmap android:gravity="center" android:src="@drawable/splash"/>
android:gravity="center" </item>
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View 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>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame --> the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

View 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>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame --> the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,12 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip

View File

@ -19,7 +19,7 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.13.2" apply false id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.0" apply false id("org.jetbrains.kotlin.android") version "2.3.0" apply false
id("com.google.gms.google-services") version("4.4.4") apply false id("com.google.gms.google-services") version("4.4.4") apply false

BIN
assets/icons/duolingo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
assets/icons/dwarf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

3
devtools_options.yaml Normal file
View 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:

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,23 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal",
"filename" : "LaunchImage.png", "filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@2x.png", "filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@3x.png", "filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -16,13 +16,19 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
</imageView> <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews> </subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/> <constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints> </constraints>
</view> </view>
</viewController> </viewController>
@ -32,6 +38,7 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="LaunchImage" width="168" height="185"/> <image name="LaunchImage" width="1024" height="1024"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources> </resources>
</document> </document>

View File

@ -1,49 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Yimaru App</string> <string>Yimaru App</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>yimaru_app</string> <string>yimaru_app</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
</dict> <key>UIStatusBarHidden</key>
<false/>
</dict>
</plist> </plist>

View File

@ -9,7 +9,6 @@ import 'package:yimaru_app/ui/views/profile/profile_view.dart';
import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart'; import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart';
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart'; import 'package:yimaru_app/ui/views/downloads/downloads_view.dart';
import 'package:yimaru_app/ui/views/progress/progress_view.dart'; import 'package:yimaru_app/ui/views/progress/progress_view.dart';
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart';
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'; import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart';
import 'package:yimaru_app/ui/views/support/support_view.dart'; import 'package:yimaru_app/ui/views/support/support_view.dart';
import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart'; import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart';
@ -30,7 +29,6 @@ import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/views/welcome/welcome_view.dart'; import 'package:yimaru_app/ui/views/welcome/welcome_view.dart';
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart'; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'; import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import 'package:yimaru_app/services/permission_handler_service.dart'; import 'package:yimaru_app/services/permission_handler_service.dart';
import 'package:yimaru_app/services/image_picker_service.dart'; import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/services/google_auth_service.dart'; import 'package:yimaru_app/services/google_auth_service.dart';
@ -38,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/forget_password/forget_password_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart'; import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'; import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/course/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_practice/course_practice_view.dart';
import 'package:yimaru_app/ui/views/course_payment/course_payment_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 // @stacked-import
@StackedApp( @StackedApp(
@ -53,7 +59,6 @@ import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
MaterialRoute(page: ProfileDetailView), MaterialRoute(page: ProfileDetailView),
MaterialRoute(page: DownloadsView), MaterialRoute(page: DownloadsView),
MaterialRoute(page: ProgressView), MaterialRoute(page: ProgressView),
MaterialRoute(page: OngoingProgressView),
MaterialRoute(page: AccountPrivacyView), MaterialRoute(page: AccountPrivacyView),
MaterialRoute(page: SupportView), MaterialRoute(page: SupportView),
MaterialRoute(page: TelegramSupportView), MaterialRoute(page: TelegramSupportView),
@ -69,14 +74,18 @@ import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
MaterialRoute(page: WelcomeView), MaterialRoute(page: WelcomeView),
MaterialRoute(page: AssessmentView), MaterialRoute(page: AssessmentView),
MaterialRoute(page: LearnLessonView), MaterialRoute(page: LearnLessonView),
MaterialRoute(page: FailureView),
MaterialRoute(page: ForgetPasswordView), MaterialRoute(page: ForgetPasswordView),
MaterialRoute(page: LearnLessonDetailView), MaterialRoute(page: LearnLessonDetailView),
MaterialRoute(page: LearnPracticeView), MaterialRoute(page: LearnPracticeView),
MaterialRoute(page: CourseView),
MaterialRoute(page: CourseModuleView),
MaterialRoute(page: CoursePracticeView), MaterialRoute(page: CoursePracticeView),
MaterialRoute(page: CoursePaymentView), MaterialRoute(page: CoursePaymentView),
MaterialRoute(page: CourseCategoryView),
MaterialRoute(page: FailureView),
MaterialRoute(page: CourseLessonView),
MaterialRoute(page: CourseLessonDetailView),
MaterialRoute(page: DuolingoView),
MaterialRoute(page: CourseSubcategoryView),
MaterialRoute(page: CourseView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [
@ -92,6 +101,9 @@ import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
LazySingleton(classType: ImagePickerService), LazySingleton(classType: ImagePickerService),
LazySingleton(classType: GoogleAuthService), LazySingleton(classType: GoogleAuthService),
LazySingleton(classType: ImageDownloaderService), LazySingleton(classType: ImageDownloaderService),
LazySingleton(classType: NotificationService),
LazySingleton(classType: SmartAuthService),
LazySingleton(classType: CourseService),
// @stacked-service // @stacked-service
], ],
bottomsheets: [ bottomsheets: [

View File

@ -13,12 +13,15 @@ import 'package:stacked_shared/stacked_shared.dart';
import '../services/api_service.dart'; import '../services/api_service.dart';
import '../services/authentication_service.dart'; import '../services/authentication_service.dart';
import '../services/course_service.dart';
import '../services/dio_service.dart'; import '../services/dio_service.dart';
import '../services/google_auth_service.dart'; import '../services/google_auth_service.dart';
import '../services/image_downloader_service.dart'; import '../services/image_downloader_service.dart';
import '../services/image_picker_service.dart'; import '../services/image_picker_service.dart';
import '../services/notification_service.dart';
import '../services/permission_handler_service.dart'; import '../services/permission_handler_service.dart';
import '../services/secure_storage_service.dart'; import '../services/secure_storage_service.dart';
import '../services/smart_auth_service.dart';
import '../services/status_checker_service.dart'; import '../services/status_checker_service.dart';
final locator = StackedLocator.instance; final locator = StackedLocator.instance;
@ -44,4 +47,7 @@ Future<void> setupLocator({
locator.registerLazySingleton(() => ImagePickerService()); locator.registerLazySingleton(() => ImagePickerService());
locator.registerLazySingleton(() => GoogleAuthService()); locator.registerLazySingleton(() => GoogleAuthService());
locator.registerLazySingleton(() => ImageDownloaderService()); locator.registerLazySingleton(() => ImageDownloaderService());
locator.registerLazySingleton(() => NotificationService());
locator.registerLazySingleton(() => SmartAuthService());
locator.registerLazySingleton(() => CourseService());
} }

File diff suppressed because it is too large Load Diff

View File

@ -64,7 +64,7 @@ class DefaultFirebaseOptions {
projectId: 'yimaru-lms-e834e', projectId: 'yimaru-lms-e834e',
storageBucket: 'yimaru-lms-e834e.firebasestorage.app', storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
androidClientId: androidClientId:
'574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com', '574860813475-glgnkruic7dflaomb59el8994b7hhfga.apps.googleusercontent.com',
iosBundleId: 'com.yimaru.lms.app', iosBundleId: 'com.yimaru.lms.app',
); );
} }

View File

@ -1,3 +1,4 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toastification/toastification.dart'; import 'package:toastification/toastification.dart';
import 'package:yimaru_app/app/app.bottomsheets.dart'; import 'package:yimaru_app/app/app.bottomsheets.dart';
@ -5,10 +6,15 @@ import 'package:yimaru_app/app/app.dialogs.dart';
import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/services/notification_service.dart';
import 'firebase_options.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await setupLocator(); await setupLocator();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await locator<NotificationService>().initialize();
setupDialogUi(); setupDialogUi();
setupBottomSheetUi(); setupBottomSheetUi();
runApp(const MainApp()); runApp(const MainApp());

View File

@ -12,7 +12,6 @@ class Assessment {
final List<Option>? options; final List<Option>? options;
@JsonKey(name: 'question_type') @JsonKey(name: 'question_type')
final String? questionType; final String? questionType;
@ -22,7 +21,6 @@ class Assessment {
@JsonKey(name: 'difficulty_level') @JsonKey(name: 'difficulty_level')
final String? difficultyLevel; final String? difficultyLevel;
const Assessment({ const Assessment({
this.id, this.id,
this.points, this.points,

39
lib/models/course.dart Normal file
View 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
View 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,
};

View 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);
}

View 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,
};

View 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);
}

View 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,
};

View 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);
}

View 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,
};

View 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);
}

View 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,
};

View 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);
}

View 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
View 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);
}

View 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,
};

View 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);
}

View 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,
};

View File

@ -1,5 +1,12 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:yimaru_app/models/assessment.dart'; import 'package:yimaru_app/models/assessment.dart';
import 'package:yimaru_app/models/course_subcategory.dart';
import 'package:yimaru_app/models/course_category.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/models/course_progress.dart';
import 'package:yimaru_app/models/course.dart';
import 'package:yimaru_app/models/practice.dart';
import 'package:yimaru_app/models/practice_question.dart';
import 'package:yimaru_app/models/user_model.dart'; import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/dio_service.dart'; import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart'; import 'package:yimaru_app/ui/common/app_constants.dart';
@ -16,7 +23,7 @@ class ApiService {
Map<String, dynamic> data) async { Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kRegisterUrl', '$kBaseUrl/$kUserBaseUrl/$kRegisterUrl',
data: data, data: data,
); );
@ -40,7 +47,7 @@ class ApiService {
} }
// Email login // Email login
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async { Future<Map<String, dynamic>> login(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kLoginUrl', '$kBaseUrl/$kLoginUrl',
@ -99,7 +106,7 @@ class ApiService {
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async { Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kVerifyOtpUrl', '$kBaseUrl/$kUserBaseUrl/$kVerifyOtpUrl',
data: data, data: data,
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
@ -126,7 +133,7 @@ class ApiService {
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async { Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kResendOtpUrl', '$kBaseUrl/$kUserBaseUrl/$kResendOtpUrl',
data: data, data: data,
); );
@ -154,7 +161,7 @@ class ApiService {
Map<String, dynamic> data) async { Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kRequestResetCode', '$kBaseUrl/$kUserBaseUrl/$kRequestResetCode',
data: data, data: data,
); );
@ -181,7 +188,7 @@ class ApiService {
Future<Map<String, dynamic>> resetPassword(Map<String, dynamic> data) async { Future<Map<String, dynamic>> resetPassword(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kResetPassword', '$kBaseUrl/$kUserBaseUrl/$kResetPassword',
data: data, data: data,
); );
@ -208,7 +215,7 @@ class ApiService {
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async { Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
try { try {
Response response = await _service.dio.get( Response response = await _service.dio.get(
'$kBaseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl', '$kBaseUrl/$kUserBaseUrl/${user?.userId}/$kProfileStatusUrl',
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
@ -235,7 +242,7 @@ class ApiService {
Future<Map<String, dynamic>> getProfileData(int? userId) async { Future<Map<String, dynamic>> getProfileData(int? userId) async {
try { try {
Response response = await _service.dio.get( Response response = await _service.dio.get(
'$kBaseUrl/$kUserUrl/$kGetUserUrl/$userId', '$kBaseUrl/$kUserBaseUrl/$kGetUserUrl',
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
@ -263,7 +270,7 @@ class ApiService {
Map<String, dynamic> data) async { Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.put( Response response = await _service.dio.put(
'$kBaseUrl/$kUserUrl', '$kBaseUrl/$kUserBaseUrl',
data: data, data: data,
); );
@ -293,7 +300,7 @@ class ApiService {
late FormData formData; late FormData formData;
if (data['profile_picture_url'] if (data['profile_picture_url']
.toString() .toString()
.contains('com.example.yimaru_app/')) { .contains('com.yimaru.lms.app/')) {
formData = FormData.fromMap({ formData = FormData.fromMap({
'file': data['profile_picture_url'].toString().isNotEmpty 'file': data['profile_picture_url'].toString().isNotEmpty
? MultipartFile.fromFileSync( ? MultipartFile.fromFileSync(
@ -315,7 +322,7 @@ class ApiService {
}); });
} }
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$userId/$kUpdateProfileImage', '$kBaseUrl/$kUserBaseUrl/$userId/$kUpdateProfileImage',
data: formData, data: formData,
); );
@ -361,4 +368,197 @@ class ApiService {
return []; return [];
} }
} }
// Course categories
Future<List<CourseCategory>> getCourseCategories() async {
try {
List<CourseCategory> categories = [];
final Response response = await _service.dio
.get('$kBaseUrl/$kCourseBaseUrl/$kCourseCategoryUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['categories'] as List;
categories = decodedData.map(
(e) {
return CourseCategory.fromJson(e);
},
).toList();
return categories;
}
return [];
} catch (e) {
return [];
}
}
// Course subcategory
Future<List<CourseSubcategory>> getCourseSubcategories(int id) async {
try {
List<CourseSubcategory> subcategories = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kCourseBaseUrl/$kCourseCategoryUrl/$id/$kCoursesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['courses'] as List;
subcategories = decodedData.map(
(e) {
return CourseSubcategory.fromJson(e);
},
).toList();
return subcategories;
}
return [];
} catch (e) {
return [];
}
}
// Sub-courses
Future<List<Course>> getCourses(int id) async {
try {
List<Course> courses = [];
final Response response = await _service.dio
.get('$kBaseUrl/$kCourseBaseUrl/$kCoursesUrl/$id/$kSubcoursesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['sub_courses'] as List;
courses = decodedData.map(
(e) {
return Course.fromJson(e);
},
).toList();
return courses;
}
return [];
} catch (e) {
return [];
}
}
// Course progress
Future<List<CourseProgress>> getCourseProgress(int id) async {
try {
List<CourseProgress> courseProgress = [];
final Response response =
await _service.dio.get('$kBaseUrl/$kCourseProgressUrl/$id');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
courseProgress = decodedData.map(
(e) {
return CourseProgress.fromJson(e);
},
).toList();
return courseProgress;
}
return [];
} catch (e) {
return [];
}
}
// Course videos
Future<List<CourseLesson>> getCourseLessons(int id) async {
try {
List<CourseLesson> courseLessons = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kCourseBaseUrl/$kSubcoursesUrl/$id/$kPublishedVideos');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
courseLessons = decodedData.map(
(e) {
return CourseLesson.fromJson(e);
},
).toList();
return courseLessons;
}
return [];
} catch (e) {
return [];
}
}
// Complete lesson
Future<Map<String, dynamic>> completeLesson(int id) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteLessonUrl',
);
if (response.statusCode == 200) {
return {'status': ResponseStatus.success, 'message': 'Video completed'};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Course practices
Future<List<Practice>> getCoursePractices(Map<String,dynamic> data) async {
try {
List<Practice> coursePractices = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice',data: data);
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
coursePractices = decodedData.map(
(e) {
return Practice.fromJson(e);
},
).toList();
return coursePractices;
}
return [];
} catch (e) {
return [];
}
}
// Course practic questions
Future<List<PracticeQuestion>> getCoursePracticeQuestions(int id) async {
try {
List<PracticeQuestion> coursePracticeQuestions = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kPracticeBaseUrl/$id/$kCoursePracticeQuestions');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
coursePracticeQuestions = decodedData.map(
(e) {
return PracticeQuestion.fromJson(e);
},
).toList();
return coursePracticeQuestions;
}
return [];
} catch (e) {
return [];
}
}
} }

View File

@ -113,12 +113,10 @@ class AuthenticationService with ListenableServiceMixin {
notifyListeners(); notifyListeners();
} }
Future<void> saveUserData( Future<void> saveUserData(UserModel data) async {
{required String image, required UserModel data}) async {
await _secureService.setBool('userInfoLoaded', true); await _secureService.setBool('userInfoLoaded', true);
await _secureService.setBool( await _secureService.setBool(
'profileCompleted', data.profileCompleted ?? false); 'profileCompleted', data.profileCompleted ?? false);
await _secureService.setString('profilePicture', image);
await _secureService.setString('email', data.email ?? ''); await _secureService.setString('email', data.email ?? '');
await _secureService.setString('region', data.region ?? ''); await _secureService.setString('region', data.region ?? '');
await _secureService.setString('gender', data.gender ?? ''); await _secureService.setString('gender', data.gender ?? '');
@ -133,7 +131,6 @@ class AuthenticationService with ListenableServiceMixin {
gender: data.gender, gender: data.gender,
region: data.region, region: data.region,
userInfoLoaded: true, userInfoLoaded: true,
profilePicture: image,
userId: _user?.userId, userId: _user?.userId,
country: data.country, country: data.country,
lastName: data.lastName, lastName: data.lastName,
@ -212,7 +209,7 @@ class AuthenticationService with ListenableServiceMixin {
return _user; return _user;
} }
Future<void> logOut() async { Future<void> logout() async {
bool firstTimeInstall = await isFirstTimeInstall(); bool firstTimeInstall = await isFirstTimeInstall();
_user = null; _user = null;
await _secureService.clear(); await _secureService.clear();

View 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();
}
}

View File

@ -158,7 +158,7 @@ class DioService {
return true; return true;
} catch (e) { } catch (e) {
await _authenticationService.logOut(); await _authenticationService.logout();
await _navigationService.replaceWithLoginView(); await _navigationService.replaceWithLoginView();
return false; return false;
} }

View File

@ -1,23 +1,39 @@
import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/google_sign_in.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_constants.dart'; import 'package:yimaru_app/ui/common/app_constants.dart';
class GoogleAuthService { class GoogleAuthService with ListenableServiceMixin {
// Initialization // 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 // Google authentication
Future<GoogleSignInAccount?> googleAuth() async { Future<void> googleAuth() async {
try { try {
GoogleSignInAccount? googleUser; await _signIn.initialize(serverClientId: kServerClientId).then((_) async {
await signIn.initialize(serverClientId: kServerClientId).then((_) async { _googleUser = await _signIn.attemptLightweightAuthentication();
googleUser = await signIn.attemptLightweightAuthentication();
googleUser ??= _googleUser ??=
await signIn.authenticate(scopeHint: ['email', 'profile']); await _signIn.authenticate(scopeHint: ['email', 'profile']);
}); });
return googleUser; notifyListeners();
} catch (e) { } catch (e) {
return null; rethrow;
} }
} }
} }

View 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);
});
}
}

View File

@ -3,7 +3,6 @@ import 'package:permission_handler/permission_handler.dart';
import '../ui/common/ui_helpers.dart'; import '../ui/common/ui_helpers.dart';
class PermissionHandlerService { class PermissionHandlerService {
// Check permission category // Check permission category
Future<PermissionStatus> requestPermission( Future<PermissionStatus> requestPermission(
Permission requestedPermission) async { Permission requestedPermission) async {

View 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;
}

View File

@ -11,6 +11,7 @@ const Color kcIndigo = Color(0xff6A1B9A);
const Color kcOrange = Color(0xFFF79400); const Color kcOrange = Color(0xFFF79400);
const Color kcSkyBlue = Color(0xFF28B4CD); const Color kcSkyBlue = Color(0xFF28B4CD);
const Color kcDarkGrey = Color(0xFF1A1B1E); const Color kcDarkGrey = Color(0xFF1A1B1E);
const Color kcDeepGreen = Color(0xFF078E37);
const Color kcMediumGrey = Color(0xFF474A54); const Color kcMediumGrey = Color(0xFF474A54);
const Color kcAquamarine = Color(0xFF1DE9B6); const Color kcAquamarine = Color(0xFF1DE9B6);
const Color kcTransparent = Colors.transparent; const Color kcTransparent = Colors.transparent;

View File

@ -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 baseUrl = 'https://api.yimaru.yaltopia.com';
String kGetUserUrl = 'single'; String kCoursesUrl = 'courses';
String kUserUrl = 'api/v1/user';
String kRegisterUrl = 'register'; String kRegisterUrl = 'register';
String kCoursePractice = 'by-owner';
String kUserBaseUrl = 'api/v1/user';
String kVerifyOtpUrl = 'verify-otp'; String kVerifyOtpUrl = 'verify-otp';
String kResendOtpUrl = 'resend-otp'; String kResendOtpUrl = 'resend-otp';
String kGetUserUrl = 'user-profile';
String kSubcoursesUrl = 'sub-courses';
String kCompleteLessonUrl = 'complete';
String kResetPassword = 'resetPassword'; String kResetPassword = 'resetPassword';
String kCourseCategoryUrl = 'categories';
String kRequestResetCode = 'sendResetCode'; String kRequestResetCode = 'sendResetCode';
String kPublishedVideos = 'videos/published';
String kCoursePracticeQuestions = 'questions';
String kUpdateProfileImage = 'profile-picture'; String kUpdateProfileImage = 'profile-picture';
String kRefreshTokenUrl = 'api/v1/auth/refresh'; String kRefreshTokenUrl = 'api/v1/auth/refresh';
String kLoginUrl = 'api/v1/auth/customer-login'; String kLoginUrl = 'api/v1/auth/customer-login';
String kPracticeBaseUrl = 'api/v1/question-sets';
String kProfileStatusUrl = 'is-profile-completed'; String kProfileStatusUrl = 'is-profile-completed';
String kCourseBaseUrl = 'api/v1/course-management';
String kLessonProgressUrl = 'api/v1/progress/videos';
String kGoogleAuthUrl = 'api/v1/auth/google/android'; String kGoogleAuthUrl = 'api/v1/auth/google/android';
String kCourseProgressUrl = 'api/v1/progress/courses';
String kAssessmentsUrl = 'api/v1/assessment/questions'; String kAssessmentsUrl = 'api/v1/assessment/questions';
String kEmptyImagePath = '/data/user/0/com.yimaru.lms.app/app_flutter';
String kSampleVideoUrl = String kSampleVideoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'; 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';

View File

@ -2,12 +2,16 @@ const String ksHomeBottomSheetTitle = 'Build Great Apps!';
const String ksSuggestion = const String ksSuggestion =
"15 minutes a day can make you 3x more fluent in 3 month"; "15 minutes a day can make you 3x more fluent in 3 month";
const String ksHomeBottomSheetDescription = const String ksHomeBottomSheetDescription =
'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more'; 'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more';
const String ksPrivacyPolicy = const String ksPrivacyPolicy =
'A brief, simple overview of Yimarus 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.'; 'A brief, simple overview of Yimarus commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.';
const String ksCategorySubtitle =
'Watch expert-led videos and reinforce your knowledge through guided practice activities.';
const String ksTerms = """ const String ksTerms = """
<p style="color:#9C2C91;font-size:13px;"> <p style="color:#9C2C91;font-size:13px;">
Last updated: October 26, 2025 Last updated: October 26, 2025

View File

@ -1,5 +1,8 @@
// Registration type // Login method
enum RegistrationType { phone, email } enum LoginMethod { phone, email, google }
// Sign-up method
enum SignUpMethod { phone, email, google }
// Response status // Response status
enum ResponseStatus { success, failure } enum ResponseStatus { success, failure }
@ -10,19 +13,29 @@ enum ProficiencyLevels { a1, a2, b1, b2, none }
// Progress status // Progress status
enum ProgressStatuses { pending, started, completed } enum ProgressStatuses { pending, started, completed }
// Duolingo assessment types
enum DuolingoAssessmentType { speaking, reading, writing, listening }
// State object // State object
enum StateObjects { enum StateObjects {
courses,
homeView, homeView,
register,
verifyOtp, verifyOtp,
resendOtp, resendOtp,
profileImage, profileImage,
courseLessons,
profileUpdate, profileUpdate,
resetPassword, resetPassword,
subcategories,
loginWithEmail, loginWithEmail,
coursePractice,
loginWithGoogle, loginWithGoogle,
loadLessonVideo, loadLessonVideo,
loadCourseVideo,
requestResetCode, requestResetCode,
registerWithEmail, courseCategories,
profileCompletion, profileCompletion,
registerWithGoogle, registerWithGoogle,
loginWithPhoneNumber,
} }

View File

@ -1,5 +1,9 @@
// Split full name // Split full name
import 'dart:math';
import 'dart:ui';
import 'app_colors.dart';
Map<String, String> splitFullName(String fullName) { Map<String, String> splitFullName(String fullName) {
final parts = fullName.trim().split(RegExp(r'\s+')); final parts = fullName.trim().split(RegExp(r'\s+'));
@ -17,3 +21,25 @@ Map<String, String> splitFullName(String fullName) {
'last_name': parts.sublist(1).join(' '), 'last_name': parts.sublist(1).join(' '),
}; };
} }
Color getColor() {
final generator = Random();
int random = generator.nextInt(8);
if (random == 1) {
return kcRed.withValues(alpha: 0.2);
} else if (random == 2) {
return kcPrimaryColor.withValues(alpha: 0.2);
} else if (random == 3) {
return kcOrange.withValues(alpha: 0.2);
} else if (random == 4) {
return kcGreen.withValues(alpha: 0.2);
} else if (random == 5) {
return kcBlue.withValues(alpha: 0.2);
} else if (random == 6) {
return kcSkyBlue.withValues(alpha: 0.2);
} else if (random == 7) {
return kcIndigo.withValues(alpha: 0.2);
} else {
return kcAquamarine.withValues(alpha: 0.2);
}
}

View File

@ -1,8 +1,9 @@
import 'dart:math'; import 'dart:math';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:pinput/pinput.dart'; import 'package:pinput/pinput.dart';
import 'package:toastification/toastification.dart'; import 'package:toastification/toastification.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
@ -208,6 +209,11 @@ TextStyle style12R700 = const TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
); );
TextStyle style12P400 = const TextStyle(
fontSize: 12,
color: kcPrimaryColor,
);
TextStyle style12DG400 = const TextStyle( TextStyle style12DG400 = const TextStyle(
fontSize: 12, fontSize: 12,
color: kcDarkGrey, color: kcDarkGrey,
@ -267,6 +273,18 @@ TextStyle style18DG500 = const TextStyle(
fontWeight: FontWeight.w500, 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( TextStyle style18DG700 = const TextStyle(
fontSize: 18, fontSize: 18,
color: kcDarkGrey, color: kcDarkGrey,
@ -309,6 +327,8 @@ TextStyle validationStyle = const TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
); );
Duration kDuration = const Duration(seconds: 1);
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16)); Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
Map<String, Style> htmlStyle = { Map<String, Style> htmlStyle = {
@ -339,13 +359,19 @@ ChewieProgressColors buildChewieProgressIndicator = ChewieProgressColors(
Widget buildToastDescription(String message) => Text( Widget buildToastDescription(String message) => Text(
message, message,
maxLines: 4, maxLines: 4,
style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500), style: style14DG500,
);
Icon buildCloseIcon() => const Icon(
Icons.close,
color: kcPrimaryColor,
); );
void showErrorToast(String message) { void showErrorToast(String message) {
toastification.show( toastification.show(
showIcon: true, showIcon: true,
dragToClose: true, dragToClose: true,
icon: buildCloseIcon(),
showProgressBar: false, showProgressBar: false,
applyBlurEffect: false, applyBlurEffect: false,
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
@ -356,19 +382,21 @@ void showErrorToast(String message) {
autoCloseDuration: const Duration(seconds: 3), autoCloseDuration: const Duration(seconds: 3),
margin: const EdgeInsets.symmetric(horizontal: 15), margin: const EdgeInsets.symmetric(horizontal: 15),
borderSide: const BorderSide(color: kcPrimaryColor), borderSide: const BorderSide(color: kcPrimaryColor),
icon: const Icon(
Icons.close,
color: kcPrimaryColor,
),
); );
} }
Icon buildCheckIcon() => const Icon(
Icons.check,
color: kcPrimaryColor,
);
void showSuccessToast(String message) { void showSuccessToast(String message) {
toastification.show( toastification.show(
showIcon: true, showIcon: true,
dragToClose: true, dragToClose: true,
showProgressBar: false, showProgressBar: false,
applyBlurEffect: false, applyBlurEffect: false,
icon: buildCheckIcon(),
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
primaryColor: kcBackgroundColor, primaryColor: kcBackgroundColor,
type: ToastificationType.success, type: ToastificationType.success,
@ -377,9 +405,5 @@ void showSuccessToast(String message) {
autoCloseDuration: const Duration(seconds: 3), autoCloseDuration: const Duration(seconds: 3),
margin: const EdgeInsets.symmetric(horizontal: 15), margin: const EdgeInsets.symmetric(horizontal: 15),
borderSide: const BorderSide(color: kcPrimaryColor), borderSide: const BorderSide(color: kcPrimaryColor),
icon: const Icon(
Icons.check,
color: kcPrimaryColor,
),
); );
} }

View File

@ -10,12 +10,29 @@ class FormValidator {
if (value.isEmpty) { if (value.isEmpty) {
return 'The field is required'; return 'The field is required';
} }
return null; 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 // Email validator
static String? validateEmail(String? value) { static String? validateEmailForm(String? value) {
if (value == null) { if (value == null) {
return null; return null;
} }
@ -32,7 +49,7 @@ class FormValidator {
} }
// Password validator // Password validator
static String? validatePassword(String? value) { static String? validatePasswordForm(String? value) {
if (value == null) { if (value == null) {
return null; return null;
} }
@ -44,7 +61,7 @@ class FormValidator {
} }
// Phone number validator // Phone number validator
static String? validatePhoneNumber(String? value) { static String? validatePhoneNumberForm(String? value) {
if (value == null) { if (value == null) {
return null; return null;
} }
@ -66,5 +83,4 @@ class FormValidator {
} }
return null; return null;
} }
} }

View File

@ -1,12 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/assessment/screens/Assessment_form_screen.dart'; import 'package:yimaru_app/ui/views/assessment/screens/Assessment_form_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/assessment_completion_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/assessment_failure_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/assessment_intro_screen.dart'; import 'package:yimaru_app/ui/views/assessment/screens/assessment_intro_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/assessment_result_screen.dart'; import 'package:yimaru_app/ui/views/assessment/screens/assessment_result_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/result_analysis_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/retake_assessment_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/start_lesson_screen.dart'; import 'package:yimaru_app/ui/views/assessment/screens/start_lesson_screen.dart';
import 'assessment_viewmodel.dart'; import 'assessment_viewmodel.dart';
@ -24,7 +20,8 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
} }
@override @override
AssessmentViewModel viewModelBuilder(BuildContext context) => AssessmentViewModel(); AssessmentViewModel viewModelBuilder(BuildContext context) =>
AssessmentViewModel();
@override @override
Widget builder( Widget builder(
@ -32,7 +29,13 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
AssessmentViewModel viewModel, AssessmentViewModel viewModel,
Widget? child, Widget? child,
) => ) =>
_buildAssessmentScreens(viewModel); _buildAssessmentScreensWrapper(viewModel);
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>
PopScope(
canPop: viewModel.currentPage == 0 ? true : false,
onPopInvokedWithResult: (value, data) => viewModel.goBack(),
child: _buildAssessmentScreens(viewModel));
Widget _buildAssessmentScreens(AssessmentViewModel viewModel) => IndexedStack( Widget _buildAssessmentScreens(AssessmentViewModel viewModel) => IndexedStack(
index: viewModel.currentPage, index: viewModel.currentPage,
@ -56,17 +59,7 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
Widget _buildAssessment() => const AssessmentFormScreen(); Widget _buildAssessment() => const AssessmentFormScreen();
Widget _buildAssessmentFailure() => const AssessmentFailureScreen();
Widget _buildRetakeAssessment() => const RetakeAssessmentScreen();
Widget _buildResultAnalysis() => const ResultAnalysisScreen();
Widget _buildAssessmentCompletion() => const AssessmentCompletionScreen();
Widget _buildAssessmentResult() => const AssessmentResultScreen(); Widget _buildAssessmentResult() => const AssessmentResultScreen();
Widget _buildStartLesson() => const StartLessonScreen(); Widget _buildStartLesson() => const StartLessonScreen();
} }

View File

@ -1,5 +1,3 @@
import 'dart:math';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
@ -13,7 +11,6 @@ import '../../../models/assessment.dart';
import '../../../services/api_service.dart'; import '../../../services/api_service.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
import '../home/home_view.dart';
class AssessmentViewModel extends BaseViewModel { class AssessmentViewModel extends BaseViewModel {
// Dependency injection // Dependency injection
@ -223,11 +220,18 @@ class AssessmentViewModel extends BaseViewModel {
rebuildUi(); rebuildUi();
} }
void pop() { void goBack() {
if (_currentPage == 0 || _currentPage == 3 /*7*/) { if (_currentPage == 0) {
_navigationService.back(); _navigationService.back();
} else if (_currentPage != 0 && _currentPage != 3) { } else if (_currentPage == 2) {
_currentPage--; _currentPage = 0;
rebuildUi();
} else if (_currentPage == 3) {
if (_proficiencyLevel != ProficiencyLevels.none) {
_currentPage--;
} else {
_currentPage = 0;
}
rebuildUi(); rebuildUi();
} }
} }
@ -244,7 +248,7 @@ class AssessmentViewModel extends BaseViewModel {
Future<void> _getAssessments() async { Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
List<Assessment> response = await _apiService.getAssessments(); _assessments = await _apiService.getAssessments();
/* /*
for (int i = 0; i < 6; i++) { for (int i = 0; i < 6; i++) {
final generator = Random(); final generator = Random();
@ -252,7 +256,6 @@ class AssessmentViewModel extends BaseViewModel {
response.add(response[random]); response.add(response[random]);
} }
*/ */
_assessments = response;
} }
} }

View File

@ -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(
'Were 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,
);
}

View File

@ -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 didnt get enough from your assessment',
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
style: style14MG400,
textAlign: TextAlign.center,
);
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,
);
}

View File

@ -65,46 +65,33 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildAssessment(AssessmentViewModel viewModel) => PageView.builder( Widget _buildAssessment(AssessmentViewModel viewModel) => PageView.builder(
controller: viewModel.pageController, controller: viewModel.pageController,
itemCount: viewModel.assessments.length, itemCount: viewModel.assessments.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (cotext, index) => 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( Widget _buildBody(
{required int index, required AssessmentViewModel viewModel}) => {required int index, required AssessmentViewModel viewModel}) =>
Column( Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel: viewModel, index: index), children: _buildBodyChildren(viewModel: viewModel, index: index),
); );
List<Widget> _buildBodyChildren( List<Widget> _buildBodyChildren(
{required int index, required AssessmentViewModel viewModel}) => {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, verticalSpaceMedium,
_buildTitle(index: index, viewModel: viewModel), _buildTitle(index: index, viewModel: viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildAnswers(index: index, viewModel: viewModel) _buildAnswers(index: index, viewModel: viewModel),
_buildContinueButtonWrapper(viewModel: viewModel, question: index + 1)
]; ];
Widget _buildTitle( Widget _buildTitle(

View File

@ -24,8 +24,11 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
children: _buildScaffoldChildren(viewModel), children: _buildScaffoldChildren(viewModel),
); );
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => [
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; _buildAppBar(viewModel),
verticalSpaceMedium,
_buildExpandedBody(viewModel)
];
Widget _buildExpandedBody(AssessmentViewModel viewModel) => Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel)); Expanded(child: _buildBodyWrapper(viewModel));
@ -59,7 +62,7 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop, onPop: viewModel.goBack,
showLanguageSelection: true, showLanguageSelection: true,
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart'; import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';

View File

@ -26,11 +26,15 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
children: _buildScaffoldChildren(viewModel), children: _buildScaffoldChildren(viewModel),
); );
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => [
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; _buildAppBar(viewModel),
verticalSpaceMedium,
_buildExpandedBody(viewModel)
];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
showBackButton: false, showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true, showLanguageSelection: true,
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
@ -50,7 +54,7 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
); );
List<Widget> _buildBodyChildren(AssessmentViewModel viewModel) => List<Widget> _buildBodyChildren(AssessmentViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildLowerColumn(viewModel)]; [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(AssessmentViewModel viewModel) => Column( Widget _buildUpperColumn(AssessmentViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -96,41 +100,18 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
mainAxisSize: MainAxisSize.min, padding: const EdgeInsets.only(bottom: 50),
children: _buildLowerColumnChildren(viewModel), child: _buildContinueButton(viewModel),
); );
List<Widget> _buildLowerColumnChildren(AssessmentViewModel viewModel) => [
_buildContinueButton(viewModel),
verticalSpaceSmall,
_buildSkipButtonWrapper(viewModel)
];
Widget _buildContinueButton(AssessmentViewModel viewModel) => Widget _buildContinueButton(AssessmentViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
safe: false,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcPrimaryColor, 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,
);
} }

View File

@ -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(
'Were now analyzing your speaking skills',
style: style14MG400,
textAlign: TextAlign.center,
);
}

View File

@ -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 didnt get enough from your assessment',
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
style: style14MG400,
textAlign: TextAlign.center,
);
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,
);
}

View File

@ -42,12 +42,15 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
children: _buildScaffoldChildren(viewModel), children: _buildScaffoldChildren(viewModel),
); );
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => [
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; _buildAppBar(viewModel),
verticalSpaceMedium,
_buildExpandedBody(viewModel)
];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop, onPop: viewModel.goBack,
showLanguageSelection: true, showLanguageSelection: true,
); );

View File

@ -1,14 +1,26 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_card.dart';
import '../../../models/course_detail.dart';
import '../../../models/course_subcategory.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
import '../../widgets/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'; import 'course_viewmodel.dart';
class CourseView extends StackedView<CourseViewModel> { 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 @override
CourseViewModel viewModelBuilder(BuildContext context) => CourseViewModel(); CourseViewModel viewModelBuilder(BuildContext context) => CourseViewModel();
@ -38,68 +50,79 @@ class CourseView extends StackedView<CourseViewModel> {
children: [ children: [
verticalSpaceMedium, verticalSpaceMedium,
_buildAppBar(viewModel), _buildAppBar(viewModel),
_buildCourseColumnWrapper(viewModel) verticalSpaceMedium,
_buildCoursesColumnWrapper(viewModel),
], ],
); );
Widget _buildAppBar(CourseViewModel viewModel) => LearnAppBar( Widget _buildAppBar(CourseViewModel viewModel) => SmallAppBar(
name: viewModel.user?.firstName, onTap: viewModel.pop,
profileImage: viewModel.user?.profilePicture, showBackButton: true,
); );
Widget _buildCourseColumnWrapper(CourseViewModel viewModel) => Widget _buildCoursesColumnWrapper(CourseViewModel viewModel) =>
Expanded(child: _buildCourseColumnScrollView(viewModel)); Expanded(child: _buildCoursesColumnScrollView(viewModel));
Widget _buildCourseColumnScrollView(CourseViewModel viewModel) => Widget _buildCoursesColumnScrollView(CourseViewModel viewModel) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildCourseColumn(viewModel), child: _buildCoursesColumn(viewModel),
); );
Widget _buildCourseColumn(CourseViewModel viewModel) => Column( Widget _buildCoursesColumn(CourseViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel), children: _buildCoursesColumnChildren(viewModel),
); );
List<Widget> _buildLevelsColumnChildren(CourseViewModel viewModel) => [ List<Widget> _buildCoursesColumnChildren(CourseViewModel viewModel) => [
verticalSpaceLarge, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
_buildSubtitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildListView(viewModel) _buildListViewBuilder(viewModel)
]; ];
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'Courses', '${subcategory.title ?? ''} courses',
style: style20DG700, style: style18DG700,
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'Choose a course to improve your professional or exam skills.', 'Explore variety of courses on ${subcategory.title ?? ''}.',
style: style14DG400, 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( Widget _buildListView(CourseViewModel viewModel) => ListView.separated(
shrinkWrap: true, shrinkWrap: true,
itemCount: viewModel.courses.length, itemCount: viewModel.courseDetail.length,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile( itemBuilder: (context, index) => _buildTile(
title: viewModel.courses[index]['title'], courseDetail: viewModel.courseDetail[index],
subtitle: viewModel.courses[index]['subtitle'], onCourseTap: () async => await viewModel
onTap: () async => await viewModel.navigateToCourseModule(), .navigateToCoursePayment(viewModel.courseDetail[index].course!),
onPracticeTap: () async => await viewModel.navigateToCoursePractice(
viewModel.courseDetail[index].course?.id ?? 0),
), ),
separatorBuilder: (context, index) => verticalSpaceSmall, separatorBuilder: (context, index) => verticalSpaceSmall,
); );
//
Widget _buildTile({ Widget _buildTile({
required String title, GestureTapCallback? onCourseTap,
required String subtitle, GestureTapCallback? onPracticeTap,
required GestureTapCallback onTap, required CourseDetail courseDetail,
}) => }) =>
CourseCard( CourseTile(
title: title, onCourseTap: onCourseTap,
onTap: onTap, courseDetail: courseDetail,
subtitle: subtitle, onPracticeTap: onPracticeTap,
); );
} }

View File

@ -1,42 +1,50 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../models/user_model.dart'; import '../../../app/app.router.dart';
import '../../../services/authentication_service.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 // Dependency injection
final _courseService = locator<CourseService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
@override // Subcourse with progress
List<ListenableServiceMixin> get listenableServices => List<CourseDetail> _courseDetail = [];
[_authenticationService];
// Current user List<CourseDetail> get courseDetail => _courseDetail;
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;
// Navigation // Navigation
Future<void> navigateToCourseModule() async => void pop() => _navigationService.back();
_navigationService.navigateToCourseModuleView();
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();
}
}
} }

View 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,
);
}

View 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();
}
}
}
}

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