From d48a6be61ab1bf968d9992b6dc2693ce87f04d89 Mon Sep 17 00:00:00 2001 From: BisratHailu Date: Thu, 14 May 2026 15:59:41 +0300 Subject: [PATCH] feat: Add localization according to the UAT comments --- assets/translations/am.json | 85 ++++++ assets/translations/en.json | 83 ++++++ lib/app/app.dart | 2 + lib/app/app.locator.dart | 2 + lib/app/app.router.dart | 33 ++- lib/main.dart | 29 +- lib/models/course_module.dart | 48 ++++ lib/models/course_module.g.dart | 34 +++ lib/models/course_unit.dart | 80 ++++++ lib/models/course_unit.g.dart | 38 +++ lib/services/api_service.dart | 50 ++++ lib/services/course_service.dart | 67 ++++- lib/services/learn_service.dart | 4 +- lib/services/localization_service.dart | 47 +++ lib/ui/common/app_constants.dart | 2 + lib/ui/common/enmus.dart | 3 +- .../common/translations/codegen_loader.g.dart | 182 ++++++++++++ lib/ui/common/translations/locale_keys.g.dart | 85 ++++++ .../assessment/assessment_viewmodel.dart | 12 +- .../screens/assessment_intro_screen.dart | 3 +- .../screens/assessment_result_screen.dart | 4 +- .../screens/start_lesson_screen.dart | 2 + lib/ui/views/course/course_viewmodel.dart | 4 +- .../course_catalog/course_catalog_view.dart | 14 +- .../course_catalog_viewmodel.dart | 24 +- .../course_practice_question_viewmodel.dart | 3 + .../screens/question_loading_screen.dart | 5 +- .../views/course_unit/course_unit_view.dart | 149 +++++++++- .../course_unit/course_unit_viewmodel.dart | 54 +++- .../forget_password_viewmodel.dart | 19 +- .../screens/request_reset_code_screen.dart | 6 +- .../screens/reset_password_screen.dart | 6 +- lib/ui/views/language/language_view.dart | 67 +++-- lib/ui/views/language/language_viewmodel.dart | 38 +-- .../views/learn_module/learn_module_view.dart | 4 +- lib/ui/views/login/login_viewmodel.dart | 15 +- .../views/login/screens/login_otp_screen.dart | 8 +- .../screens/login_with_email_screen.dart | 22 +- .../login_with_phone_number_screen.dart | 6 +- .../onboarding/onboarding_viewmodel.dart | 36 +-- .../screens/age_group_form_screen.dart | 4 +- .../screens/challenge_form_screen.dart | 4 +- .../screens/country_region_form_screen.dart | 3 +- .../educational_background_form_screen.dart | 3 +- .../screens/full_name_form_screen.dart | 3 +- .../screens/gender_form_screen.dart | 4 +- .../screens/language_goal_form_screen.dart | 3 +- .../screens/learning_goal_form_screen.dart | 4 +- .../screens/occupation_form_screen.dart | 4 +- .../onboarding/screens/topic_form_screen.dart | 4 +- lib/ui/views/register/register_viewmodel.dart | 24 +- .../screens/create_password_screen.dart | 6 +- .../screens/register_with_email_screen.dart | 6 +- .../register_with_phone_number_screen.dart | 6 +- .../screens/registration_otp_screen.dart | 2 + lib/ui/views/startup/startup_viewmodel.dart | 8 +- lib/ui/widgets/assessment_loading_screen.dart | 2 +- lib/ui/widgets/course_catalog_tile.dart | 6 +- ...ile.dart => course_module_tile_small.dart} | 9 +- lib/ui/widgets/course_unit_tile.dart | 267 ++++++++++++++++++ lib/ui/widgets/large_app_bar.dart | 4 +- lib/ui/widgets/option_text_divider.dart | 8 +- ...rn_progress.dart => overall_progress.dart} | 8 +- lib/ui/widgets/register_for_account.dart | 14 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 77 +++++ pubspec.yaml | 6 +- test/helpers/test_helpers.dart | 10 + test/services/localization_service_test.dart | 11 + 69 files changed, 1703 insertions(+), 184 deletions(-) create mode 100644 assets/translations/am.json create mode 100644 assets/translations/en.json create mode 100644 lib/models/course_module.dart create mode 100644 lib/models/course_module.g.dart create mode 100644 lib/models/course_unit.dart create mode 100644 lib/models/course_unit.g.dart create mode 100644 lib/services/localization_service.dart create mode 100644 lib/ui/common/translations/codegen_loader.g.dart create mode 100644 lib/ui/common/translations/locale_keys.g.dart rename lib/ui/widgets/{course_topic_tile.dart => course_module_tile_small.dart} (86%) create mode 100644 lib/ui/widgets/course_unit_tile.dart rename lib/ui/widgets/{overall_learn_progress.dart => overall_progress.dart} (92%) create mode 100644 test/services/localization_service_test.dart diff --git a/assets/translations/am.json b/assets/translations/am.json new file mode 100644 index 0000000..b002ba2 --- /dev/null +++ b/assets/translations/am.json @@ -0,0 +1,85 @@ +{ + + "welcome_back": "እንኳን በደህና ተመለሱ", + "dont_have_account": "መለያ የለዎትም? ይመዝገቡ", + "email": "ኢሜይል", + "password": "የይለፍ ቃል", + "forgot_password": "የይለፍ ቃል ረሱ?", + "cont": "ቀጥል", + "register": "ይመዝገቡ", + "login_with_google": "በጉግል ይግቡ", + "or": "ወይም", + "login_with_phone": "በስልክ ቁጥር ይግቡ", + "create_account": "አዲስ መለያ ይፍጠሩ", + "already_have_account": "መለያ አለዎት?", + "login": " ይግቡ ", + "register_with_google": "በጉግል ይመዝገቡ", + "register_with_phone": "በስልክ ቁጥር ይመዝገቡ", + "enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።", + "login_with_email": "በኢሜይል ይግቡ", + "create_password": "የይለፍ ቃል ይፍጠሩ", + "confirm_password": "የይለፍ ቃል ያረጋግጡ", + "sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።" , + "reset_password": " የይለፍ ቃልን ይቀይሩ ", + "enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።" , + "please_wait": "እባክዎ ይጠብቁ", + "reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል" , + "reset_code": " የመቀየሪያ ኮድ ", + "new_password": "አዲስ የይለፍ ቃል", + "logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል", + "view_course": " ኮርሱን ይመልከቱ ", + "take_practice": " ልምምድ ያድርጉ ", + "your_current_level": "የአሁኑ ደረጃዎ", + "overall_progress": "አጠቃላይ እድገት", + "great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው ", + "view_module": "ሞጁሉን ይመልከቱ", + "progress": "እድገት", + "keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ", + "lessons_in_module": " በዚህ ሞጁል ውስጥ ያሉ ትምህርቶች ", + "practice": "ልምምድ", + "start": "ጀምር", + "in_progress": "በሂደት ላይ", + "hello": "ሰላም", + "ready_to_learn": " ዛሬ እንግሊዝኛ ለመማር ተዘጋጅተዋል? ", + "learn": "ይማሩ ", + "course": "ኮርስ", + "profile": " ፕሮፋይል ", + "speaking_partner": "የንግግር ጓደኛ ", + "practice_what_you_learned": "አሁን የተማሩትን እንለማመድ", + "practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ", + "start_practice": "ልምምድ ጀምር", + "almost_there": "ሊጨርሱ ተቃርበዋል", + "finish_session": "እድገትዎን ለማየት ክፍለ ጊዜውን ያጠናቅቁ", + "continue_practice": "ልምምዱን ይቀጥሉ", + "end_session": "ክፍለ ጊዜውን ያብቁ ", + "tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ", + "practice_speaking": "ንግግርን ይለማመዱ ", + "tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ", + "reply": "እንደገና አዳምጥ", + "cancel": "ይቅር", + "you_are_speaking": "እየተናገሩ ነው", + "practice_completed": "ልምምዱ ተጠናቅቋል", + "great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው", + "practice_again": "እንደገና ይለማመዱ", + "conversation_review": "የንግግር ግምገማ ", + "result": "ውጤት", + "quick_tip": "ጠቃሚ ምክር", + "retry": "እንደገና ይሞክሩ", + "completed_a1": "እንኳን ደስ አለዎት! A1 ደረጃን አጠናቅቀዋል", + "analyzing_speaking": "የንግግር ችሎታዎን እየገመገምን ነው", + "view_profile": "ፕሮፋይሎን ይመልከቱ ", + "hi": "ሰላም", + "edit_profile": "መገለጫ ያስተካክሉ", + "first_name": "የመጀመሪያ ስም", + "last_name": "የአባት ስም", + "gender": "ፆታ", + "male": "ወንድ", + "female": "ሴት", + "phone_number": "የስልክ ቁጥር", + "country": "ሀገር", + "region": "ክልል", + "occupation": "የስራ መስክ ", + "save_changes": "ለውጦችን ያስቀምጡ" + +} + diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 0000000..c71689d --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,83 @@ + + { + "welcome_back": "Welcome back", + "dont_have_account": "Don't have an account? Register", + "email": "Email", + "password": "Password", + "forgot_password": "Forgot password?", + "cont": "Continue", + "register": "Register", + "login_with_google": "Login with Google", + "or": "Or", + "login_with_phone": "Login with phone number", + "create_account": "Create an account", + "already_have_account": "Already have an account?", + "login": "Login", + "register_with_google": "Register with Google", + "register_with_phone": "Register with phone number", + "enter_phone_number": "Enter your phone number. We will send you a confirmation code there.", + "login_with_email": "Login with email", + "create_password": "Create password", + "confirm_password": "Confirm password", + "sign_up_agreement": "By clicking ‘Sign Up’, you agree to our ‘Terms of Service’ and ‘Privacy Policy’", + "reset_password": "Reset Password", + "enter_email_reset_code": "Enter your email. We will send you a reset code.", + "please_wait": "Please wait", + "reset_code_sent": "Reset code sent successfully", + "reset_code": "Reset code", + "new_password": "New password", + "logged_in_successfully": "Logged in successfully", + "view_course": "View course", + "take_practice": "Take practice", + "your_current_level": "Your current level", + "overall_progress": "Overall progress", + "great_work": "Keep up the great work! You're doing amazing", + "view_module": "View module", + "progress": "Progress", + "keep_going": "Let's keep going - you're more than half there", + "lessons_in_module": "Lessons in this module", + "practice": "Practice", + "start": "Start", + "in_progress": "In Progress", + "hello": "Hello", + "ready_to_learn": "Ready to keep learning English today", + "learn": "Learn", + "course": "Course", + "profile": "Profile", + "speaking_partner": "Speaking partner", + "practice_what_you_learned": "Let's practice what you just learnt", + "practice_questions": "I will ask you a few questions and you can respond", + "start_practice": "Start practice", + "almost_there": "You're almost there", + "finish_session": "Finish the session to see your progress", + "continue_practice": "Continue practice", + "end_session": "End session", + "tap_start_to_listen": "Tap the start button to listen", + "practice_speaking": "Practice speaking", + "tap_microphone": "Tap the microphone to speak", + "reply": "Reply", + "cancel": "Cancel", + "you_are_speaking": "You're speaking", + "practice_completed": "Practice completed", + "great_improvement": "You sound more confident this time, great improvement", + "practice_again": "Practice again", + "conversation_review": "Conversation review", + "result": "Result", + "quick_tip": "Quick tip", + "retry": "Retry", + "completed_a1": "Yay, you've completed A1", + "analyzing_speaking": "We're now analyzing your speaking skill", + "view_profile": "View profile", + "hi": "Hi", + "edit_profile": "Edit profile", + "first_name": "First name", + "last_name": "Last name", + "gender": "Gender", + "male": "Male", + "female": "Female", + "phone_number": "Phone number", + "country": "Country", + "region": "Region", + "occupation": "Occupation", + "save_changes": "Save changes" + } diff --git a/lib/app/app.dart b/lib/app/app.dart index 10fc195..49530dc 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -58,6 +58,7 @@ import 'package:yimaru_app/ui/views/arif_pay/arif_pay_view.dart'; import 'package:yimaru_app/services/learn_service.dart'; import 'package:yimaru_app/ui/views/course_catalog/course_catalog_view.dart'; import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart'; +import 'package:yimaru_app/services/localization_service.dart'; // @stacked-import @StackedApp( @@ -124,6 +125,7 @@ import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart'; LazySingleton(classType: UrlLauncherService), LazySingleton(classType: PhoneCallerService), LazySingleton(classType: LearnService), + LazySingleton(classType: LocalizationService), // @stacked-service ], bottomsheets: [ diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index a1330b4..c1c146a 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -22,6 +22,7 @@ import '../services/image_downloader_service.dart'; import '../services/image_picker_service.dart'; import '../services/in_app_update_service.dart'; import '../services/learn_service.dart'; +import '../services/localization_service.dart'; import '../services/notification_service.dart'; import '../services/permission_handler_service.dart'; import '../services/phone_caller_service.dart'; @@ -63,4 +64,5 @@ Future setupLocator( locator.registerLazySingleton(() => UrlLauncherService()); locator.registerLazySingleton(() => PhoneCallerService()); locator.registerLazySingleton(() => LearnService()); + locator.registerLazySingleton(() => LocalizationService()); } diff --git a/lib/app/app.router.dart b/lib/app/app.router.dart index 367b249..913371d 100644 --- a/lib/app/app.router.dart +++ b/lib/app/app.router.dart @@ -6,11 +6,12 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/material.dart' as _i39; import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' as _i39; import 'package:stacked/stacked.dart' as _i1; -import 'package:stacked_services/stacked_services.dart' as _i46; +import 'package:stacked_services/stacked_services.dart' as _i47; import 'package:yimaru_app/models/course.dart' as _i44; +import 'package:yimaru_app/models/course_catalog.dart' as _i46; import 'package:yimaru_app/models/course_lesson.dart' as _i45; import 'package:yimaru_app/models/learn_course.dart' as _i40; import 'package:yimaru_app/models/learn_lesson.dart' as _i42; @@ -664,11 +665,10 @@ class StackedRouter extends _i1.RouterBase { ); }, _i38.CourseUnitView: (data) { - final args = data.getArgs( - orElse: () => const CourseUnitViewArguments(), - ); + final args = data.getArgs(nullOk: false); return _i39.MaterialPageRoute( - builder: (context) => _i38.CourseUnitView(key: args.key), + builder: (context) => + _i38.CourseUnitView(key: args.key, catalog: args.catalog), settings: data, ); }, @@ -1586,28 +1586,33 @@ class CourseCatalogViewArguments { } class CourseUnitViewArguments { - const CourseUnitViewArguments({this.key}); + const CourseUnitViewArguments({ + this.key, + required this.catalog, + }); final _i39.Key? key; + final _i46.CourseCatalog catalog; + @override String toString() { - return '{"key": "$key"}'; + return '{"key": "$key", "catalog": "$catalog"}'; } @override bool operator ==(covariant CourseUnitViewArguments other) { if (identical(this, other)) return true; - return other.key == key; + return other.key == key && other.catalog == catalog; } @override int get hashCode { - return key.hashCode; + return key.hashCode ^ catalog.hashCode; } } -extension NavigatorStateExtension on _i46.NavigationService { +extension NavigatorStateExtension on _i47.NavigationService { Future navigateToHomeView({ _i39.Key? key, int? routerId, @@ -2216,6 +2221,7 @@ extension NavigatorStateExtension on _i46.NavigationService { Future navigateToCourseUnitView({ _i39.Key? key, + required _i46.CourseCatalog catalog, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -2223,7 +2229,7 @@ extension NavigatorStateExtension on _i46.NavigationService { transition, }) async { return navigateTo(Routes.courseUnitView, - arguments: CourseUnitViewArguments(key: key), + arguments: CourseUnitViewArguments(key: key, catalog: catalog), id: routerId, preventDuplicates: preventDuplicates, parameters: parameters, @@ -2838,6 +2844,7 @@ extension NavigatorStateExtension on _i46.NavigationService { Future replaceWithCourseUnitView({ _i39.Key? key, + required _i46.CourseCatalog catalog, int? routerId, bool preventDuplicates = true, Map? parameters, @@ -2845,7 +2852,7 @@ extension NavigatorStateExtension on _i46.NavigationService { transition, }) async { return replaceWith(Routes.courseUnitView, - arguments: CourseUnitViewArguments(key: key), + arguments: CourseUnitViewArguments(key: key, catalog: catalog), id: routerId, preventDuplicates: preventDuplicates, parameters: parameters, diff --git a/lib/main.dart b/lib/main.dart index 6031bc0..6c03ada 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,8 @@ import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.router.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/services/notification_service.dart'; - +import 'package:easy_localization/easy_localization.dart'; +import 'package:yimaru_app/ui/common/translations/codegen_loader.g.dart'; import 'firebase_options.dart'; Future main() async { @@ -15,26 +16,42 @@ Future main() async { await setupLocator(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await locator().initialize(); + await EasyLocalization.ensureInitialized(); setupDialogUi(); setupBottomSheetUi(); - runApp(const MainApp()); + runApp( + EasyLocalization( + supportedLocales: const [ + Locale('en'), + Locale('am'), + ], + path: 'assets/translations', + startLocale: const Locale('en'), + assetLoader: const CodegenLoader(), + fallbackLocale: const Locale('en'), + child: const MainApp(), + ), + ); } class MainApp extends StatelessWidget { const MainApp({super.key}); @override - Widget build(BuildContext context) => _buildMaterialWrapper(); + Widget build(BuildContext context) => _buildMaterialWrapper(context); - Widget _buildMaterialWrapper() => ToastificationWrapper( - child: _buildMaterialApp(), + Widget _buildMaterialWrapper(BuildContext context) => ToastificationWrapper( + child: _buildMaterialApp(context), ); - Widget _buildMaterialApp() => MaterialApp( + Widget _buildMaterialApp(BuildContext context) => MaterialApp( + locale: context.locale, initialRoute: Routes.startupView, theme: ThemeData(fontFamily: 'Aeonik'), navigatorKey: StackedService.navigatorKey, + supportedLocales: context.supportedLocales, onGenerateRoute: StackedRouter().onGenerateRoute, navigatorObservers: [StackedService.routeObserver], + localizationsDelegates: context.localizationDelegates, ); } diff --git a/lib/models/course_module.dart b/lib/models/course_module.dart new file mode 100644 index 0000000..a91f710 --- /dev/null +++ b/lib/models/course_module.dart @@ -0,0 +1,48 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'course_module.g.dart'; + +@JsonSerializable() +class CourseModule { + final int? id; + + final String? name; + + final String? icon; + + final String? thumbnail; + + final String? description; + + @JsonKey(name: 'unit_id') + final int? unitId; + + @JsonKey(name: 'sort_order') + final int? sortOrder; + + @JsonKey(name: 'has_practice') + final bool? hasPractice; + + @JsonKey(name: 'lessons_count') + final int? lessonsCount; + + @JsonKey(name: 'practices_count') + final int? practice; + + const CourseModule( + {this.id, + this.icon, + this.name, + this.unitId, + this.practice, + this.thumbnail, + this.sortOrder, + this.hasPractice, + this.description, + this.lessonsCount}); + + factory CourseModule.fromJson(Map json) => + _$CourseModuleFromJson(json); + + Map toJson() => _$CourseModuleToJson(this); +} diff --git a/lib/models/course_module.g.dart b/lib/models/course_module.g.dart new file mode 100644 index 0000000..ff2b5df --- /dev/null +++ b/lib/models/course_module.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'course_module.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CourseModule _$CourseModuleFromJson(Map json) => CourseModule( + id: (json['id'] as num?)?.toInt(), + icon: json['icon'] as String?, + name: json['name'] as String?, + unitId: (json['unit_id'] as num?)?.toInt(), + practice: (json['practices_count'] as num?)?.toInt(), + thumbnail: json['thumbnail'] as String?, + sortOrder: (json['sort_order'] as num?)?.toInt(), + hasPractice: json['has_practice'] as bool?, + description: json['description'] as String?, + lessonsCount: (json['lessons_count'] as num?)?.toInt(), + ); + +Map _$CourseModuleToJson(CourseModule instance) => + { + 'id': instance.id, + 'name': instance.name, + 'icon': instance.icon, + 'thumbnail': instance.thumbnail, + 'description': instance.description, + 'unit_id': instance.unitId, + 'sort_order': instance.sortOrder, + 'has_practice': instance.hasPractice, + 'lessons_count': instance.lessonsCount, + 'practices_count': instance.practice, + }; diff --git a/lib/models/course_unit.dart b/lib/models/course_unit.dart new file mode 100644 index 0000000..07dae95 --- /dev/null +++ b/lib/models/course_unit.dart @@ -0,0 +1,80 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:yimaru_app/models/course_module.dart'; + +part 'course_unit.g.dart'; + +@JsonSerializable() +class CourseUnit { + final int? id; + + final String? name; + + final String? thumbnail; + + final String? description; + + final List? modules; + + @JsonKey(name: 'sort_order') + final int? sortOrder; + + @JsonKey(name: 'has_practice') + final bool? hasPractice; + + @JsonKey(name: 'lessons_count') + final int? lessonsCount; + + @JsonKey(name: 'modules_count') + final int? modulesCount; + + @JsonKey(name: 'practices_count') + final int? practice; + + @JsonKey(name: 'catalog_course_id') + final int? catalogCourseId; + + const CourseUnit( + {this.id, + this.name, + this.modules, + this.practice, + this.thumbnail, + this.sortOrder, + this.hasPractice, + this.description, + this.modulesCount, + this.lessonsCount, + this.catalogCourseId}); + + factory CourseUnit.fromJson(Map json) => + _$CourseUnitFromJson(json); + + Map toJson() => _$CourseUnitToJson(this); + + CourseUnit copyWith({ + int? id, + String? name, + int? practice, + int? sortOrder, + bool? hasPractice, + int? lessonsCount, + int? modulesCount, + String? thumbnail, + String? description, + int? catalogCourseId, + List? modules, + }) => + CourseUnit( + id: id ?? this.id, + name: name ?? this.name, + modules: modules ?? this.modules, + practice: practice ?? this.practice, + thumbnail: thumbnail ?? this.thumbnail, + sortOrder: sortOrder ?? this.sortOrder, + hasPractice: hasPractice ?? this.hasPractice, + description: description ?? this.description, + lessonsCount: lessonsCount ?? this.lessonsCount, + modulesCount: modulesCount ?? this.modulesCount, + catalogCourseId: catalogCourseId ?? this.catalogCourseId, + ); +} diff --git a/lib/models/course_unit.g.dart b/lib/models/course_unit.g.dart new file mode 100644 index 0000000..a350a89 --- /dev/null +++ b/lib/models/course_unit.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'course_unit.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CourseUnit _$CourseUnitFromJson(Map json) => CourseUnit( + id: (json['id'] as num?)?.toInt(), + name: json['name'] as String?, + modules: (json['modules'] as List?) + ?.map((e) => CourseModule.fromJson(e as Map)) + .toList(), + practice: (json['practices_count'] as num?)?.toInt(), + thumbnail: json['thumbnail'] as String?, + sortOrder: (json['sort_order'] as num?)?.toInt(), + hasPractice: json['has_practice'] as bool?, + description: json['description'] as String?, + modulesCount: (json['modules_count'] as num?)?.toInt(), + lessonsCount: (json['lessons_count'] as num?)?.toInt(), + catalogCourseId: (json['catalog_course_id'] as num?)?.toInt(), + ); + +Map _$CourseUnitToJson(CourseUnit instance) => + { + 'id': instance.id, + 'name': instance.name, + 'thumbnail': instance.thumbnail, + 'description': instance.description, + 'modules': instance.modules, + 'sort_order': instance.sortOrder, + 'has_practice': instance.hasPractice, + 'lessons_count': instance.lessonsCount, + 'modules_count': instance.modulesCount, + 'practices_count': instance.practice, + 'catalog_course_id': instance.catalogCourseId, + }; diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 3469704..ef9d48c 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -14,6 +14,8 @@ import 'package:yimaru_app/services/dio_service.dart'; import 'package:yimaru_app/ui/common/app_constants.dart'; import '../app/app.locator.dart'; +import '../models/course_module.dart'; +import '../models/course_unit.dart'; import '../models/learn_course.dart'; import '../models/learn_module.dart'; import '../models/learn_question.dart'; @@ -729,6 +731,54 @@ class ApiService { } } + // Get course units + Future> getCourseUnits(int id) async { + try { + List units = []; + + final Response response = await _service.dio.get( + '$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kCatalogCoursesUrl/$id/$kUnitsUrl'); + + if (response.statusCode == 200) { + var data = response.data; + var decodedData = data['data']['units'] as List; + units = decodedData.map( + (e) { + return CourseUnit.fromJson(e); + }, + ).toList(); + return units; + } + return []; + } catch (e) { + return []; + } + } + + // Get course modules + Future> getCourseModules(int id) async { + try { + List modules = []; + + final Response response = await _service.dio.get( + '$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kUnitsUrl/$id/$kModulesUrl'); + + if (response.statusCode == 200) { + var data = response.data; + var decodedData = data['data']['modules'] as List; + modules = decodedData.map( + (e) { + return CourseModule.fromJson(e); + }, + ).toList(); + return modules; + } + return []; + } catch (e) { + return []; + } + } + /* TO BE MODIFIED*/ // Get courses diff --git a/lib/services/course_service.dart b/lib/services/course_service.dart index d3474bb..242dd2e 100644 --- a/lib/services/course_service.dart +++ b/lib/services/course_service.dart @@ -1,13 +1,78 @@ +import 'package:flutter_html/flutter_html.dart'; +import 'package:stacked/stacked.dart'; import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/models/course_progress.dart'; import 'package:yimaru_app/services/api_service.dart'; +import '../models/course_catalog.dart'; import '../models/course_detail.dart'; +import '../models/course_module.dart'; +import '../models/course_unit.dart'; -class CourseService { +class CourseService with ListenableServiceMixin { // Dependency injection final _apiService = locator(); + // Initialization + courseService() { + listenToReactiveValues([_catalogs]); + } + + // Course catalogs + List _catalogs = []; + + List get catalogs => _catalogs; + + // Course units + List _units = []; + + List get units => _units; + + // Course modules + List _modules = []; + + List get modules => _modules; + + // Course catalogs + Future getCourseCatalogs() async { + _catalogs = await _apiService.getCourseCatalogs(); + _catalogs.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0)); + notifyListeners(); + } + + // Course units + Future getCourseUnits(int id) async { + _units = await _apiService.getCourseUnits(id); + _units.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0)); + notifyListeners(); + } + + // Course modules + Future getCourseUnitModule({ + required int id, + required int index, + }) async { + List modules = await _apiService.getCourseModules(id); + + modules.sort( + (a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0), + ); + + final updatedUnit = _units[index].copyWith( + modules: modules, + ); + + _units[index] = updatedUnit; + + notifyListeners(); + } + + Future getCourseModules(int id) async { + _modules = await _apiService.getCourseModules(id); + _modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0)); + notifyListeners(); + } + // Get course detail Future> getCoursesDetail(int id) async { final courses = await _apiService.getCourses(id); diff --git a/lib/services/learn_service.dart b/lib/services/learn_service.dart index 2630296..1c978b4 100644 --- a/lib/services/learn_service.dart +++ b/lib/services/learn_service.dart @@ -12,8 +12,8 @@ class LearnService with ListenableServiceMixin { final _apiService = locator(); // Initialization - LearnLessonService() { - listenToReactiveValues([_programs, _lessons]); + learnService() { + listenToReactiveValues([_programs, _lessons, _modules]); } // Learn program diff --git a/lib/services/localization_service.dart b/lib/services/localization_service.dart new file mode 100644 index 0000000..a7860bc --- /dev/null +++ b/lib/services/localization_service.dart @@ -0,0 +1,47 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +class LocalizationService with ListenableServiceMixin { + // Initialization + localizationService() { + listenToReactiveValues([_selectedLanguage]); + } + + // Languages + Map _selectedLanguage = { + 'code': 'EN', + 'language': 'English' + }; + + Map get selectedLanguage => _selectedLanguage; + + final List> _languages = [ + {'code': 'አማ', 'language': 'አማርኛ'}, + {'code': 'EN', 'language': 'English'}, + ]; + + List> get languages => _languages; + + bool isSelectedLanguage(String title) => + _selectedLanguage['language'] == title; + + Future setSelectedLanguage( + {required BuildContext context, + required Map title}) async { + _selectedLanguage = title; + + if (title['code'] == 'አማ') { + await setAmharicLanguage(context); + } else { + await setAmharicLanguage(context); + } + notifyListeners(); + } + + Future setAmharicLanguage(BuildContext context) async => + await context.setLocale(const Locale('am')); + + Future setEnglishLanguage(BuildContext context) async => + await context.setLocale(const Locale('en')); +} diff --git a/lib/ui/common/app_constants.dart b/lib/ui/common/app_constants.dart index 6cb4eb9..4b6388a 100644 --- a/lib/ui/common/app_constants.dart +++ b/lib/ui/common/app_constants.dart @@ -3,6 +3,8 @@ String kBaseUrl = 'https://api.yimaruacademy.com'; String kApiUrl = 'api'; +String kUnitsUrl = 'units'; + String kApiVersionUrl = 'v1'; String kLevelsUrl = 'levels'; diff --git a/lib/ui/common/enmus.dart b/lib/ui/common/enmus.dart index 789f4d0..8b1ec9a 100644 --- a/lib/ui/common/enmus.dart +++ b/lib/ui/common/enmus.dart @@ -32,6 +32,7 @@ enum StateObjects { register, verifyOtp, resendOtp, + courseUnits, assessments, startupView, learnLessons, @@ -39,6 +40,7 @@ enum StateObjects { learnCourses, profileImage, learnPrograms, + courseModules, courseLessons, profileUpdate, resetPassword, @@ -49,7 +51,6 @@ enum StateObjects { loginWithGoogle, loadLessonVideo, loadCourseVideo, - learnSubmodules, requestResetCode, profileCompletion, learnSubscription, diff --git a/lib/ui/common/translations/codegen_loader.g.dart b/lib/ui/common/translations/codegen_loader.g.dart new file mode 100644 index 0000000..b555b87 --- /dev/null +++ b/lib/ui/common/translations/codegen_loader.g.dart @@ -0,0 +1,182 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +// ignore_for_file: prefer_single_quotes, avoid_renaming_method_parameters, constant_identifier_names + +import 'dart:ui'; + +import 'package:easy_localization/easy_localization.dart' show AssetLoader; + +class CodegenLoader extends AssetLoader{ + const CodegenLoader(); + + @override + Future?> load(String path, Locale locale) { + return Future.value(mapLocales[locale.toString()]); + } + + static const Map _am = { + "welcome_back": "እንኳን በደህና ተመለሱ", + "dont_have_account": "መለያ የለዎትም? ይመዝገቡ", + "email": "ኢሜይል", + "password": "የይለፍ ቃል", + "forgot_password": "የይለፍ ቃል ረሱ?", + "cont": "ቀጥል", + "register": "ይመዝገቡ", + "login_with_google": "በጉግል ይግቡ", + "or": "ወይም", + "login_with_phone": "በስልክ ቁጥር ይግቡ", + "create_account": "አዲስ መለያ ይፍጠሩ", + "already_have_account": "መለያ አለዎት?", + "login": " ይግቡ ", + "register_with_google": "በጉግል ይመዝገቡ", + "register_with_phone": "በስልክ ቁጥር ይመዝገቡ", + "enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።", + "login_with_email": "በኢሜይል ይግቡ", + "create_password": "የይለፍ ቃል ይፍጠሩ", + "confirm_password": "የይለፍ ቃል ያረጋግጡ", + "sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።", + "reset_password": " የይለፍ ቃልን ይቀይሩ ", + "enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።", + "please_wait": "እባክዎ ይጠብቁ", + "reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል", + "reset_code": " የመቀየሪያ ኮድ ", + "new_password": "አዲስ የይለፍ ቃል", + "logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል", + "view_course": " ኮርሱን ይመልከቱ ", + "take_practice": " ልምምድ ያድርጉ ", + "your_current_level": "የአሁኑ ደረጃዎ", + "overall_progress": "አጠቃላይ እድገት", + "great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው ", + "view_module": "ሞጁሉን ይመልከቱ", + "progress": "እድገት", + "keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ", + "lessons_in_module": " በዚህ ሞጁል ውስጥ ያሉ ትምህርቶች ", + "practice": "ልምምድ", + "start": "ጀምር", + "in_progress": "በሂደት ላይ", + "hello": "ሰላም", + "ready_to_learn": " ዛሬ እንግሊዝኛ ለመማር ተዘጋጅተዋል? ", + "learn": "ይማሩ ", + "course": "ኮርስ", + "profile": " ፕሮፋይል ", + "speaking_partner": "የንግግር ጓደኛ ", + "practice_what_you_learned": "አሁን የተማሩትን እንለማመድ", + "practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ", + "start_practice": "ልምምድ ጀምር", + "almost_there": "ሊጨርሱ ተቃርበዋል", + "finish_session": "እድገትዎን ለማየት ክፍለ ጊዜውን ያጠናቅቁ", + "continue_practice": "ልምምዱን ይቀጥሉ", + "end_session": "ክፍለ ጊዜውን ያብቁ ", + "tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ", + "practice_speaking": "ንግግርን ይለማመዱ ", + "tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ", + "reply": "እንደገና አዳምጥ", + "cancel": "ይቅር", + "you_are_speaking": "እየተናገሩ ነው", + "practice_completed": "ልምምዱ ተጠናቅቋል", + "great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው", + "practice_again": "እንደገና ይለማመዱ", + "conversation_review": "የንግግር ግምገማ ", + "result": "ውጤት", + "quick_tip": "ጠቃሚ ምክር", + "retry": "እንደገና ይሞክሩ", + "completed_a1": "እንኳን ደስ አለዎት! A1 ደረጃን አጠናቅቀዋል", + "analyzing_speaking": "የንግግር ችሎታዎን እየገመገምን ነው", + "view_profile": "ፕሮፋይሎን ይመልከቱ ", + "hi": "ሰላም", + "edit_profile": "መገለጫ ያስተካክሉ", + "first_name": "የመጀመሪያ ስም", + "last_name": "የአባት ስም", + "gender": "ፆታ", + "male": "ወንድ", + "female": "ሴት", + "phone_number": "የስልክ ቁጥር", + "country": "ሀገር", + "region": "ክልል", + "occupation": "የስራ መስክ ", + "save_changes": "ለውጦችን ያስቀምጡ" +}; +static const Map _en = { + "welcome_back": "Welcome back", + "dont_have_account": "Don't have an account? Register", + "email": "Email", + "password": "Password", + "forgot_password": "Forgot password?", + "cont": "Continue", + "register": "Register", + "login_with_google": "Login with Google", + "or": "Or", + "login_with_phone": "Login with phone number", + "create_account": "Create an account", + "already_have_account": "Already have an account?", + "login": "Login", + "register_with_google": "Register with Google", + "register_with_phone": "Register with phone number", + "enter_phone_number": "Enter your phone number. We will send you a confirmation code there.", + "login_with_email": "Login with email", + "create_password": "Create password", + "confirm_password": "Confirm password", + "sign_up_agreement": "By clicking ‘Sign Up’, you agree to our ‘Terms of Service’ and ‘Privacy Policy’", + "reset_password": "Reset Password", + "enter_email_reset_code": "Enter your email. We will send you a reset code.", + "please_wait": "Please wait", + "reset_code_sent": "Reset code sent successfully", + "reset_code": "Reset code", + "new_password": "New password", + "logged_in_successfully": "Logged in successfully", + "view_course": "View course", + "take_practice": "Take practice", + "your_current_level": "Your current level", + "overall_progress": "Overall progress", + "great_work": "Keep up the great work! You're doing amazing", + "view_module": "View module", + "progress": "Progress", + "keep_going": "Let's keep going - you're more than half there", + "lessons_in_module": "Lessons in this module", + "practice": "Practice", + "start": "Start", + "in_progress": "In Progress", + "hello": "Hello", + "ready_to_learn": "Ready to keep learning English today", + "learn": "Learn", + "course": "Course", + "profile": "Profile", + "speaking_partner": "Speaking partner", + "practice_what_you_learned": "Let's practice what you just learnt", + "practice_questions": "I will ask you a few questions and you can respond", + "start_practice": "Start practice", + "almost_there": "You're almost there", + "finish_session": "Finish the session to see your progress", + "continue_practice": "Continue practice", + "end_session": "End session", + "tap_start_to_listen": "Tap the start button to listen", + "practice_speaking": "Practice speaking", + "tap_microphone": "Tap the microphone to speak", + "reply": "Reply", + "cancel": "Cancel", + "you_are_speaking": "You're speaking", + "practice_completed": "Practice completed", + "great_improvement": "You sound more confident this time, great improvement", + "practice_again": "Practice again", + "conversation_review": "Conversation review", + "result": "Result", + "quick_tip": "Quick tip", + "retry": "Retry", + "completed_a1": "Yay, you've completed A1", + "analyzing_speaking": "We're now analyzing your speaking skill", + "view_profile": "View profile", + "hi": "Hi", + "edit_profile": "Edit profile", + "first_name": "First name", + "last_name": "Last name", + "gender": "Gender", + "male": "Male", + "female": "Female", + "phone_number": "Phone number", + "country": "Country", + "region": "Region", + "occupation": "Occupation", + "save_changes": "Save changes" +}; +static const Map> mapLocales = {"am": _am, "en": _en}; +} diff --git a/lib/ui/common/translations/locale_keys.g.dart b/lib/ui/common/translations/locale_keys.g.dart new file mode 100644 index 0000000..be77d58 --- /dev/null +++ b/lib/ui/common/translations/locale_keys.g.dart @@ -0,0 +1,85 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +// ignore_for_file: constant_identifier_names + +abstract class LocaleKeys { + static const welcome_back = 'welcome_back'; + static const dont_have_account = 'dont_have_account'; + static const email = 'email'; + static const password = 'password'; + static const forgot_password = 'forgot_password'; + static const cont = 'cont'; + static const register = 'register'; + static const login_with_google = 'login_with_google'; + static const or = 'or'; + static const login_with_phone = 'login_with_phone'; + static const create_account = 'create_account'; + static const already_have_account = 'already_have_account'; + static const login = 'login'; + static const register_with_google = 'register_with_google'; + static const register_with_phone = 'register_with_phone'; + static const enter_phone_number = 'enter_phone_number'; + static const login_with_email = 'login_with_email'; + static const create_password = 'create_password'; + static const confirm_password = 'confirm_password'; + static const sign_up_agreement = 'sign_up_agreement'; + static const reset_password = 'reset_password'; + static const enter_email_reset_code = 'enter_email_reset_code'; + static const please_wait = 'please_wait'; + static const reset_code_sent = 'reset_code_sent'; + static const reset_code = 'reset_code'; + static const new_password = 'new_password'; + static const logged_in_successfully = 'logged_in_successfully'; + static const view_course = 'view_course'; + static const take_practice = 'take_practice'; + static const your_current_level = 'your_current_level'; + static const overall_progress = 'overall_progress'; + static const great_work = 'great_work'; + static const view_module = 'view_module'; + static const progress = 'progress'; + static const keep_going = 'keep_going'; + static const lessons_in_module = 'lessons_in_module'; + static const practice = 'practice'; + static const start = 'start'; + static const in_progress = 'in_progress'; + static const hello = 'hello'; + static const ready_to_learn = 'ready_to_learn'; + static const learn = 'learn'; + static const course = 'course'; + static const profile = 'profile'; + static const speaking_partner = 'speaking_partner'; + static const practice_what_you_learned = 'practice_what_you_learned'; + static const practice_questions = 'practice_questions'; + static const start_practice = 'start_practice'; + static const almost_there = 'almost_there'; + static const finish_session = 'finish_session'; + static const continue_practice = 'continue_practice'; + static const end_session = 'end_session'; + static const tap_start_to_listen = 'tap_start_to_listen'; + static const practice_speaking = 'practice_speaking'; + static const tap_microphone = 'tap_microphone'; + static const reply = 'reply'; + static const cancel = 'cancel'; + static const you_are_speaking = 'you_are_speaking'; + static const practice_completed = 'practice_completed'; + static const great_improvement = 'great_improvement'; + static const practice_again = 'practice_again'; + static const conversation_review = 'conversation_review'; + static const result = 'result'; + static const quick_tip = 'quick_tip'; + static const retry = 'retry'; + static const completed_a1 = 'completed_a1'; + static const analyzing_speaking = 'analyzing_speaking'; + static const view_profile = 'view_profile'; + static const hi = 'hi'; + static const edit_profile = 'edit_profile'; + static const first_name = 'first_name'; + static const last_name = 'last_name'; + static const gender = 'gender'; + static const phone_number = 'phone_number'; + static const country = 'country'; + static const region = 'region'; + static const occupation = 'occupation'; + static const save_changes = 'save_changes'; + +} diff --git a/lib/ui/views/assessment/assessment_viewmodel.dart b/lib/ui/views/assessment/assessment_viewmodel.dart index 3802f6f..46ccf56 100644 --- a/lib/ui/views/assessment/assessment_viewmodel.dart +++ b/lib/ui/views/assessment/assessment_viewmodel.dart @@ -10,15 +10,25 @@ import '../../../app/app.locator.dart'; import '../../../app/app.router.dart'; import '../../../models/assessment.dart'; import '../../../services/api_service.dart'; +import '../../../services/localization_service.dart'; import '../../common/app_colors.dart'; import '../../common/ui_helpers.dart'; -class AssessmentViewModel extends BaseViewModel { +class AssessmentViewModel extends ReactiveViewModel { // Dependency injection final _apiService = locator(); final _dialogService = locator(); final _statusChecker = locator(); final _navigationService = locator(); + final _localizationService = locator(); + + @override + List get listenableServices => [_localizationService]; + + // Languages + Map get _selectedLanguage => _localizationService.selectedLanguage; + + Map get selectedLanguage => _selectedLanguage; // In-app navigation int _currentPage = 0; diff --git a/lib/ui/views/assessment/screens/assessment_intro_screen.dart b/lib/ui/views/assessment/screens/assessment_intro_screen.dart index 2ee36b2..5a0f906 100644 --- a/lib/ui/views/assessment/screens/assessment_intro_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_intro_screen.dart @@ -67,7 +67,8 @@ class AssessmentIntroScreen extends ViewModelWidget { showBackButton: true, onPop: viewModel.goBack, showLanguageSelection: true, - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => Text( diff --git a/lib/ui/views/assessment/screens/assessment_result_screen.dart b/lib/ui/views/assessment/screens/assessment_result_screen.dart index 638e6ff..14e277d 100644 --- a/lib/ui/views/assessment/screens/assessment_result_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_result_screen.dart @@ -35,7 +35,9 @@ class AssessmentResultScreen extends ViewModelWidget { showBackButton: true, onPop: viewModel.goBack, showLanguageSelection: true, - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildExpandedBody(AssessmentViewModel viewModel) => diff --git a/lib/ui/views/assessment/screens/start_lesson_screen.dart b/lib/ui/views/assessment/screens/start_lesson_screen.dart index fdf4873..62df74e 100644 --- a/lib/ui/views/assessment/screens/start_lesson_screen.dart +++ b/lib/ui/views/assessment/screens/start_lesson_screen.dart @@ -52,6 +52,8 @@ class StartLessonScreen extends ViewModelWidget { showBackButton: true, onPop: viewModel.goBack, showLanguageSelection: true, + language: viewModel.selectedLanguage['code'], + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildExpandedBody(AssessmentViewModel viewModel) => diff --git a/lib/ui/views/course/course_viewmodel.dart b/lib/ui/views/course/course_viewmodel.dart index 2409e66..4ee3806 100644 --- a/lib/ui/views/course/course_viewmodel.dart +++ b/lib/ui/views/course/course_viewmodel.dart @@ -34,11 +34,11 @@ class CourseViewModel extends ReactiveViewModel { 'description': 'Prepare for IELTS, TOEFL, or Duolingo with structured practice.' }, - { + /* { 'title': 'Skill-Based Courses', 'description': 'Learn English for the workplace, travel, and real-life communication.' - }, + },*/ ]; List> get courses => _courses; diff --git a/lib/ui/views/course_catalog/course_catalog_view.dart b/lib/ui/views/course_catalog/course_catalog_view.dart index fd0b1bc..ee859de 100644 --- a/lib/ui/views/course_catalog/course_catalog_view.dart +++ b/lib/ui/views/course_catalog/course_catalog_view.dart @@ -101,23 +101,25 @@ class CourseCatalogView extends StackedView { Widget _buildListView(CourseCatalogViewModel viewModel) => ListView.separated( shrinkWrap: true, - itemCount: viewModel.courseCatalogs.length, + itemCount: viewModel.catalogs.length, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => _buildTile( - courseCatalog: viewModel.courseCatalogs[index], - onCourseTap: () {}, - onPracticeTap: () {}), + onPracticeTap: () {}, + catalog: viewModel.catalogs[index], + onCourseTap: () async => + await viewModel.navigateToCourseUnit(viewModel.catalogs[index]), + ), separatorBuilder: (context, index) => verticalSpaceSmall, ); Widget _buildTile({ - required CourseCatalog courseCatalog, + required CourseCatalog catalog, required GestureTapCallback onCourseTap, required GestureTapCallback onPracticeTap, }) => CourseCatalogTile( + catalog: catalog, onCourseTap: onCourseTap, onPracticeTap: onPracticeTap, - courseCatalog: courseCatalog, ); } diff --git a/lib/ui/views/course_catalog/course_catalog_viewmodel.dart b/lib/ui/views/course_catalog/course_catalog_viewmodel.dart index e9243dd..ea61e14 100644 --- a/lib/ui/views/course_catalog/course_catalog_viewmodel.dart +++ b/lib/ui/views/course_catalog/course_catalog_viewmodel.dart @@ -5,21 +5,25 @@ import 'package:yimaru_app/models/course_catalog.dart'; import '../../../app/app.locator.dart'; import '../../../app/app.router.dart'; import '../../../services/api_service.dart'; +import '../../../services/course_service.dart'; import '../../../services/status_checker_service.dart'; import '../../common/enmus.dart'; -class CourseCatalogViewModel extends BaseViewModel { +class CourseCatalogViewModel extends ReactiveViewModel { // Dependency injection - final _apiService = locator(); + final _courseService = locator(); final _statusChecker = locator(); final _navigationService = locator(); - // Course catalogs - List _courseCatalogs = []; + @override + List get listenableServices => [_courseService]; - List get courseCatalogs => _courseCatalogs; + // Learn lessons + List get _catalogs => _courseService.catalogs; + + List get catalogs => _catalogs; // Navigation void pop() => _navigationService.back(); @@ -27,19 +31,19 @@ class CourseCatalogViewModel extends BaseViewModel { Future navigateToCoursePractice(int id) async => _navigationService.navigateToCoursePracticeView(id: id); - // Future navigateToSubcourse(Subcategory subcategory) async => - // _navigationService.navigateToCourseView(subcategory: subcategory); + Future navigateToCourseUnit(CourseCatalog catalog) async => + await _navigationService.navigateToCourseUnitView(catalog: catalog); // Remote api call // Course catalogs Future getCourseCatalogs() async => - await runBusyFuture(_getSubcategories(), + await runBusyFuture(_getCourseCatalogs(), busyObject: StateObjects.courseCatalogs); - Future _getSubcategories() async { + Future _getCourseCatalogs() async { if (await _statusChecker.checkConnection()) { - _courseCatalogs = await _apiService.getCourseCatalogs(); + await _courseService.getCourseCatalogs(); } } } 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 2b243de..d3eb88e 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 @@ -3,6 +3,7 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import '../../../app/app.locator.dart'; +import '../../../app/app.router.dart'; import '../../../models/option.dart'; import '../../../models/assessment_question.dart'; import '../../../services/api_service.dart'; @@ -153,6 +154,8 @@ class CoursePracticeQuestionViewModel extends FormViewModel { // Navigation void pop() => _navigationService.back(); + + // Remote api call // Question navigation diff --git a/lib/ui/views/course_practice_question/screens/question_loading_screen.dart b/lib/ui/views/course_practice_question/screens/question_loading_screen.dart index 56ab395..dee4931 100644 --- a/lib/ui/views/course_practice_question/screens/question_loading_screen.dart +++ b/lib/ui/views/course_practice_question/screens/question_loading_screen.dart @@ -43,8 +43,9 @@ class QuestionLoadingScreen extends StatelessWidget { Widget _buildAppBar() => LargeAppBar( onPop: onPop, showBackButton: true, - showLanguageSelection: true, - ); + showLanguageSelection: false, + + ); Widget _buildBody() => Expanded(child: Container()); diff --git a/lib/ui/views/course_unit/course_unit_view.dart b/lib/ui/views/course_unit/course_unit_view.dart index 6324926..6beb031 100644 --- a/lib/ui/views/course_unit/course_unit_view.dart +++ b/lib/ui/views/course_unit/course_unit_view.dart @@ -1,29 +1,148 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/models/course_catalog.dart'; +import 'package:yimaru_app/ui/widgets/course_unit_tile.dart'; +import '../../../models/course_unit.dart'; +import '../../common/app_colors.dart'; +import '../../common/enmus.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/course_module_banner.dart'; +import '../../widgets/custom_circular_progress_indicator.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/overall_progress.dart'; +import '../../widgets/small_app_bar.dart'; import 'course_unit_viewmodel.dart'; class CourseUnitView extends StackedView { - const CourseUnitView({Key? key}) : super(key: key); + final CourseCatalog catalog; + + const CourseUnitView({Key? key, required this.catalog}) : super(key: key); + + @override + void onViewModelReady(CourseUnitViewModel viewModel) async { + await viewModel.getCourseUnits(catalog.id ?? 0); + super.onViewModelReady(viewModel); + } + + @override + CourseUnitViewModel viewModelBuilder(BuildContext context) => + CourseUnitViewModel(); @override Widget builder( BuildContext context, CourseUnitViewModel viewModel, Widget? child, - ) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - body: Container( - padding: const EdgeInsets.only(left: 25.0, right: 25.0), - child: const Center(child: Text("CourseUnitView")), - ), - ); - } - - @override - CourseUnitViewModel viewModelBuilder( - BuildContext context, ) => - CourseUnitViewModel(); + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(CourseUnitViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(CourseUnitViewModel viewModel) => + SafeArea(child: _buildBody(viewModel)); + + Widget _buildBody(CourseUnitViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(CourseUnitViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildAppBar(viewModel), + verticalSpaceMedium, + _buildModulesColumnWrapper(viewModel), + ], + ); + + Widget _buildAppBar(CourseUnitViewModel viewModel) => SmallAppBar( + onPop: viewModel.pop, + showBackButton: true, + title: 'Course Detail', + ); + + Widget _buildModulesColumnWrapper(CourseUnitViewModel viewModel) => + Expanded(child: _buildLevelsColumnScrollView(viewModel)); + + Widget _buildLevelsColumnScrollView(CourseUnitViewModel viewModel) => + SingleChildScrollView( + child: _buildLevelsColumn(viewModel), + ); + + Widget _buildLevelsColumn(CourseUnitViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildLevelsColumnChildren(viewModel), + ); + + List _buildLevelsColumnChildren(CourseUnitViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildCourseModuleBanner(), + verticalSpaceMedium, + _buildOverallProgress(), + verticalSpaceTiny, + _buildContinueButton(viewModel), + verticalSpaceMedium, + _buildListViewBuilder(viewModel) + ]; + + Widget _buildTitle() => Text( + catalog.name ?? '', + style: style18P600, + ); + + Widget _buildCourseModuleBanner() => const CourseModuleBanner(); + + Widget _buildOverallProgress() => const OverallProgress( + progress: 0, + backgroundColor: Colors.transparent, + indicatorBackgroundColor: kcVeryLightGrey, + ); + + Widget _buildContinueButton(CourseUnitViewModel viewModel) => + const CustomElevatedButton( + height: 55, + borderRadius: 12, + foregroundColor: kcWhite, + text: 'Continue Course', + backgroundColor: kcPrimaryColor); + + Widget _buildListViewBuilder(CourseUnitViewModel viewModel) => + viewModel.busy(StateObjects.courseUnits) + ? _buildProgressIndicator() + : _buildListView(viewModel); + + Widget _buildProgressIndicator() => const Center( + child: CustomCircularProgressIndicator(color: kcPrimaryColor), + ); + + Widget _buildListView(CourseUnitViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.units.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildTile( + index: index, + unit: viewModel.units[index], + onPracticeTap: () {}, + onLessonTap: () {}), + ); + + Widget _buildTile({ + required int index, + required CourseUnit unit, + required GestureTapCallback onLessonTap, + required GestureTapCallback onPracticeTap, + }) => + CourseUnitTile( + unit: unit, + index: index, + onLessonTap: onLessonTap, + onPracticeTap: onPracticeTap, + ); } diff --git a/lib/ui/views/course_unit/course_unit_viewmodel.dart b/lib/ui/views/course_unit/course_unit_viewmodel.dart index 32b5f84..3495727 100644 --- a/lib/ui/views/course_unit/course_unit_viewmodel.dart +++ b/lib/ui/views/course_unit/course_unit_viewmodel.dart @@ -1,3 +1,55 @@ import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/models/course_module.dart'; -class CourseUnitViewModel extends BaseViewModel {} +import '../../../app/app.locator.dart'; +import '../../../models/course_unit.dart'; +import '../../../services/api_service.dart'; +import '../../../services/course_service.dart'; +import '../../../services/status_checker_service.dart'; +import '../../common/enmus.dart'; + +class CourseUnitViewModel extends ReactiveViewModel { + // Dependency injection + final _courseService = locator(); + + final _statusChecker = locator(); + + final _navigationService = locator(); + + @override + List get listenableServices => [_courseService]; + + // Course units + List get _units => _courseService.units; + + List get units => _units; + + // Navigation + void pop() => _navigationService.back(); + + // Remote api call + + // Course units + Future getCourseUnits(int id) async => + await runBusyFuture(_getCourseUnits(id), + busyObject: StateObjects.courseUnits); + + Future _getCourseUnits(int id) async { + if (await _statusChecker.checkConnection()) { + await _courseService.getCourseUnits(id); + } + } + + Future getCourseUnitModules( + {required int id, required int index}) async => + await runBusyFuture(_getCourseUnitModules(id: id, index: index), + busyObject: StateObjects.courseModules); + + Future _getCourseUnitModules( + {required int id, required int index}) async { + if (await _statusChecker.checkConnection()) { + await _courseService.getCourseUnitModule(id: id, index: index); + } + } +} diff --git a/lib/ui/views/forget_password/forget_password_viewmodel.dart b/lib/ui/views/forget_password/forget_password_viewmodel.dart index 65237fb..0dd11cf 100644 --- a/lib/ui/views/forget_password/forget_password_viewmodel.dart +++ b/lib/ui/views/forget_password/forget_password_viewmodel.dart @@ -4,17 +4,30 @@ import 'package:stacked_services/stacked_services.dart'; import '../../../app/app.locator.dart'; import '../../../app/app.router.dart'; import '../../../services/api_service.dart'; +import '../../../services/localization_service.dart'; import '../../../services/status_checker_service.dart'; import '../../common/enmus.dart'; import '../../common/ui_helpers.dart'; -class ForgetPasswordViewModel extends FormViewModel { +class ForgetPasswordViewModel extends ReactiveViewModel + with FormStateHelper + implements FormViewModel { final _apiService = locator(); final _statusChecker = locator(); final _navigationService = locator(); + final _localizationService = locator(); + + @override + List get listenableServices => [_localizationService]; + + // Languages + Map get _selectedLanguage => _localizationService.selectedLanguage; + + Map get selectedLanguage => _selectedLanguage; + // User data final Map _userData = {}; @@ -165,6 +178,10 @@ class ForgetPasswordViewModel extends FormViewModel { // Navigation void pop() => _navigationService.back(); + Future navigateToLanguage() async => + await _navigationService.navigateToLanguageView(); + + Future replaceWithLogin() async => await _navigationService.clearStackAndShow(Routes.loginView); 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 d540362..5f4f00b 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 @@ -88,7 +88,11 @@ class RequestCodeScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _inAppPop(viewModel), - ); + language: viewModel.selectedLanguage['code'], + + onLanguage: ()async => await viewModel.navigateToLanguage(), + + ); Widget _buildExpandedBody( {required BuildContext context, diff --git a/lib/ui/views/forget_password/screens/reset_password_screen.dart b/lib/ui/views/forget_password/screens/reset_password_screen.dart index 1e89530..4868df6 100644 --- a/lib/ui/views/forget_password/screens/reset_password_screen.dart +++ b/lib/ui/views/forget_password/screens/reset_password_screen.dart @@ -86,7 +86,11 @@ class ResetPasswordScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _inAppPop(viewModel), - ); + language: viewModel.selectedLanguage['code'], + + onLanguage: ()async => await viewModel.navigateToLanguage(), + + ); Widget _buildExpandedBody(ForgetPasswordViewModel viewModel) => Expanded(child: _buildColumnScroller(viewModel)); diff --git a/lib/ui/views/language/language_view.dart b/lib/ui/views/language/language_view.dart index 35883b0..6e2b150 100644 --- a/lib/ui/views/language/language_view.dart +++ b/lib/ui/views/language/language_view.dart @@ -22,48 +22,72 @@ class LanguageView extends StackedView { LanguageViewModel viewModel, Widget? child, ) => - _buildScaffoldWrapper(viewModel); + _buildScaffoldWrapper(context: context, viewModel: viewModel); - Widget _buildScaffoldWrapper(LanguageViewModel viewModel) => Scaffold( + Widget _buildScaffoldWrapper( + {required BuildContext context, + required LanguageViewModel viewModel}) => + Scaffold( backgroundColor: kcBackgroundColor, - body: _buildScaffold(viewModel), + body: _buildScaffold(context: context, viewModel: viewModel), ); - Widget _buildScaffold(LanguageViewModel viewModel) => - SafeArea(child: _buildBodyWrapper(viewModel)); + Widget _buildScaffold( + {required BuildContext context, + required LanguageViewModel viewModel}) => + SafeArea( + child: _buildBodyWrapper(context: context, viewModel: viewModel)); - Widget _buildBodyWrapper(LanguageViewModel viewModel) => Column( + Widget _buildBodyWrapper( + {required BuildContext context, + required LanguageViewModel viewModel}) => + Column( crossAxisAlignment: CrossAxisAlignment.start, - children: _buildBodyChildren(viewModel), + children: _buildBodyChildren(context: context, viewModel: viewModel), ); - List _buildBodyChildren(LanguageViewModel viewModel) => [ + List _buildBodyChildren( + {required BuildContext context, + required LanguageViewModel viewModel}) => + [ verticalSpaceMedium, _buildAppBarWrapper(viewModel), - _buildExpandedBody(viewModel) + _buildExpandedBody(context: context, viewModel: viewModel) ]; - Widget _buildExpandedBody(LanguageViewModel viewModel) => - Expanded(child: _buildColumnWrapper(viewModel)); + Widget _buildExpandedBody( + {required BuildContext context, + required LanguageViewModel viewModel}) => + Expanded( + child: _buildColumnWrapper(context: context, viewModel: viewModel)); - Widget _buildColumnWrapper(LanguageViewModel viewModel) => Padding( + Widget _buildColumnWrapper( + {required BuildContext context, + required LanguageViewModel viewModel}) => + Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: _buildColumn(viewModel), + child: _buildColumn(context: context, viewModel: viewModel), ); - Widget _buildColumn(LanguageViewModel viewModel) => Column( + Widget _buildColumn( + {required BuildContext context, + required LanguageViewModel viewModel}) => + Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - children: _buildColumnChildren(viewModel), + children: _buildColumnChildren(context: context, viewModel: viewModel), ); - List _buildColumnChildren(LanguageViewModel viewModel) => [ + List _buildColumnChildren( + {required BuildContext context, + required LanguageViewModel viewModel}) => + [ verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, _buildSubtitle(), verticalSpaceMedium, - _buildLanguages(viewModel) + _buildLanguages(context: context, viewModel: viewModel) ]; Widget _buildAppBarWrapper(LanguageViewModel viewModel) => Padding( @@ -87,16 +111,19 @@ class LanguageView extends StackedView { style: style14MG400, ); - Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder( + Widget _buildLanguages( + {required BuildContext context, + required LanguageViewModel viewModel}) => + ListView.builder( shrinkWrap: true, itemCount: viewModel.languages.length, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => _buildLanguage( title: viewModel.languages[index]['language'], + onTap: () => viewModel.setSelectedLanguage( + context: context, title: viewModel.languages[index]), selected: viewModel .isSelectedLanguage(viewModel.languages[index]['language']), - onTap: () => - viewModel.setSelectedLanguage(viewModel.languages[index]), ), ); diff --git a/lib/ui/views/language/language_viewmodel.dart b/lib/ui/views/language/language_viewmodel.dart index 7fa0701..dfbad13 100644 --- a/lib/ui/views/language/language_viewmodel.dart +++ b/lib/ui/views/language/language_viewmodel.dart @@ -1,34 +1,38 @@ +import 'package:flutter/cupertino.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/services/localization_service.dart'; import '../../../app/app.locator.dart'; -class LanguageViewModel extends BaseViewModel { +class LanguageViewModel extends ReactiveViewModel { + // Dependency injection final _navigationService = locator(); + final _localizationService = locator(); + + @override + List get listenableServices => [_localizationService]; + + + // Languages - Map _selectedLanguage = { - 'code': 'EN', - 'language': 'English' - }; - - Map get selectedLanguage => _selectedLanguage; - - final List> _languages = [ - {'code': 'አማ', 'language': 'አማርኛ'}, - {'code': 'EN', 'language': 'English'}, - ]; + List> get _languages => _localizationService.languages; List> get languages => _languages; + Map get _selectedLanguage => _localizationService.selectedLanguage; + + Map get selectedLanguage => _selectedLanguage; // Languages bool isSelectedLanguage(String title) => - _selectedLanguage['language'] == title; + _localizationService.isSelectedLanguage(title); - void setSelectedLanguage(Map title) { - _selectedLanguage = title; - rebuildUi(); - } + Future setSelectedLanguage( + {required BuildContext context, + required Map title}) async => + await _localizationService.setSelectedLanguage( + context: context, title: title); // Navigation void pop() => _navigationService.back(); diff --git a/lib/ui/views/learn_module/learn_module_view.dart b/lib/ui/views/learn_module/learn_module_view.dart index fe15390..a2d5193 100644 --- a/lib/ui/views/learn_module/learn_module_view.dart +++ b/lib/ui/views/learn_module/learn_module_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:yimaru_app/models/learn_module.dart'; import 'package:yimaru_app/ui/widgets/learn_module_tile.dart'; -import 'package:yimaru_app/ui/widgets/overall_learn_progress.dart'; +import 'package:yimaru_app/ui/widgets/overall_progress.dart'; import '../../../models/learn_course.dart'; import '../../common/app_colors.dart'; @@ -96,7 +96,7 @@ class LearnModuleView extends StackedView { style: style14P400, ); - Widget _buildOverallProgress() => OverallLearnProgress( + Widget _buildOverallProgress() => OverallProgress( indicatorBackgroundColor: kcWhite, progress: course.access?.progressPercent ?? 0, backgroundColor: kcPrimaryColor.withOpacity(0.1), diff --git a/lib/ui/views/login/login_viewmodel.dart b/lib/ui/views/login/login_viewmodel.dart index f408ae9..9c5f264 100644 --- a/lib/ui/views/login/login_viewmodel.dart +++ b/lib/ui/views/login/login_viewmodel.dart @@ -9,6 +9,7 @@ import 'package:yimaru_app/models/user.dart'; import '../../../services/api_service.dart'; import '../../../services/authentication_service.dart'; import '../../../services/google_auth_service.dart'; +import '../../../services/localization_service.dart'; import '../../../services/status_checker_service.dart'; import '../../common/enmus.dart'; import '../../common/ui_helpers.dart'; @@ -24,16 +25,23 @@ class LoginViewModel extends ReactiveViewModel final _googleAuthService = locator(); + final _localizationService = locator(); + final _authenticationService = locator(); @override - List get listenableServices => [_googleAuthService]; + List get listenableServices => [_googleAuthService,_localizationService]; // Google user GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser; GoogleSignInAccount? get googleUser => _googleUser; + // Languages + Map get _selectedLanguage => _localizationService.selectedLanguage; + + Map get selectedLanguage => _selectedLanguage; + // In-app navigation int _currentPage = 0; @@ -158,9 +166,14 @@ class LoginViewModel extends ReactiveViewModel Future navigateToRegister() async => await _navigationService.navigateToRegisterView(); + Future navigateToLanguage() async => + await _navigationService.navigateToLanguageView(); + Future navigateToForgetPassword() async => await _navigationService.navigateToForgetPasswordView(); + + Future replaceWithStartUp() async => await _navigationService.clearStackAndShow(Routes.startupView); diff --git a/lib/ui/views/login/screens/login_otp_screen.dart b/lib/ui/views/login/screens/login_otp_screen.dart index 03d1d8a..e0a35dc 100644 --- a/lib/ui/views/login/screens/login_otp_screen.dart +++ b/lib/ui/views/login/screens/login_otp_screen.dart @@ -80,10 +80,14 @@ class LoginOtpScreen extends ViewModelWidget { _buildExpandedBody(context: context, viewModel: viewModel) ]; - Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar( + Widget _buildAppBar(LoginViewModel viewModel) => LargeAppBar( showBackButton: false, showLanguageSelection: true, - ); + language: viewModel.selectedLanguage['code'], + + onLanguage: ()async => await viewModel.navigateToLanguage(), + + ); Widget _buildExpandedBody( {required BuildContext context, required LoginViewModel viewModel}) => diff --git a/lib/ui/views/login/screens/login_with_email_screen.dart b/lib/ui/views/login/screens/login_with_email_screen.dart index e7e8442..bfb8aaa 100644 --- a/lib/ui/views/login/screens/login_with_email_screen.dart +++ b/lib/ui/views/login/screens/login_with_email_screen.dart @@ -1,5 +1,7 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart'; import 'package:yimaru_app/ui/views/login/login_view.form.dart'; import 'package:yimaru_app/ui/widgets/obscure_password.dart'; @@ -76,9 +78,11 @@ class LoginWithEmailScreen extends ViewModelWidget { _buildExpandedBody(context: context, viewModel: viewModel) ]; - Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar( + Widget _buildAppBar(LoginViewModel viewModel) => LargeAppBar( showBackButton: false, showLanguageSelection: true, + language: viewModel.selectedLanguage['code'], + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildExpandedBody( @@ -140,7 +144,7 @@ class LoginWithEmailScreen extends ViewModelWidget { ]; Widget _buildTitle() => Text( - 'Welcome Back', + LocaleKeys.welcome_back.tr(), style: style25DG600, ); @@ -150,10 +154,10 @@ class LoginWithEmailScreen extends ViewModelWidget { Widget _buildEmailFormField(LoginViewModel viewModel) => TextFormField( controller: emailController, - keyboardType: TextInputType.emailAddress, onTap: viewModel.setEmailFocus, + keyboardType: TextInputType.emailAddress, decoration: inputDecoration( - hint: 'Email', + hint: LocaleKeys.email.tr(), focus: viewModel.focusEmail, filled: emailController.text.isNotEmpty), ); @@ -173,7 +177,7 @@ class LoginWithEmailScreen extends ViewModelWidget { onTap: viewModel.setPasswordFocus, obscureText: viewModel.obscurePassword, decoration: inputDecoration( - hint: 'Password', + hint: LocaleKeys.password.tr(), focus: viewModel.focusPassword, suffix: _buildObscureButton(viewModel), filled: passwordController.text.isNotEmpty), @@ -207,7 +211,7 @@ class LoginWithEmailScreen extends ViewModelWidget { ); Widget _buildForgetPasswordText() => Text( - 'Forgot Password?', + LocaleKeys.forgot_password.tr(), style: style14P400, ); @@ -227,9 +231,9 @@ class LoginWithEmailScreen extends ViewModelWidget { Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton( height: 55, safe: false, - text: 'Continue', borderRadius: 12, foregroundColor: kcWhite, + text: LocaleKeys.cont.tr(), onTap: emailController.text.isNotEmpty && passwordController.text.isNotEmpty ? () async => await _login(viewModel) @@ -245,9 +249,9 @@ class LoginWithEmailScreen extends ViewModelWidget { height: 55, borderRadius: 12, backgroundColor: kcWhite, - text: 'Login with Google', borderColor: kcPrimaryColor, foregroundColor: kcPrimaryColor, + text: LocaleKeys.login_with_google.tr(), leadingImage: 'assets/icons/google.png', onTap: () async => await viewModel.signInWithGoogle(), ); @@ -263,7 +267,7 @@ class LoginWithEmailScreen extends ViewModelWidget { borderColor: kcPrimaryColor, onTap: () => viewModel.goTo(1), foregroundColor: kcPrimaryColor, - text: 'Login with Phone Number', + text: LocaleKeys.login_with_phone.tr() ); Widget _buildLoginWithEmailState(LoginViewModel viewModel) => diff --git a/lib/ui/views/login/screens/login_with_phone_number_screen.dart b/lib/ui/views/login/screens/login_with_phone_number_screen.dart index f01adc2..4765476 100644 --- a/lib/ui/views/login/screens/login_with_phone_number_screen.dart +++ b/lib/ui/views/login/screens/login_with_phone_number_screen.dart @@ -76,7 +76,11 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => viewModel.goTo(0), - ); + language: viewModel.selectedLanguage['code'], + + onLanguage: ()async => await viewModel.navigateToLanguage(), + + ); Widget _buildExpandedBody( {required BuildContext context, required LoginViewModel viewModel}) => diff --git a/lib/ui/views/onboarding/onboarding_viewmodel.dart b/lib/ui/views/onboarding/onboarding_viewmodel.dart index b80877d..52aad06 100644 --- a/lib/ui/views/onboarding/onboarding_viewmodel.dart +++ b/lib/ui/views/onboarding/onboarding_viewmodel.dart @@ -4,6 +4,7 @@ import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.router.dart'; import '../../../app/app.locator.dart'; import '../../../services/google_auth_service.dart'; +import '../../../services/localization_service.dart'; class OnboardingViewModel extends ReactiveViewModel with FormStateHelper @@ -14,14 +15,24 @@ class OnboardingViewModel extends ReactiveViewModel final _googleAuthService = locator(); + final _localizationService = locator(); + + + @override - List get listenableServices => [_googleAuthService]; + List get listenableServices => [_googleAuthService,_localizationService]; // Google user GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser; GoogleSignInAccount? get googleUser => _googleUser; + + // Languages + Map get _selectedLanguage => _localizationService.selectedLanguage; + + Map get selectedLanguage => _selectedLanguage; + // Navigation int _currentPage = 0; @@ -207,20 +218,6 @@ class OnboardingViewModel extends ReactiveViewModel List get topics => _topics; - // Languages - final List> _languages = [ - {'code': 'አማ', 'language': 'አማርኛ'}, - {'code': 'EN', 'language': 'English'}, - ]; - - List> get languages => _languages; - - Map _selectedLanguage = { - 'code': 'EN', - 'language': 'English' - }; - - Map get selectedLanguage => _selectedLanguage; // User data final Map _userData = {}; @@ -548,15 +545,6 @@ class OnboardingViewModel extends ReactiveViewModel bool isSelectedTopic(String value) => _selectedTopic == value; - // Language - void setSelectedLanguage(Map value) { - _selectedLanguage = value; - rebuildUi(); - } - - bool isSelectedLanguage(String value) => - _selectedLanguage['language'] == value; - // Add user data void addUserData(Map data) { _userData.addAll(data); diff --git a/lib/ui/views/onboarding/screens/age_group_form_screen.dart b/lib/ui/views/onboarding/screens/age_group_form_screen.dart index 75e1dd2..a05640d 100644 --- a/lib/ui/views/onboarding/screens/age_group_form_screen.dart +++ b/lib/ui/views/onboarding/screens/age_group_form_screen.dart @@ -88,7 +88,9 @@ class AgeGroupFormScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => Text( diff --git a/lib/ui/views/onboarding/screens/challenge_form_screen.dart b/lib/ui/views/onboarding/screens/challenge_form_screen.dart index 37cc18b..9ce2fa7 100644 --- a/lib/ui/views/onboarding/screens/challenge_form_screen.dart +++ b/lib/ui/views/onboarding/screens/challenge_form_screen.dart @@ -102,7 +102,9 @@ class ChallengeFormScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => Text( diff --git a/lib/ui/views/onboarding/screens/country_region_form_screen.dart b/lib/ui/views/onboarding/screens/country_region_form_screen.dart index 6592e5e..6b5c424 100644 --- a/lib/ui/views/onboarding/screens/country_region_form_screen.dart +++ b/lib/ui/views/onboarding/screens/country_region_form_screen.dart @@ -112,7 +112,8 @@ class CountryRegionFormScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => Text( diff --git a/lib/ui/views/onboarding/screens/educational_background_form_screen.dart b/lib/ui/views/onboarding/screens/educational_background_form_screen.dart index 2297f11..ba49d19 100644 --- a/lib/ui/views/onboarding/screens/educational_background_form_screen.dart +++ b/lib/ui/views/onboarding/screens/educational_background_form_screen.dart @@ -88,7 +88,8 @@ class EducationalBackgroundFormScreen showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => const Text( diff --git a/lib/ui/views/onboarding/screens/full_name_form_screen.dart b/lib/ui/views/onboarding/screens/full_name_form_screen.dart index 3e86413..b15fb23 100644 --- a/lib/ui/views/onboarding/screens/full_name_form_screen.dart +++ b/lib/ui/views/onboarding/screens/full_name_form_screen.dart @@ -88,7 +88,8 @@ class FullNameFormScreen extends ViewModelWidget { Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( showBackButton: false, showLanguageSelection: true, - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => const Text( diff --git a/lib/ui/views/onboarding/screens/gender_form_screen.dart b/lib/ui/views/onboarding/screens/gender_form_screen.dart index b9fba80..4e29cef 100644 --- a/lib/ui/views/onboarding/screens/gender_form_screen.dart +++ b/lib/ui/views/onboarding/screens/gender_form_screen.dart @@ -80,7 +80,9 @@ class GenderFormScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => Text( diff --git a/lib/ui/views/onboarding/screens/language_goal_form_screen.dart b/lib/ui/views/onboarding/screens/language_goal_form_screen.dart index e6d3e3d..6474fea 100644 --- a/lib/ui/views/onboarding/screens/language_goal_form_screen.dart +++ b/lib/ui/views/onboarding/screens/language_goal_form_screen.dart @@ -103,7 +103,8 @@ class LanguageGoalFormScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => Text( diff --git a/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart b/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart index c7a23d5..a2823bd 100644 --- a/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart +++ b/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart @@ -98,7 +98,9 @@ class LearningGoalFormScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle(OnboardingViewModel viewModel) => Text.rich( diff --git a/lib/ui/views/onboarding/screens/occupation_form_screen.dart b/lib/ui/views/onboarding/screens/occupation_form_screen.dart index 7453571..31f2eec 100644 --- a/lib/ui/views/onboarding/screens/occupation_form_screen.dart +++ b/lib/ui/views/onboarding/screens/occupation_form_screen.dart @@ -86,7 +86,9 @@ class OccupationFormScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildTitle() => Text( diff --git a/lib/ui/views/onboarding/screens/topic_form_screen.dart b/lib/ui/views/onboarding/screens/topic_form_screen.dart index 4b7c167..0569107 100644 --- a/lib/ui/views/onboarding/screens/topic_form_screen.dart +++ b/lib/ui/views/onboarding/screens/topic_form_screen.dart @@ -58,7 +58,9 @@ class TopicFormScreen extends ViewModelWidget { showBackButton: true, showLanguageSelection: true, onPop: () => _pop(viewModel), - onLanguage: () async => await viewModel.navigateToLanguage(), + language: viewModel.selectedLanguage['code'], + + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildExpandedBody(OnboardingViewModel viewModel) => diff --git a/lib/ui/views/register/register_viewmodel.dart b/lib/ui/views/register/register_viewmodel.dart index 344e9be..de4fc4e 100644 --- a/lib/ui/views/register/register_viewmodel.dart +++ b/lib/ui/views/register/register_viewmodel.dart @@ -11,6 +11,7 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import '../../../app/app.locator.dart'; import '../../../models/user.dart'; import '../../../services/google_auth_service.dart'; +import '../../../services/localization_service.dart'; import '../../../services/status_checker_service.dart'; class RegisterViewModel extends ReactiveViewModel @@ -24,16 +25,24 @@ class RegisterViewModel extends ReactiveViewModel final _googleAuthService = locator(); + final _localizationService = locator(); + final _authenticationService = locator(); @override - List get listenableServices => [_googleAuthService]; + List get listenableServices => [_googleAuthService,_localizationService]; // Google user GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser; GoogleSignInAccount? get googleUser => _googleUser; + // Languages + Map get _selectedLanguage => _localizationService.selectedLanguage; + + Map get selectedLanguage => _selectedLanguage; + + // Navigation int _currentPage = 0; @@ -260,14 +269,19 @@ class RegisterViewModel extends ReactiveViewModel void pop() => _navigationService.back(); - Future navigateToTermsAndConditions() async => - await _navigationService.navigateToTermsAndConditionsView(); + Future replaceToLogin() async => + await _navigationService.replaceWithLoginView(); + + Future navigateToLanguage() async => + await _navigationService.navigateToLanguageView(); + Future navigateToPrivacyPolicy() async => await _navigationService.navigateToPrivacyPolicyView(); - Future replaceToLogin() async => - await _navigationService.replaceWithLoginView(); + + Future navigateToTermsAndConditions() async => + await _navigationService.navigateToTermsAndConditionsView(); Future replaceWithStartUp() async => await _navigationService.clearStackAndShow(Routes.startupView); diff --git a/lib/ui/views/register/screens/create_password_screen.dart b/lib/ui/views/register/screens/create_password_screen.dart index ab8c9f0..4147dff 100644 --- a/lib/ui/views/register/screens/create_password_screen.dart +++ b/lib/ui/views/register/screens/create_password_screen.dart @@ -68,7 +68,11 @@ class CreatePasswordScreen extends ViewModelWidget { showBackButton: true, onPop: viewModel.goBack, showLanguageSelection: true, - ); + language: viewModel.selectedLanguage['code'], + + onLanguage: ()async => await viewModel.navigateToLanguage(), + + ); Widget _buildExpandedBody(RegisterViewModel viewModel) => Expanded(child: _buildColumnScroller(viewModel)); diff --git a/lib/ui/views/register/screens/register_with_email_screen.dart b/lib/ui/views/register/screens/register_with_email_screen.dart index cb45201..4806cfb 100644 --- a/lib/ui/views/register/screens/register_with_email_screen.dart +++ b/lib/ui/views/register/screens/register_with_email_screen.dart @@ -81,7 +81,11 @@ class RegisterWithEmailScreen extends ViewModelWidget { showBackButton: true, onPop: viewModel.goBack, showLanguageSelection: true, - ); + language: viewModel.selectedLanguage['code'], + + onLanguage: ()async => await viewModel.navigateToLanguage(), + + ); Widget _buildExpandedBody( {required BuildContext context, diff --git a/lib/ui/views/register/screens/register_with_phone_number_screen.dart b/lib/ui/views/register/screens/register_with_phone_number_screen.dart index 4eea817..f2cc333 100644 --- a/lib/ui/views/register/screens/register_with_phone_number_screen.dart +++ b/lib/ui/views/register/screens/register_with_phone_number_screen.dart @@ -84,7 +84,11 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget { showBackButton: true, onPop: viewModel.goBack, showLanguageSelection: true, - ); + language: viewModel.selectedLanguage['code'], + + onLanguage: ()async => await viewModel.navigateToLanguage(), + + ); Widget _buildExpandedBody( {required BuildContext context, diff --git a/lib/ui/views/register/screens/registration_otp_screen.dart b/lib/ui/views/register/screens/registration_otp_screen.dart index 2e7d2cc..e5dcecc 100644 --- a/lib/ui/views/register/screens/registration_otp_screen.dart +++ b/lib/ui/views/register/screens/registration_otp_screen.dart @@ -95,6 +95,8 @@ class RegistrationOtpScreen extends ViewModelWidget { showBackButton: true, onPop: viewModel.goBack, showLanguageSelection: true, + language: viewModel.selectedLanguage['code'], + onLanguage: () async => await viewModel.navigateToLanguage(), ); Widget _buildExpandedBody( diff --git a/lib/ui/views/startup/startup_viewmodel.dart b/lib/ui/views/startup/startup_viewmodel.dart index 4ebe791..eb9f4a1 100644 --- a/lib/ui/views/startup/startup_viewmodel.dart +++ b/lib/ui/views/startup/startup_viewmodel.dart @@ -47,16 +47,20 @@ class StartupViewModel extends ReactiveViewModel { } // Navigation + + Future replaceWithHome() async => + await _navigationService.replaceWithHomeView(); + Future replaceWithFailure() async => await _navigationService.replaceWithFailureView( label: 'Check you internet connection', onTap: () async => await _getProfileStatus()); + + Future replaceWithOnboarding() async => await _navigationService.replaceWithOnboardingView(); - Future replaceWithHome() async => - await _navigationService.replaceWithHomeView(); // Remote api calls diff --git a/lib/ui/widgets/assessment_loading_screen.dart b/lib/ui/widgets/assessment_loading_screen.dart index 7015704..36abbed 100644 --- a/lib/ui/widgets/assessment_loading_screen.dart +++ b/lib/ui/widgets/assessment_loading_screen.dart @@ -43,7 +43,7 @@ class AssessmentLoadingScreen extends StatelessWidget { Widget _buildAppBar() => LargeAppBar( onPop: onPop, showBackButton: true, - showLanguageSelection: true, + showLanguageSelection: false, ); Widget _buildBody() => Expanded(child: Container()); diff --git a/lib/ui/widgets/course_catalog_tile.dart b/lib/ui/widgets/course_catalog_tile.dart index 40ddcb2..d0e7cf9 100644 --- a/lib/ui/widgets/course_catalog_tile.dart +++ b/lib/ui/widgets/course_catalog_tile.dart @@ -7,7 +7,7 @@ import '../common/ui_helpers.dart'; import 'custom_elevated_button.dart'; class CourseCatalogTile extends StatelessWidget { - final CourseCatalog courseCatalog; + final CourseCatalog catalog; final GestureTapCallback? onCourseTap; final GestureTapCallback? onPracticeTap; @@ -15,7 +15,7 @@ class CourseCatalogTile extends StatelessWidget { super.key, this.onCourseTap, this.onPracticeTap, - required this.courseCatalog, + required this.catalog, }); @override @@ -57,7 +57,7 @@ class CourseCatalogTile extends StatelessWidget { ]; Widget _buildTitle() => Text( - courseCatalog.name ?? '', + catalog.name ?? '', style: style16P600, ); diff --git a/lib/ui/widgets/course_topic_tile.dart b/lib/ui/widgets/course_module_tile_small.dart similarity index 86% rename from lib/ui/widgets/course_topic_tile.dart rename to lib/ui/widgets/course_module_tile_small.dart index bddff23..514576b 100644 --- a/lib/ui/widgets/course_topic_tile.dart +++ b/lib/ui/widgets/course_module_tile_small.dart @@ -3,11 +3,12 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; -class CourseTopicTile extends StatelessWidget { +class CourseModuleTileSmall extends StatelessWidget { final String title; final ProgressStatuses status; - const CourseTopicTile({super.key, required this.title, required this.status}); + const CourseModuleTileSmall( + {super.key, required this.title, required this.status}); @override Widget build(BuildContext context) => _buildTile(); @@ -27,7 +28,9 @@ class CourseTopicTile extends StatelessWidget { Widget _buildTitle() => Text( title, - style: style16DG600, + maxLines: 1, + softWrap: false, + style: style14DG600, ); Widget _buildLeadingWrapper() => CircleAvatar( diff --git a/lib/ui/widgets/course_unit_tile.dart b/lib/ui/widgets/course_unit_tile.dart new file mode 100644 index 0000000..d5e2943 --- /dev/null +++ b/lib/ui/widgets/course_unit_tile.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/models/course_unit.dart'; +import 'package:yimaru_app/models/submodule.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/widgets/course_module_tile_small.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; +import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import '../views/course_unit/course_unit_viewmodel.dart'; +import 'custom_circular_progress_indicator.dart'; +import 'custom_elevated_button.dart'; + +class CourseUnitTile extends ViewModelWidget { + final int index; + final CourseUnit unit; + final GestureTapCallback? onLessonTap; + final GestureTapCallback? onPracticeTap; + + const CourseUnitTile({ + super.key, + this.onLessonTap, + this.onPracticeTap, + required this.unit, + required this.index, + }); + + Future _getCourseModules({ + required bool expanded, + required CourseUnitViewModel viewModel, + }) async { + if (!expanded) return; + + // Prevent duplicate API calls + if ((unit.modules?.isNotEmpty ?? false)) return; + + await viewModel.getCourseUnitModules(index: index, id: unit.id ?? 0); + } + + Future _showSheet( + {required BuildContext context, + required CourseUnitViewModel viewModel}) async => + await showModalBottomSheet( + context: context, + backgroundColor: kcTransparent, + builder: (_) => _buildSheet(viewModel), + ); + + @override + Widget build(BuildContext context, CourseUnitViewModel viewModel) => + _buildExpansionTileCard(context: context, viewModel: viewModel); + + Widget _buildExpansionTileCard( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all(color: kcVeryLightGrey), + ), + child: _buildTileStack(context: context, viewModel: viewModel), + ); + + Widget _buildTileStack( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + Stack( + children: [ + _buildExpansionTile(context: context, viewModel: viewModel), + // _buildContainerShaderState() + ], + ); + + Widget _buildExpansionTile( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + ExpansionTile( + enabled: true, + title: _buildTitle(), + textColor: kcDarkGrey, + showTrailingIcon: true, + initiallyExpanded: false, + subtitle: _buildSubtitle(), + // key: Key(unit.id.toString()), + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + backgroundColor: kcBackgroundColor, + shape: Border.all(color: kcTransparent), + expandedAlignment: Alignment.centerLeft, + collapsedBackgroundColor: kcBackgroundColor, + + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + onExpansionChanged: (bool expanded) async => await _getCourseModules( + expanded: expanded, + viewModel: viewModel, + ), + // enabled: status != ProgressStatuses.pending, + // showTrailingIcon: status != ProgressStatuses.pending ? true : false, + //initiallyExpanded: status == ProgressStatuses.started ? true : false, + children: + _buildExpansionTileChildren(context: context, viewModel: viewModel), + ); + + Widget _buildTitle() => Text( + unit.name ?? '', + maxLines: 1, + softWrap: false, + style: style16P600, + overflow: TextOverflow.ellipsis, + ); + + Widget _buildSubtitle() => Text( + '0% completed', + style: style14DG500, + ); + + List _buildExpansionTileChildren( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + [_buildExpansionTileItem(context: context, viewModel: viewModel)]; + + Widget _buildExpansionTileItem( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildExpansionTileItemChildren( + context: context, viewModel: viewModel), + ); + + List _buildExpansionTileItemChildren( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + [ + _buildProgressRowWrapper(), + verticalSpaceSmall, + _buildActionButtonWrapper(context: context, viewModel: viewModel), + verticalSpaceMedium, + _buildCourseModulesState(viewModel) + ]; + + Widget _buildProgressRowWrapper() => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildProgressRow(), + ); + + Widget _buildProgressRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildProgressChildren(), + ); + + List _buildProgressChildren() => + [_buildProgressStatusWrapper(), horizontalSpaceSmall, _buildProgress()]; + + Widget _buildProgressStatusWrapper() => Expanded( + child: _buildProgressStatus(), + ); + + Widget _buildProgressStatus() => const CustomLinearProgressIndicator( + progress: 0, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey); + + Widget _buildProgress() => const Text( + '0/0', + style: TextStyle(color: kcDarkGrey), + ); + + Widget _buildActionButtonWrapper( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + Container( + height: 40, + margin: const EdgeInsets.symmetric(horizontal: 15), + child: _buildActionButtons(context: context, viewModel: viewModel), + ); + + Widget _buildActionButtons( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + Row( + children: [ + _buildLessonButtonWrapper(viewModel), + horizontalSpaceSmall, + _buildPracticeButtonWrapper(context: context, viewModel: viewModel), + ], + ); + + Widget _buildLessonButtonWrapper(CourseUnitViewModel viewModel) => Expanded( + child: _buildLessonButton(viewModel), + ); + + Widget _buildLessonButton(CourseUnitViewModel viewModel) => + CustomElevatedButton( + height: 15, + borderRadius: 12, + onTap: onLessonTap, + text: 'View Module', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); + + Widget _buildPracticeButtonWrapper( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + Expanded( + child: _buildPracticeButton(context: context, viewModel: viewModel), + ); + + Widget _buildPracticeButton( + {required BuildContext context, + required CourseUnitViewModel viewModel}) => + CustomElevatedButton( + height: 15, + borderRadius: 12, + onTap: onPracticeTap, + text: 'View Practices', + backgroundColor: kcWhite, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + ); + + Widget _buildSheet(CourseUnitViewModel viewModel) => FinishPracticeSheet( + onTap: viewModel.pop, + ); + + Widget _buildCourseModulesState(CourseUnitViewModel viewModel) => + viewModel.busy(StateObjects.courseModules) + ? _buildProgressIndicator() + : _buildCourseModules(viewModel); + + Widget _buildProgressIndicator() => const Center( + child: CustomCircularProgressIndicator(color: kcPrimaryColor), + ); + + Widget _buildCourseModules(CourseUnitViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: unit.modules?.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => + _buildCourseModuleCard(unit.modules?[index].name ?? ''), + ); + + Widget _buildCourseModuleCard(String title) => + CourseModuleTileSmall(title: title, status: ProgressStatuses.completed); + + // Widget _buildContainerShaderState() => status == ProgressStatuses.pending + // ? _buildContainerShaderWrapper() + // : Container(); + + Widget _buildContainerShaderWrapper() => Positioned.fill( + child: _buildContainerShader(), + ); + + Widget _buildContainerShader() => Container( + decoration: BoxDecoration( + color: kcWhite.withOpacity(0.5), + borderRadius: BorderRadius.circular(5), + ), + ); +} diff --git a/lib/ui/widgets/large_app_bar.dart b/lib/ui/widgets/large_app_bar.dart index 9d2c675..caaf408 100644 --- a/lib/ui/widgets/large_app_bar.dart +++ b/lib/ui/widgets/large_app_bar.dart @@ -4,6 +4,7 @@ import 'package:yimaru_app/ui/widgets/app_bar_pattern.dart'; import 'package:yimaru_app/ui/widgets/language_button.dart'; class LargeAppBar extends StatelessWidget { + final String? language; final bool showBackButton; final GestureTapCallback? onPop; final bool showLanguageSelection; @@ -14,6 +15,7 @@ class LargeAppBar extends StatelessWidget { {super.key, this.onPop, this.onClose, + this.language, this.onLanguage, required this.showBackButton, required this.showLanguageSelection}); @@ -72,8 +74,8 @@ class LargeAppBar extends StatelessWidget { : _buildCloseButton()); Widget _buildLanguageSelector() => LanguageButton( - language: 'EN', onTap: onLanguage, + language: language ?? '', ); Widget _buildCloseButton() => IconButton( diff --git a/lib/ui/widgets/option_text_divider.dart b/lib/ui/widgets/option_text_divider.dart index de62133..9441c7b 100644 --- a/lib/ui/widgets/option_text_divider.dart +++ b/lib/ui/widgets/option_text_divider.dart @@ -1,6 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import '../common/app_colors.dart'; +import '../common/translations/locale_keys.g.dart'; import '../common/ui_helpers.dart'; class OptionTextDivider extends StatelessWidget { @@ -22,9 +24,9 @@ class OptionTextDivider extends StatelessWidget { Widget _buildDivider() => const Divider(color: kcVeryLightGrey); - Widget _buildOrText() => const Text( - 'or', + Widget _buildOrText() => Text( + LocaleKeys.or.tr(), + style: style14MG400, textAlign: TextAlign.center, - style: TextStyle(color: kcMediumGrey), ); } diff --git a/lib/ui/widgets/overall_learn_progress.dart b/lib/ui/widgets/overall_progress.dart similarity index 92% rename from lib/ui/widgets/overall_learn_progress.dart rename to lib/ui/widgets/overall_progress.dart index e79c4cf..3de6a24 100644 --- a/lib/ui/widgets/overall_learn_progress.dart +++ b/lib/ui/widgets/overall_progress.dart @@ -3,11 +3,11 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; -class OverallLearnProgress extends StatelessWidget { +class OverallProgress extends StatelessWidget { final int progress; final Color backgroundColor; final Color indicatorBackgroundColor; - const OverallLearnProgress( + const OverallProgress( {super.key, required this.progress, required this.backgroundColor, @@ -63,8 +63,8 @@ class OverallLearnProgress extends StatelessWidget { backgroundColor: indicatorBackgroundColor, ); - Widget _buildSubtitle() => const Text( + Widget _buildSubtitle() => Text( 'Keep up the great work! You\'re doing amazing.', - style: TextStyle(color: kcDarkGrey), + style: style14DG500, ); } diff --git a/lib/ui/widgets/register_for_account.dart b/lib/ui/widgets/register_for_account.dart index 933712b..7a403ff 100644 --- a/lib/ui/widgets/register_for_account.dart +++ b/lib/ui/widgets/register_for_account.dart @@ -1,4 +1,6 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart'; import '../common/app_colors.dart'; import '../common/ui_helpers.dart'; @@ -18,9 +20,9 @@ class RegisterForAccount extends StatelessWidget { ], ); - Widget _buildLeadingText() => const Text( - 'Don’t have an account? ', - style: TextStyle(color: kcMediumGrey), + Widget _buildLeadingText() => Text( + '${LocaleKeys.dont_have_account.tr()} ', + style: style14MG400, ); Widget _buildRegisterTextButton() => TextButton( @@ -31,8 +33,8 @@ class RegisterForAccount extends StatelessWidget { child: _buildRegisterText(), ); - Widget _buildRegisterText() => const Text( - 'Register', - style: TextStyle(color: kcPrimaryColor), + Widget _buildRegisterText() => Text( + LocaleKeys.register.tr(), + style:style14P400 , ); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 80a3442..2f01177 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,6 +17,7 @@ import flutter_secure_storage_darwin import google_sign_in_ios import package_info_plus import record_macos +import shared_preferences_foundation import sqflite_darwin import url_launcher_macos import video_player_avfoundation @@ -35,6 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 6ee6eba..34535e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -377,6 +377,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed" + url: "https://pub.dev" + source: hosted + version: "3.0.8" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" email_validator: dependency: "direct main" description: @@ -654,6 +670,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_native_splash: dependency: "direct main" description: @@ -1472,6 +1493,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a4a743a..ce5ce46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: yimaru_app -version: 0.1.15+17 +version: 0.1.16+18 publish_to: 'none' description: A new Flutter project. @@ -9,6 +9,7 @@ environment: dependencies: flutter: sdk: flutter + http: any intl: any dio: ^5.9.0 path: ^1.9.1 @@ -37,6 +38,7 @@ dependencies: json_annotation: ^4.9.0 flutter_spinkit: ^5.2.2 stacked_services: ^1.1.0 + easy_localization: ^3.0.8 omni_datetime_picker: any json_serializable: ^6.8.0 waveform_recorder: ^1.8.0 @@ -54,7 +56,6 @@ dependencies: flutter_local_notifications: ^20.1.0 internet_connection_checker_plus: ^2.9.1+2 - http: any dev_dependencies: flutter_test: sdk: flutter @@ -74,6 +75,7 @@ flutter: assets: - assets/icons/ - assets/images/ + - assets/translations/ fonts: - family: Aeonik fonts: diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart index 2cdea1c..e6484fa 100644 --- a/test/helpers/test_helpers.dart +++ b/test/helpers/test_helpers.dart @@ -21,6 +21,7 @@ import 'package:yimaru_app/services/vimeo_service.dart'; import 'package:yimaru_app/services/url_launcher_service.dart'; import 'package:yimaru_app/services/phone_caller_service.dart'; import 'package:yimaru_app/services/learn_service.dart'; +import 'package:yimaru_app/services/localization_service.dart'; // @stacked-import import 'test_helpers.mocks.dart'; @@ -56,6 +57,7 @@ import 'test_helpers.mocks.dart'; MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), // @stacked-mock-spec ], ) @@ -86,6 +88,7 @@ void registerServices() { getAndRegisterLearnLessonService(); getAndRegisterLearnService(); getAndRegisterLearnService(); + getAndRegisterLocalizationService(); // @stacked-mock-register } @@ -275,6 +278,13 @@ MockLearnService getAndRegisterLearnService() { locator.registerSingleton(service); return service; } + +MockLocalizationService getAndRegisterLocalizationService() { + _removeRegistrationIfExists(); + final service = MockLocalizationService(); + locator.registerSingleton(service); + return service; +} // @stacked-mock-create void _removeRegistrationIfExists() { diff --git a/test/services/localization_service_test.dart b/test/services/localization_service_test.dart new file mode 100644 index 0000000..2d67d8c --- /dev/null +++ b/test/services/localization_service_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('LocalizationServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +}