From c222b3c67a96bc4adc40f439469df526f6114ffc Mon Sep 17 00:00:00 2001 From: BisratHailu Date: Mon, 12 Jan 2026 09:22:52 +0300 Subject: [PATCH] feat: Start learn phase --- .../android/app/src/main/AndroidManifest.xml | 3 +- .../yimaru_app/assets/files/terms.txt | 188 +++++ .../yimaru_app/assets/icons/flag.png | Bin 0 -> 1408 bytes .../yimaru_app/assets/icons/google.png | Bin 0 -> 713 bytes .../yimaru_app/assets/icons/logo.svg | 37 +- .../yimaru_app/assets/images/coming_soon.png | Bin 0 -> 29484 bytes .../yimaru_app/assets/images/image_1.png | Bin 0 -> 161405 bytes .../yimaru_app/assets/images/profile.png | Bin 0 -> 9804 bytes StudioProjects/yimaru_app/lib/app/app.dart | 45 +- .../yimaru_app/lib/app/app.locator.dart | 9 + .../yimaru_app/lib/app/app.router.dart | 730 +++++++++++++++++- StudioProjects/yimaru_app/lib/main.dart | 23 +- .../yimaru_app/lib/models/user_model.dart | 22 + .../yimaru_app/lib/models/user_model.g.dart | 19 + .../yimaru_app/lib/services/api_service.dart | 171 ++++ .../lib/services/authentication_service.dart | 32 + .../yimaru_app/lib/services/dio_service.dart | 41 + .../lib/services/secure_storage_service.dart | 64 ++ .../yimaru_app/lib/ui/common/app_colors.dart | 15 +- .../lib/ui/common/app_constants.dart | 14 + .../yimaru_app/lib/ui/common/app_strings.dart | 51 ++ .../yimaru_app/lib/ui/common/enmus.dart | 7 + .../yimaru_app/lib/ui/common/ui_helpers.dart | 163 +++- .../ui/common/validators/form_validator.dart | 64 ++ .../validators/onboarding_form_validator.dart | 12 - .../account_privacy/account_privacy_view.dart | 158 ++++ .../account_privacy_viewmodel.dart | 21 + .../views/call_support/call_support_view.dart | 125 +++ .../call_support/call_support_viewmodel.dart | 9 + .../ui/views/downloads/downloads_view.dart | 218 ++++++ .../views/downloads/downloads_viewmodel.dart | 42 + .../lib/ui/views/home/home_view.dart | 117 ++- .../lib/ui/views/home/home_viewmodel.dart | 36 +- .../lib/ui/views/language/language_view.dart | 115 +++ .../ui/views/language/language_viewmodel.dart | 35 + .../lib/ui/views/learn/learn_view.dart | 84 ++ .../lib/ui/views/learn/learn_viewmodel.dart | 33 + .../views/learn_level/learn_level_view.dart | 87 +++ .../learn_level/learn_level_viewmodel.dart | 29 + .../views/learn_module/learn_module_view.dart | 114 +++ .../learn_module/learn_module_viewmodel.dart | 38 + .../lib/ui/views/login/login_view.dart | 99 +++ .../lib/ui/views/login/login_view.form.dart | 269 +++++++ .../lib/ui/views/login/login_viewmodel.dart | 161 ++++ .../views/login/screens/login_otp_screen.dart | 159 ++++ .../screens/login_with_email_screen.dart | 201 +++++ .../login_with_phone_number_screen.dart | 157 ++++ .../ui/views/onboarding/onboarding_view.dart | 143 ++-- .../onboarding/onboarding_view.form.dart | 14 +- .../onboarding/onboarding_viewmodel.dart | 7 +- ...dart => assessment_completion_screen.dart} | 12 +- ...re.dart => assessment_failure_screen.dart} | 25 +- ...ntro.dart => assessment_intro_screen.dart} | 25 +- ...ult.dart => assessment_result_screen.dart} | 23 +- ...dart => first_assessment_form_screen.dart} | 28 +- ...art => fourth_assessment_form_screen.dart} | 14 +- ...lysis.dart => result_analysis_screen.dart} | 17 +- ...ent.dart => retake_assessment_screen.dart} | 23 +- ...art => second_assessment_form_screen.dart} | 19 +- ...t_lesson.dart => start_lesson_screen.dart} | 15 +- ...dart => third_assessment_form_screen.dart} | 14 +- ...p_form.dart => age_group_form_screen.dart} | 31 +- ...e_form.dart => challenge_form_screen.dart} | 43 +- ...m.dart => country_region_form_screen.dart} | 32 +- ...> educational_background_form_screen.dart} | 32 +- ...e_form.dart => full_name_form_screen.dart} | 34 +- ...rm.dart => learning_goal_form_screen.dart} | 29 +- ....dart => learning_reason_form_screen.dart} | 46 +- ..._form.dart => occupation_form_screen.dart} | 35 +- ...topic_form.dart => topic_form_screen.dart} | 45 +- .../onboarding/screens/language_selector.dart | 24 +- ...welcome.dart => first_welcome_screen.dart} | 17 +- ...elcome.dart => second_welcome_screen.dart} | 13 +- ...welcome.dart => third_welcome_screen.dart} | 18 +- .../ongoing_progress_view.dart | 97 +++ .../ongoing_progress_viewmodel.dart | 21 + .../privacy_policy/privacy_policy_view.dart | 93 +++ .../privacy_policy_viewmodel.dart | 23 + .../lib/ui/views/profile/profile_view.dart | 147 ++++ .../ui/views/profile/profile_viewmodel.dart | 32 + .../profile_detail/profile_detail_view.dart | 569 ++++++++++++++ .../profile_detail_view.form.dart | 278 +++++++ .../profile_detail_viewmodel.dart | 86 +++ .../lib/ui/views/progress/progress_view.dart | 166 ++++ .../ui/views/progress/progress_viewmodel.dart | 65 ++ .../lib/ui/views/register/register_view.dart | 111 +++ .../ui/views/register/register_view.form.dart | 308 ++++++++ .../ui/views/register/register_viewmodel.dart | 310 ++++++++ .../screens/create_password_screen.dart | 255 ++++++ .../screens/register_with_email_screen.dart | 147 ++++ .../register_with_phone_number_screen.dart | 162 ++++ .../screens/registration_otp_screen.dart | 178 +++++ .../lib/ui/views/startup/startup_view.dart | 16 +- .../ui/views/startup/startup_viewmodel.dart | 14 +- .../lib/ui/views/support/support_view.dart | 99 +++ .../ui/views/support/support_viewmodel.dart | 18 + .../telegram_support_view.dart | 152 ++++ .../telegram_support_viewmodel.dart | 9 + .../terms_and_conditions_view.dart | 109 +++ .../terms_and_conditions_viewmodel.dart | 11 + .../lib/ui/widgets/birthday_selector.dart | 102 +++ .../lib/ui/widgets/circular_icon.dart | 24 + .../lib/ui/widgets/coming_soon.dart | 48 ++ .../lib/ui/widgets/course_level_card.dart | 107 +++ .../ui/widgets/course_progress_section.dart | 131 ++++ .../lib/ui/widgets/custom_back_button.dart | 20 + .../custom_circular_progress_indicator.dart | 16 + .../lib/ui/widgets/custom_column.dart | 36 + .../lib/ui/widgets/custom_cursor.dart | 23 + .../lib/ui/widgets/custom_dropdown.dart | 18 +- .../ui/widgets/custom_elevated_button.dart | 48 +- .../lib/ui/widgets/custom_form_label.dart | 16 + .../ui/widgets/custom_large_radio_button.dart | 4 +- .../custom_linear_progress_indicator.dart | 33 + .../lib/ui/widgets/custom_list_tile.dart | 70 ++ .../ui/widgets/custom_small_radio_button.dart | 4 +- .../lib/ui/widgets/download_card.dart | 149 ++++ .../lib/ui/widgets/language_button.dart | 4 +- .../lib/ui/widgets/large_app_bar.dart | 76 ++ .../lib/ui/widgets/learn_app_bar.dart | 78 ++ .../lib/ui/widgets/learn_level_tile.dart | 111 +++ .../lib/ui/widgets/learn_module_tile.dart | 197 +++++ .../lib/ui/widgets/learn_sub_level_tile.dart | 136 ++++ .../ui/widgets/learning_progress_card.dart | 106 +++ .../lib/ui/widgets/login_account.dart | 38 + .../lib/ui/widgets/obscure_password.dart | 31 + .../lib/ui/widgets/onboarding_app_bar.dart | 78 -- .../lib/ui/widgets/option_text_divider.dart | 30 + .../ui/widgets/overall_learn_progress.dart | 70 ++ .../ui/widgets/page_loading_indicator.dart | 60 ++ .../lib/ui/widgets/phone_number_prefix.dart | 44 ++ .../lib/ui/widgets/privacy_policy_tile.dart | 80 ++ .../lib/ui/widgets/profile_card.dart | 68 ++ .../lib/ui/widgets/profile_image.dart | 47 ++ .../lib/ui/widgets/progress_status.dart | 29 + .../lib/ui/widgets/register_for_account.dart | 38 + .../lib/ui/widgets/skill_progress.dart | 100 +++ .../lib/ui/widgets/small_app_bar.dart | 42 + .../lib/ui/widgets/suggestion_card.dart | 42 + .../lib/ui/widgets/support_card.dart | 52 ++ .../lib/ui/widgets/validator_list_tile.dart | 39 + .../lib/ui/widgets/view_profile_button.dart | 35 + .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 4 + StudioProjects/yimaru_app/pubspec.lock | 307 +++++++- StudioProjects/yimaru_app/pubspec.yaml | 20 +- .../yimaru_app/test/helpers/test_helpers.dart | 43 +- .../test/helpers/test_helpers.mocks.dart | 437 ++++++++--- .../test/services/api_service_test.dart | 11 + .../services/authentication_service_test.dart | 11 + .../test/services/dio_service_test.dart | 11 + .../services/secure_storage_service_test.dart | 11 + .../account_privacy_viewmodel_test.dart | 11 + .../call_support_viewmodel_test.dart | 11 + .../viewmodels/downloads_viewmodel_test.dart | 11 + .../test/viewmodels/home_viewmodel_test.dart | 8 - .../learn_level_viewmodel_test.dart | 11 + .../learn_module_viewmodel_test.dart | 11 + .../test/viewmodels/learn_viewmodel_test.dart | 11 + .../test/viewmodels/login_viewmodel_test.dart | 11 + .../ongoing_progress_viewmodel_test.dart | 11 + .../privacy_policy_viewmodel_test.dart | 11 + .../profile_detail_viewmodel_test.dart | 11 + .../viewmodels/profile_viewmodel_test.dart | 11 + .../viewmodels/progress_viewmodel_test.dart | 11 + .../viewmodels/register_viewmodel_test.dart | 11 + .../viewmodels/support_viewmodel_test.dart | 11 + .../telegram_support_viewmodel_test.dart | 11 + .../terms_and_conditions_viewmodel_test.dart | 11 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 172 files changed, 11140 insertions(+), 659 deletions(-) create mode 100644 StudioProjects/yimaru_app/assets/files/terms.txt create mode 100644 StudioProjects/yimaru_app/assets/icons/flag.png create mode 100644 StudioProjects/yimaru_app/assets/icons/google.png create mode 100644 StudioProjects/yimaru_app/assets/images/coming_soon.png create mode 100644 StudioProjects/yimaru_app/assets/images/image_1.png create mode 100644 StudioProjects/yimaru_app/assets/images/profile.png create mode 100644 StudioProjects/yimaru_app/lib/models/user_model.dart create mode 100644 StudioProjects/yimaru_app/lib/models/user_model.g.dart create mode 100644 StudioProjects/yimaru_app/lib/services/api_service.dart create mode 100644 StudioProjects/yimaru_app/lib/services/authentication_service.dart create mode 100644 StudioProjects/yimaru_app/lib/services/dio_service.dart create mode 100644 StudioProjects/yimaru_app/lib/services/secure_storage_service.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/common/app_constants.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/common/enmus.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/common/validators/form_validator.dart delete mode 100644 StudioProjects/yimaru_app/lib/ui/common/validators/onboarding_form_validator.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/language/language_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/language/language_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/learn/learn_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/learn/learn_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/login/login_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/login/login_view.form.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/login/login_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/login/screens/login_otp_screen.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_email_screen.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_phone_number_screen.dart rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{assessment_completion.dart => assessment_completion_screen.dart} (91%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{assessment_failure.dart => assessment_failure_screen.dart} (85%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{assessment_intro.dart => assessment_intro_screen.dart} (84%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{assessment_result.dart => assessment_result_screen.dart} (86%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{first_assessment_form.dart => first_assessment_form_screen.dart} (81%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{fourth_assessment_form.dart => fourth_assessment_form_screen.dart} (91%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{result_analysis.dart => result_analysis_screen.dart} (81%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{retake_assessment.dart => retake_assessment_screen.dart} (85%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{second_assessment_form.dart => second_assessment_form_screen.dart} (88%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{start_lesson.dart => start_lesson_screen.dart} (89%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/{third_assessment_form.dart => third_assessment_form_screen.dart} (91%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{age_group_form.dart => age_group_form_screen.dart} (81%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{challenge_form.dart => challenge_form_screen.dart} (81%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{country_region_form.dart => country_region_form_screen.dart} (80%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{educational_background_form.dart => educational_background_form_screen.dart} (81%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{full_name_form.dart => full_name_form_screen.dart} (78%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{learning_goal_form.dart => learning_goal_form_screen.dart} (83%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{learning_reason_form.dart => learning_reason_form_screen.dart} (79%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{occupation_form.dart => occupation_form_screen.dart} (78%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/{topic_form.dart => topic_form_screen.dart} (80%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/{first_welcome.dart => first_welcome_screen.dart} (86%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/{second_welcome.dart => second_welcome_screen.dart} (88%) rename StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/{third_welcome.dart => third_welcome_screen.dart} (86%) create mode 100644 StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/profile/profile_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/profile/profile_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.form.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/progress/progress_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/progress/progress_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/register/register_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/register/register_view.form.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/register/register_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/register/screens/create_password_screen.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_email_screen.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_phone_number_screen.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/register/screens/registration_otp_screen.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/support/support_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/support/support_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_view.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_viewmodel.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/birthday_selector.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/circular_icon.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/coming_soon.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/course_level_card.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/course_progress_section.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/custom_back_button.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/custom_circular_progress_indicator.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/custom_column.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/custom_cursor.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/custom_form_label.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/custom_linear_progress_indicator.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/custom_list_tile.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/download_card.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/large_app_bar.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/learn_app_bar.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/learn_level_tile.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/learn_module_tile.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/learn_sub_level_tile.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/learning_progress_card.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/login_account.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/obscure_password.dart delete mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/onboarding_app_bar.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/option_text_divider.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/overall_learn_progress.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/page_loading_indicator.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/phone_number_prefix.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/privacy_policy_tile.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/profile_card.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/profile_image.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/progress_status.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/register_for_account.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/skill_progress.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/small_app_bar.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/suggestion_card.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/support_card.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/validator_list_tile.dart create mode 100644 StudioProjects/yimaru_app/lib/ui/widgets/view_profile_button.dart create mode 100644 StudioProjects/yimaru_app/test/services/api_service_test.dart create mode 100644 StudioProjects/yimaru_app/test/services/authentication_service_test.dart create mode 100644 StudioProjects/yimaru_app/test/services/dio_service_test.dart create mode 100644 StudioProjects/yimaru_app/test/services/secure_storage_service_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/account_privacy_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/call_support_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/downloads_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/learn_level_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/learn_module_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/learn_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/login_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/ongoing_progress_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/privacy_policy_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/profile_detail_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/profile_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/progress_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/register_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/support_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/telegram_support_viewmodel_test.dart create mode 100644 StudioProjects/yimaru_app/test/viewmodels/terms_and_conditions_viewmodel_test.dart diff --git a/StudioProjects/yimaru_app/android/app/src/main/AndroidManifest.xml b/StudioProjects/yimaru_app/android/app/src/main/AndroidManifest.xml index 6561fcc..cd0a3f9 100644 --- a/StudioProjects/yimaru_app/android/app/src/main/AndroidManifest.xml +++ b/StudioProjects/yimaru_app/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + + + + + + Terms and Condition + + + + + + +
+ +
+
+
Terms and Condition
+
+ +
Last updated: October 26, 2025
+ + +

Introduction

+

+ Welcome to Yimaru! These terms and conditions outline the rules and + regulations for the use of our application. By accessing this app, + we assume you accept these terms and conditions. +

+ + +

User Accounts

+

+ When you create an account with us, you must provide us with information + that is accurate, complete, and current at all times. Failure to do so + constitutes a breach of the Terms, which may result in immediate + termination of your account on our Service. +

+ + +

Content & Services

+

+ Our Service allows you to access learning materials. You are granted a + limited license to access and use the app content for personal, + non-commercial purposes. You agree not to: +

+ +
    +
  • Reproduce, duplicate, copy, or sell any material from the app.
  • +
  • Redistribute content from Yimaru.
  • +
  • Use the app in any way that is damaging or harmful.
  • +
+ + +

Privacy Policy

+

+ Your privacy is important to us. Please read our + Privacy Policy to understand how we collect, use, + and share information about you. +

+ + +

Contact Us

+

+ If you have any questions about these Terms, please contact us at + support@yimaru.et. +

+ +
+
+ + + + + + diff --git a/StudioProjects/yimaru_app/assets/icons/flag.png b/StudioProjects/yimaru_app/assets/icons/flag.png new file mode 100644 index 0000000000000000000000000000000000000000..b0c75c7f9df5761399e3af03adf3c6d6987cbad3 GIT binary patch literal 1408 zcmV-`1%LX9P)P000>X1^@s6#OZ}&00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yPu_~fP9wu#Vt=2ZtkeEtLOD1e%yM)x^f8 zX`1v$(Y8M{5-qJ_qeW0#YYDPgst5w3$on-=9G6{oZto1tu)Bcbne5Jeocn#}eCOUf z1dlOWq1z^#k_&eT(=pVY}fkPsWn>k zs=cth3a+>wL`4p3{pAVwSP+l2Hs72Dt}QKug~|V}dJOoX`kDC;q&gH=W?=QAdNg#zLx}4{Np+&(8{3kL;=EGC%rLUIxn}vZc}qkf zE6J)DSzN5l4vmIAHTROSX5n4dQ*i#~N_?>C3}P%YN^Ycw_ztmJU9Zi+-rQ1SjDTz< zfyNot4BV{_H1*iArr-!H{0KgJxfu4C0aibvkvpsd7?5Yd8{n{7xQoi>Bn@QPXU3pB z6ICsVILorlUFN~by@!KHAo4naYWKSxs*v#dO*s17R_t6~0hzVGzq}FO?*1Ad{;-Gj zc_v>a`i7$MF9Y8HYB5>|XKMti)ryP4lz@#79_Sg0869Re=$(OYet!{fWnI9>ubhV0 zXMyaC&_cU>XDJTtIF3k@4+l^0Kx4Plz~~;bBFt>+?lMl&EQ>cJgjlw#h7~is?98Mk zh)8mFY9Z`eSB}N;ZTRWh^APM^;E7@^i;ErzCjrM4H%L+)4>a`ov0d22BuSaqg5y`R zFn>-rUi<0*-pnk++4m2kzRQVIm22_P%W5IIkn?OUa+ciGFp?eZAp|UO4BV}pIvuizW`PwHeh~C56W&W!%Hiw@m={w1{9KT#_iUU3-*Z>|xFY;*W?Q?$f*upW8N0 zUaU$MQr0wm~R#K*>0FuM~Q-^>o4p!X%-ibD(Dz{S{AA(A9J zI?>SSF#0nCGTv@0#rCc;EkNBUrD957yDNm`w#_N)lsi_w|Pa!ltEwqNJrt9byQHYW22xZo=VGIFV`!yIwlXv$Z8A zB>=l$8auX_K){@H7E7%Yzl^~daUA=A1xYtuQzcRqnxkhhC5di#;^r1Yp}VF<#-dj@ zPfAZ);bha~N$YRVaprR%*{rB5n77*iB~)T#rW4W{(4_C$>E3>ffrg9A)_(2SBHVqZ(-Mo z4>G0xDS3m}3XDa6nP_95OAsafrwq;Bkh-~61}{?6oLZdTlb5lt&DTLL3(O~o45xkt zJRoLA4rN1t!9PZO4Ny#E2cGGn2y7RKRV(bUh{S@TBICR32O(D*xdgDF*?SAugI&{q z^48jtusC7en+>ieT3^5ki>sy|u6>WeOl?H2e)`z_cmh0-xcyb>ByW-#-2wq$U7h$A zuNL)i`$0zc-9#igGy9ipX;sS%wk+E8xuf%E(+qLeP9%N{DiWXbL35o9xsw=+Gkth+ z;N8^_8v9$%n)P}_2vi&eB#z!WHzSglVT)jbJn-ov_-mum{A^)*cc!!6XK#3a@6E&T z_@O}|avZh*Vn4tLK9_TpeR3t1vDbSZIMOb$b#EY`0&W7*HiW5H7Z&nPb=M=)5@HO; v#5RsvVgyt{#FELWX#O - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StudioProjects/yimaru_app/assets/images/coming_soon.png b/StudioProjects/yimaru_app/assets/images/coming_soon.png new file mode 100644 index 0000000000000000000000000000000000000000..0bc8cb8e129c20c8a6b140a870dc5cfedf1341c2 GIT binary patch literal 29484 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�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 az?ONNrRc#8yuQON!Q`b?q#7j5!v6=w9+IB` literal 0 HcmV?d00001 diff --git a/StudioProjects/yimaru_app/assets/images/image_1.png b/StudioProjects/yimaru_app/assets/images/image_1.png new file mode 100644 index 0000000000000000000000000000000000000000..6eab34f161ee203449302c8cd963c5cfbc13302e GIT binary patch literal 161405 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%0DQcRvN2&MgaAQ|`o7-}5Z$-<&MXrqo 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

&8HxqB6|fe)Q7^P+7$D3gPn9 zzd`HFFZ|3GxOv8o)Mad6U9*yLdHLxbP3MPYa%9{}IjVyOa08%4V9M%EI`N%5e~y6k z0}nnRcied==lxu}eogV0qHd-&(&16rGFhDV&|YQiy;5vUvn~MM>zY}tZLZ2z9c;~# zZt!i{*jnSnMOvq7MzXrGLW~FC?~-CeATogOT$#NpFP(oycG`8R>+?sCQ#eS`6N-RR z^tBL}JnX$~PW!V`7bQ>0O_R{w-mTaWnqM(qn6t3OK!$XF%?v{ zwD9k^Z(}3Ve44CIoAoME`_Ok25k&PCY`TRPnQj}vn{h0GO*rcWsnF{p1WkqPN*K|| zw9Ic7J`2FfBsXRX3SjX*hRB1DsdWRM<3bthTZ&!c6$iiC#3lhCXd;_wij7b}aE%g| zxm!7e&w~@IP_mZu#K%2EZ3_<^%I%f|O}3dAs0TFDZkv(92h5b;0v61{ITAK8=Py1V zh_fQe9pNmqiJhU#@{$yb>)r2uudK~3$>hL@Wiwfpi>1t?MT2RVKAS$>Wa|xz5bHGa z)|0uI$}=-(<%R1PXiNCugAdDR{?ZrZU;Y1nSw8cgX z@t2+LO=tx?oYr#C~j*toiIr`WNoKpYHDOg zSeMwN0Ec78*utpNx{oNqdh1hc2c;SgHPg`uvmDG-V&$Umi33nep3yI{&f>72Mh@hm z@hP0DNibY;a5hQI4IL55)TEiUY`Z+XLpPbu1DmMqo7N0|#IuWp9h`+dU}~T~18B=d zT{5aU_6STca!;Bc{%iDeXGU-cfcRN-qEf)?Ky186ex7&b`V_ms&i3zUpUqxOr+!9uXV+wZ|CsE+3>U-7#o8o7 zpI4==flbp|1V=DC83kl*Sy4*!H-7LfY3ucFC>Z-Me(B%nj}I!vd0HNRM>nSc7V3A( zSHJq4{N%?!#x$QE!wnnGedU$cWJIx`bzSWH?zjVh0H0NLa!lH~v9~W>m7DLqQ%}=T zx%9?0#W;Fo|J1kw#IfN`08jO1UVrgr1!srQz6#}|3@M#Vdq#|h&@3iyn^h#F{h*SE z6)Vw)Ej7&&05W1j^eO0g;OJpFcJfXQB9F?$k3FUtkp`qvpKR#n2ED(l&!MZQi$MC0 zmV46^(~2t$%ht}i0_fJrhpYh>pfnvv*$b8^X|lY5c@&h<-9>BUT;JW)k_nzeEozLA z!E9&=!6rw&_7~oGNzPw8$Btu^Q8rYsn3EW6rzn=a%RD0sWsGEj%pi0nv{gmBi_0#+ zU`o^*>nNz|_D_rN3!v^K8$@Ug?hPuXDK~;im@>vpBHp_JWBPWW2LMsG?b!`CoV0S8x|!mdQ+N_eAYwHIXIxTq zODlv7r<5DS;8!Ed=-Gg;rJfy0V2*tDEp94gzeV4Kk#CmF0c6&f70aALh7>$&@qT8} z(D88POUA)N#5Nz9rlQ>+q>I5X2H%70z&+>)%@U@*Jf+Yt#M(G!hJowBDZoXhi@QWS zLt{?p4O%ox9YUk3Yk0bJG3;z?&;YsO85y&qVgTB#iyHw9&xn322?%esWNmq9qyXdQiUnYv*KmTsK~Cm)w9<-6h`Lx-o%!oa`Hs zGgr^abl-riluWaO`+MwxyX4C6euJ6BW_^q6GGSjyn=Sqz_mxwWLw7$QbFaN2wHpgE zeqdbI=T>Ay15(dm4+mDYV1uSXi#i&V^sp_^z%Vg7Nh79XCvKPH_ue7nN z*_2s%_54f9IBkS3qrWK%OBP_giWxwd#+nu|6uv60rPiNSqCDb{M3 zti!QTuP`Fs69kLMwPU6$E93_(;RSbI#J)ny0zSLPN|oe z?Q~&2t#?`HGnS$#?=~fyr>|RU?6BE!ZDCz*nw*jyuoD%fG&O2aqMjMs23L3dm!o6# zIaL(D*wSYP?~cb`e~GVmV|hkC`EA*J@A5*4(&!_xpb=|LkA?Q<^Q+2GzLR7NUL zoY5Ko)Vtp=SD$%WYMRv)dk17=X;p^zjVbV`Own$_z8s9;z#*{(xA%sYIx7v`7(H}6 zd;W*d%5DRtsNyP(EzL%jbo1;mGsL~aNj#t!(9rOhk`Y~e)?h5L8PR+=)ITDRKJs3< ze*Lnn%-_)WS5Q`iMsG2pvD1LQ&&1$_48n{EWj%B>3TDqKZo^C%SXfV4agrL#h^^fX z*=*IM3CjY-bq0qAHA`%I!O2|p9ht4Ct}ZW;ZDL}uD=-Gt+7Pf_u;=h^z_}9d zZ3V> zAIhd@6W9r0U66K{i#Glu$ugg7W2!<<4zz|zD^FZMQtmR}4UF&<-}J~9@)AcP4%H%+ z$5hS?fwS1A-qA=mDtdO*@mk3n1YJ?P7wc(q_<6V0mT~Y;wVy>+Mc<58!r8c?&j^iS zDAPFlRDrBAMRFM@9NO@%O5WSl_J@Ldgw}dw6r{>MLPA3VDiC!#GAE?Fv}cG6SFv%q zK7bn39KIhkpJz|(kk$uU;kEfa)6J7cQQq(uf+cV(y0>GXV!6P3#mS8shD=p$-ce-Z z7WkUzAf-61OA%ekjOx*!4wOF<(2x-f}X0E@aDTDCdiPJtL&rZYU;a1$P7Y%T z)!LS3C`!xfrtcZRAg=~9l-_=f1npEH*p@A3o?VJfB>I_7o*b0zse`hS6b7=By(pZE{V5oovE^%!b;UEg=L<;S8nR>Is0q1N;FqQF5s;*a)OiLGuy}pilw@m;2!6#(tY( zAvfd&G9G69=C+Yhl$%1)`7BMnJ=*}xQn8RTR^c=!C9SD_p;F9&Om_Zywu1+Tdl0&T zg9eAeF>RZRR^&WPWF=ne1GRAq642H>F@;Hx;~>R?V=HmwvsEnn+ugM}!n7$UC~HzS zV`XgmlB*w6Qpg4K1}-=N>>8fXjh%QI2tc~FQTnnVIjt^d#Ha+s&#RKX6PAo?Mds$22gc)AH)`k$mH;Kajutcm9@~ zJTWOV*H)PEnsBUwq(Zhdo9JzpWU_lyfz?sz#b9}Oxfnkn2^#d^2^*VND-5qF1e7R< zm57N%1p}o=j_#9V_neZAXU@sMq^9%p>oRfMhz#kP-nyX~hX#v+4B6bCa*4R!M!H2C zfTX-72M!+OJsMS}=-Ba_<=V_;nZI#WPvLFDCKTsrXjy-C>9R6Ct1>mRkEPJAW|C6} z4#?2hm>!}U#B}ZVYAFR~r=h_R+Z6}trUK9FN(nEnY{-Gb2Q|~Fa8dx)(%R;->@F(4 zq7l1ES$>0u?FLF7#aK49G%=l0D90l|R7)&hh0p?UyP|=%Z2at)qP1GFtSZn9>;!=Y zB`5Sikl`aBv6rq`;Nc0c9_Vqfx1fuO*h5WoX0 z&5tcp!656R4R^+6S?@|`7&O43FmlucYc>WE4+bY;po?*Ow;W{Ud$^#h<8)>{3{Vn= zmVm@5?jYzZxmh$zxJHY?3ZDzLF6#NFdyEDen>vj+m}aLu(lJ-L?&V(+y12!AE~DV< zS9IO}aHO8UtZRCpGJ+2+7a0hn*I!0~lw?JTC zURkE^%I4CV98d#~nNezU|Gq=AsvF_0 zk7+`gsQlf3_=`-B=Vv##I0NfW-Ux+r&)cAnM}qEhXXW;tfS zV9!?xA}^c!f+>t>IoQP99AwU|Cf6o56as47>3sC+B(;{@@z5jk_$%k7in44|*V#^6 z`nAlM(2{QX%4MZe*JPl_x_JlD*$bvgH`bD}G1o3!=4cemnHe4)mz!_8Q|9L`%d0Ov zCkwNe>7Lf;iP^id)mFOsiefc`qcSlxDcMZKQVq)6NdH^x8Io=dkc}j!=BC)wEdzRu zHGQu{v)~&_mq*GX4NQ#4rh>W4+gn=h!K$*U*;S1$fz6hU?XBw1Hhuk@grPI~1b^Pz z+V=LBQa*DTYXy@R%#=Em_8qrH{0i)7U+<8Q!Rhqi0fImZ8>|qD9 zE9Ak_`smZb!2^fC#CG^n(P!nf9rwWOnVL7t+2HQ55zMx~l~cv$sbD;JnEsB%jg4j- z+2-w7>EQ0!v8Ao;+|0$S5jZs4LmcrM7l~19=Nb&U+Q(KJ5N~mlYT_jb5Fxt}%W8tl zckn$4j#hi6`j$#ct`5cq2OdM|GP1R%)uf4UR8YN8;M#y`YqVp~^h|&Q#W}vxu9x_Q zNTF1x9Tgqb;kiAr& zlgfc9DIYz^G#c6Ju;LEePrW1~6~#I9duC=96}PC!{^}@qS%G3KZUl^R1wNqn zZS~?T9oCRN3~4#kH=&@he%4jZ8j6a`!~hb3H+U0gp9YqiW)B<7tC|_v{pnHsWn5XD zBm0hMX{MWcw?$Lvdb@6Hj+U8QS`I9%-k`rteW%Hd7;8W&Q$x~iLP!p2v$DWCy+tJZ#qi%KkK;(Hk|e=Wa_rAEt4!d zlURtXlRg~Cbvo;Jxz~y7WSR|@NvsOGy0}MSp51l>vVtcH`17uQ_WSR@H&>k_$l+&@ zZ~&jmVCX$QKKZ4hmkdafIE*|qATkWfB4wM|1}AD9)U=6BbVFSXNqi7o8NFKSZ1lEG zcE?7<2~%}_O6uhj`z#_m*$HqufNJz~Npk14*m`E@L=jndiydGsxOo%4p8=#;$Y1U;dX`^4ulIZ#%@3a$}>xP07!ZB{X76JXGxa^Kw`-h>4Lw4wayi8%&m#1ii`_VT1@+ zm2ssuuRQyzCFN-Yw;593A1_+|DS5JYef!18WuU`*RdUUt*qy=Z3dkv_*eLG7cGE|?-63>}R*)qwShL{mVHrq5%RS~IPZ=x#LXocXOW*LX6KiN&`BQ^tNnfhi4 zIuQ1vuph-5@4`uLdX`o$(4pXtE#^iW1ACxo2i?&{>wVz$7dqX@u`?RteLKNNHQ+i$ z$5`<1Emz&LbJ`&OB*|w7feJhp6>#v~&UOGJC@Nr%o4^OLp%hH6<-F zpF8imgSfHF?+j-z)du zcZ?@pbGOY!O;cxQ!>C=s3~B^mT{uG{!*cfQS?SlhzPHp%j}M%zqZ%xb9fO_+tJKn; z#enc`1y}LR;Bf+d1k8@kU%9G(9); z@OoH-R=-l*7@CD=$C=%_iD3tTAcMI*0}3*4FRsblg-bGj?Xv9XHMDvwGIjifyyM*; zl>6^^i%fSFG2v;59iPy)^b9o%ISqZ*iXyqF4{ksp9&5`1kRFMlH@0AV*4;AOB$Ib z01peJB}3bO*chP43|9%S$hzvA)>a4Mi+@6m}EF|i^Qsr zaWt(8%7e(qZqTC6Sq}COdB2YddOF?B=znOnjlK<3aKIp1P8qW_DO+$#sauIyY7y9U zMJwK{w3kjUn1@Knu#$FXRx&n77&8#YIZE8=aJJ0N`kIiL(b~X0arrnMoJ?IClQ?q~ zIe;2U1G?P9^08o+ebC%gd%I0{;$75x4Z}lr6?{O0;En1^FRk!exaW^P`Zn4RV9pAr z>jLa3`AHpxrbjYD=DdeFySN`0uU(Rvm3bS02Gy_9ukZQrhm?%~hzqRDI-t2$+!@~v z4_Dyg?;5Snz4D5@<=81LFLVucYrxYCqf0j)TPz@itXNM}L4(t9R3@v?*Vm&bcuRit zqbKBBKm0TK$N%P+r5;&xpBW*wd5yqk4n2C04Bc`>wv|3E==0j0S>R{un;w^aCyz>x z1`QlQIF;c?g3S|L?kl>qvppbQ?;nRUc+!ab$Y zZL%CV?I`;OWds@&8_MZOvjs4BDQv)+QgM`kM`rEMR2$NIGCtU}1rv^uLeSF;H5UX| zbQ~+N;}*_kMIg%h=X%plW&@VYM>b|o`O(*bSRJzc$QRzzSVClB!(#e*Kr z5#TtN7#fBG)9Q@rvRNat;K^dTUbS7~pNPqsyWAWquv&O99ahZyozl#WfeWm!@zW4` znSj-0|Cw(xb~q-w(Q7%KhtxYsnq)ulOl(r38)@e)5q1g#aE0o&>B2U=e5dWfeQ<$$ z)LT?P$KI2Je_xuPm-jyQZaKLBAWexZIZGJu%g%hToWnJmXoU9!EE4F|XD*!9lPFU}H-MdNgZ+ zI53grOEYrunU|!XSwp$6BHaq|_A2#zL^GD#Zn;x#KYUV-j~td#W_x2d*^&9xYjXOP z=M+<#W3Lia&S;Xw!N+;lT1xHevw8i@Y4++g^a1VY-*+@{1CYfqxrZKlKsGnmXc@Y> zvBusZOi4iLVc49=@Zhjaj*W8AgyQ_12%5xUPT16Ql^wEZZpq9IFTORxmhBi@sPMf*%0X14ut8r5-@gPOABtpxZSC}+%}%^sMw7(_WE#IYA81Gl`Iw5$UCCw1#W#$ras zg+QIOZHz0@P0OAnFA<@dh51s!@vX$E-MCP29Z@UCdzdY@-3%%=8&0S^Kl|xV>q1|o zd_9KMCUP0{N+AfKR>#l5+Og5%f@;*;%tFu1UXf?7o|Yjk3wrcq+FVFD=pockc8h zX>DlAuH{`>%czQ$6qsm;F|*Y^P3NGT5mU)g|y>D zwrcCLQs0y%rIaxlVM{*`Ot+37yGf>|r{rs2|C)UDYyVxA6f8!YO2hlP8Xg!hvSmp# zA^hy>${N+X>+2gXVl$_E>}RY|8{AcO#63{nwqyw!bYrc5be z1`}7s^tP4Q_InIK3u!-SPzr~{nt{3ra;2m$+fJuuS;wFn7ezU8W_5X&PgBE&Wk3p> z6Cb{1T9Gn`OE)ZVL^1t7fg2Ebbnqyo*dZ`goB;}KNZF|`{2c+r0;10ba$nmp`7HQ< z5c~w@<6aPvH_Ms*UEAux05p5B!Obpur!)gpc-=j2b+|TU4`CQr0HHfh`<7`W&KB{W z_&{@910~Cm8z$@hK#RH2qBe|+#x{;Qalo0=dXic9aTxC!8pZ5>XL%3`nGu2vbPbo4 zHvQPgK2G0|`o@k7b=k;-X3Ho;Vq0irqy{JQ|2l4@_xAg*J|#uX*5b6nnYvo-$Ok_D zle(GpooCwaoa_uYO(Lz3+AfgKJVy<|ZB}%iQL&jNEoYPdYbI zc1AH{>Dx_4Yz3Xo`kCMH(4%sE@{q`uuA>bthcx5CBtEQd1VU4XTB!$IYy94ma`2(M zWNmIye@59K%{E}OQPB0;HKajhSOH!=Sb7wX!2yMArHJPW>c5c~+o-!@WO7iJX0FM# z*Um_NZHoTT*bN|q= zpdehB-8@w1x>1!Nm|ku&29^lMq>DWR@rt3O3I<(ET>oLU{1E z#i0QlcxZ!xtiiM&9E)IQD+82!(lYv z6^t)WlFr;2Y|a)jfQ+Xkz_F;{jf2jRaE1va>xroj@$SqDGNWFDze;dy3(QK#$jObp zy&Yh$WrOKkg1J-F!6%&EX*QeAjM?W#J>M=^z&4Q7RJM@5;K+}rcn@X zVv}JGa$Lg5QQh>d(72jXd|BQ5 z#;Tsv&)<~OXJ69wR+iiEyRPmJ#aO>D7RWv8Me{$LimAF=e4>s%7jKSbq+TPdh6q?o->Kg(eaM3?u{C9Ao35 zykOhCZvcWcv{`J^pCO7&LlRChTfnyQGCvi=xW>-d%Tzi8xndJrwUWHEH;e_%M`C8i zsrRru(kBVJh$ql`38~+r8!pEVOciqlERkrvMr0rOyQxi62Bo&XCMY?u3^4S81p)_M zdj~N#Cf&yybwjw*G+(5qeKBMk@4>Xw1!`7ena2xie;1uKy4~T+SU{O$v0k-O?{8){nI?Ln)Os}eehP@AYEow zjn9-=pah(O_eDo4u#~ReemQgcv~0e7QBL+B)QzO;2CQ9oBo}U6mYW{BPc~t$RWuz_ z!5O>i^<_Ex`Wq5yiP9A-t^p>c*pjJR4$1JraXF(*QBw~J%$O!a7y0A`2gsS}pb4*- z$|GO=l)i6GcILG_QlPe7YZB8#;3`HoIKIja=}u*u2_96s7XB_+cVO3m>!As6@!ECS zT3li?W_SOfZpJ~&7;&Is%&UIh(cw`!GJQlpPnG>}^UK$0-UJH;cnPC2*s5>Ks?xMr zYZ&?lrU%af?E%0@5KwMDew{@9cvJpu)qeP ztD7Zz$>bTEoW&gzgM;R)#|M4^Lxb0Cm=ck@*n>LPFMwR>a>H*B8xal9j5el<8rUL6 zvtSV;eX1}+NO`g$%Rv7PBB8b8n*nsSVy@x2l|g1L{$XN$Fp~~JBDmz6Xe`P%5l$?k z+{9O+{KKS#dEB_6dy zkSjPv8nO;}skP_D@M?YGDl z??Xg@nD~{xZ?e28eG}udxV9p9oH(i3!6o_n*Z)ZFx%*Z&(*62x{y*~JpZE~*4wi+H z_X&B%$TAis%Z_fU$q~8tr#>YcOAE4o=?c}V#R%gvlr_`xCBpPeup){N^(odt7e15$ zeZ91ggbEg&#}y53ZGA4=ib2)aY_{+4zI{9hceb{9??E&~TB`L_dZnk-O<;bzz9EZC zvs_b`G&|W)pmuq4m51E6W({q}!LXjugJUC8Oj!*~<;L8c2Ibe~*_U3DE7xXptyPI@ zLA_l<7ktwUm4e4`Fo-vBHDYPl4(jyG)*SYI+3iQ7ppwrUYE2+Yy4xWI*cODW9tRJE zkDN9MOciQvw0;Z@4zZj7z|-4fF4iMj8={P8dCH1k2mpy^BjQr@Knb8$NuX|nG7kfr&=V8!z3XIlU=AZ`#|=MZKp?De6R-&pM@p_U z37)lh*a$kIGdN^V*t*u{FS%=co^9;bjgNOP&b>2#3i)gMv_UURsJ<zDf_%UAUD4{~8({lK!Kk(tUVD8j+ruy3pav0dV&0#j|p0 z{<_?I=bgGCnz~NYy-SWN zVENUrd{s`IyhZN2?|uTa_FRm@M+U{L*)z?eZanzT?tA!NnR@U6x$(wDsVU==YQ{oL z0A*|}KBTQcCWG22jx{Z_Tq}1&9JWi04^qKJEj`v15XC%Z;7WrdBMbto7=o_921du^ zkTPJ$51y3%-Z5F%0CM)?>x!#bckcY^qJpw|*wlAqTfwNT=vW%ZqUeF9*#bs)L|9KQ zE(LXu-~tQMhL%fxJtY|&=rZFhvB?L_DjiG>&TgApuC5?E((D#9x;Z-7JKDp#L3T2v zr6@~ufd7qpekg}7^+Cm)NE?@ppy9rVY|28B-prbD?{M0Dn~qH6{o~k0Tt(i{if-bH zW(nQB{Y>AfmW9Oum4!doeCZ_>QAJmbUTa84GjNf_hMwO-9de8 zvqKW<9B3=23$pCSyU+_%+=VS{?#LVWV!x)4vSPypY{eJ{^MD|Y>c-gY2F|>SbLA`L z{mxDlBXi!$JV?HIRB7xkca{x>mIC%Pq3?uqa*OOR#eXxKlJO?oC3Pu3E&Pf;Z4t&g zjhY9^U{Ym4gV>I@J+Lf@t*-(nX?XaNXFm8GNNMM9+>pQX^S>zjCMIQdb)9v7rc~b4 z#5BD@Py)hfy|vqtD?d6d(y#UZeaB?{!P_L78iI|8{MpM-$*5*0{rcHfFV9MIr!03o zdWY8GeauE!W~A15i2KIoODc97^40(Ld-CqFJET$_X zqeHO=B4ltHFyH_OrlpBsrRB-$^;y|oS~fDE+N1lRm(Lr4sY)iZAY;S(CMzE1;C7;W@aBs>{9STnEW2x?e}&y+nion6ch@1Mu886g#j@T`VL?pNb}zK&4adZ z6RgRw?Rr(CyHV=}dUx*t_HaW0&|+<5UY2KLzD(FuEZQ6}qO|roJ^p z>?Fr+g18O=2%y{BA9++h^6vL3b-KhkGbK#ULvIW;2YPBE@bkW-{E0O4S=p7%H?GNr zm(R-Z*o0hMoRhOFv+|zD-YMswdtQo-F2VSiN8kU}+`yQOnq+&}zY1IMfA?4ZA9=q9 zld(3sdn?RT5rDded*qd+b24!ENex;PQfQ|%#cs#en^Rua(xP!e4&Bl%x8F9UwD5Jg z(O8j_x8ExBGmG-#Q!mOV-upoXSvU5uTpfFh@XX0v`QZRLd`n9f4BvX&-~5ZRs#NRD zk6x6PVk5ZERgABwmMQkbCPbzQCa2VG0W+DriIlNI!q?AEN0 zDlnPs*mnjC?*V@qG+$b;Rm01ys+HUNvjZ;CH!D_@uie!8KrM~A%JA}m!WeRsEzFHZ zvcPF-W$Q4c)?X~rW8?L3ZfQr7j>v~h2BlEl`(9uV&agJ3c5gbn_l_;>bTIItTqn&1Z$X!szV z4;WP9buhtVpRv<+QB>sjz!bniN^P^K4U}$M4-|T<^naiK{Lk=oYI5|2g48e=(#4Ly zw^BRV5%x;G71};-ot>51r4?cQ%nB1v=Y;D^{ zf$*84d_!iU*($Q$ul(lk%ezNzlV1IvM$L2(sdMRT>86}sy(&k4;$3pBy-oTNzPK&D zK9ogEKYCsc9nl5bvqJ!6K%2kAt9O6kBk~`9?N{YDf9H4QM?d(XY$~vdWqcm5>63I7o#bUxxJPU%}?%-Oh*xd~AoH2N<%UfC&=H{ioZ4B73vNPLyy*v8nbuG8JuiEf1 zsS%n$b&z;az~0@__YB&U*c)cCzK|7%!@lSTW`VAQEglenLoj+YXj8a1s_q$$GSM3n zV|sc(LS3un%Fm!RvEf;9F7C1v$r|?vAkjd{=K0tO9+_=p($1+Hr}?lmYe2NA zf{_D;0rLpiT5FH5%w9M95I|eamSX{lcPwXA)@D43LblVP35z~YXLS_=*Kd(KPTtBB zx4pi_Huy}ly0EuihwZ0>?(`Sz51bqfh3A9{n%*;#wsUvdoJrBu+ z$DfiCI=?aAR0GQ3=m<@w*Ou2bd(nfc)}RLrfJr#aO&mB(Yy$?}FoD|KT-W<_K{vFP zlgiEv9h#Q@(Lv1sHe_Rajn}lkGcT8BPRoJ(KL*+M1!VxMeeq z*)S`nNCsP#ft7?Es2;k#y07{bFkV|{ zuy5u-u}0?CDqcQVpN!S^*qA730+zyhv}Hy{QQVoXh`V0!z!`Kb=1}J+QEK`V1L0z( zn}J5hXA4w(puTMulC_)L+-K{sg_>48=ml0O^$5G7fZU@GKP;uDeg;?v=mvwTGfE=I(v7g&kpK2C{-b>4wzo;Vs@YVf#4;KEF(7n@rbgtYl}oa#!E5{W zV_M?GQjL5TFKiM^{nB{twA}q@l@4@gpT{XEGJUu%-NT#miJ!eo#t-%ALaxe#k33>M zV9Y+lT$H-TZq3xdZUXq! z4cJC-($l`CWz3ESm!5%PIdJ$U#V7jM`T;CuM@ydVr8T}jT=U13nHn1E=Y3h(U6q$E zJ}u{0HSj)JiZBC#&8&uZH{HieH}{uwbmWS77c34XR4q@K4FQ@XA| zJFu!r&75rbon;xQF3{Q$#@N^)Xc;keW05h58|$0aRI1tNmIf2N=8zIK_`R~c!UK9& zYuuwpkJw&JZDfWCP)%74tTE2alZMA2*c0>)SnAWT*&tAZX6E3=g&fj#Z-zs(3nyxq z(@$Y&cTm5=S~1AO@us*L6hI}G+T%l$qJ{TJYN9jYrmC?Ir&Wh-V?xi>G~3ev(x@8_ySoCXa~F9XgKI+jC^5lc+BS_no|iC;sQrA(#=-N>a1yHqimuIj_UtOeah zye3Xp!|i-p)6=)#{}$a0wkfDFf+Zm2^qcnVa0+1|G?o-g(Gzg^!CR;VzW&EgOa1bq ztjsOQ2jBV_gToR)l( zB&O#<#<{F1|M`_`a@(hWLKKFPN*XcCU}Vr!xADdW#Zz9Csgqiktqkb`Pqnn_QTn`2 zLFJ~dr^{#8X!4EE6exW08BfHZ+SZ+oO}VnTOt*SG2aFHJM#2FIYyq_RnwAJBKk=BB zB3d4-tkFEGs5E!420n~MLAC?+ILd#zs;w-_+Uk;m*h6yDiCYPn!WVXXcY|X^TAMo@ zwTdw#gPP6E>i0i=;YFEm=vpgTi^CiSn?VBKRkf0pjGRbtCZsmX%VY6L1>g`vZ~=&n zXJ&REk=&wm`cHrMr{yO;{9%H(2rAfDU}f7}7YP%Emdh3b)&|=I94f`wmC>F*YFOu$eIG)T56X*O^X2P0!%%8=G= z050P+SRO^j>$j78}ra0hz2GaL+k%` z^})vg2iu^j*}|zOLDQ(@W<8JQNY@~}2Lpnn!>BXy8<}wdx4dTo|6VFs10Zf5fx~K- z88Ca)M$7!(L1n-`O63As7pMi1L6u8p>xj&tT;u>z3d(eab7o9SPlPs&rhupTX&9S9 zHMM3Ctm#H5#Wt>-1CjthYoLIuTdLVarC2qfmUalKmycRH;=!dRi)RZ1MxJ0{QBc;h zfX$)k88MyPPuwC0CMR9!rOy@t3Vs=ym0&dIh@LoUvx~Bgu3s%pcD6RnmQgoO zeQQUWihXSBdO7;uN9F2;%Y5cwaH^OXI6lr`S=S(ek{!Vp+56Vk8nKFUZ=c?W+Z8kF zlTAI<=a*(R;H~P-ZYe&pC@)`lP0rl7zzn6Hnp${fc1AbtrfyJJF%<16ifp2zX`{^6 z5mOm)_*OWhDKl-6xVqX&h*;Olgo5V#4;+w#`aAY%nrLWD$ew0gr&xo~2L(q)It!Ky z9N7U=X=FE6CPU!5#RK{J%&gqdVB9~@&n$LgY=pCqN?wNHpo4v7+1(R~uR4ZO3@+;3 z-GzL57#=i+CQ%3@rXYnrprO&X=p{m6I0EYsr0BTtKE3(a>{QpH9)t51G6bmYDnNyO z++ne7Ad>zZZT=7yHCqC+y;I*7qjr7dM$zbBY&769OxGPtfdm-u0Wi>dZG6ss@&X25(;Gtj`kVCNYkQcVMPWL1k9WT`{PJRd+<&DFPfeblfx{W8alA~!4IDT6Y#XeU}tF`&0T z^dL7MpkvI+jr6`^hH$y4WnZ;q4Pf;aafYhmNX_L9cJ$&T+->d1lCl@44%{T){++MM z$n=!l~;FZ_kSARmA47b(?-p){_2SOdj3zWyDVRCwTc*9h$h>NHc*AlI!xXT^GW z{`+%(D&udtT`B3?WwBvH#|o(phrOw1RF;142^pKbArl9C<>Kp_iZA!d$vX;MKa?CK zH5z}{-I4+gZ4gqCX~LNXkdFqKL*)&S&gL@Yvs91lKmGlJ~s~321;<}P%(?33IsqQcA!!pg~jS0Q;)GeHjNV79%Zov|v3Msl-8dSsFr--~@Ti7Gx&i6qNBJ^rS zioT;Ksf*YgC&^lw53MqwHs~`#a~n1YY&NWRGo1$JAzPchd7Z}IB!{w~(q-=Z@YldY zK}KQ2pi0yf9POs^xvQ$M!*ahi}`m{h!wSx)EOpkj1zlU{cau=2mB>Xb0Jb-qF{T7 z&kax*0R<;tfkL)2OH3?um)$_uH0CZp|m9Tzw-lfSZn8}UVTw|_f5!K9)6T2 z%9&w*1g&QjtbOS#e=1+R>%E#LYgVS&W?2JOWujlM)mE8ZymH~JELT&x`{BD~0hqfHFjV#r5mg2~5)1TGz^s&nd==7*srJ-_!wFzW$0_o4ZUfb#`H1Y6ZyYYb@ip z^q`wtSYW1O0Dh8BmSEW$bqtfJ1Jmor`x+oGOXsbn2v;*gA>HS3MhtGwW0Iy;kw%wJY+ z>{KRU5FD~15Fjy!UV|=%j9y=`g44%AMC@Ob85EVdaSKiiHp!LB+@w4sy;(4fC(WDV zoUWtDO<%JPT2OClR*d;N!DYVW;4j5#UDmP>nh^mg(9HpKMT>Oex(j2BI$ge{aw)Gx zvorvs4%P-KynnXfv)OjT>p+jYEs~bvd|ukF-yT_FHKFA8!QJvVa8FxZ*=5s z<_$i0%9q7XA{rKX7Q+YXK?eT?kOq1l3wLCA*qN+^y-0ZQG|z!hso7C7hWm_Pt4quB zu16k~iIFib5G=noFd_{^2pCBBk>Nra$rKg~Qa4h4yQUbyif-sNGHJbfLXT@;I&2pTnO z$xMA+PJQwNN)c-i*M(Y876qo*H|q)C``V%`pZl@A|MR1oX>RH349KN7no6_wi;Z`% z^d1`lV?+-g+OL^`X1TjK5Q4LwE2cw94XFc$fkAVl;Arl$63&bRI|Mt`Bk{GD*p<;xfg=!OI2~Zeq559Rds%c(r171Ef3KR@y+WjpzyEqLS9nXv1##hTG6I8zl^Z zveb-`tX|Ab9(8Y|d<;{a7^CP7LT_Nsx>$+(-vnc5(Ws8nM(UOtb zp%1CY(@7&{YH%JyhD7RG%n1z{4B0CWPN~=DP-rSbBfYwbe(GZ%mzAXzv-KD^sAQ!TxHWyr`MQx30Y=|KNZBWgFu# z*e65%J=9#3^qI|GTal*z-goRC>=b=>#jkoai#;+XO=WxrTV;9t+N*NkXMR%7^cM^O zMpK$*e#hzxXny+{S-bjU`Op`SDFd~lpSvQ>K;0Bn47JLu_l^U=}Oasf7@ID zVZWgV!@5$&?Dt8``vemT03DzVYPWXz^|28Jj!nVAHDtj!Hmbas_(dKoqaF0vS#EndUN1td`iB*p2} zb|WJZ$*Jk^zAR_FRUfa-@`nIZC#djp#a@Soz7;q0#R!J1kk98aH}FcH-Qb#9&1OE| zhdTjNE+z%B+2m~bHcLE)n<@GAZi#h zg6v8^6J_iz1HDle?SP8MVsmwE>>|EZP2E5BlOK}<2M)^Y)oa#W+S-tIp&}!&A=Cg> zR(cq2caV1L|4D0gM^>*d$dUq`4SioY!~>7|gFpKkjh+9OfBP>Ph*=lS^v#N*NP?y|Ld;|w^LeMi|Sw$FCpYfGzSDB@- zH?Cz;fAO-1*q)fun1`O2u(H2@kO2#r7Lzm@KGk#$*2LOKi#DyVG^W{FU8bfcH9M)x zVO<*&N*{yiTG#Tk0$fsq*@g4xSz2{@_RlgLqgfJf#4MW;Jj(S_Y$bME-|xYq)T0QA{Wqqu2qla;)tg>LLb{!p*DEEz ziR8vMB4Lx6d2{TV3wF;I3}mEr0O&afc4NGUn8SL@=NW@St+|{%7&LiBlG_>(*U8dc z=MiT31QlxR-Nj+H46~ME#O#3?g$J@QrXxvPdF@KtyKSb+)Pq2upcz+D?B@y4lB?gb zz9r~A*qUH1<(vwVg@kdKBM-fM+v~)Fyn_0 z)c*XTpZK8CrArJZz(+Qf9xqSrV+U+Gu5s)IB(hE2RBKmeWaIp-e*Y>LZ>HtRfFk-o zeDcR~%ex+uPkjETWd7O>GFez;3%YU6T|O^o-*`i|ZY(SAu_d2;+s9;AnUoEbj#EQ2 z`Ib|1{%b#wJ`JFkwwGo6tq;o#3|DnyV*20R?LImG)$eG@bXvaf*N#Y9-PT}&(U-k4 zb74f^uLqV@dM^OJ<4~#-Xs=P6Sdf`3R~3BMvaA5$(aHc}U}>Ajs&{2$VjpDmHL+)k zjI*(}!;`jmu%9ekQ+Iw%%NIPq6!#B@#0Kuyg!JltSe;*>NNw@rEC+OzdrHz>?bWhw zlbO+0T}#Dwjf@sDBrr4vptdO@TvOI$O`li6*~8Fh708Uett3(CkcWmzgY*320?YaS zz8-?ou+?B2fu!te-ln!z84 zxTW5kFi#5CzuVaLrcz|UyYk2*cgykP#|Q%NDj;5Fov$%4IH(z7 zMHV!Te(2L5mRs&VC39D2d0l9Y2OYk!qOWyuzpNFy<-T{mL!SA{x1^-mNe1<^BdM z`$1X0a9xTETXIe5)!L|H7bgy~hiFjm!aXu7FaO%_OHco-W+rz?tGHxpf|CYYFfN)NlGQgZ>ZVcVrAvdmmPS|Cmt;X{ z;;pT9&h|mZmlnm=cF}rVuAO_s14U}&LL}b3Z}t?IMMRjwwyVCNzPxsWc$uSnU&f! z{K|ZYdd4};z?QHX&>hYSMehf2lk-ZY5BB%!HQF@7Cj4z0=8;190wsTj!_7?S0bLaN zA@wFB1)^PCYAV?%4x1%mM8XeKtFL!pdwYkqTI>%LeIZ?1Z-xdjW@#uViY1SPg-)F% zQI1=L-`3~Rc7YADnq7|Rv@#%ZHmTEP+wj5Wp%ltI8{(Ks_XG^wfOhEe~r3v%w8q+paO7;`iwIq$Eo_ zJlVP{Jp`Enu8tk4$W72#V6JbMd5zAjFU!Ex7_}ZK<#5kn!USgH;M9Kk(T|^$cfS2m zIeqr5=pAE5wx*}_g^jBr(Yp=+`{(kx6H=dP4f9WS>Uaxgb1JK>ss66|tzat|f z^YXc0d{DNN8N@cd_CBe`N9D=y)s=10#T!)#5Tf)$>fWpage(!e>g^eq=Ey!7=o!+~ zyux&wzBvwF(##xyt8Ng;#=GIGqt6M4gJ?#BF&R7CM#Cb#hZY7wdPH%C2Jf>51T@sO z%;2kEzYdUl$149X1+S& zUD$yQ^?;K1Z+K-ua36;mGFwC&sK9mabY6)+Aw_=0!PmMiu;wb~Z5o!Ip!z%oqkTgr!i zDg2SMGMvo(PYgMtt@uvjeQr5zW9PbUN}5iA=`!W~l=gP_QA zNvsqjg6>|j*7JmMpczGc?0+g1b91T0{crid0RCoa4r&JIS2gSfy&o+bB)>8< zhc`1rZ*Rx28A^J+iD5KVO~Fq+e2e_(sV8Ny+M`T}UdLF!^c+))c6LFkTP?ZTydlM- zdcsc(abp~mUODr7U(+@7ynOl>9+kEFc?B}z4^xq@;&D0iY*~Xzmu^f2yb0B{xxQ9b zSG5G|b|wJ7zptj739k;HlZ~}iIeP0!dE(pOk+Un;v{Y)!E`*DbNe)2iB2&P9 zg8wUwxX)d_NUajDGIft%Z_Qxvz~P!o4jq{8%qaOm7+@FRP}j|MpD6VN@}Z}@%KjOY zz87aVI3sywY1M*c5>Y@%e-=>Y`rOf&xU2z@1GoT6@0hO%_Ced{@C5(i&~MhI?=h{} z84vGrkr@mpcxn(@+t^_Sjcll^t55HZ2JlvquLltv)^!mw+g+A9skN7r{uM?qb`zJY z`H(A2j)S5!jnjMv3_p0ez~DghA_}YuJn7?H1eemgBWPI69ba!i(NvBo7Z9|zcfjr2 zr0f8sg5GTAz97LaF*53wC5!hb5WA@4U~KSy4Urj}6=Ix#yHJScCw|Aw*%_Fk8S~

ze9oNie9J zW>eEyo)?8SN13$sWNYm}GQKL0KmVj0eCSRa1p!X^;H{suj{TKZhhFSf_4H8#9I2CKXXBb2M5?g z1G2z=WfZm{=+mGmGO0g)77~0MD%5Cz0ZPmq4Ac?Ew{YC91_J~WkOjREyd{=V?rs=X zGN{?XN@G!q`$uK|spqA;ugZ)UYz=fz*myJ@)7R-yy8XGQUnN6X@eI_~X=mosUVD7B0SkR@VrJN|het_T0zJ1dS8pH=vo7SlmmGz#{@Y*PPm_0#pUBX;y zT}oJ2h$Y1x_b_QYi-AwpKQK&B2TJN=d&8Q0N~R1Bao*_1vIN;e!A+E*uuE(RSKUwR z*v)T(Tp#rxX9GutJZ%Sa6S|#4S%QrmczHaqhMT>Fsd*uIAX~tR9USuMcZ&u>m}a|z zkKMSvUv59W*-1tFUUQg`T+ZWIN&L= z?5?c=WM=7#+kBLVe&r8jOaZ-B4Z5RS1$pV4-;fV{ zYFY}t=*j6~c{tL0yFBsTEqU(o%W~rA-BRC;xqg7ztZiwYTdLFa$;NU3iz-tIdSmv6 zoVw?&%!o^h89=#>XWaog%>xCQ5v~huB)i+2WO!ONE#EfQ6_~bw4qL~H{`~fqF-j#Z ztoo-Q+B z1@otlt%BiqUMJ;WL*zr5G(I{_-em=a6Yp?My%DXM>-#G{ zJRM4xG#I_OdNyjg|7w^c99da_8X2==nXaZykDeMF3g%COP0tOkkvj9&i(Sc^y7N6S z5Zn-gRvu_@txf0olhAJvMo_puC9fS@ZD$9XOsxU#p^*^4Xwe6m2RD1eY0PbnpsCmI zS!$V*wJAh7?!f_9-p+t_^1$2#nz%D^!`=fy6rZ_Qvk7=$!21GJF|K(~7c~S#^rAtz zqSpe;!H<3Zv+~4qFUrmP_VMIIa3+9^v>w>Z`W6oXc%=>w4^fl`6RoChmN#B{TIMy7 z^z{wNsHXQAVxG7i0E2|C6gc!jS$db2N}F4H$^qX(_M%4<1Y1#Gmj(VE-S7)Da-*@P zxJ-`>>M7PZe^sU?3v$=P`!uLw?veu9*(v$6uWiY~)l}}k>s<=^Mx-S-*A_LfGONR4iVbw%Q5t<{Xp}?EIIK<|&Z?FbgA?O&;Lvfo{f_(O#TS3fQyhIe zdQ54?p3uS(_u|zHZ|G)so{0T~^Dw&pNo_Su`wBM0yT883T zjr0w3^lG$Z9Ll|mKnbIlXK-jp zU`XfN(-k%NLrz1eo%VrhcOYpM5#q>i?ewsk8;fI5|`Zz|#nnz5kS3TZ$4R;nC@PLrTp*C*^AlEaamIuDrGcT+h4sLflOWH;{ zAWP2)vOaB}J7n~F?48r0s}|aZhB7}>`wq+giAnj@|L`y5FMsA!)C8oC0riiJD=?`T zM34E)>^r<)3W{-Tt!{EUT@&s2y@PtMEh|JbXw09tybl69SXZuXF3O{Ce~T`l0-r-x zDbP(X+qD7f5PRX70R)}V`!;aoknAWWJKe0x)mLATkN)H-%`O)8CAE|(j>{V_G~~7C zSC!_xP1aZ1Ozjg5wmrQ=vb7GYfi2CLH|6~6TCyD&GqbI>W?9N)ZEQG59T{^*RRvRX z%REb7uG%$##*RGFC;j!{k&Pr*UN zwvvR_1n{zOqujddP|zYk3x98}L;8gjyh^J?1MSL1qr2#AFV-}N(gfKKeQZHNyQDw( zp%O#}eiIvw2Q3437&^;8{bu!w! zkSKFdmWod(m8qP?3UBden0ltE&u6w$1&gw#|9#J6ACPB$`~zMKGC4G6qE5!==pUPq zT@4=Wti}{W_{+d09invxxJ)LG-K?ATgr4HV(yO2{X7Ut$v}&UG3Jw^IdpL4pzk-b| z8!w80r5|BtML}2?G|_;l%?2L?=i%{jsp!|E4P^%#n=(9=8a*z`0-c+bC!V+}Z+q|k za_rQIR7Mo&>s^<@G2l%t86E++r<+B;x2kY%b6eTMLX|x-Jp+0j$EK){BHXSx3Q})o ztq?*cW~3g5$uYa+FsmxB4es!40!4O>UX_Ms!sRZmO}MHd6CgznWpvRPBy8+7ZIGh? zP<;5{5tdKzJVEIJ;ard2xACDd?>=ww`G#P^5irOC`2oz5gBJbS{m zm1u4w44@{Gv?j`8v5+SX&{W}IhAe_=U!dm*EU#E9LKXHnFu`I*$iZ@L}Dxnz=>M49FW0nf+n5FM&v(%B2yW{-DKGI+Sgh8rGE__|@}+Fb|^a#@bkBzY%x zH_L>~Y3$HI8M?E{Kyl!9;v+#@EK|5(ytc-t({lsLU9%Y#aJLVo!DD{}w6_v>0%kTVy5AU(qya(VeFdG5kD zW$d7C?5;>Nfe}4vdvu4C*-us~DjQT9&{+Ygg_0MIFEuo020s3+97E%Z4n?yY+LhHto!)3T+!u}d!R#jKg8&)(tt_(JE+D%i#gAX}J~j8hu2=YWd@cQuhtZ&8}JcU!Yx?l$L5 ziX5xp3fh9JDU)Cf-LPoRHiKdz?-|1VXtmADHAuT7XIprq-B2m$TT)*z0jS2k47Lb{ zKV^>HxLzmijMJc9WFgKKv3OlLxfv^Ewhd(uQn^)|Wb z$WiaUE+`YxN2|UrJ$*pmQ&}9D!8QUbhiPl3*4$MlqokW|pvw2YcJ(>|Mx?k146qYC zb?-@K44M=(MhO0T65e^w-O?&qK!mpl)Xg{?rjOVlFZk*-c2~7r*_Nm);Xtj$S!FDg z71&&vlL@Wsf8vox<>u)rdFNw~XzZxTH-GSDnVcHdAUUSg>A2n#n?8xx?dtBAsj))} zRQ72WGiqPYT+`v}0mK3ht(oiBWp3^U6~pi|!RLo2Ak5%}9woy8?#yw*`U7c#Lz?|^?vWDaU0a8Gk@3SA`mxLWsZ_<#u8IN2!7Z-fUL(v# zU@xiH8G04yx9c~`W|WfoW0nk_>4t8mt4ni| z!D9rth6bGekwK$u0hA(G_b9ed)az(ry>!cs`5ig)`WyUA4cU>I_3M)LS7hqwA^EF+ z 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 A(*OVf literal 0 HcmV?d00001 diff --git a/StudioProjects/yimaru_app/assets/images/profile.png b/StudioProjects/yimaru_app/assets/images/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..7d74c01663c7149a8dd71a49232f1d499f5f5a86 GIT binary patch literal 9804 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 mr@gQ`;2YlN*>vj5?f(L2^S%e`t6Nb30000 setupLocator({ @@ -25,4 +30,8 @@ Future setupLocator({ locator.registerLazySingleton(() => BottomSheetService()); locator.registerLazySingleton(() => DialogService()); locator.registerLazySingleton(() => NavigationService()); + locator.registerLazySingleton(() => AuthenticationService()); + locator.registerLazySingleton(() => ApiService()); + locator.registerLazySingleton(() => SecureStorageService()); + locator.registerLazySingleton(() => DioService()); } diff --git a/StudioProjects/yimaru_app/lib/app/app.router.dart b/StudioProjects/yimaru_app/lib/app/app.router.dart index b3725f5..f2872b4 100644 --- a/StudioProjects/yimaru_app/lib/app/app.router.dart +++ b/StudioProjects/yimaru_app/lib/app/app.router.dart @@ -5,14 +5,38 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/material.dart' as _i6; +import 'package:flutter/material.dart' as _i22; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart' as _i1; -import 'package:stacked_services/stacked_services.dart' as _i7; +import 'package:stacked_services/stacked_services.dart' as _i23; +import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' + as _i10; +import 'package:yimaru_app/ui/views/call_support/call_support_view.dart' + as _i13; +import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2; -import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart' as _i5; +import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14; +import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19; +import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20; +import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart' + as _i21; +import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3; +import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart' + as _i9; +import 'package:yimaru_app/ui/views/privacy_policy/privacy_policy_view.dart' + as _i15; +import 'package:yimaru_app/ui/views/profile/profile_view.dart' as _i5; +import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart' + as _i6; +import 'package:yimaru_app/ui/views/progress/progress_view.dart' as _i8; +import 'package:yimaru_app/ui/views/register/register_view.dart' as _i17; import 'package:yimaru_app/ui/views/startup/startup_view.dart' as _i4; +import 'package:yimaru_app/ui/views/support/support_view.dart' as _i11; +import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart' + as _i12; +import 'package:yimaru_app/ui/views/terms_and_conditions/terms_and_conditions_view.dart' + as _i16; class Routes { static const homeView = '/home-view'; @@ -21,12 +45,61 @@ class Routes { static const startupView = '/startup-view'; + static const profileView = '/profile-view'; + static const profileDetailView = '/profile-detail-view'; + + static const downloadsView = '/downloads-view'; + + static const progressView = '/progress-view'; + + static const ongoingProgressView = '/ongoing-progress-view'; + + static const accountPrivacyView = '/account-privacy-view'; + + static const supportView = '/support-view'; + + static const telegramSupportView = '/telegram-support-view'; + + static const callSupportView = '/call-support-view'; + + static const languageView = '/language-view'; + + static const privacyPolicyView = '/privacy-policy-view'; + + static const termsAndConditionsView = '/terms-and-conditions-view'; + + static const registerView = '/register-view'; + + static const loginView = '/login-view'; + + static const learnView = '/learn-view'; + + static const learnLevelView = '/learn-level-view'; + + static const learnModuleView = '/learn-module-view'; static const all = { homeView, onboardingView, startupView, + profileView, + profileDetailView, + downloadsView, + progressView, + ongoingProgressView, + accountPrivacyView, + supportView, + telegramSupportView, + callSupportView, + languageView, + privacyPolicyView, + termsAndConditionsView, + registerView, + loginView, + learnView, + learnLevelView, + learnModuleView, }; } @@ -44,31 +117,194 @@ class StackedRouter extends _i1.RouterBase { Routes.startupView, page: _i4.StartupView, ), - + _i1.RouteDef( + Routes.profileView, + page: _i5.ProfileView, + ), + _i1.RouteDef( + Routes.profileDetailView, + page: _i6.ProfileDetailView, + ), + _i1.RouteDef( + Routes.downloadsView, + page: _i7.DownloadsView, + ), + _i1.RouteDef( + Routes.progressView, + page: _i8.ProgressView, + ), + _i1.RouteDef( + Routes.ongoingProgressView, + page: _i9.OngoingProgressView, + ), + _i1.RouteDef( + Routes.accountPrivacyView, + page: _i10.AccountPrivacyView, + ), + _i1.RouteDef( + Routes.supportView, + page: _i11.SupportView, + ), + _i1.RouteDef( + Routes.telegramSupportView, + page: _i12.TelegramSupportView, + ), + _i1.RouteDef( + Routes.callSupportView, + page: _i13.CallSupportView, + ), + _i1.RouteDef( + Routes.languageView, + page: _i14.LanguageView, + ), + _i1.RouteDef( + Routes.privacyPolicyView, + page: _i15.PrivacyPolicyView, + ), + _i1.RouteDef( + Routes.termsAndConditionsView, + page: _i16.TermsAndConditionsView, + ), + _i1.RouteDef( + Routes.registerView, + page: _i17.RegisterView, + ), + _i1.RouteDef( + Routes.loginView, + page: _i18.LoginView, + ), + _i1.RouteDef( + Routes.learnView, + page: _i19.LearnView, + ), + _i1.RouteDef( + Routes.learnLevelView, + page: _i20.LearnLevelView, + ), + _i1.RouteDef( + Routes.learnModuleView, + page: _i21.LearnModuleView, + ), ]; final _pagesMap = { _i2.HomeView: (data) { - return _i6.MaterialPageRoute( + return _i22.MaterialPageRoute( builder: (context) => const _i2.HomeView(), settings: data, ); }, _i3.OnboardingView: (data) { - return _i6.MaterialPageRoute( + return _i22.MaterialPageRoute( builder: (context) => const _i3.OnboardingView(), settings: data, ); }, _i4.StartupView: (data) { - return _i6.MaterialPageRoute( + return _i22.MaterialPageRoute( builder: (context) => const _i4.StartupView(), settings: data, ); }, - _i5.LanguageSelector: (data) { - return _i6.MaterialPageRoute( - builder: (context) => const _i5.LanguageSelector(), + _i5.ProfileView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i5.ProfileView(), + settings: data, + ); + }, + _i6.ProfileDetailView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i6.ProfileDetailView(), + settings: data, + ); + }, + _i7.DownloadsView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i7.DownloadsView(), + settings: data, + ); + }, + _i8.ProgressView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i8.ProgressView(), + settings: data, + ); + }, + _i9.OngoingProgressView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i9.OngoingProgressView(), + settings: data, + ); + }, + _i10.AccountPrivacyView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i10.AccountPrivacyView(), + settings: data, + ); + }, + _i11.SupportView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i11.SupportView(), + settings: data, + ); + }, + _i12.TelegramSupportView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i12.TelegramSupportView(), + settings: data, + ); + }, + _i13.CallSupportView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i13.CallSupportView(), + settings: data, + ); + }, + _i14.LanguageView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i14.LanguageView(), + settings: data, + ); + }, + _i15.PrivacyPolicyView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i15.PrivacyPolicyView(), + settings: data, + ); + }, + _i16.TermsAndConditionsView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i16.TermsAndConditionsView(), + settings: data, + ); + }, + _i17.RegisterView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i17.RegisterView(), + settings: data, + ); + }, + _i18.LoginView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i18.LoginView(), + settings: data, + ); + }, + _i19.LearnView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i19.LearnView(), + settings: data, + ); + }, + _i20.LearnLevelView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i20.LearnLevelView(), + settings: data, + ); + }, + _i21.LearnModuleView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i21.LearnModuleView(), settings: data, ); }, @@ -81,7 +317,7 @@ class StackedRouter extends _i1.RouterBase { Map get pagesMap => _pagesMap; } -extension NavigatorStateExtension on _i7.NavigationService { +extension NavigatorStateExtension on _i23.NavigationService { Future navigateToHomeView([ int? routerId, bool preventDuplicates = true, @@ -124,7 +360,243 @@ extension NavigatorStateExtension on _i7.NavigationService { transition: transition); } + Future navigateToProfileView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.profileView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future navigateToProfileDetailView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.profileDetailView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToDownloadsView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.downloadsView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToProgressView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.progressView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToOngoingProgressView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.ongoingProgressView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToAccountPrivacyView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.accountPrivacyView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.supportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToTelegramSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.telegramSupportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToCallSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.callSupportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLanguageView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.languageView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToPrivacyPolicyView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.privacyPolicyView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToTermsAndConditionsView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.termsAndConditionsView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToRegisterView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.registerView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLoginView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.loginView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLearnView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.learnView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLearnLevelView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.learnLevelView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLearnModuleView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.learnModuleView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } Future replaceWithHomeView([ int? routerId, @@ -168,5 +640,241 @@ extension NavigatorStateExtension on _i7.NavigationService { transition: transition); } + Future replaceWithProfileView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.profileView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future replaceWithProfileDetailView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.profileDetailView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithDownloadsView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.downloadsView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithProgressView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.progressView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithOngoingProgressView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.ongoingProgressView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithAccountPrivacyView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.accountPrivacyView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.supportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithTelegramSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.telegramSupportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithCallSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.callSupportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLanguageView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.languageView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithPrivacyPolicyView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.privacyPolicyView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithTermsAndConditionsView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.termsAndConditionsView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithRegisterView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.registerView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLoginView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.loginView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLearnView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.learnView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLearnLevelView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.learnLevelView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLearnModuleView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.learnModuleView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } } diff --git a/StudioProjects/yimaru_app/lib/main.dart b/StudioProjects/yimaru_app/lib/main.dart index d23ff42..ad534c7 100644 --- a/StudioProjects/yimaru_app/lib/main.dart +++ b/StudioProjects/yimaru_app/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:toastification/toastification.dart'; import 'package:yimaru_app/app/app.bottomsheets.dart'; import 'package:yimaru_app/app/app.dialogs.dart'; import 'package:yimaru_app/app/app.locator.dart'; @@ -17,13 +18,17 @@ class MainApp extends StatelessWidget { const MainApp({super.key}); @override - Widget build(BuildContext context) { - return MaterialApp( - initialRoute: Routes.startupView, - theme: ThemeData(fontFamily: 'Aeonik'), - onGenerateRoute: StackedRouter().onGenerateRoute, - navigatorKey: StackedService.navigatorKey, - navigatorObservers: [StackedService.routeObserver], - ); - } + Widget build(BuildContext context) => _buildMaterialWrapper(); + + Widget _buildMaterialWrapper() => ToastificationWrapper( + child: _buildMaterialApp(), + ); + + Widget _buildMaterialApp() => MaterialApp( + initialRoute: Routes.startupView, + theme: ThemeData(fontFamily: 'Aeonik'), + onGenerateRoute: StackedRouter().onGenerateRoute, + navigatorKey: StackedService.navigatorKey, + navigatorObservers: [StackedService.routeObserver], + ); } diff --git a/StudioProjects/yimaru_app/lib/models/user_model.dart b/StudioProjects/yimaru_app/lib/models/user_model.dart new file mode 100644 index 0000000..deab289 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/models/user_model.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user_model.g.dart'; + +@JsonSerializable() +class UserModel { + @JsonKey(name: 'user_id') + final int? userId; + + @JsonKey(name: 'access_token') + final String? accessToken; + + @JsonKey(name: 'refresh_token') + final String? refreshToken; + + UserModel({this.userId, this.accessToken, this.refreshToken}); + + factory UserModel.fromJson(Map json) => + _$UserModelFromJson(json); + + Map toJson() => _$UserModelToJson(this); +} diff --git a/StudioProjects/yimaru_app/lib/models/user_model.g.dart b/StudioProjects/yimaru_app/lib/models/user_model.g.dart new file mode 100644 index 0000000..36e2c21 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/models/user_model.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserModel _$UserModelFromJson(Map json) => UserModel( + userId: (json['user_id'] as num?)?.toInt(), + accessToken: json['access_token'] as String?, + refreshToken: json['refresh_token'] as String?, + ); + +Map _$UserModelToJson(UserModel instance) => { + 'user_id': instance.userId, + 'access_token': instance.accessToken, + 'refresh_token': instance.refreshToken, + }; diff --git a/StudioProjects/yimaru_app/lib/services/api_service.dart b/StudioProjects/yimaru_app/lib/services/api_service.dart new file mode 100644 index 0000000..047a5fc --- /dev/null +++ b/StudioProjects/yimaru_app/lib/services/api_service.dart @@ -0,0 +1,171 @@ +import 'package:dio/dio.dart'; +import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/services/dio_service.dart'; +import 'package:yimaru_app/ui/common/app_constants.dart'; + +import '../app/app.locator.dart'; +import '../ui/common/enmus.dart'; + +class ApiService { + final _service = locator(); + + // Http headers + Map _getHeaders({String? token}) => { + + // if (token != null) 'Authorization': 'Bearer $token', + + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', + if (token != null) 'Authorization': 'Bearer $token' + }; + + // Dio options + Options? _getOptions({String? token}) { + return Options( + // followRedirects: false, + // validateStatus: (status) => true, + headers: _getHeaders(token: token), + ); + } + + // Register + Future> register(Map data) async { + try { + Response response = await _service.dio.post( + '$baseUrl/$userUrl/$kRegisterUrl', + data: data, + options: _getOptions(), + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Otp sent successfully' + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': 'Unknown Error Occurred' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + + // Login + Future> login(Map data) async { + try { + Response response = await _service.dio.post( + '$baseUrl/$kLoginUrl', + data: data, + options: _getOptions(), + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Logged in successfully', + 'data': UserModel.fromJson(response.data['data']), + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + + // Verify otp + Future> verifyOtp(Map data) async { + try { + Response response = await _service.dio.post( + '$baseUrl/$userUrl/$kVerifyOtpUrl', + data: data, + options: _getOptions(), + ); + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Otp verified successfully', + //'data': UserModel.fromJson(response.data['data']), + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + + // Resend otp + Future> resendOtp(Map data) async { + try { + Response response = await _service.dio.post( + '$baseUrl/$userUrl/$kResendOtpUrl', + data: data, + options: _getOptions(), + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Otp resend successfully' + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': 'Unknown Error Occurred' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + + // Profile completed + Future> getProfileStatus(UserModel? user) async { + try { + Response response = await _service.dio.get( + '$baseUrl/$userUrl/${user?.userId}/$kProfileStatusUrl', + options: _getOptions(token: user?.accessToken), + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Profile completion status fetched successfully', + 'data': response.data['data']['is_profile_completed'] as bool, + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } +} diff --git a/StudioProjects/yimaru_app/lib/services/authentication_service.dart b/StudioProjects/yimaru_app/lib/services/authentication_service.dart new file mode 100644 index 0000000..131d687 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/services/authentication_service.dart @@ -0,0 +1,32 @@ +import 'package:yimaru_app/app/app.locator.dart'; +import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/services/secure_storage_service.dart'; + +class AuthenticationService { + final _secureService = locator(); + + Future userLoggedIn() async { + if (await _secureService.getString('userId') != null) { + return true; + } + return false; + } + + Future saveUserData(Map data) async { + await _secureService.setInt('userId', data['userId']); + await _secureService.setString('accessToken', data['accessToken']); + await _secureService.setString('refreshToken', data['refreshToken']); + } + + Future getUser() async { + UserModel user = UserModel( + userId: await _secureService.getInt('userId'), + accessToken: await _secureService.getString('accessToken'), + refreshToken: await _secureService.getString('refreshToken')); + return user; + } + + Future logOut() async { + await _secureService.clear(); + } +} diff --git a/StudioProjects/yimaru_app/lib/services/dio_service.dart b/StudioProjects/yimaru_app/lib/services/dio_service.dart new file mode 100644 index 0000000..3a8eddf --- /dev/null +++ b/StudioProjects/yimaru_app/lib/services/dio_service.dart @@ -0,0 +1,41 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; + +import '../ui/common/app_constants.dart'; + +class DioService { + final Dio _dio = Dio(); + + DioService() { + _dio.options.baseUrl = baseUrl; + _dio.options.connectTimeout = const Duration(seconds: 30); + _dio.options.receiveTimeout = const Duration(seconds: 30); + + _dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + debugPrint('➡️➡️➡️ REQUEST ➡️➡️➡️'); + debugPrint('➡️ Data: ${options.data}'); + debugPrint('➡️ Headers: ${options.headers}'); + debugPrint('➡️ ${options.method} ${options.uri}'); + handler.next(options); + }, + onResponse: (response, handler) { + debugPrint('✅✅✅ RESPONSE ✅✅✅'); + debugPrint('✅ Data : ${response.data}'); + debugPrint('✅ Status Code : ${response.statusCode}'); + handler.next(response); + }, + onError: (error, handler) { + debugPrint('❌❌❌ ERROR ❌❌❌'); + debugPrint('❌ ${error.message}'); + debugPrint('❌ URI: ${error.requestOptions.uri}'); + debugPrint('❌ Headers sent: ${error.requestOptions.headers}'); + handler.next(error); + }, + ), + ); + } + + Dio get dio => _dio; +} diff --git a/StudioProjects/yimaru_app/lib/services/secure_storage_service.dart b/StudioProjects/yimaru_app/lib/services/secure_storage_service.dart new file mode 100644 index 0000000..68fd4ac --- /dev/null +++ b/StudioProjects/yimaru_app/lib/services/secure_storage_service.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +extension BoolParsing on String { + bool parseBool() { + if (toLowerCase() == 'true') { + return true; + } else if (toLowerCase() == 'false') { + return false; + } + + throw '"$this" can not be parsed to boolean.'; + } +} + +class SecureStorageService { + // Create storage + + late final FlutterSecureStorage _storage; + + SecureStorageService() { + _storage = Platform.isAndroid + ? const FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + ) + : const FlutterSecureStorage( + iOptions: IOSOptions(), + ); + } + + Future clear() async { + _storage.deleteAll(); + } + + Future getBool(String key) async { + String? result = await _storage.read(key: key); + return result?.parseBool(); + } + + Future getString(String key) async { + return await _storage.read(key: key); + } + + Future getInt(String key) async { + return await _storage.read(key: key) == null + ? null + : int.parse(await _storage.read(key: key) ?? '0'); + } + + Future setString(String key, String value) async { + await _storage.write(key: key, value: value); + } + + Future setInt(String key, int value) async { + await _storage.write(key: key, value: value.toString()); + } + + Future setBool(String key, bool value) async { + await _storage.write(key: key, value: value.toString()); + } +} diff --git a/StudioProjects/yimaru_app/lib/ui/common/app_colors.dart b/StudioProjects/yimaru_app/lib/ui/common/app_colors.dart index b9b1b22..f7d0acb 100644 --- a/StudioProjects/yimaru_app/lib/ui/common/app_colors.dart +++ b/StudioProjects/yimaru_app/lib/ui/common/app_colors.dart @@ -1,10 +1,19 @@ import 'package:flutter/material.dart'; -const Color kcBackgroundColor = kcWhiteColor; -const Color kcWhiteColor = Color(0xFFFFFFFF); +const Color kcBlack = Colors.black; +const Color kcRed = Color(0xffFF4C4C); +const Color kcGreen = Color(0xFF1DE964); +const Color kcBackgroundColor = kcWhite; +const Color kcWhite = Color(0xFFFFFFFF); +const Color kcIndigo = Color(0xff6A1B9A); +const Color kcOrange = Color(0xFFF79400); +const Color kcSkyBlue = Color(0xFF28B4CD); +const Color kcDarkGrey = Color(0xFF1A1B1E); const Color kcMediumGrey = Color(0xFF474A54); +const Color kcAquamarine = Color(0xFF1DE9B6); +const Color kcTransparent = Colors.transparent; const Color kcPrimaryColor = Color(0xFF9E2891); -const Color kcDarkGreyColor = Color(0xFF1A1B1E); +const Color kcPrimaryAccent = Color(0xFF6A1B9A); const Color kcVeryLightGrey = Color(0xFFE3E3E3); const Color kcPrimaryColorDark = Color(0xFF300151); const Color kcPrimaryColorLight = Color(0x149E2891); diff --git a/StudioProjects/yimaru_app/lib/ui/common/app_constants.dart b/StudioProjects/yimaru_app/lib/ui/common/app_constants.dart new file mode 100644 index 0000000..69839b0 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/common/app_constants.dart @@ -0,0 +1,14 @@ +//String baseUrl = 'http://195.35.29.82:8080'; +String baseUrl = 'https://api.yimaru.yaltopia.com'; + +String userUrl = 'api/v1/user'; + +String kRegisterUrl = 'register'; + +String kVerifyOtpUrl = 'verify-otp'; + +String kResendOtpUrl = 'resend-otp'; + +String kLoginUrl = 'api/v1/auth/customer-login'; + +String kProfileStatusUrl = 'is-profile-completed'; diff --git a/StudioProjects/yimaru_app/lib/ui/common/app_strings.dart b/StudioProjects/yimaru_app/lib/ui/common/app_strings.dart index 06e7c8f..5cc0474 100644 --- a/StudioProjects/yimaru_app/lib/ui/common/app_strings.dart +++ b/StudioProjects/yimaru_app/lib/ui/common/app_strings.dart @@ -1,3 +1,54 @@ +const String ksSuggestion = + "15 minutes a day can make you 3x more fluent in 3 month"; const String ksHomeBottomSheetTitle = 'Build Great Apps!'; +const String ksPrivacyPolicy = + 'A brief, simple overview of Yimaru’s commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.'; const String ksHomeBottomSheetDescription = 'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more'; + +const String ksTerms = """ +

+Last updated: October 26, 2025 +

+ +

Introduction

+

+Welcome to Yimaru! These terms and conditions outline the rules and regulations +for the use of our application. By accessing this app, we assume you accept +these terms and conditions. +

+ +

User Accounts

+

+When you create an account with us, you must provide us with information that is +accurate, complete, and current at all times. Failure to do so constitutes a +breach of the Terms, which may result in immediate termination of your account +on our Service. +

+ +

Content & Services

+

+Our Service allows you to access learning materials. You are granted a limited +license to access and use the app content for personal, non-commercial purposes. +You agree not to: +

+ +
    +
  • Reproduce, duplicate, copy, or sell any material from the app.
  • +
  • Redistribute content from Yimaru.
  • +
  • Use the app in any way that is damaging or harmful.
  • +
+ +

Privacy Policy

+

+Your privacy is important to us. Please read our +Privacy Policy +to understand how we collect, use, and share information about you. +

+ +

Contact Us

+

+If you have any questions about these Terms, please contact us at +support@yimaru.et. +

+"""; diff --git a/StudioProjects/yimaru_app/lib/ui/common/enmus.dart b/StudioProjects/yimaru_app/lib/ui/common/enmus.dart new file mode 100644 index 0000000..10ab36e --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/common/enmus.dart @@ -0,0 +1,7 @@ +// Registration type +enum RegistrationType { phone, email } + +// Report status +enum ResponseStatus { success, failure } + +enum LearnLevelStatus { pending, started, completed } diff --git a/StudioProjects/yimaru_app/lib/ui/common/ui_helpers.dart b/StudioProjects/yimaru_app/lib/ui/common/ui_helpers.dart index 1cf232c..b159372 100644 --- a/StudioProjects/yimaru_app/lib/ui/common/ui_helpers.dart +++ b/StudioProjects/yimaru_app/lib/ui/common/ui_helpers.dart @@ -1,6 +1,9 @@ import 'dart:math'; - +import 'package:flutter_html/flutter_html.dart'; +import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; +import 'package:pinput/pinput.dart'; +import 'package:toastification/toastification.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; const double _tinySize = 5.0; @@ -34,6 +37,9 @@ double screenWidth(BuildContext context) => MediaQuery.of(context).size.width; double screenHeight(BuildContext context) => MediaQuery.of(context).size.height; +double buttonAlignment(BuildContext context) => + MediaQuery.of(context).size.height * 0.9; + double screenHeightFraction( BuildContext context, { int dividedBy = 1, @@ -92,18 +98,20 @@ double getResponsiveFontSize( return responsiveSize; } -InputDecoration inputDecoration({ - String? hint, - required bool focus, -}) => +InputDecoration inputDecoration( + {String? hint, + Widget? suffix, + required bool focus, + required bool filled}) => InputDecoration( - hintText: hint, filled: true, + hintText: hint, border: border, + suffixIcon: suffix, errorBorder: errorBorder, enabledBorder: enabledBorder, focusedBorder: focusedBorder, - fillColor: focus ? kcPrimaryColor.withOpacity(0.2) : kcWhiteColor, + fillColor: focus || filled ? kcPrimaryColor.withOpacity(0.1) : kcWhite, ); Border rightBorder = Border( @@ -113,6 +121,8 @@ Border rightBorder = Border( ), ); +DateFormat format = DateFormat("d MMM, yyyy"); + OutlineInputBorder border = const OutlineInputBorder(borderSide: BorderSide(color: kcPrimaryColor)); OutlineInputBorder errorBorder = @@ -124,3 +134,142 @@ OutlineInputBorder focusedBorder = UnderlineInputBorder searchBorder = const UnderlineInputBorder(borderSide: BorderSide(color: kcPrimaryColor)); + +TextStyle defaultPinTextStyle = const TextStyle( + fontSize: 22, + color: kcDarkGrey, +); + +BoxDecoration defaultPinDecoration = BoxDecoration( + borderRadius: BorderRadius.circular(19), + border: Border.all( + color: kcPrimaryColor.withOpacity(0.5), + ), +); + +PinTheme defaultPin = PinTheme( + width: 56, + height: 56, + textStyle: defaultPinTextStyle, + decoration: defaultPinDecoration, +); + +PinTheme focusedThemePin = defaultPin.copyWith( + decoration: defaultPin.decoration?.copyWith( + border: Border.all(color: kcPrimaryColor, width: 3), + ), +); + +PinTheme submittedThemePin = defaultPin.copyWith( + decoration: defaultPin.decoration?.copyWith( + borderRadius: BorderRadius.circular(19), + border: Border.all(color: kcPrimaryColor), + ), +); + +PinTheme errorPinTheme = defaultPin.copyBorderWith( + border: Border.all(color: Colors.red), +); + +TextStyle validationStyle = const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, +); + +TextStyle style25DG600 = const TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, +); + +TextStyle style12R700 = const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, +); + +TextStyle style16DG600 = const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, +); + +TextStyle style16DG400 = const TextStyle( + fontSize: 16, + color: kcDarkGrey, +); + +TextStyle style14DG400 = const TextStyle( + color: kcDarkGrey, +); + +TextStyle style14P400 = const TextStyle( + color: kcPrimaryColor, +); + +TextStyle style14P600 = const TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, +); + +Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16)); + +Map htmlStyle = { + "p": htmlDefaultStyle, + "h1": htmlDefaultStyle, + "h2": htmlDefaultStyle, + "h3": htmlDefaultStyle, + "h4": htmlDefaultStyle, + "h5": htmlDefaultStyle, + "h6": htmlDefaultStyle, + "li": Style( + color: kcDarkGrey, + margin: Margins.zero, + fontSize: FontSize(16), + padding: HtmlPaddings.zero, + fontWeight: FontWeight.w400, + listStyleType: ListStyleType.circle, + verticalAlign: VerticalAlign.baseline, + ), +}; + +Widget buildToastDescription(String message) => Text( + message, + maxLines: 4, + style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500), + ); + +void showErrorToast(String message) { + toastification.show( + showIcon: true, + dragToClose: true, + primaryColor: kcRed, + showProgressBar: false, + applyBlurEffect: false, + icon: const Icon(Icons.check), + type: ToastificationType.success, + alignment: Alignment.bottomCenter, + style: ToastificationStyle.fillColored, + description: buildToastDescription(message), + autoCloseDuration: const Duration(seconds: 10), + margin: const EdgeInsets.symmetric(horizontal: 15), + ); +} + +void showSuccessToast(String message) { + toastification.show( + showIcon: true, + dragToClose: true, + showProgressBar: false, + applyBlurEffect: false, + icon: const Icon(Icons.check), + primaryColor: kcPrimaryColor, + type: ToastificationType.success, + alignment: Alignment.bottomCenter, + style: ToastificationStyle.fillColored, + description: buildToastDescription(message), + autoCloseDuration: const Duration(seconds: 10), + margin: const EdgeInsets.symmetric(horizontal: 15), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/common/validators/form_validator.dart b/StudioProjects/yimaru_app/lib/ui/common/validators/form_validator.dart new file mode 100644 index 0000000..faaa377 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/common/validators/form_validator.dart @@ -0,0 +1,64 @@ +import 'package:email_validator/email_validator.dart'; + +class FormValidator { + static String? validateForm(String? value) { + if (value == null) { + return null; + } + + if (value.isEmpty) { + return 'The field is required'; + } + return null; + } + + static String? validatePhoneNumber(String? value) { + if (value == null) { + return null; + } + + if (value.isEmpty) { + return 'The field is required'; + } + + // Regex validation + final regex = RegExp(r'^251'); + + if (!regex.hasMatch(value)) { + return 'Phone number must start with 251'; + } + + // Length check first (optional but recommended) + if (value.length != 12) { + return 'Phone number must be 12 digits'; + } + return null; + } + + static String? validateEmail(String? value) { + if (value == null) { + return null; + } + + if (value.isEmpty) { + return 'The field is required'; + } + + if (!EmailValidator.validate(value)) { + return 'Invalid email format'; + } + + return null; + } + + static String? validatePassword(String? value) { + if (value == null) { + return null; + } + + if (value.isEmpty) { + return 'The field is required'; + } + return null; + } +} diff --git a/StudioProjects/yimaru_app/lib/ui/common/validators/onboarding_form_validator.dart b/StudioProjects/yimaru_app/lib/ui/common/validators/onboarding_form_validator.dart deleted file mode 100644 index e0b711e..0000000 --- a/StudioProjects/yimaru_app/lib/ui/common/validators/onboarding_form_validator.dart +++ /dev/null @@ -1,12 +0,0 @@ -class OnboardingFormValidator { - static String? validateForm(String? value) { - if (value == null) { - return null; - } - - if (value.isEmpty) { - return 'The field is required'; - } - return null; - } -} diff --git a/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_view.dart b/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_view.dart new file mode 100644 index 0000000..9599dc7 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_view.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/custom_list_tile.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'account_privacy_viewmodel.dart'; + +class AccountPrivacyView extends StackedView { + const AccountPrivacyView({Key? key}) : super(key: key); + + @override + AccountPrivacyViewModel viewModelBuilder( + BuildContext context, + ) => + AccountPrivacyViewModel(); + + @override + Widget builder( + BuildContext context, + AccountPrivacyViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(AccountPrivacyViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(AccountPrivacyViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(AccountPrivacyViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(AccountPrivacyViewModel viewModel) => + _buildColumn(viewModel); + + Widget _buildColumn(AccountPrivacyViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(AccountPrivacyViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(AccountPrivacyViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar( + title: 'Account Privacy', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) => + Expanded(child: _buildContentColumnWrapper(viewModel)); + + Widget _buildContentColumnWrapper(AccountPrivacyViewModel viewModel) => + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildContentColumn(viewModel), + ); + + Widget _buildContentColumn(AccountPrivacyViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentChildren(viewModel), + ); + + List _buildContentChildren(AccountPrivacyViewModel viewModel) => + [_buildMenuColumnScrollView(viewModel), _buildDeleteButtonWrapper()]; + + Widget _buildMenuColumnScrollView(AccountPrivacyViewModel viewModel) => + SingleChildScrollView( + child: _buildMenuColumn(viewModel), + ); + + Widget _buildMenuColumn(AccountPrivacyViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildMenuColumnChildren(viewModel), + ); + + List _buildMenuColumnChildren(AccountPrivacyViewModel viewModel) => [ + verticalSpaceLarge, + _buildHeader('App Settings'), + verticalSpaceSmall, + _buildLanguageMenu(viewModel), + _buildDividerWrapper(), + verticalSpaceMedium, + _buildHeader('Legal & Information'), + verticalSpaceSmall, + _buildTermsAndConditionsMenu(viewModel), + _buildPrivacyPolicy(viewModel), + _buildDividerWrapper(), + ]; + + Widget _buildHeader(String title) => Text( + title, + style: const TextStyle( + fontSize: 18, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) => + CustomListTile( + isLanguage: true, + language: 'English', + icon: Icons.language, + title: 'Change Language', + onTap: () async => await viewModel.navigateToLanguage(), + ); + + Widget _buildTermsAndConditionsMenu(AccountPrivacyViewModel viewModel) => + CustomListTile( + icon: Icons.handshake, + title: 'Terms & Conditions', + onTap: () async => await viewModel.navigateToTerms(), + ); + + Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) => + CustomListTile( + icon: Icons.shield_moon_outlined, + title: 'Privacy Policy', + onTap: () async => await viewModel.navigateToPrivacyPolicy(), + ); + + Widget _buildDividerWrapper() => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _buildDivider(), + ); + + Widget _buildDivider() => const Divider(color: kcVeryLightGrey); + + Widget _buildDeleteButtonWrapper() => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildDeleteButton(), + ); + Widget _buildDeleteButton() => CustomElevatedButton( + height: 55, + text: 'Delete Account', + borderRadius: 12, + foregroundColor: kcRed, + backgroundColor: kcRed.withOpacity(0.25), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_viewmodel.dart new file mode 100644 index 0000000..6dcc0b7 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_viewmodel.dart @@ -0,0 +1,21 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; + +import '../../../app/app.locator.dart'; + +class AccountPrivacyViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Navigation + void pop() => _navigationService.back(); + + Future navigateToLanguage() async => + await _navigationService.navigateToLanguageView(); + + Future navigateToPrivacyPolicy() async => + await _navigationService.navigateToPrivacyPolicyView(); + + Future navigateToTerms() async => + await _navigationService.navigateToTermsAndConditionsView(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_view.dart b/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_view.dart new file mode 100644 index 0000000..ccb5e99 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_view.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/circular_icon.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'call_support_viewmodel.dart'; + +class CallSupportView extends StackedView { + const CallSupportView({Key? key}) : super(key: key); + + @override + CallSupportViewModel viewModelBuilder( + BuildContext context, + ) => + CallSupportViewModel(); + + @override + Widget builder( + BuildContext context, + CallSupportViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(CallSupportViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(CallSupportViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(CallSupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(CallSupportViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + _buildExpandedColumn(viewModel) + ]; + + Widget _buildAppBarWrapper(CallSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar( + title: 'Call Support', + onTap: viewModel.pop, + ); + + Widget _buildExpandedColumn(CallSupportViewModel viewModel) => + Expanded(child: _buildColumnWrapper(viewModel)); + + Widget _buildColumnWrapper(CallSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(CallSupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(CallSupportViewModel viewModel) => + [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildUpperColumn(CallSupportViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(CallSupportViewModel viewModel) => [ + verticalSpaceLarge, + _buildIcon(), + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildSubTitle('+2519012345678'), + verticalSpaceSmall, + _buildSubTitle('+2519012345678'), + ]; + + Widget _buildIcon() => + const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor); + + Widget _buildTitle() => const Text( + 'Call our support team between 9 AM - 6 PM', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle(String title) => Text( + title, + textAlign: TextAlign.center, + style: const TextStyle(color: kcPrimaryColor), + ); + + Widget _buildContinueButtonWrapper(CallSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildContinueButton(viewModel), + ); + + Widget _buildContinueButton(CallSupportViewModel viewModel) => + const CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Tap to Call', + leadingIcon: Icons.call, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_viewmodel.dart new file mode 100644 index 0000000..119e069 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_viewmodel.dart @@ -0,0 +1,9 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class CallSupportViewModel extends BaseViewModel { + final _navigationService = locator(); + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_view.dart b/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_view.dart new file mode 100644 index 0000000..27d95ac --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_view.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; +import 'package:yimaru_app/ui/widgets/download_card.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'downloads_viewmodel.dart'; + +class DownloadsView extends StackedView { + const DownloadsView({Key? key}) : super(key: key); + + @override + DownloadsViewModel viewModelBuilder( + BuildContext context, + ) => + DownloadsViewModel(); + + @override + Widget builder( + BuildContext context, + DownloadsViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(DownloadsViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(DownloadsViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(DownloadsViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(DownloadsViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(DownloadsViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(DownloadsViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppbar(viewModel), + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppbar(DownloadsViewModel viewModel) => SmallAppBar( + title: 'Offline Downloads', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(DownloadsViewModel viewModel) => + viewModel.showDownload + ? _buildEmptyContent(viewModel) + : _buildContent(viewModel); + + Widget _buildContent(DownloadsViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentChildren(viewModel), + ); + + List _buildContentChildren(DownloadsViewModel viewModel) => [ + verticalSpaceMedium, + _buildStorageSection(viewModel), + verticalSpaceLarge, + _buildDownloads(viewModel) + ]; + + Widget _buildStorageSection(DownloadsViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildStorageSectionChildren(viewModel), + ); + + List _buildStorageSectionChildren(DownloadsViewModel viewModel) => [ + _buildStorageInfoWrapper(viewModel), + _buildStorageIndicator(), + ]; + + Widget _buildStorageInfoWrapper(DownloadsViewModel viewModel) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildStorageInfoChildren(viewModel), + ); + + List _buildStorageInfoChildren(DownloadsViewModel viewModel) => + [_buildStorageInfo(), _buildManageButton(viewModel)]; + + Widget _buildStorageInfo() => const Text.rich( + TextSpan( + text: '1.2GB', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + children: [ + TextSpan( + text: ' used of 2GB', + style: TextStyle( + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ) + ]), + ); + + Widget _buildManageButton(DownloadsViewModel viewModel) => TextButton( + onPressed: viewModel.setShowDownload, child: _buildManageText()); + + Widget _buildManageText() => const Text( + 'Manage Storage', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildStorageIndicator() => const CustomLinearProgressIndicator( + progress: 0.75, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + ); + + Widget _buildDownloads(DownloadsViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.downloads.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildDownload( + size: viewModel.downloads[index]['size'], + title: viewModel.downloads[index]['title'], + duration: viewModel.downloads[index]['duration'], + thumbnail: viewModel.downloads[index]['thumbnail']), + ); + + Widget _buildDownload( + {required String title, + required String size, + required String duration, + required String thumbnail}) => + DownloadCard( + size: size, + title: title, + duration: duration, + thumbnail: thumbnail, + ); + + Widget _buildEmptyContent(DownloadsViewModel viewModel) => Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildEmptyContentChildren(viewModel), + ), + ); + + List _buildEmptyContentChildren(DownloadsViewModel viewModel) => + [_buildUpperEmptyContent(viewModel), _buildGoButtonWrapper(viewModel)]; + + Widget _buildUpperEmptyContent(DownloadsViewModel viewModel) => Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildUpperEmptyContentChildren(viewModel), + ), + ); + + List _buildUpperEmptyContentChildren(DownloadsViewModel viewModel) => + [ + verticalSpaceMassive, + _buildEmptyIcon(), + verticalSpaceMedium, + _buildEmptyTitle(), + verticalSpaceSmall, + _buildEmptySubTitle(), + ]; + + Widget _buildEmptyIcon() => const Icon( + Icons.hourglass_empty, + size: 100, + color: kcPrimaryColor, + ); + + Widget _buildEmptyTitle() => const Text( + 'Looking for something to download?', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildEmptySubTitle() => const Text( + 'Start by exploring your learning materials and save them for offline access.', + textAlign: TextAlign.center, + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildGoButtonWrapper(DownloadsViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildGoButton(viewModel), + ); + Widget _buildGoButton(DownloadsViewModel viewModel) => CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Go to Learn Section', + onTap: viewModel.setShowDownload, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_viewmodel.dart new file mode 100644 index 0000000..0cf971f --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_viewmodel.dart @@ -0,0 +1,42 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class DownloadsViewModel extends BaseViewModel { + final _navigationService = locator(); + + bool _showDownload = false; + + bool get showDownload => _showDownload; + // Downloads + final List> _downloads = [ + { + 'size': '120 MB', + 'duration': '3h 46 m', + 'title': 'Duolingo English', + 'thumbnail': 'assets/images/image_1.png', + }, + { + 'size': '79 MB', + 'duration': '1h 34 m', + 'title': 'IELTS Listening', + 'thumbnail': 'assets/images/image_1.png', + }, + { + 'size': '120 MB', + 'duration': '3h 46 m', + 'title': 'Customer Service', + 'thumbnail': 'assets/images/image_1.png', + }, + ]; + + List> get downloads => _downloads; + + void setShowDownload() { + _showDownload = !_showDownload; + rebuildUi(); + } + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/home/home_view.dart b/StudioProjects/yimaru_app/lib/ui/views/home/home_view.dart index 26b32ed..ff5ccb4 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/home/home_view.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/home/home_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; -import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/views/learn/learn_view.dart'; +import 'package:yimaru_app/ui/views/profile/profile_view.dart'; +import 'package:yimaru_app/ui/widgets/coming_soon.dart'; import 'home_viewmodel.dart'; @@ -9,66 +11,61 @@ class HomeView extends StackedView { const HomeView({Key? key}) : super(key: key); @override - Widget builder(BuildContext context, HomeViewModel viewModel, Widget? child) { - return Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 25.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - verticalSpaceLarge, - Column( - children: [ - const Text( - 'Hello, STACKED!', - style: TextStyle( - fontSize: 35, - fontWeight: FontWeight.w900, - ), - ), - verticalSpaceMedium, - MaterialButton( - color: Colors.black, - onPressed: viewModel.incrementCounter, - child: Text( - viewModel.counterLabel, - style: const TextStyle(color: Colors.white), - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MaterialButton( - color: kcDarkGreyColor, - onPressed: viewModel.showDialog, - child: const Text( - 'Show Dialog', - style: TextStyle(color: Colors.white), - ), - ), - MaterialButton( - color: kcDarkGreyColor, - onPressed: viewModel.showBottomSheet, - child: const Text( - 'Show Bottom Sheet', - style: TextStyle(color: Colors.white), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); + HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel(); + + @override + void onViewModelReady(HomeViewModel viewModel) { + viewModel.getProfileStatus(); + super.onViewModelReady(viewModel); } @override - HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel(); + Widget builder( + BuildContext context, HomeViewModel viewModel, Widget? child) => + _buildScaffold(viewModel); + + Widget _buildScaffold(HomeViewModel viewModel) => Scaffold( + body: getViewForIndex(viewModel.currentIndex), + bottomNavigationBar: BottomNavigationBar( + onTap: viewModel.setCurrentIndex, + items: _buildNavBarItems(), + selectedItemColor: kcPrimaryColor, + backgroundColor: kcBackgroundColor, + type: BottomNavigationBarType.fixed, + currentIndex: viewModel.currentIndex, + ), + ); + + List _buildNavBarItems() => [ + _buildLearnItem(), + _buildCourseItem(), + _buildProfileItem(), + ]; + + BottomNavigationBarItem _buildLearnItem() => const BottomNavigationBarItem( + label: 'Learn', + icon: Icon(Icons.school), + ); + + BottomNavigationBarItem _buildCourseItem() => const BottomNavigationBarItem( + label: 'Course', + icon: Icon(Icons.book), + ); + + BottomNavigationBarItem _buildProfileItem() => const BottomNavigationBarItem( + label: 'Profile', + icon: Icon(Icons.person), + ); +} + +Widget getViewForIndex(int index) { + switch (index) { + case 0: + return const LearnView(); + case 1: + return const ComingSoon(); + + default: + return const ProfileView(); + } } diff --git a/StudioProjects/yimaru_app/lib/ui/views/home/home_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/home/home_viewmodel.dart index 5928270..428a58f 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/home/home_viewmodel.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/home/home_viewmodel.dart @@ -1,20 +1,30 @@ import 'package:yimaru_app/app/app.bottomsheets.dart'; import 'package:yimaru_app/app/app.dialogs.dart'; import 'package:yimaru_app/app/app.locator.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/models/user_model.dart'; import 'package:yimaru_app/ui/common/app_strings.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; +import '../../../services/api_service.dart'; +import '../../../services/authentication_service.dart'; +import '../../common/enmus.dart'; + class HomeViewModel extends BaseViewModel { + final _apiService = locator(); final _dialogService = locator(); + final _navigationService = locator(); final _bottomSheetService = locator(); + final _authenticationService = locator(); - String get counterLabel => 'Counter is: $_counter'; + // Bottom navigation + int _currentIndex = 0; - int _counter = 0; + int get currentIndex => _currentIndex; - void incrementCounter() { - _counter++; + void setCurrentIndex(int index) { + _currentIndex = index; rebuildUi(); } @@ -22,7 +32,7 @@ class HomeViewModel extends BaseViewModel { _dialogService.showCustomDialog( variant: DialogType.infoAlert, title: 'Stacked Rocks!', - description: 'Give stacked $_counter stars on Github', + description: 'Give stacked stars on Github', ); } @@ -33,4 +43,20 @@ class HomeViewModel extends BaseViewModel { description: ksHomeBottomSheetDescription, ); } + + // Navigation + Future replaceWithOnboarding() async => + await _navigationService.replaceWithOnboardingView(); + + // Remote api calls + Future getProfileStatus() async { + UserModel user = await _authenticationService.getUser(); + + Map response = await runBusyFuture>( + _apiService.getProfileStatus(user)); + + if (response['status'] == ResponseStatus.success && !response['data']) { + await replaceWithOnboarding(); + } + } } diff --git a/StudioProjects/yimaru_app/lib/ui/views/language/language_view.dart b/StudioProjects/yimaru_app/lib/ui/views/language/language_view.dart new file mode 100644 index 0000000..100f9ae --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/language/language_view.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_small_radio_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'language_viewmodel.dart'; + +class LanguageView extends StackedView { + const LanguageView({Key? key}) : super(key: key); + + @override + LanguageViewModel viewModelBuilder( + BuildContext context, + ) => + LanguageViewModel(); + + @override + Widget builder( + BuildContext context, + LanguageViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LanguageViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LanguageViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(LanguageViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(LanguageViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + _buildExpandedBody(viewModel) + ]; + + Widget _buildExpandedBody(LanguageViewModel viewModel) => + Expanded(child: _buildColumnWrapper(viewModel)); + + Widget _buildColumnWrapper(LanguageViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LanguageViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(LanguageViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceSmall, + _buildSubTitle(), + verticalSpaceMedium, + _buildLanguages(viewModel) + ]; + + Widget _buildAppBarWrapper(LanguageViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar( + title: 'Language Preference', + onTap: viewModel.pop, + ); + + Widget _buildTitle() => const Text( + 'Choose your language', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => const Text( + 'You can switch languages anytime', + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.languages.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildLanguage( + title: viewModel.languages[index]['language'], + selected: viewModel + .isSelectedLanguage(viewModel.languages[index]['language']), + onTap: () => + viewModel.setSelectedLanguage(viewModel.languages[index]), + ), + ); + + Widget _buildLanguage( + {required String title, + required bool selected, + required GestureTapCallback onTap}) => + CustomSmallRadioButton( + title: title, + onTap: onTap, + selected: selected, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/language/language_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/language/language_viewmodel.dart new file mode 100644 index 0000000..74ff8ad --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/language/language_viewmodel.dart @@ -0,0 +1,35 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class LanguageViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Languages + + Map _selectedLanguage = { + 'code': 'EN', + 'language': 'English' + }; + + Map get selectedLanguage => _selectedLanguage; + + final List> _languages = [ + {'code': 'አማ', 'language': 'አማርኛ'}, + {'code': 'EN', 'language': 'English'}, + ]; + + List> get languages => _languages; + + // Languages + void setSelectedLanguage(Map title) { + _selectedLanguage = title; + rebuildUi(); + } + + bool isSelectedLanguage(String title) => + _selectedLanguage['language'] == title; + // Navigation + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn/learn_view.dart b/StudioProjects/yimaru_app/lib/ui/views/learn/learn_view.dart new file mode 100644 index 0000000..b13ab6a --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn/learn_view.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/learn_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/learn_level_tile.dart'; + +import '../../common/app_colors.dart'; +import '../../common/enmus.dart'; +import '../../common/ui_helpers.dart'; +import 'learn_viewmodel.dart'; + +class LearnView extends StackedView { + const LearnView({Key? key}) : super(key: key); + + @override + LearnViewModel viewModelBuilder(BuildContext context) => LearnViewModel(); + + @override + Widget builder( + BuildContext context, + LearnViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LearnViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LearnViewModel viewModel) => + SafeArea(child: _buildBody(viewModel)); + + Widget _buildBody(LearnViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LearnViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildAppBar(), + _buildLevelsColumnWrapper(viewModel) + ], + ); + + Widget _buildAppBar() => const LearnAppBar(); + + Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) => + Expanded(child: _buildLevelsColumnScrollView(viewModel)); + + Widget _buildLevelsColumnScrollView(LearnViewModel viewModel) => + SingleChildScrollView( + child: _buildLevelsColumn(viewModel), + ); + + Widget _buildLevelsColumn(LearnViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildLevelsColumnChildren(viewModel), + ); + + List _buildLevelsColumnChildren(LearnViewModel viewModel) => + [verticalSpaceLarge, _buildListView(viewModel)]; + + Widget _buildListView(LearnViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.learnLevels.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildTile( + title: viewModel.learnLevels[index]['title'], + status: viewModel.learnLevels[index]['status'], + subtitle: viewModel.learnLevels[index]['subtitle']), + ); + + Widget _buildTile( + {required String title, + required String subtitle, + required LearnLevelStatus status}) => + LearnLevelTile( + title: title, + status: status, + subtitle: subtitle, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn/learn_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/learn/learn_viewmodel.dart new file mode 100644 index 0000000..0ac3f71 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn/learn_viewmodel.dart @@ -0,0 +1,33 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; + +import '../../../app/app.locator.dart'; + +class LearnViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _learnLevels = [ + { + 'title': 'Beginner', + 'status': LearnLevelStatus.completed, + 'subtitle': 'Start your journey with the basics of English.', + }, + { + 'title': 'Intermediate', + 'status': LearnLevelStatus.started, + 'subtitle': 'Practice real conversations and expand vocabulary.', + }, + { + 'title': 'Advanced', + 'status': LearnLevelStatus.pending, + 'subtitle': 'Achieve fluency and master complex topics.', + }, + ]; + + List> get learnLevels => _learnLevels; + + Future navigateToLearnLevel() async => + _navigationService.navigateToLearnLevelView(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_view.dart b/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_view.dart new file mode 100644 index 0000000..7bcfed6 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_view.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/learn_sub_level_tile.dart'; +import 'package:yimaru_app/ui/widgets/small_app_bar.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import 'learn_level_viewmodel.dart'; + +class LearnLevelView extends StackedView { + const LearnLevelView({Key? key}) : super(key: key); + + @override + LearnLevelViewModel viewModelBuilder(BuildContext context) => + LearnLevelViewModel(); + + @override + Widget builder( + BuildContext context, + LearnLevelViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LearnLevelViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LearnLevelViewModel viewModel) => + SafeArea(child: _buildBody(viewModel)); + + Widget _buildBody(LearnLevelViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LearnLevelViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildAppBar(viewModel), + _buildLevelsColumnWrapper(viewModel) + ], + ); + + Widget _buildAppBar(LearnLevelViewModel viewModel) => SmallAppBar( + onTap: viewModel.pop, + ); + + Widget _buildLevelsColumnWrapper(LearnLevelViewModel viewModel) => + Expanded(child: _buildLevelsColumnScrollView(viewModel)); + + Widget _buildLevelsColumnScrollView(LearnLevelViewModel viewModel) => + SingleChildScrollView( + child: _buildLevelsColumn(viewModel), + ); + + Widget _buildLevelsColumn(LearnLevelViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildLevelsColumnChildren(viewModel), + ); + + List _buildLevelsColumnChildren(LearnLevelViewModel viewModel) => + [verticalSpaceLarge, _buildListView(viewModel)]; + + Widget _buildListView(LearnLevelViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.learnSubLevels.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildTile( + title: viewModel.learnSubLevels[index]['title'], + current: viewModel.learnSubLevels[index]['current'], + subtitle: viewModel.learnSubLevels[index]['subtitle']), + ); + + Widget _buildTile({ + required String title, + required bool current, + required String subtitle, + }) => + LearnSubLevelTile( + title: title, + current: current, + subtitle: subtitle, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_viewmodel.dart new file mode 100644 index 0000000..01141b6 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_viewmodel.dart @@ -0,0 +1,29 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; + +import '../../../app/app.locator.dart'; + +class LearnLevelViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _learnSubLevels = [ + { + 'title': 'A1', + 'current': true, + 'subtitle': 'Start your journey with the basics of English.', + }, + { + 'title': 'A2', + 'current': false, + 'subtitle': 'Build upon your foundational knowledge.', + }, + ]; + + List> get learnSubLevels => _learnSubLevels; + + Future navigateToLearnModule() async => + _navigationService.navigateToLearnModuleView(); + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_view.dart b/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_view.dart new file mode 100644 index 0000000..db4b79d --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_view.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/widgets/learn_module_tile.dart'; +import 'package:yimaru_app/ui/widgets/overall_learn_progress.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'learn_module_viewmodel.dart'; + +class LearnModuleView extends StackedView { + const LearnModuleView({Key? key}) : super(key: key); + + @override + LearnModuleViewModel viewModelBuilder(BuildContext context) => + LearnModuleViewModel(); + + @override + Widget builder( + BuildContext context, + LearnModuleViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LearnModuleViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LearnModuleViewModel viewModel) => + SafeArea(child: _buildBody(viewModel)); + + Widget _buildBody(LearnModuleViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LearnModuleViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildAppBar(viewModel), + _buildLevelsColumnWrapper(viewModel), + ], + ); + + Widget _buildAppBar(LearnModuleViewModel viewModel) => SmallAppBar( + onTap: viewModel.pop, + ); + + Widget _buildLevelsColumnWrapper(LearnModuleViewModel viewModel) => + Expanded(child: _buildLevelsColumnScrollView(viewModel)); + + Widget _buildLevelsColumnScrollView(LearnModuleViewModel viewModel) => + SingleChildScrollView( + child: _buildLevelsColumn(viewModel), + ); + + Widget _buildLevelsColumn(LearnModuleViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildLevelsColumnChildren(viewModel), + ); + + List _buildLevelsColumnChildren(LearnModuleViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitle(), + verticalSpaceMedium, + _buildOverallProgress(), + verticalSpaceMedium, + _buildListView(viewModel) + ]; + + Widget _buildTitle() => const Text( + 'A1 - Beginner', + style: TextStyle( + fontSize: 18, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => const Text( + 'Your Current Level', + style: TextStyle( + color: kcDarkGrey, + ), + ); + + Widget _buildOverallProgress() => const OverallLearnProgress(); + + Widget _buildListView(LearnModuleViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.modules.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildTile( + title: viewModel.modules[index]['title'], + status: viewModel.modules[index]['status'], + subtitle: viewModel.modules[index]['subtitle']), + ); + + Widget _buildTile({ + required String title, + required String subtitle, + required LearnLevelStatus status, + }) => + LearnModuleTile( + title: title, + status: status, + subtitle: subtitle, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_viewmodel.dart new file mode 100644 index 0000000..87eaa35 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_viewmodel.dart @@ -0,0 +1,38 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; +import '../../common/enmus.dart'; + +class LearnModuleViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _modules = [ + { + 'status': LearnLevelStatus.started, + 'title': 'Module 1: Greetings & Introductions', + 'subtitle': + 'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.', + }, + { + 'status': LearnLevelStatus.pending, + 'title': 'Module 2: Everyday Basics', + 'subtitle': 'Learn numbers, colors, and common objects.', + }, + { + 'title': 'Module 3: At the Cafe', + 'status': LearnLevelStatus.pending, + 'subtitle': 'Practice ordering food and drinks confidently.', + }, + { + 'progress': 0, + 'status': LearnLevelStatus.pending, + 'title': 'Module 4: Asking for Directions', + 'subtitle': 'Learn numbers, colors, and common objects.', + }, + ]; + + List> get modules => _modules; + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/login_view.dart b/StudioProjects/yimaru_app/lib/ui/views/login/login_view.dart new file mode 100644 index 0000000..068a0f4 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/login_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/views/login/screens/login_otp_screen.dart'; +import 'package:yimaru_app/ui/views/login/screens/login_with_email_screen.dart'; +import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart'; + +import '../../common/app_colors.dart'; +import '../../common/validators/form_validator.dart'; +import '../../widgets/large_app_bar.dart'; +import '../../widgets/page_loading_indicator.dart'; +import 'login_viewmodel.dart'; + +import 'login_view.form.dart'; + +@FormView(fields: [ + FormTextField(name: 'otp', validator: FormValidator.validateForm), + FormTextField(name: 'email', validator: FormValidator.validateEmail), + FormTextField(name: 'password', validator: FormValidator.validateForm), + FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm) +]) +class LoginView extends StackedView with $LoginView { + const LoginView({Key? key}) : super(key: key); + + @override + void onViewModelReady(LoginViewModel viewModel) { + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel(); + + @override + Widget builder( + BuildContext context, + LoginViewModel viewModel, + Widget? child, + ) => + _buildLoginScreensWrapper(viewModel); + + Widget _buildLoginScreensWrapper(LoginViewModel viewModel) => PopScope( + canPop: true, + onPopInvokedWithResult: (value, data) { + if (!value) return; + WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); + }, + child: _buildScaffoldWrapper(viewModel)); + + Widget _buildScaffoldWrapper(LoginViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffoldStack(viewModel), + ); + + Widget _buildScaffoldStack(LoginViewModel viewModel) => + Stack(children: [_buildScaffold(viewModel), _buildBusyLogin(viewModel)]); + + Widget _buildScaffold(LoginViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildScaffoldChildren(viewModel), + ); + + List _buildScaffoldChildren(LoginViewModel viewModel) => + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; + + Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + ); + Widget _buildExpandedBody(LoginViewModel viewModel) => + Expanded(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(LoginViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), + ); + + Widget _buildBody(LoginViewModel viewModel) => + IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); + + List _buildScreens() => [ + _buildLoginWithEmailScreen(), + _buildLoginWithPhoneScreen(), + _buildLoginOtpScreen() + ]; + + Widget _buildLoginWithEmailScreen() => LoginWithEmailScreen( + emailController: emailController, passwordController: passwordController); + + Widget _buildLoginWithPhoneScreen() => + LoginWithPhoneNumberScreen(phoneNumberController: phoneNumberController); + + Widget _buildLoginOtpScreen() => LoginOtpScreen( + otpController: otpController, + phoneNumberController: phoneNumberController); + + Widget _buildBusyLogin(LoginViewModel viewModel) => + viewModel.isBusy ? const PageLoadingIndicator() : Container(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/login_view.form.dart b/StudioProjects/yimaru_app/lib/ui/views/login/login_view.form.dart new file mode 100644 index 0000000..5a64de2 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/login_view.form.dart @@ -0,0 +1,269 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// StackedFormGenerator +// ************************************************************************** + +// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; + +const bool _autoTextFieldValidation = true; + +const String OtpValueKey = 'otp'; +const String EmailValueKey = 'email'; +const String PasswordValueKey = 'password'; +const String PhoneNumberValueKey = 'phoneNumber'; + +final Map _LoginViewTextEditingControllers = {}; + +final Map _LoginViewFocusNodes = {}; + +final Map _LoginViewTextValidations = { + OtpValueKey: FormValidator.validateForm, + EmailValueKey: FormValidator.validateEmail, + PasswordValueKey: FormValidator.validateForm, + PhoneNumberValueKey: FormValidator.validateForm, +}; + +mixin $LoginView { + TextEditingController get otpController => + _getFormTextEditingController(OtpValueKey); + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); + TextEditingController get passwordController => + _getFormTextEditingController(PasswordValueKey); + TextEditingController get phoneNumberController => + _getFormTextEditingController(PhoneNumberValueKey); + + FocusNode get otpFocusNode => _getFormFocusNode(OtpValueKey); + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); + FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey); + FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey); + + TextEditingController _getFormTextEditingController( + String key, { + String? initialValue, + }) { + if (_LoginViewTextEditingControllers.containsKey(key)) { + return _LoginViewTextEditingControllers[key]!; + } + + _LoginViewTextEditingControllers[key] = + TextEditingController(text: initialValue); + return _LoginViewTextEditingControllers[key]!; + } + + FocusNode _getFormFocusNode(String key) { + if (_LoginViewFocusNodes.containsKey(key)) { + return _LoginViewFocusNodes[key]!; + } + _LoginViewFocusNodes[key] = FocusNode(); + return _LoginViewFocusNodes[key]!; + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + void syncFormWithViewModel(FormStateHelper model) { + otpController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + @Deprecated( + 'Use syncFormWithViewModel instead.' + 'This feature was deprecated after 3.1.0.', + ) + void listenToFormUpdated(FormViewModel model) { + otpController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Updates the formData on the FormViewModel + void _updateFormData(FormStateHelper model, {bool forceValidate = false}) { + model.setData( + model.formValueMap + ..addAll({ + OtpValueKey: otpController.text, + EmailValueKey: emailController.text, + PasswordValueKey: passwordController.text, + PhoneNumberValueKey: phoneNumberController.text, + }), + ); + + if (_autoTextFieldValidation || forceValidate) { + updateValidationData(model); + } + } + + bool validateFormFields(FormViewModel model) { + _updateFormData(model, forceValidate: true); + return model.isFormValid; + } + + /// Calls dispose on all the generated controllers and focus nodes + void disposeForm() { + // The dispose function for a TextEditingController sets all listeners to null + + for (var controller in _LoginViewTextEditingControllers.values) { + controller.dispose(); + } + for (var focusNode in _LoginViewFocusNodes.values) { + focusNode.dispose(); + } + + _LoginViewTextEditingControllers.clear(); + _LoginViewFocusNodes.clear(); + } +} + +extension ValueProperties on FormStateHelper { + bool get hasAnyValidationMessage => this + .fieldsValidationMessages + .values + .any((validation) => validation != null); + + bool get isFormValid { + if (!_autoTextFieldValidation) this.validateForm(); + + return !hasAnyValidationMessage; + } + + String? get otpValue => this.formValueMap[OtpValueKey] as String?; + String? get emailValue => this.formValueMap[EmailValueKey] as String?; + String? get passwordValue => this.formValueMap[PasswordValueKey] as String?; + String? get phoneNumberValue => + this.formValueMap[PhoneNumberValueKey] as String?; + + set otpValue(String? value) { + this.setData( + this.formValueMap..addAll({OtpValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(OtpValueKey)) { + _LoginViewTextEditingControllers[OtpValueKey]?.text = value ?? ''; + } + } + + set emailValue(String? value) { + this.setData( + this.formValueMap..addAll({EmailValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(EmailValueKey)) { + _LoginViewTextEditingControllers[EmailValueKey]?.text = value ?? ''; + } + } + + set passwordValue(String? value) { + this.setData( + this.formValueMap..addAll({PasswordValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(PasswordValueKey)) { + _LoginViewTextEditingControllers[PasswordValueKey]?.text = value ?? ''; + } + } + + set phoneNumberValue(String? value) { + this.setData( + this.formValueMap..addAll({PhoneNumberValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(PhoneNumberValueKey)) { + _LoginViewTextEditingControllers[PhoneNumberValueKey]?.text = value ?? ''; + } + } + + bool get hasOtp => + this.formValueMap.containsKey(OtpValueKey) && + (otpValue?.isNotEmpty ?? false); + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); + bool get hasPassword => + this.formValueMap.containsKey(PasswordValueKey) && + (passwordValue?.isNotEmpty ?? false); + bool get hasPhoneNumber => + this.formValueMap.containsKey(PhoneNumberValueKey) && + (phoneNumberValue?.isNotEmpty ?? false); + + bool get hasOtpValidationMessage => + this.fieldsValidationMessages[OtpValueKey]?.isNotEmpty ?? false; + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; + bool get hasPasswordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false; + bool get hasPhoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false; + + String? get otpValidationMessage => + this.fieldsValidationMessages[OtpValueKey]; + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; + String? get passwordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]; + String? get phoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]; +} + +extension Methods on FormStateHelper { + setOtpValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[OtpValueKey] = validationMessage; + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; + setPasswordValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PasswordValueKey] = validationMessage; + setPhoneNumberValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage; + + /// Clears text input fields on the Form + void clearForm() { + otpValue = ''; + emailValue = ''; + passwordValue = ''; + phoneNumberValue = ''; + } + + /// Validates text input fields on the Form + void validateForm() { + this.setValidationMessages({ + OtpValueKey: getValidationMessage(OtpValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + }); + } +} + +/// Returns the validation message for the given key +String? getValidationMessage(String key) { + final validatorForKey = _LoginViewTextValidations[key]; + if (validatorForKey == null) return null; + + String? validationMessageForKey = validatorForKey( + _LoginViewTextEditingControllers[key]!.text, + ); + + return validationMessageForKey; +} + +/// Updates the fieldsValidationMessages on the FormViewModel +void updateValidationData(FormStateHelper model) => + model.setValidationMessages({ + OtpValueKey: getValidationMessage(OtpValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + }); diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/login_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/login/login_viewmodel.dart new file mode 100644 index 0000000..a58582d --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/login_viewmodel.dart @@ -0,0 +1,161 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.locator.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/models/user_model.dart'; + +import '../../../services/api_service.dart'; +import '../../../services/authentication_service.dart'; +import '../../common/enmus.dart'; +import '../../common/ui_helpers.dart'; +import '../home/home_view.dart'; + +class LoginViewModel extends FormViewModel { + final _apiService = locator(); + final _navigationService = locator(); + final _authenticationService = locator(); + + // Navigation + int _currentIndex = 0; + + int get currentIndex => _currentIndex; + + // Email + bool _focusEmail = false; + + bool get focusEmail => _focusEmail; + + // Password + bool _focusPassword = false; + + bool get focusPassword => _focusPassword; + + bool _obscurePassword = true; + + bool get obscurePassword => _obscurePassword; + + // Phone number + bool _focusPhoneNumber = false; + + bool get focusPhoneNumber => _focusPhoneNumber; + + // Focus otp + bool _focusOtp = false; + + bool get focusOtp => _focusOtp; + + // Focus node + final FocusNode _focusNode = FocusNode(); + + FocusNode get focusNode => _focusNode; + + // Resend button state + bool _buttonActive = false; + + bool get buttonActive => _buttonActive; + + // User data + final Map _userData = {}; + + Map get userData => _userData; + + // Email + void setEmailFocus() { + _focusEmail = true; + rebuildUi(); + } + + // Password + void setPasswordFocus() { + _focusPassword = true; + rebuildUi(); + } + + void setObscurePassword() { + _obscurePassword = !_obscurePassword; + rebuildUi(); + } + + // Phone number + void setPhoneNumberFocus() { + _focusPhoneNumber = true; + rebuildUi(); + } + + // Otp + void setOtpFocus() { + _focusOtp = true; + rebuildUi(); + } + + // Validate otp + Future validateOtp(String otp) async {} + + void setResendButton() { + _buttonActive = true; + rebuildUi(); + } + + // Add user data + void addUserData(Map data) { + _userData.addAll(data); + } + + void clearUserData() { + _userData.clear(); + } + + // Remote api calls + Future login() async { + Map response = + await runBusyFuture>(_login()); + + if (response['status'] == ResponseStatus.success) { + await replaceWithHome(); + } + } + + Future> _login() async { + Map response = await _apiService.login(_userData); + if (response['status'] == ResponseStatus.success) { + UserModel user = response['data'] as UserModel; + Map data = { + 'userId': user.userId, + 'accessToken': user.accessToken, + 'refreshToken': user.refreshToken + }; + + await _authenticationService.saveUserData(data); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + + return response; + } + + // Navigation + void goTo(int page) { + _currentIndex = page; + rebuildUi(); + } + + void goBack() { + if (_currentIndex == 1) { + _currentIndex = 0; + rebuildUi(); + } else if (_currentIndex == 2) { + _currentIndex = 1; + rebuildUi(); + } else { + _navigationService.back(); + } + } + + Future navigateToRegister() async => + await _navigationService.navigateToRegisterView(); + + Future replaceWithHome() async => + await _navigationService.clearStackAndShowView(const HomeView()); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_otp_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_otp_screen.dart new file mode 100644 index 0000000..cd8d881 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_otp_screen.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_timer_countdown/flutter_timer_countdown.dart'; +import 'package:pinput/pinput.dart'; +import 'package:stacked/stacked.dart'; + +import 'package:yimaru_app/ui/views/register/register_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_cursor.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/large_app_bar.dart'; +import '../login_viewmodel.dart'; +import '../login_view.form.dart'; + +class LoginOtpScreen extends ViewModelWidget { + final TextEditingController otpController; + final TextEditingController phoneNumberController; + + const LoginOtpScreen( + {super.key, + required this.otpController, + required this.phoneNumberController}); + + @override + Widget build(BuildContext context, LoginViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(LoginViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(LoginViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(LoginViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(LoginViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildSubtitleWrapper(), + verticalSpaceMedium, + _buildPinPutWrapper(viewModel), + if (viewModel.hasOtpValidationMessage && viewModel.focusOtp) + verticalSpaceTiny, + if (viewModel.hasOtpValidationMessage && viewModel.focusOtp) + _buildOtpValidatorWrapper(viewModel), + verticalSpaceSmall, + _buildTimerWrapper(viewModel) + ]; + + Widget _buildTitle() => Text( + 'Verification Code', + style: style25DG600, + ); + + Widget _buildSubtitleWrapper() => + phoneNumberController.text.length == 9 ? _buildSubtitle() : Container(); + + Widget _buildSubtitle() => Text( + 'Code sent to your number +251${phoneNumberController.text.substring(0, 5)}****', + style: style14DG400, + ); + + Widget _buildPinPutWrapper(LoginViewModel viewModel) => Center( + child: _buildPinPut(viewModel), + ); + + Widget _buildPinPut(LoginViewModel viewModel) => Pinput( + controller: otpController, + defaultPinTheme: defaultPin, + cursor: const CustomCursor(), + errorPinTheme: errorPinTheme, + onTap: viewModel.setOtpFocus, + focusNode: viewModel.focusNode, + errorTextStyle: validationStyle, + //smsRetriever: locator(), + focusedPinTheme: focusedThemePin, + submittedPinTheme: submittedThemePin, + hapticFeedbackType: HapticFeedbackType.heavyImpact, + separatorBuilder: (index) => const SizedBox(width: 25), + onCompleted: (otp) async => await viewModel.validateOtp(otp), + ); + + Widget _buildOtpValidatorWrapper(LoginViewModel viewModel) => + viewModel.hasOtpValidationMessage + ? _buildOtpValidator(viewModel) + : Container(); + + Widget _buildOtpValidator(LoginViewModel viewModel) => Text( + viewModel.otpValidationMessage!, + style: style12R700, + ); + + Widget _buildTimerWrapper(LoginViewModel viewModel) => !viewModel.buttonActive + ? _buildTimerSection(viewModel) + : _buildResendButton(); + + Widget _buildResendButton() => + TextButton(onPressed: () {}, child: _buildResendText()); + + Widget _buildResendText() => Text( + 'Resend code', + style: style14P600.copyWith(fontStyle: FontStyle.italic), + ); + + Widget _buildTimerSection(LoginViewModel viewModel) => Row( + children: [ + _buildCountdownText(), + horizontalSpaceSmall, + _buildTimer(viewModel) + ], + ); + + Widget _buildCountdownText() => Text('Resend code in ', style: style14DG400); + + Widget _buildTimer(LoginViewModel viewModel) => TimerCountdown( + enableDescriptions: false, + timeTextStyle: style14P600, + endTime: DateTime.now().add(const Duration(minutes: 3, seconds: 0)), + onEnd: viewModel.setResendButton, + format: CountDownTimerFormat.minutesSeconds, + colonsTextStyle: const TextStyle(color: kcPrimaryColor), + ); + + Widget _buildContinueButtonWrapper(LoginViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildContinueButton(viewModel), + ); + + Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: viewModel.focusOtp && + otpController.text.length == 4 && + !viewModel.hasOtpValidationMessage + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + onTap: viewModel.focusOtp && + otpController.text.length == 4 && + !viewModel.hasOtpValidationMessage + ? () => viewModel.replaceWithHome() + : null, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_email_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_email_screen.dart new file mode 100644 index 0000000..5e31632 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_email_screen.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/login/login_view.form.dart'; +import 'package:yimaru_app/ui/widgets/obscure_password.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/option_text_divider.dart'; +import '../../../widgets/register_for_account.dart'; +import '../login_viewmodel.dart'; + +class LoginWithEmailScreen extends ViewModelWidget { + final TextEditingController emailController; + final TextEditingController passwordController; + + const LoginWithEmailScreen( + {super.key, + required this.emailController, + required this.passwordController}); + + Future _login(LoginViewModel viewModel) async { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'email': emailController.text, + 'password': passwordController.text, + }; + viewModel.addUserData(data); + + await viewModel.login(); + } + + @override + Widget build(BuildContext context, LoginViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(LoginViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(LoginViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildColumnScroller(LoginViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(LoginViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitleWrapper(viewModel), + verticalSpaceLarge, + _buildEmailFormField(viewModel), + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + verticalSpaceTiny, + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + _buildEmailValidatorWrapper(viewModel), + verticalSpaceMedium, + _buildPasswordFormField(viewModel), + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + verticalSpaceTiny, + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + _buildPasswordValidationWrapper(viewModel), + _buildForgetPasswordTextButtonWrapper(), + ]; + + Widget _buildTitle() => const Text( + 'Welcome Back', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( + onTap: () async => await viewModel.navigateToRegister(), + ); + + Widget _buildEmailFormField(LoginViewModel viewModel) => TextFormField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + onTap: viewModel.setEmailFocus, + decoration: inputDecoration( + hint: 'Email', + focus: viewModel.focusEmail, + filled: emailController.text.isNotEmpty), + ); + + Widget _buildEmailValidatorWrapper(LoginViewModel viewModel) => + viewModel.hasEmailValidationMessage + ? _buildEmailValidator(viewModel) + : Container(); + + Widget _buildEmailValidator(LoginViewModel viewModel) => Text( + viewModel.emailValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField( + controller: passwordController, + onTap: viewModel.setPasswordFocus, + obscureText: viewModel.obscurePassword, + decoration: inputDecoration( + hint: 'Password', + focus: viewModel.focusPassword, + suffix: _buildObscureButton(viewModel), + filled: passwordController.text.isNotEmpty), + ); + + Widget _buildObscureButton(LoginViewModel viewModel) => ObscurePassword( + focus: viewModel.focusPassword, + obscure: viewModel.obscurePassword, + onTap: viewModel.setObscurePassword, + ); + + Widget _buildPasswordValidationWrapper(LoginViewModel viewModel) => + viewModel.hasPasswordValidationMessage + ? _buildPasswordValidator(viewModel) + : Container(); + + Widget _buildPasswordValidator(LoginViewModel viewModel) => Text( + viewModel.passwordValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildForgetPasswordTextButtonWrapper() => Align( + alignment: Alignment.centerRight, + child: _buildForgetPasswordTextButton(), + ); + + Widget _buildForgetPasswordTextButton() => TextButton( + onPressed: () {}, + child: _buildForgetPasswordText(), + ); + + Widget _buildForgetPasswordText() => const Text( + 'Forget Password?', + style: TextStyle(color: kcPrimaryColor), + ); + + Widget _buildLowerColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(LoginViewModel viewModel) => [ + _buildContinueButton(viewModel), + _buildOptionTextDivider(), + _buildLoginWithEmailButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: (viewModel.focusEmail && + emailController.text.isNotEmpty) && + (viewModel.focusPassword && passwordController.text.isNotEmpty) + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + onTap: (viewModel.focusEmail && emailController.text.isNotEmpty) && + (viewModel.focusPassword && passwordController.text.isNotEmpty) + ? () async => await _login(viewModel) + : null, + ); + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildLoginWithEmailButton(LoginViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + backgroundColor: kcWhite, + leadingIcon: Icons.phone, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + text: 'Login with Phone Number', + onTap: () => viewModel.goTo(1), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_phone_number_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_phone_number_screen.dart new file mode 100644 index 0000000..b42e4cf --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_phone_number_screen.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/login/login_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/option_text_divider.dart'; +import 'package:yimaru_app/ui/widgets/register_for_account.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/phone_number_prefix.dart'; +import '../login_view.form.dart'; + +class LoginWithPhoneNumberScreen extends ViewModelWidget { + final TextEditingController phoneNumberController; + const LoginWithPhoneNumberScreen( + {super.key, required this.phoneNumberController}); + + @override + Widget build(BuildContext context, LoginViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(LoginViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(LoginViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildColumnScroller(LoginViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(LoginViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitleWrapper(viewModel), + verticalSpaceMedium, + _buildSubtitle(), + verticalSpaceMedium, + _buildPhoneNumberWrapper(viewModel), + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + verticalSpaceTiny, + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + _buildPhoneNumberValidatorWrapper(viewModel), + ]; + + Widget _buildTitle() => const Text( + 'Welcome Back', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( + onTap: () async => await viewModel.navigateToRegister(), + ); + Widget _buildSubtitle() => const Text( + 'Enter your phone number. We will send you a confirmation code there', + style: TextStyle(color: kcMediumGrey), + ); + Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildPhoneNumberChildren(viewModel), + ); + + List _buildPhoneNumberChildren(LoginViewModel viewModel) => [ + _buildPhoneNumberPrefix(viewModel), + horizontalSpaceSmall, + _buildPhoneNumberFormFieldWrapper(viewModel), + ]; + + Widget _buildPhoneNumberPrefix(LoginViewModel viewModel) => + PhoneNumberPrefix(selected: viewModel.focusPhoneNumber); + + Widget _buildPhoneNumberFormFieldWrapper(LoginViewModel viewModel) => + Expanded(child: _buildPhoneNumberFormField(viewModel)); + + Widget _buildPhoneNumberFormField(LoginViewModel viewModel) => TextFormField( + maxLength: 9, + keyboardType: TextInputType.phone, + controller: phoneNumberController, + onTap: viewModel.setPhoneNumberFocus, + decoration: inputDecoration( + focus: viewModel.focusPhoneNumber, + filled: phoneNumberController.text.isNotEmpty), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ); + + Widget _buildPhoneNumberValidatorWrapper(LoginViewModel viewModel) => + viewModel.hasPhoneNumberValidationMessage + ? _buildPhoneNumberValidator(viewModel) + : Container(); + + Widget _buildPhoneNumberValidator(LoginViewModel viewModel) => Text( + viewModel.phoneNumberValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildLowerColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(LoginViewModel viewModel) => [ + _buildContinueButton(viewModel), + _buildOptionTextDivider(), + _buildLoginWitPhoneNumberButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: + viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty + ? () => viewModel.goTo(2) + : null, + backgroundColor: + viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); + + Widget _buildLoginWitPhoneNumberButton(LoginViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Login with Email', + backgroundColor: kcWhite, + leadingIcon: Icons.email, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + onTap: () => viewModel.goTo(0), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.dart index 4206bcc..bddc8a8 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.dart @@ -1,52 +1,64 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked/stacked_annotations.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_completion.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_failure.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_intro.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_result.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/first_assessment_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/result_analysis.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/retake_assessment.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/second_assessment_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/start_lesson.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/third_assessment_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/age_group_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/challenge_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/country_region_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/educational_background_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/full_name_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_goal_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_reason_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/occupation_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/topic_form.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_result_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/result_analysis_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/start_lesson_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/age_group_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/challenge_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/country_region_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/educational_background_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/full_name_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/occupation_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/topic_form_screen.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/welcome/first_welcome.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/welcome/second_welcome.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/welcome/third_welcome.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/welcome/first_welcome_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/welcome/second_welcome_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/welcome/third_welcome_screen.dart'; -import '../../common/validators/onboarding_form_validator.dart'; +import '../../common/validators/form_validator.dart'; import 'onboarding_viewmodel.dart'; import 'onboarding_view.form.dart'; @FormView(fields: [ - FormTextField( - name: 'answer', validator: OnboardingFormValidator.validateForm), - FormTextField( - name: 'fullName', validator: OnboardingFormValidator.validateForm), - FormTextField( - name: 'challenge', validator: OnboardingFormValidator.validateForm), - FormTextField( - name: 'occupation', validator: OnboardingFormValidator.validateForm), - FormTextField( - name: 'learningReason', validator: OnboardingFormValidator.validateForm), - FormTextField(name: 'topic', validator: OnboardingFormValidator.validateForm), + FormTextField(name: 'answer', validator: FormValidator.validateForm), + FormTextField(name: 'fullName', validator: FormValidator.validateForm), + FormTextField(name: 'challenge', validator: FormValidator.validateForm), + FormTextField(name: 'occupation', validator: FormValidator.validateForm), + FormTextField(name: 'learningReason', validator: FormValidator.validateForm), + FormTextField(name: 'topic', validator: FormValidator.validateForm), ]) class OnboardingView extends StackedView with $OnboardingView { const OnboardingView({Key? key}) : super(key: key); + void _initFormFields() { + answerController.text = 'Book'; + } + + @override + void onViewModelReady(OnboardingViewModel viewModel) { + _initFormFields(); + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + OnboardingViewModel viewModelBuilder( + BuildContext context, + ) => + OnboardingViewModel(); + @override Widget builder( BuildContext context, @@ -94,68 +106,57 @@ class OnboardingView extends StackedView _buildLanguageSelector() ]; - Widget _buildFirstWelcome() => const FirstWelcome(); + Widget _buildFirstWelcome() => const FirstWelcomeScreen(); - Widget _buildSecondWelcome() => const SecondWelcome(); + Widget _buildSecondWelcome() => const SecondWelcomeScreen(); - Widget _buildThirdWelcome() => const ThirdWelcome(); + Widget _buildThirdWelcome() => const ThirdWelcomeScreen(); Widget _buildFullNameForm() => - FullNameForm(fullNameController: fullNameController); + FullNameFormScreen(fullNameController: fullNameController); - Widget _buildEducationalBackgroundForm() => const EducationalBackgroundForm(); + Widget _buildEducationalBackgroundForm() => + const EducationalBackgroundFormScreen(); - Widget _buildAgeGroupForm() => const AgeGroupForm(); + Widget _buildAgeGroupForm() => const AgeGroupFormScreen(); Widget _buildOccupationForm() => - OccupationForm(occupationController: occupationController); + OccupationFormScreen(occupationController: occupationController); - Widget _buildCountryRegionForm() => const CountryRegionForm(); + Widget _buildCountryRegionForm() => const CountryRegionFormScreen(); - Widget _buildLearningGoalForm() => const LearningGoalForm(); + Widget _buildLearningGoalForm() => const LearningGoalFormScreen(); - Widget _buildLearningReasonForm() => - LearningReasonForm(learningReasonController: learningReasonController); + Widget _buildLearningReasonForm() => LearningReasonFormScreen( + learningReasonController: learningReasonController); Widget _buildChallengeForm() => - ChallengeForm(challengeController: challengeController); + ChallengeFormScreen(challengeController: challengeController); - Widget _buildTopicForm() => TopicForm(topicController: topicController); + Widget _buildTopicForm() => TopicFormScreen(topicController: topicController); - Widget _buildAssessmentIntro() => const AssessmentIntro(); + Widget _buildAssessmentIntro() => const AssessmentIntroScreen(); Widget _buildFirstAssessmentForm() => - FirstAssessmentForm(answerController: answerController); + FirstAssessmentFormScreen(answerController: answerController); - Widget _buildSecondAssessment() => const SecondAssessmentForm(); + Widget _buildSecondAssessment() => const SecondAssessmentFormScreen(); - Widget _buildThirdAssessment() => const ThirdAssessmentForm(); + Widget _buildThirdAssessment() => const ThirdAssessmentFormScreen(); - Widget _buildFourthAssessment() => const FourthAssessmentForm(); + Widget _buildFourthAssessment() => const FourthAssessmentFormScreen(); - Widget _buildAssessmentFailure() => const AssessmentFailure(); + Widget _buildAssessmentFailure() => const AssessmentFailureScreen(); - Widget _buildRetakeAssessment() => const RetakeAssessment(); + Widget _buildRetakeAssessment() => const RetakeAssessmentScreen(); - Widget _buildResultAnalysis() => const ResultAnalysis(); + Widget _buildResultAnalysis() => const ResultAnalysisScreen(); - Widget _buildAssessmentCompletion() => const AssessmentCompletion(); + Widget _buildAssessmentCompletion() => const AssessmentCompletionScreen(); - Widget _buildAssessmentResult() => const AssessmentResult(); + Widget _buildAssessmentResult() => const AssessmentResultScreen(); - Widget _buildStartLesson() => const StartLesson(); + Widget _buildStartLesson() => const StartLessonScreen(); Widget _buildLanguageSelector() => const LanguageSelector(); - - @override - void onViewModelReady(OnboardingViewModel viewModel) { - syncFormWithViewModel(viewModel); - super.onViewModelReady(viewModel); - } - - @override - OnboardingViewModel viewModelBuilder( - BuildContext context, - ) => - OnboardingViewModel(); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.form.dart index c2c4d89..2aed793 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.form.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; -import 'package:yimaru_app/ui/common/validators/onboarding_form_validator.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; const bool _autoTextFieldValidation = true; @@ -25,12 +25,12 @@ final Map _OnboardingViewTextEditingControllers = final Map _OnboardingViewFocusNodes = {}; final Map _OnboardingViewTextValidations = { - AnswerValueKey: OnboardingFormValidator.validateForm, - FullNameValueKey: OnboardingFormValidator.validateForm, - ChallengeValueKey: OnboardingFormValidator.validateForm, - OccupationValueKey: OnboardingFormValidator.validateForm, - LearningReasonValueKey: OnboardingFormValidator.validateForm, - TopicValueKey: OnboardingFormValidator.validateForm, + AnswerValueKey: FormValidator.validateForm, + FullNameValueKey: FormValidator.validateForm, + ChallengeValueKey: FormValidator.validateForm, + OccupationValueKey: FormValidator.validateForm, + LearningReasonValueKey: FormValidator.validateForm, + TopicValueKey: FormValidator.validateForm, }; mixin $OnboardingView { diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_viewmodel.dart index 2cf1fef..e74175d 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_viewmodel.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_viewmodel.dart @@ -2,7 +2,6 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.router.dart'; import '../../../app/app.locator.dart'; -import 'onboarding_view.form.dart'; class OnboardingViewModel extends FormViewModel { final _navigationService = locator(); @@ -26,7 +25,7 @@ class OnboardingViewModel extends FormViewModel { 'Primary school', 'Secondary /High school', 'College / Diploma', - 'Bachelor’s and above' + 'Bachelor’s and above', ]; List get educationalBackgrounds => _educationalBackgrounds; @@ -357,6 +356,10 @@ class OnboardingViewModel extends FormViewModel { bool isSelectedLanguage(String title) => _selectedLanguage['language'] == title; + // Navigation + Future navigateToHome() async => + await _navigationService.navigateToHomeView(); + void next({int? page}) async { if (page == null) { if (_previousPage != 0) { diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart similarity index 91% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart index 1b7641a..5cd813a 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AssessmentCompletion extends ViewModelWidget { - const AssessmentCompletion({super.key}); +class AssessmentCompletionScreen extends ViewModelWidget { + const AssessmentCompletionScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -27,7 +27,7 @@ class AssessmentCompletion extends ViewModelWidget { List _buildScaffoldChildren(OnboardingViewModel viewModel) => [_buildAppBar(), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -73,7 +73,7 @@ class AssessmentCompletion extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -95,7 +95,7 @@ class AssessmentCompletion extends ViewModelWidget { borderRadius: 12, text: 'View My Results', onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart similarity index 85% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart index c2a5fc6..d65cdfb 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AssessmentFailure extends ViewModelWidget { - const AssessmentFailure({super.key}); +class AssessmentFailureScreen extends ViewModelWidget { + const AssessmentFailureScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -25,9 +25,15 @@ class AssessmentFailure extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -69,7 +75,7 @@ class AssessmentFailure extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -87,17 +93,18 @@ class AssessmentFailure extends ViewModelWidget { List _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ _buildContinueButton(viewModel), - verticalSpaceMedium, + verticalSpaceSmall, _buildSkipButtonWrapper(viewModel) ]; Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, + safe: false, borderRadius: 12, text: 'Continue Assessment', onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); @@ -113,7 +120,7 @@ class AssessmentFailure extends ViewModelWidget { borderRadius: 12, borderColor: kcPrimaryColor, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, + backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart similarity index 84% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart index fc376a0..150de8d 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart @@ -4,10 +4,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AssessmentIntro extends ViewModelWidget { - const AssessmentIntro({super.key}); +class AssessmentIntroScreen extends ViewModelWidget { + const AssessmentIntroScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -24,7 +24,7 @@ class AssessmentIntro extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -56,13 +56,19 @@ class AssessmentIntro extends ViewModelWidget { _buildSubTitle(), ]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Want a quick assessment to know your English level?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -79,17 +85,18 @@ class AssessmentIntro extends ViewModelWidget { List _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ _buildContinueButton(viewModel), - verticalSpaceMedium, + verticalSpaceSmall, _buildSkipButtonWrapper(viewModel) ]; Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, + safe: false, text: 'Continue', borderRadius: 12, onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); @@ -104,7 +111,7 @@ class AssessmentIntro extends ViewModelWidget { text: 'Skip', borderRadius: 12, borderColor: kcPrimaryColor, - backgroundColor: kcWhiteColor, + backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result_screen.dart similarity index 86% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result_screen.dart index c7e2186..7b3350b 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AssessmentResult extends ViewModelWidget { - const AssessmentResult({super.key}); +class AssessmentResultScreen extends ViewModelWidget { + const AssessmentResultScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -25,9 +25,15 @@ class AssessmentResult extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -95,17 +101,18 @@ class AssessmentResult extends ViewModelWidget { List _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ _buildContinueButton(viewModel), - verticalSpaceMedium, + verticalSpaceSmall, _buildSkipButtonWrapper(viewModel) ]; Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, + safe: false, text: 'Continue', borderRadius: 12, onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); @@ -121,7 +128,7 @@ class AssessmentResult extends ViewModelWidget { text: 'Practice Speaking', borderColor: kcPrimaryColor, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, + backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart index 5ccb94e..667e921 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart @@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import '../../onboarding_view.form.dart'; -class FirstAssessmentForm extends ViewModelWidget { +class FirstAssessmentFormScreen extends ViewModelWidget { final TextEditingController answerController; - const FirstAssessmentForm({super.key, required this.answerController}); + const FirstAssessmentFormScreen({super.key, required this.answerController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -66,7 +66,7 @@ class FirstAssessmentForm extends ViewModelWidget { _buildFirstAssessmentValidatorWrapper(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -75,7 +75,7 @@ class FirstAssessmentForm extends ViewModelWidget { '1. What is the plural of “book”?', style: TextStyle( fontSize: 16, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -84,7 +84,9 @@ class FirstAssessmentForm extends ViewModelWidget { TextFormField( controller: answerController, onTap: viewModel.setFirstAssessmentFocus, - decoration: inputDecoration(focus: viewModel.focusFirstAssessment), + decoration: inputDecoration( + focus: viewModel.focusFirstAssessment, + filled: answerController.text.isNotEmpty), ); Widget _buildFirstAssessmentValidatorWrapper(OnboardingViewModel viewModel) => @@ -111,13 +113,15 @@ class FirstAssessmentForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, - backgroundColor: - viewModel.focusFirstAssessment && answerController.text.isNotEmpty + foregroundColor: kcWhite, + backgroundColor: answerController.text.isNotEmpty + ? kcPrimaryColor + : viewModel.focusFirstAssessment && answerController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), - onTap: - viewModel.focusFirstAssessment && answerController.text.isNotEmpty + : kcPrimaryColor.withOpacity(0.1), + onTap: answerController.text.isNotEmpty + ? () => viewModel.next() + : viewModel.focusFirstAssessment && answerController.text.isNotEmpty ? () => viewModel.next() : null, ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart similarity index 91% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart index 5fb4f43..2f8d446 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class FourthAssessmentForm extends ViewModelWidget { - const FourthAssessmentForm({super.key}); +class FourthAssessmentFormScreen extends ViewModelWidget { + const FourthAssessmentFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -57,7 +57,7 @@ class FourthAssessmentForm extends ViewModelWidget { _buildAnswers(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -66,7 +66,7 @@ class FourthAssessmentForm extends ViewModelWidget { 'Q4.  Choose the word that best matches the meaning of ‘meticulous’:', style: TextStyle( fontSize: 16, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -104,11 +104,11 @@ class FourthAssessmentForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedA4Answer != null ? () => viewModel.next() : null, backgroundColor: viewModel.selectedA4Answer != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis_screen.dart index 48399e5..a7dafee 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis_screen.dart @@ -3,12 +3,11 @@ import 'package:flutter_svg/svg.dart'; import 'package:stacked/stacked.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; -import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class ResultAnalysis extends ViewModelWidget { - const ResultAnalysis({super.key}); +class ResultAnalysisScreen extends ViewModelWidget { + const ResultAnalysisScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -25,7 +24,7 @@ class ResultAnalysis extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -51,7 +50,11 @@ class ResultAnalysis extends ViewModelWidget { _buildSubTitle(), ]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop(language: false)); Widget _buildIcon() => SvgPicture.asset( 'assets/icons/progress_indicator.svg', @@ -62,7 +65,7 @@ class ResultAnalysis extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart similarity index 85% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart index f960b11..f552473 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart @@ -4,10 +4,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class RetakeAssessment extends ViewModelWidget { - const RetakeAssessment({super.key}); +class RetakeAssessmentScreen extends ViewModelWidget { + const RetakeAssessmentScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -24,7 +24,7 @@ class RetakeAssessment extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -59,7 +59,11 @@ class RetakeAssessment extends ViewModelWidget { _buildSubTitle(), ]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop(language: false)); Widget _buildIcon() => const Icon( Icons.warning_amber_rounded, @@ -72,7 +76,7 @@ class RetakeAssessment extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -90,17 +94,18 @@ class RetakeAssessment extends ViewModelWidget { List _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ _buildContinueButton(viewModel), - verticalSpaceMedium, + verticalSpaceSmall, _buildSkipButtonWrapper(viewModel) ]; Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, + safe: false, borderRadius: 12, text: 'Retake Assessment', onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); @@ -116,7 +121,7 @@ class RetakeAssessment extends ViewModelWidget { borderRadius: 12, borderColor: kcPrimaryColor, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, + backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart similarity index 88% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart index 0869e2c..7f0a973 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class SecondAssessmentForm extends ViewModelWidget { - const SecondAssessmentForm({super.key}); +class SecondAssessmentFormScreen extends ViewModelWidget { + const SecondAssessmentFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -57,7 +57,7 @@ class SecondAssessmentForm extends ViewModelWidget { _buildAnswers(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -66,7 +66,7 @@ class SecondAssessmentForm extends ViewModelWidget { 'Q2. Choose the correct word to complete the sentence:\nI ____ to school yesterday. ', style: TextStyle( fontSize: 16, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -103,12 +103,11 @@ class SecondAssessmentForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, - onTap: viewModel.selectedA2Answer != null - ? () => viewModel.next() - : null, + foregroundColor: kcWhite, + onTap: + viewModel.selectedA2Answer != null ? () => viewModel.next() : null, backgroundColor: viewModel.selectedA2Answer != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson_screen.dart similarity index 89% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson_screen.dart index d8de773..510cac4 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class StartLesson extends ViewModelWidget { - const StartLesson({super.key}); +class StartLessonScreen extends ViewModelWidget { + const StartLessonScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -27,7 +27,7 @@ class StartLesson extends ViewModelWidget { List _buildScaffoldChildren(OnboardingViewModel viewModel) => [_buildAppBar(), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -71,7 +71,7 @@ class StartLesson extends ViewModelWidget { text: 'Welcome aboard', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), children: [ @@ -97,11 +97,12 @@ class StartLesson extends ViewModelWidget { ); Widget _buildContinueButton(OnboardingViewModel viewModel) => - const CustomElevatedButton( + CustomElevatedButton( height: 55, borderRadius: 12, text: 'Go to My Lessons', - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, + onTap: () async => await viewModel.navigateToHome(), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart similarity index 91% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart index 2c7cd26..609cc80 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class ThirdAssessmentForm extends ViewModelWidget { - const ThirdAssessmentForm({super.key}); +class ThirdAssessmentFormScreen extends ViewModelWidget { + const ThirdAssessmentFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -57,7 +57,7 @@ class ThirdAssessmentForm extends ViewModelWidget { _buildAnswers(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -66,7 +66,7 @@ class ThirdAssessmentForm extends ViewModelWidget { 'Q3. Which word means the same as ‘expand’?', style: TextStyle( fontSize: 16, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -103,10 +103,10 @@ class ThirdAssessmentForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: viewModel.selectedA3Answer != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), onTap: viewModel.selectedA3Answer != null ? () => viewModel.next() : null, ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form_screen.dart index b8d0f9c..d6f8c3f 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form_screen.dart @@ -5,17 +5,15 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AgeGroupForm extends ViewModelWidget { - const AgeGroupForm({super.key}); +class AgeGroupFormScreen extends ViewModelWidget { + const AgeGroupFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => _buildScaffoldWrapper(viewModel); - - Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( backgroundColor: kcBackgroundColor, body: _buildScaffold(viewModel), @@ -27,11 +25,15 @@ class AgeGroupForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildColumnScroller(viewModel)); + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyWrapper(viewModel), + ); Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: _buildBody(viewModel), @@ -61,13 +63,20 @@ class AgeGroupForm extends ViewModelWidget { _buildAgeGroups(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Which age range are you in?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -109,10 +118,10 @@ class AgeGroupForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: viewModel.selectedAgeGroup != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), onTap: viewModel.selectedAgeGroup != null ? () => viewModel.next() : null, ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form_screen.dart index 8eee3aa..11870a8 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form_screen.dart @@ -6,12 +6,12 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class ChallengeForm extends ViewModelWidget { +class ChallengeFormScreen extends ViewModelWidget { final TextEditingController challengeController; - const ChallengeForm({super.key, required this.challengeController}); + const ChallengeFormScreen({super.key, required this.challengeController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -28,17 +28,18 @@ class ChallengeForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildBodyScroller(viewModel)); - Widget _buildBodyWrapper(OnboardingViewModel viewModel) => + Widget _buildBodyScroller(OnboardingViewModel viewModel) => SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: _buildBody(viewModel), - ), + child: _buildBodyWrapper(viewModel), + ); + Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), ); Widget _buildBody(OnboardingViewModel viewModel) => Column( @@ -75,13 +76,20 @@ class ChallengeForm extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'What challenge do you face most with English?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -119,7 +127,10 @@ class ChallengeForm extends ViewModelWidget { maxLines: 3, controller: challengeController, onTap: viewModel.setChallengesFocus, - decoration: inputDecoration(focus: true, hint: 'Write your challenge…'), + decoration: inputDecoration( + focus: true, + hint: 'Write your challenge…', + filled: challengeController.text.isNotEmpty), ); Widget _buildChallengeValidatorWrapper(OnboardingViewModel viewModel) => @@ -146,7 +157,7 @@ class ChallengeForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedChallenge != null ? viewModel.selectedChallenge?.toLowerCase() == 'other' ? viewModel.focusChallenge @@ -159,7 +170,7 @@ class ChallengeForm extends ViewModelWidget { ? viewModel.focusChallenge && challengeController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2) + : kcPrimaryColor.withOpacity(0.1) : kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2)); + : kcPrimaryColor.withOpacity(0.1)); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form_screen.dart similarity index 80% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form_screen.dart index 5af3ee4..a3fff2b 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_dropdown.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class CountryRegionForm extends ViewModelWidget { - const CountryRegionForm({super.key}); +class CountryRegionFormScreen extends ViewModelWidget { + const CountryRegionFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -25,7 +25,7 @@ class CountryRegionForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -42,7 +42,12 @@ class CountryRegionForm extends ViewModelWidget { ); List _buildBodyChildren(OnboardingViewModel viewModel) => - [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, @@ -62,13 +67,20 @@ class CountryRegionForm extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Where are you from?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -79,7 +91,7 @@ class CountryRegionForm extends ViewModelWidget { ); Widget _buildCountryDropDown(OnboardingViewModel viewModel) => - CustomDropDownPicker( + CustomDropdownPicker( onChanged: (value) {}, hint: 'Select country', icon: _buildSearchIcon(), @@ -88,7 +100,7 @@ class CountryRegionForm extends ViewModelWidget { ); Widget _buildRegionDropDown(OnboardingViewModel viewModel) => - CustomDropDownPicker( + CustomDropdownPicker( hint: 'Select region', onChanged: (value) {}, icon: _buildSearchIcon(), @@ -112,7 +124,7 @@ class CountryRegionForm extends ViewModelWidget { text: 'Continue', borderRadius: 12, onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form_screen.dart index 963f474..8befcb4 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form_screen.dart @@ -5,16 +5,16 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class EducationalBackgroundForm extends ViewModelWidget { - const EducationalBackgroundForm({super.key}); +class EducationalBackgroundFormScreen + extends ViewModelWidget { + const EducationalBackgroundFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => _buildScaffoldWrapper(viewModel); - Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( backgroundColor: kcBackgroundColor, body: _buildScaffold(viewModel), @@ -26,10 +26,15 @@ class EducationalBackgroundForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildColumnScroller(viewModel)); + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyWrapper(viewModel), + ); Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( padding: const EdgeInsets.symmetric(horizontal: 15), @@ -60,13 +65,20 @@ class EducationalBackgroundForm extends ViewModelWidget { _buildEducationalLevels(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'What’s your current educational level?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -110,12 +122,12 @@ class EducationalBackgroundForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedEducationalBackground != null ? () => viewModel.next() : null, backgroundColor: viewModel.selectedEducationalBackground != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form_screen.dart similarity index 78% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form_screen.dart index d153648..8a897d4 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form_screen.dart @@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import '../../onboarding_view.form.dart'; -class FullNameForm extends ViewModelWidget { +class FullNameFormScreen extends ViewModelWidget { final TextEditingController fullNameController; - const FullNameForm({super.key, required this.fullNameController}); + const FullNameFormScreen({super.key, required this.fullNameController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -28,7 +28,7 @@ class FullNameForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -45,7 +45,12 @@ class FullNameForm extends ViewModelWidget { ); List _buildBodyChildren(OnboardingViewModel viewModel) => - [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, @@ -66,13 +71,19 @@ class FullNameForm extends ViewModelWidget { _buildFullNameValidatorWrapper(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'What should we call you? 😊', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -86,7 +97,10 @@ class FullNameForm extends ViewModelWidget { TextFormField( controller: fullNameController, onTap: viewModel.setFullNameFocus, - decoration: inputDecoration(focus: viewModel.focusFullName), + decoration: inputDecoration( + hint: 'Enter Your Name', + focus: viewModel.focusFullName, + filled: fullNameController.text.isNotEmpty), ); Widget _buildFullNameValidatorWrapper(OnboardingViewModel viewModel) => @@ -113,13 +127,13 @@ class FullNameForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.focusFullName && fullNameController.text.isNotEmpty ? () => viewModel.next() : null, backgroundColor: viewModel.focusFullName && fullNameController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart similarity index 83% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart index 1c3cc9a..3cb70fb 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart @@ -6,10 +6,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_large_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class LearningGoalForm extends ViewModelWidget { - const LearningGoalForm({super.key}); +class LearningGoalFormScreen extends ViewModelWidget { + const LearningGoalFormScreen({super.key}); IconData getIcon(int icon) { switch (icon) { @@ -38,11 +38,15 @@ class LearningGoalForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildBodyScroller(viewModel)); + Widget _buildBodyScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyWrapper(viewModel), + ); Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: _buildBody(viewModel), @@ -70,13 +74,20 @@ class LearningGoalForm extends ViewModelWidget { _buildLearningGoals(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Hi Johnny, Choose your learning goal.', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -120,12 +131,12 @@ class LearningGoalForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedLearningGoal != null ? () => viewModel.next() : null, backgroundColor: viewModel.selectedLearningGoal != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart similarity index 79% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart index 09e9c62..064b05f 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart @@ -6,12 +6,13 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class LearningReasonForm extends ViewModelWidget { +class LearningReasonFormScreen extends ViewModelWidget { final TextEditingController learningReasonController; - const LearningReasonForm({super.key, required this.learningReasonController}); + const LearningReasonFormScreen( + {super.key, required this.learningReasonController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -28,17 +29,19 @@ class LearningReasonForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildBodyScroller(viewModel)); - Widget _buildBodyWrapper(OnboardingViewModel viewModel) => + Widget _buildBodyScroller(OnboardingViewModel viewModel) => SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: _buildBody(viewModel), - ), + child: _buildBodyWrapper(viewModel), + ); + + Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), ); Widget _buildBody(OnboardingViewModel viewModel) => Column( @@ -75,13 +78,18 @@ class LearningReasonForm extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop(language: false)); Widget _buildTitle() => const Text( 'What’s your main goal for improving your English?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -121,7 +129,10 @@ class LearningReasonForm extends ViewModelWidget { maxLines: 3, controller: learningReasonController, onTap: viewModel.setLearningReasonFocus, - decoration: inputDecoration(focus: true, hint: 'Write your goal…'), + decoration: inputDecoration( + focus: true, + hint: 'Write your goal…', + filled: learningReasonController.text.isNotEmpty), ); Widget _buildReasonValidatorWrapper(OnboardingViewModel viewModel) => @@ -148,7 +159,7 @@ class LearningReasonForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedLearningReason != null ? viewModel.selectedLearningReason?.toLowerCase() == 'other' ? viewModel.focusLearningReason @@ -158,9 +169,10 @@ class LearningReasonForm extends ViewModelWidget { : null, backgroundColor: viewModel.selectedLearningReason != null ? viewModel.selectedLearningReason?.toLowerCase() == 'other' - ? viewModel.focusLearningReason && learningReasonController.text.isNotEmpty + ? viewModel.focusLearningReason && + learningReasonController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2) + : kcPrimaryColor.withOpacity(0.1) : kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2)); + : kcPrimaryColor.withOpacity(0.1)); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form_screen.dart similarity index 78% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form_screen.dart index f8f155d..19d08fd 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form_screen.dart @@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import '../../onboarding_view.form.dart'; -class OccupationForm extends ViewModelWidget { +class OccupationFormScreen extends ViewModelWidget { final TextEditingController occupationController; - const OccupationForm({super.key, required this.occupationController}); + const OccupationFormScreen({super.key, required this.occupationController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -28,7 +28,7 @@ class OccupationForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -45,7 +45,12 @@ class OccupationForm extends ViewModelWidget { ); List _buildBodyChildren(OnboardingViewModel viewModel) => - [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, @@ -68,13 +73,20 @@ class OccupationForm extends ViewModelWidget { _buildOccupationValidatorWrapper(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.pop, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'What’s your occupation?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -88,7 +100,10 @@ class OccupationForm extends ViewModelWidget { TextFormField( controller: occupationController, onTap: viewModel.setOccupationFocus, - decoration: inputDecoration(focus: viewModel.focusOccupation), + decoration: inputDecoration( + hint: 'Enter Your Occupation', + focus: viewModel.focusOccupation, + filled: occupationController.text.isNotEmpty), ); Widget _buildOccupationValidatorWrapper(OnboardingViewModel viewModel) => @@ -115,13 +130,13 @@ class OccupationForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.focusOccupation && occupationController.text.isNotEmpty ? () => viewModel.next() : null, backgroundColor: viewModel.focusOccupation && occupationController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form_screen.dart similarity index 80% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form_screen.dart index f80cf4e..c38eaaa 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form_screen.dart @@ -6,18 +6,17 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class TopicForm extends ViewModelWidget { +class TopicFormScreen extends ViewModelWidget { final TextEditingController topicController; - const TopicForm({super.key, required this.topicController}); + const TopicFormScreen({super.key, required this.topicController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => _buildScaffoldWrapper(viewModel); - Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( backgroundColor: kcBackgroundColor, body: _buildScaffold(viewModel), @@ -29,17 +28,19 @@ class TopicForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildBodyScroller(viewModel)); - Widget _buildBodyWrapper(OnboardingViewModel viewModel) => + Widget _buildBodyScroller(OnboardingViewModel viewModel) => SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: _buildBody(viewModel), - ), + child: _buildBodyWrapper(viewModel), + ); + + Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), ); Widget _buildBody(OnboardingViewModel viewModel) => Column( @@ -76,13 +77,20 @@ class TopicForm extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.pop, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Which topics interest you most?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -118,7 +126,10 @@ class TopicForm extends ViewModelWidget { maxLines: 3, controller: topicController, onTap: viewModel.setTopicsFocus, - decoration: inputDecoration(focus: true, hint: 'Write you interest…'), + decoration: inputDecoration( + focus: true, + hint: 'Write you interest…', + filled: topicController.text.isNotEmpty), ); Widget _buildTopicWrapper(OnboardingViewModel viewModel) => @@ -145,7 +156,7 @@ class TopicForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedTopic != null ? viewModel.selectedTopic?.toLowerCase() == 'other' ? viewModel.focusTopic @@ -157,7 +168,7 @@ class TopicForm extends ViewModelWidget { ? viewModel.selectedTopic?.toLowerCase() == 'other' ? viewModel.focusTopic && topicController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2) + : kcPrimaryColor.withOpacity(0.1) : kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2)); + : kcPrimaryColor.withOpacity(0.1)); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/language_selector.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/language_selector.dart index b4450e1..ebc05e6 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/language_selector.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/language_selector.dart @@ -6,7 +6,7 @@ import '../../../common/app_colors.dart'; import '../../../common/ui_helpers.dart'; import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_small_radio_button.dart'; -import '../../../widgets/onboarding_app_bar.dart'; +import '../../../widgets/large_app_bar.dart'; class LanguageSelector extends ViewModelWidget { const LanguageSelector({Key? key}) : super(key: key); @@ -26,7 +26,7 @@ class LanguageSelector extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -43,7 +43,12 @@ class LanguageSelector extends ViewModelWidget { ); List _buildBodyChildren(OnboardingViewModel viewModel) => - [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, @@ -60,13 +65,20 @@ class LanguageSelector extends ViewModelWidget { _buildLanguages(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(language: true); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.pop, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: true, + )); Widget _buildTitle() => const Text( 'Choose your language', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -109,7 +121,7 @@ class LanguageSelector extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, onTap: () => viewModel.pop(language: true), ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome_screen.dart similarity index 86% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome_screen.dart index f99dbf0..fe16c18 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome_screen.dart @@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; -class FirstWelcome extends ViewModelWidget { - const FirstWelcome({super.key}); +class FirstWelcomeScreen extends ViewModelWidget { + const FirstWelcomeScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -55,14 +53,17 @@ class FirstWelcome extends ViewModelWidget { _buildTitle(), ]; - Widget _buildIcon() => SvgPicture.asset('assets/icons/logo.svg'); + Widget _buildIcon() => SvgPicture.asset( + 'assets/icons/logo.svg', + height: 50, + ); Widget _buildTitle() => const Text( 'Small daily practice. Big lifelong results.', textAlign: TextAlign.center, style: TextStyle( fontSize: 30, - color: kcWhiteColor, + color: kcWhite, fontWeight: FontWeight.w600, ), ); @@ -80,11 +81,11 @@ class FirstWelcome extends ViewModelWidget { Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, - icon: true, borderRadius: 12, text: 'Start Learning', + backgroundColor: kcWhite, + trailingIcon: Icons.arrow_forward, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome_screen.dart similarity index 88% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome_screen.dart index 2b49b96..487866d 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome_screen.dart @@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; -class SecondWelcome extends ViewModelWidget { - const SecondWelcome({super.key}); +class SecondWelcomeScreen extends ViewModelWidget { + const SecondWelcomeScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -57,6 +55,7 @@ class SecondWelcome extends ViewModelWidget { Widget _buildIcon() => SvgPicture.asset( 'assets/icons/logo.svg', + height: 50, ); Widget _buildTitle() => const Text( @@ -64,7 +63,7 @@ class SecondWelcome extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 30, - color: kcWhiteColor, + color: kcWhite, fontWeight: FontWeight.w600, ), ); @@ -82,11 +81,11 @@ class SecondWelcome extends ViewModelWidget { Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, - icon: true, borderRadius: 12, text: 'Start Learning', + backgroundColor: kcWhite, + trailingIcon: Icons.arrow_forward, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome_screen.dart similarity index 86% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome_screen.dart index 7702dda..cb590ac 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome_screen.dart @@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; -class ThirdWelcome extends ViewModelWidget { - const ThirdWelcome({super.key}); +class ThirdWelcomeScreen extends ViewModelWidget { + const ThirdWelcomeScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -54,15 +52,17 @@ class ThirdWelcome extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), ]; - - Widget _buildIcon() => SvgPicture.asset('assets/icons/logo.svg'); + Widget _buildIcon() => SvgPicture.asset( + 'assets/icons/logo.svg', + height: 50, + ); Widget _buildTitle() => const Text( 'Every conversation brings you closer to the life you want.', textAlign: TextAlign.center, style: TextStyle( fontSize: 30, - color: kcWhiteColor, + color: kcWhite, fontWeight: FontWeight.w600, ), ); @@ -80,11 +80,11 @@ class ThirdWelcome extends ViewModelWidget { Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, - icon: true, borderRadius: 12, text: 'Start Learning', + backgroundColor: kcWhite, + trailingIcon: Icons.arrow_forward, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_view.dart b/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_view.dart new file mode 100644 index 0000000..4cd3fc3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_view.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/course_progress_section.dart'; +import 'package:yimaru_app/ui/widgets/learning_progress_card.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'ongoing_progress_viewmodel.dart'; + +class OngoingProgressView extends StackedView { + const OngoingProgressView({Key? key}) : super(key: key); + + @override + OngoingProgressViewModel viewModelBuilder( + BuildContext context, + ) => + OngoingProgressViewModel(); + + @override + Widget builder( + BuildContext context, + OngoingProgressViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(OngoingProgressViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(OngoingProgressViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(OngoingProgressViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(OngoingProgressViewModel viewModel) => + _buildNestedScrollView(viewModel); + + Widget _buildNestedScrollView(OngoingProgressViewModel viewModel) => + NestedScrollView( + scrollDirection: Axis.vertical, + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) => + [_buildSliverAppbarWrapper(viewModel)], + body: _buildContentScrollViewWrapper(viewModel)); + + Widget _buildSliverAppbarWrapper(OngoingProgressViewModel viewModel) => + SliverAppBar( + pinned: true, + automaticallyImplyLeading: false, + backgroundColor: kcBackgroundColor, + surfaceTintColor: kcBackgroundColor, + title: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(OngoingProgressViewModel viewModel) => SmallAppBar( + title: 'My Progress', + onTap: viewModel.pop, + ); + + Widget _buildContentScrollViewWrapper(OngoingProgressViewModel viewModel) => + SingleChildScrollView( + child: _buildContentWrapper(viewModel), + ); + + Widget _buildContentWrapper(OngoingProgressViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildContentColumn(viewModel), + ); + + Widget _buildContentColumn(OngoingProgressViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentChildren(viewModel), + ); + + List _buildContentChildren(OngoingProgressViewModel viewModel) => [ + verticalSpaceMedium, + _buildText(), + verticalSpaceMedium, + _buildLearningProgressCard(), + verticalSpaceMedium, + _buildCourseProgressSection() + ]; + + Widget _buildText() => const Text( + 'Track your learning journey and see your growth over time.', + style: TextStyle(color: kcDarkGrey), + ); + + Widget _buildLearningProgressCard() => const LearningProgressCard(); + + Widget _buildCourseProgressSection() => const CourseProgressSection(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart new file mode 100644 index 0000000..0c3c308 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart @@ -0,0 +1,21 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class OngoingProgressViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _courses = [ + { + 'title': 'IELTS Preparation', + }, + { + 'title': 'Duolingo English Test', + }, + ]; + + List> get courses => _courses; + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_view.dart b/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_view.dart new file mode 100644 index 0000000..f4a04f5 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_view.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/privacy_policy_tile.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'privacy_policy_viewmodel.dart'; + +class PrivacyPolicyView extends StackedView { + const PrivacyPolicyView({Key? key}) : super(key: key); + + @override + PrivacyPolicyViewModel viewModelBuilder(BuildContext context) => + PrivacyPolicyViewModel(); + + @override + Widget builder( + BuildContext context, + PrivacyPolicyViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(PrivacyPolicyViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(PrivacyPolicyViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(PrivacyPolicyViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(PrivacyPolicyViewModel viewModel) => + _buildColumn(viewModel); + + Widget _buildColumn(PrivacyPolicyViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(PrivacyPolicyViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(PrivacyPolicyViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar( + title: 'Privacy Policy', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) => + Expanded(child: _buildContentColumnWrapper(viewModel)); + + Widget _buildContentColumnWrapper(PrivacyPolicyViewModel viewModel) => + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildMenuColumnScrollView(viewModel), + ); + + Widget _buildMenuColumnScrollView(PrivacyPolicyViewModel viewModel) => + SingleChildScrollView( + child: _buildMenuColumn(viewModel), + ); + + Widget _buildMenuColumn(PrivacyPolicyViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildMenuColumnChildren(viewModel), + ); + + List _buildMenuColumnChildren(PrivacyPolicyViewModel viewModel) => + [verticalSpaceLarge, _buildListView(viewModel)]; + + Widget _buildListView(PrivacyPolicyViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.privacyPolicies.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => + _buildTile(viewModel.privacyPolicies[index]['title']), + ); + + Widget _buildTile(String title) => PrivacyPolicyTile(title: title); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_viewmodel.dart new file mode 100644 index 0000000..11c56a6 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_viewmodel.dart @@ -0,0 +1,23 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class PrivacyPolicyViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Privacy policy + final List> _privacyPolicies = [ + {'title': 'Introduction'}, + {'title': 'Information We Collect'}, + {'title': 'How We Use Your Information'}, + {'title': 'Data Sharing and Disclosure'}, + {'title': 'Your Rights and Choices'}, + {'title': 'Data Security'} + ]; + + List> get privacyPolicies => _privacyPolicies; + + // Navigation + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile/profile_view.dart b/StudioProjects/yimaru_app/lib/ui/views/profile/profile_view.dart new file mode 100644 index 0000000..fc79f97 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile/profile_view.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/widgets/profile_card.dart'; +import 'package:yimaru_app/ui/widgets/profile_image.dart'; +import 'package:yimaru_app/ui/widgets/view_profile_button.dart'; + +import '../../widgets/custom_elevated_button.dart'; +import 'profile_viewmodel.dart'; + +class ProfileView extends StackedView { + const ProfileView({Key? key}) : super(key: key); + + @override + ProfileViewModel viewModelBuilder( + BuildContext context, + ) => + ProfileViewModel(); + + @override + Widget builder( + BuildContext context, + ProfileViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(ProfileViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView( + child: _buildBody(viewModel), + ); + + Widget _buildBody(ProfileViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(ProfileViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildNotificationIconWrapper(), + _buildProfileSection(), + verticalSpaceSmall, + _buildViewProfileButton(viewModel), + verticalSpaceLarge, + _buildSettingsSection(viewModel), + verticalSpaceLarge, + _buildLogOutButton(viewModel), + verticalSpaceLarge, + ], + ); + + Widget _buildNotificationIconWrapper() => + Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon()); + + Widget _buildNotificationIcon() => const Icon( + Icons.notifications_none, + color: kcDarkGrey, + ); + + Widget _buildProfileSection() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildProfileSectionChildren(), + ); + + List _buildProfileSectionChildren() => [ + _buildProfileImage(), + verticalSpaceSmall, + _buildProfileName(), + ]; + + Widget _buildProfileImage() => const ProfileImage(); + + Widget _buildProfileName() => const Text( + 'Hi, Bisrat 👋', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildViewProfileButton(ProfileViewModel viewModel) => + ViewProfileButton( + onTap: () async => await viewModel.navigateToProfileDetail(), + ); + + Widget _buildSettingsSection(ProfileViewModel viewModel) => GridView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, mainAxisSpacing: 15, crossAxisSpacing: 15), + children: _buildSettingsChildren(viewModel)); + + List _buildSettingsChildren(ProfileViewModel viewModel) => [ + _buildDownloadsCard(viewModel), + _buildProgressCard(viewModel), + _buildAccountCard(viewModel), + _buildSupportCard(viewModel) + ]; + + Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard( + icon: Icons.download, + title: 'My Downloads', + subTitle: 'Access offline lessons and saved videos', + onTap: () async => await viewModel.navigateToDownloads(), + ); + + Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard( + title: 'My Progress', + icon: Icons.stacked_bar_chart, + subTitle: 'Track your achievements and learning streak', + onTap: () async => await viewModel.navigateToProgress(), + ); + + Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard( + title: 'Account & Privacy', + icon: Icons.privacy_tip_outlined, + subTitle: 'Manage setting and app preference', + onTap: () async => await viewModel.navigateToAccountPrivacy(), + ); + + Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard( + title: 'Support', + icon: Icons.headphones, + subTitle: 'Get help through phone or Telegram', + onTap: () async => await viewModel.navigateToSupport(), + ); + + Widget _buildLogOutButton(ProfileViewModel viewModel) => CustomElevatedButton( + height: 55, + text: 'Log Out', + borderRadius: 12, + foregroundColor: kcRed, + onTap: () async => await viewModel.logOut(), + backgroundColor: kcRed.withOpacity(0.25), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile/profile_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/profile/profile_viewmodel.dart new file mode 100644 index 0000000..ec35e68 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile/profile_viewmodel.dart @@ -0,0 +1,32 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; + +import '../../../app/app.locator.dart'; +import '../../../services/authentication_service.dart'; + +class ProfileViewModel extends BaseViewModel { + final _navigationService = locator(); + + final _authenticationService = locator(); + + Future logOut() async { + await _authenticationService.logOut(); + await _navigationService.replaceWithLoginView(); + } + + Future navigateToProfileDetail() async => + await _navigationService.navigateToProfileDetailView(); + + Future navigateToDownloads() async => + await _navigationService.navigateToDownloadsView(); + + Future navigateToProgress() async => + await _navigationService.navigateToProgressView(); + + Future navigateToAccountPrivacy() async => + await _navigationService.navigateToAccountPrivacyView(); + + Future navigateToSupport() async => + await _navigationService.navigateToSupportView(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.dart b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.dart new file mode 100644 index 0000000..d7b64a7 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.dart @@ -0,0 +1,569 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/widgets/birthday_selector.dart'; +import 'package:yimaru_app/ui/widgets/custom_form_label.dart'; +import 'package:yimaru_app/ui/widgets/small_app_bar.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../common/validators/form_validator.dart'; +import '../../widgets/custom_dropdown.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/profile_image.dart'; +import 'profile_detail_viewmodel.dart'; + +import 'profile_detail_view.form.dart'; + +@FormView(fields: [ + FormTextField(name: 'email', validator: FormValidator.validateForm), + FormTextField( + name: 'phoneNumber', validator: FormValidator.validatePhoneNumber), + FormTextField(name: 'lastName', validator: FormValidator.validateForm), + FormTextField(name: 'firstName', validator: FormValidator.validateForm), +]) +class ProfileDetailView extends StackedView + with $ProfileDetailView { + const ProfileDetailView({Key? key}) : super(key: key); + + void _onModelReady() { + firstNameController.text = 'Abel'; + lastNameController.text = 'Abebe'; + phoneNumberController.text = '251900000000'; + emailController.text = 'email@test.com'; + } + + @override + void onViewModelReady(ProfileDetailViewModel viewModel) { + _onModelReady(); + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + ProfileDetailViewModel viewModelBuilder(BuildContext context) => + ProfileDetailViewModel(); + + @override + Widget builder( + BuildContext context, + ProfileDetailViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(ProfileDetailViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(ProfileDetailViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(ProfileDetailViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), + ); + + Widget _buildBody(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(ProfileDetailViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppbar(viewModel), + verticalSpaceSmall, + _buildColumnWrapper(viewModel) + ]; + + Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar( + title: 'Edit Profile', + onTap: viewModel.pop, + ); + + Widget _buildColumnWrapper(ProfileDetailViewModel viewModel) => + Expanded(child: _buildBodyColumn(viewModel)); + + Widget _buildBodyColumn(ProfileDetailViewModel viewModel) => + SingleChildScrollView( + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(ProfileDetailViewModel viewModel) => [ + verticalSpaceMedium, + _buildProfileImage(), + verticalSpaceMedium, + _buildNameFormSection(viewModel), + verticalSpaceMedium, + _buildGenderFormFieldWrapper(viewModel), + verticalSpaceSmall, + _buildBirthdayColumn(viewModel), + verticalSpaceSmall, + _buildPhoneNumberFormFieldSection(viewModel), + verticalSpaceTiny, + _buildEmailFormFieldSection(viewModel), + verticalSpaceMedium, + _buildCountryRegionSection(viewModel), + verticalSpaceMedium, + _buildOccupationDropdownWrapper(viewModel), + verticalSpaceLarge, + _buildLowerColumn(viewModel) + ]; + + Widget _buildProfileImage() => + const Align(alignment: Alignment.center, child: ProfileImage()); + + Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildNameFormChildren(viewModel), + ); + + List _buildNameFormChildren(ProfileDetailViewModel viewModel) => [ + _buildFirstNameFormFieldWrapper(viewModel), + const SizedBox(width: 20), + _buildLastNameFormFieldWrapper(viewModel) + ]; + + Widget _buildFirstNameFormFieldWrapper(ProfileDetailViewModel viewModel) => + Expanded(child: _buildFirstNameFormFieldColumn(viewModel)); + + Widget _buildFirstNameFormFieldColumn(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: _buildFirstNameFormFieldChildren(viewModel), + ); + + List _buildFirstNameFormFieldChildren( + ProfileDetailViewModel viewModel) => + [ + _buildFirstNameLabel(), + verticalSpaceSmall, + _buildFirstNameFormField(viewModel), + if (viewModel.hasFirstNameValidationMessage && viewModel.focusFirstName) + verticalSpaceTiny, + if (viewModel.hasFirstNameValidationMessage && viewModel.focusFirstName) + _buildFirstNameValidatorWrapper(viewModel) + ]; + + Widget _buildFirstNameLabel() => CustomFormLabel( + label: 'First Name', + style: style16DG600, + ); + + Widget _buildFirstNameFormField(ProfileDetailViewModel viewModel) => + TextFormField( + controller: firstNameController, + onTap: viewModel.setFirstNameFocus, + decoration: inputDecoration( + focus: viewModel.focusFirstName, + filled: firstNameController.text.isNotEmpty), + ); + + Widget _buildFirstNameValidatorWrapper(ProfileDetailViewModel viewModel) => + viewModel.hasFirstNameValidationMessage + ? _buildFirstNameValidator(viewModel) + : Container(); + + Widget _buildFirstNameValidator(ProfileDetailViewModel viewModel) => Text( + viewModel.firstNameValidationMessage!, + style: validationStyle, + ); + + Widget _buildLastNameFormFieldWrapper(ProfileDetailViewModel viewModel) => + Expanded(child: _buildLastNameFormFieldColumn(viewModel)); + + Widget _buildLastNameFormFieldColumn(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildLastNameFormFieldChildren(viewModel), + ); + + List _buildLastNameFormFieldChildren( + ProfileDetailViewModel viewModel) => + [ + _buildLastNameLabel(), + verticalSpaceSmall, + _buildLastNameFormField(viewModel), + if (viewModel.hasLastNameValidationMessage && viewModel.focusLastName) + verticalSpaceTiny, + if (viewModel.hasLastNameValidationMessage && viewModel.focusLastName) + _buildLastNameValidatorWrapper(viewModel) + ]; + + Widget _buildLastNameLabel() => CustomFormLabel( + label: 'Last Name', + style: style16DG600, + ); + + Widget _buildLastNameFormField(ProfileDetailViewModel viewModel) => + TextFormField( + controller: lastNameController, + onTap: viewModel.setLastNameFocus, + decoration: inputDecoration( + focus: viewModel.focusLastName, + filled: lastNameController.text.isNotEmpty), + ); + + Widget _buildLastNameValidatorWrapper(ProfileDetailViewModel viewModel) => + viewModel.hasLastNameValidationMessage + ? _buildLastNameValidator(viewModel) + : Container(); + + Widget _buildLastNameValidator(ProfileDetailViewModel viewModel) => Text( + viewModel.lastNameValidationMessage!, + style: validationStyle, + ); + + Widget _buildGenderFormFieldWrapper(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildGenderFormFieldChildren(viewModel), + ); + + List _buildGenderFormFieldChildren( + ProfileDetailViewModel viewModel) => + [ + _buildGenderLabel(), + verticalSpaceTiny, + _buildRadioButtonWrapper(viewModel), + ]; + + Widget _buildGenderLabel() => CustomFormLabel( + label: 'Gender', + style: style16DG600, + ); + + Widget _buildRadioButtonWrapper(ProfileDetailViewModel viewModel) => Row( + mainAxisSize: MainAxisSize.min, + children: _buildRadioButtonChildren(viewModel), + ); + + List _buildRadioButtonChildren(ProfileDetailViewModel viewModel) => + [_buildMaleRadioButton(viewModel), _buildFemaleRadioButton(viewModel)]; + + Widget _buildMaleRadioButton(ProfileDetailViewModel viewModel) => + RadioGroup( + groupValue: viewModel.selectedGender, + onChanged: (value) => viewModel.setGender(value ?? ''), + child: _buildMaleRadioTileWrapper(viewModel)); + + Widget _buildMaleRadioTileWrapper(ProfileDetailViewModel viewModel) => + Container( + width: 125, + alignment: Alignment.centerLeft, + child: _buildMaleRadioTile(viewModel)); + + Widget _buildMaleRadioTile(ProfileDetailViewModel viewModel) => + RadioListTile( + value: 'Male', + title: _buildMaleTitle(), + activeColor: kcPrimaryColor, + contentPadding: EdgeInsets.zero, + ); + + Widget _buildMaleTitle() => const Text( + 'Male', + style: TextStyle( + fontSize: 14, + color: kcDarkGrey, + fontWeight: FontWeight.w500, + ), + ); + + Widget _buildFemaleRadioButton(ProfileDetailViewModel viewModel) => + RadioGroup( + groupValue: viewModel.selectedGender, + onChanged: (value) => viewModel.setGender(value ?? ''), + child: _buildFemaleRadioTileWrapper(viewModel)); + + Widget _buildFemaleRadioTileWrapper(ProfileDetailViewModel viewModel) => + Container( + width: 125, + alignment: Alignment.centerLeft, + child: _buildFemaleRadioTile(viewModel), + ); + + Widget _buildFemaleRadioTile(ProfileDetailViewModel viewModel) => + RadioListTile( + value: 'Female', + title: _buildFemaleTitle(), + activeColor: kcPrimaryColor, + contentPadding: EdgeInsets.zero, + ); + + Widget _buildFemaleTitle() => const Text( + 'Female', + style: TextStyle( + fontSize: 14, + color: kcDarkGrey, + fontWeight: FontWeight.w500, + ), + ); + + Widget _buildBirthdayColumn(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBirthdayChildren(viewModel), + ); + + List _buildBirthdayChildren(ProfileDetailViewModel viewModel) => [ + _buildBirthdayLabel(), + verticalSpaceSmall, + _buildBirthdayFormField(), + ]; + + Widget _buildBirthdayLabel() => CustomFormLabel( + label: 'Birthday', + style: style16DG600, + ); + + Widget _buildBirthdayFormField() => const BirthdaySelector(); + + Widget _buildPhoneNumberFormFieldSection(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildPhoneNumberFormFieldChildren(viewModel), + ); + + List _buildPhoneNumberFormFieldChildren( + ProfileDetailViewModel viewModel) => + [ + _buildPhoneNumberLabel(), + verticalSpaceSmall, + _buildPhoneNumberFormField(viewModel), + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + verticalSpaceTiny, + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + _buildPhoneNumberValidatorWrapper(viewModel) + ]; + + Widget _buildPhoneNumberLabel() => CustomFormLabel( + label: 'Phone Number', + style: style16DG600, + ); + + Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) => + TextFormField( + maxLength: 12, + keyboardType: TextInputType.phone, + controller: phoneNumberController, + onTap: viewModel.setPhoneNumberFocus, + decoration: inputDecoration( + hint: '251', + focus: viewModel.focusPhoneNumber, + filled: phoneNumberController.text.isNotEmpty), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ); + + Widget _buildPhoneNumberValidatorWrapper(ProfileDetailViewModel viewModel) => + viewModel.hasPhoneNumberValidationMessage + ? _buildPhoneNumberValidator(viewModel) + : Container(); + + Widget _buildPhoneNumberValidator(ProfileDetailViewModel viewModel) => Text( + viewModel.phoneNumberValidationMessage!, + style: validationStyle, + ); + + Widget _buildEmailFormFieldSection(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildEmailFormFieldChildren(viewModel), + ); + + List _buildEmailFormFieldChildren(ProfileDetailViewModel viewModel) => + [ + _buildEmailLabel(), + verticalSpaceSmall, + _buildEmailFormField(viewModel), + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + verticalSpaceTiny, + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + _buildEmailValidatorWrapper(viewModel) + ]; + + Widget _buildEmailLabel() => CustomFormLabel( + label: 'Email', + style: style16DG600, + ); + + Widget _buildEmailFormField(ProfileDetailViewModel viewModel) => + TextFormField( + controller: emailController, + onTap: viewModel.setPhoneNumberFocus, + keyboardType: TextInputType.emailAddress, + decoration: inputDecoration( + focus: viewModel.focusEmail, + filled: emailController.text.isNotEmpty), + ); + + Widget _buildEmailValidatorWrapper(ProfileDetailViewModel viewModel) => + viewModel.hasEmailValidationMessage + ? _buildEmailValidator(viewModel) + : Container(); + + Widget _buildEmailValidator(ProfileDetailViewModel viewModel) => Text( + viewModel.emailValidationMessage!, + style: validationStyle, + ); + + Widget _buildCountryRegionSection(ProfileDetailViewModel viewModel) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildCountryRegionChildren(viewModel), + ); + + List _buildCountryRegionChildren(ProfileDetailViewModel viewModel) => + [ + _buildCountryDropdownColumnWrapper(viewModel), + const SizedBox(width: 20), + _buildRegionDropdownColumnWrapper(viewModel) + ]; + + Widget _buildCountryDropdownColumnWrapper(ProfileDetailViewModel viewModel) => + Expanded( + child: _buildCountryDropdownColumn(viewModel), + ); + + Widget _buildCountryDropdownColumn(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: _buildCountryDropdownChildren(viewModel), + ); + + List _buildCountryDropdownChildren( + ProfileDetailViewModel viewModel) => + [ + _buildCountryDropdownLabel(), + verticalSpaceSmall, + _buildCountryDropdown(viewModel) + ]; + + Widget _buildCountryDropdownLabel() => CustomFormLabel( + label: 'Country', + style: style16DG600, + ); + + Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) => + CustomDropdownPicker( + onChanged: (value) {}, + hint: 'Select country', + selectedItem: 'Ethiopia', + items: (value, props) => viewModel.getCountries(), + ); + + Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) => + Expanded( + child: _buildRegionDropdownColumn(viewModel), + ); + + Widget _buildRegionDropdownColumn(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: _buildRegionDropdownChildren(viewModel), + ); + + List _buildRegionDropdownChildren(ProfileDetailViewModel viewModel) => + [ + _buildRegionDropdownLabel(), + verticalSpaceSmall, + _buildRegionDropdown(viewModel) + ]; + + Widget _buildRegionDropdownLabel() => CustomFormLabel( + label: 'Region', + style: style16DG600, + ); + + Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) => + CustomDropdownPicker( + hint: 'Select region', + onChanged: (value) {}, + selectedItem: 'Addis Ababa', + items: (value, props) => viewModel.getRegions('Addis Ababa'), + ); + + Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: _buildOccupationDropdownChildren(viewModel), + ); + + List _buildOccupationDropdownChildren( + ProfileDetailViewModel viewModel) => + [ + _buildOccupationDropdownLabel(), + verticalSpaceSmall, + _buildOccupationDropdown(viewModel) + ]; + + Widget _buildOccupationDropdownLabel() => CustomFormLabel( + label: 'Occupation', + style: style16DG600, + ); + + Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) => + CustomDropdownPicker( + hint: 'Select occupation', + onChanged: (value) {}, + selectedItem: 'Student', + items: (value, props) => viewModel.getOccupations('Student'), + ); + + Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [ + _buildSaveButton(viewModel), + verticalSpaceSmall, + _buildCancelButtonWrapper(viewModel) + ]; + + Widget _buildSaveButton(ProfileDetailViewModel viewModel) => + const CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Save Changes', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); + + Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildCancelButton(viewModel), + ); + + Widget _buildCancelButton(ProfileDetailViewModel viewModel) => + const CustomElevatedButton( + height: 55, + text: 'Cancel', + borderRadius: 12, + borderColor: kcPrimaryColor, + backgroundColor: kcWhite, + foregroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.form.dart b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.form.dart new file mode 100644 index 0000000..d5e8df2 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.form.dart @@ -0,0 +1,278 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// StackedFormGenerator +// ************************************************************************** + +// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; + +const bool _autoTextFieldValidation = true; + +const String EmailValueKey = 'email'; +const String PhoneNumberValueKey = 'phoneNumber'; +const String LastNameValueKey = 'lastName'; +const String FirstNameValueKey = 'firstName'; + +final Map + _ProfileDetailViewTextEditingControllers = {}; + +final Map _ProfileDetailViewFocusNodes = {}; + +final Map + _ProfileDetailViewTextValidations = { + EmailValueKey: FormValidator.validateForm, + PhoneNumberValueKey: FormValidator.validatePhoneNumber, + LastNameValueKey: FormValidator.validateForm, + FirstNameValueKey: FormValidator.validateForm, +}; + +mixin $ProfileDetailView { + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); + TextEditingController get phoneNumberController => + _getFormTextEditingController(PhoneNumberValueKey); + TextEditingController get lastNameController => + _getFormTextEditingController(LastNameValueKey); + TextEditingController get firstNameController => + _getFormTextEditingController(FirstNameValueKey); + + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); + FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey); + FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey); + FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey); + + TextEditingController _getFormTextEditingController( + String key, { + String? initialValue, + }) { + if (_ProfileDetailViewTextEditingControllers.containsKey(key)) { + return _ProfileDetailViewTextEditingControllers[key]!; + } + + _ProfileDetailViewTextEditingControllers[key] = + TextEditingController(text: initialValue); + return _ProfileDetailViewTextEditingControllers[key]!; + } + + FocusNode _getFormFocusNode(String key) { + if (_ProfileDetailViewFocusNodes.containsKey(key)) { + return _ProfileDetailViewFocusNodes[key]!; + } + _ProfileDetailViewFocusNodes[key] = FocusNode(); + return _ProfileDetailViewFocusNodes[key]!; + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + void syncFormWithViewModel(FormStateHelper model) { + emailController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + lastNameController.addListener(() => _updateFormData(model)); + firstNameController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + @Deprecated( + 'Use syncFormWithViewModel instead.' + 'This feature was deprecated after 3.1.0.', + ) + void listenToFormUpdated(FormViewModel model) { + emailController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + lastNameController.addListener(() => _updateFormData(model)); + firstNameController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Updates the formData on the FormViewModel + void _updateFormData(FormStateHelper model, {bool forceValidate = false}) { + model.setData( + model.formValueMap + ..addAll({ + EmailValueKey: emailController.text, + PhoneNumberValueKey: phoneNumberController.text, + LastNameValueKey: lastNameController.text, + FirstNameValueKey: firstNameController.text, + }), + ); + + if (_autoTextFieldValidation || forceValidate) { + updateValidationData(model); + } + } + + bool validateFormFields(FormViewModel model) { + _updateFormData(model, forceValidate: true); + return model.isFormValid; + } + + /// Calls dispose on all the generated controllers and focus nodes + void disposeForm() { + // The dispose function for a TextEditingController sets all listeners to null + + for (var controller in _ProfileDetailViewTextEditingControllers.values) { + controller.dispose(); + } + for (var focusNode in _ProfileDetailViewFocusNodes.values) { + focusNode.dispose(); + } + + _ProfileDetailViewTextEditingControllers.clear(); + _ProfileDetailViewFocusNodes.clear(); + } +} + +extension ValueProperties on FormStateHelper { + bool get hasAnyValidationMessage => this + .fieldsValidationMessages + .values + .any((validation) => validation != null); + + bool get isFormValid { + if (!_autoTextFieldValidation) this.validateForm(); + + return !hasAnyValidationMessage; + } + + String? get emailValue => this.formValueMap[EmailValueKey] as String?; + String? get phoneNumberValue => + this.formValueMap[PhoneNumberValueKey] as String?; + String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?; + String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?; + + set emailValue(String? value) { + this.setData( + this.formValueMap..addAll({EmailValueKey: value}), + ); + + if (_ProfileDetailViewTextEditingControllers.containsKey(EmailValueKey)) { + _ProfileDetailViewTextEditingControllers[EmailValueKey]?.text = + value ?? ''; + } + } + + set phoneNumberValue(String? value) { + this.setData( + this.formValueMap..addAll({PhoneNumberValueKey: value}), + ); + + if (_ProfileDetailViewTextEditingControllers.containsKey( + PhoneNumberValueKey)) { + _ProfileDetailViewTextEditingControllers[PhoneNumberValueKey]?.text = + value ?? ''; + } + } + + set lastNameValue(String? value) { + this.setData( + this.formValueMap..addAll({LastNameValueKey: value}), + ); + + if (_ProfileDetailViewTextEditingControllers.containsKey( + LastNameValueKey)) { + _ProfileDetailViewTextEditingControllers[LastNameValueKey]?.text = + value ?? ''; + } + } + + set firstNameValue(String? value) { + this.setData( + this.formValueMap..addAll({FirstNameValueKey: value}), + ); + + if (_ProfileDetailViewTextEditingControllers.containsKey( + FirstNameValueKey)) { + _ProfileDetailViewTextEditingControllers[FirstNameValueKey]?.text = + value ?? ''; + } + } + + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); + bool get hasPhoneNumber => + this.formValueMap.containsKey(PhoneNumberValueKey) && + (phoneNumberValue?.isNotEmpty ?? false); + bool get hasLastName => + this.formValueMap.containsKey(LastNameValueKey) && + (lastNameValue?.isNotEmpty ?? false); + bool get hasFirstName => + this.formValueMap.containsKey(FirstNameValueKey) && + (firstNameValue?.isNotEmpty ?? false); + + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; + bool get hasPhoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false; + bool get hasLastNameValidationMessage => + this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false; + bool get hasFirstNameValidationMessage => + this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false; + + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; + String? get phoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]; + String? get lastNameValidationMessage => + this.fieldsValidationMessages[LastNameValueKey]; + String? get firstNameValidationMessage => + this.fieldsValidationMessages[FirstNameValueKey]; +} + +extension Methods on FormStateHelper { + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; + setPhoneNumberValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage; + setLastNameValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[LastNameValueKey] = validationMessage; + setFirstNameValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[FirstNameValueKey] = validationMessage; + + /// Clears text input fields on the Form + void clearForm() { + emailValue = ''; + phoneNumberValue = ''; + lastNameValue = ''; + firstNameValue = ''; + } + + /// Validates text input fields on the Form + void validateForm() { + this.setValidationMessages({ + EmailValueKey: getValidationMessage(EmailValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + LastNameValueKey: getValidationMessage(LastNameValueKey), + FirstNameValueKey: getValidationMessage(FirstNameValueKey), + }); + } +} + +/// Returns the validation message for the given key +String? getValidationMessage(String key) { + final validatorForKey = _ProfileDetailViewTextValidations[key]; + if (validatorForKey == null) return null; + + String? validationMessageForKey = validatorForKey( + _ProfileDetailViewTextEditingControllers[key]!.text, + ); + + return validationMessageForKey; +} + +/// Updates the fieldsValidationMessages on the FormViewModel +void updateValidationData(FormStateHelper model) => + model.setValidationMessages({ + EmailValueKey: getValidationMessage(EmailValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + LastNameValueKey: getValidationMessage(LastNameValueKey), + FirstNameValueKey: getValidationMessage(FirstNameValueKey), + }); diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_viewmodel.dart new file mode 100644 index 0000000..16769e6 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_viewmodel.dart @@ -0,0 +1,86 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class ProfileDetailViewModel extends FormViewModel { + final _navigationService = locator(); + // First name + bool _focusFirstName = false; + + bool get focusFirstName => _focusFirstName; + + // Last name + bool _focusLastName = false; + + bool get focusLastName => _focusLastName; + + // Gender + String? _selectedGender; + + String? get selectedGender => _selectedGender; + + // Birthday + String? _selectedBirthday; + + String? get selectedBirthday => _selectedBirthday; + + // First name + bool _focusPhoneNumber = false; + + bool get focusPhoneNumber => _focusPhoneNumber; + + // Email + bool _focusEmail = false; + + bool get focusEmail => _focusEmail; + + // First name + void setFirstNameFocus() { + _focusFirstName = true; + rebuildUi(); + } + + // Last name + void setLastNameFocus() { + _focusLastName = true; + rebuildUi(); + } + + // Gender + void setGender(String value) { + _selectedGender = value; + rebuildUi(); + } + + // Birthday + void setBirthday(String value) { + _selectedBirthday = value; + rebuildUi(); + } + + // Phone number + void setPhoneNumberFocus() { + _focusPhoneNumber = true; + rebuildUi(); + } + + // Email + void setEmailFocus() { + _focusEmail = true; + rebuildUi(); + } + + // Country + Future> getCountries() async => ['Ethiopia', 'Djibouti']; + + // Region + Future> getRegions(String country) async => + ['Addis Ababa', 'Oromia']; + + // Occupation + Future> getOccupations(String country) async => + ['Student', 'Worker']; + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/progress/progress_view.dart b/StudioProjects/yimaru_app/lib/ui/views/progress/progress_view.dart new file mode 100644 index 0000000..4b90527 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/progress/progress_view.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/course_level_card.dart'; +import 'package:yimaru_app/ui/widgets/skill_progress.dart'; +import 'package:yimaru_app/ui/widgets/suggestion_card.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'progress_viewmodel.dart'; + +class ProgressView extends StackedView { + const ProgressView({Key? key}) : super(key: key); + + @override + ProgressViewModel viewModelBuilder(BuildContext context) => + ProgressViewModel(); + + @override + Widget builder( + BuildContext context, + ProgressViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(ProgressViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(ProgressViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(ProgressViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(ProgressViewModel viewModel) => _buildColumn(viewModel); + + Widget _buildColumn(ProgressViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(ProgressViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(ProgressViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(ProgressViewModel viewModel) => SmallAppBar( + title: 'My Progress', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(ProgressViewModel viewModel) => + Expanded(child: _buildContentScrollView(viewModel)); + + Widget _buildContentScrollView(ProgressViewModel viewModel) => + SingleChildScrollView( + child: _buildContentColumn(viewModel), + ); + + Widget _buildContentColumn(ProgressViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentChildren(viewModel), + ); + + List _buildContentChildren(ProgressViewModel viewModel) => [ + verticalSpaceMedium, + _buildCourseProgressSection(viewModel), + verticalSpaceMedium, + _buildSkillTitleWrapper(), + verticalSpaceMedium, + _buildSkillsWrapper(viewModel), + verticalSpaceLarge, + _buildSuggestionCard(), + verticalSpaceMassive + ]; + + Widget _buildCourseProgressSection(ProgressViewModel viewModel) => SizedBox( + height: 250, + width: double.maxFinite, + child: _buildListView(viewModel), + ); + + Widget _buildListView(ProgressViewModel viewModel) => ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: viewModel.progresses.length, + controller: PageController(viewportFraction: 0.9), + itemBuilder: (context, index) => _buildCourseLeveCard( + viewModel: viewModel, + icon: viewModel.progresses[index]['icon'], + title: viewModel.progresses[index]['title'], + color: viewModel.progresses[index]['color'], + status: viewModel.progresses[index]['status'], + subTitle: viewModel.progresses[index]['subTitle'], + isCompleted: viewModel.progresses[index]['isCompleted'], + ), + ); + + Widget _buildCourseLeveCard( + {required Color color, + required String title, + required String icon, + required String status, + required String subTitle, + required bool isCompleted, + required ProgressViewModel viewModel}) => + CourseLevelCard( + icon: icon, + title: title, + color: color, + status: status, + subTitle: subTitle, + isCompleted: isCompleted, + onTap: viewModel.navigateToOngoingProgress, + ); + + Widget _buildSkillTitleWrapper() => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildSkillTitle(), + ); + + Widget _buildSkillTitle() => const Text( + 'Skill Proficiency', + style: TextStyle( + fontSize: 18, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSkillsWrapper(ProgressViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildSkills(viewModel), + ); + + Widget _buildSkills(ProgressViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.skillsLevel.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildSkill( + skill: viewModel.skillsLevel[index]['skill'], + progress: viewModel.skillsLevel[index]['progress'], + ), + ); + + Widget _buildSkill({ + required String skill, + required double progress, + }) => + SkillProgress( + skill: skill, + progress: progress, + ); + + Widget _buildSuggestionCard() => const SuggestionCard(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/progress/progress_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/progress/progress_viewmodel.dart new file mode 100644 index 0000000..042b9ea --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/progress/progress_viewmodel.dart @@ -0,0 +1,65 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +import '../../../app/app.locator.dart'; + +class ProgressViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _progresses = [ + { + 'color': kcGreen, + 'title': 'Beginner', + 'isCompleted': true, + 'status': 'Completed', + 'icon': 'assets/icons/b1.svg', + 'subTitle': 'You’ve mastered everyday English basics!', + }, + { + 'title': 'Elementary', + 'isCompleted': false, + 'status': 'In Progress', + 'color': kcPrimaryColor, + 'icon': 'assets/icons/b1.svg', + 'subTitle': 'Continue improving your conversations and fluency.', + }, + { + 'title': 'Beginner', + 'isCompleted': true, + 'status': 'In Progress', + 'color': kcPrimaryColor, + 'icon': 'assets/icons/b1.svg', + 'subTitle': 'You’ve mastered everyday English basics!', + }, + ]; + + List> get progresses => _progresses; + + final List> _skillsLevel = [ + { + 'progress': 0.8, + 'skill': 'Speaking', + }, + { + 'progress': 0.95, + 'skill': 'Listening', + }, + { + 'progress': 0.75, + 'skill': 'Writing', + }, + { + 'progress': 0.8, + 'skill': 'Reading', + }, + ]; + + List> get skillsLevel => _skillsLevel; + + Future navigateToOngoingProgress() async => + await _navigationService.navigateToOngoingProgressView(); + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/register_view.dart b/StudioProjects/yimaru_app/lib/ui/views/register/register_view.dart new file mode 100644 index 0000000..e84a96b --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/register_view.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/views/register/screens/create_password_screen.dart'; +import 'package:yimaru_app/ui/views/register/screens/register_with_email_screen.dart'; +import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart'; +import 'package:yimaru_app/ui/views/register/screens/registration_otp_screen.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart'; + +import '../../common/app_colors.dart'; +import '../../common/validators/form_validator.dart'; +import 'register_viewmodel.dart'; + +import 'register_view.form.dart'; + +@FormView(fields: [ + FormTextField(name: 'otp', validator: FormValidator.validateForm), + FormTextField(name: 'email', validator: FormValidator.validateEmail), + FormTextField(name: 'password', validator: FormValidator.validateForm), + FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm), + FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm), +]) +class RegisterView extends StackedView with $RegisterView { + const RegisterView({Key? key}) : super(key: key); + + @override + void onViewModelReady(RegisterViewModel viewModel) { + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + RegisterViewModel viewModelBuilder(BuildContext context) => + RegisterViewModel(); + + @override + Widget builder( + BuildContext context, + RegisterViewModel viewModel, + Widget? child, + ) => + _buildRegisterScreensWrapper(viewModel); + + Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope( + canPop: false, + onPopInvokedWithResult: (value, data) { + if (value) return; + WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); + }, + child: _buildScaffoldWrapper(viewModel)); + + Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffoldStack(viewModel), + ); + + Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack( + children: [_buildScaffold(viewModel), _buildBusyRegistration(viewModel)]); + + Widget _buildScaffold(RegisterViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildScaffoldChildren(viewModel), + ); + + List _buildScaffoldChildren(RegisterViewModel viewModel) => + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; + + Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.goBack, + showLanguageSelection: true, + ); + + Widget _buildExpandedBody(RegisterViewModel viewModel) => + Expanded(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(RegisterViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), + ); + + Widget _buildBody(RegisterViewModel viewModel) => + IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); + + List _buildScreens() => [ + _buildRegisterWithEmailScreen(), + _buildRegisterWithPhoneScreen(), + _buildCreatePasswordScreen(), + _buildRegistrationOtpScreen(), + ]; + + Widget _buildRegisterWithEmailScreen() => + RegisterWithEmailScreen(emailController: emailController); + + Widget _buildRegisterWithPhoneScreen() => RegisterWithPhoneNumberScreen( + phoneNumberController: phoneNumberController); + + Widget _buildRegistrationOtpScreen() => RegistrationOtpScreen( + otpController: otpController, + emailController: emailController, + phoneNumberController: phoneNumberController, + ); + + Widget _buildCreatePasswordScreen() => CreatePasswordScreen( + passwordController: passwordController, + confirmPasswordController: confirmPasswordController); + + Widget _buildBusyRegistration(RegisterViewModel viewModel) => + viewModel.isBusy ? const PageLoadingIndicator() : Container(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/register_view.form.dart b/StudioProjects/yimaru_app/lib/ui/views/register/register_view.form.dart new file mode 100644 index 0000000..222853d --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/register_view.form.dart @@ -0,0 +1,308 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// StackedFormGenerator +// ************************************************************************** + +// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; + +const bool _autoTextFieldValidation = true; + +const String OtpValueKey = 'otp'; +const String EmailValueKey = 'email'; +const String PasswordValueKey = 'password'; +const String PhoneNumberValueKey = 'phoneNumber'; +const String ConfirmPasswordValueKey = 'confirmPassword'; + +final Map _RegisterViewTextEditingControllers = + {}; + +final Map _RegisterViewFocusNodes = {}; + +final Map _RegisterViewTextValidations = { + OtpValueKey: FormValidator.validateForm, + EmailValueKey: FormValidator.validateEmail, + PasswordValueKey: FormValidator.validateForm, + PhoneNumberValueKey: FormValidator.validateForm, + ConfirmPasswordValueKey: FormValidator.validateForm, +}; + +mixin $RegisterView { + TextEditingController get otpController => + _getFormTextEditingController(OtpValueKey); + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); + TextEditingController get passwordController => + _getFormTextEditingController(PasswordValueKey); + TextEditingController get phoneNumberController => + _getFormTextEditingController(PhoneNumberValueKey); + TextEditingController get confirmPasswordController => + _getFormTextEditingController(ConfirmPasswordValueKey); + + FocusNode get otpFocusNode => _getFormFocusNode(OtpValueKey); + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); + FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey); + FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey); + FocusNode get confirmPasswordFocusNode => + _getFormFocusNode(ConfirmPasswordValueKey); + + TextEditingController _getFormTextEditingController( + String key, { + String? initialValue, + }) { + if (_RegisterViewTextEditingControllers.containsKey(key)) { + return _RegisterViewTextEditingControllers[key]!; + } + + _RegisterViewTextEditingControllers[key] = + TextEditingController(text: initialValue); + return _RegisterViewTextEditingControllers[key]!; + } + + FocusNode _getFormFocusNode(String key) { + if (_RegisterViewFocusNodes.containsKey(key)) { + return _RegisterViewFocusNodes[key]!; + } + _RegisterViewFocusNodes[key] = FocusNode(); + return _RegisterViewFocusNodes[key]!; + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + void syncFormWithViewModel(FormStateHelper model) { + otpController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + confirmPasswordController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + @Deprecated( + 'Use syncFormWithViewModel instead.' + 'This feature was deprecated after 3.1.0.', + ) + void listenToFormUpdated(FormViewModel model) { + otpController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + confirmPasswordController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Updates the formData on the FormViewModel + void _updateFormData(FormStateHelper model, {bool forceValidate = false}) { + model.setData( + model.formValueMap + ..addAll({ + OtpValueKey: otpController.text, + EmailValueKey: emailController.text, + PasswordValueKey: passwordController.text, + PhoneNumberValueKey: phoneNumberController.text, + ConfirmPasswordValueKey: confirmPasswordController.text, + }), + ); + + if (_autoTextFieldValidation || forceValidate) { + updateValidationData(model); + } + } + + bool validateFormFields(FormViewModel model) { + _updateFormData(model, forceValidate: true); + return model.isFormValid; + } + + /// Calls dispose on all the generated controllers and focus nodes + void disposeForm() { + // The dispose function for a TextEditingController sets all listeners to null + + for (var controller in _RegisterViewTextEditingControllers.values) { + controller.dispose(); + } + for (var focusNode in _RegisterViewFocusNodes.values) { + focusNode.dispose(); + } + + _RegisterViewTextEditingControllers.clear(); + _RegisterViewFocusNodes.clear(); + } +} + +extension ValueProperties on FormStateHelper { + bool get hasAnyValidationMessage => this + .fieldsValidationMessages + .values + .any((validation) => validation != null); + + bool get isFormValid { + if (!_autoTextFieldValidation) this.validateForm(); + + return !hasAnyValidationMessage; + } + + String? get otpValue => this.formValueMap[OtpValueKey] as String?; + String? get emailValue => this.formValueMap[EmailValueKey] as String?; + String? get passwordValue => this.formValueMap[PasswordValueKey] as String?; + String? get phoneNumberValue => + this.formValueMap[PhoneNumberValueKey] as String?; + String? get confirmPasswordValue => + this.formValueMap[ConfirmPasswordValueKey] as String?; + + set otpValue(String? value) { + this.setData( + this.formValueMap..addAll({OtpValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey(OtpValueKey)) { + _RegisterViewTextEditingControllers[OtpValueKey]?.text = value ?? ''; + } + } + + set emailValue(String? value) { + this.setData( + this.formValueMap..addAll({EmailValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey(EmailValueKey)) { + _RegisterViewTextEditingControllers[EmailValueKey]?.text = value ?? ''; + } + } + + set passwordValue(String? value) { + this.setData( + this.formValueMap..addAll({PasswordValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey(PasswordValueKey)) { + _RegisterViewTextEditingControllers[PasswordValueKey]?.text = value ?? ''; + } + } + + set phoneNumberValue(String? value) { + this.setData( + this.formValueMap..addAll({PhoneNumberValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey(PhoneNumberValueKey)) { + _RegisterViewTextEditingControllers[PhoneNumberValueKey]?.text = + value ?? ''; + } + } + + set confirmPasswordValue(String? value) { + this.setData( + this.formValueMap..addAll({ConfirmPasswordValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey( + ConfirmPasswordValueKey)) { + _RegisterViewTextEditingControllers[ConfirmPasswordValueKey]?.text = + value ?? ''; + } + } + + bool get hasOtp => + this.formValueMap.containsKey(OtpValueKey) && + (otpValue?.isNotEmpty ?? false); + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); + bool get hasPassword => + this.formValueMap.containsKey(PasswordValueKey) && + (passwordValue?.isNotEmpty ?? false); + bool get hasPhoneNumber => + this.formValueMap.containsKey(PhoneNumberValueKey) && + (phoneNumberValue?.isNotEmpty ?? false); + bool get hasConfirmPassword => + this.formValueMap.containsKey(ConfirmPasswordValueKey) && + (confirmPasswordValue?.isNotEmpty ?? false); + + bool get hasOtpValidationMessage => + this.fieldsValidationMessages[OtpValueKey]?.isNotEmpty ?? false; + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; + bool get hasPasswordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false; + bool get hasPhoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false; + bool get hasConfirmPasswordValidationMessage => + this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ?? + false; + + String? get otpValidationMessage => + this.fieldsValidationMessages[OtpValueKey]; + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; + String? get passwordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]; + String? get phoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]; + String? get confirmPasswordValidationMessage => + this.fieldsValidationMessages[ConfirmPasswordValueKey]; +} + +extension Methods on FormStateHelper { + setOtpValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[OtpValueKey] = validationMessage; + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; + setPasswordValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PasswordValueKey] = validationMessage; + setPhoneNumberValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage; + setConfirmPasswordValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[ConfirmPasswordValueKey] = + validationMessage; + + /// Clears text input fields on the Form + void clearForm() { + otpValue = ''; + emailValue = ''; + passwordValue = ''; + phoneNumberValue = ''; + confirmPasswordValue = ''; + } + + /// Validates text input fields on the Form + void validateForm() { + this.setValidationMessages({ + OtpValueKey: getValidationMessage(OtpValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey), + }); + } +} + +/// Returns the validation message for the given key +String? getValidationMessage(String key) { + final validatorForKey = _RegisterViewTextValidations[key]; + if (validatorForKey == null) return null; + + String? validationMessageForKey = validatorForKey( + _RegisterViewTextEditingControllers[key]!.text, + ); + + return validationMessageForKey; +} + +/// Updates the fieldsValidationMessages on the FormViewModel +void updateValidationData(FormStateHelper model) => + model.setValidationMessages({ + OtpValueKey: getValidationMessage(OtpValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey), + }); diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/register_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/register/register_viewmodel.dart new file mode 100644 index 0000000..be38d68 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/register_viewmodel.dart @@ -0,0 +1,310 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/services/api_service.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/views/home/home_view.dart'; +import 'package:yimaru_app/ui/views/login/login_view.dart'; + +import '../../../app/app.locator.dart'; +import '../../../models/user_model.dart'; + +class RegisterViewModel extends FormViewModel { + final _apiService = locator(); + final _navigationService = locator(); + final _authenticationService = locator(); + + // Navigation + int _currentIndex = 0; + + int get currentIndex => _currentIndex; + + // Email + bool _focusEmail = false; + + bool get focusEmail => _focusEmail; + + // Password + bool _length = false; + + bool get length => _length; + + bool _number = false; + + bool get number => _number; + + bool _specialChar = false; + + bool get specialChar => _specialChar; + + bool _focusPassword = false; + + bool get focusPassword => _focusPassword; + + bool _obscurePassword = true; + + bool get obscurePassword => _obscurePassword; + + bool _passwordMatch = false; + + bool get passwordMatch => _passwordMatch; + + // Confirm password + bool _focusConfirmPassword = false; + + bool get focusConfirmPassword => _focusConfirmPassword; + + bool _obscureConfirmPassword = true; + + bool get obscureConfirmPassword => _obscureConfirmPassword; + + // Phone number + bool _focusPhoneNumber = false; + + bool get focusPhoneNumber => _focusPhoneNumber; + + // Terms and conditions + bool _agree = false; + + bool get agree => _agree; + + // Focus otp + bool _focusOtp = false; + + bool get focusOtp => _focusOtp; + + // Focus node + final FocusNode _focusNode = FocusNode(); + + FocusNode get focusNode => _focusNode; + + // Registration type + RegistrationType? _registrationType; + + RegistrationType? get registrationType => _registrationType; + + // Resend button state + bool _buttonActive = false; + + bool get buttonActive => _buttonActive; + + DateTime _resendTime = + DateTime.now().add(const Duration(minutes: 3, seconds: 0)); + + DateTime get resendTime => _resendTime; + + // User data + final Map _userData = {}; + + Map get userData => _userData; + + // Email + void setEmailFocus() { + _focusEmail = true; + rebuildUi(); + } + + // Password + void setPasswordFocus() { + _focusPassword = true; + rebuildUi(); + } + + void validatePassword( + {required String password, required String confirmPassword}) { + if (password.length > 8) { + _length = true; + } else { + _length = false; + } + + if (RegExp(r'\d').hasMatch(password)) { + _number = true; + } else { + _number = false; + } + + if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) { + _specialChar = true; + } else { + _specialChar = false; + } + + if (password == confirmPassword) { + _passwordMatch = true; + } else { + _passwordMatch = false; + } + rebuildUi(); + } + + double validationProgress() { + int completed = 0; + + if (_length) completed++; + if (_number) completed++; + if (_specialChar) completed++; + if (_passwordMatch) completed++; + + return completed / 4; // returns 0.0 → 1.0 + } + + void setObscurePassword() { + _obscurePassword = !_obscurePassword; + rebuildUi(); + } + + // Confirm password + void setConfirmPasswordFocus() { + _focusConfirmPassword = true; + rebuildUi(); + } + + void setObscureConfirmPassword() { + _obscureConfirmPassword = !_obscureConfirmPassword; + rebuildUi(); + } + + // Phone number + void setPhoneNumberFocus() { + _focusPhoneNumber = true; + rebuildUi(); + } + + // Otp + void setOtpFocus() { + _focusOtp = true; + rebuildUi(); + } + + // Terms and Conditions + void setAgreement(bool value) { + _agree = value; + rebuildUi(); + } + + void setResendButton() { + _buttonActive = true; + rebuildUi(); + } + + void resetButton() { + _buttonActive = false; + + _resendTime = DateTime.now().add(const Duration(minutes: 3, seconds: 0)); + } + + // Validate otp + Future validateOtp(String otp) async {} + + // Add user data + void addUserData(Map data) { + _userData.addAll(data); + } + + void clearUserData() { + _userData.clear(); + } + + // Remote api calls + Future register() async { + Map response = await runBusyFuture>( + _apiService.register(_userData)); + + if (response['status'] == ResponseStatus.success) { + goTo(page: 3); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + } + + Future verifyOtp() async { + Map response = + await runBusyFuture>(_verifyOtp()); + + if (response['status'] == ResponseStatus.success) { + await replaceWithHome(); + } + } + + Future> _verifyOtp() async { + Map response = await _apiService.verifyOtp(_userData); + if (response['status'] == ResponseStatus.success) { + // UserModel user = response['data'] as UserModel; + // Map data = { + // 'userId': user.userId, + // 'accessToken': user.accessToken, + // 'refreshToken': user.refreshToken + // }; + + await _authenticationService.saveUserData({ + 'userId': 10, + 'accessToken': 'accessToken', + 'refreshToken': 'refreshToken' + }); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + + return response; + } + + Future resendOtp() async { + resetButton(); + + Map response = await runBusyFuture>( + _apiService.resendOtp(_userData)); + + if (response['status'] == ResponseStatus.success) { + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + } + + // Navigation + void goTo({required int page, RegistrationType? type}) { + _currentIndex = page; + if (type != null) { + _registrationType = type; + } + rebuildUi(); + } + + void goBack() { + if (_currentIndex == 1) { + _currentIndex = 0; + rebuildUi(); + } else if (_currentIndex == 2) { + _currentIndex = 0; + rebuildUi(); + } else if (_currentIndex == 3) { + if (_registrationType == RegistrationType.phone) { + _currentIndex = 1; + } else { + _currentIndex = 2; + } + + rebuildUi(); + } else { + _navigationService.back(); + } + } + + Future navigateToTermsAndConditions() async => + await _navigationService.navigateToTermsAndConditionsView(); + + Future navigateToPrivacyPolicy() async => + await _navigationService.navigateToPrivacyPolicyView(); + + Future replaceToLogin() async => + await _navigationService.replaceWithLoginView(); + + Future replaceWithHome() async => + await _navigationService.clearStackAndShowView(const HomeView()); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/screens/create_password_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/register/screens/create_password_screen.dart new file mode 100644 index 0000000..87aaa6e --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/screens/create_password_screen.dart @@ -0,0 +1,255 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/register/register_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_form_label.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; +import 'package:yimaru_app/ui/widgets/validator_list_tile.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/obscure_password.dart'; +import '../register_view.form.dart'; + +class CreatePasswordScreen extends ViewModelWidget { + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; + + const CreatePasswordScreen( + {super.key, + required this.passwordController, + required this.confirmPasswordController}); + + Future _signUp(RegisterViewModel viewModel) async { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'role': 'STUDENT', + 'otp_medium': 'email', + 'password': passwordController.text, + }; + viewModel.addUserData(data); + + await viewModel.register(); + } + + @override + Widget build(BuildContext context, RegisterViewModel viewModel) => + _buildBodyChildren(viewModel); + + Widget _buildBodyChildren(RegisterViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyColumn(viewModel), + ); + + Widget _buildBodyColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyColumnChildren(viewModel), + ); + + List _buildBodyColumnChildren(RegisterViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildPasswordLabel('Password'), + verticalSpaceSmall, + _buildPasswordFormField(viewModel), + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + verticalSpaceTiny, + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + _buildPasswordValidationWrapper(viewModel), + verticalSpaceMedium, + _buildPasswordLabel('Confirm Password'), + verticalSpaceSmall, + _buildConfirmPasswordFormField(viewModel), + if (viewModel.hasConfirmPasswordValidationMessage && + viewModel.focusConfirmPassword) + verticalSpaceTiny, + if (viewModel.hasConfirmPasswordValidationMessage && + viewModel.focusConfirmPassword) + _buildConfirmPasswordValidationWrapper(viewModel), + verticalSpaceMedium, + _buildLinearProgressIndicator(viewModel), + verticalSpaceSmall, + _buildCharLengthValidator(viewModel), + _buildNumberValidator(viewModel), + _buildSymbolValidator(viewModel), + _buildPasswordMatchValidator(viewModel), + _buildCheckBox(viewModel), + verticalSpaceSmall, + _buildSignUpButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildTitle() => const Text( + 'Create Password', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildPasswordLabel(String label) => CustomFormLabel( + label: label, + style: style14DG400, + ); + + Widget _buildPasswordFormField(RegisterViewModel viewModel) => TextFormField( + controller: passwordController, + onTap: viewModel.setPasswordFocus, + obscureText: viewModel.obscurePassword, + decoration: inputDecoration( + hint: 'Password', + focus: viewModel.focusPassword, + suffix: _buildObscurePassword(viewModel), + filled: passwordController.text.isNotEmpty), + onChanged: (value) => viewModel.validatePassword( + password: passwordController.text, + confirmPassword: confirmPasswordController.text), + ); + + Widget _buildObscurePassword(RegisterViewModel viewModel) => ObscurePassword( + focus: viewModel.focusPassword, + obscure: viewModel.obscurePassword, + onTap: viewModel.setObscurePassword, + ); + + Widget _buildPasswordValidationWrapper(RegisterViewModel viewModel) => + viewModel.hasPasswordValidationMessage + ? _buildPasswordValidator(viewModel) + : Container(); + + Widget _buildPasswordValidator(RegisterViewModel viewModel) => Text( + viewModel.passwordValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildConfirmPasswordFormField(RegisterViewModel viewModel) => + TextFormField( + controller: confirmPasswordController, + onTap: viewModel.setConfirmPasswordFocus, + obscureText: viewModel.obscureConfirmPassword, + onChanged: (value) => viewModel.validatePassword( + password: passwordController.text, + confirmPassword: confirmPasswordController.text), + decoration: inputDecoration( + hint: 'Confirm Password', + focus: viewModel.focusConfirmPassword, + suffix: _buildObscureConfirmPassword(viewModel), + filled: confirmPasswordController.text.isNotEmpty), + ); + + Widget _buildObscureConfirmPassword(RegisterViewModel viewModel) => + ObscurePassword( + focus: viewModel.focusConfirmPassword, + obscure: viewModel.obscureConfirmPassword, + onTap: viewModel.setObscureConfirmPassword, + ); + + Widget _buildConfirmPasswordValidationWrapper(RegisterViewModel viewModel) => + viewModel.hasConfirmPasswordValidationMessage + ? _buildConfirmPasswordValidator(viewModel) + : Container(); + + Widget _buildConfirmPasswordValidator(RegisterViewModel viewModel) => Text( + viewModel.confirmPasswordValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildLinearProgressIndicator(RegisterViewModel viewModel) => + CustomLinearProgressIndicator( + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + progress: viewModel.validationProgress(), + ); + + Widget _buildCharLengthValidator(RegisterViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey, + label: '8 characters minimum'); + + Widget _buildNumberValidator(RegisterViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey, + label: 'a number'); + + Widget _buildSymbolValidator(RegisterViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey, + label: 'one symbol minimum'); + + Widget _buildPasswordMatchValidator(RegisterViewModel viewModel) => + ValidatorListTile( + backgroundColor: + viewModel.passwordMatch ? kcPrimaryColor : kcLightGrey, + label: 'password match'); + + Widget _buildCheckBox(RegisterViewModel viewMode) => CheckboxListTile( + value: viewMode.agree, + activeColor: kcPrimaryColor, + title: _buildCheckBoxTitle(viewMode), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) => viewMode.setAgreement(value ?? false)); + + Widget _buildCheckBoxTitle(RegisterViewModel viewMode) => Text.rich( + TextSpan( + text: 'By clicking "Sign Up", you agree to our', + style: style14DG400, + children: [ + TextSpan( + text: ' Terms of Service', + style: style14P600, + recognizer: TapGestureRecognizer() + ..onTap = () => viewMode.navigateToTermsAndConditions()), + TextSpan(text: ' and ', style: style14DG400), + TextSpan( + text: 'Privacy Policy', + style: style14P600, + recognizer: TapGestureRecognizer() + ..onTap = () => viewMode.navigateToPrivacyPolicy()), + ]), + ); + + Widget _buildSignUpButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Sign Up', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: + (viewModel.focusPassword && passwordController.text.isNotEmpty) && + (viewModel.focusConfirmPassword && + confirmPasswordController.text.isNotEmpty) && + viewModel.number && + viewModel.length && + viewModel.specialChar && + viewModel.specialChar && + viewModel.passwordMatch && + viewModel.agree + ? () async => await _signUp(viewModel) + : null, + backgroundColor: + (viewModel.focusPassword && passwordController.text.isNotEmpty) && + (viewModel.focusConfirmPassword && + confirmPasswordController.text.isNotEmpty) && + viewModel.number && + viewModel.length && + viewModel.specialChar && + viewModel.specialChar && + viewModel.passwordMatch && + viewModel.agree + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_email_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_email_screen.dart new file mode 100644 index 0000000..6b35e65 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_email_screen.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/widgets/login_account.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/option_text_divider.dart'; +import '../register_viewmodel.dart'; +import '../register_view.form.dart'; + +class RegisterWithEmailScreen extends ViewModelWidget { + final TextEditingController emailController; + + const RegisterWithEmailScreen({ + super.key, + required this.emailController, + }); + + void _addUserData(RegisterViewModel viewModel) { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'email': emailController.text, + }; + viewModel.addUserData(data); + viewModel.goTo(page: 2, type: RegistrationType.email); + } + + @override + Widget build(BuildContext context, RegisterViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(RegisterViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(RegisterViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildColumnScroller(RegisterViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(RegisterViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitleWrapper(viewModel), + verticalSpaceLarge, + _buildEmailFormField(viewModel), + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + verticalSpaceTiny, + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + _buildEmailValidatorWrapper(viewModel), + ]; + + Widget _buildTitle() => const Text( + 'Create an Account', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount( + onTap: () async => await viewModel.replaceToLogin(), + ); + + Widget _buildEmailFormField(RegisterViewModel viewModel) => TextFormField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + onTap: viewModel.setEmailFocus, + decoration: inputDecoration( + hint: 'Email', + focus: viewModel.focusEmail, + filled: emailController.text.isNotEmpty), + ); + + Widget _buildEmailValidatorWrapper(RegisterViewModel viewModel) => + viewModel.hasEmailValidationMessage + ? _buildEmailValidator(viewModel) + : Container(); + + Widget _buildEmailValidator(RegisterViewModel viewModel) => Text( + viewModel.emailValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildLowerColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(RegisterViewModel viewModel) => [ + _buildContinueButton(viewModel), + _buildOptionTextDivider(), + _buildRegisterWithEmailButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildContinueButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: viewModel.focusEmail && + emailController.text.isNotEmpty && + !viewModel.hasEmailValidationMessage + ? () => _addUserData(viewModel) + : null, + backgroundColor: viewModel.focusEmail && + emailController.text.isNotEmpty && + !viewModel.hasEmailValidationMessage + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildRegisterWithEmailButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + backgroundColor: kcWhite, + leadingIcon: Icons.phone, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + text: 'Register with Phone Number', + onTap: () => viewModel.goTo(page: 1), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_phone_number_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_phone_number_screen.dart new file mode 100644 index 0000000..77e3e58 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_phone_number_screen.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/login_account.dart'; +import 'package:yimaru_app/ui/widgets/option_text_divider.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/enmus.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/phone_number_prefix.dart'; +import '../register_viewmodel.dart'; +import '../register_view.form.dart'; + +class RegisterWithPhoneNumberScreen extends ViewModelWidget { + final TextEditingController phoneNumberController; + const RegisterWithPhoneNumberScreen( + {super.key, required this.phoneNumberController}); + + @override + Widget build(BuildContext context, RegisterViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(RegisterViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(RegisterViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildColumnScroller(RegisterViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(RegisterViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitleWrapper(viewModel), + verticalSpaceMedium, + _buildSubtitle(), + verticalSpaceMedium, + _buildPhoneNumberWrapper(viewModel), + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + verticalSpaceTiny, + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + _buildPhoneNumberValidatorWrapper(viewModel), + ]; + + Widget _buildTitle() => const Text( + 'Create an Account', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount( + onTap: () async => await viewModel.replaceToLogin(), + ); + + Widget _buildSubtitle() => const Text( + 'Enter your phone number. We will send you a confirmation code there', + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildPhoneNumberWrapper(RegisterViewModel viewModel) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildPhoneNumberChildren(viewModel), + ); + + List _buildPhoneNumberChildren(RegisterViewModel viewModel) => [ + _buildPhoneNumberPrefix(viewModel), + horizontalSpaceSmall, + _buildPhoneNumberFormFieldWrapper(viewModel), + ]; + + Widget _buildPhoneNumberPrefix(RegisterViewModel viewModel) => + PhoneNumberPrefix(selected: viewModel.focusPhoneNumber); + + Widget _buildPhoneNumberFormFieldWrapper(RegisterViewModel viewModel) => + Expanded(child: _buildPhoneNumberFormField(viewModel)); + + Widget _buildPhoneNumberFormField(RegisterViewModel viewModel) => + TextFormField( + maxLength: 9, + keyboardType: TextInputType.phone, + controller: phoneNumberController, + onTap: viewModel.setPhoneNumberFocus, + decoration: inputDecoration( + focus: viewModel.focusPhoneNumber, + filled: phoneNumberController.text.isNotEmpty), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ); + + Widget _buildPhoneNumberValidatorWrapper(RegisterViewModel viewModel) => + viewModel.hasPhoneNumberValidationMessage + ? _buildPhoneNumberValidator(viewModel) + : Container(); + + Widget _buildPhoneNumberValidator(RegisterViewModel viewModel) => Text( + viewModel.phoneNumberValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildLowerColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(RegisterViewModel viewModel) => [ + _buildContinueButton(viewModel), + _buildOptionTextDivider(), + _buildRegisterWitPhoneNumberButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildContinueButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: + viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty + ? () => viewModel.goTo(page: 3, type: RegistrationType.phone) + : null, + backgroundColor: + viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); + + Widget _buildRegisterWitPhoneNumberButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + backgroundColor: kcWhite, + leadingIcon: Icons.email, + borderColor: kcPrimaryColor, + text: 'Register with Email', + foregroundColor: kcPrimaryColor, + onTap: () => viewModel.goTo(page: 0), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/screens/registration_otp_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/register/screens/registration_otp_screen.dart new file mode 100644 index 0000000..9c822ee --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/screens/registration_otp_screen.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_timer_countdown/flutter_timer_countdown.dart'; +import 'package:pinput/pinput.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; + +import 'package:yimaru_app/ui/views/register/register_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_cursor.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; + +import '../register_view.form.dart'; + +class RegistrationOtpScreen extends ViewModelWidget { + final TextEditingController otpController; + final TextEditingController emailController; + final TextEditingController phoneNumberController; + + const RegistrationOtpScreen( + {super.key, + required this.otpController, + required this.emailController, + required this.phoneNumberController}); + + Future _verifyOtp(RegisterViewModel viewModel) async { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'otp': otpController.text, + 'email': emailController.text, + }; + viewModel.clearUserData(); + viewModel.addUserData(data); + + await viewModel.verifyOtp(); + } + + @override + Widget build(BuildContext context, RegisterViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(RegisterViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(RegisterViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(RegisterViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(RegisterViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildSubtitleWrapper(), + verticalSpaceMedium, + _buildPinPutWrapper(viewModel), + if (viewModel.hasOtpValidationMessage && viewModel.focusOtp) + verticalSpaceTiny, + if (viewModel.hasOtpValidationMessage && viewModel.focusOtp) + _buildOtpValidatorWrapper(viewModel), + verticalSpaceSmall, + _buildTimerWrapper(viewModel) + ]; + + Widget _buildTitle() => Text( + 'Verification Code', + style: style25DG600, + ); + + Widget _buildSubtitleWrapper() => + phoneNumberController.text.length == 9 ? _buildSubtitle() : Container(); + + Widget _buildSubtitle() => Text( + 'Code sent to your number +251${phoneNumberController.text.substring(0, 5)}****', + style: style14DG400, + ); + + Widget _buildPinPutWrapper(RegisterViewModel viewModel) => Center( + child: _buildPinPut(viewModel), + ); + + Widget _buildPinPut(RegisterViewModel viewModel) => Pinput( + length: 6, + controller: otpController, + defaultPinTheme: defaultPin, + cursor: const CustomCursor(), + errorPinTheme: errorPinTheme, + onTap: viewModel.setOtpFocus, + focusNode: viewModel.focusNode, + errorTextStyle: validationStyle, + //smsRetriever: locator(), + focusedPinTheme: focusedThemePin, + submittedPinTheme: submittedThemePin, + hapticFeedbackType: HapticFeedbackType.heavyImpact, + separatorBuilder: (index) => const SizedBox(width: 10), + onCompleted: (otp) async => await _verifyOtp(viewModel), + ); + + Widget _buildOtpValidatorWrapper(RegisterViewModel viewModel) => + viewModel.hasOtpValidationMessage + ? _buildOtpValidator(viewModel) + : Container(); + + Widget _buildOtpValidator(RegisterViewModel viewModel) => Text( + viewModel.otpValidationMessage!, + style: style12R700, + ); + + Widget _buildTimerWrapper(RegisterViewModel viewModel) => + !viewModel.buttonActive + ? _buildTimerSection(viewModel) + : _buildResendButton(viewModel); + + Widget _buildResendButton(RegisterViewModel viewModel) => TextButton( + onPressed: () async => await viewModel.resendOtp(), + child: _buildResendText()); + + Widget _buildResendText() => Text( + 'Resend code', + style: style14P600.copyWith(fontStyle: FontStyle.italic), + ); + + Widget _buildTimerSection(RegisterViewModel viewModel) => Row( + children: [ + _buildCountdownText(), + horizontalSpaceSmall, + _buildTimer(viewModel) + ], + ); + + Widget _buildCountdownText() => Text('Resend code in ', style: style14DG400); + + Widget _buildTimer(RegisterViewModel viewModel) => TimerCountdown( + enableDescriptions: false, + timeTextStyle: style14P600, + endTime: viewModel.resendTime, + onEnd: viewModel.setResendButton, + format: CountDownTimerFormat.minutesSeconds, + colonsTextStyle: const TextStyle(color: kcPrimaryColor), + ); + + Widget _buildContinueButtonWrapper(RegisterViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildContinueButton(viewModel), + ); + + Widget _buildContinueButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: viewModel.focusOtp && + otpController.text.length == 6 && + !viewModel.hasOtpValidationMessage + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + onTap: viewModel.focusOtp && + otpController.text.length == 6 && + !viewModel.hasOtpValidationMessage + ? () async => await _verifyOtp(viewModel) + : null, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/startup/startup_view.dart b/StudioProjects/yimaru_app/lib/ui/views/startup/startup_view.dart index 42e039f..384a0ad 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/startup/startup_view.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/startup/startup_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stacked/stacked.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart'; import '../../common/app_colors.dart'; import 'startup_viewmodel.dart'; @@ -47,7 +48,9 @@ class StartupView extends StackedView { ); List _buildUpperColumnChildren() => - [_buildIconWrapper(), _buildLoadingTextContainer()]; + [_buildIconWrapper(), _buildSafeWrapper()]; + + Widget _buildSafeWrapper() => SafeArea(child: _buildLoadingTextContainer()); Widget _buildLoadingTextContainer() => Padding( padding: const EdgeInsets.only(bottom: 50), @@ -66,8 +69,8 @@ class StartupView extends StackedView { _buildIndicatorWrapper(), ]; - Widget _buildLoadingText() => const Text('Loading ...', - style: TextStyle(color: kcWhiteColor, fontSize: 16)); + Widget _buildLoadingText() => + const Text('Loading ...', style: TextStyle(color: kcWhite, fontSize: 16)); Widget _buildIndicatorWrapper() => SizedBox( width: 16, @@ -75,10 +78,8 @@ class StartupView extends StackedView { child: _buildIndicator(), ); - Widget _buildIndicator() => const CircularProgressIndicator( - strokeWidth: 6, - color: kcWhiteColor, - ); + Widget _buildIndicator() => + const CustomCircularProgressIndicator(color: kcWhite); Widget _buildIconWrapper() => Padding( padding: const EdgeInsets.only(top: 100), @@ -87,6 +88,7 @@ class StartupView extends StackedView { Widget _buildIcon() => SvgPicture.asset( 'assets/icons/logo.svg', + height: 50, ); @override diff --git a/StudioProjects/yimaru_app/lib/ui/views/startup/startup_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/startup/startup_viewmodel.dart index 1f4bc32..32b149a 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/startup/startup_viewmodel.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/startup/startup_viewmodel.dart @@ -1,19 +1,21 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; import '../../../app/app.locator.dart'; import '../../../app/app.router.dart'; class StartupViewModel extends BaseViewModel { final _navigationService = locator(); + final _authenticationService = locator(); // Place anything here that needs to happen before we get into the application Future runStartupLogic() async { - await Future.delayed(const Duration(seconds: 3)); - - // This is where you can make decisions on where your app should navigate when - // you have custom startup logic - - _navigationService.replaceWithOnboardingView(); + final response = await _authenticationService.userLoggedIn(); + if (response) { + _navigationService.replaceWithHomeView(); + } else { + _navigationService.replaceWithLoginView(); + } } } diff --git a/StudioProjects/yimaru_app/lib/ui/views/support/support_view.dart b/StudioProjects/yimaru_app/lib/ui/views/support/support_view.dart new file mode 100644 index 0000000..0010b9f --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/support/support_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/support_card.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'support_viewmodel.dart'; + +class SupportView extends StackedView { + const SupportView({Key? key}) : super(key: key); + + @override + SupportViewModel viewModelBuilder(BuildContext context) => SupportViewModel(); + + @override + Widget builder( + BuildContext context, + SupportViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(SupportViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(SupportViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(SupportViewModel viewModel) => _buildBody(viewModel); + + Widget _buildBody(SupportViewModel viewModel) => _buildColumn(viewModel); + + Widget _buildColumn(SupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(SupportViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(SupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar( + title: 'Need Help?', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(SupportViewModel viewModel) => + Expanded(child: _buildContentColumnWrapper(viewModel)); + + Widget _buildContentColumnWrapper(SupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildMenuColumnScrollView(viewModel), + ); + + Widget _buildMenuColumnScrollView(SupportViewModel viewModel) => + SingleChildScrollView( + child: _buildMenuColumn(viewModel), + ); + + Widget _buildMenuColumn(SupportViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildMenuColumnChildren(viewModel), + ); + + List _buildMenuColumnChildren(SupportViewModel viewModel) => [ + verticalSpaceLarge, + _buildCallSupport(viewModel), + verticalSpaceMedium, + _buildTelegramSupport(viewModel) + ]; + + Widget _buildCallSupport(SupportViewModel viewModel) => SupportCard( + icon: Icons.call, + color: kcPrimaryColor, + title: 'Call Support', + subtitle: 'Talk with our support team directly', + onTap: () async => await viewModel.navigateToCallSupport(), + ); + + Widget _buildTelegramSupport(SupportViewModel viewModel) => SupportCard( + color: kcSkyBlue, + icon: Icons.telegram, + title: 'Telegram Support', + subtitle: 'Chat Instantly via Telegram', + onTap: () async => await viewModel.navigateToTelegramSupport(), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/support/support_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/support/support_viewmodel.dart new file mode 100644 index 0000000..9691bf3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/support/support_viewmodel.dart @@ -0,0 +1,18 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; + +import '../../../app/app.locator.dart'; + +class SupportViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Navigation + void pop() => _navigationService.back(); + + Future navigateToTelegramSupport() async => + await _navigationService.navigateToTelegramSupportView(); + + Future navigateToCallSupport() async => + await _navigationService.navigateToCallSupportView(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_view.dart b/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_view.dart new file mode 100644 index 0000000..0d5507e --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_view.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/circular_icon.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/option_text_divider.dart'; +import '../../widgets/small_app_bar.dart'; +import 'telegram_support_viewmodel.dart'; + +class TelegramSupportView extends StackedView { + const TelegramSupportView({Key? key}) : super(key: key); + + @override + TelegramSupportViewModel viewModelBuilder(BuildContext context) => + TelegramSupportViewModel(); + + @override + Widget builder( + BuildContext context, + TelegramSupportViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(TelegramSupportViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(TelegramSupportViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(TelegramSupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(TelegramSupportViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + _buildExpandedColumn(viewModel) + ]; + + Widget _buildAppBarWrapper(TelegramSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(TelegramSupportViewModel viewModel) => SmallAppBar( + title: 'Telegram Support', + onTap: viewModel.pop, + ); + + Widget _buildExpandedColumn(TelegramSupportViewModel viewModel) => + Expanded(child: _buildColumnWrapper(viewModel)); + + Widget _buildColumnWrapper(TelegramSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(TelegramSupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(TelegramSupportViewModel viewModel) => + [_buildUpperColumn(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildUpperColumn(TelegramSupportViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(TelegramSupportViewModel viewModel) => + [ + verticalSpaceLarge, + _buildIcon(), + verticalSpaceMedium, + _buildTitle(), + verticalSpaceSmall, + _buildSubTitle(), + ]; + + Widget _buildIcon() => + const CircularIcon(icon: Icons.telegram, size: 50, color: kcSkyBlue); + + Widget _buildTitle() => const Text( + 'Join Yimaru Academy on Telegram', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => const Text( + 'Connect with our support team instantly on Telegram for quick assistance and community updates', + textAlign: TextAlign.center, + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildLowerColumn(TelegramSupportViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(TelegramSupportViewModel viewModel) => + [ + _buildContinueButton(viewModel), + verticalSpaceSmall, + _buildOptionTextDivider(), + verticalSpaceSmall, + _buildSearchText(), + verticalSpaceMedium + ]; + + Widget _buildContinueButton(TelegramSupportViewModel viewModel) => + const CustomElevatedButton( + height: 55, + borderRadius: 12, + leadingIcon: Icons.telegram, + text: 'Open in Telegram', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildSearchText() => const Text.rich( + TextSpan( + text: 'Search for', + style: TextStyle( + color: kcDarkGrey, + ), + children: [ + TextSpan( + text: ' @YimaruSupport', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ) + ]), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_viewmodel.dart new file mode 100644 index 0000000..c9f6120 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_viewmodel.dart @@ -0,0 +1,9 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class TelegramSupportViewModel extends BaseViewModel { + final _navigationService = locator(); + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_view.dart b/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_view.dart new file mode 100644 index 0000000..2b03944 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_view.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/app_strings.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'terms_and_conditions_viewmodel.dart'; + +class TermsAndConditionsView extends StackedView { + const TermsAndConditionsView({Key? key}) : super(key: key); + + @override + TermsAndConditionsViewModel viewModelBuilder(BuildContext context) => + TermsAndConditionsViewModel(); + + @override + Widget builder( + BuildContext context, + TermsAndConditionsViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(TermsAndConditionsViewModel viewModel) => + Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(TermsAndConditionsViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(TermsAndConditionsViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(TermsAndConditionsViewModel viewModel) => + _buildColumn(viewModel); + + Widget _buildColumn(TermsAndConditionsViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(TermsAndConditionsViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(TermsAndConditionsViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar( + title: 'Terms and Conditions', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) => + Expanded(child: _buildContentColumnWrapper(viewModel)); + + Widget _buildContentColumnWrapper(TermsAndConditionsViewModel viewModel) => + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildMenuColumnScrollView(viewModel), + ); + + Widget _buildMenuColumnScrollView(TermsAndConditionsViewModel viewModel) => + SingleChildScrollView( + child: _buildContentColumn(), + ); + + Widget _buildContentColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentColumnChildren(), + ); + + List _buildContentColumnChildren() => [ + _buildContent(), + verticalSpaceMedium, + _buildDownloadButtonWrapper(), + ]; + + Widget _buildContent() => Html( + data: ksTerms, + shrinkWrap: true, + style: htmlStyle, + ); + + Widget _buildDownloadButtonWrapper() => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildDownloadButton(), + ); + + Widget _buildDownloadButton() => const CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Download PDF', + leadingIcon: Icons.download, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_viewmodel.dart new file mode 100644 index 0000000..589a058 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_viewmodel.dart @@ -0,0 +1,11 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class TermsAndConditionsViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Navigation + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/birthday_selector.dart b/StudioProjects/yimaru_app/lib/ui/widgets/birthday_selector.dart new file mode 100644 index 0000000..8aa15e8 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/birthday_selector.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/profile_detail/profile_detail_viewmodel.dart'; +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'package:omni_datetime_picker/omni_datetime_picker.dart'; + +class BirthdaySelector extends ViewModelWidget { + const BirthdaySelector({super.key}); + + DateTime _initialDate(ProfileDetailViewModel viewModel) { + try { + final parsedDate = format.parse(viewModel.selectedBirthday ?? ''); + return parsedDate.isAfter(DateTime.now()) ? DateTime.now() : parsedDate; + } catch (_) { + return DateTime.now(); + } + } + + Future _pickDateTime( + {required BuildContext context, + required ProfileDetailViewModel viewModel}) async { + DateTime? dateTime = await showOmniDateTimePicker( + context: context, + is24HourMode: false, + isShowSeconds: false, + lastDate: DateTime.now(), + firstDate: DateTime(1900), + barrierDismissible: true, + titleSeparator: const Divider(), + padding: const EdgeInsets.all(16), + type: OmniDateTimePickerType.date, + initialDate: _initialDate(viewModel), + borderRadius: const BorderRadius.all(Radius.circular(15)), + insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24), + title: const Text('Birthday', style: TextStyle(fontSize: 16)), + theme: ThemeData( + colorScheme: + const ColorScheme.light().copyWith(primary: kcPrimaryColor), + ), + ); + + if (dateTime != null) { + String formattedDateTime = DateFormat('d MMM, yyyy').format(dateTime); + + viewModel.setBirthday(DateFormat('d MMM, yyyy').format(dateTime)); + //onChanged(formattedDateTime); + } + } + + @override + Widget build(BuildContext context, ProfileDetailViewModel viewModel) => + _buildButtonWrapper(context: context, viewModel: viewModel); + + Widget _buildButtonWrapper( + {required BuildContext context, + required ProfileDetailViewModel viewModel}) => + Container( + height: 50, + width: double.maxFinite, + margin: const EdgeInsets.only(bottom: 15), + child: _buildContainerWrapper(context: context, viewModel: viewModel), + ); + + Widget _buildContainerWrapper( + {required BuildContext context, + required ProfileDetailViewModel viewModel}) => + GestureDetector( + onTap: () async => + await _pickDateTime(context: context, viewModel: viewModel), + child: _buildContainer(viewModel), + ); + + Widget _buildContainer(ProfileDetailViewModel viewModel) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: kcPrimaryColor.withOpacity(0.1), + border: Border.all(color: kcPrimaryColor), + ), + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildButtonRowWrapper(viewModel), + ); + + Widget _buildButtonRowWrapper(ProfileDetailViewModel viewModel) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildButtonRowChildren(viewModel), + ); + + List _buildButtonRowChildren(ProfileDetailViewModel viewModel) => + [_buildText(viewModel), _buildIcon()]; + + Widget _buildText(ProfileDetailViewModel viewModel) => Text( + viewModel.selectedBirthday ?? 'Pick birthday', + style: const TextStyle(color: kcDarkGrey), + ); + + Widget _buildIcon() => const Icon( + Icons.calendar_month, + color: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/circular_icon.dart b/StudioProjects/yimaru_app/lib/ui/widgets/circular_icon.dart new file mode 100644 index 0000000..f1241a3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/circular_icon.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class CircularIcon extends StatelessWidget { + final Color color; + final double size; + final IconData icon; + const CircularIcon( + {super.key, required this.icon, required this.size, required this.color}); + + @override + Widget build(BuildContext context) => _buildIconWrapper(); + + Widget _buildIconWrapper() => CircleAvatar( + radius: size, + backgroundColor: color.withOpacity(0.25), + child: _buildIcon(), + ); + + Widget _buildIcon() => Icon( + icon, + size: size, + color: color, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/coming_soon.dart b/StudioProjects/yimaru_app/lib/ui/widgets/coming_soon.dart new file mode 100644 index 0000000..3c5eb35 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/coming_soon.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class ComingSoon extends StatelessWidget { + const ComingSoon({super.key}); + + @override + Widget build(BuildContext context) => _buildScaffoldWrapper(); + + Widget _buildScaffoldWrapper() => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(), + ); + + Widget _buildScaffold() => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBodyWrapper(), + ); + + Widget _buildBodyWrapper() => Center( + child: _buildBody(), + ); + + Widget _buildBody() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildBodyChildren(), + ); + + List _buildBodyChildren() => [ + verticalSpaceLarge, + _buildIcon(), + verticalSpaceSmall, + _buildTitle(), + ]; + + Widget _buildIcon() => Image.asset('assets/images/coming_soon.png'); + + Widget _buildTitle() => const Text( + 'Launching Very Soon!', + style: TextStyle( + fontSize: 22, + color: kcMediumGrey, + fontWeight: FontWeight.w700, + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/course_level_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/course_level_card.dart new file mode 100644 index 0000000..b3e1074 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/course_level_card.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:yimaru_app/ui/widgets/progress_status.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; + +class CourseLevelCard extends StatelessWidget { + final Color color; + final String icon; + final String title; + final String status; + final String subTitle; + final bool isCompleted; + final GestureTapCallback? onTap; + + const CourseLevelCard({ + super.key, + this.onTap, + required this.icon, + required this.title, + required this.color, + required this.status, + required this.subTitle, + required this.isCompleted, + }); + + @override + Widget build(BuildContext context) => _buildContainerWrapper(); + + Widget _buildContainerWrapper() => GestureDetector( + onTap: onTap, + child: _buildContainer(), + ); + + Widget _buildContainer() => Container( + width: 200, + padding: const EdgeInsets.all(15), + margin: const EdgeInsets.only(left: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => [ + _buildIconSection(), + verticalSpaceSmall, + _buildTitle(), + verticalSpaceSmall, + _buildSubTitle(), + verticalSpaceSmall, + _buildActionButton() + ]; + + Widget _buildIconSection() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildIconSectionChildren(), + ); + + List _buildIconSectionChildren() => + [_buildIcon(), _buildProgressStatus()]; + + Widget _buildIcon() => SvgPicture.asset( + icon, + height: 50, + width: 50, + ); + + Widget _buildProgressStatus() => ProgressStatus( + color: color, + status: status, + ); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => Expanded( + child: Text( + subTitle, + maxLines: 3, + style: const TextStyle(color: kcMediumGrey), + ), + ); + + Widget _buildActionButton() => CustomElevatedButton( + height: 15, + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + text: isCompleted ? 'Review Course' : 'Continue Learning', + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/course_progress_section.dart b/StudioProjects/yimaru_app/lib/ui/widgets/course_progress_section.dart new file mode 100644 index 0000000..ca54b1e --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/course_progress_section.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'custom_column.dart'; +import 'custom_elevated_button.dart'; + +class CourseProgressSection extends ViewModelWidget { + const CourseProgressSection({super.key}); + + @override + Widget build(BuildContext context, OngoingProgressViewModel viewModel) => + _buildContainer(viewModel); + + Widget _buildContainer(OngoingProgressViewModel viewModel) => Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(OngoingProgressViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(OngoingProgressViewModel viewModel) => [ + _buildIcon(), + verticalSpaceSmall, + _buildTitle('Course'), + verticalSpaceMedium, + _buildExpansionTileWrapper(viewModel) + ]; + + Widget _buildIcon() => const Icon( + Icons.book, + size: 50, + color: kcPrimaryColor, + ); + + Widget _buildExpansionTileWrapper(OngoingProgressViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + children: viewModel.courses + .map( + (course) => _buildExpansionTileCard(course['title']), + ) + .toList(), + ); + + Widget _buildExpansionTileCard(String title) => Padding( + padding: const EdgeInsets.only(bottom: 15), + child: _buildExpansionTile(title), + ); + + Widget _buildExpansionTile(String title) => ExpansionTile( + showTrailingIcon: true, + initiallyExpanded: true, + title: _buildTitle(title), + iconColor: kcDarkGrey, + textColor: kcDarkGrey, + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + subtitle: _buildProgressIndicator(), + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.all(15), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + shape: Border.all(color: kcPrimaryColor.withOpacity(0.2)), + children: _buildExpansionTileChildren(), + ); + + List _buildExpansionTileChildren() => [_buildTileWrapper()]; + + Widget _buildTitle(String title) => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildTileWrapper() => Column( + mainAxisSize: MainAxisSize.min, + children: _buildTileChildren(), + ); + + List _buildTileChildren() => + [_buildLearningStatus(), verticalSpaceSmall, _buildActionButton()]; + + Widget _buildProgressIndicator() => const CustomLinearProgressIndicator( + progress: 0.5, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + ); + + Widget _buildLearningStatus() => Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: _buildLearningStatusChildren(), + ); + + List _buildLearningStatusChildren() => [ + horizontalSpaceMedium, + _buildWatchedVideos(), + horizontalSpaceSmall, + _buildCompletedPractices(), + horizontalSpaceMedium, + ]; + + Widget _buildWatchedVideos() => + const CustomColumn(title: '15/25', subtitle: 'Videos'); + + Widget _buildCompletedPractices() => + const CustomColumn(title: '8/12', subtitle: 'Practices'); + + Widget _buildActionButton() => const CustomElevatedButton( + height: 15, + borderRadius: 12, + text: 'Continue Course', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_back_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_back_button.dart new file mode 100644 index 0000000..5e045fc --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_back_button.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class CustomBackButton extends StatelessWidget { + final GestureTapCallback? onTap; + const CustomBackButton({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildBackButtonWrapper(); + + Widget _buildBackButtonWrapper() => GestureDetector( + onTap: onTap, + child: _buildBackIcon(), + ); + + Widget _buildBackIcon() => const Icon( + Icons.arrow_back, + color: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_circular_progress_indicator.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_circular_progress_indicator.dart new file mode 100644 index 0000000..946ea16 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_circular_progress_indicator.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; + +class CustomCircularProgressIndicator extends StatelessWidget { + final Color color; + const CustomCircularProgressIndicator({super.key, required this.color}); + + @override + Widget build(BuildContext context) => _buildIndicator(); + + Widget _buildIndicator() => CircularProgressIndicator( + color: color, + strokeWidth: 6, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_column.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_column.dart new file mode 100644 index 0000000..bb7488d --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_column.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class CustomColumn extends StatelessWidget { + final String title; + final String subtitle; + const CustomColumn({super.key, required this.title, required this.subtitle}); + + @override + Widget build(BuildContext context) => _buildColumn(); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => + [_buildTitle(), verticalSpaceTiny, _buildSubtitle()]; + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + Widget _buildSubtitle() => Text( + subtitle, + maxLines: 1, + softWrap: false, + style: const TextStyle(color: kcMediumGrey), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_cursor.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_cursor.dart new file mode 100644 index 0000000..358e817 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_cursor.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class CustomCursor extends StatelessWidget { + const CustomCursor({super.key}); + + @override + Widget build(BuildContext context) => _buildCursor(); + + Widget _buildCursor() => Column( + mainAxisAlignment: MainAxisAlignment.end, + children: _buildPinIndicator(), + ); + + List _buildPinIndicator() => [ + Container( + width: 25, + height: 1, + color: kcPrimaryColor, + margin: const EdgeInsets.only(bottom: 9), + ), + ]; +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_dropdown.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_dropdown.dart index ec6edc2..be63f7c 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/custom_dropdown.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_dropdown.dart @@ -5,17 +5,17 @@ import 'package:flutter/material.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; -class CustomDropDownPicker extends StatelessWidget { - final Icon icon; +class CustomDropdownPicker extends StatelessWidget { + final Icon? icon; final String hint; final String selectedItem; final void Function(String?)? onChanged; final FutureOr> Function(String value, LoadProps? props)? items; - const CustomDropDownPicker( + const CustomDropdownPicker( {super.key, + this.icon, required this.hint, - required this.icon, required this.items, required this.onChanged, required this.selectedItem}); @@ -61,7 +61,6 @@ class CustomDropDownPicker extends StatelessWidget { InputDecoration _popUpDecoration() => InputDecoration( filled: true, errorBorder: searchBorder, - prefix: _buildPrefixIcon(), focusedBorder: searchBorder, enabledBorder: searchBorder, disabledBorder: searchBorder, @@ -70,7 +69,8 @@ class CustomDropDownPicker extends StatelessWidget { fontSize: 14, color: kcLightGrey, ), - fillColor: kcPrimaryColor.withOpacity(0.2), + fillColor: kcPrimaryColor.withOpacity(0.1), + prefix: icon != null ? _buildPrefixIcon() : null, contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 0), ); @@ -82,7 +82,7 @@ class CustomDropDownPicker extends StatelessWidget { Widget _buildPopupProsBuilder(String value) => Text( value, maxLines: 1, - style: const TextStyle(color: kcDarkGreyColor, fontSize: 14), + style: const TextStyle(color: kcDarkGrey, fontSize: 14), ); DropDownDecoratorProps _dropDownDecoratorProps() => DropDownDecoratorProps( @@ -103,7 +103,7 @@ class CustomDropDownPicker extends StatelessWidget { fontSize: 14, color: kcLightGrey, ), - fillColor: kcPrimaryColor.withOpacity(0.2), + fillColor: kcPrimaryColor.withOpacity(0.1), contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), ); @@ -121,7 +121,7 @@ class CustomDropDownPicker extends StatelessWidget { maxLines: 1, style: const TextStyle( fontSize: 14, - color: kcDarkGreyColor, + color: kcDarkGrey, ), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_elevated_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_elevated_button.dart index 113833a..b44e509 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/custom_elevated_button.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_elevated_button.dart @@ -1,22 +1,30 @@ import 'package:flutter/material.dart'; class CustomElevatedButton extends StatelessWidget { - final bool icon; + final bool safe; final String text; final double width; final double height; final Color? borderColor; final double borderRadius; + final String? leadingImage; + final IconData? leadingIcon; final Color backgroundColor; final Color foregroundColor; + final String? trailingImage; + final IconData? trailingIcon; final GestureTapCallback? onTap; const CustomElevatedButton({ super.key, this.onTap, + this.leadingIcon, this.borderColor, - this.icon = false, + this.safe = true, + this.leadingImage, + this.trailingIcon, required this.text, + this.trailingImage, required this.height, this.borderRadius = 0, required this.backgroundColor, @@ -25,7 +33,10 @@ class CustomElevatedButton extends StatelessWidget { }); @override - Widget build(BuildContext context) => _buildButtonWrapper(); + Widget build(BuildContext context) => _buildSafeWrapper(); + + Widget _buildSafeWrapper() => + SafeArea(bottom: safe, child: _buildButtonWrapper()); Widget _buildButtonWrapper() => SizedBox(height: 50, width: width, child: _buildButton()); @@ -48,16 +59,39 @@ class CustomElevatedButton extends StatelessWidget { children: _buildRowChildren(), ); - List _buildRowChildren() => - [_buildText(), const SizedBox(width: 5), if (icon) _buildIcon()]; + List _buildRowChildren() => [ + if (leadingIcon != null) _buildIcon(leadingIcon!), + if (leadingImage != null) _buildImage(leadingImage!), + if (leadingIcon != null || leadingImage != null) + const SizedBox(width: 5), + leadingIcon == null && + trailingIcon == null && + leadingImage == null && + trailingImage == null + ? _buildExpandedText() + : _buildText(), + if (trailingIcon != null || trailingImage != null) + const SizedBox(width: 5), + if (trailingIcon != null) _buildIcon(trailingIcon!), + if (trailingImage != null) _buildImage(trailingImage!), + ]; - Widget _buildIcon() => Icon( - Icons.arrow_forward, + Widget _buildIcon(IconData icon) => Icon( + icon, color: foregroundColor, ); + Widget _buildImage(String image) => Image.asset(image); + + Widget _buildExpandedText() => Expanded( + child: _buildText(), + ); + Widget _buildText() => Text( text, + maxLines: 1, + softWrap: false, + textAlign: TextAlign.center, style: TextStyle(color: foregroundColor, fontWeight: FontWeight.bold), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_form_label.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_form_label.dart new file mode 100644 index 0000000..2b1142c --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_form_label.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class CustomFormLabel extends StatelessWidget { + final String label; + final TextStyle style; + + const CustomFormLabel({super.key, required this.label, required this.style}); + + @override + Widget build(BuildContext context) => _buildLabel(); + + Widget _buildLabel() => Text( + label, + style: style, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_large_radio_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_large_radio_button.dart index d62b74f..8a7bbd7 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/custom_large_radio_button.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_large_radio_button.dart @@ -35,7 +35,7 @@ class CustomLargeRadioButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), - color: selected ? kcPrimaryColor.withOpacity(0.2) : kcWhiteColor, + color: selected ? kcPrimaryColor.withOpacity(0.1) : kcWhite, border: Border.all( color: selected ? kcPrimaryColor : kcPrimaryColor.withOpacity(0.75), ), @@ -69,7 +69,7 @@ class CustomLargeRadioButton extends StatelessWidget { title, style: const TextStyle( fontSize: 18, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w500, ), ); diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_linear_progress_indicator.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_linear_progress_indicator.dart new file mode 100644 index 0000000..6651618 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_linear_progress_indicator.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class CustomLinearProgressIndicator extends StatelessWidget { + final double progress; + final Color activeColor; + final Color backgroundColor; + + const CustomLinearProgressIndicator( + {super.key, + required this.progress, + required this.activeColor, + required this.backgroundColor}); + + @override + Widget build(BuildContext context) => _buildProgressIndicatorWrapper(); + + Widget _buildProgressIndicatorWrapper() => SizedBox( + height: 5, + width: double.maxFinite, + child: _buildProgressIndicatorClipper(), + ); + + Widget _buildProgressIndicatorClipper() => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: _buildProgressIndicator(), + ); + + Widget _buildProgressIndicator() => LinearProgressIndicator( + value: progress, + color: activeColor, + backgroundColor: backgroundColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_list_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_list_tile.dart new file mode 100644 index 0000000..c345915 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_list_tile.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; + +class CustomListTile extends StatelessWidget { + final String title; + final IconData icon; + final String? language; + final bool? isLanguage; + final GestureTapCallback? onTap; + + const CustomListTile({ + super.key, + this.onTap, + this.language, + this.isLanguage, + required this.icon, + required this.title, + }); + + @override + Widget build(BuildContext context) => _buildLitTile(); + + Widget _buildLitTile() => ListTile( + onTap: onTap, + title: _buildTitle(), + leading: _buildLeading(), + trailing: _buildTrailing(), + ); + + Widget _buildLeading() => Icon( + icon, + color: kcPrimaryColor, + ); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + color: kcDarkGrey, + ), + ); + + Widget _buildTrailing() => + isLanguage != null ? _buildTrailingRow() : _buildTrailingIcon(); + + Widget _buildTrailingRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildTrailingChildren(), + ); + + List _buildTrailingChildren() => [ + if (language != null) _buildTrailingText(), + if (language != null) horizontalSpaceSmall, + _buildTrailingIcon() + ]; + + Widget _buildTrailingText() => Text( + language ?? '', + style: const TextStyle( + fontSize: 12, + color: kcDarkGrey, + ), + ); + + Widget _buildTrailingIcon() => const Icon( + Icons.keyboard_arrow_right, + color: kcLightGrey, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_small_radio_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_small_radio_button.dart index aad8769..96fc29f 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/custom_small_radio_button.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_small_radio_button.dart @@ -28,7 +28,7 @@ class CustomSmallRadioButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), - color: selected ? kcPrimaryColor.withOpacity(0.2) : kcWhiteColor, + color: selected ? kcPrimaryColor.withOpacity(0.1) : kcWhite, border: Border.all( color: selected ? kcPrimaryColor : kcPrimaryColor.withOpacity(0.75), ), @@ -46,7 +46,7 @@ class CustomSmallRadioButton extends StatelessWidget { Widget _buildText() => Text( title, - style: const TextStyle(color: kcDarkGreyColor), + style: const TextStyle(color: kcDarkGrey), ); Widget _buildIcon() => const Icon( diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/download_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/download_card.dart new file mode 100644 index 0000000..76c0ef3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/download_card.dart @@ -0,0 +1,149 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import '../common/app_colors.dart'; +import 'custom_elevated_button.dart'; + +class DownloadCard extends StatelessWidget { + final String size; + final String title; + final String duration; + final String thumbnail; + + const DownloadCard( + {super.key, + required this.size, + required this.title, + required this.thumbnail, + required this.duration}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + height: 75, + width: double.maxFinite, + padding: const EdgeInsets.all(15), + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildRow(), + ); + + Widget _buildRow() => Row( + children: [ + _buildLeadingWrapper(), + const SizedBox(width: 10), + _buildCourseInfo(), + const SizedBox(width: 10), + _buildRemoveButtonWrapper(), + ], + ); + + Widget _buildLeadingWrapper() => SizedBox( + width: 50, + height: double.maxFinite, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: _buildLeadingStack(), + ), + ); + + Widget _buildLeadingStack() => Stack( + alignment: Alignment.center, + children: [_buildImageWrapper(), _buildPlayButtonWrapper()], + ); + + Widget _buildImageWrapper() => + Align(alignment: Alignment.center, child: _buildImage()); + + Widget _buildImage() => Image.asset( + thumbnail, + fit: BoxFit.fill, + width: double.maxFinite, + ); + + Widget _buildPlayButtonWrapper() => Align( + alignment: Alignment.center, + child: _buildPlayButton(), + ); + + Widget _buildPlayButton() => CircleAvatar( + radius: 14, + backgroundColor: kcTransparent, + child: _buildPlayIconClipper(), + ); + + Widget _buildPlayIconClipper() => ClipRRect( + borderRadius: BorderRadius.circular(50), + child: _buildPlayIconBlender(), + ); + + Widget _buildPlayIconBlender() => BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: _buildPlayIcon(), + ); + + Widget _buildPlayIcon() => const Icon( + Icons.play_arrow, + color: kcWhite, + ); + + Widget _buildCourseInfo() => Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildCourseInfoChildren(), + ), + ); + + List _buildCourseInfoChildren() => [_buildTitle(), _buildMiddleRow()]; + + Widget _buildTitle() => Text( + title, + style: style16DG600, + ); + + Widget _buildMiddleRow() => Row( + children: [ + _buildSize(), + const SizedBox(width: 10), + _buildDot(), + const SizedBox(width: 10), + _buildDuration() + ], + ); + + Widget _buildDuration() => Text( + duration, + style: style14P400, + ); + + Widget _buildDot() => Text( + '-', + style: style14P400, + ); + + Widget _buildSize() => Text( + size, + style: style14P400, + ); + + Widget _buildRemoveButtonWrapper() => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: _buildRemoveButton(), + ); + + Widget _buildRemoveButton() => CustomElevatedButton( + width: 110, + height: 15, + text: 'Remove', + borderRadius: 12, + foregroundColor: kcRed, + backgroundColor: kcRed.withOpacity(0.25), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/language_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/language_button.dart index ba30dcc..53a4bac 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/language_button.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/language_button.dart @@ -20,12 +20,12 @@ class LanguageButton extends StatelessWidget { decoration: BoxDecoration( color: kcPrimaryColor, borderRadius: BorderRadius.circular(10), - border: Border.all(color: kcWhiteColor)), + border: Border.all(color: kcWhite)), child: _buildLanguage(), ); Widget _buildLanguage() => Text( language, - style: const TextStyle(color: kcWhiteColor), + style: const TextStyle(color: kcWhite), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/large_app_bar.dart b/StudioProjects/yimaru_app/lib/ui/widgets/large_app_bar.dart new file mode 100644 index 0000000..16ba72b --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/large_app_bar.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/widgets/language_button.dart'; + +class LargeAppBar extends StatelessWidget { + final bool showBackButton; + final GestureTapCallback? onTap; + final GestureTapCallback? onPop; + final bool showLanguageSelection; + final GestureTapCallback? onLanguage; + + const LargeAppBar( + {super.key, + this.onTap, + this.onPop, + this.onLanguage, + required this.showBackButton, + required this.showLanguageSelection}); + + @override + Widget build(BuildContext context) => _buildAppBarWrapper(); + + Widget _buildAppBarWrapper() => Container( + height: 125, + width: double.maxFinite, + alignment: Alignment.bottomCenter, + decoration: const BoxDecoration( + color: kcPrimaryColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + padding: const EdgeInsets.only(bottom: 25, right: 15), + child: _buildAppBarItems(), + ); + + Widget _buildAppBarItems() => Stack( + children: _buildAppBarItemChildren(), + ); + + List _buildAppBarItemChildren() => + [if (showBackButton) _buildBackButtonWrapper(), _buildRightButton()]; + + Widget _buildBackButtonWrapper() => Align( + alignment: Alignment.bottomLeft, + child: _buildBackButton(), + ); + + Widget _buildBackButton() => BackButton( + onPressed: onTap, + style: + const ButtonStyle(foregroundColor: WidgetStatePropertyAll(kcWhite)), + ); + + Widget _buildRightButton() => Align( + alignment: Alignment.bottomRight, + child: showLanguageSelection + ? _buildLanguageSelector() + : _buildCloseButton()); + + Widget _buildLanguageSelector() => LanguageButton( + language: 'EN', + onTap: onLanguage, + ); + + Widget _buildCloseButton() => IconButton( + onPressed: () {}, + icon: _buildCloseIcon(), + ); + + Widget _buildCloseIcon() => const Icon( + Icons.close, + color: kcWhite, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learn_app_bar.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learn_app_bar.dart new file mode 100644 index 0000000..e3e331c --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learn_app_bar.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import '../common/app_colors.dart'; + +class LearnAppBar extends StatelessWidget { + const LearnAppBar({super.key}); + + @override + Widget build(BuildContext context) => _buildStack(); + + Widget _buildStack() => Stack( + alignment: Alignment.center, + children: _buildStackChildren(), + ); + + List _buildStackChildren() => + [_buildProfileWrapper(), _buildNotificationIconWrapper()]; + + Widget _buildProfileWrapper() => Align( + alignment: Alignment.centerLeft, + child: _buildProfileRow(), + ); + + Widget _buildProfileRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildProfileRowChildren(), + ); + + List _buildProfileRowChildren() => + [_buildProfileImage(), horizontalSpaceSmall, _buildGreetingTextColumn()]; + + Widget _buildProfileImage() => const CircleAvatar( + radius: 25, + backgroundImage: AssetImage('assets/images/profile.png'), + ); + + Widget _buildGreetingTextColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildGreetingChildren(), + ); + + List _buildGreetingChildren() => + [_buildGreetingTitle(), _buildSubTitle()]; + + Widget _buildGreetingTitle() => const Text.rich( + TextSpan( + text: 'Hello,', + style: TextStyle( + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + children: [ + TextSpan( + text: ' Bisrat!', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ) + ]), + ); + + Widget _buildSubTitle() => const Text( + 'Ready to keep learning English today?', + textAlign: TextAlign.center, + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildNotificationIconWrapper() => + Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon()); + + Widget _buildNotificationIcon() => const Icon( + Icons.notifications_none, + color: kcDarkGrey, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learn_level_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learn_level_tile.dart new file mode 100644 index 0000000..d858595 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learn_level_tile.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/views/learn/learn_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/progress_status.dart'; + +import '../common/app_colors.dart'; +import 'custom_elevated_button.dart'; + +class LearnLevelTile extends ViewModelWidget { + final String title; + final String subtitle; + final LearnLevelStatus status; + + const LearnLevelTile({ + super.key, + required this.title, + required this.status, + required this.subtitle, + }); + + @override + Widget build(BuildContext context, LearnViewModel viewModel) => + _buildExpansionTileCard(viewModel); + + Widget _buildExpansionTileCard(LearnViewModel viewModel) => Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all( + color: status == LearnLevelStatus.started + ? kcPrimaryColor.withOpacity(0.2) + : kcVeryLightGrey), + ), + child: _buildExpansionTile(viewModel), + ); + + Widget _buildExpansionTile(LearnViewModel viewModel) => ExpansionTile( + textColor: kcDarkGrey, + title: _buildTitleRow(), + subtitle: _buildContent(), + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + shape: Border.all(color: kcTransparent), + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.all(15), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + backgroundColor: status != LearnLevelStatus.pending + ? kcPrimaryColor.withOpacity(0.1) + : kcBackgroundColor, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + enabled: status != LearnLevelStatus.pending ? true : false, + collapsedBackgroundColor: status != LearnLevelStatus.pending + ? kcPrimaryColor.withOpacity(0.1) + : kcBackgroundColor, + showTrailingIcon: status != LearnLevelStatus.pending ? true : false, + initiallyExpanded: status == LearnLevelStatus.started ? true : false, + children: _buildExpansionTileChildren(viewModel), + ); + + List _buildExpansionTileChildren(LearnViewModel viewModel) => + [_buildActionButton(viewModel)]; + + Widget _buildTitleRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildTitleChildren(), + ); + + List _buildTitleChildren() => [ + _buildTitle(), + if (status != LearnLevelStatus.pending) horizontalSpaceSmall, + if (status != LearnLevelStatus.pending) _buildProgressStatus() + ]; + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgressStatus() => ProgressStatus( + status: status.name.substring(0, 1).toUpperCase() + + status.name.substring(1, status.name.length), + color: kcPrimaryColor, + ); + + Widget _buildContent() => Text( + subtitle, + style: const TextStyle( + color: kcDarkGrey, + ), + ); + + Widget _buildActionButton(LearnViewModel viewModel) => CustomElevatedButton( + height: 15, + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + text: status == LearnLevelStatus.completed + ? 'Review Course' + : status == LearnLevelStatus.pending + ? 'Start Learning' + : 'Continue Learning', + onTap: () async => await viewModel.navigateToLearnLevel(), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learn_module_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learn_module_tile.dart new file mode 100644 index 0000000..679aec9 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learn_module_tile.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:iconsax/iconsax.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; + +import '../common/app_colors.dart'; +import '../common/enmus.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; + +class LearnModuleTile extends ViewModelWidget { + final String title; + final String subtitle; + final LearnLevelStatus status; + + const LearnModuleTile({ + super.key, + required this.title, + required this.status, + required this.subtitle, + }); + + IconData _getIcon() { + if (title.contains('Module 1')) { + return Iconsax.cake; + } else if (title.contains('Module 2')) { + return Icons.all_inbox; + } else if (title.contains('Module 3')) { + return Icons.lightbulb_outline; + } else if (title.contains('Module 4')) { + return Icons.search; + } else { + return Iconsax.pen_add; + } + } + + @override + Widget build(BuildContext context, LearnModuleViewModel viewModel) => + _buildExpansionTileCard(viewModel); + + Widget _buildExpansionTileCard(LearnModuleViewModel viewModel) => Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all(color: kcVeryLightGrey), + ), + child: _buildTileStack(viewModel), + ); + + Widget _buildTileStack(LearnModuleViewModel viewModel) => Stack( + children: [ + _buildExpansionTile(viewModel), + _buildContainerShaderWrapper() + ], + ); + + Widget _buildExpansionTile(LearnModuleViewModel viewModel) => ExpansionTile( + textColor: kcDarkGrey, + title: _buildTitle(), + subtitle: _buildContent(), + leading: _buildIconWrapper(), + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + backgroundColor: kcBackgroundColor, + shape: Border.all(color: kcTransparent), + expandedAlignment: Alignment.centerLeft, + collapsedBackgroundColor: kcBackgroundColor, + enabled: status != LearnLevelStatus.pending, + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + childrenPadding: const EdgeInsets.fromLTRB(70, 15, 15, 15), + showTrailingIcon: status != LearnLevelStatus.pending ? true : false, + initiallyExpanded: status == LearnLevelStatus.started ? true : false, + children: _buildExpansionTileChildren(viewModel), + ); + + Widget _buildIconWrapper() => CircleAvatar( + backgroundColor: kcPrimaryColor.withOpacity(0.1), + child: _buildIcon(), + ); + + Widget _buildIcon() => Icon( + _getIcon(), + color: kcPrimaryColor, + ); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildContent() => Text( + subtitle, + style: const TextStyle( + color: kcDarkGrey, + ), + ); + + List _buildExpansionTileChildren(LearnModuleViewModel viewModel) => + [_buildExpansionTileItem(viewModel)]; + + Widget _buildExpansionTileItem(LearnModuleViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildExpansionTileItemChildren(viewModel), + ); + + List _buildExpansionTileItemChildren( + LearnModuleViewModel viewModel) => + [ + _buildProgressRow(), + verticalSpaceSmall, + _buildActionButtonWrapper(viewModel) + ]; + + Widget _buildProgressRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildProgressChildren(), + ); + + List _buildProgressChildren() => + [_buildProgressStatusWrapper(), horizontalSpaceSmall, _buildProgress()]; + + Widget _buildProgressStatusWrapper() => Expanded( + child: _buildProgressStatus(), + ); + + Widget _buildProgressStatus() => const CustomLinearProgressIndicator( + progress: 0.75, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey); + + Widget _buildProgress() => const Text( + '2/3', + style: TextStyle(color: kcDarkGrey), + ); + + Widget _buildActionButtonWrapper(LearnModuleViewModel viewModel) => SizedBox( + height: 40, + child: _buildActionButtons(viewModel), + ); + + Widget _buildActionButtons(LearnModuleViewModel viewModel) => Row( + children: [ + _buildLessonButtonWrapper(viewModel), + horizontalSpaceSmall, + _buildPracticeButtonWrapper(viewModel) + ], + ); + + Widget _buildLessonButtonWrapper(LearnModuleViewModel viewModel) => Expanded( + child: _buildLessonButton(viewModel), + ); + + Widget _buildLessonButton(LearnModuleViewModel viewModel) => + const CustomElevatedButton( + height: 15, + borderRadius: 12, + text: 'View Lessons', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + //onTap: () async => await viewModel.navigateToLearnModule(), + ); + + Widget _buildPracticeButtonWrapper(LearnModuleViewModel viewModel) => + Expanded( + child: _buildPracticeButton(viewModel), + ); + + Widget _buildPracticeButton(LearnModuleViewModel viewModel) => + const CustomElevatedButton( + height: 15, + borderRadius: 12, + text: 'View Practices', + backgroundColor: kcWhite, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + // onTap: () async => await viewModel.navigateToLearnLevel(), + ); + + Widget _buildContainerShaderWrapper() => Positioned.fill( + child: _buildContainerShader(), + ); + + Widget _buildContainerShader() => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all(color: kcWhite.withOpacity(0.75)), + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learn_sub_level_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learn_sub_level_tile.dart new file mode 100644 index 0000000..b50e48a --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learn_sub_level_tile.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/learn_level/learn_level_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/progress_status.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; + +class LearnSubLevelTile extends ViewModelWidget { + final bool current; + final String title; + final String subtitle; + + const LearnSubLevelTile({ + super.key, + required this.title, + required this.current, + required this.subtitle, + }); + + @override + Widget build(BuildContext context, LearnLevelViewModel viewModel) => + _buildExpansionTileCard(viewModel); + + Widget _buildExpansionTileCard(LearnLevelViewModel viewModel) => Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all( + color: + current ? kcPrimaryColor.withOpacity(0.2) : kcVeryLightGrey), + ), + child: _buildExpansionTile(viewModel), + ); + + Widget _buildExpansionTile(LearnLevelViewModel viewModel) => ExpansionTile( + enabled: current, + textColor: kcDarkGrey, + title: _buildTitleRow(), + showTrailingIcon: current, + subtitle: _buildContent(), + initiallyExpanded: current, + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + shape: Border.all(color: kcTransparent), + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.all(15), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + backgroundColor: + current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + collapsedBackgroundColor: + current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor, + children: _buildExpansionTileChildren(viewModel), + ); + + List _buildExpansionTileChildren(LearnLevelViewModel viewModel) => + [_buildActionButtonWrapper(viewModel)]; + + Widget _buildTitleRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildTitleChildren(), + ); + + List _buildTitleChildren() => [ + _buildTitle(), + if (current) horizontalSpaceSmall, + if (current) _buildProgressStatus() + ]; + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgressStatus() => const ProgressStatus( + color: kcPrimaryColor, + status: 'Current Level', + ); + + Widget _buildContent() => Text( + subtitle, + style: const TextStyle( + color: kcDarkGrey, + ), + ); + + Widget _buildActionButtonWrapper(LearnLevelViewModel viewModel) => SizedBox( + height: 40, + child: _buildActionButtons(viewModel), + ); + + Widget _buildActionButtons(LearnLevelViewModel viewModel) => Row( + children: [ + _buildViewButtonWrapper(viewModel), + horizontalSpaceSmall, + _buildPracticeButtonWrapper(viewModel) + ], + ); + + Widget _buildViewButtonWrapper(LearnLevelViewModel viewModel) => Expanded( + child: _buildViewButton(viewModel), + ); + + Widget _buildViewButton(LearnLevelViewModel viewModel) => + CustomElevatedButton( + height: 15, + borderRadius: 12, + text: 'View Course', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + onTap: () async => await viewModel.navigateToLearnModule(), + ); + + Widget _buildPracticeButtonWrapper(LearnLevelViewModel viewModel) => Expanded( + child: _buildPracticeButton(viewModel), + ); + + Widget _buildPracticeButton(LearnLevelViewModel viewModel) => + const CustomElevatedButton( + height: 15, + text: 'Practice', + borderRadius: 12, + backgroundColor: kcWhite, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + + // onTap: () async => await viewModel.navigateToLearnLevel(), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learning_progress_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learning_progress_card.dart new file mode 100644 index 0000000..f1a5330 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learning_progress_card.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/widgets/custom_column.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; +import 'custom_linear_progress_indicator.dart'; + +class LearningProgressCard extends StatelessWidget { + final GestureTapCallback? onTap; + + const LearningProgressCard({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildContainerWrapper(); + + Widget _buildContainerWrapper() => GestureDetector( + onTap: onTap, + child: _buildContainer(), + ); + + Widget _buildContainer() => Container( + height: 320, + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => [ + _buildIcon(), + verticalSpaceSmall, + _buildTitle(), + verticalSpaceTiny, + _buildSubtitle(), + verticalSpaceSmall, + _buildProgressIndicator(), + verticalSpaceSmall, + _buildLearningStatus(), + verticalSpaceMedium, + _buildActionButton(), + ]; + + Widget _buildIcon() => const Icon( + Icons.menu_book_rounded, + size: 50, + color: kcPrimaryColor, + ); + + Widget _buildTitle() => const Text( + 'Learn English', + style: TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubtitle() => const Text( + 'Great job! Keep the momentum.', + maxLines: 2, + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildProgressIndicator() => const CustomLinearProgressIndicator( + progress: 0.5, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + ); + + Widget _buildLearningStatus() => Row( + children: _buildLearningStatusChildren(), + ); + + List _buildLearningStatusChildren() => [ + _buildWatchedVideos(), + horizontalSpaceSmall, + _buildCompletedPractices(), + horizontalSpaceSmall, + _buildTakenQuizzes() + ]; + + Widget _buildWatchedVideos() => const Expanded( + child: CustomColumn(title: '120', subtitle: 'Videos Watched')); + Widget _buildCompletedPractices() => const Expanded( + child: CustomColumn(title: '85', subtitle: 'Practices Completed')); + Widget _buildTakenQuizzes() => const Expanded( + child: CustomColumn(title: '45', subtitle: 'Quizzes Taken')); + + Widget _buildActionButton() => const CustomElevatedButton( + height: 15, + width: 200, + borderRadius: 12, + text: 'Continue Learning', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/login_account.dart b/StudioProjects/yimaru_app/lib/ui/widgets/login_account.dart new file mode 100644 index 0000000..4c17a03 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/login_account.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; + +class LoginAccount extends StatelessWidget { + final GestureTapCallback? onTap; + const LoginAccount({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildRow(); + + Widget _buildRow() => Row( + children: [ + _buildLeadingText(), + horizontalSpaceTiny, + _buildRegisterTextButton() + ], + ); + + Widget _buildLeadingText() => const Text( + 'Already have an account? ', + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildRegisterTextButton() => TextButton( + onPressed: onTap, + style: const ButtonStyle( + alignment: Alignment.centerLeft, + padding: WidgetStatePropertyAll(EdgeInsets.zero)), + child: _buildRegisterText(), + ); + + Widget _buildRegisterText() => const Text( + 'Login', + style: TextStyle(color: kcPrimaryColor), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/obscure_password.dart b/StudioProjects/yimaru_app/lib/ui/widgets/obscure_password.dart new file mode 100644 index 0000000..f66d66a --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/obscure_password.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class ObscurePassword extends StatelessWidget { + final bool focus; + final bool obscure; + final GestureTapCallback? onTap; + const ObscurePassword( + {super.key, this.onTap, required this.focus, required this.obscure}); + + @override + Widget build(BuildContext context) => _buildButton(); + + Widget _buildButton() => GestureDetector( + onTap: onTap, + child: _buildIconWrapper(), + ); + + Widget _buildIconWrapper() => + obscure ? _buildObscuredIcon() : _buildUnObscuredIcon(); + + Widget _buildObscuredIcon() => Icon( + Icons.visibility, + color: focus ? kcPrimaryColor : kcLightGrey, + ); + + Widget _buildUnObscuredIcon() => Icon( + Icons.visibility_off, + color: focus ? kcPrimaryColor : kcLightGrey, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/onboarding_app_bar.dart b/StudioProjects/yimaru_app/lib/ui/widgets/onboarding_app_bar.dart deleted file mode 100644 index 5344294..0000000 --- a/StudioProjects/yimaru_app/lib/ui/widgets/onboarding_app_bar.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stacked/stacked.dart'; -import 'package:yimaru_app/ui/common/app_colors.dart'; -import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/language_button.dart'; - -class OnboardingAppBar extends ViewModelWidget { - final bool language; - final bool showBackButton; - final bool showLanguageSelection; - - const OnboardingAppBar( - {super.key, - this.language = false, - this.showBackButton = true, - this.showLanguageSelection = true}); - - @override - Widget build(BuildContext context, OnboardingViewModel viewModel) => - _buildAppBarWrapper(viewModel); - - Widget _buildAppBarWrapper(OnboardingViewModel viewModel) => Container( - height: 125, - width: double.maxFinite, - alignment: Alignment.bottomCenter, - decoration: const BoxDecoration( - color: kcPrimaryColor, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), - ), - ), - padding: const EdgeInsets.only(bottom: 25, right: 15), - child: _buildAppBarItems(viewModel), - ); - - Widget _buildAppBarItems(OnboardingViewModel viewModel) => Stack( - children: _buildAppBarItemChildren(viewModel), - ); - - List _buildAppBarItemChildren(OnboardingViewModel viewModel) => [ - if (showBackButton) _buildBackButtonWrapper(viewModel), - _buildRightButton(viewModel) - ]; - - Widget _buildBackButtonWrapper(OnboardingViewModel viewModel) => Align( - alignment: Alignment.bottomLeft, - child: _buildBackButton(viewModel), - ); - - Widget _buildBackButton(OnboardingViewModel viewModel) => BackButton( - onPressed: ()=> viewModel.pop(language: language), - style: const ButtonStyle( - foregroundColor: WidgetStatePropertyAll(kcWhiteColor)), - ); - - Widget _buildRightButton(OnboardingViewModel viewModel) => Align( - alignment: Alignment.bottomRight, - child: showLanguageSelection - ? _buildLanguageSelector(viewModel) - : _buildCloseButton()); - - Widget _buildLanguageSelector(OnboardingViewModel viewModel) => - LanguageButton( - language: 'EN', - onTap: () => viewModel.next(page: 23), - ); - - Widget _buildCloseButton() => IconButton( - onPressed: () {}, - icon: _buildCloseIcon(), - ); - - Widget _buildCloseIcon() => const Icon( - Icons.close, - color: kcWhiteColor, - ); -} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/option_text_divider.dart b/StudioProjects/yimaru_app/lib/ui/widgets/option_text_divider.dart new file mode 100644 index 0000000..de62133 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/option_text_divider.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; + +class OptionTextDivider extends StatelessWidget { + const OptionTextDivider({super.key}); + + @override + Widget build(BuildContext context) => _buildOrTextWrapper(); + + Widget _buildOrTextWrapper() => Row( + children: [ + _buildDividerWrapper(), + horizontalSpaceSmall, + _buildOrText(), + horizontalSpaceSmall, + _buildDividerWrapper() + ], + ); + Widget _buildDividerWrapper() => Expanded(child: _buildDivider()); + + Widget _buildDivider() => const Divider(color: kcVeryLightGrey); + + Widget _buildOrText() => const Text( + 'or', + textAlign: TextAlign.center, + style: TextStyle(color: kcMediumGrey), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/overall_learn_progress.dart b/StudioProjects/yimaru_app/lib/ui/widgets/overall_learn_progress.dart new file mode 100644 index 0000000..1a047b2 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/overall_learn_progress.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; + +class OverallLearnProgress extends StatelessWidget { + const OverallLearnProgress({super.key}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 25), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildProgressSection(), + ); + + Widget _buildProgressSection() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildProgressSectionChildren(), + ); + + List _buildProgressSectionChildren() => [ + _buildProgressInfoWrapper(), + verticalSpaceSmall, + _buildProgressIndicator(), + verticalSpaceSmall, + _buildSubtitle() + ]; + + Widget _buildProgressInfoWrapper() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildProgressInfoChildren(), + ); + + List _buildProgressInfoChildren() => + [_buildProgressInfo(), _buildProgress()]; + + Widget _buildProgressInfo() => const Text( + 'Overall Progress', + style: TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgress() => const Text( + '35%', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgressIndicator() => const CustomLinearProgressIndicator( + progress: 0.75, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + ); + + Widget _buildSubtitle() => const Text( + 'Keep up the great work! You\'re doing amazing.', + style: TextStyle(color: kcDarkGrey), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/page_loading_indicator.dart b/StudioProjects/yimaru_app/lib/ui/widgets/page_loading_indicator.dart new file mode 100644 index 0000000..e582d57 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/page_loading_indicator.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import 'custom_circular_progress_indicator.dart'; + +class PageLoadingIndicator extends StatelessWidget { + const PageLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) => _buildBody(context); + + Widget _buildBody(BuildContext context) => Material( + color: kcTransparent, + child: _buildContainer(context), + ); + + Widget _buildContainer(BuildContext context) => Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + color: kcBlack.withOpacity(0.3), + child: _buildBoxContainerWrapper(), + ); + + Widget _buildBoxContainerWrapper() => Center( + child: _buildBoxContainer(), + ); + + Widget _buildBoxContainer() => Container( + width: 150, + height: 100, + alignment: Alignment.center, + decoration: BoxDecoration( + color: kcBackgroundColor, + borderRadius: BorderRadius.circular(7), + ), + child: _buildColumnWrapper(), + ); + + Widget _buildColumnWrapper() => Center( + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => + [_buildShimmer(), verticalSpaceSmall, _buildText()]; + + Widget _buildShimmer() => const Center( + child: CustomCircularProgressIndicator(color: kcPrimaryColor), + ); + + Widget _buildText() => const Text( + 'Please wait', + style: TextStyle(color: kcPrimaryColor), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/phone_number_prefix.dart b/StudioProjects/yimaru_app/lib/ui/widgets/phone_number_prefix.dart new file mode 100644 index 0000000..ef73e55 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/phone_number_prefix.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import '../common/app_colors.dart'; + +class PhoneNumberPrefix extends StatelessWidget { + final bool selected; + const PhoneNumberPrefix({ + super.key, + required this.selected, + }); + + @override + Widget build(BuildContext context) => _buildButtonWrapper(); + + Widget _buildButtonWrapper() => Container( + height: 57, + padding: const EdgeInsets.symmetric(horizontal: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: selected ? kcPrimaryColor.withOpacity(0.1) : kcWhite, + border: Border.all( + color: selected ? kcPrimaryColor : kcPrimaryColor.withOpacity(0.75), + ), + ), + child: _buildContainerWrapper(), + ); + + Widget _buildContainerWrapper() => Row( + children: _buildButtonRowChildren(), + ); + + List _buildButtonRowChildren() => + [_buildIcon(), horizontalSpaceSmall, _buildText()]; + + Widget _buildText() => const Text( + '+251', + style: TextStyle(color: kcDarkGrey), + ); + + Widget _buildIcon() => Image.asset( + 'assets/icons/flag.png', + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/privacy_policy_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/privacy_policy_tile.dart new file mode 100644 index 0000000..f6529bc --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/privacy_policy_tile.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:iconsax/iconsax.dart'; +import 'package:yimaru_app/ui/common/app_strings.dart'; + +import '../common/app_colors.dart'; + +class PrivacyPolicyTile extends StatelessWidget { + final String title; + + const PrivacyPolicyTile({super.key, required this.title}); + + IconData _getIcon() { + if (title == 'Introduction') { + return Icons.list_alt; + } else if (title == 'Information We Collect') { + return Icons.all_inbox; + } else if (title == 'How We Use Your Information') { + return Icons.lightbulb_outline; + } else if (title == 'Data Sharing and Disclosure') { + return Icons.share; + } else if (title == 'Your Rights and Choices') { + return Icons.confirmation_num; + } else if (title == 'Data Security') { + return Icons.shield_moon_outlined; + } else { + return Iconsax.pen_add; + } + } + + @override + Widget build(BuildContext context) => _buildExpansionTileCard(); + + Widget _buildExpansionTileCard() => Padding( + padding: const EdgeInsets.only(bottom: 15), + child: _buildExpansionTile(), + ); + + Widget _buildExpansionTile() => ExpansionTile( + title: _buildTitle(), + iconColor: kcDarkGrey, + textColor: kcDarkGrey, + leading: _buildIcon(), + showTrailingIcon: true, + initiallyExpanded: false, + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.all(15), + backgroundColor: kcPrimaryColor.withOpacity(0.1), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1), + shape: Border.all(color: kcPrimaryColor.withOpacity(0.2)), + children: _buildExpansionTileChildren(), + ); + + Widget _buildIcon() => Icon( + _getIcon(), + color: kcPrimaryColor, + ); + + List _buildExpansionTileChildren() => [_buildContent()]; + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildContent() => const Text( + ksPrivacyPolicy, + style: TextStyle( + color: kcDarkGrey, + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/profile_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/profile_card.dart new file mode 100644 index 0000000..6563a8a --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/profile_card.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class ProfileCard extends StatelessWidget { + final String title; + final IconData icon; + final String subTitle; + final GestureTapCallback? onTap; + + const ProfileCard( + {super.key, + this.onTap, + required this.icon, + required this.title, + required this.subTitle}); + + @override + Widget build(BuildContext context) => _buildContainerWrapper(); + + Widget _buildContainerWrapper() => GestureDetector( + onTap: onTap, + child: _buildContainer(), + ); + + Widget _buildContainer() => Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => [ + _buildIcon(), + verticalSpaceSmall, + _buildTitle(), + verticalSpaceSmall, + _buildSubTitle() + ]; + + Widget _buildIcon() => Icon( + icon, + size: 35, + color: kcPrimaryColor, + ); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => Text( + subTitle, + style: const TextStyle(color: kcMediumGrey), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/profile_image.dart b/StudioProjects/yimaru_app/lib/ui/widgets/profile_image.dart new file mode 100644 index 0000000..42f61f3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/profile_image.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class ProfileImage extends StatelessWidget { + const ProfileImage({super.key}); + + @override + Widget build(BuildContext context) => _buildSizedBox(); + + Widget _buildSizedBox() => SizedBox( + height: 125, + width: 125, + child: _buildStack(), + ); + + Widget _buildStack() => Stack( + children: [_buildProfileImageWrapper(), _buildCameraButtonWrapper()], + ); + + Widget _buildProfileImageWrapper() => Align( + alignment: Alignment.center, + child: _buildProfileImage(), + ); + + Widget _buildProfileImage() => const CircleAvatar( + radius: 50, + backgroundImage: AssetImage('assets/images/profile.png'), + ); + + Widget _buildCameraButtonWrapper() => Align( + alignment: Alignment.bottomCenter, + child: _buildCameraButton(), + ); + + Widget _buildCameraButton() => Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 7), + decoration: BoxDecoration( + color: kcPrimaryColor, borderRadius: BorderRadius.circular(25)), + child: _buildCameraIcon(), + ); + + Widget _buildCameraIcon() => const Icon( + Icons.camera_alt_outlined, + size: 18, + color: kcWhite, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/progress_status.dart b/StudioProjects/yimaru_app/lib/ui/widgets/progress_status.dart new file mode 100644 index 0000000..76ef5a8 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/progress_status.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class ProgressStatus extends StatelessWidget { + final Color color; + final String status; + + const ProgressStatus({super.key, required this.color, required this.status}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2.5), + child: _buildLabel(), + ); + + Widget _buildLabel() => Text( + status, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/register_for_account.dart b/StudioProjects/yimaru_app/lib/ui/widgets/register_for_account.dart new file mode 100644 index 0000000..933712b --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/register_for_account.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; + +class RegisterForAccount extends StatelessWidget { + final GestureTapCallback? onTap; + const RegisterForAccount({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildRow(); + + Widget _buildRow() => Row( + children: [ + _buildLeadingText(), + horizontalSpaceTiny, + _buildRegisterTextButton() + ], + ); + + Widget _buildLeadingText() => const Text( + 'Don’t have an account? ', + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildRegisterTextButton() => TextButton( + onPressed: onTap, + style: const ButtonStyle( + alignment: Alignment.centerLeft, + padding: WidgetStatePropertyAll(EdgeInsets.zero)), + child: _buildRegisterText(), + ); + + Widget _buildRegisterText() => const Text( + 'Register', + style: TextStyle(color: kcPrimaryColor), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/skill_progress.dart b/StudioProjects/yimaru_app/lib/ui/widgets/skill_progress.dart new file mode 100644 index 0000000..3543d51 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/skill_progress.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:iconsax/iconsax.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import 'custom_linear_progress_indicator.dart'; + +class SkillProgress extends StatelessWidget { + final String skill; + final double progress; + + const SkillProgress({super.key, required this.skill, required this.progress}); + + Color _getColor() { + if (skill == 'Speaking') { + return kcOrange; + } else if (skill == 'Listening') { + return kcGreen; + } else if (skill == 'Reading') { + return kcAquamarine; + } else { + return kcIndigo; + } + } + + IconData _getIcon() { + if (skill == 'Speaking') { + return Icons.mic_none; + } else if (skill == 'Listening') { + return Icons.headphones_outlined; + } else if (skill == 'Reading') { + return Icons.chrome_reader_mode_outlined; + } else { + return Iconsax.pen_add; + } + } + + @override + Widget build(BuildContext context) => _buildSkillSection(); + + Widget _buildSkillSection() => Column( + mainAxisSize: MainAxisSize.min, + children: _buildStorageSectionChildren(), + ); + + List _buildStorageSectionChildren() => [ + verticalSpaceSmall, + _buildSkillInfoWrapper(), + verticalSpaceSmall, + _buildProgressIndicator(), + verticalSpaceMedium + ]; + + Widget _buildSkillInfoWrapper() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildSkillInfoChildren(), + ); + + List _buildSkillInfoChildren() => + [_buildSkillRow(), _buildProgress()]; + + Widget _buildSkillRow() => Row( + children: [ + _buildIcon(), + const SizedBox( + width: 5, + ), + _buildSkill() + ], + ); + + Widget _buildIcon() => Icon( + _getIcon(), + color: _getColor(), + ); + + Widget _buildSkill() => Text( + skill, + style: const TextStyle( + fontSize: 18, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgress() => Text( + '${(progress * 100).toInt()}%', + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgressIndicator() => CustomLinearProgressIndicator( + progress: progress, + activeColor: _getColor(), + backgroundColor: kcVeryLightGrey, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/small_app_bar.dart b/StudioProjects/yimaru_app/lib/ui/widgets/small_app_bar.dart new file mode 100644 index 0000000..97a28af --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/small_app_bar.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/widgets/custom_back_button.dart'; + +class SmallAppBar extends StatelessWidget { + final String? title; + final GestureTapCallback? onTap; + + const SmallAppBar({super.key, this.onTap, this.title}); + + @override + Widget build(BuildContext context) => _buildAppBar(); + + Widget _buildAppBar() => Stack( + alignment: Alignment.center, + children: _buildAppBarChildren(), + ); + + List _buildAppBarChildren() => + [_buildBackButtonWrapper(), if (title != null) _buildTitleWrapper()]; + + Widget _buildBackButtonWrapper() => Align( + alignment: Alignment.centerLeft, + child: _buildBackButton(), + ); + + Widget _buildBackButton() => CustomBackButton(onTap: onTap); + + Widget _buildTitleWrapper() => Align( + alignment: Alignment.center, + child: _buildTitle(), + ); + + Widget _buildTitle() => Text( + title ?? '', + style: const TextStyle( + fontSize: 18, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/suggestion_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/suggestion_card.dart new file mode 100644 index 0000000..88fd382 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/suggestion_card.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_strings.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import '../common/app_colors.dart'; + +class SuggestionCard extends StatelessWidget { + const SuggestionCard({super.key}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + height: 75, + width: double.maxFinite, + margin: const EdgeInsets.symmetric(horizontal: 15), + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [kcPrimaryAccent, kcPrimaryColor]), + ), + child: _buildRow(), + ); + + Widget _buildRow() => Row( + children: [_buildIcon(), horizontalSpaceSmall, _buildTitleWrapper()], + ); + + Widget _buildIcon() => const Icon(Icons.lightbulb_outline, color: kcWhite); + + Widget _buildTitleWrapper() => Expanded( + child: _buildTitle(), + ); + + Widget _buildTitle() => const Text( + ksSuggestion, + style: TextStyle(color: kcWhite), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/support_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/support_card.dart new file mode 100644 index 0000000..dd3a92c --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/support_card.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/widgets/circular_icon.dart'; + +class SupportCard extends StatelessWidget { + final Color color; + final String title; + final IconData icon; + final String subtitle; + final GestureTapCallback? onTap; + + const SupportCard({ + super.key, + this.onTap, + required this.icon, + required this.color, + required this.title, + required this.subtitle, + }); + + @override + Widget build(BuildContext context) => _buildLitTile(); + + Widget _buildLitTile() => ListTile( + onTap: onTap, + title: _buildTitle(), + subtitle: _buildSubtitle(), + trailing: _buildTrailingIcon(), + leading: _buildLeadingWrapper(), + tileColor: color.withOpacity(0.2), + ); + + Widget _buildLeadingWrapper() => + CircularIcon(icon: icon, size: 20, color: color); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, color: kcDarkGrey, fontWeight: FontWeight.w600), + ); + + Widget _buildSubtitle() => Text( + subtitle, + maxLines: 2, + style: const TextStyle(color: kcDarkGrey), + ); + + Widget _buildTrailingIcon() => Icon( + Icons.keyboard_arrow_right, + color: color, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/validator_list_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/validator_list_tile.dart new file mode 100644 index 0000000..b77e981 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/validator_list_tile.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class ValidatorListTile extends StatelessWidget { + final String label; + final Color backgroundColor; + const ValidatorListTile( + {super.key, required this.label, required this.backgroundColor}); + + @override + Widget build(BuildContext context) => _buildRowWrapper(); + + Widget _buildRowWrapper() => Padding( + padding: const EdgeInsets.only(bottom: 15), + child: _buildRow(), + ); + + Widget _buildRow() => Row( + children: [_buildIconWrapper(), horizontalSpaceSmall, _buildLabel()], + ); + + Widget _buildIconWrapper() => CircleAvatar( + radius: 8, + backgroundColor: backgroundColor, + child: _buildIcon(), + ); + + Widget _buildIcon() => const Icon( + Icons.check, + size: 14, + color: kcWhite, + ); + + Widget _buildLabel() => Text( + label, + style: style16DG400, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/view_profile_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/view_profile_button.dart new file mode 100644 index 0000000..fe45130 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/view_profile_button.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class ViewProfileButton extends StatelessWidget { + final GestureTapCallback? onTap; + const ViewProfileButton({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildButtonWrapper(); + + Widget _buildButtonWrapper() => GestureDetector( + onTap: onTap, + child: _buildButtonRow(), + ); + + Widget _buildButtonRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildButtonRowChildren(), + ); + + List _buildButtonRowChildren() => + [_buildButtonText(), const SizedBox(width: 10), _buildButtonIcon()]; + + Widget _buildButtonText() => const Text( + 'View Profile', + style: TextStyle( + color: kcPrimaryColor, fontSize: 16, fontWeight: FontWeight.w900), + ); + + Widget _buildButtonIcon() => const Icon( + Icons.arrow_forward, + size: 16, + color: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/linux/flutter/generated_plugin_registrant.cc b/StudioProjects/yimaru_app/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/StudioProjects/yimaru_app/linux/flutter/generated_plugin_registrant.cc +++ b/StudioProjects/yimaru_app/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/StudioProjects/yimaru_app/linux/flutter/generated_plugins.cmake b/StudioProjects/yimaru_app/linux/flutter/generated_plugins.cmake index 2e1de87..b29e9ba 100644 --- a/StudioProjects/yimaru_app/linux/flutter/generated_plugins.cmake +++ b/StudioProjects/yimaru_app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/StudioProjects/yimaru_app/macos/Flutter/GeneratedPluginRegistrant.swift b/StudioProjects/yimaru_app/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..61d01d0 100644 --- a/StudioProjects/yimaru_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/StudioProjects/yimaru_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_darwin +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/StudioProjects/yimaru_app/pubspec.lock b/StudioProjects/yimaru_app/pubspec.lock index da6e4ea..9e6e165 100644 --- a/StudioProjects/yimaru_app/pubspec.lock +++ b/StudioProjects/yimaru_app/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: a2cebb899f91d36eeeaa55c7b20b5915db5a9df1b8fd4a3c9c825e22e474537d + url: "https://pub.dev" + source: hosted + version: "9.1.0" boolean_selector: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dart_style: dependency: transitive description: @@ -169,6 +185,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" dropdown_search: dependency: "direct main" description: @@ -177,6 +209,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + email_validator: + dependency: "direct main" + description: + name: email_validator + sha256: b19aa5d92fdd76fbc65112060c94d45ba855105a28bb6e462de7ff03b12fa1fb + url: "https://pub.dev" + source: hosted + version: "3.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -185,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.dev" + source: hosted + version: "2.1.5" file: dependency: transitive description: @@ -206,6 +262,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: transitive + description: + name: flutter_bloc + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + url: "https://pub.dev" + source: hosted + version: "9.1.1" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_lints: dependency: "direct dev" description: @@ -214,6 +286,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + url: "https://pub.dev" + source: hosted + version: "10.0.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter_svg: dependency: "direct main" description: @@ -227,6 +347,19 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_timer_countdown: + dependency: "direct main" + description: + name: flutter_timer_countdown + sha256: "0c73e1593ad7949c007752199a17e7ed50bb581568743dbc32f061f49873219e" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" freezed_annotation: dependency: transitive description: @@ -283,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: transitive description: @@ -315,6 +456,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.8" + iconsax_flutter: + dependency: transitive + description: + name: iconsax_flutter + sha256: d14b4cec8586025ac15276bdd40f6eea308cb85748135965bb6255f14beb2564 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: @@ -332,13 +489,21 @@ packages: source: hosted version: "0.7.2" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted version: "4.9.0" + json_serializable: + dependency: "direct main" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" leak_tracker: dependency: transitive description: @@ -371,6 +536,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" logger: dependency: transitive description: @@ -435,6 +608,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + omni_datetime_picker: + dependency: "direct main" + description: + name: omni_datetime_picker + sha256: bb360790e76109ea2e53b45643cdaab779649c4bf1a9d2794d3a135bfe9746e1 + url: "https://pub.dev" + source: hosted + version: "2.3.1" package_config: dependency: transitive description: @@ -459,6 +640,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" petitparser: dependency: transitive description: @@ -467,6 +704,30 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "692e1c29703abefad6c502e97b4d946d506384397438ea242afadbfe48354819" + url: "https://pub.dev" + source: hosted + version: "6.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -507,6 +768,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -536,6 +805,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" source_span: dependency: transitive description: @@ -632,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + toastification: + dependency: "direct main" + description: + name: toastification + sha256: "69db2bff425b484007409650d8bcd5ed1ce2e9666293ece74dcd917dacf23112" + url: "https://pub.dev" + source: hosted + version: "3.0.3" typed_data: dependency: transitive description: @@ -648,6 +933,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_graphics: dependency: transitive description: @@ -720,6 +1013,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -745,5 +1046,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.32.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/StudioProjects/yimaru_app/pubspec.yaml b/StudioProjects/yimaru_app/pubspec.yaml index 16c45fd..0a57982 100644 --- a/StudioProjects/yimaru_app/pubspec.yaml +++ b/StudioProjects/yimaru_app/pubspec.yaml @@ -9,13 +9,24 @@ environment: dependencies: flutter: sdk: flutter + intl: any + dio: ^5.9.0 + pinput: ^6.0.1 stacked: ^3.4.0 iconsax: ^0.0.8 flutter_svg: ^2.2.3 - dropdown_search: ^6.0.2 - stacked_services: ^1.1.0 - stacked_shared: any + flutter_html: ^3.0.0 + email_validator: any + toastification: ^3.0.3 + dropdown_search: ^6.0.2 + json_annotation: ^4.9.0 + stacked_services: ^1.1.0 + omni_datetime_picker: any + json_serializable: ^6.8.0 + flutter_secure_storage: ^10.0.0 + flutter_timer_countdown: ^1.0.7 + dev_dependencies: flutter_test: sdk: flutter @@ -25,6 +36,9 @@ dev_dependencies: golden_toolkit: ^0.15.0 stacked_generator: ^1.3.3 +dependency_overrides: + intl: ^0.20.2 + flutter: uses-material-design: true diff --git a/StudioProjects/yimaru_app/test/helpers/test_helpers.dart b/StudioProjects/yimaru_app/test/helpers/test_helpers.dart index df3b057..ae13e93 100644 --- a/StudioProjects/yimaru_app/test/helpers/test_helpers.dart +++ b/StudioProjects/yimaru_app/test/helpers/test_helpers.dart @@ -2,6 +2,10 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:yimaru_app/app/app.locator.dart'; import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; +import 'package:yimaru_app/services/api_service.dart'; +import 'package:yimaru_app/services/secure_storage_service.dart'; +import 'package:yimaru_app/services/dio_service.dart'; // @stacked-import import 'test_helpers.mocks.dart'; @@ -12,14 +16,22 @@ import 'test_helpers.mocks.dart'; MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), - // @stacked-mock-spec + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), +// @stacked-mock-spec ], ) void registerServices() { getAndRegisterNavigationService(); getAndRegisterBottomSheetService(); getAndRegisterDialogService(); - // @stacked-mock-register + getAndRegisterAuthenticationService(); + getAndRegisterApiService(); + getAndRegisterSecureStorageService(); + getAndRegisterDioService(); +// @stacked-mock-register } MockNavigationService getAndRegisterNavigationService() { @@ -76,6 +88,33 @@ MockDialogService getAndRegisterDialogService() { return service; } +MockAuthenticationService getAndRegisterAuthenticationService() { + _removeRegistrationIfExists(); + final service = MockAuthenticationService(); + locator.registerSingleton(service); + return service; +} + +MockApiService getAndRegisterApiService() { + _removeRegistrationIfExists(); + final service = MockApiService(); + locator.registerSingleton(service); + return service; +} + +MockSecureStorageService getAndRegisterSecureStorageService() { + _removeRegistrationIfExists(); + final service = MockSecureStorageService(); + locator.registerSingleton(service); + return service; +} + +MockDioService getAndRegisterDioService() { + _removeRegistrationIfExists(); + final service = MockDioService(); + locator.registerSingleton(service); + return service; +} // @stacked-mock-create void _removeRegistrationIfExists() { diff --git a/StudioProjects/yimaru_app/test/helpers/test_helpers.mocks.dart b/StudioProjects/yimaru_app/test/helpers/test_helpers.mocks.dart index 651298f..99a2c8b 100644 --- a/StudioProjects/yimaru_app/test/helpers/test_helpers.mocks.dart +++ b/StudioProjects/yimaru_app/test/helpers/test_helpers.mocks.dart @@ -3,13 +3,18 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:ui' as _i6; +import 'dart:async' as _i6; +import 'dart:ui' as _i7; -import 'package:flutter/material.dart' as _i4; +import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i3; -import 'package:stacked_services/stacked_services.dart' as _i2; +import 'package:mockito/src/dummies.dart' as _i4; +import 'package:stacked_services/stacked_services.dart' as _i3; +import 'package:yimaru_app/models/user_model.dart' as _i2; +import 'package:yimaru_app/services/api_service.dart' as _i9; +import 'package:yimaru_app/services/authentication_service.dart' as _i8; +import 'package:yimaru_app/services/dio_service.dart' as _i11; +import 'package:yimaru_app/services/secure_storage_service.dart' as _i10; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -24,18 +29,28 @@ import 'package:stacked_services/stacked_services.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeUserModel_0 extends _i1.SmartFake implements _i2.UserModel { + _FakeUserModel_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [NavigationService]. /// /// See the documentation for Mockito's code generation for more information. -class MockNavigationService extends _i1.Mock implements _i2.NavigationService { +class MockNavigationService extends _i1.Mock implements _i3.NavigationService { @override String get previousRoute => (super.noSuchMethod( Invocation.getter(#previousRoute), - returnValue: _i3.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.getter(#previousRoute), ), - returnValueForMissingStub: _i3.dummyValue( + returnValueForMissingStub: _i4.dummyValue( this, Invocation.getter(#previousRoute), ), @@ -44,25 +59,25 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { @override String get currentRoute => (super.noSuchMethod( Invocation.getter(#currentRoute), - returnValue: _i3.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.getter(#currentRoute), ), - returnValueForMissingStub: _i3.dummyValue( + returnValueForMissingStub: _i4.dummyValue( this, Invocation.getter(#currentRoute), ), ) as String); @override - _i4.GlobalKey<_i4.NavigatorState>? nestedNavigationKey(int? index) => + _i5.GlobalKey<_i5.NavigatorState>? nestedNavigationKey(int? index) => (super.noSuchMethod( Invocation.method( #nestedNavigationKey, [index], ), returnValueForMissingStub: null, - ) as _i4.GlobalKey<_i4.NavigatorState>?); + ) as _i5.GlobalKey<_i5.NavigatorState>?); @override void config({ @@ -71,7 +86,7 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { bool? defaultOpaqueRoute, Duration? defaultDurationTransition, bool? defaultGlobalState, - _i2.Transition? defaultTransitionStyle, + _i3.Transition? defaultTransitionStyle, String? defaultTransition, }) => super.noSuchMethod( @@ -92,18 +107,18 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { ); @override - _i5.Future? navigateWithTransition( - _i4.Widget? page, { + _i6.Future? navigateWithTransition( + _i5.Widget? page, { bool? opaque, String? transition = r'', Duration? duration, bool? popGesture, int? id, - _i4.Curve? curve, + _i5.Curve? curve, bool? fullscreenDialog = false, bool? preventDuplicates = true, - _i2.Transition? transitionClass, - _i2.Transition? transitionStyle, + _i3.Transition? transitionClass, + _i3.Transition? transitionStyle, String? routeName, }) => (super.noSuchMethod( @@ -125,21 +140,21 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? replaceWithTransition( - _i4.Widget? page, { + _i6.Future? replaceWithTransition( + _i5.Widget? page, { bool? opaque, String? transition = r'', Duration? duration, bool? popGesture, int? id, - _i4.Curve? curve, + _i5.Curve? curve, bool? fullscreenDialog = false, bool? preventDuplicates = true, - _i2.Transition? transitionClass, - _i2.Transition? transitionStyle, + _i3.Transition? transitionClass, + _i3.Transition? transitionStyle, String? routeName, }) => (super.noSuchMethod( @@ -161,7 +176,7 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override bool back({ @@ -183,7 +198,7 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { @override void popUntil( - _i4.RoutePredicate? predicate, { + _i5.RoutePredicate? predicate, { int? id, }) => super.noSuchMethod( @@ -205,13 +220,13 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { ); @override - _i5.Future? navigateTo( + _i6.Future? navigateTo( String? routeName, { dynamic arguments, int? id, bool? preventDuplicates = true, Map? parameters, - _i4.RouteTransitionsBuilder? transition, + _i5.RouteTransitionsBuilder? transition, }) => (super.noSuchMethod( Invocation.method( @@ -226,21 +241,21 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? navigateToView( - _i4.Widget? view, { + _i6.Future? navigateToView( + _i5.Widget? view, { dynamic arguments, int? id, bool? opaque, - _i4.Curve? curve, + _i5.Curve? curve, Duration? duration, bool? fullscreenDialog = false, bool? popGesture, bool? preventDuplicates = true, - _i2.Transition? transition, - _i2.Transition? transitionStyle, + _i3.Transition? transition, + _i3.Transition? transitionStyle, }) => (super.noSuchMethod( Invocation.method( @@ -260,16 +275,16 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? replaceWith( + _i6.Future? replaceWith( String? routeName, { dynamic arguments, int? id, bool? preventDuplicates = true, Map? parameters, - _i4.RouteTransitionsBuilder? transition, + _i5.RouteTransitionsBuilder? transition, }) => (super.noSuchMethod( Invocation.method( @@ -284,10 +299,10 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? clearStackAndShow( + _i6.Future? clearStackAndShow( String? routeName, { dynamic arguments, int? id, @@ -304,11 +319,11 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? clearStackAndShowView( - _i4.Widget? view, { + _i6.Future? clearStackAndShowView( + _i5.Widget? view, { dynamic arguments, int? id, }) => @@ -322,10 +337,10 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? clearTillFirstAndShow( + _i6.Future? clearTillFirstAndShow( String? routeName, { dynamic arguments, int? id, @@ -344,11 +359,11 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? clearTillFirstAndShowView( - _i4.Widget? view, { + _i6.Future? clearTillFirstAndShowView( + _i5.Widget? view, { dynamic arguments, int? id, }) => @@ -362,12 +377,12 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? pushNamedAndRemoveUntil( + _i6.Future? pushNamedAndRemoveUntil( String? routeName, { - _i4.RoutePredicate? predicate, + _i5.RoutePredicate? predicate, dynamic arguments, int? id, }) => @@ -382,16 +397,16 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); } /// A class which mocks [BottomSheetService]. /// /// See the documentation for Mockito's code generation for more information. class MockBottomSheetService extends _i1.Mock - implements _i2.BottomSheetService { + implements _i3.BottomSheetService { @override - void setCustomSheetBuilders(Map? builders) => + void setCustomSheetBuilders(Map? builders) => super.noSuchMethod( Invocation.method( #setCustomSheetBuilders, @@ -401,7 +416,7 @@ class MockBottomSheetService extends _i1.Mock ); @override - _i5.Future<_i2.SheetResponse?> showBottomSheet({ + _i6.Future<_i3.SheetResponse?> showBottomSheet({ required String? title, String? description, String? confirmButtonTitle = r'Ok', @@ -434,13 +449,13 @@ class MockBottomSheetService extends _i1.Mock #elevation: elevation, }, ), - returnValue: _i5.Future<_i2.SheetResponse?>.value(), + returnValue: _i6.Future<_i3.SheetResponse?>.value(), returnValueForMissingStub: - _i5.Future<_i2.SheetResponse?>.value(), - ) as _i5.Future<_i2.SheetResponse?>); + _i6.Future<_i3.SheetResponse?>.value(), + ) as _i6.Future<_i3.SheetResponse?>); @override - _i5.Future<_i2.SheetResponse?> showCustomSheet({ + _i6.Future<_i3.SheetResponse?> showCustomSheet({ dynamic variant, String? title, String? description, @@ -453,7 +468,7 @@ class MockBottomSheetService extends _i1.Mock bool? showIconInAdditionalButton = false, String? additionalButtonTitle, bool? takesInput = false, - _i6.Color? barrierColor = const _i6.Color(2315255808), + _i7.Color? barrierColor = const _i7.Color(2315255808), double? elevation = 1.0, bool? barrierDismissible = true, bool? isScrollControlled = false, @@ -497,12 +512,12 @@ class MockBottomSheetService extends _i1.Mock #useRootNavigator: useRootNavigator, }, ), - returnValue: _i5.Future<_i2.SheetResponse?>.value(), - returnValueForMissingStub: _i5.Future<_i2.SheetResponse?>.value(), - ) as _i5.Future<_i2.SheetResponse?>); + returnValue: _i6.Future<_i3.SheetResponse?>.value(), + returnValueForMissingStub: _i6.Future<_i3.SheetResponse?>.value(), + ) as _i6.Future<_i3.SheetResponse?>); @override - void completeSheet(_i2.SheetResponse? response) => + void completeSheet(_i3.SheetResponse? response) => super.noSuchMethod( Invocation.method( #completeSheet, @@ -515,10 +530,10 @@ class MockBottomSheetService extends _i1.Mock /// A class which mocks [DialogService]. /// /// See the documentation for Mockito's code generation for more information. -class MockDialogService extends _i1.Mock implements _i2.DialogService { +class MockDialogService extends _i1.Mock implements _i3.DialogService { @override void registerCustomDialogBuilders( - Map? builders) => + Map? builders) => super.noSuchMethod( Invocation.method( #registerCustomDialogBuilders, @@ -530,10 +545,10 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { @override void registerCustomDialogBuilder({ required dynamic variant, - required _i4.Widget Function( - _i4.BuildContext, - _i2.DialogRequest, - dynamic Function(_i2.DialogResponse), + required _i5.Widget Function( + _i5.BuildContext, + _i3.DialogRequest, + dynamic Function(_i3.DialogResponse), )? builder, }) => super.noSuchMethod( @@ -549,17 +564,17 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { ); @override - _i5.Future<_i2.DialogResponse?> showDialog({ + _i6.Future<_i3.DialogResponse?> showDialog({ String? title, String? description, String? cancelTitle, - _i6.Color? cancelTitleColor, + _i7.Color? cancelTitleColor, String? buttonTitle = r'Ok', - _i6.Color? buttonTitleColor, + _i7.Color? buttonTitleColor, bool? barrierDismissible = false, - _i4.RouteSettings? routeSettings, - _i4.GlobalKey<_i4.NavigatorState>? navigatorKey, - _i2.DialogPlatform? dialogPlatform, + _i5.RouteSettings? routeSettings, + _i5.GlobalKey<_i5.NavigatorState>? navigatorKey, + _i3.DialogPlatform? dialogPlatform, }) => (super.noSuchMethod( Invocation.method( @@ -578,13 +593,13 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { #dialogPlatform: dialogPlatform, }, ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), + returnValue: _i6.Future<_i3.DialogResponse?>.value(), returnValueForMissingStub: - _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); + _i6.Future<_i3.DialogResponse?>.value(), + ) as _i6.Future<_i3.DialogResponse?>); @override - _i5.Future<_i2.DialogResponse?> showCustomDialog({ + _i6.Future<_i3.DialogResponse?> showCustomDialog({ dynamic variant, String? title, String? description, @@ -597,13 +612,13 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { bool? showIconInAdditionalButton = false, String? additionalButtonTitle, bool? takesInput = false, - _i6.Color? barrierColor = const _i6.Color(2315255808), + _i7.Color? barrierColor = const _i7.Color(2315255808), bool? barrierDismissible = false, String? barrierLabel = r'', bool? useSafeArea = true, - _i4.RouteSettings? routeSettings, - _i4.GlobalKey<_i4.NavigatorState>? navigatorKey, - _i4.RouteTransitionsBuilder? transitionBuilder, + _i5.RouteSettings? routeSettings, + _i5.GlobalKey<_i5.NavigatorState>? navigatorKey, + _i5.RouteTransitionsBuilder? transitionBuilder, dynamic customData, R? data, }) => @@ -635,21 +650,21 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { #data: data, }, ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), - returnValueForMissingStub: _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); + returnValue: _i6.Future<_i3.DialogResponse?>.value(), + returnValueForMissingStub: _i6.Future<_i3.DialogResponse?>.value(), + ) as _i6.Future<_i3.DialogResponse?>); @override - _i5.Future<_i2.DialogResponse?> showConfirmationDialog({ + _i6.Future<_i3.DialogResponse?> showConfirmationDialog({ String? title, String? description, String? cancelTitle = r'Cancel', - _i6.Color? cancelTitleColor, + _i7.Color? cancelTitleColor, String? confirmationTitle = r'Ok', - _i6.Color? confirmationTitleColor, + _i7.Color? confirmationTitleColor, bool? barrierDismissible = false, - _i4.RouteSettings? routeSettings, - _i2.DialogPlatform? dialogPlatform, + _i5.RouteSettings? routeSettings, + _i3.DialogPlatform? dialogPlatform, }) => (super.noSuchMethod( Invocation.method( @@ -667,13 +682,13 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { #dialogPlatform: dialogPlatform, }, ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), + returnValue: _i6.Future<_i3.DialogResponse?>.value(), returnValueForMissingStub: - _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); + _i6.Future<_i3.DialogResponse?>.value(), + ) as _i6.Future<_i3.DialogResponse?>); @override - void completeDialog(_i2.DialogResponse? response) => + void completeDialog(_i3.DialogResponse? response) => super.noSuchMethod( Invocation.method( #completeDialog, @@ -682,3 +697,235 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { returnValueForMissingStub: null, ); } + +/// A class which mocks [AuthenticationService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthenticationService extends _i1.Mock + implements _i8.AuthenticationService { + @override + _i6.Future userLoggedIn() => (super.noSuchMethod( + Invocation.method( + #userLoggedIn, + [], + ), + returnValue: _i6.Future.value(false), + returnValueForMissingStub: _i6.Future.value(false), + ) as _i6.Future); + + @override + _i6.Future saveUserData(Map? data) => + (super.noSuchMethod( + Invocation.method( + #saveUserData, + [data], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i2.UserModel> getUser() => (super.noSuchMethod( + Invocation.method( + #getUser, + [], + ), + returnValue: _i6.Future<_i2.UserModel>.value(_FakeUserModel_0( + this, + Invocation.method( + #getUser, + [], + ), + )), + returnValueForMissingStub: + _i6.Future<_i2.UserModel>.value(_FakeUserModel_0( + this, + Invocation.method( + #getUser, + [], + ), + )), + ) as _i6.Future<_i2.UserModel>); + + @override + _i6.Future logOut() => (super.noSuchMethod( + Invocation.method( + #logOut, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} + +/// A class which mocks [ApiService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockApiService extends _i1.Mock implements _i9.ApiService { + @override + _i6.Future> register(Map? data) => + (super.noSuchMethod( + Invocation.method( + #register, + [data], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); + + @override + _i6.Future> login(Map? data) => + (super.noSuchMethod( + Invocation.method( + #login, + [data], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); + + @override + _i6.Future> verifyOtp(Map? data) => + (super.noSuchMethod( + Invocation.method( + #verifyOtp, + [data], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); + + @override + _i6.Future> resendOtp(Map? data) => + (super.noSuchMethod( + Invocation.method( + #resendOtp, + [data], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); + + @override + _i6.Future> getProfileStatus(_i2.UserModel? user) => + (super.noSuchMethod( + Invocation.method( + #getProfileStatus, + [user], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); +} + +/// A class which mocks [SecureStorageService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSecureStorageService extends _i1.Mock + implements _i10.SecureStorageService { + @override + _i6.Future clear() => (super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getBool(String? key) => (super.noSuchMethod( + Invocation.method( + #getBool, + [key], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getString(String? key) => (super.noSuchMethod( + Invocation.method( + #getString, + [key], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getInt(String? key) => (super.noSuchMethod( + Invocation.method( + #getInt, + [key], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setString( + String? key, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setString, + [ + key, + value, + ], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setInt( + String? key, + int? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setInt, + [ + key, + value, + ], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setBool( + String? key, + bool? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setBool, + [ + key, + value, + ], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} + +/// A class which mocks [DioService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDioService extends _i1.Mock implements _i11.DioService {} diff --git a/StudioProjects/yimaru_app/test/services/api_service_test.dart b/StudioProjects/yimaru_app/test/services/api_service_test.dart new file mode 100644 index 0000000..93e9612 --- /dev/null +++ b/StudioProjects/yimaru_app/test/services/api_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('ApiServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/services/authentication_service_test.dart b/StudioProjects/yimaru_app/test/services/authentication_service_test.dart new file mode 100644 index 0000000..a06da40 --- /dev/null +++ b/StudioProjects/yimaru_app/test/services/authentication_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('AuthenticationServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/services/dio_service_test.dart b/StudioProjects/yimaru_app/test/services/dio_service_test.dart new file mode 100644 index 0000000..0651d4c --- /dev/null +++ b/StudioProjects/yimaru_app/test/services/dio_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('DioServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/services/secure_storage_service_test.dart b/StudioProjects/yimaru_app/test/services/secure_storage_service_test.dart new file mode 100644 index 0000000..d6f4985 --- /dev/null +++ b/StudioProjects/yimaru_app/test/services/secure_storage_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('SecureStorageServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/account_privacy_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/account_privacy_viewmodel_test.dart new file mode 100644 index 0000000..d687122 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/account_privacy_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('AccountPrivacyViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/call_support_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/call_support_viewmodel_test.dart new file mode 100644 index 0000000..52150a2 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/call_support_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('CallSupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/downloads_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/downloads_viewmodel_test.dart new file mode 100644 index 0000000..342bdcd --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/downloads_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('DownloadsViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/home_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/home_viewmodel_test.dart index 5405153..0c5d802 100644 --- a/StudioProjects/yimaru_app/test/viewmodels/home_viewmodel_test.dart +++ b/StudioProjects/yimaru_app/test/viewmodels/home_viewmodel_test.dart @@ -14,14 +14,6 @@ void main() { setUp(() => registerServices()); tearDown(() => locator.reset()); - group('incrementCounter -', () { - test('When called once should return Counter is: 1', () { - final model = getModel(); - model.incrementCounter(); - expect(model.counterLabel, 'Counter is: 1'); - }); - }); - group('showBottomSheet -', () { test( 'When called, should show custom bottom sheet using notice variant', diff --git a/StudioProjects/yimaru_app/test/viewmodels/learn_level_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/learn_level_viewmodel_test.dart new file mode 100644 index 0000000..5d97e54 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/learn_level_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LearnLevelViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/learn_module_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/learn_module_viewmodel_test.dart new file mode 100644 index 0000000..0080f51 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/learn_module_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LearnModuleViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/learn_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/learn_viewmodel_test.dart new file mode 100644 index 0000000..cf11c9c --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/learn_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LearnViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/login_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/login_viewmodel_test.dart new file mode 100644 index 0000000..8ff6466 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/login_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LoginViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/ongoing_progress_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/ongoing_progress_viewmodel_test.dart new file mode 100644 index 0000000..94a3ed4 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/ongoing_progress_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('OngoingProgressViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/privacy_policy_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/privacy_policy_viewmodel_test.dart new file mode 100644 index 0000000..81c404c --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/privacy_policy_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('PrivacyPolicyViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/profile_detail_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/profile_detail_viewmodel_test.dart new file mode 100644 index 0000000..a1a9504 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/profile_detail_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ProfileDetailViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/profile_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/profile_viewmodel_test.dart new file mode 100644 index 0000000..c72bc93 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/profile_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ProfileViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/progress_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/progress_viewmodel_test.dart new file mode 100644 index 0000000..6f79cf5 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/progress_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ProgressViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/register_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/register_viewmodel_test.dart new file mode 100644 index 0000000..78e03a5 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/register_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('RegisterViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/support_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/support_viewmodel_test.dart new file mode 100644 index 0000000..260dc47 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/support_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('SupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/telegram_support_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/telegram_support_viewmodel_test.dart new file mode 100644 index 0000000..1605ed4 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/telegram_support_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('TelegramSupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/terms_and_conditions_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/terms_and_conditions_viewmodel_test.dart new file mode 100644 index 0000000..7da4d4a --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/terms_and_conditions_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('TermsAndConditionsViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/windows/flutter/generated_plugin_registrant.cc b/StudioProjects/yimaru_app/windows/flutter/generated_plugin_registrant.cc index 8b6d468..0c50753 100644 --- a/StudioProjects/yimaru_app/windows/flutter/generated_plugin_registrant.cc +++ b/StudioProjects/yimaru_app/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/StudioProjects/yimaru_app/windows/flutter/generated_plugins.cmake b/StudioProjects/yimaru_app/windows/flutter/generated_plugins.cmake index b93c4c3..4fc759c 100644 --- a/StudioProjects/yimaru_app/windows/flutter/generated_plugins.cmake +++ b/StudioProjects/yimaru_app/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST