diff --git a/android/app/google-services.json b/android/app/google-services.json index 56a8b0b..a783b3b 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -1,46 +1,49 @@ { "project_info": { - "project_number": "574860813475", - "project_id": "yimaru-lms-e834e", - "storage_bucket": "yimaru-lms-e834e.firebasestorage.app" + "project_number": "900714037062", + "project_id": "yimaru-academy-5e7e2", + "storage_bucket": "yimaru-academy-5e7e2.firebasestorage.app" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:574860813475:android:cd7fa6cf3a0527d97acb16", + "mobilesdk_app_id": "1:900714037062:android:f11e3b69315b05304e6f47", "android_client_info": { "package_name": "com.yimaru.lms.app" } }, "oauth_client": [ { - "client_id": "574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com", + "client_id": "900714037062-ngc0gc426sfnnjjr494g4vni46ne5uqv.apps.googleusercontent.com", "client_type": 1, "android_info": { "package_name": "com.yimaru.lms.app", - "certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a" + "certificate_hash": "139ee56ac9763191d1eee882efc440c10530e6e9" } }, { - "client_id": "574860813475-m90u87plqaac4tb8oug32k41usossiod.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.yimaru.lms.app", - "certificate_hash": "29797902ad6a24212b9d9fad71562907956f6a6c" - } + "client_id": "900714037062-mtm9rps15br603nnn3a451rr2vllopro.apps.googleusercontent.com", + "client_type": 3 } ], "api_key": [ { - "current_key": "AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk" + "current_key": "AIzaSyAi8e4PMFH8QQU11DWftHdNEu8WUP7i2ww" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [ { - "client_id": "574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com", + "client_id": "900714037062-24ria5fcfet71o3vde8f6gsvsj1n68ec.apps.googleusercontent.com", "client_type": 3 + }, + { + "client_id": "900714037062-35bg0hsou56hg37mbcbpiar9uti7tcku.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.yimaru.lms.app" + } } ] } diff --git a/firebase.json b/firebase.json index 4d7ff66..8a4ee39 100644 --- a/firebase.json +++ b/firebase.json @@ -1 +1 @@ -{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-lms-e834e","appId":"1:574860813475:android:cd7fa6cf3a0527d97acb16","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-lms-e834e","configurations":{"android":"1:574860813475:android:cd7fa6cf3a0527d97acb16","ios":"1:574860813475:ios:3ac9f7c4ae1771287acb16"}}}}}} \ No newline at end of file +{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-academy-5e7e2","appId":"1:900714037062:android:f11e3b69315b05304e6f47","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-academy-5e7e2","configurations":{"android":"1:900714037062:android:f11e3b69315b05304e6f47","ios":"1:900714037062:ios:1caf8f24a4333b8e4e6f47"}}}}}} \ No newline at end of file diff --git a/lib/app/app.dart b/lib/app/app.dart index ff9e3fb..5897ed4 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -51,6 +51,7 @@ 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'; import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_view.dart'; +import 'package:yimaru_app/ui/views/learn_subcategory/learn_subcategory_view.dart'; // @stacked-import @StackedApp( @@ -90,6 +91,7 @@ import 'package:yimaru_app/ui/views/course_practice_question/course_practice_que MaterialRoute(page: CourseSubcategoryView), MaterialRoute(page: CourseView), MaterialRoute(page: CoursePracticeQuestionView), + MaterialRoute(page: LearnSubcategoryView), // @stacked-route ], dependencies: [ diff --git a/lib/app/app.router.dart b/lib/app/app.router.dart index 9198b1b..93af0b9 100644 --- a/lib/app/app.router.dart +++ b/lib/app/app.router.dart @@ -5,14 +5,15 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/material.dart' as _i37; +import 'package:flutter/material.dart' as _i38; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart' as _i1; -import 'package:stacked_services/stacked_services.dart' as _i42; -import 'package:yimaru_app/models/course.dart' as _i38; -import 'package:yimaru_app/models/course_category.dart' as _i40; -import 'package:yimaru_app/models/course_lesson.dart' as _i39; -import 'package:yimaru_app/models/course_subcategory.dart' as _i41; +import 'package:stacked_services/stacked_services.dart' as _i44; +import 'package:yimaru_app/models/category.dart' as _i42; +import 'package:yimaru_app/models/course.dart' as _i40; +import 'package:yimaru_app/models/course_lesson.dart' as _i41; +import 'package:yimaru_app/models/level.dart' as _i39; +import 'package:yimaru_app/models/subcategory.dart' as _i43; import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' as _i9; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i22; @@ -50,6 +51,8 @@ import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart' as _i20; import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart' as _i26; +import 'package:yimaru_app/ui/views/learn_subcategory/learn_subcategory_view.dart' + as _i37; import 'package:yimaru_app/ui/views/login/login_view.dart' as _i17; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3; import 'package:yimaru_app/ui/views/privacy_policy/privacy_policy_view.dart' @@ -138,6 +141,8 @@ class Routes { static const coursePracticeQuestionView = '/course-practice-question-view'; + static const learnSubcategoryView = '/learn-subcategory-view'; + static const all = { homeView, onboardingView, @@ -174,6 +179,7 @@ class Routes { courseSubcategoryView, courseView, coursePracticeQuestionView, + learnSubcategoryView, }; } @@ -319,17 +325,21 @@ class StackedRouter extends _i1.RouterBase { Routes.coursePracticeQuestionView, page: _i36.CoursePracticeQuestionView, ), + _i1.RouteDef( + Routes.learnSubcategoryView, + page: _i37.LearnSubcategoryView, + ), ]; final _pagesMap = { _i2.HomeView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i2.HomeView(), settings: data, ); }, _i3.OnboardingView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i3.OnboardingView(), settings: data, ); @@ -338,116 +348,120 @@ class StackedRouter extends _i1.RouterBase { final args = data.getArgs( orElse: () => const StartupViewArguments(), ); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i4.StartupView(key: args.key, label: args.label), settings: data, ); }, _i5.ProfileView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i5.ProfileView(), settings: data, ); }, _i6.ProfileDetailView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i6.ProfileDetailView(), settings: data, ); }, _i7.DownloadsView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i7.DownloadsView(), settings: data, ); }, _i8.ProgressView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i8.ProgressView(), settings: data, ); }, _i9.AccountPrivacyView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i9.AccountPrivacyView(), settings: data, ); }, _i10.SupportView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i10.SupportView(), settings: data, ); }, _i11.TelegramSupportView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i11.TelegramSupportView(), settings: data, ); }, _i12.CallSupportView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i12.CallSupportView(), settings: data, ); }, _i13.LanguageView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i13.LanguageView(), settings: data, ); }, _i14.PrivacyPolicyView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i14.PrivacyPolicyView(), settings: data, ); }, _i15.TermsAndConditionsView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i15.TermsAndConditionsView(), settings: data, ); }, _i16.RegisterView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i16.RegisterView(), settings: data, ); }, _i17.LoginView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i17.LoginView(), settings: data, ); }, _i18.LearnView: (data) { - return _i37.MaterialPageRoute( - builder: (context) => const _i18.LearnView(), + final args = data.getArgs(nullOk: false); + return _i38.MaterialPageRoute( + builder: (context) => _i18.LearnView(key: args.key, id: args.id), settings: data, ); }, _i19.LearnLevelView: (data) { - return _i37.MaterialPageRoute( - builder: (context) => const _i19.LearnLevelView(), + final args = data.getArgs(nullOk: false); + return _i38.MaterialPageRoute( + builder: (context) => _i19.LearnLevelView(key: args.key, id: args.id), settings: data, ); }, _i20.LearnModuleView: (data) { - return _i37.MaterialPageRoute( - builder: (context) => const _i20.LearnModuleView(), + final args = data.getArgs(nullOk: false); + return _i38.MaterialPageRoute( + builder: (context) => + _i20.LearnModuleView(key: args.key, level: args.level), settings: data, ); }, _i21.WelcomeView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i21.WelcomeView(), settings: data, ); }, _i22.AssessmentView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i22.AssessmentView(key: args.key, data: args.data), settings: data, @@ -455,7 +469,7 @@ class StackedRouter extends _i1.RouterBase { }, _i23.LearnLessonView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i23.LearnLessonView( key: args.key, title: args.title, @@ -467,14 +481,14 @@ class StackedRouter extends _i1.RouterBase { ); }, _i24.ForgetPasswordView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i24.ForgetPasswordView(), settings: data, ); }, _i25.LearnLessonDetailView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i25.LearnLessonDetailView( key: args.key, title: args.title, @@ -485,7 +499,7 @@ class StackedRouter extends _i1.RouterBase { }, _i26.LearnPracticeView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i26.LearnPracticeView( key: args.key, title: args.title, @@ -497,7 +511,7 @@ class StackedRouter extends _i1.RouterBase { }, _i27.CoursePracticeView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i27.CoursePracticeView(key: args.key, id: args.id), settings: data, @@ -505,21 +519,21 @@ class StackedRouter extends _i1.RouterBase { }, _i28.CoursePaymentView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i28.CoursePaymentView(key: args.key, course: args.course), settings: data, ); }, _i29.CourseCategoryView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i29.CourseCategoryView(), settings: data, ); }, _i30.FailureView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i30.FailureView(key: args.key, label: args.label), settings: data, @@ -527,7 +541,7 @@ class StackedRouter extends _i1.RouterBase { }, _i31.CourseLessonView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i31.CourseLessonView(key: args.key, course: args.course), settings: data, @@ -535,21 +549,21 @@ class StackedRouter extends _i1.RouterBase { }, _i32.CourseLessonDetailView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i32.CourseLessonDetailView(key: args.key, lesson: args.lesson), settings: data, ); }, _i33.DuolingoView: (data) { - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => const _i33.DuolingoView(), settings: data, ); }, _i34.CourseSubcategoryView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i34.CourseSubcategoryView(key: args.key, category: args.category), settings: data, @@ -557,7 +571,7 @@ class StackedRouter extends _i1.RouterBase { }, _i35.CourseView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i35.CourseView(key: args.key, subcategory: args.subcategory), settings: data, @@ -566,12 +580,18 @@ class StackedRouter extends _i1.RouterBase { _i36.CoursePracticeQuestionView: (data) { final args = data.getArgs(nullOk: false); - return _i37.MaterialPageRoute( + return _i38.MaterialPageRoute( builder: (context) => _i36.CoursePracticeQuestionView(key: args.key, id: args.id), settings: data, ); }, + _i37.LearnSubcategoryView: (data) { + return _i38.MaterialPageRoute( + builder: (context) => const _i37.LearnSubcategoryView(), + settings: data, + ); + }, }; @override @@ -587,7 +607,7 @@ class StartupViewArguments { this.label = 'Loading', }); - final _i37.Key? key; + final _i38.Key? key; final String label; @@ -608,13 +628,94 @@ class StartupViewArguments { } } +class LearnViewArguments { + const LearnViewArguments({ + this.key, + required this.id, + }); + + final _i38.Key? key; + + final int id; + + @override + String toString() { + return '{"key": "$key", "id": "$id"}'; + } + + @override + bool operator ==(covariant LearnViewArguments other) { + if (identical(this, other)) return true; + return other.key == key && other.id == id; + } + + @override + int get hashCode { + return key.hashCode ^ id.hashCode; + } +} + +class LearnLevelViewArguments { + const LearnLevelViewArguments({ + this.key, + required this.id, + }); + + final _i38.Key? key; + + final int id; + + @override + String toString() { + return '{"key": "$key", "id": "$id"}'; + } + + @override + bool operator ==(covariant LearnLevelViewArguments other) { + if (identical(this, other)) return true; + return other.key == key && other.id == id; + } + + @override + int get hashCode { + return key.hashCode ^ id.hashCode; + } +} + +class LearnModuleViewArguments { + const LearnModuleViewArguments({ + this.key, + required this.level, + }); + + final _i38.Key? key; + + final _i39.Level level; + + @override + String toString() { + return '{"key": "$key", "level": "$level"}'; + } + + @override + bool operator ==(covariant LearnModuleViewArguments other) { + if (identical(this, other)) return true; + return other.key == key && other.level == level; + } + + @override + int get hashCode { + return key.hashCode ^ level.hashCode; + } +} + class AssessmentViewArguments { const AssessmentViewArguments({ this.key, required this.data, }); - final _i37.Key? key; + final _i38.Key? key; final Map data; @@ -645,7 +746,7 @@ class LearnLessonViewArguments { required this.description, }); - final _i37.Key? key; + final _i38.Key? key; final String title; @@ -692,7 +793,7 @@ class LearnLessonDetailViewArguments { required this.description, }); - final _i37.Key? key; + final _i38.Key? key; final String title; @@ -732,7 +833,7 @@ class LearnPracticeViewArguments { required this.buttonLabel, }); - final _i37.Key? key; + final _i38.Key? key; final String title; @@ -773,7 +874,7 @@ class CoursePracticeViewArguments { required this.id, }); - final _i37.Key? key; + final _i38.Key? key; final int id; @@ -800,9 +901,9 @@ class CoursePaymentViewArguments { required this.course, }); - final _i37.Key? key; + final _i38.Key? key; - final _i38.Course course; + final _i40.Course course; @override String toString() { @@ -827,7 +928,7 @@ class FailureViewArguments { required this.label, }); - final _i37.Key? key; + final _i38.Key? key; final String label; @@ -854,9 +955,9 @@ class CourseLessonViewArguments { required this.course, }); - final _i37.Key? key; + final _i38.Key? key; - final _i38.Course course; + final _i40.Course course; @override String toString() { @@ -881,9 +982,9 @@ class CourseLessonDetailViewArguments { required this.lesson, }); - final _i37.Key? key; + final _i38.Key? key; - final _i39.CourseLesson lesson; + final _i41.CourseLesson lesson; @override String toString() { @@ -908,9 +1009,9 @@ class CourseSubcategoryViewArguments { required this.category, }); - final _i37.Key? key; + final _i38.Key? key; - final _i40.CourseCategory category; + final _i42.Category category; @override String toString() { @@ -935,9 +1036,9 @@ class CourseViewArguments { required this.subcategory, }); - final _i37.Key? key; + final _i38.Key? key; - final _i41.CourseSubcategory subcategory; + final _i43.Subcategory subcategory; @override String toString() { @@ -962,7 +1063,7 @@ class CoursePracticeQuestionViewArguments { required this.id, }); - final _i37.Key? key; + final _i38.Key? key; final int id; @@ -983,7 +1084,7 @@ class CoursePracticeQuestionViewArguments { } } -extension NavigatorStateExtension on _i42.NavigationService { +extension NavigatorStateExtension on _i44.NavigationService { Future navigateToHomeView([ int? routerId, bool preventDuplicates = true, @@ -1013,7 +1114,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToStartupView({ - _i37.Key? key, + _i38.Key? key, String label = 'Loading', int? routerId, bool preventDuplicates = true, @@ -1211,42 +1312,51 @@ extension NavigatorStateExtension on _i42.NavigationService { transition: transition); } - Future navigateToLearnView([ + Future navigateToLearnView({ + _i38.Key? key, + required int id, int? routerId, bool preventDuplicates = true, Map? parameters, Widget Function(BuildContext, Animation, Animation, Widget)? transition, - ]) async { + }) async { return navigateTo(Routes.learnView, + arguments: LearnViewArguments(key: key, id: id), id: routerId, preventDuplicates: preventDuplicates, parameters: parameters, transition: transition); } - Future navigateToLearnLevelView([ + Future navigateToLearnLevelView({ + _i38.Key? key, + required int id, int? routerId, bool preventDuplicates = true, Map? parameters, Widget Function(BuildContext, Animation, Animation, Widget)? transition, - ]) async { + }) async { return navigateTo(Routes.learnLevelView, + arguments: LearnLevelViewArguments(key: key, id: id), id: routerId, preventDuplicates: preventDuplicates, parameters: parameters, transition: transition); } - Future navigateToLearnModuleView([ + Future navigateToLearnModuleView({ + _i38.Key? key, + required _i39.Level level, int? routerId, bool preventDuplicates = true, Map? parameters, Widget Function(BuildContext, Animation, Animation, Widget)? transition, - ]) async { + }) async { return navigateTo(Routes.learnModuleView, + arguments: LearnModuleViewArguments(key: key, level: level), id: routerId, preventDuplicates: preventDuplicates, parameters: parameters, @@ -1268,7 +1378,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToAssessmentView({ - _i37.Key? key, + _i38.Key? key, required Map data, int? routerId, bool preventDuplicates = true, @@ -1285,7 +1395,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToLearnLessonView({ - _i37.Key? key, + _i38.Key? key, required String title, required String topics, required String subtitle, @@ -1326,7 +1436,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToLearnLessonDetailView({ - _i37.Key? key, + _i38.Key? key, required String title, required List> practices, required String description, @@ -1349,7 +1459,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToLearnPracticeView({ - _i37.Key? key, + _i38.Key? key, required String title, required String subtitle, required List> practices, @@ -1374,7 +1484,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToCoursePracticeView({ - _i37.Key? key, + _i38.Key? key, required int id, int? routerId, bool preventDuplicates = true, @@ -1391,8 +1501,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToCoursePaymentView({ - _i37.Key? key, - required _i38.Course course, + _i38.Key? key, + required _i40.Course course, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -1422,7 +1532,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToFailureView({ - _i37.Key? key, + _i38.Key? key, required String label, int? routerId, bool preventDuplicates = true, @@ -1439,8 +1549,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToCourseLessonView({ - _i37.Key? key, - required _i38.Course course, + _i38.Key? key, + required _i40.Course course, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -1456,8 +1566,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToCourseLessonDetailView({ - _i37.Key? key, - required _i39.CourseLesson lesson, + _i38.Key? key, + required _i41.CourseLesson lesson, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -1487,8 +1597,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToCourseSubcategoryView({ - _i37.Key? key, - required _i40.CourseCategory category, + _i38.Key? key, + required _i42.Category category, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -1504,8 +1614,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToCourseView({ - _i37.Key? key, - required _i41.CourseSubcategory subcategory, + _i38.Key? key, + required _i43.Subcategory subcategory, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -1521,7 +1631,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future navigateToCoursePracticeQuestionView({ - _i37.Key? key, + _i38.Key? key, required int id, int? routerId, bool preventDuplicates = true, @@ -1537,6 +1647,20 @@ extension NavigatorStateExtension on _i42.NavigationService { transition: transition); } + Future navigateToLearnSubcategoryView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.learnSubcategoryView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future replaceWithHomeView([ int? routerId, bool preventDuplicates = true, @@ -1566,7 +1690,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithStartupView({ - _i37.Key? key, + _i38.Key? key, String label = 'Loading', int? routerId, bool preventDuplicates = true, @@ -1764,42 +1888,51 @@ extension NavigatorStateExtension on _i42.NavigationService { transition: transition); } - Future replaceWithLearnView([ + Future replaceWithLearnView({ + _i38.Key? key, + required int id, int? routerId, bool preventDuplicates = true, Map? parameters, Widget Function(BuildContext, Animation, Animation, Widget)? transition, - ]) async { + }) async { return replaceWith(Routes.learnView, + arguments: LearnViewArguments(key: key, id: id), id: routerId, preventDuplicates: preventDuplicates, parameters: parameters, transition: transition); } - Future replaceWithLearnLevelView([ + Future replaceWithLearnLevelView({ + _i38.Key? key, + required int id, int? routerId, bool preventDuplicates = true, Map? parameters, Widget Function(BuildContext, Animation, Animation, Widget)? transition, - ]) async { + }) async { return replaceWith(Routes.learnLevelView, + arguments: LearnLevelViewArguments(key: key, id: id), id: routerId, preventDuplicates: preventDuplicates, parameters: parameters, transition: transition); } - Future replaceWithLearnModuleView([ + Future replaceWithLearnModuleView({ + _i38.Key? key, + required _i39.Level level, int? routerId, bool preventDuplicates = true, Map? parameters, Widget Function(BuildContext, Animation, Animation, Widget)? transition, - ]) async { + }) async { return replaceWith(Routes.learnModuleView, + arguments: LearnModuleViewArguments(key: key, level: level), id: routerId, preventDuplicates: preventDuplicates, parameters: parameters, @@ -1821,7 +1954,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithAssessmentView({ - _i37.Key? key, + _i38.Key? key, required Map data, int? routerId, bool preventDuplicates = true, @@ -1838,7 +1971,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithLearnLessonView({ - _i37.Key? key, + _i38.Key? key, required String title, required String topics, required String subtitle, @@ -1879,7 +2012,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithLearnLessonDetailView({ - _i37.Key? key, + _i38.Key? key, required String title, required List> practices, required String description, @@ -1902,7 +2035,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithLearnPracticeView({ - _i37.Key? key, + _i38.Key? key, required String title, required String subtitle, required List> practices, @@ -1927,7 +2060,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithCoursePracticeView({ - _i37.Key? key, + _i38.Key? key, required int id, int? routerId, bool preventDuplicates = true, @@ -1944,8 +2077,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithCoursePaymentView({ - _i37.Key? key, - required _i38.Course course, + _i38.Key? key, + required _i40.Course course, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -1975,7 +2108,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithFailureView({ - _i37.Key? key, + _i38.Key? key, required String label, int? routerId, bool preventDuplicates = true, @@ -1992,8 +2125,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithCourseLessonView({ - _i37.Key? key, - required _i38.Course course, + _i38.Key? key, + required _i40.Course course, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -2009,8 +2142,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithCourseLessonDetailView({ - _i37.Key? key, - required _i39.CourseLesson lesson, + _i38.Key? key, + required _i41.CourseLesson lesson, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -2040,8 +2173,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithCourseSubcategoryView({ - _i37.Key? key, - required _i40.CourseCategory category, + _i38.Key? key, + required _i42.Category category, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -2057,8 +2190,8 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithCourseView({ - _i37.Key? key, - required _i41.CourseSubcategory subcategory, + _i38.Key? key, + required _i43.Subcategory subcategory, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -2074,7 +2207,7 @@ extension NavigatorStateExtension on _i42.NavigationService { } Future replaceWithCoursePracticeQuestionView({ - _i37.Key? key, + _i38.Key? key, required int id, int? routerId, bool preventDuplicates = true, @@ -2089,4 +2222,18 @@ extension NavigatorStateExtension on _i42.NavigationService { parameters: parameters, transition: transition); } + + Future replaceWithLearnSubcategoryView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.learnSubcategoryView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } } diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 27ce554..d638499 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -50,21 +50,23 @@ class DefaultFirebaseOptions { } static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk', - appId: '1:574860813475:android:cd7fa6cf3a0527d97acb16', - messagingSenderId: '574860813475', - projectId: 'yimaru-lms-e834e', - storageBucket: 'yimaru-lms-e834e.firebasestorage.app', + apiKey: 'AIzaSyAi8e4PMFH8QQU11DWftHdNEu8WUP7i2ww', + appId: '1:900714037062:android:f11e3b69315b05304e6f47', + messagingSenderId: '900714037062', + projectId: 'yimaru-academy-5e7e2', + storageBucket: 'yimaru-academy-5e7e2.firebasestorage.app', ); static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyBBcQ17JB6RBTjD7G7mh6Xf_FMUGxP5cC8', - appId: '1:574860813475:ios:3ac9f7c4ae1771287acb16', - messagingSenderId: '574860813475', - projectId: 'yimaru-lms-e834e', - storageBucket: 'yimaru-lms-e834e.firebasestorage.app', + apiKey: 'AIzaSyDbaGD47oUJOyn9n3b0pbH6ozmbGyIlOKk', + appId: '1:900714037062:ios:1caf8f24a4333b8e4e6f47', + messagingSenderId: '900714037062', + projectId: 'yimaru-academy-5e7e2', + storageBucket: 'yimaru-academy-5e7e2.firebasestorage.app', androidClientId: - '574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com', + '900714037062-ngc0gc426sfnnjjr494g4vni46ne5uqv.apps.googleusercontent.com', + iosClientId: + '900714037062-35bg0hsou56hg37mbcbpiar9uti7tcku.apps.googleusercontent.com', iosBundleId: 'com.yimaru.lms.app', ); } diff --git a/lib/models/category.dart b/lib/models/category.dart new file mode 100644 index 0000000..7da14a8 --- /dev/null +++ b/lib/models/category.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'category.g.dart'; + +@JsonSerializable() +class Category { + final int? id; + + final String? name; + + @JsonKey(name: 'is_active') + final bool? isActive; + + @JsonKey(name: 'total_count') + final int? totalCount; + + const Category({this.id, this.name, this.isActive, this.totalCount}); + + factory Category.fromJson(Map json) => + _$CategoryFromJson(json); + + Map toJson() => _$CategoryToJson(this); +} diff --git a/lib/models/course_category.g.dart b/lib/models/category.g.dart similarity index 62% rename from lib/models/course_category.g.dart rename to lib/models/category.g.dart index cf3580e..c4f1ea2 100644 --- a/lib/models/course_category.g.dart +++ b/lib/models/category.g.dart @@ -1,21 +1,21 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'course_category.dart'; +part of 'category.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -CourseCategory _$CourseCategoryFromJson(Map json) => - CourseCategory( +Category _$CategoryFromJson(Map json) => Category( id: (json['id'] as num?)?.toInt(), name: json['name'] as String?, isActive: json['is_active'] as bool?, + totalCount: (json['total_count'] as num?)?.toInt(), ); -Map _$CourseCategoryToJson(CourseCategory instance) => - { +Map _$CategoryToJson(Category instance) => { 'id': instance.id, 'name': instance.name, 'is_active': instance.isActive, + 'total_count': instance.totalCount, }; diff --git a/lib/models/course.dart b/lib/models/course.dart index 240e39f..87a71fc 100644 --- a/lib/models/course.dart +++ b/lib/models/course.dart @@ -6,32 +6,38 @@ part 'course.g.dart'; 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; + @JsonKey(name: 'category_id') + final int? categoryId; - const Course( - {this.id, - this.level, - this.title, - this.isActive, - this.courseId, - this.thumbnail, - this.description, - this.displayOrder}); + @JsonKey(name: 'total_count') + final int? totalCount; + + @JsonKey(name: 'intro_video_url') + final String? introVideoUrl; + + @JsonKey(name: 'sub_category_id') + final int? subCategoryId; + + const Course({ + this.id, + this.title, + this.isActive, + this.thumbnail, + this.totalCount, + this.categoryId, + this.description, + this.introVideoUrl, + this.subCategoryId, + }); factory Course.fromJson(Map json) => _$CourseFromJson(json); diff --git a/lib/models/course.g.dart b/lib/models/course.g.dart index a85f7a4..38f7cc8 100644 --- a/lib/models/course.g.dart +++ b/lib/models/course.g.dart @@ -8,22 +8,24 @@ part of 'course.dart'; Course _$CourseFromJson(Map 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?, + totalCount: (json['total_count'] as num?)?.toInt(), + categoryId: (json['category_id'] as num?)?.toInt(), description: json['description'] as String?, - displayOrder: (json['display_order'] as num?)?.toInt(), + introVideoUrl: json['intro_video_url'] as String?, + subCategoryId: (json['sub_category_id'] as num?)?.toInt(), ); Map _$CourseToJson(Course instance) => { '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, + 'category_id': instance.categoryId, + 'total_count': instance.totalCount, + 'intro_video_url': instance.introVideoUrl, + 'sub_category_id': instance.subCategoryId, }; diff --git a/lib/models/course_category.dart b/lib/models/course_category.dart deleted file mode 100644 index e38c990..0000000 --- a/lib/models/course_category.dart +++ /dev/null @@ -1,20 +0,0 @@ -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 json) => - _$CourseCategoryFromJson(json); - - Map toJson() => _$CourseCategoryToJson(this); -} diff --git a/lib/models/course_lesson.g.dart b/lib/models/course_lesson.g.dart index 522709f..c4055be 100644 --- a/lib/models/course_lesson.g.dart +++ b/lib/models/course_lesson.g.dart @@ -33,8 +33,8 @@ Map _$CourseLessonToJson(CourseLesson instance) => 'visibility': instance.visibility, 'description': instance.description, 'video_url': instance.videoUrl, + 'vimeo_status': instance.vimeoStatus, 'instructor_id': instance.instructorId, 'sub_course_id': instance.courseId, - 'vimeo_status': instance.vimeoStatus, 'display_order': instance.displayOrder, }; diff --git a/lib/models/course_subcategory.dart b/lib/models/course_subcategory.dart deleted file mode 100644 index 2fa6a36..0000000 --- a/lib/models/course_subcategory.dart +++ /dev/null @@ -1,33 +0,0 @@ -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 json) => - _$CourseSubcategoryFromJson(json); - - Map toJson() => _$CourseSubcategoryToJson(this); -} diff --git a/lib/models/level.dart b/lib/models/level.dart new file mode 100644 index 0000000..525ebff --- /dev/null +++ b/lib/models/level.dart @@ -0,0 +1,41 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'level.g.dart'; + +@JsonSerializable() +class Level { + final int? id; + + final String? title; + + final String? thumbnail; + + final String? description; + + @JsonKey(name: 'is_active') + final bool? isActive; + + @JsonKey(name: 'cefr_level') + final String? cerfLevel; + + @JsonKey(name: 'total_count') + final int? totalCount; + + @JsonKey(name: 'display_order') + final int? displayOrder; + + const Level({ + this.id, + this.title, + this.isActive, + this.cerfLevel, + this.thumbnail, + this.totalCount, + this.description, + this.displayOrder, + }); + + factory Level.fromJson(Map json) => _$LevelFromJson(json); + + Map toJson() => _$LevelToJson(this); +} diff --git a/lib/models/course_subcategory.g.dart b/lib/models/level.g.dart similarity index 58% rename from lib/models/course_subcategory.g.dart rename to lib/models/level.g.dart index 5961724..9cb0b1d 100644 --- a/lib/models/course_subcategory.g.dart +++ b/lib/models/level.g.dart @@ -1,27 +1,29 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'course_subcategory.dart'; +part of 'level.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -CourseSubcategory _$CourseSubcategoryFromJson(Map json) => - CourseSubcategory( +Level _$LevelFromJson(Map json) => Level( id: (json['id'] as num?)?.toInt(), title: json['title'] as String?, isActive: json['is_active'] as bool?, + cerfLevel: json['cefr_level'] as String?, thumbnail: json['thumbnail'] as String?, - categoryId: (json['category_id'] as num?)?.toInt(), + totalCount: (json['total_count'] as num?)?.toInt(), description: json['description'] as String?, + displayOrder: (json['display_order'] as num?)?.toInt(), ); -Map _$CourseSubcategoryToJson(CourseSubcategory instance) => - { +Map _$LevelToJson(Level instance) => { 'id': instance.id, 'title': instance.title, 'thumbnail': instance.thumbnail, 'description': instance.description, 'is_active': instance.isActive, - 'category_id': instance.categoryId, + 'cefr_level': instance.cerfLevel, + 'total_count': instance.totalCount, + 'display_order': instance.displayOrder, }; diff --git a/lib/models/module.dart b/lib/models/module.dart new file mode 100644 index 0000000..87e6fff --- /dev/null +++ b/lib/models/module.dart @@ -0,0 +1,38 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'module.g.dart'; + +@JsonSerializable() +class Module { + final int? id; + + final String? title; + + final String? description; + + @JsonKey(name: 'icon_url') + final String? iconUrl; + + @JsonKey(name: 'level_id') + final int? levelId; + + @JsonKey(name: 'is_active') + final bool? isActive; + + @JsonKey(name: 'display_order') + final int? displayOrder; + + const Module({ + this.id, + this.title, + this.iconUrl, + this.levelId, + this.isActive, + this.description, + this.displayOrder, + }); + + factory Module.fromJson(Map json) => _$ModuleFromJson(json); + + Map toJson() => _$ModuleToJson(this); +} diff --git a/lib/models/module.g.dart b/lib/models/module.g.dart new file mode 100644 index 0000000..54c3bda --- /dev/null +++ b/lib/models/module.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'module.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Module _$ModuleFromJson(Map json) => Module( + id: (json['id'] as num?)?.toInt(), + title: json['title'] as String?, + iconUrl: json['icon_url'] as String?, + levelId: (json['level_id'] as num?)?.toInt(), + isActive: json['is_active'] as bool?, + description: json['description'] as String?, + displayOrder: (json['display_order'] as num?)?.toInt(), + ); + +Map _$ModuleToJson(Module instance) => { + 'id': instance.id, + 'title': instance.title, + 'description': instance.description, + 'icon_url': instance.iconUrl, + 'level_id': instance.levelId, + 'is_active': instance.isActive, + 'display_order': instance.displayOrder, + }; diff --git a/lib/models/option.g.dart b/lib/models/option.g.dart index 9a35085..556d786 100644 --- a/lib/models/option.g.dart +++ b/lib/models/option.g.dart @@ -14,6 +14,6 @@ Option _$OptionFromJson(Map json) => Option( Map _$OptionToJson(Option instance) => { 'id': instance.id, - 'option_text': instance.optionText, 'is_correct': instance.isCorrect, + 'option_text': instance.optionText, }; diff --git a/lib/models/subcategory.dart b/lib/models/subcategory.dart new file mode 100644 index 0000000..c658c7f --- /dev/null +++ b/lib/models/subcategory.dart @@ -0,0 +1,42 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'subcategory.g.dart'; + +@JsonSerializable() +class Subcategory { + final int? id; + + final String? name; + + final String? description; + + @JsonKey(name: 'is_active') + final bool? isActive; + + @JsonKey(name: 'total_count') + final int? totalCount; + + @JsonKey(name: 'category_id') + final int? categoryId; + + @JsonKey(name: 'category_name') + final String? categoryName; + + @JsonKey(name: 'display_order') + final int? displayOrder; + + const Subcategory( + {this.id, + this.name, + this.isActive, + this.totalCount, + this.categoryId, + this.description, + this.categoryName, + this.displayOrder}); + + factory Subcategory.fromJson(Map json) => + _$SubcategoryFromJson(json); + + Map toJson() => _$SubcategoryToJson(this); +} diff --git a/lib/models/subcategory.g.dart b/lib/models/subcategory.g.dart new file mode 100644 index 0000000..ce5c15a --- /dev/null +++ b/lib/models/subcategory.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'subcategory.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Subcategory _$SubcategoryFromJson(Map json) => Subcategory( + id: (json['id'] as num?)?.toInt(), + name: json['name'] as String?, + isActive: json['is_active'] as bool?, + totalCount: (json['total_count'] as num?)?.toInt(), + categoryId: (json['category_id'] as num?)?.toInt(), + description: json['description'] as String?, + categoryName: json['category_name'] as String?, + displayOrder: (json['display_order'] as num?)?.toInt(), + ); + +Map _$SubcategoryToJson(Subcategory instance) => + { + 'id': instance.id, + 'name': instance.name, + 'description': instance.description, + 'is_active': instance.isActive, + 'total_count': instance.totalCount, + 'category_id': instance.categoryId, + 'category_name': instance.categoryName, + 'display_order': instance.displayOrder, + }; diff --git a/lib/models/user_model.dart b/lib/models/user.dart similarity index 89% rename from lib/models/user_model.dart rename to lib/models/user.dart index 1308447..99ada3d 100644 --- a/lib/models/user_model.dart +++ b/lib/models/user.dart @@ -1,9 +1,9 @@ import 'package:json_annotation/json_annotation.dart'; -part 'user_model.g.dart'; +part 'user.g.dart'; @JsonSerializable() -class UserModel { +class User { final String? email; final String? gender; @@ -40,7 +40,7 @@ class UserModel { @JsonKey(name: 'profile_picture_url') final String? profilePicture; - const UserModel({ + const User({ this.email, this.region, this.gender, @@ -57,7 +57,7 @@ class UserModel { this.profileCompleted, }); - UserModel copyWith( + User copyWith( {int? userId, String? email, String? gender, @@ -72,7 +72,7 @@ class UserModel { bool? userInfoLoaded, bool? profileCompleted, String? profilePicture}) => - UserModel( + User( email: email ?? this.email, userId: userId ?? this.userId, gender: gender ?? this.gender, @@ -88,8 +88,7 @@ class UserModel { profilePicture: profilePicture ?? this.profilePicture, profileCompleted: profileCompleted ?? this.profileCompleted); - factory UserModel.fromJson(Map json) => - _$UserModelFromJson(json); + factory User.fromJson(Map json) => _$UserFromJson(json); - Map toJson() => _$UserModelToJson(this); + Map toJson() => _$UserToJson(this); } diff --git a/lib/models/user_model.g.dart b/lib/models/user.g.dart similarity index 89% rename from lib/models/user_model.g.dart rename to lib/models/user.g.dart index e52ef39..312d7d5 100644 --- a/lib/models/user_model.g.dart +++ b/lib/models/user.g.dart @@ -1,12 +1,12 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'user_model.dart'; +part of 'user.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -UserModel _$UserModelFromJson(Map json) => UserModel( +User _$UserFromJson(Map json) => User( email: json['email'] as String?, region: json['region'] as String?, gender: json['gender'] as String?, @@ -23,7 +23,7 @@ UserModel _$UserModelFromJson(Map json) => UserModel( profileCompleted: json['profile_completed'] as bool?, ); -Map _$UserModelToJson(UserModel instance) => { +Map _$UserToJson(User instance) => { 'email': instance.email, 'gender': instance.gender, 'region': instance.region, diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 2bc3727..ceac617 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -1,17 +1,19 @@ import 'package:dio/dio.dart'; +import 'package:yimaru_app/models/level.dart'; import 'package:yimaru_app/models/question.dart'; -import 'package:yimaru_app/models/course_subcategory.dart'; -import 'package:yimaru_app/models/course_category.dart'; +import 'package:yimaru_app/models/subcategory.dart'; +import 'package:yimaru_app/models/category.dart'; import 'package:yimaru_app/models/course_lesson.dart'; import 'package:yimaru_app/models/course_progress.dart'; import 'package:yimaru_app/models/course.dart'; import 'package:yimaru_app/models/practice.dart'; import 'package:yimaru_app/models/practice_question.dart'; -import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/models/user.dart'; import 'package:yimaru_app/services/dio_service.dart'; import 'package:yimaru_app/ui/common/app_constants.dart'; import '../app/app.locator.dart'; +import '../models/module.dart'; import '../ui/common/enmus.dart'; class ApiService { @@ -58,7 +60,7 @@ class ApiService { return { 'status': ResponseStatus.success, 'message': 'Logged in successfully', - 'data': UserModel.fromJson(response.data['data']), + 'data': User.fromJson(response.data['data']), }; } else { return { @@ -86,7 +88,7 @@ class ApiService { return { 'status': ResponseStatus.success, 'message': 'Logged in successfully', - 'data': UserModel.fromJson(response.data['data']), + 'data': User.fromJson(response.data['data']), }; } else { return { @@ -113,7 +115,7 @@ class ApiService { return { 'status': ResponseStatus.success, 'message': 'Otp verified successfully', - 'data': UserModel.fromJson(response.data['data']), + 'data': User.fromJson(response.data['data']), }; } else { return { @@ -212,7 +214,7 @@ class ApiService { } // GEt profile completion status - Future> getProfileStatus(UserModel? user) async { + Future> getProfileStatus(User? user) async { try { Response response = await _service.dio.get( '$kBaseUrl/$kUserBaseUrl/${user?.userId}/$kProfileStatusUrl', @@ -249,7 +251,7 @@ class ApiService { return { 'status': ResponseStatus.success, 'message': 'Profile fetched successfully', - 'data': UserModel.fromJson(response.data['data']), + 'data': User.fromJson(response.data['data']), }; } else { return { @@ -369,20 +371,20 @@ class ApiService { } } - // Get course categories - Future> getCourseCategories() async { + // Get categories + Future> getCategories() async { try { - List categories = []; + List categories = []; - final Response response = await _service.dio - .get('$kBaseUrl/$kCourseBaseUrl/$kCourseCategoryUrl'); + final Response response = await _service.dio.get( + '$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kCategoryUrl'); if (response.statusCode == 200) { var data = response.data; var decodedData = data['data']['categories'] as List; categories = decodedData.map( (e) { - return CourseCategory.fromJson(e); + return Category.fromJson(e); }, ).toList(); return categories; @@ -394,19 +396,19 @@ class ApiService { } // Get course subcategory - Future> getCourseSubcategories(int id) async { + Future> getSubcategories(int id) async { try { - List subcategories = []; + List subcategories = []; - final Response response = await _service.dio.get( - '$kBaseUrl/$kCourseBaseUrl/$kCourseCategoryUrl/$id/$kCoursesUrl'); + final Response response = await _service.dio + .get('$kBaseUrl/$kCourseBaseUrl/$kCategoryUrl/$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); + return Subcategory.fromJson(e); }, ).toList(); return subcategories; @@ -418,28 +420,28 @@ class ApiService { } // Get courses - Future> getCourses(int id) async { - try { - List 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 []; - } - } + // Future> getCourses(int id) async { + // try { + // List 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 []; + // } + // } // Get course progress Future> getCourseProgress(int id) async { @@ -576,4 +578,100 @@ class ApiService { return null; } } + + // Get learn subcategories + Future> getLearnSubcategories() async { + try { + List learnSubcategories = []; + + final Response response = await _service.dio.get( + '$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLearnSubcategoriesUrl'); + + if (response.statusCode == 200) { + var data = response.data; + var decodedData = data['data']['sub_categories'] as List; + learnSubcategories = decodedData.map( + (e) { + return Subcategory.fromJson(e); + }, + ).toList(); + return learnSubcategories; + } + return []; + } catch (e) { + return []; + } + } + + // Get courses + Future> getCourses(int id) async { + try { + List courses = []; + + final Response response = await _service.dio.get( + '$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kSubcategoriesUrl/$id/$kCoursesUrl'); + + if (response.statusCode == 200) { + var data = response.data; + var decodedData = data['data']['courses'] as List; + courses = decodedData.map( + (e) { + return Course.fromJson(e); + }, + ).toList(); + return courses; + } + return []; + } catch (e) { + return []; + } + } + + // Get levels + Future> getLevels(int id) async { + try { + List levels = []; + + final Response response = await _service.dio.get( + '$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kCoursesUrl/$id/$kLevelsUrl'); + + if (response.statusCode == 200) { + var data = response.data; + var decodedData = data['data']['levels'] as List; + levels = decodedData.map( + (e) { + return Level.fromJson(e); + }, + ).toList(); + return levels; + } + return []; + } catch (e) { + return []; + } + } + + // Get modules + Future> getModules(int id) async { + try { + List modules = []; + + final Response response = await _service.dio.get( + '$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLevelsUrl/$id/$kModulesUrl'); + + if (response.statusCode == 200) { + var data = response.data; + var decodedData = data['data']['modules'] as List; + modules = decodedData.map( + (e) { + return Module.fromJson(e); + }, + ).toList(); + return modules; + } + return []; + } catch (e) { + return []; + } + } } diff --git a/lib/services/authentication_service.dart b/lib/services/authentication_service.dart index 92f2868..1896566 100644 --- a/lib/services/authentication_service.dart +++ b/lib/services/authentication_service.dart @@ -1,6 +1,6 @@ import 'package:stacked/stacked.dart'; import 'package:yimaru_app/app/app.locator.dart'; -import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/models/user.dart'; import 'package:yimaru_app/services/secure_storage_service.dart'; class AuthenticationService with ListenableServiceMixin { @@ -8,9 +8,9 @@ class AuthenticationService with ListenableServiceMixin { final _secureService = locator(); // User data - UserModel? _user; + User? _user; - UserModel? get user => _user; + User? get user => _user; // Initialization AuthenticationService() { @@ -51,7 +51,7 @@ class AuthenticationService with ListenableServiceMixin { await _secureService.setString('accessToken', data['accessToken']); await _secureService.setString('refreshToken', data['refreshToken']); - _user = UserModel( + _user = User( userId: await _secureService.getInt('userId'), accessToken: await _secureService.getString('accessToken'), refreshToken: await _secureService.getString('refreshToken'), @@ -67,7 +67,6 @@ class AuthenticationService with ListenableServiceMixin { profileCompleted: await _secureService.getBool('profileCompleted'), ); - notifyListeners(); } @@ -83,7 +82,7 @@ class AuthenticationService with ListenableServiceMixin { } // Save user data - Future saveUserData(UserModel data) async { + Future saveUserData(User data) async { await _secureService.setBool('userInfoLoaded', true); await _secureService.setBool( 'profileCompleted', data.profileCompleted ?? false); @@ -96,7 +95,7 @@ class AuthenticationService with ListenableServiceMixin { await _secureService.setString('firstName', data.firstName ?? ''); await _secureService.setString('occupation', data.occupation ?? ''); - _user = UserModel( + _user = User( email: data.email, gender: data.gender, region: data.region, @@ -148,8 +147,8 @@ class AuthenticationService with ListenableServiceMixin { } // Get user data - Future getUser() async { - _user = UserModel( + Future getUser() async { + _user = User( userId: await _secureService.getInt('userId'), email: await _secureService.getString('email'), region: await _secureService.getString('region'), diff --git a/lib/services/dio_service.dart b/lib/services/dio_service.dart index 1558619..14caa98 100644 --- a/lib/services/dio_service.dart +++ b/lib/services/dio_service.dart @@ -2,7 +2,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.router.dart'; -import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/models/user.dart'; import 'package:yimaru_app/services/authentication_service.dart'; import '../app/app.locator.dart'; @@ -135,7 +135,7 @@ class DioService { // Refresh token Future _refreshToken() async { - final UserModel? user = await _authenticationService.getUser(); + final User? user = await _authenticationService.getUser(); if (user?.refreshToken == null) return false; diff --git a/lib/ui/common/app_constants.dart b/lib/ui/common/app_constants.dart index fa0a855..63a14dc 100644 --- a/lib/ui/common/app_constants.dart +++ b/lib/ui/common/app_constants.dart @@ -1,9 +1,19 @@ String kBaseUrl = 'https://api.yimaruacademy.com'; +String kApiUrl = 'api'; + +String kApiVersionUrl = 'v1'; + +String kLevelsUrl = 'levels'; + String kCoursesUrl = 'courses'; +String kModulesUrl = 'modules'; + String kRegisterUrl = 'register'; +String kCategoryUrl = 'categories'; + String kCoursePractice = 'by-owner'; String kUserBaseUrl = 'api/v1/user'; @@ -20,10 +30,10 @@ String kCompleteLessonUrl = 'complete'; String kResetPassword = 'resetPassword'; -String kCourseCategoryUrl = 'categories'; - String kRequestResetCode = 'sendResetCode'; +String kSubcategoriesUrl = 'sub-categories'; + String kPublishedVideos = 'videos/published'; String kCoursePracticeQuestions = 'questions'; @@ -36,6 +46,8 @@ String kLoginUrl = 'api/v1/auth/customer-login'; String kPracticeBaseUrl = 'api/v1/question-sets'; +String kCourseManagementUrl = 'course-management'; + String kProfileStatusUrl = 'is-profile-completed'; String kCourseBaseUrl = 'api/v1/course-management'; @@ -50,10 +62,12 @@ String kCourseProgressUrl = 'api/v1/progress/courses'; String kAssessmentsUrl = 'api/v1/assessment/questions'; +String kLearnSubcategoriesUrl = 'human-language/sub-categories'; + 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'; + '900714037062-24ria5fcfet71o3vde8f6gsvsj1n68ec.apps.googleusercontent.com'; diff --git a/lib/ui/common/app_strings.dart b/lib/ui/common/app_strings.dart index 1cde717..0aee3bb 100644 --- a/lib/ui/common/app_strings.dart +++ b/lib/ui/common/app_strings.dart @@ -12,8 +12,6 @@ const String ksHomeBottomSheetDescription = 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 ksTerms = """

Last updated: October 26, 2025 diff --git a/lib/ui/common/enmus.dart b/lib/ui/common/enmus.dart index ddf461d..fa8789f 100644 --- a/lib/ui/common/enmus.dart +++ b/lib/ui/common/enmus.dart @@ -27,6 +27,9 @@ enum StateObjects { register, verifyOtp, resendOtp, + learnLevels, + learnModules, + learnCourses, profileImage, courseLessons, profileUpdate, @@ -41,6 +44,7 @@ enum StateObjects { courseCategories, profileCompletion, registerWithGoogle, + learnSubcategories, learnPracticeSample, learnPracticeAnswer, loginWithPhoneNumber, diff --git a/lib/ui/common/helper_functions.dart b/lib/ui/common/helper_functions.dart index a58b7da..eac0293 100644 --- a/lib/ui/common/helper_functions.dart +++ b/lib/ui/common/helper_functions.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'dart:ui'; import 'app_colors.dart'; + // Split full name Map splitFullName(String fullName) { final parts = fullName.trim().split(RegExp(r'\s+')); diff --git a/lib/ui/views/account_privacy/account_privacy_view.dart b/lib/ui/views/account_privacy/account_privacy_view.dart index e2fd270..d68c7f8 100644 --- a/lib/ui/views/account_privacy/account_privacy_view.dart +++ b/lib/ui/views/account_privacy/account_privacy_view.dart @@ -127,8 +127,8 @@ class AccountPrivacyView extends StackedView { Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) => CustomListTile( - icon: Icons.shield_moon_outlined, title: 'Privacy Policy', + icon: Icons.shield_moon_outlined, onTap: () async => await viewModel.navigateToPrivacyPolicy(), ); diff --git a/lib/ui/views/account_privacy/account_privacy_viewmodel.dart b/lib/ui/views/account_privacy/account_privacy_viewmodel.dart index 5d0fc55..cba03ed 100644 --- a/lib/ui/views/account_privacy/account_privacy_viewmodel.dart +++ b/lib/ui/views/account_privacy/account_privacy_viewmodel.dart @@ -11,12 +11,12 @@ class AccountPrivacyViewModel extends BaseViewModel { // Navigation void pop() => _navigationService.back(); + Future navigateToTerms() async => + await _navigationService.navigateToTermsAndConditionsView(); + Future navigateToLanguage() async => await _navigationService.navigateToLanguageView(); Future navigateToPrivacyPolicy() async => await _navigationService.navigateToPrivacyPolicyView(); - - Future navigateToTerms() async => - await _navigationService.navigateToTermsAndConditionsView(); } diff --git a/lib/ui/views/assessment/assessment_view.dart b/lib/ui/views/assessment/assessment_view.dart index f07a4c9..5cdaf28 100644 --- a/lib/ui/views/assessment/assessment_view.dart +++ b/lib/ui/views/assessment/assessment_view.dart @@ -1,6 +1,6 @@ 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_questions_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/start_lesson_screen.dart'; @@ -51,7 +51,7 @@ class AssessmentView extends StackedView { Widget _buildAssessmentIntro() => const AssessmentIntroScreen(); - Widget _buildAssessment() => const AssessmentFormScreen(); + Widget _buildAssessment() => const AssessmentQuestionsScreen(); Widget _buildAssessmentResult() => const AssessmentResultScreen(); diff --git a/lib/ui/views/assessment/assessment_viewmodel.dart b/lib/ui/views/assessment/assessment_viewmodel.dart index bfdbcb8..3c08b2c 100644 --- a/lib/ui/views/assessment/assessment_viewmodel.dart +++ b/lib/ui/views/assessment/assessment_viewmodel.dart @@ -37,14 +37,14 @@ class AssessmentViewModel extends BaseViewModel { int get currentQuestion => _currentQuestion; - ProficiencyLevels _proficiencyLevel = ProficiencyLevels.none; - - ProficiencyLevels get proficiencyLevel => _proficiencyLevel; - List _assessments = []; List get assessments => _assessments; + ProficiencyLevels _proficiencyLevel = ProficiencyLevels.none; + + ProficiencyLevels get proficiencyLevel => _proficiencyLevel; + final Map _selectedAnswers = {}; Map get selectedAnswers => _selectedAnswers; @@ -55,20 +55,6 @@ class AssessmentViewModel extends BaseViewModel { Map get userData => _userData; // Assessment - int countCorrectAnswersUntil(int untilQuestion) { - int count = 0; - - for (int i = 1; i <= untilQuestion; i++) { - final answer = _selectedAnswers[i.toString()]; - - if (answer is Map && answer['correct'] == true) { - count++; - } - } - - return count; - } - Map evaluateAssessment() { if (_currentQuestion == 5) { // A1 @@ -111,6 +97,24 @@ class AssessmentViewModel extends BaseViewModel { } } + int countCorrectAnswersUntil(int untilQuestion) { + int count = 0; + + for (int i = 1; i <= untilQuestion; i++) { + final answer = _selectedAnswers[i.toString()]; + + if (answer is Map && answer['correct'] == true) { + count++; + } + } + + return count; + } + + bool isSelectedAnswer({required int question, required String answer}) { + return _selectedAnswers[question.toString()]?['option'] == answer; + } + void setSelectedAnswer({required int question, required Option? option}) { bool correct = false; if (option?.isCorrect ?? false) { @@ -133,43 +137,18 @@ class AssessmentViewModel extends BaseViewModel { rebuildUi(); } - bool isSelectedAnswer({required int question, required String answer}) { - return _selectedAnswers[question.toString()]?['option'] == answer; - } - - // Add user data - void initUserData(Map data) { - clearUserData(); - _userData.addAll(data); + // User data + void clearUserData() { + _userData.clear(); } void addUserData(Map data) { _userData.addAll(data); } - void clearUserData() { - _userData.clear(); - } - - // Dialog - Future showAbortDialog() async { - DialogResponse? response = await _dialogService.showDialog( - cancelTitle: 'No', - buttonTitle: 'Yes', - barrierDismissible: true, - title: 'Abort Assessment', - cancelTitleColor: kcDarkGrey, - buttonTitleColor: kcPrimaryColor, - description: 'Are you sure to abort the assessment ?', - ); - return response?.confirmed; - } - - Future abort() async { - bool? response = await showAbortDialog(); - if (response != null && response) { - next(page: 3); - } + void initUserData(Map data) { + clearUserData(); + _userData.addAll(data); } // Question navigation @@ -206,19 +185,6 @@ class AssessmentViewModel extends BaseViewModel { } // In-app navigation - void next({int? page}) async { - if (page == null) { - if (_previousPage != 0) { - _currentPage = _previousPage; - } else { - _currentPage++; - } - } else { - _previousPage = _currentPage; - _currentPage = page; - } - rebuildUi(); - } void goBack() { if (_currentPage == 0) { @@ -236,23 +202,51 @@ class AssessmentViewModel extends BaseViewModel { } } + void next({int? page}) async { + if (page == null) { + if (_previousPage != 0) { + _currentPage = _previousPage; + } else { + _currentPage++; + } + } else { + _previousPage = _currentPage; + _currentPage = page; + } + rebuildUi(); + } + + // Dialog + Future abort() async { + bool? response = await showAbortDialog(); + if (response != null && response) { + next(page: 3); + } + } + + Future showAbortDialog() async { + DialogResponse? response = await _dialogService.showDialog( + cancelTitle: 'No', + buttonTitle: 'Yes', + barrierDismissible: true, + title: 'Abort Assessment', + cancelTitleColor: kcDarkGrey, + buttonTitleColor: kcPrimaryColor, + description: 'Are you sure to abort the assessment ?', + ); + return response?.confirmed; + } + // Navigation void pop() => _navigationService.back(); - Future navigateToLanguage() async => - await _navigationService.navigateToLanguageView(); - Future replaceWithHome() async => await _navigationService.clearStackAndShow(Routes.homeView); - // Remote api call - Future getAssessments() async => await runBusyFuture(_getAssessments()); + Future navigateToLanguage() async => + await _navigationService.navigateToLanguageView(); - Future _getAssessments() async { - if (await _statusChecker.checkConnection()) { - _assessments = await _apiService.getAssessments(); - } - } + // Remote api call // Complete profile Future completeProfile() async => @@ -272,4 +266,13 @@ class AssessmentViewModel extends BaseViewModel { } } } + + // Assessments + Future _getAssessments() async { + if (await _statusChecker.checkConnection()) { + _assessments = await _apiService.getAssessments(); + } + } + + Future getAssessments() async => await runBusyFuture(_getAssessments()); } diff --git a/lib/ui/views/assessment/screens/assessment_form_screen.dart b/lib/ui/views/assessment/screens/assessment_questions_screen.dart similarity index 97% rename from lib/ui/views/assessment/screens/assessment_form_screen.dart rename to lib/ui/views/assessment/screens/assessment_questions_screen.dart index 06e5ed2..0f0ebef 100644 --- a/lib/ui/views/assessment/screens/assessment_form_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_questions_screen.dart @@ -9,8 +9,8 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import '../assessment_viewmodel.dart'; import 'assessment_loading_screen.dart'; -class AssessmentFormScreen extends ViewModelWidget { - const AssessmentFormScreen({super.key}); +class AssessmentQuestionsScreen extends ViewModelWidget { + const AssessmentQuestionsScreen({super.key}); @override Widget build(BuildContext context, AssessmentViewModel viewModel) => @@ -109,14 +109,14 @@ class AssessmentFormScreen extends ViewModelWidget { physics: const NeverScrollableScrollPhysics(), itemCount: viewModel.assessments[index].options?.length, itemBuilder: (context, inner) => _buildAnswer( + onTap: () => viewModel.setSelectedAnswer( + question: index + 1, + option: viewModel.assessments[index].options?[inner]), title: viewModel.assessments[index].options?[inner].optionText ?? '', selected: viewModel.isSelectedAnswer( question: index + 1, answer: viewModel.assessments[index].options?[inner].optionText ?? ''), - onTap: () => viewModel.setSelectedAnswer( - question: index + 1, - option: viewModel.assessments[index].options?[inner]), ), ); diff --git a/lib/ui/views/course/course_view.dart b/lib/ui/views/course/course_view.dart index 754c875..592c1a0 100644 --- a/lib/ui/views/course/course_view.dart +++ b/lib/ui/views/course/course_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import '../../../models/course_detail.dart'; -import '../../../models/course_subcategory.dart'; +import '../../../models/subcategory.dart'; import '../../common/app_colors.dart'; import '../../common/enmus.dart'; import '../../common/ui_helpers.dart'; @@ -12,7 +12,7 @@ import '../../widgets/small_app_bar.dart'; import 'course_viewmodel.dart'; class CourseView extends StackedView { - final CourseSubcategory subcategory; + final Subcategory subcategory; const CourseView({Key? key, required this.subcategory}) : super(key: key); @@ -83,12 +83,12 @@ class CourseView extends StackedView { ]; Widget _buildTitle() => Text( - '${subcategory.title ?? ''} courses', + '${subcategory.name ?? ''} courses', style: style18DG700, ); Widget _buildSubtitle() => Text( - 'Explore variety of courses on ${subcategory.title ?? ''}.', + 'Explore variety of courses on ${subcategory.name ?? ''}.', style: style14DG400, ); @@ -105,6 +105,7 @@ class CourseView extends StackedView { shrinkWrap: true, itemCount: viewModel.courseDetail.length, physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => verticalSpaceSmall, itemBuilder: (context, index) => _buildTile( courseDetail: viewModel.courseDetail[index], onCourseTap: () async => await viewModel @@ -112,7 +113,6 @@ class CourseView extends StackedView { onPracticeTap: () async => await viewModel.navigateToCoursePractice( viewModel.courseDetail[index].course?.id ?? 0), ), - separatorBuilder: (context, index) => verticalSpaceSmall, ); Widget _buildTile({ diff --git a/lib/ui/views/course/course_viewmodel.dart b/lib/ui/views/course/course_viewmodel.dart index 44a27cf..4de1a05 100644 --- a/lib/ui/views/course/course_viewmodel.dart +++ b/lib/ui/views/course/course_viewmodel.dart @@ -11,7 +11,6 @@ import '../../common/enmus.dart'; class CourseViewModel extends BaseViewModel { // Dependency injection - final _courseService = locator(); final _statusChecker = locator(); @@ -26,15 +25,15 @@ class CourseViewModel extends BaseViewModel { // Navigation void pop() => _navigationService.back(); - Future navigateToCoursePayment(Course course) async => - _navigationService.navigateToCoursePaymentView(course: course); - Future navigateToCoursePractice(int id) => _navigationService.navigateToCoursePracticeView(id: id); + Future navigateToCoursePayment(Course course) async => + _navigationService.navigateToCoursePaymentView(course: course); + // Remote api call - // Sub course + // Course detail Future getCourseDetails(int id) async => await runBusyFuture(_getCourseDetails(id), busyObject: StateObjects.courses); @@ -42,8 +41,8 @@ class CourseViewModel extends BaseViewModel { Future _getCourseDetails(int id) async { if (await _statusChecker.checkConnection()) { _courseDetail = await _courseService.getCoursesDetail(id); - _courseDetail.sort((a, b) => - (a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0)); + // _courseDetail.sort((a, b) => + // (a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0)); } } } diff --git a/lib/ui/views/course_category/course_category_view.dart b/lib/ui/views/course_category/course_category_view.dart index 48086aa..05d11eb 100644 --- a/lib/ui/views/course_category/course_category_view.dart +++ b/lib/ui/views/course_category/course_category_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; -import '../../../models/course_category.dart'; +import '../../../models/category.dart'; import '../../common/app_colors.dart'; import '../../common/enmus.dart'; import '../../common/ui_helpers.dart'; @@ -15,7 +15,7 @@ class CourseCategoryView extends StackedView { @override void onViewModelReady(CourseCategoryViewModel viewModel) async { - await viewModel.getCourseCategories(); + await viewModel.getCategories(); super.onViewModelReady(viewModel); } @@ -114,7 +114,7 @@ class CourseCategoryView extends StackedView { // Widget _buildTile({ - required CourseCategory category, + required Category category, required GestureTapCallback onTap, }) => CourseCategoryCard( diff --git a/lib/ui/views/course_category/course_category_viewmodel.dart b/lib/ui/views/course_category/course_category_viewmodel.dart index 8cfb712..683f6f3 100644 --- a/lib/ui/views/course_category/course_category_viewmodel.dart +++ b/lib/ui/views/course_category/course_category_viewmodel.dart @@ -3,8 +3,8 @@ import 'package:stacked_services/stacked_services.dart'; import '../../../app/app.locator.dart'; import '../../../app/app.router.dart'; -import '../../../models/course_category.dart'; -import '../../../models/user_model.dart'; +import '../../../models/category.dart'; +import '../../../models/user.dart'; import '../../../services/api_service.dart'; import '../../../services/authentication_service.dart'; import '../../../services/status_checker_service.dart'; @@ -25,31 +25,30 @@ class CourseCategoryViewModel extends ReactiveViewModel { [_authenticationService]; // Current user - UserModel? get _user => _authenticationService.user; + User? get _user => _authenticationService.user; - UserModel? get user => _user; + User? get user => _user; // Course categories - List _categories = []; + List _categories = []; - List get categories => _categories; + List get categories => _categories; // Navigation - Future navigateToCourseCategory(CourseCategory category) async => + Future navigateToCourseCategory(Category category) async => _navigationService.navigateToCourseSubcategoryView(category: category); // Remote api call // Course categories - Future getCourseCategories() async => - await runBusyFuture(_getCourseCategories(), + Future getCategories() async => + await runBusyFuture(_getCategories(), busyObject: StateObjects.courseCategories); - Future _getCourseCategories() async { + Future _getCategories() async { if (categories.isEmpty) { if (await _statusChecker.checkConnection()) { - _categories = await _apiService.getCourseCategories(); - + _categories = await _apiService.getCategories(); } } } diff --git a/lib/ui/views/course_lesson/course_lesson_view.dart b/lib/ui/views/course_lesson/course_lesson_view.dart index cf88f6d..b4e5a2d 100644 --- a/lib/ui/views/course_lesson/course_lesson_view.dart +++ b/lib/ui/views/course_lesson/course_lesson_view.dart @@ -76,12 +76,8 @@ class CourseLessonView extends StackedView { children: _buildLessonColumnChildren(viewModel), ); - List _buildLessonColumnChildren(CourseLessonViewModel viewModel) => [ - // verticalSpaceLarge, - _buildTitle(), - verticalSpaceMedium, - _buildListViewBuilder(viewModel) - ]; + List _buildLessonColumnChildren(CourseLessonViewModel viewModel) => + [_buildTitle(), verticalSpaceMedium, _buildListViewBuilder(viewModel)]; Widget _buildTitle() => Text( '${course.title} course lessons', diff --git a/lib/ui/views/course_lesson_detail/course_lesson_detail_viewmodel.dart b/lib/ui/views/course_lesson_detail/course_lesson_detail_viewmodel.dart index 257b4ac..7d76635 100644 --- a/lib/ui/views/course_lesson_detail/course_lesson_detail_viewmodel.dart +++ b/lib/ui/views/course_lesson_detail/course_lesson_detail_viewmodel.dart @@ -29,6 +29,38 @@ class CourseLessonDetailViewModel extends BaseViewModel { VideoPlayerController? get videoPlayerController => _videoPlayerController; // Video player + + @override + void dispose() { + _videoPlayerController?.dispose(); + _chewieController?.dispose(); + super.dispose(); + } + + Future pause() async { + await _chewieController?.pause(); + } + + Future _videoListener(CourseLesson lesson) async { + final controller = _videoPlayerController; + + if (controller == null || !controller.value.isInitialized) return; + + final position = controller.value.position; + final duration = controller.value.duration; + + if (duration.inSeconds == 0) return; + + double progress = position.inSeconds / duration.inSeconds; + + print("Video progress: ${(progress * 100).toStringAsFixed(2)}%"); + + // Example: mark completion at 95% + if (progress >= 0.95) { + await _onVideoCompleted(lesson); + } + } + Future initializePlayer(CourseLesson lesson) async => await runBusyFuture(_initializePlayer(lesson), busyObject: StateObjects.loadCourseVideo); @@ -59,26 +91,6 @@ class CourseLessonDetailViewModel extends BaseViewModel { rebuildUi(); } - Future _videoListener(CourseLesson lesson) async { - final controller = _videoPlayerController; - - if (controller == null || !controller.value.isInitialized) return; - - final position = controller.value.position; - final duration = controller.value.duration; - - if (duration.inSeconds == 0) return; - - double progress = position.inSeconds / duration.inSeconds; - - print("Video progress: ${(progress * 100).toStringAsFixed(2)}%"); - - // Example: mark completion at 95% - if (progress >= 0.95) { - await _onVideoCompleted(lesson); - } - } - Future _onVideoCompleted(CourseLesson lesson) async { if (_isCompleted) return; @@ -89,17 +101,6 @@ class CourseLessonDetailViewModel extends BaseViewModel { await _apiService.completeLesson(lesson.id ?? 0); } - Future pause() async { - await _chewieController?.pause(); - } - - @override - void dispose() { - _videoPlayerController?.dispose(); - _chewieController?.dispose(); - super.dispose(); - } - // Navigation void pop() => _navigationService.back(); diff --git a/lib/ui/views/course_practice/course_practice_view.dart b/lib/ui/views/course_practice/course_practice_view.dart index c8635d5..16b8020 100644 --- a/lib/ui/views/course_practice/course_practice_view.dart +++ b/lib/ui/views/course_practice/course_practice_view.dart @@ -95,17 +95,17 @@ class CoursePracticeView extends StackedView { shrinkWrap: true, itemCount: viewModel.coursePractices.length, physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => _buildCard( - title: viewModel.coursePractices[index].title ?? '', - onTap: () async => - await viewModel.navigateToCoursePracticeQuestion(viewModel.coursePractices[index].id ?? 0), - ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 15, crossAxisSpacing: 15, childAspectRatio: 1.2, ), + itemBuilder: (context, index) => _buildCard( + title: viewModel.coursePractices[index].title ?? '', + onTap: () async => await viewModel.navigateToCoursePracticeQuestion( + viewModel.coursePractices[index].id ?? 0), + ), ); Widget _buildCard({ diff --git a/lib/ui/views/course_practice_question/course_practice_question_viewmodel.dart b/lib/ui/views/course_practice_question/course_practice_question_viewmodel.dart index 3238f42..67cce1b 100644 --- a/lib/ui/views/course_practice_question/course_practice_question_viewmodel.dart +++ b/lib/ui/views/course_practice_question/course_practice_question_viewmodel.dart @@ -56,13 +56,80 @@ class CoursePracticeQuestionViewModel extends FormViewModel { Map get selectedAnswers => _selectedAnswers; - // Question + // Question navigation + void previousQuestion() { + if (_currentQuestionIndex != 0) { + _currentQuestionIndex--; + _pageController.previousPage( + duration: const Duration(microseconds: 100), curve: Curves.linear); + rebuildUi(); + } + } + + // In-app navigation + void goBack() { + if (_currentPage == 0) { + pop(); + } else { + _currentPage = 0; + rebuildUi(); + } + } + + void goTo(int page) { + _currentPage = page; + rebuildUi(); + } + + void next({int? page}) async { + if (page == null) { + if (_previousPage != 0) { + _currentPage = _previousPage; + } else { + _currentPage++; + } + } else { + _previousPage = _currentPage; + _currentPage = page; + } + rebuildUi(); + } + + // Answer + void reset() { + _selectedAnswers.clear(); + rebuildUi(); + } void setAnswerFocus() { _focusAnswer = true; rebuildUi(); } + Future abort() async { + bool? response = await showAbortDialog(); + if (response != null && response) { + next(page: 1); + } + } + + Future showAbortDialog() async { + DialogResponse? response = await _dialogService.showDialog( + cancelTitle: 'No', + buttonTitle: 'Yes', + title: 'Abort Practice', + barrierDismissible: true, + cancelTitleColor: kcDarkGrey, + buttonTitleColor: kcPrimaryColor, + description: 'Are you sure to abort the practice ?', + ); + return response?.confirmed; + } + + bool isSelectedAnswer({required int question, required String answer}) { + return _selectedAnswers[question.toString()]?['option'] == answer; + } + void setSelectedAnswer({required int question, required Option? option}) { bool correct = false; if (option?.isCorrect ?? false) { @@ -84,81 +151,29 @@ class CoursePracticeQuestionViewModel extends FormViewModel { rebuildUi(); } - bool isSelectedAnswer({required int question, required String answer}) { - return _selectedAnswers[question.toString()]?['option'] == answer; - } - - // Dialog - Future showAbortDialog() async { - DialogResponse? response = await _dialogService.showDialog( - cancelTitle: 'No', - buttonTitle: 'Yes', - title: 'Abort Practice', - barrierDismissible: true, - cancelTitleColor: kcDarkGrey, - buttonTitleColor: kcPrimaryColor, - description: 'Are you sure to abort the practice ?', - ); - return response?.confirmed; - } - - Future abort() async { - bool? response = await showAbortDialog(); - if (response != null && response) { - next(page: 1); - } - } - - // Reset practice - void reset() { - _selectedAnswers.clear(); - rebuildUi(); - } - - // Question navigation - void previousQuestion() { - if (_currentQuestionIndex != 0) { - _currentQuestionIndex--; - _pageController.previousPage( - duration: const Duration(microseconds: 100), curve: Curves.linear); - rebuildUi(); - } - } - - // In-app navigation - void goTo(int page) { - _currentPage = page; - rebuildUi(); - } - - void next({int? page}) async { - if (page == null) { - if (_previousPage != 0) { - _currentPage = _previousPage; - } else { - _currentPage++; - } - } else { - _previousPage = _currentPage; - _currentPage = page; - } - rebuildUi(); - } - - void goBack() { - if (_currentPage == 0) { - pop(); - } else { - _currentPage = 0; - rebuildUi(); - } - } - // Navigation void pop() => _navigationService.back(); // Remote api call + // Question navigation + Future _nextQuestion(int id) async { + _currentQuestionIndex++; + + if (_currentQuestionIndex == _coursePracticeQuestions.length) { + next(); + } else { + if (await _statusChecker.checkConnection()) { + _currentQuestion = await _apiService.getCoursePracticeQuestion(id); + _pageController.jumpToPage(_currentQuestionIndex); + } + } + } + + Future nextQuestion(int id) async => + await runBusyFuture(_nextQuestion(id), + busyObject: StateObjects.coursePractice); + // Course practice questions Future getCoursePracticeQuestions(int id) async => await runBusyFuture(_getCoursePracticeQuestions(id), @@ -185,25 +200,4 @@ class CoursePracticeQuestionViewModel extends FormViewModel { _currentQuestion = await _apiService.getCoursePracticeQuestion(id); } } - - // Question navigation - Future nextQuestion(int id) async => - await runBusyFuture(_nextQuestion(id), - busyObject: StateObjects.coursePractice); - - Future _nextQuestion(int id)async{ - _currentQuestionIndex++; - - if (_currentQuestionIndex == _coursePracticeQuestions.length) { - next(); - } else { - if (await _statusChecker.checkConnection()) { - _currentQuestion = await _apiService.getCoursePracticeQuestion(id); - _pageController.jumpToPage(_currentQuestionIndex); - - } - } - } - - } diff --git a/lib/ui/views/course_subcategory/course_subcategory_view.dart b/lib/ui/views/course_subcategory/course_subcategory_view.dart index 63491b6..8a659f8 100644 --- a/lib/ui/views/course_subcategory/course_subcategory_view.dart +++ b/lib/ui/views/course_subcategory/course_subcategory_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; -import '../../../models/course_category.dart'; -import '../../../models/course_subcategory.dart'; +import '../../../models/category.dart'; +import '../../../models/subcategory.dart'; import '../../common/app_colors.dart'; import '../../common/enmus.dart'; import '../../common/ui_helpers.dart'; @@ -12,14 +12,14 @@ import '../../widgets/small_app_bar.dart'; import 'course_subcategory_viewmodel.dart'; class CourseSubcategoryView extends StackedView { - final CourseCategory category; + final Category category; const CourseSubcategoryView({Key? key, required this.category}) : super(key: key); @override void onViewModelReady(CourseSubcategoryViewModel viewModel) async { - await viewModel.getCourseSubcategories(category.id ?? 0); + await viewModel.getSubcategories(category.id ?? 0); super.onViewModelReady(viewModel); } @@ -121,7 +121,7 @@ class CourseSubcategoryView extends StackedView { Widget _buildTile({ GestureTapCallback? onCourseTap, GestureTapCallback? onPracticeTap, - required CourseSubcategory subcategory, + required Subcategory subcategory, }) => CourseSubcategoryTile( subcategory: subcategory, diff --git a/lib/ui/views/course_subcategory/course_subcategory_viewmodel.dart b/lib/ui/views/course_subcategory/course_subcategory_viewmodel.dart index e84127d..4d33faf 100644 --- a/lib/ui/views/course_subcategory/course_subcategory_viewmodel.dart +++ b/lib/ui/views/course_subcategory/course_subcategory_viewmodel.dart @@ -3,7 +3,7 @@ import 'package:stacked_services/stacked_services.dart'; import '../../../app/app.locator.dart'; import '../../../app/app.router.dart'; -import '../../../models/course_subcategory.dart'; +import '../../../models/subcategory.dart'; import '../../../services/api_service.dart'; import '../../../services/status_checker_service.dart'; import '../../common/enmus.dart'; @@ -16,10 +16,10 @@ class CourseSubcategoryViewModel extends BaseViewModel { final _navigationService = locator(); - // Courses - List _subcategories = []; + // Course subcategories + List _subcategories = []; - List get subcategories => _subcategories; + List get subcategories => _subcategories; // Navigation void pop() => _navigationService.back(); @@ -27,20 +27,19 @@ class CourseSubcategoryViewModel extends BaseViewModel { Future navigateToCoursePractice(int id) async => _navigationService.navigateToCoursePracticeView(id: id); - Future navigateToSubcourse(CourseSubcategory subcategory) async => + Future navigateToSubcourse(Subcategory subcategory) async => _navigationService.navigateToCourseView(subcategory: subcategory); // Remote api call - // Courses - Future getCourseSubcategories(int id) async => - await runBusyFuture(_getCourseSubcategories(id), + // Course subcategories + Future getSubcategories(int id) async => + await runBusyFuture(_getSubcategories(id), busyObject: StateObjects.subcategories); - Future _getCourseSubcategories(int id) async { + Future _getSubcategories(int id) async { if (await _statusChecker.checkConnection()) { - _subcategories = await _apiService.getCourseSubcategories(id); - + _subcategories = await _apiService.getSubcategories(id); } } } diff --git a/lib/ui/views/duolingo/duolingo_viewmodel.dart b/lib/ui/views/duolingo/duolingo_viewmodel.dart index 8e55004..d5d08b8 100644 --- a/lib/ui/views/duolingo/duolingo_viewmodel.dart +++ b/lib/ui/views/duolingo/duolingo_viewmodel.dart @@ -169,41 +169,32 @@ class DuolingoViewModel extends FormViewModel { List get listeningAssessments => _listeningAssessments; - // Topics - void setAssessmentFocus() { - _assessmentFocus = true; - rebuildUi(); - } - - // Speaking state + // Assessment void setSpeakingState() { _isSpeaking = !_isSpeaking; rebuildUi(); } - // Set assessment + void setAssessmentFocus() { + _assessmentFocus = true; + rebuildUi(); + } + void setSelectedAssessment( {required int page, required Map assessment}) { _selectedAssessment = assessment; goTo(page); } - // Listening assessment + bool isSelectedListeningAssessment(String value) => + _selectedListeningAssessment == value; void setSelectedListeningAssessment(String value) { _selectedListeningAssessment = value; rebuildUi(); } - bool isSelectedListeningAssessment(String value) => - _selectedListeningAssessment == value; - // In-app navigation - void goTo(int page) { - _currentPage = page; - rebuildUi(); - } - void goBack() { if (_currentPage == 0) { pop(); @@ -213,6 +204,11 @@ class DuolingoViewModel extends FormViewModel { } } + void goTo(int page) { + _currentPage = page; + rebuildUi(); + } + // Navigation void pop() => _navigationService.back(); } diff --git a/lib/ui/views/duolingo/screens/duolingo_retake_screen.dart b/lib/ui/views/duolingo/screens/duolingo_retake_screen.dart index 8d062a9..45c8a95 100644 --- a/lib/ui/views/duolingo/screens/duolingo_retake_screen.dart +++ b/lib/ui/views/duolingo/screens/duolingo_retake_screen.dart @@ -105,8 +105,8 @@ class DuolingoRetakeScreen extends ViewModelWidget { height: 55, safe: false, borderRadius: 12, - foregroundColor: kcWhite, text: 'Practice Again', + foregroundColor: kcWhite, onTap: () => viewModel.goTo(0), backgroundColor: kcPrimaryColor, ); diff --git a/lib/ui/views/duolingo/screens/duolingo_speaking_assessment_1_question.dart b/lib/ui/views/duolingo/screens/duolingo_speaking_assessment_1_question.dart index 0d27c9e..b56d400 100644 --- a/lib/ui/views/duolingo/screens/duolingo_speaking_assessment_1_question.dart +++ b/lib/ui/views/duolingo/screens/duolingo_speaking_assessment_1_question.dart @@ -89,14 +89,11 @@ class DuolingoSpeakingAssessment1Question [_buildTimer(), _buildCountdownTime()]; Widget _buildTimer() => const CircularProgressIndicator( - constraints: BoxConstraints( - minWidth: 50, - minHeight: 50, - ), value: 1.0, strokeWidth: 5, - padding: EdgeInsets.zero, color: kcPrimaryColor, + padding: EdgeInsets.zero, + constraints: BoxConstraints(minWidth: 50, minHeight: 50), ); Widget _buildCountdownTime() => Text( diff --git a/lib/ui/views/forget_password/forget_password_view.dart b/lib/ui/views/forget_password/forget_password_view.dart index 4032a91..ef1de92 100644 --- a/lib/ui/views/forget_password/forget_password_view.dart +++ b/lib/ui/views/forget_password/forget_password_view.dart @@ -9,9 +9,9 @@ import '../../common/validators/form_validator.dart'; import 'forget_password_viewmodel.dart'; @FormView(fields: [ - FormTextField(name: 'email', validator: FormValidator.validateEmailForm), - FormTextField(name: 'resetCode', validator: FormValidator.validateForm), FormTextField(name: 'password', validator: FormValidator.validateForm), + FormTextField(name: 'resetCode', validator: FormValidator.validateForm), + FormTextField(name: 'email', validator: FormValidator.validateEmailForm), FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm) ]) class ForgetPasswordView extends StackedView diff --git a/lib/ui/views/forget_password/forget_password_view.form.dart b/lib/ui/views/forget_password/forget_password_view.form.dart index d220300..0780f96 100644 --- a/lib/ui/views/forget_password/forget_password_view.form.dart +++ b/lib/ui/views/forget_password/forget_password_view.form.dart @@ -12,9 +12,9 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart'; const bool _autoTextFieldValidation = true; -const String EmailValueKey = 'email'; -const String ResetCodeValueKey = 'resetCode'; const String PasswordValueKey = 'password'; +const String ResetCodeValueKey = 'resetCode'; +const String EmailValueKey = 'email'; const String ConfirmPasswordValueKey = 'confirmPassword'; final Map @@ -24,25 +24,25 @@ final Map _ForgetPasswordViewFocusNodes = {}; final Map _ForgetPasswordViewTextValidations = { - EmailValueKey: FormValidator.validateEmailForm, - ResetCodeValueKey: FormValidator.validateForm, PasswordValueKey: FormValidator.validateForm, + ResetCodeValueKey: FormValidator.validateForm, + EmailValueKey: FormValidator.validateEmailForm, ConfirmPasswordValueKey: FormValidator.validateForm, }; mixin $ForgetPasswordView { - TextEditingController get emailController => - _getFormTextEditingController(EmailValueKey); - TextEditingController get resetCodeController => - _getFormTextEditingController(ResetCodeValueKey); TextEditingController get passwordController => _getFormTextEditingController(PasswordValueKey); + TextEditingController get resetCodeController => + _getFormTextEditingController(ResetCodeValueKey); + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); TextEditingController get confirmPasswordController => _getFormTextEditingController(ConfirmPasswordValueKey); - FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); - FocusNode get resetCodeFocusNode => _getFormFocusNode(ResetCodeValueKey); FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey); + FocusNode get resetCodeFocusNode => _getFormFocusNode(ResetCodeValueKey); + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); FocusNode get confirmPasswordFocusNode => _getFormFocusNode(ConfirmPasswordValueKey); @@ -70,9 +70,9 @@ mixin $ForgetPasswordView { /// Registers a listener on every generated controller that calls [model.setData()] /// with the latest textController values void syncFormWithViewModel(FormStateHelper model) { - emailController.addListener(() => _updateFormData(model)); - resetCodeController.addListener(() => _updateFormData(model)); passwordController.addListener(() => _updateFormData(model)); + resetCodeController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); confirmPasswordController.addListener(() => _updateFormData(model)); _updateFormData(model, forceValidate: _autoTextFieldValidation); @@ -85,9 +85,9 @@ mixin $ForgetPasswordView { 'This feature was deprecated after 3.1.0.', ) void listenToFormUpdated(FormViewModel model) { - emailController.addListener(() => _updateFormData(model)); - resetCodeController.addListener(() => _updateFormData(model)); passwordController.addListener(() => _updateFormData(model)); + resetCodeController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); confirmPasswordController.addListener(() => _updateFormData(model)); _updateFormData(model, forceValidate: _autoTextFieldValidation); @@ -98,9 +98,9 @@ mixin $ForgetPasswordView { model.setData( model.formValueMap ..addAll({ - EmailValueKey: emailController.text, - ResetCodeValueKey: resetCodeController.text, PasswordValueKey: passwordController.text, + ResetCodeValueKey: resetCodeController.text, + EmailValueKey: emailController.text, ConfirmPasswordValueKey: confirmPasswordController.text, }), ); @@ -143,19 +143,20 @@ extension ValueProperties on FormStateHelper { return !hasAnyValidationMessage; } - String? get emailValue => this.formValueMap[EmailValueKey] as String?; - String? get resetCodeValue => this.formValueMap[ResetCodeValueKey] as String?; String? get passwordValue => this.formValueMap[PasswordValueKey] as String?; + String? get resetCodeValue => this.formValueMap[ResetCodeValueKey] as String?; + String? get emailValue => this.formValueMap[EmailValueKey] as String?; String? get confirmPasswordValue => this.formValueMap[ConfirmPasswordValueKey] as String?; - set emailValue(String? value) { + set passwordValue(String? value) { this.setData( - this.formValueMap..addAll({EmailValueKey: value}), + this.formValueMap..addAll({PasswordValueKey: value}), ); - if (_ForgetPasswordViewTextEditingControllers.containsKey(EmailValueKey)) { - _ForgetPasswordViewTextEditingControllers[EmailValueKey]?.text = + if (_ForgetPasswordViewTextEditingControllers.containsKey( + PasswordValueKey)) { + _ForgetPasswordViewTextEditingControllers[PasswordValueKey]?.text = value ?? ''; } } @@ -172,14 +173,13 @@ extension ValueProperties on FormStateHelper { } } - set passwordValue(String? value) { + set emailValue(String? value) { this.setData( - this.formValueMap..addAll({PasswordValueKey: value}), + this.formValueMap..addAll({EmailValueKey: value}), ); - if (_ForgetPasswordViewTextEditingControllers.containsKey( - PasswordValueKey)) { - _ForgetPasswordViewTextEditingControllers[PasswordValueKey]?.text = + if (_ForgetPasswordViewTextEditingControllers.containsKey(EmailValueKey)) { + _ForgetPasswordViewTextEditingControllers[EmailValueKey]?.text = value ?? ''; } } @@ -196,64 +196,64 @@ extension ValueProperties on FormStateHelper { } } - bool get hasEmail => - this.formValueMap.containsKey(EmailValueKey) && - (emailValue?.isNotEmpty ?? false); - bool get hasResetCode => - this.formValueMap.containsKey(ResetCodeValueKey) && - (resetCodeValue?.isNotEmpty ?? false); bool get hasPassword => this.formValueMap.containsKey(PasswordValueKey) && (passwordValue?.isNotEmpty ?? false); + bool get hasResetCode => + this.formValueMap.containsKey(ResetCodeValueKey) && + (resetCodeValue?.isNotEmpty ?? false); + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); bool get hasConfirmPassword => this.formValueMap.containsKey(ConfirmPasswordValueKey) && (confirmPasswordValue?.isNotEmpty ?? false); - bool get hasEmailValidationMessage => - this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; - bool get hasResetCodeValidationMessage => - this.fieldsValidationMessages[ResetCodeValueKey]?.isNotEmpty ?? false; bool get hasPasswordValidationMessage => this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false; + bool get hasResetCodeValidationMessage => + this.fieldsValidationMessages[ResetCodeValueKey]?.isNotEmpty ?? false; + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; bool get hasConfirmPasswordValidationMessage => this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ?? false; - String? get emailValidationMessage => - this.fieldsValidationMessages[EmailValueKey]; - String? get resetCodeValidationMessage => - this.fieldsValidationMessages[ResetCodeValueKey]; String? get passwordValidationMessage => this.fieldsValidationMessages[PasswordValueKey]; + String? get resetCodeValidationMessage => + this.fieldsValidationMessages[ResetCodeValueKey]; + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; String? get confirmPasswordValidationMessage => this.fieldsValidationMessages[ConfirmPasswordValueKey]; } extension Methods on FormStateHelper { - setEmailValidationMessage(String? validationMessage) => - this.fieldsValidationMessages[EmailValueKey] = validationMessage; - setResetCodeValidationMessage(String? validationMessage) => - this.fieldsValidationMessages[ResetCodeValueKey] = validationMessage; setPasswordValidationMessage(String? validationMessage) => this.fieldsValidationMessages[PasswordValueKey] = validationMessage; + setResetCodeValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[ResetCodeValueKey] = validationMessage; + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; setConfirmPasswordValidationMessage(String? validationMessage) => this.fieldsValidationMessages[ConfirmPasswordValueKey] = validationMessage; /// Clears text input fields on the Form void clearForm() { - emailValue = ''; - resetCodeValue = ''; passwordValue = ''; + resetCodeValue = ''; + emailValue = ''; confirmPasswordValue = ''; } /// Validates text input fields on the Form void validateForm() { this.setValidationMessages({ - EmailValueKey: getValidationMessage(EmailValueKey), - ResetCodeValueKey: getValidationMessage(ResetCodeValueKey), PasswordValueKey: getValidationMessage(PasswordValueKey), + ResetCodeValueKey: getValidationMessage(ResetCodeValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey), }); } @@ -274,8 +274,8 @@ String? getValidationMessage(String key) { /// Updates the fieldsValidationMessages on the FormViewModel void updateValidationData(FormStateHelper model) => model.setValidationMessages({ - EmailValueKey: getValidationMessage(EmailValueKey), - ResetCodeValueKey: getValidationMessage(ResetCodeValueKey), PasswordValueKey: getValidationMessage(PasswordValueKey), + ResetCodeValueKey: getValidationMessage(ResetCodeValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey), }); diff --git a/lib/ui/views/forget_password/forget_password_viewmodel.dart b/lib/ui/views/forget_password/forget_password_viewmodel.dart index f836a77..4e2fef3 100644 --- a/lib/ui/views/forget_password/forget_password_viewmodel.dart +++ b/lib/ui/views/forget_password/forget_password_viewmodel.dart @@ -20,7 +20,7 @@ class ForgetPasswordViewModel extends FormViewModel { Map get userData => _userData; - // Navigation + // In-app navigation int _currentPage = 0; int get currentPage => _currentPage; @@ -69,15 +69,6 @@ class ForgetPasswordViewModel extends FormViewModel { bool get obscureConfirmPassword => _obscureConfirmPassword; - // Add user data - void addUserData(Map data) { - _userData.addAll(data); - } - - void clearUserData() { - _userData.clear(); - } - // Email void setEmailFocus() { _focusEmail = true; @@ -90,12 +81,37 @@ class ForgetPasswordViewModel extends FormViewModel { rebuildUi(); } + // User data + void clearUserData() { + _userData.clear(); + } + + void addUserData(Map data) { + _userData.addAll(data); + } + // Password void setPasswordFocus() { _focusPassword = true; rebuildUi(); } + void setObscurePassword() { + _obscurePassword = !_obscurePassword; + rebuildUi(); + } + + double validationProgress() { + int completed = 0; + + if (_length) completed++; + if (_number) completed++; + if (_specialChar) completed++; + if (_passwordMatch) completed++; + + return completed / 4; // returns 0.0 → 1.0 + } + void validatePassword( {required String password, required String confirmPassword}) { if (password.length > 8) { @@ -124,22 +140,6 @@ class ForgetPasswordViewModel extends FormViewModel { rebuildUi(); } - double validationProgress() { - int completed = 0; - - if (_length) completed++; - if (_number) completed++; - if (_specialChar) completed++; - if (_passwordMatch) completed++; - - return completed / 4; // returns 0.0 → 1.0 - } - - void setObscurePassword() { - _obscurePassword = !_obscurePassword; - rebuildUi(); - } - // Confirm password void setConfirmPasswordFocus() { _focusConfirmPassword = true; @@ -172,11 +172,6 @@ class ForgetPasswordViewModel extends FormViewModel { } // In-app navigation - void goTo(int page) { - _currentPage = page; - rebuildUi(); - } - void goBack() { if (_currentPage == 1) { _currentPage = 0; @@ -186,6 +181,11 @@ class ForgetPasswordViewModel extends FormViewModel { } } + void goTo(int page) { + _currentPage = page; + rebuildUi(); + } + // Navigation void pop() => _navigationService.back(); @@ -194,6 +194,23 @@ class ForgetPasswordViewModel extends FormViewModel { // Remote api calls + // Request reset code + Future resetPassword() async => await runBusyFuture(_resetPassword(), + busyObject: StateObjects.resetPassword); + + Future _resetPassword() async { + if (await _statusChecker.checkConnection()) { + Map response = + await _apiService.resetPassword(_userData); + if (response['status'] == ResponseStatus.success) { + showSuccessToast(response['message']); + await replaceWithLogin(); + } else { + showErrorToast(response['message']); + } + } + } + // Request reset code Future requestResetCode() async => await runBusyFuture(_requestResetCode(), @@ -211,21 +228,4 @@ class ForgetPasswordViewModel extends FormViewModel { } } } - - // Request reset code - Future resetPassword() async => await runBusyFuture(_resetPassword(), - busyObject: StateObjects.resetPassword); - - Future _resetPassword() async { - if (await _statusChecker.checkConnection()) { - Map response = - await _apiService.resetPassword(_userData); - if (response['status'] == ResponseStatus.success) { - showSuccessToast(response['message']); - await replaceWithLogin(); - } else { - showErrorToast(response['message']); - } - } - } } diff --git a/lib/ui/views/forget_password/screens/request_reset_code_screen.dart b/lib/ui/views/forget_password/screens/request_reset_code_screen.dart index dde7213..d540362 100644 --- a/lib/ui/views/forget_password/screens/request_reset_code_screen.dart +++ b/lib/ui/views/forget_password/screens/request_reset_code_screen.dart @@ -20,9 +20,7 @@ class RequestCodeScreen extends ViewModelWidget { Widget getPadding(context) { double half = screenHeight(context) / 2; - return SizedBox( - height: half + 375 - half, - ); + return SizedBox(height: half + 375 - half); } void _inAppPop(ForgetPasswordViewModel viewModel) { diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index df1527b..3235d78 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -3,7 +3,7 @@ import 'package:stacked/stacked.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/views/course_category/course_category_view.dart'; -import 'package:yimaru_app/ui/views/learn/learn_view.dart'; +import 'package:yimaru_app/ui/views/learn_subcategory/learn_subcategory_view.dart'; import 'package:yimaru_app/ui/views/profile/profile_view.dart'; import 'package:yimaru_app/ui/views/startup/startup_view.dart'; @@ -18,6 +18,7 @@ class HomeView extends StackedView { @override void onViewModelReady(HomeViewModel viewModel) async { // Removable + print('HERE'); await _init(viewModel); super.onViewModelReady(viewModel); } @@ -82,7 +83,7 @@ Widget _buildProfileIcon() => const Icon(Icons.person); Widget getViewForIndex(int index) { switch (index) { case 0: - return const LearnView(); + return const LearnSubcategoryView(); case 1: return const CourseCategoryView(); diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index 7a61fa3..29f04ed 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -1,8 +1,7 @@ import 'package:yimaru_app/app/app.bottomsheets.dart'; -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:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/models/user.dart'; import 'package:yimaru_app/services/status_checker_service.dart'; import 'package:yimaru_app/ui/common/app_strings.dart'; import 'package:stacked/stacked.dart'; @@ -16,7 +15,6 @@ import '../../common/ui_helpers.dart'; class HomeViewModel extends ReactiveViewModel { final _apiService = locator(); - final _dialogService = locator(); final _statusChecker = locator(); final _navigationService = locator(); final _bottomSheetService = locator(); @@ -28,9 +26,9 @@ class HomeViewModel extends ReactiveViewModel { [_authenticationService]; // Current user - UserModel? get _user => _authenticationService.user; + User? get _user => _authenticationService.user; - UserModel? get user => _user; + User? get user => _user; // Bottom navigation int _currentPage = 0; @@ -38,19 +36,6 @@ class HomeViewModel extends ReactiveViewModel { int get currentPage => _currentPage; // Bottom navigation - void setCurrentIndex(int index) { - _currentPage = index; - rebuildUi(); - } - - void showDialog() { - _dialogService.showCustomDialog( - title: 'Stacked Rocks!', - variant: DialogType.infoAlert, - description: 'Give stacked stars on Github', - ); - } - void showBottomSheet() { _bottomSheetService.showCustomSheet( title: ksHomeBottomSheetTitle, @@ -59,6 +44,12 @@ class HomeViewModel extends ReactiveViewModel { ); } + void setCurrentIndex(int index) { + _currentPage = index; + rebuildUi(); + } + + // Save profile status Future saveProfileStatus(bool value) async => await _authenticationService.saveProfileStatus(value); @@ -80,7 +71,34 @@ class HomeViewModel extends ReactiveViewModel { await _getProfileData(); } - // Profile status + // Get profile data + Future _getProfileData() async { + if (!(_user?.userInfoLoaded ?? false)) { + Map response = {}; + + if (_user?.profileCompleted != null && + (_user?.profileCompleted ?? false)) { + if (await _statusChecker.checkConnection()) { + response = await _apiService.getProfileData(_user?.userId); + + if (response['status'] == ResponseStatus.success) { + User user = response['data'] as User; + + await _authenticationService.saveUserData(user); + + String image = + await _imageDownloaderService.downloader(user.profilePicture); + + await _authenticationService.saveProfilePicture(image); + } + } else { + await replaceWithFailure(); + } + } + } + } + + // Get profile status Future _getProfileStatus() async { Map response = {}; if (_user?.profileCompleted == null) { @@ -102,32 +120,4 @@ class HomeViewModel extends ReactiveViewModel { await saveProfileStatus(response['data']); } } - - // Profile data - - Future _getProfileData() async { - if (!(_user?.userInfoLoaded ?? false)) { - Map response = {}; - - if (_user?.profileCompleted != null && - (_user?.profileCompleted ?? false)) { - if (await _statusChecker.checkConnection()) { - response = await _apiService.getProfileData(_user?.userId); - - if (response['status'] == ResponseStatus.success) { - UserModel user = response['data'] as UserModel; - - await _authenticationService.saveUserData(user); - - String image = - await _imageDownloaderService.downloader(user.profilePicture); - - await _authenticationService.saveProfilePicture(image); - } - } else { - await replaceWithFailure(); - } - } - } - } } diff --git a/lib/ui/views/language/language_viewmodel.dart b/lib/ui/views/language/language_viewmodel.dart index 74ff8ad..7fa0701 100644 --- a/lib/ui/views/language/language_viewmodel.dart +++ b/lib/ui/views/language/language_viewmodel.dart @@ -7,7 +7,6 @@ class LanguageViewModel extends BaseViewModel { final _navigationService = locator(); // Languages - Map _selectedLanguage = { 'code': 'EN', 'language': 'English' @@ -23,13 +22,14 @@ class LanguageViewModel extends BaseViewModel { List> get languages => _languages; // Languages + bool isSelectedLanguage(String title) => + _selectedLanguage['language'] == title; + void setSelectedLanguage(Map title) { _selectedLanguage = title; rebuildUi(); } - bool isSelectedLanguage(String title) => - _selectedLanguage['language'] == title; // Navigation void pop() => _navigationService.back(); } diff --git a/lib/ui/views/learn/learn_view.dart b/lib/ui/views/learn/learn_view.dart index d2d7bc4..6651270 100644 --- a/lib/ui/views/learn/learn_view.dart +++ b/lib/ui/views/learn/learn_view.dart @@ -1,15 +1,25 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; -import 'package:yimaru_app/ui/widgets/profile_app_bar.dart'; -import 'package:yimaru_app/ui/widgets/learn_level_tile.dart'; +import 'package:yimaru_app/ui/widgets/learn_tile.dart'; +import '../../../models/course.dart'; import '../../common/app_colors.dart'; import '../../common/enmus.dart'; import '../../common/ui_helpers.dart'; +import '../../widgets/custom_circular_progress_indicator.dart'; +import '../../widgets/small_app_bar.dart'; import 'learn_viewmodel.dart'; class LearnView extends StackedView { - const LearnView({Key? key}) : super(key: key); + final int id; + + const LearnView({Key? key, required this.id}) : super(key: key); + + @override + void onViewModelReady(LearnViewModel viewModel) async { + await viewModel.getCourses(id); + super.onViewModelReady(viewModel); + } @override LearnViewModel viewModelBuilder(BuildContext context) => LearnViewModel(); @@ -40,49 +50,50 @@ class LearnView extends StackedView { verticalSpaceMedium, _buildAppBar(viewModel), verticalSpaceMedium, - _buildLevelsColumnWrapper(viewModel) + _buildLearnColumnWrapper(viewModel) ], ); - Widget _buildAppBar(LearnViewModel viewModel) => ProfileAppBar( - name: viewModel.user?.firstName, - profileImage: viewModel.user?.profilePicture, + Widget _buildAppBar(LearnViewModel viewModel) => SmallAppBar( + onTap: viewModel.pop, + showBackButton: true, ); - Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) => - Expanded(child: _buildLevelsColumnScrollView(viewModel)); + Widget _buildLearnColumnWrapper(LearnViewModel viewModel) => + Expanded(child: _buildLearnColumnScrollView(viewModel)); - Widget _buildLevelsColumnScrollView(LearnViewModel viewModel) => + Widget _buildLearnColumnScrollView(LearnViewModel viewModel) => SingleChildScrollView( - child: _buildLevelsColumn(viewModel), + child: _buildListViewBuilder(viewModel), ); - Widget _buildLevelsColumn(LearnViewModel viewModel) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildLevelsColumnChildren(viewModel), + Widget _buildListViewBuilder(LearnViewModel viewModel) => + viewModel.busy(StateObjects.learnCourses) + ? _buildProgressIndicator() + : _buildListView(viewModel); + + Widget _buildProgressIndicator() => const Center( + child: CustomCircularProgressIndicator(color: kcPrimaryColor), ); - List _buildLevelsColumnChildren(LearnViewModel viewModel) => - [verticalSpaceLarge, _buildListView(viewModel)]; - - Widget _buildListView(LearnViewModel viewModel) => ListView.builder( + Widget _buildListView(LearnViewModel viewModel) => ListView.separated( shrinkWrap: true, - itemCount: viewModel.learnLevels.length, + itemCount: viewModel.courses.length, physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => verticalSpaceSmall, itemBuilder: (context, index) => _buildTile( - title: viewModel.learnLevels[index]['title'], - status: viewModel.learnLevels[index]['status'], - subtitle: viewModel.learnLevels[index]['subtitle']), + course: viewModel.courses[index], + onTap: () async => await viewModel + .navigateToLearnLevel(viewModel.courses[index].id ?? 0), + ), ); - Widget _buildTile( - {required String title, - required String subtitle, - required ProgressStatuses status}) => - LearnLevelTile( - title: title, - status: status, - subtitle: subtitle, + Widget _buildTile({ + required Course course, + required GestureTapCallback onTap, + }) => + LearnTile( + onTap: onTap, + course: course, ); } diff --git a/lib/ui/views/learn/learn_viewmodel.dart b/lib/ui/views/learn/learn_viewmodel.dart index b8fba5e..0b890bd 100644 --- a/lib/ui/views/learn/learn_viewmodel.dart +++ b/lib/ui/views/learn/learn_viewmodel.dart @@ -1,45 +1,43 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.router.dart'; -import 'package:yimaru_app/models/user_model.dart'; -import 'package:yimaru_app/services/authentication_service.dart'; +import 'package:yimaru_app/models/course.dart'; import 'package:yimaru_app/ui/common/enmus.dart'; import '../../../app/app.locator.dart'; +import '../../../services/api_service.dart'; +import '../../../services/status_checker_service.dart'; + +class LearnViewModel extends BaseViewModel { + // Dependency injection + final _apiService = locator(); + + final _statusChecker = locator(); -class LearnViewModel extends ReactiveViewModel { final _navigationService = locator(); - final _authenticationService = locator(); - @override - List get listenableServices => - [_authenticationService]; + // Learn courses + List _courses = []; - // Current user - UserModel? get _user => _authenticationService.user; + List get courses => _courses; - UserModel? get user => _user; + // Navigation + void pop() => _navigationService.back(); - final List> _learnLevels = [ - { - 'title': 'Beginner', - 'status': ProgressStatuses.started, - 'subtitle': 'Start your journey with the basics of English.', - }, - // { - // 'title': 'Intermediate', - // 'status': ProgressStatuses.started, - // 'subtitle': 'Practice real conversations and expand vocabulary.', - // }, - // { - // 'title': 'Advanced', - // 'status': ProgressStatuses.pending, - // 'subtitle': 'Achieve fluency and master complex topics.', - // }, - ]; + Future navigateToLearnLevel(int id) async => + _navigationService.navigateToLearnLevelView(id: id); - List> get learnLevels => _learnLevels; + // Remote api call - Future navigateToLearnLevel() async => - _navigationService.navigateToLearnLevelView(); + // Learn courses + Future getCourses(int id) async => await runBusyFuture(_getCourses(id), + busyObject: StateObjects.learnCourses); + + Future _getCourses(int id) async { + if (_courses.isEmpty) { + if (await _statusChecker.checkConnection()) { + _courses = await _apiService.getCourses(id); + } + } + } } diff --git a/lib/ui/views/learn_lesson_detail/learn_lesson_detail_viewmodel.dart b/lib/ui/views/learn_lesson_detail/learn_lesson_detail_viewmodel.dart index a30e1d6..f7d6f1e 100644 --- a/lib/ui/views/learn_lesson_detail/learn_lesson_detail_viewmodel.dart +++ b/lib/ui/views/learn_lesson_detail/learn_lesson_detail_viewmodel.dart @@ -25,6 +25,15 @@ class LearnLessonDetailViewModel extends BaseViewModel { VideoPlayerController? get videoPlayerController => _videoPlayerController; // Video player + void close() { + _videoPlayerController?.dispose(); + _chewieController?.dispose(); + } + + Future pause() async { + await _chewieController?.pause(); + } + Future initializePlayer() async => await runBusyFuture(_initializePlayer(), busyObject: StateObjects.loadLessonVideo); @@ -51,15 +60,6 @@ class LearnLessonDetailViewModel extends BaseViewModel { // rebuildUi(); } - Future pause() async { - await _chewieController?.pause(); - } - - void close() { - _videoPlayerController?.dispose(); - _chewieController?.dispose(); - } - // Navigation void pop() => _navigationService.back(); diff --git a/lib/ui/views/learn_level/learn_level_view.dart b/lib/ui/views/learn_level/learn_level_view.dart index a2a9032..4dc8c7c 100644 --- a/lib/ui/views/learn_level/learn_level_view.dart +++ b/lib/ui/views/learn_level/learn_level_view.dart @@ -1,14 +1,24 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; -import 'package:yimaru_app/ui/widgets/learn_sub_level_tile.dart'; +import 'package:yimaru_app/ui/widgets/learn_level_tile.dart'; import 'package:yimaru_app/ui/widgets/small_app_bar.dart'; +import '../../../models/level.dart'; import '../../common/app_colors.dart'; +import '../../common/enmus.dart'; import '../../common/ui_helpers.dart'; +import '../../widgets/custom_circular_progress_indicator.dart'; import 'learn_level_viewmodel.dart'; class LearnLevelView extends StackedView { - const LearnLevelView({Key? key}) : super(key: key); + final int id; + const LearnLevelView({Key? key, required this.id}) : super(key: key); + + @override + void onViewModelReady(LearnLevelViewModel viewModel) async { + await viewModel.getLevels(id); + super.onViewModelReady(viewModel); + } @override LearnLevelViewModel viewModelBuilder(BuildContext context) => @@ -54,36 +64,35 @@ class LearnLevelView extends StackedView { Widget _buildLevelsColumnScrollView(LearnLevelViewModel viewModel) => SingleChildScrollView( - child: _buildLevelsColumn(viewModel), + child: _buildListViewBuilder(viewModel), ); - Widget _buildLevelsColumn(LearnLevelViewModel viewModel) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildLevelsColumnChildren(viewModel), + Widget _buildListViewBuilder(LearnLevelViewModel viewModel) => + viewModel.busy(StateObjects.learnLevels) + ? _buildProgressIndicator() + : _buildListView(viewModel); + + Widget _buildProgressIndicator() => const Center( + child: CustomCircularProgressIndicator(color: kcPrimaryColor), ); - List _buildLevelsColumnChildren(LearnLevelViewModel viewModel) => - [verticalSpaceLarge, _buildListView(viewModel)]; - - Widget _buildListView(LearnLevelViewModel viewModel) => ListView.builder( + Widget _buildListView(LearnLevelViewModel viewModel) => ListView.separated( shrinkWrap: true, - itemCount: viewModel.learnSubLevels.length, + itemCount: viewModel.levels.length, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => _buildTile( - title: viewModel.learnSubLevels[index]['title'], - current: viewModel.learnSubLevels[index]['current'], - subtitle: viewModel.learnSubLevels[index]['subtitle']), + level: viewModel.levels[index], + onTap: () async => await viewModel.navigateToModule( viewModel.levels[index]), + ), + separatorBuilder: (context, index) => verticalSpaceSmall, ); Widget _buildTile({ - required String title, - required bool current, - required String subtitle, + required Level level, + required GestureTapCallback onTap, }) => - LearnSubLevelTile( - title: title, - current: current, - subtitle: subtitle, + LearnLevelTile( + onTap: onTap, + level: level, ); } diff --git a/lib/ui/views/learn_level/learn_level_viewmodel.dart b/lib/ui/views/learn_level/learn_level_viewmodel.dart index 5666511..cbda49d 100644 --- a/lib/ui/views/learn_level/learn_level_viewmodel.dart +++ b/lib/ui/views/learn_level/learn_level_viewmodel.dart @@ -3,28 +3,43 @@ import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.router.dart'; import '../../../app/app.locator.dart'; +import '../../../models/level.dart'; +import '../../../services/api_service.dart'; +import '../../../services/status_checker_service.dart'; +import '../../common/enmus.dart'; class LearnLevelViewModel extends BaseViewModel { + // Dependency injection + final _apiService = locator(); + + final _statusChecker = locator(); + final _navigationService = locator(); - final List> _learnSubLevels = [ - { - 'title': 'A1', - 'current': true, - 'subtitle': 'Start your journey with the basics of English.', - }, - // { - // 'title': 'A2', - // 'current': false, - // 'subtitle': 'Build upon your foundational knowledge.', - // }, - ]; + // Learn levels + List _levels = []; - List> get learnSubLevels => _learnSubLevels; + List get levels => _levels; // Navigation void pop() => _navigationService.back(); - Future navigateToLearnModule() async => - _navigationService.navigateToLearnModuleView(); + Future navigateToModule(Level level) async => + _navigationService.navigateToLearnModuleView(level: level); + + // Remote api call + + // Learn levels + Future getLevels(int id) async => + await runBusyFuture(_getLevels(id), busyObject: StateObjects.learnLevels); + + Future _getLevels(int id) async { + if (_levels.isEmpty) { + if (await _statusChecker.checkConnection()) { + _levels = await _apiService.getLevels(id); + _levels.sort( + (a, b) => (a.displayOrder ?? 0).compareTo(b.displayOrder ?? 0)); + } + } + } } diff --git a/lib/ui/views/learn_module/learn_module_view.dart b/lib/ui/views/learn_module/learn_module_view.dart index ba8388a..8e8699e 100644 --- a/lib/ui/views/learn_module/learn_module_view.dart +++ b/lib/ui/views/learn_module/learn_module_view.dart @@ -1,16 +1,26 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/models/level.dart'; import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/widgets/learn_module_tile.dart'; import 'package:yimaru_app/ui/widgets/overall_learn_progress.dart'; +import '../../../models/module.dart'; import '../../common/app_colors.dart'; import '../../common/ui_helpers.dart'; import '../../widgets/small_app_bar.dart'; import 'learn_module_viewmodel.dart'; class LearnModuleView extends StackedView { - const LearnModuleView({Key? key}) : super(key: key); + final Level level; + + const LearnModuleView({Key? key, required this.level}) : super(key: key); + + @override + void onViewModelReady(LearnModuleViewModel viewModel) async { + await viewModel.getModules(level.id ?? 0); + super.onViewModelReady(viewModel); + } @override LearnModuleViewModel viewModelBuilder(BuildContext context) => @@ -42,7 +52,7 @@ class LearnModuleView extends StackedView { verticalSpaceMedium, _buildAppBar(viewModel), verticalSpaceMedium, - _buildLevelsColumnWrapper(viewModel), + _buildModulesColumnWrapper(viewModel), ], ); @@ -51,7 +61,7 @@ class LearnModuleView extends StackedView { showBackButton: true, ); - Widget _buildLevelsColumnWrapper(LearnModuleViewModel viewModel) => + Widget _buildModulesColumnWrapper(LearnModuleViewModel viewModel) => Expanded(child: _buildLevelsColumnScrollView(viewModel)); Widget _buildLevelsColumnScrollView(LearnModuleViewModel viewModel) => @@ -61,7 +71,7 @@ class LearnModuleView extends StackedView { Widget _buildLevelsColumn(LearnModuleViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: _buildLevelsColumnChildren(viewModel), ); @@ -76,7 +86,7 @@ class LearnModuleView extends StackedView { ]; Widget _buildTitle() => Text( - 'A1 - Beginner', + level.title ?? '', style: style18P600, ); @@ -92,27 +102,19 @@ class LearnModuleView extends StackedView { itemCount: viewModel.modules.length, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => _buildTile( - title: viewModel.modules[index]['title'], - status: viewModel.modules[index]['status'], - topics: viewModel.modules[index]['topics'], - subtitle: viewModel.modules[index]['subtitle'], - practices: viewModel.modules[index]['practices'], - description: viewModel.modules[index]['description']), + module: viewModel.modules[index], + onLessonTap: () {}, + onPracticeTap: () {}), ); - Widget _buildTile( - {required String title, - required String topics, - required String subtitle, - required String description, - required ProgressStatuses status, - required List> practices}) => + Widget _buildTile({ + required Module module, + required GestureTapCallback onLessonTap, + required GestureTapCallback onPracticeTap, + }) => LearnModuleTile( - title: title, - status: status, - topics: topics, - subtitle: subtitle, - practices: practices, - description: description, + module: module, + onLessonTap: onLessonTap, + onPracticeTap: onPracticeTap, ); } diff --git a/lib/ui/views/learn_module/learn_module_viewmodel.dart b/lib/ui/views/learn_module/learn_module_viewmodel.dart index 0b9bc30..60fe809 100644 --- a/lib/ui/views/learn_module/learn_module_viewmodel.dart +++ b/lib/ui/views/learn_module/learn_module_viewmodel.dart @@ -1,351 +1,25 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/models/module.dart'; import '../../../app/app.locator.dart'; +import '../../../services/api_service.dart'; +import '../../../services/status_checker_service.dart'; import '../../common/enmus.dart'; class LearnModuleViewModel extends BaseViewModel { + // Dependency injection + final _apiService = locator(); + + final _statusChecker = locator(); + final _navigationService = locator(); - // Modules - final List> _modules = [ - { - 'status': ProgressStatuses.started, - 'title': 'Lesson 1.1', - 'subtitle': 'Start Speaking English Today! Greetings & Introductions', - 'topics': - """👉 How to use "Good Morning," "Afternoon," and "Evening" at the right time. - 👉 Why saying "I'm" is often better than "I am" in spoken English. - 👉 Master "My," "Your," and the verb "To Be" without the headache. - 👉 How to perfectly say the "Long I" sound in "Hi" and "My." - """, - 'practices': [ - { - 'question_text': 'Good morning! How are you?', - 'question_audio_url': - 'https://drive.google.com/file/d/1El9hhmZvLnrTYtVreHR0EDXNrZyGphT9/view?usp=sharing', - 'sample_answer': - 'https://drive.google.com/file/d/1dUSahuj_VdunV293gEZr0XL9d4WV7_8G/view?usp=sharing' - }, - { - 'question_text': 'What\'s your name?', - 'sample_answer': - 'https://drive.google.com/file/d/14oAqcMRltXeQhQ-RTGizO2DZ4CkHmKdu/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1iAsOIXD4NcsctKuubnvWlnwXodTMlou5/view?usp=drive_link' - }, - { - 'question_text': 'Nice to meet you!', - 'question_audio_url': - 'https://drive.google.com/file/d/10bOaNCcpNFzxpJ4d2-5KYahMCCcWt668/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1h8V6lFuOiOf0zRRN-NvYjtLrmMdx0SeX/view?usp=drive_link' - }, - { - 'question_text': - 'You are doing great! Tell me, are you happy to be here?', - 'sample_answer': - 'https://drive.google.com/file/d/1sjNFofDRr9KB8fQptdM12LDJ5DIaHujs/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1kcNIL5NCt_lfTzAbmCkM3yojD_UzjSx7/view?usp=drive_link', - }, - ], - 'description': - """Stop feeling nervous when you meet someone new! In this lesson, we break down the most common English greetings and show you exactly how to introduce yourself with confidence. - Whether you are at the market, in a classroom, or meeting someone online, the first 10 seconds of a conversation are the most important. Our teacher will guide you through the exact phrases you need to sound natural and friendly from the very first moment. - """, - }, - { - 'status': ProgressStatuses.started, - 'title': 'Lesson 1.2', - 'subtitle': 'Talk About Your Home! "Where Are You From?" & Locations', - 'topics': - """👉 The difference between "Where are you from?" and "Where do you live?" - 👉 How to use "Am," "Is," and "Are" to talk about your home. - 👉 Learn to say "Where-are-you" as one smooth sound (word linking). - 👉 Practice a full conversation with Miss Alem. - 👉 A fast-paced game to test your speed and memory. - """, - 'practices': [ - { - 'question_text': - 'Hey, It is a pleasure to meet you. Where are you from?', - 'sample_answer': - 'https://drive.google.com/file/d/1F98PdPqeDhkF5KMzFjgmUoy4HanIDHVs/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1vipFTk-DYgOCYhyDyWVaORec7g0JeidD/view?usp=drive_link', - }, - { - 'question_text': 'Ethiopia is beautiful! What city are you from?', - 'sample_answer': - 'https://drive.google.com/file/d/1nZJLV9lOgFGqYr-W3vlpt8rbay_bwARJ/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1T8JIYQ6T9Mq_7TazGr85ag4PCyQgHhzi/view?usp=drive_link' - }, - { - 'question_text': 'I see. And where does your family live now?', - 'question_audio_url': - 'https://drive.google.com/file/d/19XAbHL3HqTpPcolvOQUVPSGef-Ythusu/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1O8o11DNKYZBcm1-f8pjMFJjNIvGmawvg/view?usp=drive_link' - }, - ], - 'description': - """Are you ready to talk about your city, town, or country with pride? In Lesson 1.2, we move past basic greetings and learn how to share where you come from and where you live now. - Whether you are meeting a tourist, a new classmate, or a colleague, being able to say "I’m from Ethiopia" or "I live in Addis Ababa" is the best way to build a connection. Miss Alem will show you how to link your words together so you sound like a natural English speaker! - """, - }, - { - 'title': 'Lesson 1.3', - 'status': ProgressStatuses.started, - 'subtitle': 'Talk About Your Family! Master "Have" vs. "Has"', - 'topics': - """👉 Master the names for parents, grandparents, and the secret word for brothers and sisters (Siblings). - 👉 Never mix up "I have" and "She has" ever again. - 👉 Why the letter "S" is the most important sound when talking about your brothers. - 👉 A close-up look at how to pronounce "Mother" and "Father" like a native speaker. - 👉 Test your eyes and ears by finding the hidden mistakes in our family game. - """, - 'practices': [ - { - 'question_text': - 'Hello! I want to know about your family. Do you have a brother or sister?', - 'sample_answer': - 'https://drive.google.com/file/d/1fred7Y5ocdD4codZuK7Pr349O38UkB0Z/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1tMoTA4D6joz0bkWS5Nfn-neUvbej6XDI/view?usp=drive_link', - }, - { - 'question_text': 'That is nice! How many people are in your family?', - 'sample_answer': - 'https://drive.google.com/file/d/1dhYhiycWwdtW0ndJUMdMXxxVyrXLXPCM/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1FwRxWvrxzKbhEIYImy2ONzd410pabkJa/view?usp=drive_link' - }, - { - 'question_text': 'Do you have grandparents?', - 'question_audio_url': - 'https://drive.google.com/file/d/1UBaQYiJINgOmZWDLFKeXZQ8Zfb7cOKNZ/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/14cnEJTO7GtGZcO8ag50JdupV2EkvDHes/view?usp=drive_link' - }, - ], - 'description': - """Do you have brothers and sisters? In Lesson 1.3, we dive into the most personal and important topic for building friendships: Your Family. Many beginners make mistakes with the words "have" and "has," but today, we will fix that! Our teacher will teach you the exact formulas to describe your parents, siblings, and grandparents with perfect grammar. By the end of this video, you will be able to introduce your entire family in English with confidence.""", - }, - { - 'status': ProgressStatuses.started, - 'title': 'Lesson 1.4', - 'subtitle': '"What Do You Do?" Talk About Your Job & Studies', - 'topics': - """👉 The clear difference between talking about work and academic life. - 👉 Simple rules to decide between "I’m a teacher" and "I’m an accountant." - 👉 How to use the "-ing" form if you are still training for your dream job. - 👉 Perfecting the final "T" in "Student" and the "SH" sound in "Cashier." - 👉 A high-energy game to test your grammar reflexes!""", - 'practices': [ - { - 'question_text': 'It is good to see you again! What do you do?', - 'question_audio_url': - 'https://drive.google.com/file/d/171fh_iN0aXS0t95_5RzcxFiVmLCB7caP/view?usp=sharing', - 'sample_answer': - 'https://drive.google.com/file/d/1wDkbp23F2PsLZM9mWlRd-JjnwZa97T-8/view?usp=drive_link', - }, - { - 'question_text': - 'That is great. Are you studying to be a professional? What are you studying?', - 'sample_answer': - 'https://drive.google.com/file/d/1egHUMIqGt9VAw5HMVMdjWR3zmamdSB6F/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1T_qrH_-KHgQFw5kX4Ti-UwXvnBFyJsWT/view?usp=drive_link' - }, - { - 'question_text': - 'Your sister works in a restaurant. Is she a waitress?', - 'question_audio_url': - 'https://drive.google.com/file/d/1Okd-W2zRZCukQfnbLEp9A4r_5rt99U_6/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1t-_JHXel9dtSHRwYU_CO1Mc8-Pa1pOa0/view?usp=drive_link' - }, - { - 'question_text': 'My friend works in a bank. Is he an accountant?', - 'question_audio_url': - 'https://drive.google.com/file/d/1K4ahmIzuBdrsUoO-gVRp525ZFyP7b1Cq/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1ecpWSQTvU9pu8BYq3qRkYUjblX5wG4RZ/view?usp=drive_link' - }, - ], - 'description': - """You’ve shared your name and your home—now it’s time to talk about your daily life! In Lesson 1.4, we master the most common question in English: "What do you do?" Whether you are working a full-time job, searching for a new career, or currently studying, this lesson gives you the exact phrases to describe your profession with perfect grammar. -Miss [Name] breaks down the tricky "A vs. An" rule so you never have to second-guess yourself again. Plus, learn how to sound more natural by "linking" your words like a native speaker! -""" - }, - { - 'title': 'Lesson 1.5', - 'status': ProgressStatuses.started, - 'subtitle': 'Talk About Your Day! Daily Routines & Time Words', - 'topics': - """👉 Master the 6 most common actions, from "Wake up" to "Go to bed." - 👉 Learn why we say "I eat" but "She eats" (and how to never forget it!). - 👉 Use "First," "Then," and "Finally" to tell a complete story about your day. - 👉 How to perfectly say the tricky "TH" sound in the word "Teeth." - 👉 A fast-paced game to test if your brain can handle the "S" rule under pressure!""", - 'practices': [ - { - 'question_text': - 'I want to hear about your day! What is the first thing you do in the morning?', - 'question_audio_url': - 'https://drive.google.com/file/d/1B_g45crvD0X2xfJJuCBdBhe7LNRJtKpa/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/13nV3uQ6Vftc0DznBJ8GQMi26dHxmMUwH/view?usp=drive_link', - }, - { - 'question_text': - 'That is a great start. What do you do for your hygiene? I brush...', - 'sample_answer': - 'https://drive.google.com/file/d/1ijbKHO9A3PEOUb712ycCw-P3ZvWX-NTp/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1r92ixsWSD58TrV9LIArPRaKk1bVE5m44/view?usp=drive_link' - }, - { - 'question_text': - 'Perfect. And then, what do you do before you leave the house?', - 'question_audio_url': - 'https://drive.google.com/file/d/1xEHuliUwyqFV35o_C2varAHcoI3QrOsS/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1RJgS3tp0ZKQ1YIt9fLRch-IqFialZR6Q/view?usp=drive_link' - }, - { - 'question_text': - 'And finally, what is the last thing you do at night?', - 'question_audio_url': - 'https://drive.google.com/file/d/1njWyQExmgAPWgDTrcpqitEoNjiPfCbEw/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/140O0xAkYpVo5FLZQmbc1BaY_6agYWeki/view?usp=drive_link' - }, - ], - 'description': - """What do you do from the moment you wake up until you go to bed? In Lesson 1.5, we master the art of describing your Daily Routine at Home. This is the best way to practice the "Simple Present Tense" so you can talk about your habits, chores, and schedules like a pro. -Our teacher will show you the "S" secret—the most important grammar rule for talking about other people—and teach you the "Time Order Words" that make your English flow naturally from one activity to the next. -""" - }, - { - 'title': 'Lesson 1.6', - 'status': ProgressStatuses.started, - 'subtitle': 'Express Your Feelings! Likes, Dislikes & Hobbies', - 'topics': - """👉 How to build perfect positive and negative sentences (I like vs. I don't like). - 👉 Master the "Do you like...?" structure and the correct short answers. - 👉 Learn why we switch from "Don't" to "Doesn't" for He and She. - 👉 How to master the "L" sound and make your "Don't" sound natural and fast. - 👉 A high-speed game to test if you can handle the grammar of likes and dislikes!""", - 'practices': [ - { - 'question_text': - 'I want to know about your hobbies. Do you like football?', - 'question_audio_url': - 'https://drive.google.com/file/d/1ombhL58UTSSMlVr4OkjEN2m1TXeIs3Jw/view?usp=sharing', - 'sample_answer': - 'https://drive.google.com/file/d/1E-NilYZkazwTyAFwwNunDQmWIxBHH_GL/view?usp=drive_link', - }, - { - 'question_text': - 'That’s fun! Some people prefer quiet things. What do you like to do at home', - 'sample_answer': - 'https://drive.google.com/file/d/1HJqwHQId9MUhaQvjoyhuhk-ZQNCz5-RP/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1HJqwHQId9MUhaQvjoyhuhk-ZQNCz5-RP/view?usp=drive_link' - }, - { - 'question_text': - 'Interesting. Many people like music. Do you like slow music?', - 'question_audio_url': - 'https://drive.google.com/file/d/13edPllK_dZYmmFIsue_A-sGmDe9fh89K/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1lA18zdiFbKKXaGwUvJv4JwBMW_Vxxkip/view?usp=drive_link' - }, - { - 'question_text': 'Do you like running?', - 'question_audio_url': - 'https://drive.google.com/file/d/1MSVceVXL-mAELGvv0_SN68t1-pCM-V6R/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1XkwoWINsoXDSe63kjJXQFczjtPKbBTY4/view?usp=drive_link' - }, - ], - 'description': - """What makes you happy? What do you avoid? In Lesson 1.6, we learn how to make your English conversations personal and fun by sharing your Likes and Dislikes. This is the final topic of Module 1, and it is the key to building real friendships by finding common interests! -We will see how to use the power of "Do" and "Don't," and show you how the "S" rule changes when we talk about what our friends like. Get ready to talk about football, coffee, music, and more! -""" - }, - { - 'title': 'Lesson 1.7', - 'status': ProgressStatuses.started, - 'subtitle': 'Module 1 Final Test! Speaking Review & Graduation', - 'topics': """👉 Fast-paced practice for names, origins, and jobs. - Mastering "Have," "Has," and the plural "S" under pressure. - 👉 Using the Simple Present Tense to describe your day and your likes. - 👉 A bonus challenge to find and correct common beginner mistakes. - 👉 Two speaking scripts to help you link your words and sound like a native speaker.""", - 'practices': [ - { - 'question_text': 'Hello! What\'s your name?', - 'question_audio_url': - 'https://drive.google.com/file/d/11xsnZpqOXQREc2nku77u_qOXucUdncDq/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1OgvhumoPNdw33WfxEZfS1xtYZHshAM-k/view?usp=drive_link', - }, - { - 'question_text': 'It was a pleasure meeting you. Nice to meet you!', - 'sample_answer': - 'https://drive.google.com/file/d/1NmKeyx0mKmbFyZdmQ4sWGM6UMpwAIrHN/view?usp=drive_link', - 'question_audio_url': - 'https://drive.google.com/file/d/1nu1fiK4dLCjW9jjENvThshU48vFj-T3h/view?usp=drive_link' - }, - { - 'question_text': 'Where are you from?', - 'question_audio_url': - 'https://drive.google.com/file/d/1uM2L6-u0H-LHyDQL_Y0txjoO64n5lHkp/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1VXhHRG2CJR3lx07cAuiZrfCxzxdOcCIz/view?usp=drive_link' - }, - { - 'question_text': 'And where do you live now?', - 'question_audio_url': - 'https://drive.google.com/file/d/1sGJpaRQ1wArA2iDq7BZM-56abMZwiISO/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/10Cj8r2KeYUttBJ7W2wETP8XSdK2ZItom/view?usp=drive_link' - }, - { - 'question_text': 'Do you have a brother or sister?', - 'question_audio_url': - 'https://drive.google.com/file/d/1cA4wD7zY7WRgorWladvf1f8QRu54oBBw/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1IeiGyN1S5XaREfXgmzfl-zESap8A_k3H/view?usp=drive_link' - }, - { - 'question_text': 'I am a teacher. Are you a student?', - 'question_audio_url': - 'https://drive.google.com/file/d/1n5w3skHBvQ2hE_OA2HdeUnr0YeN4zL2_/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1j86TVmeNzZvL8RueZeIueCQO_40i9aMh/view?usp=drive_link' - }, - { - 'question_text': 'What is the first thing you do in the morning?', - 'question_audio_url': - 'https://drive.google.com/file/d/1oCrDDtYF4-1S2UYu6shilqmQSqyRG3v_/view?usp=drive_link', - 'sample_answer': - 'https://drive.google.com/file/d/1shpCr2XvV12cXaxuJ-AjW-0XWvBdtT8Q/view?usp=drive_link' - }, - ], - 'description': - """Congratulations! You have reached the end of Module 1. You started with simple greetings, and now you are ready to hold a full conversation in English! This is our Final Speaking Review, where we test everything you’ve learned so far. -From your name and family to your daily habits and hobbies, this lesson is designed to push your speed, accuracy, and confidence. We will lead you through three high-intensity speaking drills and a bonus Grammar Check to make sure you are 100% ready for Module 2! -""" - }, - ]; + // Learn module + List _modules = []; - List> get modules => _modules; + List get modules => _modules; // Navigation void pop() => _navigationService.back(); @@ -371,4 +45,20 @@ From your name and family to your daily habits and hobbies, this lesson is desig buttonLabel: 'Begin Lesson Practice', subtitle: 'Let’s quickly review what you’ve learned in this lesson!', ); + + // Remote api call + + // Learn modules + Future getModules(int id) async => await runBusyFuture(_getModules(id), + busyObject: StateObjects.learnModules); + + Future _getModules(int id) async { + if (_modules.isEmpty) { + if (await _statusChecker.checkConnection()) { + _modules = await _apiService.getModules(id); + _modules.sort( + (a, b) => (a.displayOrder ?? 0).compareTo(b.displayOrder ?? 0)); + } + } + } } diff --git a/lib/ui/views/learn_practice/learn_practice_viewmodel.dart b/lib/ui/views/learn_practice/learn_practice_viewmodel.dart index 71625be..fe79e90 100644 --- a/lib/ui/views/learn_practice/learn_practice_viewmodel.dart +++ b/lib/ui/views/learn_practice/learn_practice_viewmodel.dart @@ -2,7 +2,7 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:waveform_recorder/waveform_recorder.dart'; -import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/models/user.dart'; import 'package:yimaru_app/services/authentication_service.dart'; import 'package:yimaru_app/services/voice_recorder_service.dart'; import 'package:yimaru_app/ui/common/enmus.dart'; @@ -32,9 +32,9 @@ class LearnPracticeViewModel extends ReactiveViewModel { [_audioPlayerService, _voiceRecorderService, _authenticationService]; // User - UserModel? get _user => _authenticationService.user; + User? get _user => _authenticationService.user; - UserModel? get user => _user; + User? get user => _user; // AudioPlayer AudioPlayer get _player => _audioPlayerService.player; @@ -55,7 +55,6 @@ class LearnPracticeViewModel extends ReactiveViewModel { } // Voice recorder - WaveformRecorderController get _waveController => _voiceRecorderService.waveController; @@ -82,22 +81,36 @@ class LearnPracticeViewModel extends ReactiveViewModel { Map get selectedPractice => _selectedPractice; - // Practice - void setPractice(Map practice) { - _selectedPractice = practice; - goTo(1); + // Voice recorder + Future stopRecording() async => + await _voiceRecorderService.stopRecording(); + + Future startRecording() async => await runBusyFuture(_startRecording(), + busyObject: StateObjects.recordLearnPracticeAnswer); + + Future _startRecording() async => + await _voiceRecorderService.startRecording(); + + // Sample audio + Future playSampleAudio() async => + await runBusyFuture(_playSampleAudio(), + busyObject: StateObjects.learnPracticeSample); + + Future _playSampleAudio() async { + setBusyObject(StateObjects.learnPracticeSample); + await _audioPlayerService.playUrl(_selectedPractice['sample_answer']); + } + + Future pauseSampleAudio() async => + await runBusyFuture(_pauseSampleAudio(), + busyObject: StateObjects.learnPracticeSample); + + Future _pauseSampleAudio() async { + setBusyObject(StateObjects.learnPracticeSample); + await _audioPlayerService.pause(); } // Play practice audio - Future playQuestionAudio() async => - await runBusyFuture(_playQuestionAudio(), - busyObject: StateObjects.learnPracticeQuestion); - - Future _playQuestionAudio() async { - goTo(3); - await _audioPlayerService.playUrl(_selectedPractice['question_audio_url']); - } - void _listenToAudio() { _audioPlayerService.durationStream.listen((dur) { _duration = dur; @@ -112,29 +125,13 @@ class LearnPracticeViewModel extends ReactiveViewModel { }); } - // Set busy object + Future playQuestionAudio() async => + await runBusyFuture(_playQuestionAudio(), + busyObject: StateObjects.learnPracticeQuestion); - void setBusyObject(StateObjects object) { - _busyObject = object; - notifyListeners(); - } - - // Sample audio - Future playSampleAudio() async => - await runBusyFuture(_playSampleAudio(), - busyObject: StateObjects.learnPracticeSample); - Future _playSampleAudio() async { - setBusyObject(StateObjects.learnPracticeSample); - await _audioPlayerService.playUrl(_selectedPractice['sample_answer']); - } - - Future pauseSampleAudio() async => - await runBusyFuture(_pauseSampleAudio(), - busyObject: StateObjects.learnPracticeSample); - - Future _pauseSampleAudio() async { - setBusyObject(StateObjects.learnPracticeSample); - await _audioPlayerService.pause(); + Future _playQuestionAudio() async { + goTo(3); + await _audioPlayerService.playUrl(_selectedPractice['question_audio_url']); } // Recorded audio @@ -157,15 +154,17 @@ class LearnPracticeViewModel extends ReactiveViewModel { await _audioPlayerService.pause(); } - // Voice recorder - Future startRecording() async => await runBusyFuture(_startRecording(), - busyObject: StateObjects.recordLearnPracticeAnswer); + // Set busy object + void setBusyObject(StateObjects object) { + _busyObject = object; + notifyListeners(); + } - Future _startRecording() async => - await _voiceRecorderService.startRecording(); - - Future stopRecording() async => - await _voiceRecorderService.stopRecording(); + // Practice + void setPractice(Map practice) { + _selectedPractice = practice; + goTo(1); + } // Dialogue Future showAbortDialog() async { diff --git a/lib/ui/views/learn_subcategory/learn_subcategory_view.dart b/lib/ui/views/learn_subcategory/learn_subcategory_view.dart new file mode 100644 index 0000000..af1cf52 --- /dev/null +++ b/lib/ui/views/learn_subcategory/learn_subcategory_view.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/learn_subcategory_card.dart'; + +import '../../../models/subcategory.dart'; +import '../../common/app_colors.dart'; +import '../../common/enmus.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_circular_progress_indicator.dart'; +import '../../widgets/profile_app_bar.dart'; +import 'learn_subcategory_viewmodel.dart'; + +class LearnSubcategoryView extends StackedView { + const LearnSubcategoryView({Key? key}) : super(key: key); + + @override + void onViewModelReady(LearnSubcategoryViewModel viewModel) async { + await viewModel.getLearnSubcategories(); + super.onViewModelReady(viewModel); + } + + @override + LearnSubcategoryViewModel viewModelBuilder(BuildContext context) => + LearnSubcategoryViewModel(); + + @override + Widget builder( + BuildContext context, + LearnSubcategoryViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LearnSubcategoryViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LearnSubcategoryViewModel viewModel) => + SafeArea(child: _buildBody(viewModel)); + + Widget _buildBody(LearnSubcategoryViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LearnSubcategoryViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildAppBar(viewModel), + verticalSpaceMedium, + _buildSubcategoryColumnWrapper(viewModel) + ], + ); + + Widget _buildAppBar(LearnSubcategoryViewModel viewModel) => ProfileAppBar( + name: viewModel.user?.firstName, + profileImage: viewModel.user?.profilePicture, + ); + + Widget _buildSubcategoryColumnWrapper(LearnSubcategoryViewModel viewModel) => + Expanded(child: _buildSubcategoryColumnScrollView(viewModel)); + + Widget _buildSubcategoryColumnScrollView( + LearnSubcategoryViewModel viewModel) => + SingleChildScrollView( + child: _buildListViewBuilder(viewModel), + ); + + Widget _buildListViewBuilder(LearnSubcategoryViewModel viewModel) => + viewModel.busy(StateObjects.learnSubcategories) + ? _buildProgressIndicator() + : _buildListView(viewModel); + + Widget _buildProgressIndicator() => const Center( + child: CustomCircularProgressIndicator(color: kcPrimaryColor), + ); + + Widget _buildListView(LearnSubcategoryViewModel viewModel) => + ListView.separated( + shrinkWrap: true, + itemCount: viewModel.subcategories.length, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => verticalSpaceSmall, + itemBuilder: (context, index) => _buildTile( + subcategory: viewModel.subcategories[index], + onTap: () async => await viewModel + .navigateToLearn(viewModel.subcategories[index].id ?? 0), + ), + ); + + Widget _buildTile({ + required Subcategory subcategory, + required GestureTapCallback onTap, + }) => + LearnSubcategoryCard( + onTap: onTap, + subcategory: subcategory, + ); +} diff --git a/lib/ui/views/learn_subcategory/learn_subcategory_viewmodel.dart b/lib/ui/views/learn_subcategory/learn_subcategory_viewmodel.dart new file mode 100644 index 0000000..73b81ed --- /dev/null +++ b/lib/ui/views/learn_subcategory/learn_subcategory_viewmodel.dart @@ -0,0 +1,55 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; +import '../../../app/app.router.dart'; +import '../../../models/subcategory.dart'; +import '../../../models/user.dart'; +import '../../../services/api_service.dart'; +import '../../../services/authentication_service.dart'; +import '../../../services/status_checker_service.dart'; +import '../../common/enmus.dart'; + +class LearnSubcategoryViewModel extends ReactiveViewModel { + // Dependency injection + final _apiService = locator(); + + final _statusChecker = locator(); + + final _navigationService = locator(); + + final _authenticationService = locator(); + + @override + List get listenableServices => + [_authenticationService]; + + // Current user + User? get _user => _authenticationService.user; + + User? get user => _user; + + // Learn subcategories + List _subcategories = []; + + List get subcategories => _subcategories; + + // Navigation + Future navigateToLearn(int id) async => + _navigationService.navigateToLearnView(id: id); + + // Remote api call + + // Learn subcategories + Future getLearnSubcategories() async => + await runBusyFuture(_getLearnSubcategories(), + busyObject: StateObjects.learnSubcategories); + + Future _getLearnSubcategories() async { + if (_subcategories.isEmpty) { + if (await _statusChecker.checkConnection()) { + _subcategories = await _apiService.getLearnSubcategories(); + } + } + } +} diff --git a/lib/ui/views/login/login_view.dart b/lib/ui/views/login/login_view.dart index 4eacf0e..12c75c4 100644 --- a/lib/ui/views/login/login_view.dart +++ b/lib/ui/views/login/login_view.dart @@ -11,8 +11,8 @@ import 'login_viewmodel.dart'; @FormView(fields: [ FormTextField(name: 'otp', validator: FormValidator.validateForm), - FormTextField(name: 'email', validator: FormValidator.validateEmailForm), FormTextField(name: 'password', validator: FormValidator.validateForm), + FormTextField(name: 'email', validator: FormValidator.validateEmailForm), FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm) ]) class LoginView extends StackedView with $LoginView { diff --git a/lib/ui/views/login/login_view.form.dart b/lib/ui/views/login/login_view.form.dart index 265de18..b809a41 100644 --- a/lib/ui/views/login/login_view.form.dart +++ b/lib/ui/views/login/login_view.form.dart @@ -13,8 +13,8 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart'; const bool _autoTextFieldValidation = true; const String OtpValueKey = 'otp'; -const String EmailValueKey = 'email'; const String PasswordValueKey = 'password'; +const String EmailValueKey = 'email'; const String PhoneNumberValueKey = 'phoneNumber'; final Map _LoginViewTextEditingControllers = {}; @@ -23,24 +23,24 @@ final Map _LoginViewFocusNodes = {}; final Map _LoginViewTextValidations = { OtpValueKey: FormValidator.validateForm, - EmailValueKey: FormValidator.validateEmailForm, PasswordValueKey: FormValidator.validateForm, + EmailValueKey: FormValidator.validateEmailForm, PhoneNumberValueKey: FormValidator.validateForm, }; mixin $LoginView { TextEditingController get otpController => _getFormTextEditingController(OtpValueKey); - TextEditingController get emailController => - _getFormTextEditingController(EmailValueKey); TextEditingController get passwordController => _getFormTextEditingController(PasswordValueKey); + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); TextEditingController get phoneNumberController => _getFormTextEditingController(PhoneNumberValueKey); FocusNode get otpFocusNode => _getFormFocusNode(OtpValueKey); - FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey); + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey); TextEditingController _getFormTextEditingController( @@ -68,8 +68,8 @@ mixin $LoginView { /// with the latest textController values void syncFormWithViewModel(FormStateHelper model) { otpController.addListener(() => _updateFormData(model)); - emailController.addListener(() => _updateFormData(model)); passwordController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); phoneNumberController.addListener(() => _updateFormData(model)); _updateFormData(model, forceValidate: _autoTextFieldValidation); @@ -83,8 +83,8 @@ mixin $LoginView { ) void listenToFormUpdated(FormViewModel model) { otpController.addListener(() => _updateFormData(model)); - emailController.addListener(() => _updateFormData(model)); passwordController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); phoneNumberController.addListener(() => _updateFormData(model)); _updateFormData(model, forceValidate: _autoTextFieldValidation); @@ -96,8 +96,8 @@ mixin $LoginView { model.formValueMap ..addAll({ OtpValueKey: otpController.text, - EmailValueKey: emailController.text, PasswordValueKey: passwordController.text, + EmailValueKey: emailController.text, PhoneNumberValueKey: phoneNumberController.text, }), ); @@ -141,8 +141,8 @@ extension ValueProperties on FormStateHelper { } String? get otpValue => this.formValueMap[OtpValueKey] as String?; - String? get emailValue => this.formValueMap[EmailValueKey] as String?; String? get passwordValue => this.formValueMap[PasswordValueKey] as String?; + String? get emailValue => this.formValueMap[EmailValueKey] as String?; String? get phoneNumberValue => this.formValueMap[PhoneNumberValueKey] as String?; @@ -156,16 +156,6 @@ extension ValueProperties on FormStateHelper { } } - set emailValue(String? value) { - this.setData( - this.formValueMap..addAll({EmailValueKey: value}), - ); - - if (_LoginViewTextEditingControllers.containsKey(EmailValueKey)) { - _LoginViewTextEditingControllers[EmailValueKey]?.text = value ?? ''; - } - } - set passwordValue(String? value) { this.setData( this.formValueMap..addAll({PasswordValueKey: value}), @@ -176,6 +166,16 @@ extension ValueProperties on FormStateHelper { } } + set emailValue(String? value) { + this.setData( + this.formValueMap..addAll({EmailValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(EmailValueKey)) { + _LoginViewTextEditingControllers[EmailValueKey]?.text = value ?? ''; + } + } + set phoneNumberValue(String? value) { this.setData( this.formValueMap..addAll({PhoneNumberValueKey: value}), @@ -189,31 +189,31 @@ extension ValueProperties on FormStateHelper { bool get hasOtp => this.formValueMap.containsKey(OtpValueKey) && (otpValue?.isNotEmpty ?? false); - bool get hasEmail => - this.formValueMap.containsKey(EmailValueKey) && - (emailValue?.isNotEmpty ?? false); bool get hasPassword => this.formValueMap.containsKey(PasswordValueKey) && (passwordValue?.isNotEmpty ?? false); + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); bool get hasPhoneNumber => this.formValueMap.containsKey(PhoneNumberValueKey) && (phoneNumberValue?.isNotEmpty ?? false); bool get hasOtpValidationMessage => this.fieldsValidationMessages[OtpValueKey]?.isNotEmpty ?? false; - bool get hasEmailValidationMessage => - this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; bool get hasPasswordValidationMessage => this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false; + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; bool get hasPhoneNumberValidationMessage => this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false; String? get otpValidationMessage => this.fieldsValidationMessages[OtpValueKey]; - String? get emailValidationMessage => - this.fieldsValidationMessages[EmailValueKey]; String? get passwordValidationMessage => this.fieldsValidationMessages[PasswordValueKey]; + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; String? get phoneNumberValidationMessage => this.fieldsValidationMessages[PhoneNumberValueKey]; } @@ -221,18 +221,18 @@ extension ValueProperties on FormStateHelper { extension Methods on FormStateHelper { setOtpValidationMessage(String? validationMessage) => this.fieldsValidationMessages[OtpValueKey] = validationMessage; - setEmailValidationMessage(String? validationMessage) => - this.fieldsValidationMessages[EmailValueKey] = validationMessage; setPasswordValidationMessage(String? validationMessage) => this.fieldsValidationMessages[PasswordValueKey] = validationMessage; + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; setPhoneNumberValidationMessage(String? validationMessage) => this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage; /// Clears text input fields on the Form void clearForm() { otpValue = ''; - emailValue = ''; passwordValue = ''; + emailValue = ''; phoneNumberValue = ''; } @@ -240,8 +240,8 @@ extension Methods on FormStateHelper { void validateForm() { this.setValidationMessages({ OtpValueKey: getValidationMessage(OtpValueKey), - EmailValueKey: getValidationMessage(EmailValueKey), PasswordValueKey: getValidationMessage(PasswordValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), }); } @@ -263,7 +263,7 @@ String? getValidationMessage(String key) { void updateValidationData(FormStateHelper model) => model.setValidationMessages({ OtpValueKey: getValidationMessage(OtpValueKey), - EmailValueKey: getValidationMessage(EmailValueKey), PasswordValueKey: getValidationMessage(PasswordValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), }); diff --git a/lib/ui/views/login/login_viewmodel.dart b/lib/ui/views/login/login_viewmodel.dart index c2df529..19f841d 100644 --- a/lib/ui/views/login/login_viewmodel.dart +++ b/lib/ui/views/login/login_viewmodel.dart @@ -4,7 +4,7 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.router.dart'; -import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/models/user.dart'; import '../../../services/api_service.dart'; import '../../../services/authentication_service.dart'; @@ -29,8 +29,6 @@ class LoginViewModel extends ReactiveViewModel @override List get listenableServices => [_googleAuthService]; - // Sms - // Google user GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser; @@ -128,21 +126,16 @@ class LoginViewModel extends ReactiveViewModel _resendTime = DateTime.now().add(const Duration(minutes: 3, seconds: 0)); } - // Add user data - void addUserData(Map data) { - _userData.addAll(data); - } - + // User data void clearUserData() { _userData.clear(); } - // In app navigation - void goTo(int page) { - _currentPage = page; - rebuildUi(); + void addUserData(Map data) { + _userData.addAll(data); } + // In app navigation void goBack() { if (_currentPage == 1) { _currentPage = 0; @@ -155,16 +148,21 @@ class LoginViewModel extends ReactiveViewModel } } + void goTo(int page) { + _currentPage = page; + rebuildUi(); + } + // Navigation + Future replaceWithHome() async => + await _navigationService.clearStackAndShow(Routes.homeView); + Future navigateToRegister() async => await _navigationService.navigateToRegisterView(); Future navigateToForgetPassword() async => await _navigationService.navigateToForgetPasswordView(); - Future replaceWithHome() async => - await _navigationService.clearStackAndShow(Routes.homeView); - // Remote api calls // Login with email @@ -175,7 +173,7 @@ class LoginViewModel extends ReactiveViewModel if (await _statusChecker.checkConnection()) { Map response = await _apiService.login(_userData); if (response['status'] == ResponseStatus.success) { - UserModel user = response['data'] as UserModel; + User user = response['data'] as User; Map data = { 'userId': user.userId, 'accessToken': user.accessToken, @@ -192,6 +190,37 @@ class LoginViewModel extends ReactiveViewModel } } + // Sign-in with google + Future _googleAuth() async { + if (await _statusChecker.checkConnection()) { + await _googleAuthService.googleAuth(); + + Map data = { + 'id_token': _googleUser?.authentication.idToken ?? '', + }; + + Map response = await _apiService.googleAuth(data); + + if (response['status'] == ResponseStatus.success) { + User user = response['data'] as User; + Map data = { + 'userId': user.userId, + 'accessToken': user.accessToken, + 'refreshToken': user.refreshToken + }; + await _authenticationService.saveUserCredential(data); + clearUserData(); + await replaceWithHome(); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + } + } + + Future signInWithGoogle() async => await runBusyFuture(_googleAuth(), + busyObject: StateObjects.loginWithGoogle); + // Login with phone Future loginWithPhoneNumber() async => await runBusyFuture(_loginWithPhoneNumber(), @@ -209,46 +238,12 @@ class LoginViewModel extends ReactiveViewModel } } - // Sign-in with google - Future signInWithGoogle() async => await runBusyFuture(_googleAuth(), - busyObject: StateObjects.loginWithGoogle); - - Future _googleAuth() async { - if (await _statusChecker.checkConnection()) { - await _googleAuthService.googleAuth(); - - Map data = { - 'id_token': _googleUser?.authentication.idToken ?? '', - }; - - Map response = await _apiService.googleAuth(data); - - if (response['status'] == ResponseStatus.success) { - UserModel user = response['data'] as UserModel; - Map data = { - 'userId': user.userId, - 'accessToken': user.accessToken, - 'refreshToken': user.refreshToken - }; - await _authenticationService.saveUserCredential(data); - clearUserData(); - await replaceWithHome(); - showSuccessToast(response['message']); - } else { - showErrorToast(response['message']); - } - } - } - // Verify otp - Future verifyOtp() async => - await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp); - Future _verifyOtp() async { if (await _statusChecker.checkConnection()) { Map response = await _apiService.verifyOtp(_userData); if (response['status'] == ResponseStatus.success) { - UserModel user = response['data'] as UserModel; + User user = response['data'] as User; Map data = { 'userId': user.userId, 'accessToken': user.accessToken, @@ -264,10 +259,10 @@ class LoginViewModel extends ReactiveViewModel } } - // Resend otp - Future resendOtp() async => - await runBusyFuture(_resendOtp(), busyObject: StateObjects.resendOtp); + Future verifyOtp() async => + await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp); + // Resend otp Future _resendOtp() async { if (await _statusChecker.checkConnection()) { resetButton(); @@ -281,4 +276,7 @@ class LoginViewModel extends ReactiveViewModel } } } + + Future resendOtp() async => + await runBusyFuture(_resendOtp(), busyObject: StateObjects.resendOtp); } diff --git a/lib/ui/views/profile/profile_viewmodel.dart b/lib/ui/views/profile/profile_viewmodel.dart index 2de2155..4c6d119 100644 --- a/lib/ui/views/profile/profile_viewmodel.dart +++ b/lib/ui/views/profile/profile_viewmodel.dart @@ -5,7 +5,7 @@ import 'package:yimaru_app/services/image_picker_service.dart'; import 'package:yimaru_app/ui/common/enmus.dart'; import '../../../app/app.locator.dart'; -import '../../../models/user_model.dart'; +import '../../../models/user.dart'; import '../../../services/api_service.dart'; import '../../../services/authentication_service.dart'; import '../../../services/google_auth_service.dart'; @@ -32,9 +32,9 @@ class ProfileViewModel extends ReactiveViewModel { [_authenticationService]; // Current user - UserModel? get _user => _authenticationService.user; + User? get _user => _authenticationService.user; - UserModel? get user => _user; + User? get user => _user; // Image picker Future openCamera() async => diff --git a/lib/ui/views/profile_detail/profile_detail_viewmodel.dart b/lib/ui/views/profile_detail/profile_detail_viewmodel.dart index 42322a6..a1a7592 100644 --- a/lib/ui/views/profile_detail/profile_detail_viewmodel.dart +++ b/lib/ui/views/profile_detail/profile_detail_viewmodel.dart @@ -2,7 +2,7 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import '../../../app/app.locator.dart'; -import '../../../models/user_model.dart'; +import '../../../models/user.dart'; import '../../../services/api_service.dart'; import '../../../services/authentication_service.dart'; import '../../../services/image_picker_service.dart'; @@ -28,9 +28,9 @@ class ProfileDetailViewModel extends ReactiveViewModel [_authenticationService]; // Current user - UserModel? get _user => _authenticationService.user; + User? get _user => _authenticationService.user; - UserModel? get user => _user; + User? get user => _user; // First name bool _focusFirstName = false; diff --git a/lib/ui/views/register/register_viewmodel.dart b/lib/ui/views/register/register_viewmodel.dart index 7d1a098..1eaa57f 100644 --- a/lib/ui/views/register/register_viewmodel.dart +++ b/lib/ui/views/register/register_viewmodel.dart @@ -9,7 +9,7 @@ import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import '../../../app/app.locator.dart'; -import '../../../models/user_model.dart'; +import '../../../models/user.dart'; import '../../../services/google_auth_service.dart'; import '../../../services/smart_auth_service.dart'; import '../../../services/status_checker_service.dart'; @@ -336,7 +336,7 @@ class RegisterViewModel extends ReactiveViewModel Map response = await _apiService.googleAuth(data); if (response['status'] == ResponseStatus.success) { - UserModel user = response['data'] as UserModel; + User user = response['data'] as User; Map data = { 'userId': user.userId, 'accessToken': user.accessToken, @@ -359,7 +359,7 @@ class RegisterViewModel extends ReactiveViewModel if (await _statusChecker.checkConnection()) { Map response = await _apiService.verifyOtp(_userData); if (response['status'] == ResponseStatus.success) { - UserModel user = response['data'] as UserModel; + User user = response['data'] as User; Map data = { 'userId': user.userId, 'accessToken': user.accessToken, diff --git a/lib/ui/widgets/course_category_card.dart b/lib/ui/widgets/course_category_card.dart index 48853a2..e22094e 100644 --- a/lib/ui/widgets/course_category_card.dart +++ b/lib/ui/widgets/course_category_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:yimaru_app/models/course_category.dart'; +import 'package:yimaru_app/models/category.dart'; import 'package:yimaru_app/ui/common/helper_functions.dart'; import '../common/app_colors.dart'; @@ -8,7 +8,7 @@ import '../common/ui_helpers.dart'; import 'custom_elevated_button.dart'; class CourseCategoryCard extends StatelessWidget { - final CourseCategory category; + final Category category; final GestureTapCallback? onTap; const CourseCategoryCard({super.key, this.onTap, required this.category}); diff --git a/lib/ui/widgets/course_subcategory_tile.dart b/lib/ui/widgets/course_subcategory_tile.dart index f1813bc..1ba1c50 100644 --- a/lib/ui/widgets/course_subcategory_tile.dart +++ b/lib/ui/widgets/course_subcategory_tile.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; -import '../../models/course_subcategory.dart'; +import '../../models/subcategory.dart'; import '../common/app_colors.dart'; import '../common/ui_helpers.dart'; import 'custom_elevated_button.dart'; class CourseSubcategoryTile extends StatelessWidget { - final CourseSubcategory subcategory; + final Subcategory subcategory; final GestureTapCallback? onCourseTap; final GestureTapCallback? onPracticeTap; @@ -57,9 +57,9 @@ class CourseSubcategoryTile extends StatelessWidget { ]; Widget _buildTitle() => Text( - (subcategory.title == null || subcategory.title!.isEmpty) + (subcategory.name == null || subcategory.name!.isEmpty) ? 'Course ${subcategory.id}' - : subcategory.title!, + : subcategory.name!, style: style16P600, ); diff --git a/lib/ui/widgets/learn_level_tile.dart b/lib/ui/widgets/learn_level_tile.dart index 619c097..8c811ff 100644 --- a/lib/ui/widgets/learn_level_tile.dart +++ b/lib/ui/widgets/learn_level_tile.dart @@ -1,67 +1,69 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; -import 'package:yimaru_app/ui/common/enmus.dart'; -import 'package:yimaru_app/ui/common/ui_helpers.dart'; -import 'package:yimaru_app/ui/views/learn/learn_viewmodel.dart'; +import 'package:yimaru_app/ui/views/learn_level/learn_level_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/progress_status.dart'; +import '../../models/level.dart'; import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; import 'custom_elevated_button.dart'; -class LearnLevelTile extends ViewModelWidget { - final String title; - final String subtitle; - final ProgressStatuses status; +class LearnLevelTile extends ViewModelWidget { + final Level level; + final GestureTapCallback? onTap; const LearnLevelTile({ super.key, - required this.title, - required this.status, - required this.subtitle, + this.onTap, + required this.level, }); @override - Widget build(BuildContext context, LearnViewModel viewModel) => + Widget build(BuildContext context, LearnLevelViewModel viewModel) => _buildExpansionTileCard(viewModel); - Widget _buildExpansionTileCard(LearnViewModel viewModel) => Container( + Widget _buildExpansionTileCard(LearnLevelViewModel viewModel) => Container( margin: const EdgeInsets.only(bottom: 15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), border: Border.all( - color: status == ProgressStatuses.started - ? kcPrimaryColor.withOpacity(0.2) - : kcVeryLightGrey), + color: kcPrimaryColor.withOpacity(0.2), + // color: + // current ? kcPrimaryColor.withOpacity(0.2) : kcVeryLightGrey, + ), ), child: _buildExpansionTile(viewModel), ); - Widget _buildExpansionTile(LearnViewModel viewModel) => ExpansionTile( + Widget _buildExpansionTile(LearnLevelViewModel viewModel) => ExpansionTile( + enabled: true, textColor: kcDarkGrey, + showTrailingIcon: true, title: _buildTitleRow(), - subtitle: _buildContent(), + initiallyExpanded: false, collapsedIconColor: kcDarkGrey, collapsedTextColor: kcDarkGrey, shape: Border.all(color: kcTransparent), expandedAlignment: Alignment.centerLeft, - childrenPadding: const EdgeInsets.all(15), + backgroundColor: kcPrimaryColor.withOpacity(0.1), controlAffinity: ListTileControlAffinity.trailing, expandedCrossAxisAlignment: CrossAxisAlignment.start, - backgroundColor: status != ProgressStatuses.pending - ? kcPrimaryColor.withOpacity(0.1) - : kcBackgroundColor, tilePadding: const EdgeInsets.symmetric(horizontal: 15), - enabled: status != ProgressStatuses.pending ? true : false, - collapsedBackgroundColor: status != ProgressStatuses.pending - ? kcPrimaryColor.withOpacity(0.1) - : kcBackgroundColor, - showTrailingIcon: status != ProgressStatuses.pending ? true : false, - initiallyExpanded: status == ProgressStatuses.started ? true : false, + collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1), + childrenPadding: const EdgeInsets.only(left: 15, right: 15, bottom: 15), + + //enabled: current, + // showTrailingIcon: current, + //initiallyExpanded: current, + // collapsedBackgroundColor: + // current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor, + // backgroundColor: + // current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor, children: _buildExpansionTileChildren(viewModel), ); - List _buildExpansionTileChildren(LearnViewModel viewModel) => - [_buildActionButton(viewModel)]; + List _buildExpansionTileChildren(LearnLevelViewModel viewModel) => + [_buildViewButton(viewModel)]; Widget _buildTitleRow() => Row( mainAxisSize: MainAxisSize.min, @@ -70,42 +72,27 @@ class LearnLevelTile extends ViewModelWidget { List _buildTitleChildren() => [ _buildTitle(), - if (status != ProgressStatuses.pending) horizontalSpaceSmall, - if (status != ProgressStatuses.pending) _buildProgressStatus() + // if (current) horizontalSpaceSmall, + // if (current) _buildProgressStatus() ]; Widget _buildTitle() => Text( - title, - style: const TextStyle( - fontSize: 16, - color: kcPrimaryColor, - fontWeight: FontWeight.w600, - ), + level.title ?? '', + style: style16P600, ); - Widget _buildProgressStatus() => ProgressStatus( - status: status.name.substring(0, 1).toUpperCase() + - status.name.substring(1, status.name.length), + Widget _buildProgressStatus() => const ProgressStatus( color: kcPrimaryColor, + status: 'Current Level', ); - Widget _buildContent() => Text( - subtitle, - style: const TextStyle( - color: kcDarkGrey, - ), - ); - - Widget _buildActionButton(LearnViewModel viewModel) => CustomElevatedButton( + Widget _buildViewButton(LearnLevelViewModel viewModel) => + CustomElevatedButton( height: 15, + onTap: onTap, borderRadius: 12, + text: 'View Level', foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, - text: status == ProgressStatuses.completed - ? 'Review Course' - : status == ProgressStatuses.pending - ? 'Start Learning' - : 'Continue Learning', - onTap: () async => await viewModel.navigateToLearnLevel(), ); } diff --git a/lib/ui/widgets/learn_module_tile.dart b/lib/ui/widgets/learn_module_tile.dart index 6f856f7..3387146 100644 --- a/lib/ui/widgets/learn_module_tile.dart +++ b/lib/ui/widgets/learn_module_tile.dart @@ -4,27 +4,19 @@ import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart'; +import '../../models/module.dart'; import '../common/app_colors.dart'; import '../common/enmus.dart'; import '../common/ui_helpers.dart'; import 'custom_elevated_button.dart'; class LearnModuleTile extends ViewModelWidget { - final String title; - final String topics; - final String subtitle; - final String description; - final ProgressStatuses status; - final List> practices; + final Module module; + final GestureTapCallback? onLessonTap; + final GestureTapCallback? onPracticeTap; const LearnModuleTile( - {super.key, - required this.title, - required this.topics, - required this.status, - required this.subtitle, - required this.practices, - required this.description}); + {super.key, this.onLessonTap, this.onPracticeTap, required this.module}); Future _showSheet( {required BuildContext context, @@ -57,7 +49,7 @@ class LearnModuleTile extends ViewModelWidget { Stack( children: [ _buildExpansionTile(context: context, viewModel: viewModel), - _buildContainerShaderState() + // _buildContainerShaderState() ], ); @@ -65,8 +57,11 @@ class LearnModuleTile extends ViewModelWidget { {required BuildContext context, required LearnModuleViewModel viewModel}) => ExpansionTile( - textColor: kcDarkGrey, + enabled: true, title: _buildTitle(), + textColor: kcDarkGrey, + showTrailingIcon: true, + initiallyExpanded: true, subtitle: _buildContent(), leading: _buildIconWrapper(), collapsedIconColor: kcDarkGrey, @@ -75,13 +70,13 @@ class LearnModuleTile extends ViewModelWidget { shape: Border.all(color: kcTransparent), expandedAlignment: Alignment.centerLeft, collapsedBackgroundColor: kcBackgroundColor, - enabled: status != ProgressStatuses.pending, controlAffinity: ListTileControlAffinity.trailing, expandedCrossAxisAlignment: CrossAxisAlignment.start, tilePadding: const EdgeInsets.symmetric(horizontal: 15), childrenPadding: const EdgeInsets.fromLTRB(70, 15, 15, 15), - showTrailingIcon: status != ProgressStatuses.pending ? true : false, - initiallyExpanded: status == ProgressStatuses.started ? true : false, + // enabled: status != ProgressStatuses.pending, + // showTrailingIcon: status != ProgressStatuses.pending ? true : false, + //initiallyExpanded: status == ProgressStatuses.started ? true : false, children: _buildExpansionTileChildren(context: context, viewModel: viewModel), ); @@ -97,13 +92,19 @@ class LearnModuleTile extends ViewModelWidget { ); Widget _buildTitle() => Text( - title, + module.title ?? '', + maxLines: 1, + softWrap: false, style: style16P600, + overflow: TextOverflow.ellipsis, ); Widget _buildContent() => Text( - subtitle, + module.description ?? '', + maxLines: 1, + softWrap: false, style: style14DG400, + overflow: TextOverflow.ellipsis, ); List _buildExpansionTileChildren( @@ -179,15 +180,16 @@ class LearnModuleTile extends ViewModelWidget { CustomElevatedButton( height: 15, borderRadius: 12, - text: 'View Lessons', + onTap: onLessonTap, + text: 'View Module', foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, - onTap: () async => await viewModel.navigateToLearnLesson( - title: title, - topics: topics, - subtitle: subtitle, - practices: practices, - description: description), + // onTap: () async => await viewModel.navigateToLearnLesson( + // title: title, + // topics: topics, + // subtitle: subtitle, + // practices: practices, + // description: description), ); Widget _buildPracticeButtonWrapper( @@ -203,20 +205,21 @@ class LearnModuleTile extends ViewModelWidget { CustomElevatedButton( height: 15, borderRadius: 12, + onTap: onPracticeTap, text: 'View Practices', backgroundColor: kcWhite, borderColor: kcPrimaryColor, foregroundColor: kcPrimaryColor, - onTap: () async => await viewModel.navigateToLearnPractice(practices), + // onTap: () async => await viewModel.navigateToLearnPractice(practices), ); Widget _buildSheet(LearnModuleViewModel viewModel) => FinishPracticeSheet( onTap: viewModel.pop, ); - Widget _buildContainerShaderState() => status == ProgressStatuses.pending - ? _buildContainerShaderWrapper() - : Container(); + // Widget _buildContainerShaderState() => status == ProgressStatuses.pending + // ? _buildContainerShaderWrapper() + // : Container(); Widget _buildContainerShaderWrapper() => Positioned.fill( child: _buildContainerShader(), diff --git a/lib/ui/widgets/learn_sub_level_tile.dart b/lib/ui/widgets/learn_sub_level_tile.dart deleted file mode 100644 index 1e06cb1..0000000 --- a/lib/ui/widgets/learn_sub_level_tile.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stacked/stacked.dart'; -import 'package:yimaru_app/ui/views/learn_level/learn_level_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/progress_status.dart'; - -import '../common/app_colors.dart'; -import '../common/ui_helpers.dart'; -import 'custom_elevated_button.dart'; - -class LearnSubLevelTile extends ViewModelWidget { - final bool current; - final String title; - final String subtitle; - - const LearnSubLevelTile({ - super.key, - required this.title, - required this.current, - required this.subtitle, - }); - - @override - Widget build(BuildContext context, LearnLevelViewModel viewModel) => - _buildExpansionTileCard(viewModel); - - Widget _buildExpansionTileCard(LearnLevelViewModel viewModel) => Container( - margin: const EdgeInsets.only(bottom: 15), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - border: Border.all( - color: - current ? kcPrimaryColor.withOpacity(0.2) : kcVeryLightGrey), - ), - child: _buildExpansionTile(viewModel), - ); - - Widget _buildExpansionTile(LearnLevelViewModel viewModel) => ExpansionTile( - enabled: current, - textColor: kcDarkGrey, - title: _buildTitleRow(), - showTrailingIcon: current, - subtitle: _buildContent(), - initiallyExpanded: current, - collapsedIconColor: kcDarkGrey, - collapsedTextColor: kcDarkGrey, - shape: Border.all(color: kcTransparent), - expandedAlignment: Alignment.centerLeft, - childrenPadding: const EdgeInsets.all(15), - controlAffinity: ListTileControlAffinity.trailing, - expandedCrossAxisAlignment: CrossAxisAlignment.start, - backgroundColor: - current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor, - tilePadding: const EdgeInsets.symmetric(horizontal: 15), - collapsedBackgroundColor: - current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor, - children: _buildExpansionTileChildren(viewModel), - ); - - List _buildExpansionTileChildren(LearnLevelViewModel viewModel) => - [_buildActionButtonWrapper(viewModel)]; - - Widget _buildTitleRow() => Row( - mainAxisSize: MainAxisSize.min, - children: _buildTitleChildren(), - ); - - List _buildTitleChildren() => [ - _buildTitle(), - if (current) horizontalSpaceSmall, - if (current) _buildProgressStatus() - ]; - - Widget _buildTitle() => Text( - title, - style: style16P600, - ); - - Widget _buildProgressStatus() => const ProgressStatus( - color: kcPrimaryColor, - status: 'Current Level', - ); - - Widget _buildContent() => Text( - subtitle, - style: style14DG400, - ); - - Widget _buildActionButtonWrapper(LearnLevelViewModel viewModel) => SizedBox( - height: 40, - child: _buildActionButtons(viewModel), - ); - - Widget _buildActionButtons(LearnLevelViewModel viewModel) => Row( - children: [ - _buildViewButtonWrapper(viewModel), - horizontalSpaceSmall, - _buildPracticeButtonWrapper(viewModel) - ], - ); - - Widget _buildViewButtonWrapper(LearnLevelViewModel viewModel) => Expanded( - child: _buildViewButton(viewModel), - ); - - Widget _buildViewButton(LearnLevelViewModel viewModel) => - CustomElevatedButton( - height: 15, - borderRadius: 12, - text: 'View Course', - foregroundColor: kcWhite, - backgroundColor: kcPrimaryColor, - onTap: () async => await viewModel.navigateToLearnModule(), - ); - - Widget _buildPracticeButtonWrapper(LearnLevelViewModel viewModel) => Expanded( - child: Container(), - ); - - Widget _buildPracticeButton(LearnLevelViewModel viewModel) => - const CustomElevatedButton( - height: 15, - text: 'Practice', - borderRadius: 12, - backgroundColor: kcWhite, - borderColor: kcPrimaryColor, - foregroundColor: kcPrimaryColor - // onTap: () async => await viewModel.navigateToLearnPractice() - ); -} diff --git a/lib/ui/widgets/learn_subcategory_card.dart b/lib/ui/widgets/learn_subcategory_card.dart new file mode 100644 index 0000000..d09c3da --- /dev/null +++ b/lib/ui/widgets/learn_subcategory_card.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/models/subcategory.dart'; + +import '../common/app_colors.dart'; +import '../common/app_strings.dart'; +import '../common/helper_functions.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; + +class LearnSubcategoryCard extends StatelessWidget { + final Subcategory subcategory; + final GestureTapCallback? onTap; + + const LearnSubcategoryCard( + {super.key, this.onTap, required this.subcategory}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + height: 200, + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: getColor(), + borderRadius: BorderRadius.circular(5), + ), + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => [ + _buildTitle(), + verticalSpaceTiny, + _buildSubtitle(), + verticalSpaceMedium, + __buildStartButtonWrapper(), + ]; + + Widget _buildTitle() => Text( + subcategory.name ?? '', + style: style18DG700, + ); + + Widget _buildSubtitle() => Text( + subcategory.description ?? ksCategorySubtitle, + maxLines: 3, + style: style16DG400, + ); + + Widget __buildStartButtonWrapper() => SizedBox( + height: 40, + child: _buildStartButton(), + ); + + Widget _buildStartButton() => CustomElevatedButton( + height: 50, + width: 200, + onTap: onTap, + borderRadius: 12, + text: 'Select Course', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/lib/ui/widgets/learn_tile.dart b/lib/ui/widgets/learn_tile.dart new file mode 100644 index 0000000..517d363 --- /dev/null +++ b/lib/ui/widgets/learn_tile.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/models/course.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/views/learn/learn_viewmodel.dart'; + +import '../common/app_colors.dart'; +import 'custom_elevated_button.dart'; + +class LearnTile extends ViewModelWidget { + final Course course; + final GestureTapCallback? onTap; + + const LearnTile({ + super.key, + this.onTap, + required this.course, + }); + + @override + Widget build(BuildContext context, LearnViewModel viewModel) => + _buildExpansionTileCard(viewModel); + + Widget _buildExpansionTileCard(LearnViewModel viewModel) => Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all(color: kcPrimaryColor.withOpacity(0.2) + // color: status == ProgressStatuses.started + // ? kcPrimaryColor.withOpacity(0.2) + // : kcVeryLightGrey, + ), + ), + child: _buildExpansionTile(viewModel), + ); + + Widget _buildExpansionTile(LearnViewModel viewModel) => ExpansionTile( + enabled: true, + textColor: kcDarkGrey, + showTrailingIcon: true, + title: _buildTitleRow(), + initiallyExpanded: false, + subtitle: _buildContent(), + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + shape: Border.all(color: kcTransparent), + expandedAlignment: Alignment.centerLeft, + backgroundColor: kcPrimaryColor.withOpacity(0.1), + controlAffinity: ListTileControlAffinity.trailing, + childrenPadding: const EdgeInsets.only(bottom: 15), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1), + // enabled: status != ProgressStatuses.pending ? true : false, + // status != ProgressStatuses.pending + // ? kcPrimaryColor.withOpacity(0.1) + // : kcBackgroundColor, + // collapsedBackgroundColor: status != ProgressStatuses.pending + // ? kcPrimaryColor.withOpacity(0.1) + // : kcBackgroundColor, + // showTrailingIcon: status != ProgressStatuses.pending ? true : false, + // initiallyExpanded: status == ProgressStatuses.started ? true : false, + children: _buildExpansionTileChildren(viewModel), + ); + + List _buildExpansionTileChildren(LearnViewModel viewModel) => [ + _buildDivider(), + verticalSpaceTiny, + _buildActionButtonWrapper(viewModel) + ]; + + Widget _buildTitleRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildTitleChildren(), + ); + + List _buildTitleChildren() => [ + _buildTitle(), + // if (status != ProgressStatuses.pending) horizontalSpaceSmall, + // if (status != ProgressStatuses.pending) _buildProgressStatus() + ]; + + Widget _buildTitle() => Text( + course.title ?? '', + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + // Widget _buildProgressStatus() => ProgressStatus( + // status: status.name.substring(0, 1).toUpperCase() + + // status.name.substring(1, status.name.length), + // color: kcPrimaryColor, + // ); + + Widget _buildContent() => Text( + course.description ?? '', + style: style14DG400, + ); + + Widget _buildDivider() => const Divider(color: kcVeryLightGrey); + + Widget _buildActionButtonWrapper(LearnViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildActionButton(viewModel), + ); + + Widget _buildActionButton(LearnViewModel viewModel) => CustomElevatedButton( + height: 15, + onTap: onTap, + borderRadius: 12, + text: 'Start Course', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + // text: status == ProgressStatuses.completed + // ? 'Review Course' + // : status == ProgressStatuses.pending + // ? 'Start Learning' + // : 'Continue Learning', + ); +} diff --git a/lib/ui/widgets/selectable_course_practice_question.dart b/lib/ui/widgets/selectable_course_practice_question.dart index 0a2dded..567eba7 100644 --- a/lib/ui/widgets/selectable_course_practice_question.dart +++ b/lib/ui/widgets/selectable_course_practice_question.dart @@ -84,7 +84,8 @@ class SelectableCoursePracticeQuestion ? _buildProgressIndicator() : _buildContinueButton(viewModel); - Widget _buildProgressIndicator()=>const CustomCircularProgressIndicator(color: kcPrimaryColor); + Widget _buildProgressIndicator() => + const CustomCircularProgressIndicator(color: kcPrimaryColor); Widget _buildContinueButton(CoursePracticeQuestionViewModel viewModel) => CustomElevatedButton( @@ -92,7 +93,7 @@ class SelectableCoursePracticeQuestion borderRadius: 12, foregroundColor: kcWhite, text: viewModel.currentQuestionIndex == - viewModel.coursePracticeQuestions.length - 1 + viewModel.coursePracticeQuestions.length - 1 ? 'Finish' : 'Continue', backgroundColor: @@ -100,7 +101,7 @@ class SelectableCoursePracticeQuestion ? kcPrimaryColor : kcPrimaryColor.withOpacity(0.1), onTap: viewModel.selectedAnswers.containsKey((index + 1).toString()) - ? ()async =>await viewModel.nextQuestion(viewModel + ? () async => await viewModel.nextQuestion(viewModel .coursePracticeQuestions[ index + 1 < viewModel.coursePracticeQuestions.length ? index + 1 @@ -108,6 +109,5 @@ class SelectableCoursePracticeQuestion .questionId ?? 0) : null, - ); } diff --git a/lib/ui/widgets/writing_course_practice_question.dart b/lib/ui/widgets/writing_course_practice_question.dart index ffcbba0..ba3aa08 100644 --- a/lib/ui/widgets/writing_course_practice_question.dart +++ b/lib/ui/widgets/writing_course_practice_question.dart @@ -99,7 +99,8 @@ class WritingCoursePracticeQuestion ? _buildProgressIndicator() : _buildContinueButton(viewModel); - Widget _buildProgressIndicator()=>const CustomCircularProgressIndicator(color: kcPrimaryColor); + Widget _buildProgressIndicator() => + const CustomCircularProgressIndicator(color: kcPrimaryColor); Widget _buildContinueButton(CoursePracticeQuestionViewModel viewModel) => CustomElevatedButton( @@ -110,7 +111,7 @@ class WritingCoursePracticeQuestion ? kcPrimaryColor : kcPrimaryColor.withOpacity(0.1), onTap: answerController.text.isNotEmpty - ? ()async =>await viewModel.nextQuestion( + ? () async => await viewModel.nextQuestion( index + 1 < viewModel.coursePracticeQuestions.length ? index + 1 : index) diff --git a/pubspec.yaml b/pubspec.yaml index 54034fa..b215f2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: yimaru_app description: A new Flutter project. publish_to: 'none' -version: 0.1.2+3 +version: 0.1.2+4 environment: sdk: '>=3.0.3 <4.0.0' diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart index ccea6c5..550914e 100644 --- a/test/helpers/test_helpers.mocks.dart +++ b/test/helpers/test_helpers.mocks.dart @@ -8,38 +8,40 @@ import 'dart:ui' as _i10; import 'package:audioplayers/audioplayers.dart' as _i4; import 'package:dio/dio.dart' as _i2; -import 'package:firebase_messaging/firebase_messaging.dart' as _i30; +import 'package:firebase_messaging/firebase_messaging.dart' as _i32; import 'package:flutter/material.dart' as _i8; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i7; -import 'package:permission_handler/permission_handler.dart' as _i25; +import 'package:permission_handler/permission_handler.dart' as _i27; import 'package:stacked_services/stacked_services.dart' as _i6; import 'package:waveform_recorder/waveform_recorder.dart' as _i5; -import 'package:yimaru_app/models/course.dart' as _i17; -import 'package:yimaru_app/models/course_category.dart' as _i15; -import 'package:yimaru_app/models/course_detail.dart' as _i33; -import 'package:yimaru_app/models/course_lesson.dart' as _i19; -import 'package:yimaru_app/models/course_progress.dart' as _i18; -import 'package:yimaru_app/models/course_subcategory.dart' as _i16; -import 'package:yimaru_app/models/practice.dart' as _i20; -import 'package:yimaru_app/models/practice_question.dart' as _i21; +import 'package:yimaru_app/models/category.dart' as _i15; +import 'package:yimaru_app/models/course.dart' as _i21; +import 'package:yimaru_app/models/course_detail.dart' as _i35; +import 'package:yimaru_app/models/course_lesson.dart' as _i18; +import 'package:yimaru_app/models/course_progress.dart' as _i17; +import 'package:yimaru_app/models/level.dart' as _i22; +import 'package:yimaru_app/models/module.dart' as _i23; +import 'package:yimaru_app/models/practice.dart' as _i19; +import 'package:yimaru_app/models/practice_question.dart' as _i20; import 'package:yimaru_app/models/question.dart' as _i14; -import 'package:yimaru_app/models/user_model.dart' as _i12; +import 'package:yimaru_app/models/subcategory.dart' as _i16; +import 'package:yimaru_app/models/user.dart' as _i12; import 'package:yimaru_app/services/api_service.dart' as _i13; -import 'package:yimaru_app/services/audio_player_service.dart' as _i34; +import 'package:yimaru_app/services/audio_player_service.dart' as _i36; import 'package:yimaru_app/services/authentication_service.dart' as _i11; -import 'package:yimaru_app/services/course_service.dart' as _i32; -import 'package:yimaru_app/services/dio_service.dart' as _i22; -import 'package:yimaru_app/services/google_auth_service.dart' as _i27; -import 'package:yimaru_app/services/image_downloader_service.dart' as _i28; -import 'package:yimaru_app/services/image_picker_service.dart' as _i26; -import 'package:yimaru_app/services/notification_service.dart' as _i29; -import 'package:yimaru_app/services/permission_handler_service.dart' as _i24; +import 'package:yimaru_app/services/course_service.dart' as _i34; +import 'package:yimaru_app/services/dio_service.dart' as _i24; +import 'package:yimaru_app/services/google_auth_service.dart' as _i29; +import 'package:yimaru_app/services/image_downloader_service.dart' as _i30; +import 'package:yimaru_app/services/image_picker_service.dart' as _i28; +import 'package:yimaru_app/services/notification_service.dart' as _i31; +import 'package:yimaru_app/services/permission_handler_service.dart' as _i26; import 'package:yimaru_app/services/secure_storage_service.dart' as _i3; -import 'package:yimaru_app/services/smart_auth_service.dart' as _i31; -import 'package:yimaru_app/services/status_checker_service.dart' as _i23; -import 'package:yimaru_app/services/voice_recorder_service.dart' as _i35; -import 'package:yimaru_app/ui/common/enmus.dart' as _i36; +import 'package:yimaru_app/services/smart_auth_service.dart' as _i33; +import 'package:yimaru_app/services/status_checker_service.dart' as _i25; +import 'package:yimaru_app/services/voice_recorder_service.dart' as _i37; +import 'package:yimaru_app/ui/common/enmus.dart' as _i38; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -857,7 +859,7 @@ class MockAuthenticationService extends _i1.Mock ) as _i9.Future); @override - _i9.Future saveUserData(_i12.UserModel? data) => (super.noSuchMethod( + _i9.Future saveUserData(_i12.User? data) => (super.noSuchMethod( Invocation.method( #saveUserData, [data], @@ -898,14 +900,14 @@ class MockAuthenticationService extends _i1.Mock ) as _i9.Future); @override - _i9.Future<_i12.UserModel?> getUser() => (super.noSuchMethod( + _i9.Future<_i12.User?> getUser() => (super.noSuchMethod( Invocation.method( #getUser, [], ), - returnValue: _i9.Future<_i12.UserModel?>.value(), - returnValueForMissingStub: _i9.Future<_i12.UserModel?>.value(), - ) as _i9.Future<_i12.UserModel?>); + returnValue: _i9.Future<_i12.User?>.value(), + returnValueForMissingStub: _i9.Future<_i12.User?>.value(), + ) as _i9.Future<_i12.User?>); @override _i9.Future logout() => (super.noSuchMethod( @@ -1053,7 +1055,7 @@ class MockApiService extends _i1.Mock implements _i13.ApiService { ) as _i9.Future>); @override - _i9.Future> getProfileStatus(_i12.UserModel? user) => + _i9.Future> getProfileStatus(_i12.User? user) => (super.noSuchMethod( Invocation.method( #getProfileStatus, @@ -1124,68 +1126,54 @@ class MockApiService extends _i1.Mock implements _i13.ApiService { ) as _i9.Future>); @override - _i9.Future> getCourseCategories() => - (super.noSuchMethod( + _i9.Future> getCategories() => (super.noSuchMethod( Invocation.method( #getCourseCategories, [], ), - returnValue: _i9.Future>.value( - <_i15.CourseCategory>[]), - returnValueForMissingStub: _i9.Future>.value( - <_i15.CourseCategory>[]), - ) as _i9.Future>); + returnValue: _i9.Future>.value(<_i15.Category>[]), + returnValueForMissingStub: + _i9.Future>.value(<_i15.Category>[]), + ) as _i9.Future>); @override - _i9.Future> getCourseSubcategories(int? id) => + _i9.Future> getSubcategories(int? id) => (super.noSuchMethod( Invocation.method( #getCourseSubcategories, [id], ), - returnValue: _i9.Future>.value( - <_i16.CourseSubcategory>[]), + returnValue: + _i9.Future>.value(<_i16.Subcategory>[]), returnValueForMissingStub: - _i9.Future>.value( - <_i16.CourseSubcategory>[]), - ) as _i9.Future>); + _i9.Future>.value(<_i16.Subcategory>[]), + ) as _i9.Future>); @override - _i9.Future> getCourses(int? id) => (super.noSuchMethod( - Invocation.method( - #getCourses, - [id], - ), - returnValue: _i9.Future>.value(<_i17.Course>[]), - returnValueForMissingStub: - _i9.Future>.value(<_i17.Course>[]), - ) as _i9.Future>); - - @override - _i9.Future> getCourseProgress(int? id) => + _i9.Future> getCourseProgress(int? id) => (super.noSuchMethod( Invocation.method( #getCourseProgress, [id], ), - returnValue: _i9.Future>.value( - <_i18.CourseProgress>[]), - returnValueForMissingStub: _i9.Future>.value( - <_i18.CourseProgress>[]), - ) as _i9.Future>); + returnValue: _i9.Future>.value( + <_i17.CourseProgress>[]), + returnValueForMissingStub: _i9.Future>.value( + <_i17.CourseProgress>[]), + ) as _i9.Future>); @override - _i9.Future> getCourseLessons(int? id) => + _i9.Future> getCourseLessons(int? id) => (super.noSuchMethod( Invocation.method( #getCourseLessons, [id], ), returnValue: - _i9.Future>.value(<_i19.CourseLesson>[]), + _i9.Future>.value(<_i18.CourseLesson>[]), returnValueForMissingStub: - _i9.Future>.value(<_i19.CourseLesson>[]), - ) as _i9.Future>); + _i9.Future>.value(<_i18.CourseLesson>[]), + ) as _i9.Future>); @override _i9.Future> completeLesson(int? id) => @@ -1201,30 +1189,87 @@ class MockApiService extends _i1.Mock implements _i13.ApiService { ) as _i9.Future>); @override - _i9.Future> getCoursePractices(int? id) => + _i9.Future> getCoursePractices(int? id) => (super.noSuchMethod( Invocation.method( #getCoursePractices, [id], ), - returnValue: _i9.Future>.value(<_i20.Practice>[]), + returnValue: _i9.Future>.value(<_i19.Practice>[]), returnValueForMissingStub: - _i9.Future>.value(<_i20.Practice>[]), - ) as _i9.Future>); + _i9.Future>.value(<_i19.Practice>[]), + ) as _i9.Future>); @override - _i9.Future> getCoursePracticeQuestions(int? id) => + _i9.Future> getCoursePracticeQuestions(int? id) => (super.noSuchMethod( Invocation.method( #getCoursePracticeQuestions, [id], ), - returnValue: _i9.Future>.value( - <_i21.PracticeQuestion>[]), + returnValue: _i9.Future>.value( + <_i20.PracticeQuestion>[]), returnValueForMissingStub: - _i9.Future>.value( - <_i21.PracticeQuestion>[]), - ) as _i9.Future>); + _i9.Future>.value( + <_i20.PracticeQuestion>[]), + ) as _i9.Future>); + + @override + _i9.Future<_i14.Question?> getCoursePracticeQuestion(int? id) => + (super.noSuchMethod( + Invocation.method( + #getCoursePracticeQuestion, + [id], + ), + returnValue: _i9.Future<_i14.Question?>.value(), + returnValueForMissingStub: _i9.Future<_i14.Question?>.value(), + ) as _i9.Future<_i14.Question?>); + + @override + _i9.Future> getLearnSubcategories() => + (super.noSuchMethod( + Invocation.method( + #getLearnSubcategories, + [], + ), + returnValue: + _i9.Future>.value(<_i16.Subcategory>[]), + returnValueForMissingStub: + _i9.Future>.value(<_i16.Subcategory>[]), + ) as _i9.Future>); + + @override + _i9.Future> getCourses(int? id) => (super.noSuchMethod( + Invocation.method( + #getCourses, + [id], + ), + returnValue: _i9.Future>.value(<_i21.Course>[]), + returnValueForMissingStub: + _i9.Future>.value(<_i21.Course>[]), + ) as _i9.Future>); + + @override + _i9.Future> getLevels(int? id) => (super.noSuchMethod( + Invocation.method( + #getLevels, + [id], + ), + returnValue: _i9.Future>.value(<_i22.Level>[]), + returnValueForMissingStub: + _i9.Future>.value(<_i22.Level>[]), + ) as _i9.Future>); + + @override + _i9.Future> getModules(int? id) => (super.noSuchMethod( + Invocation.method( + #getModules, + [id], + ), + returnValue: _i9.Future>.value(<_i23.Module>[]), + returnValueForMissingStub: + _i9.Future>.value(<_i23.Module>[]), + ) as _i9.Future>); } /// A class which mocks [SecureStorageService]. @@ -1327,7 +1372,7 @@ class MockSecureStorageService extends _i1.Mock /// A class which mocks [DioService]. /// /// See the documentation for Mockito's code generation for more information. -class MockDioService extends _i1.Mock implements _i22.DioService { +class MockDioService extends _i1.Mock implements _i24.DioService { @override _i2.Dio get dio => (super.noSuchMethod( Invocation.getter(#dio), @@ -1346,7 +1391,7 @@ class MockDioService extends _i1.Mock implements _i22.DioService { /// /// See the documentation for Mockito's code generation for more information. class MockStatusCheckerService extends _i1.Mock - implements _i23.StatusCheckerService { + implements _i25.StatusCheckerService { @override _i3.SecureStorageService get storage => (super.noSuchMethod( Invocation.getter(#storage), @@ -1412,40 +1457,40 @@ class MockStatusCheckerService extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockPermissionHandlerService extends _i1.Mock - implements _i24.PermissionHandlerService { + implements _i26.PermissionHandlerService { @override - _i9.Future<_i25.PermissionStatus> requestPermission( - _i25.Permission? requestedPermission) => + _i9.Future<_i27.PermissionStatus> requestPermission( + _i27.Permission? requestedPermission) => (super.noSuchMethod( Invocation.method( #requestPermission, [requestedPermission], ), - returnValue: _i9.Future<_i25.PermissionStatus>.value( - _i25.PermissionStatus.denied), - returnValueForMissingStub: _i9.Future<_i25.PermissionStatus>.value( - _i25.PermissionStatus.denied), - ) as _i9.Future<_i25.PermissionStatus>); + returnValue: _i9.Future<_i27.PermissionStatus>.value( + _i27.PermissionStatus.denied), + returnValueForMissingStub: _i9.Future<_i27.PermissionStatus>.value( + _i27.PermissionStatus.denied), + ) as _i9.Future<_i27.PermissionStatus>); @override - _i9.Future<_i25.PermissionStatus> request(_i25.Permission? permission) => + _i9.Future<_i27.PermissionStatus> request(_i27.Permission? permission) => (super.noSuchMethod( Invocation.method( #request, [permission], ), - returnValue: _i9.Future<_i25.PermissionStatus>.value( - _i25.PermissionStatus.denied), - returnValueForMissingStub: _i9.Future<_i25.PermissionStatus>.value( - _i25.PermissionStatus.denied), - ) as _i9.Future<_i25.PermissionStatus>); + returnValue: _i9.Future<_i27.PermissionStatus>.value( + _i27.PermissionStatus.denied), + returnValueForMissingStub: _i9.Future<_i27.PermissionStatus>.value( + _i27.PermissionStatus.denied), + ) as _i9.Future<_i27.PermissionStatus>); } /// A class which mocks [ImagePickerService]. /// /// See the documentation for Mockito's code generation for more information. class MockImagePickerService extends _i1.Mock - implements _i26.ImagePickerService { + implements _i28.ImagePickerService { @override _i9.Future gallery() => (super.noSuchMethod( Invocation.method( @@ -1470,7 +1515,7 @@ class MockImagePickerService extends _i1.Mock /// A class which mocks [GoogleAuthService]. /// /// See the documentation for Mockito's code generation for more information. -class MockGoogleAuthService extends _i1.Mock implements _i27.GoogleAuthService { +class MockGoogleAuthService extends _i1.Mock implements _i29.GoogleAuthService { @override int get listenersCount => (super.noSuchMethod( Invocation.getter(#listenersCount), @@ -1540,7 +1585,7 @@ class MockGoogleAuthService extends _i1.Mock implements _i27.GoogleAuthService { /// /// See the documentation for Mockito's code generation for more information. class MockImageDownloaderService extends _i1.Mock - implements _i28.ImageDownloaderService { + implements _i30.ImageDownloaderService { @override _i9.Future downloader(String? networkImage) => (super.noSuchMethod( Invocation.method( @@ -1569,7 +1614,7 @@ class MockImageDownloaderService extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockNotificationService extends _i1.Mock - implements _i29.NotificationService { + implements _i31.NotificationService { @override _i9.Future initialize() => (super.noSuchMethod( Invocation.method( @@ -1591,7 +1636,7 @@ class MockNotificationService extends _i1.Mock ) as _i9.Future); @override - _i9.Future showNotification(_i30.RemoteMessage? message) => + _i9.Future showNotification(_i32.RemoteMessage? message) => (super.noSuchMethod( Invocation.method( #showNotification, @@ -1625,7 +1670,7 @@ class MockNotificationService extends _i1.Mock /// A class which mocks [SmartAuthService]. /// /// See the documentation for Mockito's code generation for more information. -class MockSmartAuthService extends _i1.Mock implements _i31.SmartAuthService { +class MockSmartAuthService extends _i1.Mock implements _i33.SmartAuthService { @override bool get listenForMultipleSms => (super.noSuchMethod( Invocation.getter(#listenForMultipleSms), @@ -1657,26 +1702,26 @@ class MockSmartAuthService extends _i1.Mock implements _i31.SmartAuthService { /// A class which mocks [CourseService]. /// /// See the documentation for Mockito's code generation for more information. -class MockCourseService extends _i1.Mock implements _i32.CourseService { +class MockCourseService extends _i1.Mock implements _i34.CourseService { @override - _i9.Future> getCoursesDetail(int? id) => + _i9.Future> getCoursesDetail(int? id) => (super.noSuchMethod( Invocation.method( #getCoursesDetail, [id], ), returnValue: - _i9.Future>.value(<_i33.CourseDetail>[]), + _i9.Future>.value(<_i35.CourseDetail>[]), returnValueForMissingStub: - _i9.Future>.value(<_i33.CourseDetail>[]), - ) as _i9.Future>); + _i9.Future>.value(<_i35.CourseDetail>[]), + ) as _i9.Future>); } /// A class which mocks [AudioPlayerService]. /// /// See the documentation for Mockito's code generation for more information. class MockAudioPlayerService extends _i1.Mock - implements _i34.AudioPlayerService { + implements _i36.AudioPlayerService { @override _i4.AudioPlayer get player => (super.noSuchMethod( Invocation.getter(#player), @@ -1800,13 +1845,13 @@ class MockAudioPlayerService extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockVoiceRecorderService extends _i1.Mock - implements _i35.VoiceRecorderService { + implements _i37.VoiceRecorderService { @override - _i36.VoiceRecordingState get recordingState => (super.noSuchMethod( + _i38.VoiceRecordingState get recordingState => (super.noSuchMethod( Invocation.getter(#recordingState), - returnValue: _i36.VoiceRecordingState.pending, - returnValueForMissingStub: _i36.VoiceRecordingState.pending, - ) as _i36.VoiceRecordingState); + returnValue: _i38.VoiceRecordingState.pending, + returnValueForMissingStub: _i38.VoiceRecordingState.pending, + ) as _i38.VoiceRecordingState); @override _i5.WaveformRecorderController get waveController => (super.noSuchMethod( diff --git a/test/viewmodels/learn_subcategory_viewmodel_test.dart b/test/viewmodels/learn_subcategory_viewmodel_test.dart new file mode 100644 index 0000000..88f0f86 --- /dev/null +++ b/test/viewmodels/learn_subcategory_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LearnSubcategoryViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +}