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