From 4eb6e9d6c309d980357d9e2ad8e3d6baf5c53677 Mon Sep 17 00:00:00 2001 From: BisratHailu Date: Fri, 20 Feb 2026 15:15:23 +0300 Subject: [PATCH] feat(course): Polish course ui --- lib/app/app.dart | 8 + lib/app/app.router.dart | 255 ++- lib/models/assessment.dart | 4 +- lib/models/assessment.g.dart | 2 +- lib/models/user_model.dart | 31 + lib/services/api_service.dart | 3 +- lib/services/authentication_service.dart | 49 +- lib/services/dio_service.dart | 12 +- lib/services/google_auth_service.dart | 2 + lib/services/image_downloader_service.dart | 2 + lib/services/image_picker_service.dart | 4 + lib/services/permission_handler_service.dart | 3 + lib/services/secure_storage_service.dart | 10 +- lib/services/status_checker_service.dart | 20 +- lib/ui/common/app_constants.dart | 5 +- lib/ui/common/app_strings.dart | 8 +- lib/ui/common/enmus.dart | 7 +- lib/ui/common/helper_functions.dart | 2 + lib/ui/common/ui_helpers.dart | 22 +- lib/ui/common/validators/form_validator.dart | 58 +- .../account_privacy/account_privacy_view.dart | 8 +- .../account_privacy_viewmodel.dart | 1 + lib/ui/views/assessment/assessment_view.dart | 9 +- .../assessment/assessment_viewmodel.dart | 2 + .../screens/assessment_completion_screen.dart | 2 +- .../screens/assessment_failure_screen.dart | 7 +- .../screens/assessment_form_screen.dart | 6 +- .../screens/assessment_loading_screen.dart | 2 +- .../screens/assessment_result_screen.dart | 2 +- .../screens/retake_assessment_screen.dart | 4 +- .../views/call_support/call_support_view.dart | 12 +- .../call_support/call_support_viewmodel.dart | 3 + lib/ui/views/course/course_view.dart | 105 ++ lib/ui/views/course/course_viewmodel.dart | 42 + .../course_module/course_module_view.dart | 105 ++ .../course_module_viewmodel.dart | 31 + .../course_payment/course_payment_view.dart | 91 ++ .../course_payment_viewmodel.dart | 12 + .../course_practice/course_practice_view.dart | 103 ++ .../course_practice_viewmodel.dart | 36 + lib/ui/views/downloads/downloads_view.dart | 50 +- .../views/downloads/downloads_viewmodel.dart | 5 + lib/ui/views/home/home_view.dart | 3 +- lib/ui/views/learn/learn_viewmodel.dart | 4 +- .../views/learn_lesson/learn_lesson_view.dart | 3 +- .../learn_lesson/learn_lesson_viewmodel.dart | 2 +- .../learn_level/learn_level_viewmodel.dart | 3 +- .../views/learn_module/learn_module_view.dart | 4 +- .../learn_module/learn_module_viewmodel.dart | 5 +- .../learn_practice/learn_practice_view.dart | 6 +- .../learn_practice_completion_screen.dart | 78 +- .../screens/learn_practice_intro_screen.dart | 4 +- .../screens/learn_practice_result_screen.dart | 3 +- .../screens/age_group_form_screen.dart | 4 +- .../educational_background_form_screen.dart | 4 +- lib/ui/views/progress/progress_view.dart | 4 +- .../telegram_support_view.dart | 4 +- lib/ui/widgets/birthday_selector.dart | 4 +- .../widgets/cancel_learn_practice_sheet.dart | 2 +- lib/ui/widgets/course_card.dart | 75 + lib/ui/widgets/course_module_tile.dart | 132 ++ lib/ui/widgets/course_payment_card.dart | 52 + lib/ui/widgets/course_practice_card.dart | 53 + ...el_card.dart => course_progress_card.dart} | 8 +- lib/ui/widgets/custom_large_radio_button.dart | 2 +- lib/ui/widgets/learn_lesson_tile.dart | 2 +- .../widgets/learn_practice_tip_section.dart | 21 +- lib/ui/widgets/learn_sub_level_tile.dart | 20 +- lib/ui/widgets/practice_response_card.dart | 5 +- lib/ui/widgets/practice_result_card.dart | 16 +- test/helpers/test_helpers.mocks.dart | 1414 +++++++++++++++++ .../course_module_viewmodel_test.dart | 11 + .../course_payment_viewmodel_test.dart | 11 + .../course_practice_viewmodel_test.dart | 11 + test/viewmodels/course_viewmodel_test.dart | 11 + 75 files changed, 2852 insertions(+), 274 deletions(-) create mode 100644 lib/ui/views/course/course_view.dart create mode 100644 lib/ui/views/course/course_viewmodel.dart create mode 100644 lib/ui/views/course_module/course_module_view.dart create mode 100644 lib/ui/views/course_module/course_module_viewmodel.dart create mode 100644 lib/ui/views/course_payment/course_payment_view.dart create mode 100644 lib/ui/views/course_payment/course_payment_viewmodel.dart create mode 100644 lib/ui/views/course_practice/course_practice_view.dart create mode 100644 lib/ui/views/course_practice/course_practice_viewmodel.dart create mode 100644 lib/ui/widgets/course_card.dart create mode 100644 lib/ui/widgets/course_module_tile.dart create mode 100644 lib/ui/widgets/course_payment_card.dart create mode 100644 lib/ui/widgets/course_practice_card.dart rename lib/ui/widgets/{course_level_card.dart => course_progress_card.dart} (94%) create mode 100644 test/helpers/test_helpers.mocks.dart create mode 100644 test/viewmodels/course_module_viewmodel_test.dart create mode 100644 test/viewmodels/course_payment_viewmodel_test.dart create mode 100644 test/viewmodels/course_practice_viewmodel_test.dart create mode 100644 test/viewmodels/course_viewmodel_test.dart diff --git a/lib/app/app.dart b/lib/app/app.dart index 50af9db..d4160d5 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -38,6 +38,10 @@ import 'package:yimaru_app/services/image_downloader_service.dart'; import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'; import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart'; import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'; +import 'package:yimaru_app/ui/views/course/course_view.dart'; +import 'package:yimaru_app/ui/views/course_module/course_module_view.dart'; +import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart'; +import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart'; // @stacked-import @StackedApp( @@ -69,6 +73,10 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'; MaterialRoute(page: ForgetPasswordView), MaterialRoute(page: LearnLessonDetailView), MaterialRoute(page: LearnPracticeView), + MaterialRoute(page: CourseView), + MaterialRoute(page: CourseModuleView), + MaterialRoute(page: CoursePracticeView), + MaterialRoute(page: CoursePaymentView), // @stacked-route ], dependencies: [ diff --git a/lib/app/app.router.dart b/lib/app/app.router.dart index ee9993e..2d3308c 100644 --- a/lib/app/app.router.dart +++ b/lib/app/app.router.dart @@ -5,15 +5,22 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/material.dart' as _i29; import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' as _i33; import 'package:stacked/stacked.dart' as _i1; -import 'package:stacked_services/stacked_services.dart' as _i30; +import 'package:stacked_services/stacked_services.dart' as _i34; import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' as _i10; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23; import 'package:yimaru_app/ui/views/call_support/call_support_view.dart' as _i13; +import 'package:yimaru_app/ui/views/course/course_view.dart' as _i29; +import 'package:yimaru_app/ui/views/course_module/course_module_view.dart' + as _i30; +import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart' + as _i32; +import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart' + as _i31; import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; import 'package:yimaru_app/ui/views/failure/failure_view.dart' as _i25; import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart' @@ -104,6 +111,14 @@ class Routes { static const learnPracticeView = '/learn-practice-view'; + static const courseView = '/course-view'; + + static const courseModuleView = '/course-module-view'; + + static const coursePracticeView = '/course-practice-view'; + + static const coursePaymentView = '/course-payment-view'; + static const all = { homeView, onboardingView, @@ -132,6 +147,10 @@ class Routes { forgetPasswordView, learnLessonDetailView, learnPracticeView, + courseView, + courseModuleView, + coursePracticeView, + coursePaymentView, }; } @@ -245,17 +264,33 @@ class StackedRouter extends _i1.RouterBase { Routes.learnPracticeView, page: _i28.LearnPracticeView, ), + _i1.RouteDef( + Routes.courseView, + page: _i29.CourseView, + ), + _i1.RouteDef( + Routes.courseModuleView, + page: _i30.CourseModuleView, + ), + _i1.RouteDef( + Routes.coursePracticeView, + page: _i31.CoursePracticeView, + ), + _i1.RouteDef( + Routes.coursePaymentView, + page: _i32.CoursePaymentView, + ), ]; final _pagesMap = { _i2.HomeView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i2.HomeView(), settings: data, ); }, _i3.OnboardingView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i3.OnboardingView(), settings: data, ); @@ -264,156 +299,156 @@ class StackedRouter extends _i1.RouterBase { final args = data.getArgs( orElse: () => const StartupViewArguments(), ); - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => _i4.StartupView(key: args.key, label: args.label), settings: data, ); }, _i5.ProfileView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i5.ProfileView(), settings: data, ); }, _i6.ProfileDetailView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i6.ProfileDetailView(), settings: data, ); }, _i7.DownloadsView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i7.DownloadsView(), settings: data, ); }, _i8.ProgressView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i8.ProgressView(), settings: data, ); }, _i9.OngoingProgressView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i9.OngoingProgressView(), settings: data, ); }, _i10.AccountPrivacyView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i10.AccountPrivacyView(), settings: data, ); }, _i11.SupportView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i11.SupportView(), settings: data, ); }, _i12.TelegramSupportView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i12.TelegramSupportView(), settings: data, ); }, _i13.CallSupportView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i13.CallSupportView(), settings: data, ); }, _i14.LanguageView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i14.LanguageView(), settings: data, ); }, _i15.PrivacyPolicyView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i15.PrivacyPolicyView(), settings: data, ); }, _i16.TermsAndConditionsView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i16.TermsAndConditionsView(), settings: data, ); }, _i17.RegisterView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i17.RegisterView(), settings: data, ); }, _i18.LoginView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i18.LoginView(), settings: data, ); }, _i19.LearnView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i19.LearnView(), settings: data, ); }, _i20.LearnLevelView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i20.LearnLevelView(), settings: data, ); }, _i21.LearnModuleView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i21.LearnModuleView(), settings: data, ); }, _i22.WelcomeView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i22.WelcomeView(), settings: data, ); }, _i23.AssessmentView: (data) { final args = data.getArgs(nullOk: false); - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => _i23.AssessmentView(key: args.key, data: args.data), settings: data, ); }, _i24.LearnLessonView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i24.LearnLessonView(), settings: data, ); }, _i25.FailureView: (data) { final args = data.getArgs(nullOk: false); - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => _i25.FailureView(key: args.key, label: args.label), settings: data, ); }, _i26.ForgetPasswordView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i26.ForgetPasswordView(), settings: data, ); }, _i27.LearnLessonDetailView: (data) { - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => const _i27.LearnLessonDetailView(), settings: data, ); }, _i28.LearnPracticeView: (data) { final args = data.getArgs(nullOk: false); - return _i29.MaterialPageRoute( + return _i33.MaterialPageRoute( builder: (context) => _i28.LearnPracticeView( key: args.key, title: args.title, @@ -422,6 +457,30 @@ class StackedRouter extends _i1.RouterBase { settings: data, ); }, + _i29.CourseView: (data) { + return _i33.MaterialPageRoute( + builder: (context) => const _i29.CourseView(), + settings: data, + ); + }, + _i30.CourseModuleView: (data) { + return _i33.MaterialPageRoute( + builder: (context) => const _i30.CourseModuleView(), + settings: data, + ); + }, + _i31.CoursePracticeView: (data) { + return _i33.MaterialPageRoute( + builder: (context) => const _i31.CoursePracticeView(), + settings: data, + ); + }, + _i32.CoursePaymentView: (data) { + return _i33.MaterialPageRoute( + builder: (context) => const _i32.CoursePaymentView(), + settings: data, + ); + }, }; @override @@ -437,7 +496,7 @@ class StartupViewArguments { this.label = 'Loading', }); - final _i29.Key? key; + final _i33.Key? key; final String label; @@ -464,7 +523,7 @@ class AssessmentViewArguments { required this.data, }); - final _i29.Key? key; + final _i33.Key? key; final Map data; @@ -491,7 +550,7 @@ class FailureViewArguments { required this.label, }); - final _i29.Key? key; + final _i33.Key? key; final String label; @@ -520,7 +579,7 @@ class LearnPracticeViewArguments { required this.buttonLabel, }); - final _i29.Key? key; + final _i33.Key? key; final String title; @@ -551,7 +610,7 @@ class LearnPracticeViewArguments { } } -extension NavigatorStateExtension on _i30.NavigationService { +extension NavigatorStateExtension on _i34.NavigationService { Future navigateToHomeView([ int? routerId, bool preventDuplicates = true, @@ -581,7 +640,7 @@ extension NavigatorStateExtension on _i30.NavigationService { } Future navigateToStartupView({ - _i29.Key? key, + _i33.Key? key, String label = 'Loading', int? routerId, bool preventDuplicates = true, @@ -850,7 +909,7 @@ extension NavigatorStateExtension on _i30.NavigationService { } Future navigateToAssessmentView({ - _i29.Key? key, + _i33.Key? key, required Map data, int? routerId, bool preventDuplicates = true, @@ -881,7 +940,7 @@ extension NavigatorStateExtension on _i30.NavigationService { } Future navigateToFailureView({ - _i29.Key? key, + _i33.Key? key, required String label, int? routerId, bool preventDuplicates = true, @@ -926,7 +985,7 @@ extension NavigatorStateExtension on _i30.NavigationService { } Future navigateToLearnPracticeView({ - _i29.Key? key, + _i33.Key? key, required String title, required String subtitle, required String buttonLabel, @@ -948,6 +1007,62 @@ extension NavigatorStateExtension on _i30.NavigationService { transition: transition); } + Future navigateToCourseView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.courseView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToCourseModuleView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.courseModuleView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToCoursePracticeView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.coursePracticeView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToCoursePaymentView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.coursePaymentView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future replaceWithHomeView([ int? routerId, bool preventDuplicates = true, @@ -977,7 +1092,7 @@ extension NavigatorStateExtension on _i30.NavigationService { } Future replaceWithStartupView({ - _i29.Key? key, + _i33.Key? key, String label = 'Loading', int? routerId, bool preventDuplicates = true, @@ -1246,7 +1361,7 @@ extension NavigatorStateExtension on _i30.NavigationService { } Future replaceWithAssessmentView({ - _i29.Key? key, + _i33.Key? key, required Map data, int? routerId, bool preventDuplicates = true, @@ -1277,7 +1392,7 @@ extension NavigatorStateExtension on _i30.NavigationService { } Future replaceWithFailureView({ - _i29.Key? key, + _i33.Key? key, required String label, int? routerId, bool preventDuplicates = true, @@ -1322,7 +1437,7 @@ extension NavigatorStateExtension on _i30.NavigationService { } Future replaceWithLearnPracticeView({ - _i29.Key? key, + _i33.Key? key, required String title, required String subtitle, required String buttonLabel, @@ -1343,4 +1458,60 @@ extension NavigatorStateExtension on _i30.NavigationService { parameters: parameters, transition: transition); } + + Future replaceWithCourseView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.courseView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithCourseModuleView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.courseModuleView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithCoursePracticeView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.coursePracticeView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithCoursePaymentView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.coursePaymentView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } } diff --git a/lib/models/assessment.dart b/lib/models/assessment.dart index 9a6da65..8a3dd1a 100644 --- a/lib/models/assessment.dart +++ b/lib/models/assessment.dart @@ -10,6 +10,9 @@ class Assessment { final String? status; + final List