fix: Apply UAT comments

This commit is contained in:
BisratHailu 2026-05-22 07:28:37 +03:00
parent 8007954a09
commit 60016afee4
82 changed files with 1770 additions and 3307 deletions

BIN
assets/images/landing_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/images/landing_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
assets/images/landing_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 448 KiB

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

View File

@ -32,18 +32,21 @@
"code_sent_to_phone": "ኮዱ ወደ ስልክ ቁጥርዎ ተልኳል", "code_sent_to_phone": "ኮዱ ወደ ስልክ ቁጥርዎ ተልኳል",
"code_sent_to_email": "ኮዱ ወደ ኢሜል ተልኳል", "code_sent_to_email": "ኮዱ ወደ ኢሜል ተልኳል",
"resend_code_in": "ኮዱን እንደገና ለመላክ የቀረው ጊዜ", "resend_code_in": "ኮዱን እንደገና ለመላክ የቀረው ጊዜ",
"reset_password": " የይለፍ ቃልን ይቀይሩ ", "reset_password": " የይለፍ ቃልን ይቀይሩ",
"enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።" , "enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።" ,
"please_wait": "እባክዎ ይጠብቁ", "please_wait": "እባክዎ ይጠብቁ",
"reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል" , "reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል" ,
"reset_code": " የመቀየሪያ ኮድ ", "reset_code": " የመቀየሪያ ኮድ ",
"new_password": "አዲስ የይለፍ ቃል", "new_password": "አዲስ የይለፍ ቃል",
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል", "logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
"view_course": " ኮርሱን ይመልከቱ ", "view_course": " ኮርሱን ይመልከቱ",
"take_practice": " ልምምድ ያድርጉ ", "continue_learning": "መማርን ይቀጥሉ",
"start_learning": "ትምህርትን ይጀምሩ",
"completed": "ተጠናቋል",
"take_practice": " ልምምድ ያድርጉ",
"your_current_level": "የአሁኑ ደረጃዎ", "your_current_level": "የአሁኑ ደረጃዎ",
"overall_progress": "አጠቃላይ እድገት", "overall_progress": "አጠቃላይ እድገት",
"great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው ", "great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው",
"view_module": "ሞጁሉን ይመልከቱ", "view_module": "ሞጁሉን ይመልከቱ",
"progress": "እድገት", "progress": "እድገት",
"keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ", "keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ",
@ -56,7 +59,7 @@
"learn": "ይማሩ ", "learn": "ይማሩ ",
"course": "ኮርስ", "course": "ኮርስ",
"profile": " ፕሮፋይል ", "profile": " ፕሮፋይል ",
"speaking_partner": "የንግግር ጓደኛ ", "speaking_partner": "የንግግር ጓደኛ",
"practice_what_you_learned": "አሁን የተማሩትን እንለማመድ", "practice_what_you_learned": "አሁን የተማሩትን እንለማመድ",
"practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ", "practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ",
"start_practice": "ልምምድ ጀምር", "start_practice": "ልምምድ ጀምር",
@ -65,7 +68,7 @@
"continue_practice": "ልምምዱን ይቀጥሉ", "continue_practice": "ልምምዱን ይቀጥሉ",
"end_session": "ክፍለ ጊዜውን ያብቁ ", "end_session": "ክፍለ ጊዜውን ያብቁ ",
"tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ", "tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ",
"practice_speaking": "ንግግርን ይለማመዱ ", "practice_speaking": "ንግግርን ይለማመዱ",
"tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ", "tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ",
"reply": "እንደገና አዳምጥ", "reply": "እንደገና አዳምጥ",
"cancel": "ይቅር", "cancel": "ይቅር",
@ -73,7 +76,7 @@
"practice_completed": "ልምምዱ ተጠናቅቋል", "practice_completed": "ልምምዱ ተጠናቅቋል",
"great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው", "great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው",
"practice_again": "እንደገና ይለማመዱ", "practice_again": "እንደገና ይለማመዱ",
"conversation_review": "የንግግር ግምገማ ", "conversation_review": "የንግግር ግምገማ",
"result": "ውጤት", "result": "ውጤት",
"quick_tip": "ጠቃሚ ምክር", "quick_tip": "ጠቃሚ ምክር",
"retry": "እንደገና ይሞክሩ", "retry": "እንደገና ይሞክሩ",
@ -90,8 +93,33 @@
"phone_number": "የስልክ ቁጥር", "phone_number": "የስልክ ቁጥር",
"country": "ሀገር", "country": "ሀገር",
"region": "ክልል", "region": "ክልል",
"occupation": "የስራ መስክ ", "select_region": "ክልል ይምረጡ",
"save_changes": "ለውጦችን ያስቀምጡ" "enter_your_city": "ከተማዎን ያስገቡ",
"occupation": "የስራ መስክ",
"select_occupation": "ሙያዎን ይምረጡ",
"save_changes": "ለውጦችን ያስቀምጡ",
"my_progress": "የእኔ እድገት",
"track_your_achievement": "ስኬቶችዎን እና ተከታታይ የትምህርት ጉዞዎን ይከታተሉ",
"account_and_privacy": "መለያ እና ግላዊነት",
"manage_settings": "ቅንብሮችን እና የመተግበሪያ ምርጫዎችን ያስተዳድሩ",
"support": "ድጋፍ",
"get_help": "በስልክ ወይም በቴሌግራም እገዛ ያግኙ",
"logout": "ውጣ",
"app_settings": "የመተግበሪያ ቅንብሮች",
"legal_and_information": "ሕጋዊ እና መረጃ",
"change_language": "ቋንቋ ቀይር",
"terms_and_conditions": "ውሎች እና ሁኔታዎች",
"delete_account": "መለያ ሰርዝ",
"language_preference": "የቋንቋ ምርጫ",
"choose_your_language": "ለውጦችን አስቀምጥ",
"switch_language_anytime": "ቋንቋዎችን በማንኛውም ጊዜ መቀየር ይችላሉ",
"need_help": "እገዛ ይፈልጋሉ?",
"call_support": "የስልክ ድጋፍ",
"talk_with_support": "በቀጥታ ከድጋፍ ቡድናችን ጋር ይነጋገሩ",
"telegram_support": "የቴሌግራም ድጋፍ",
"chat_via_telegram": "በቴሌግራም በፍጥነት ይወያዩ",
"call_our_support": "ከ3 ጠዋት እስከ 12 ማታ ድረስ የድጋፍ ቡድናችንን ይደውሉ",
"tap_to_call": "ለመደወል ይንኩ"
} }

View File

@ -39,6 +39,9 @@
"reset_code": "Reset code", "reset_code": "Reset code",
"new_password": "New password", "new_password": "New password",
"logged_in_successfully": "Logged in successfully", "logged_in_successfully": "Logged in successfully",
"continue_learning": "Continue Learning",
"start_learning": "Start Learning",
"completed": "Completed",
"view_course": "View course", "view_course": "View course",
"take_practice": "Take practice", "take_practice": "Take practice",
"your_current_level": "Your current level", "your_current_level": "Your current level",
@ -90,6 +93,32 @@
"phone_number": "Phone number", "phone_number": "Phone number",
"country": "Country", "country": "Country",
"region": "Region", "region": "Region",
"select_region": "Select region",
"enter_your_city": "Enter your city",
"occupation": "Occupation", "occupation": "Occupation",
"save_changes": "Save changes" "select_occupation": "Select occupation",
"save_changes": "Save changes",
"my_progress": "My progress",
"track_your_achievement": "Track your achievements and learning streak",
"account_and_privacy": "Account & Privacy",
"manage_settings": "Manage settings and app preference",
"support": "Support",
"get_help": "Get help through phone or Telegram",
"logout": "Logout",
"app_settings": "App settings",
"legal_and_information": "Legal & Information",
"change_language": "Change language",
"terms_and_conditions":"Terms & Conditions",
"delete_account": "Delete account",
"language_preference": "Language preference",
"choose_your_language": "Choose your language",
"switch_language_anytime": "You can switch languages anytime",
"need_help": "Need help?",
"call_support": "Call support",
"talk_with_support": "Talk with our support team directly",
"telegram_support": "Telegram support",
"chat_via_telegram" :"Chat instantly via Telegram",
"call_our_support": "Call our support team between 9 AM - 6 PM",
"tap_to_call": "Tap to call"
} }

View File

@ -24,7 +24,6 @@ import 'package:yimaru_app/services/api_service.dart';
import 'package:yimaru_app/services/secure_storage_service.dart'; import 'package:yimaru_app/services/secure_storage_service.dart';
import 'package:yimaru_app/services/dio_service.dart'; import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/services/status_checker_service.dart'; import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/views/welcome/welcome_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'; import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
import 'package:yimaru_app/services/permission_handler_service.dart'; import 'package:yimaru_app/services/permission_handler_service.dart';
import 'package:yimaru_app/services/image_picker_service.dart'; import 'package:yimaru_app/services/image_picker_service.dart';
@ -33,10 +32,8 @@ import 'package:yimaru_app/services/image_downloader_service.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'; import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart'; import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'; import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart';
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart'; import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart'; import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import 'package:yimaru_app/ui/views/course_lesson/course_lesson_view.dart';
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart'; import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
import 'package:yimaru_app/services/notification_service.dart'; import 'package:yimaru_app/services/notification_service.dart';
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart'; import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
@ -45,7 +42,6 @@ import 'package:yimaru_app/services/course_service.dart';
import 'package:yimaru_app/ui/views/course/course_view.dart'; import 'package:yimaru_app/ui/views/course/course_view.dart';
import 'package:yimaru_app/services/audio_player_service.dart'; import 'package:yimaru_app/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart'; import 'package:yimaru_app/services/voice_recorder_service.dart';
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_view.dart';
import 'package:yimaru_app/services/in_app_update_service.dart'; import 'package:yimaru_app/services/in_app_update_service.dart';
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart'; import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart'; import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
@ -60,6 +56,7 @@ import 'package:yimaru_app/ui/views/course_catalog/course_catalog_view.dart';
import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart'; import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart';
import 'package:yimaru_app/services/localization_service.dart'; import 'package:yimaru_app/services/localization_service.dart';
import 'package:yimaru_app/ui/views/landing/landing_view.dart'; import 'package:yimaru_app/ui/views/landing/landing_view.dart';
import 'package:yimaru_app/ui/views/course_module/course_module_view.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -81,19 +78,15 @@ import 'package:yimaru_app/ui/views/landing/landing_view.dart';
MaterialRoute(page: RegisterView), MaterialRoute(page: RegisterView),
MaterialRoute(page: LoginView), MaterialRoute(page: LoginView),
MaterialRoute(page: LearnModuleView), MaterialRoute(page: LearnModuleView),
MaterialRoute(page: WelcomeView),
MaterialRoute(page: LearnLessonView), MaterialRoute(page: LearnLessonView),
MaterialRoute(page: ForgetPasswordView), MaterialRoute(page: ForgetPasswordView),
MaterialRoute(page: LearnLessonDetailView), MaterialRoute(page: LearnLessonDetailView),
MaterialRoute(page: LearnPracticeView), MaterialRoute(page: LearnPracticeView),
MaterialRoute(page: CoursePracticeView),
MaterialRoute(page: CoursePaymentView), MaterialRoute(page: CoursePaymentView),
MaterialRoute(page: FailureView), MaterialRoute(page: FailureView),
MaterialRoute(page: CourseLessonView),
MaterialRoute(page: CourseLessonDetailView), MaterialRoute(page: CourseLessonDetailView),
MaterialRoute(page: DuolingoView), MaterialRoute(page: DuolingoView),
MaterialRoute(page: CourseView), MaterialRoute(page: CourseView),
MaterialRoute(page: CoursePracticeQuestionView),
MaterialRoute(page: LearnProgramView), MaterialRoute(page: LearnProgramView),
MaterialRoute(page: LearnCourseView), MaterialRoute(page: LearnCourseView),
MaterialRoute(page: AssessmentView), MaterialRoute(page: AssessmentView),
@ -102,6 +95,8 @@ import 'package:yimaru_app/ui/views/landing/landing_view.dart';
MaterialRoute(page: CourseCatalogView), MaterialRoute(page: CourseCatalogView),
MaterialRoute(page: CourseUnitView), MaterialRoute(page: CourseUnitView),
MaterialRoute(page: LandingView), MaterialRoute(page: LandingView),
MaterialRoute(page: CourseModuleView),
MaterialRoute(page: LearnCourseView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [

File diff suppressed because it is too large Load Diff

View File

@ -4,51 +4,35 @@ part 'course_lesson.g.dart';
@JsonSerializable() @JsonSerializable()
class CourseLesson { class CourseLesson {
int? id; final int? id;
String? title; final String? title;
int? duration; final String? thumbnail;
String? status; final String? description;
String? thumbnail;
String? resolution;
String? visibility;
String? description;
@JsonKey(name: 'video_url') @JsonKey(name: 'video_url')
String? videoUrl; final String? videoUrl;
@JsonKey(name: 'vimeo_status') @JsonKey(name: 'sort_order')
String? vimeoStatus; final int? sortOrder;
@JsonKey(name: 'instructor_id') @JsonKey(name: 'has_practice')
int? instructorId; final bool? hasPractice;
@JsonKey(name: 'sub_course_id') @JsonKey(name: 'unit_module_id')
int? courseId; final int? unitModuleId;
@JsonKey(name: 'display_order') const CourseLesson(
int? displayOrder;
CourseLesson(
{this.id, {this.id,
this.title, this.title,
this.status,
this.courseId,
this.videoUrl, this.videoUrl,
this.duration, this.sortOrder,
this.thumbnail, this.thumbnail,
this.visibility,
this.resolution,
this.vimeoStatus,
this.description, this.description,
this.displayOrder, this.hasPractice,
this.instructorId}); this.unitModuleId});
factory CourseLesson.fromJson(Map<String, dynamic> json) => factory CourseLesson.fromJson(Map<String, dynamic> json) =>
_$CourseLessonFromJson(json); _$CourseLessonFromJson(json);

View File

@ -9,32 +9,22 @@ part of 'course_lesson.dart';
CourseLesson _$CourseLessonFromJson(Map<String, dynamic> json) => CourseLesson( CourseLesson _$CourseLessonFromJson(Map<String, dynamic> json) => CourseLesson(
id: (json['id'] as num?)?.toInt(), id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?, title: json['title'] as String?,
status: json['status'] as String?,
courseId: (json['sub_course_id'] as num?)?.toInt(),
videoUrl: json['video_url'] as String?, videoUrl: json['video_url'] as String?,
duration: (json['duration'] as num?)?.toInt(), sortOrder: (json['sort_order'] as num?)?.toInt(),
thumbnail: json['thumbnail'] as String?, thumbnail: json['thumbnail'] as String?,
visibility: json['visibility'] as String?,
resolution: json['resolution'] as String?,
vimeoStatus: json['vimeo_status'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
displayOrder: (json['display_order'] as num?)?.toInt(), hasPractice: json['has_practice'] as bool?,
instructorId: (json['instructor_id'] as num?)?.toInt(), unitModuleId: (json['unit_module_id'] as num?)?.toInt(),
); );
Map<String, dynamic> _$CourseLessonToJson(CourseLesson instance) => Map<String, dynamic> _$CourseLessonToJson(CourseLesson instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'title': instance.title, 'title': instance.title,
'duration': instance.duration,
'status': instance.status,
'thumbnail': instance.thumbnail, 'thumbnail': instance.thumbnail,
'resolution': instance.resolution,
'visibility': instance.visibility,
'description': instance.description, 'description': instance.description,
'video_url': instance.videoUrl, 'video_url': instance.videoUrl,
'vimeo_status': instance.vimeoStatus, 'sort_order': instance.sortOrder,
'instructor_id': instance.instructorId, 'has_practice': instance.hasPractice,
'sub_course_id': instance.courseId, 'unit_module_id': instance.unitModuleId,
'display_order': instance.displayOrder,
}; };

View File

@ -2,13 +2,9 @@ import 'package:dio/dio.dart';
import 'package:yimaru_app/models/learn_lesson.dart'; import 'package:yimaru_app/models/learn_lesson.dart';
import 'package:yimaru_app/models/learn_practice.dart'; import 'package:yimaru_app/models/learn_practice.dart';
import 'package:yimaru_app/models/learn_program.dart'; import 'package:yimaru_app/models/learn_program.dart';
import 'package:yimaru_app/models/level.dart';
import 'package:yimaru_app/models/assessment_question.dart'; import 'package:yimaru_app/models/assessment_question.dart';
import 'package:yimaru_app/models/course_catalog.dart'; import 'package:yimaru_app/models/course_catalog.dart';
import 'package:yimaru_app/models/course_lesson.dart'; import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/models/course_progress.dart';
import 'package:yimaru_app/models/course.dart';
import 'package:yimaru_app/models/practice.dart';
import 'package:yimaru_app/models/user.dart'; import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/services/dio_service.dart'; import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart'; import 'package:yimaru_app/ui/common/app_constants.dart';
@ -20,10 +16,7 @@ import '../models/learn_course.dart';
import '../models/learn_module.dart'; import '../models/learn_module.dart';
import '../models/learn_question.dart'; import '../models/learn_question.dart';
import '../models/learn_subscription.dart'; import '../models/learn_subscription.dart';
import '../models/lesson.dart';
import '../models/module.dart';
import '../models/assessment.dart'; import '../models/assessment.dart';
import '../models/submodule.dart';
import '../models/learn_subscription_request.dart'; import '../models/learn_subscription_request.dart';
import '../ui/common/enmus.dart'; import '../ui/common/enmus.dart';
@ -779,303 +772,20 @@ class ApiService {
} }
} }
/* TO BE MODIFIED*/
// Get courses
// Future<List<Course>> getCourses(int id) async {
// try {
// List<Course> courses = [];
//
// final Response response = await _service.dio
// .get('$kBaseUrl/$kCourseBaseUrl/$kCoursesUrl/$id/$kSubcoursesUrl');
//
// if (response.statusCode == 200) {
// var data = response.data;
// var decodedData = data['data']['sub_courses'] as List;
// courses = decodedData.map(
// (e) {
// return Course.fromJson(e);
// },
// ).toList();
// return courses;
// }
// return [];
// } catch (e) {
// return [];
// }
// }
// Get course progress
Future<List<CourseProgress>> getCourseProgress(int id) async {
try {
List<CourseProgress> courseProgress = [];
final Response response =
await _service.dio.get('$kBaseUrl/$kCourseProgressUrl/$id');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
courseProgress = decodedData.map(
(e) {
return CourseProgress.fromJson(e);
},
).toList();
return courseProgress;
}
return [];
} catch (e) {
return [];
}
}
// Get course lessons // Get course lessons
Future<List<CourseLesson>> getCourseLessons(int id) async { Future<List<CourseLesson>> getCourseLessons(int id) async {
try { try {
List<CourseLesson> courseLessons = []; List<CourseLesson> lessons = [];
final Response response = await _service.dio.get( final Response response = await _service.dio.get(
'$kBaseUrl/$kCourseBaseUrl/$kSubcoursesUrl/$id/$kPublishedVideos'); '$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kModulesUrl/$id/$kLessonsUrl');
if (response.statusCode == 200) { if (response.statusCode == 200) {
var data = response.data; var data = response.data;
var decodedData = data['data'] as List; var decodedData = data['data']['lessons'] as List;
courseLessons = decodedData.map(
(e) {
return CourseLesson.fromJson(e);
},
).toList();
return courseLessons;
}
return [];
} catch (e) {
return [];
}
}
// Complete lesson
Future<Map<String, dynamic>> completeLesson(int id) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteUrl',
);
if (response.statusCode == 200) {
return {'status': ResponseStatus.success, 'message': 'Video completed'};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Course practices
Future<List<Practice>> getCoursePractices(int id) async {
try {
List<Practice> coursePractices = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice?owner_type=SUB_COURSE&owner_id=$id');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
coursePractices = decodedData.map(
(e) {
return Practice.fromJson(e);
},
).toList();
return coursePractices;
}
return [];
} catch (e) {
return [];
}
}
// Get course practic questions
Future<List<AssessmentQuestion>> getCoursePracticeQuestions(int id) async {
try {
List<AssessmentQuestion> coursePracticeQuestions = [];
final Response response = await _service.dio
.get('$kBaseUrl/$kPracticeBaseUrl/$id/$kCoursePracticeQuestions');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
coursePracticeQuestions = decodedData.map(
(e) {
return AssessmentQuestion.fromJson(e);
},
).toList();
return coursePracticeQuestions;
}
return [];
} catch (e) {
return [];
}
}
// Get course practice question
Future<AssessmentQuestion?> getCoursePracticeQuestion(int id) async {
try {
final Response response =
await _service.dio.get('$kBaseUrl/$kCoursePracticeQuestion/$id');
if (response.statusCode == 200) {
AssessmentQuestion question =
AssessmentQuestion.fromJson(response.data['data']);
return question;
}
return null;
} catch (e) {
return null;
}
}
// Get learn subcategories
Future<List<CourseCatalog>> getLearnSubcategories() async {
try {
List<CourseCatalog> learnSubcategories = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLearnSubcategoriesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['sub_categories'] as List;
learnSubcategories = decodedData.map(
(e) {
return CourseCatalog.fromJson(e);
},
).toList();
return learnSubcategories;
}
return [];
} catch (e) {
return [];
}
}
// Get courses
Future<List<Course>> getCourses(int id) async {
try {
List<Course> courses = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kSubcategoriesUrl/$id/$kCoursesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['courses'] as List;
courses = decodedData.map(
(e) {
return Course.fromJson(e);
},
).toList();
return courses;
}
return [];
} catch (e) {
return [];
}
}
// Get levels
Future<List<Level>> getLevels(int id) async {
try {
List<Level> levels = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kCoursesUrl/$id/$kLevelsUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['levels'] as List;
levels = decodedData.map(
(e) {
return Level.fromJson(e);
},
).toList();
return levels;
}
return [];
} catch (e) {
return [];
}
}
// Get modules
Future<List<Module>> getModules(int id) async {
try {
List<Module> modules = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLevelsUrl/$id/$kModulesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['modules'] as List;
modules = decodedData.map(
(e) {
return Module.fromJson(e);
},
).toList();
return modules;
}
return [];
} catch (e) {
return [];
}
}
// Get submodules
Future<List<Submodule>> getSubmodules(int id) async {
try {
List<Submodule> submodules = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kModulesUrl/$id/$kSubmodulesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['sub_modules'] as List;
submodules = decodedData.map(
(e) {
return Submodule.fromJson(e);
},
).toList();
return submodules;
}
return [];
} catch (e) {
return [];
}
}
// Get lessons
Future<List<Lesson>> getLessons(int id) async {
try {
List<Lesson> lessons = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kSubmodulesUrl/$id/$kLessonsUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
lessons = decodedData.map( lessons = decodedData.map(
(e) { (e) {
return Lesson.fromJson(e); return CourseLesson.fromJson(e);
}, },
).toList(); ).toList();
return lessons; return lessons;
@ -1085,52 +795,4 @@ class ApiService {
return []; return [];
} }
} }
// Practices
Future<List<Practice>> getPractices(int id) async {
try {
List<Practice> coursePractices = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice?owner_type=SUB_MODULE&owner_id=$id');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
coursePractices = decodedData.map(
(e) {
return Practice.fromJson(e);
},
).toList();
return coursePractices;
}
return [];
} catch (e) {
return [];
}
}
// Questions
Future<List<AssessmentQuestion>> getQuestions(int id) async {
try {
List<AssessmentQuestion> questions = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kQuestionSetsUrl/$id/$kQuestionsUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] as List;
questions = decodedData.map(
(e) {
return AssessmentQuestion.fromJson(e);
},
).toList();
return questions;
}
return [];
} catch (e) {
return [];
}
}
} }

View File

@ -3,10 +3,14 @@ import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/user.dart'; import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/services/secure_storage_service.dart'; import 'package:yimaru_app/services/secure_storage_service.dart';
import 'localization_service.dart';
class AuthenticationService with ListenableServiceMixin { class AuthenticationService with ListenableServiceMixin {
// Dependency injection // Dependency injection
final _secureService = locator<SecureStorageService>(); final _secureService = locator<SecureStorageService>();
final _localizationService = locator<LocalizationService>();
// User data // User data
User? _user; User? _user;
@ -14,7 +18,7 @@ class AuthenticationService with ListenableServiceMixin {
// Initialization // Initialization
AuthenticationService() { AuthenticationService() {
listenToReactiveValues([_user]); listenToReactiveValues([_user, _localizationService]);
} }
// Check user logged in // Check user logged in
@ -172,9 +176,12 @@ class AuthenticationService with ListenableServiceMixin {
// Logout // Logout
Future<void> logout() async { Future<void> logout() async {
bool firstTimeInstall = await isFirstTimeInstall(); bool firstTimeInstall = await isFirstTimeInstall();
String language = await _localizationService.selectedLanguage['code'];
_user = null; _user = null;
await _secureService.clear(); await _secureService.clear();
await setFirstTimeInstall(firstTimeInstall); await setFirstTimeInstall(firstTimeInstall);
await _secureService.setString('language', language);
notifyListeners(); notifyListeners();
} }
} }

View File

@ -1,10 +1,9 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/course_progress.dart';
import 'package:yimaru_app/services/api_service.dart'; import 'package:yimaru_app/services/api_service.dart';
import '../models/course_catalog.dart'; import '../models/course_catalog.dart';
import '../models/course_detail.dart'; import '../models/course_lesson.dart';
import '../models/course_module.dart'; import '../models/course_module.dart';
import '../models/course_unit.dart'; import '../models/course_unit.dart';
@ -28,10 +27,15 @@ class CourseService with ListenableServiceMixin {
List<CourseUnit> get units => _units; List<CourseUnit> get units => _units;
// Course modules // Course modules
List<CourseModule> _modules = []; final List<CourseModule> _modules = [];
List<CourseModule> get modules => _modules; List<CourseModule> get modules => _modules;
// Course lessons
List<CourseLesson> _lessons = [];
List<CourseLesson> get lessons => _lessons;
// Course catalogs // Course catalogs
Future<void> getCourseCatalogs() async { Future<void> getCourseCatalogs() async {
_catalogs = await _apiService.getCourseCatalogs(); _catalogs = await _apiService.getCourseCatalogs();
@ -47,7 +51,7 @@ class CourseService with ListenableServiceMixin {
} }
// Course modules // Course modules
Future<void> getCourseUnitModule({ Future<void> getCourseModules({
required int id, required int id,
required int index, required int index,
}) async { }) async {
@ -66,26 +70,10 @@ class CourseService with ListenableServiceMixin {
notifyListeners(); notifyListeners();
} }
Future<void> getCourseModules(int id) async { // Course units
_modules = await _apiService.getCourseModules(id); Future<void> getCourseLessons(int id) async {
_modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0)); _lessons = await _apiService.getCourseLessons(id);
_lessons.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
notifyListeners(); notifyListeners();
} }
// Get course detail
Future<List<CourseDetail>> getCoursesDetail(int id) async {
final courses = await _apiService.getCourses(id);
final progress = await _apiService.getCourseProgress(id);
final progressMap = {
for (var p in progress.whereType<CourseProgress>()) p.courseId: p
};
return courses.map((course) {
return CourseDetail(
course: course,
courseProgress: progressMap[course.id],
);
}).toList();
}
} }

View File

@ -1,8 +1,14 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
import '../app/app.locator.dart';
class LocalizationService with ListenableServiceMixin { class LocalizationService with ListenableServiceMixin {
// Dependency injection
final _secureService = locator<SecureStorageService>();
// Initialization // Initialization
localizationService() { localizationService() {
listenToReactiveValues([_selectedLanguage]); listenToReactiveValues([_selectedLanguage]);
@ -10,7 +16,7 @@ class LocalizationService with ListenableServiceMixin {
// Languages // Languages
Map<String, dynamic> _selectedLanguage = { Map<String, dynamic> _selectedLanguage = {
'code': 'EN', 'code': 'en',
'language': 'English' 'language': 'English'
}; };
@ -18,7 +24,7 @@ class LocalizationService with ListenableServiceMixin {
final List<Map<String, dynamic>> _languages = [ final List<Map<String, dynamic>> _languages = [
{'code': 'አማ', 'language': 'አማርኛ'}, {'code': 'አማ', 'language': 'አማርኛ'},
{'code': 'EN', 'language': 'English'}, {'code': 'en', 'language': 'English'},
]; ];
List<Map<String, dynamic>> get languages => _languages; List<Map<String, dynamic>> get languages => _languages;
@ -34,14 +40,33 @@ class LocalizationService with ListenableServiceMixin {
if (title['code'] == 'አማ') { if (title['code'] == 'አማ') {
await setAmharicLanguage(context); await setAmharicLanguage(context);
} else { } else {
await setAmharicLanguage(context); await setEnglishLanguage(context);
} }
notifyListeners(); notifyListeners();
} }
Future<void> setAmharicLanguage(BuildContext context) async => Future<void> loadSelectedLanguage() async {
await context.setLocale(const Locale('am')); String language = await _secureService.getString('language') ?? 'en';
Future<void> setEnglishLanguage(BuildContext context) async => if (language == 'en') {
await context.setLocale(const Locale('en')); _selectedLanguage = {'code': 'en', 'language': 'English'};
} else {
_selectedLanguage = {'code': 'አማ', 'language': 'አማርኛ'};
}
notifyListeners();
print('SELECTED LANGUAGE: $language $_selectedLanguage');
}
Future<void> setAmharicLanguage(BuildContext context) async {
await context.setLocale(const Locale('am'));
await _secureService.setString('language', 'am');
notifyListeners();
}
Future<void> setEnglishLanguage(BuildContext context) async {
await context.setLocale(const Locale('en'));
await _secureService.setString('language', 'en');
notifyListeners();
}
} }

View File

@ -40,7 +40,6 @@ enum StateObjects {
learnCourses, learnCourses,
profileImage, profileImage,
learnPrograms, learnPrograms,
courseModules,
courseLessons, courseLessons,
profileUpdate, profileUpdate,
resetPassword, resetPassword,

View File

@ -6,7 +6,7 @@ import 'dart:ui';
import 'package:easy_localization/easy_localization.dart' show AssetLoader; import 'package:easy_localization/easy_localization.dart' show AssetLoader;
class CodegenLoader extends AssetLoader { class CodegenLoader extends AssetLoader{
const CodegenLoader(); const CodegenLoader();
@override @override
@ -14,203 +14,251 @@ class CodegenLoader extends AssetLoader {
return Future.value(mapLocales[locale.toString()]); return Future.value(mapLocales[locale.toString()]);
} }
static const Map<String, dynamic> _am = { static const Map<String,dynamic> _am = {
"loading": "በመጫን ላይ", "loading": "በመጫን ላይ",
"welcome_back": "እንኳን በደህና ተመለሱ", "welcome_back": "እንኳን በደህና ተመለሱ",
"checking_user_info": "የተጠቃሚ መረጃን በማረጋገጥ ላይ", "checking_user_info": "የተጠቃሚ መረጃን በማረጋገጥ ላይ",
"dont_have_account": "መለያ የለዎትም? ይመዝገቡ", "dont_have_account": "መለያ የለዎትም? ይመዝገቡ",
"email": "ኢሜይል", "email": "ኢሜይል",
"password": "የይለፍ ቃል", "password": "የይለፍ ቃል",
"forgot_password": "የይለፍ ቃል ረሱ?", "forgot_password": "የይለፍ ቃል ረሱ?",
"cont": "ቀጥል", "cont": "ቀጥል",
"register": "ይመዝገቡ", "register": "ይመዝገቡ",
"login_with_google": "በጉግል ይግቡ", "login_with_google": "በጉግል ይግቡ",
"or": "ወይም", "or": "ወይም",
"login_with_phone": "በስልክ ቁጥር ይግቡ", "login_with_phone": "በስልክ ቁጥር ይግቡ",
"create_account": "አዲስ መለያ ይፍጠሩ", "create_account": "አዲስ መለያ ይፍጠሩ",
"already_have_account": "መለያ አለዎት?", "already_have_account": "መለያ አለዎት?",
"login": " ይግቡ ", "login": " ይግቡ ",
"register_with_google": "በጉግል ይመዝገቡ", "register_with_google": "በጉግል ይመዝገቡ",
"register_with_phone": "በስልክ ቁጥር ይመዝገቡ", "register_with_phone": "በስልክ ቁጥር ይመዝገቡ",
"enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።", "enter_phone_number": "የስልክ ቁጥርዎን ያስገቡ። የማረጋገጫ ኮድ እንልክልዎታለን።",
"login_with_email": "በኢሜይል ይግቡ", "login_with_email": "በኢሜይል ይግቡ",
"create_password": "የይለፍ ቃል ይፍጠሩ", "create_password": "የይለፍ ቃል ይፍጠሩ",
"confirm_password": "የይለፍ ቃል ያረጋግጡ", "confirm_password": "የይለፍ ቃል ያረጋግጡ",
"eight_character_minimum": "ቢያንስ 8 ፊደላት", "eight_character_minimum": "ቢያንስ 8 ፊደላት",
"password_match": "የይለፍ ቃሉ ተመሳስሏል", "password_match": "የይለፍ ቃሉ ተመሳስሏል",
"sign_up_agreement": "sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።",
"‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።", "terms_of_services": "የአገልግሎት ውሎች",
"terms_of_services": "የአገልግሎት ውሎች", "and": "እና",
"and": "እና", "privacy_policy": "የግላዊነት ፖሊሲ",
"privacy_policy": "የግላዊነት ፖሊሲ", "register_with_email": "በኢሜል ይመዝገቡ",
"register_with_email": "በኢሜል ይመዝገቡ", "verification_code": "የማረጋገጫ ኮድ",
"verification_code": "የማረጋገጫ ኮድ", "resend_code": "ኮዱን እንደገና ላክ",
"resend_code": "ኮዱን እንደገና ላክ", "code_sent_to_phone": "ኮዱ ወደ ስልክ ቁጥርዎ ተልኳል",
"code_sent_to_phone": "ኮዱ ወደ ስልክ ቁጥርዎ ተልኳል", "code_sent_to_email": "ኮዱ ወደ ኢሜል ተልኳል",
"code_sent_to_email": "ኮዱ ወደ ኢሜል ተልኳል", "resend_code_in": "ኮዱን እንደገና ለመላክ የቀረው ጊዜ",
"resend_code_in": "ኮዱን እንደገና ለመላክ የቀረው ጊዜ", "reset_password": " የይለፍ ቃልን ይቀይሩ",
"reset_password": " የይለፍ ቃልን ይቀይሩ ", "enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።",
"enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።", "please_wait": "እባክዎ ይጠብቁ",
"please_wait": "እባክዎ ይጠብቁ", "reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል",
"reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል", "reset_code": " የመቀየሪያ ኮድ ",
"reset_code": " የመቀየሪያ ኮድ ", "new_password": "አዲስ የይለፍ ቃል",
"new_password": "አዲስ የይለፍ ቃል", "logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል", "view_course": " ኮርሱን ይመልከቱ",
"view_course": " ኮርሱን ይመልከቱ ", "continue_learning": "መማርን ይቀጥሉ",
"take_practice": " ልምምድ ያድርጉ ", "start_learning": "ትምህርትን ይጀምሩ",
"your_current_level": "የአሁኑ ደረጃዎ", "completed": "ተጠናቋል",
"overall_progress": "አጠቃላይ እድገት", "take_practice": " ልምምድ ያድርጉ",
"great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው ", "your_current_level": "የአሁኑ ደረጃዎ",
"view_module": "ሞጁሉን ይመልከቱ", "overall_progress": "አጠቃላይ እድገት",
"progress": "እድገት", "great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው",
"keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ", "view_module": "ሞጁሉን ይመልከቱ",
"lessons_in_module": " በዚህ ሞጁል ውስጥ ያሉ ትምህርቶች ", "progress": "እድገት",
"practice": "ልምምድ", "keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ",
"start": "ጀምር", "lessons_in_module": " በዚህ ሞጁል ውስጥ ያሉ ትምህርቶች ",
"in_progress": "በሂደት ላይ", "practice": "ልምምድ",
"hello": "ሰላም", "start": "ጀምር",
"ready_to_learn": " ዛሬ እንግሊዝኛ ለመማር ተዘጋጅተዋል? ", "in_progress": "በሂደት ላይ",
"learn": "ይማሩ ", "hello": "ሰላም",
"course": "ኮርስ", "ready_to_learn": " ዛሬ እንግሊዝኛ ለመማር ተዘጋጅተዋል? ",
"profile": " ፕሮፋይል ", "learn": "ይማሩ ",
"speaking_partner": "የንግግር ጓደኛ ", "course": "ኮርስ",
"practice_what_you_learned": "አሁን የተማሩትን እንለማመድ", "profile": " ፕሮፋይል ",
"practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ", "speaking_partner": "የንግግር ጓደኛ",
"start_practice": "ልምምድ ጀምር", "practice_what_you_learned": "አሁን የተማሩትን እንለማመድ",
"almost_there": "ሊጨርሱ ተቃርበዋል", "practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ",
"finish_session": "እድገትዎን ለማየት ክፍለ ጊዜውን ያጠናቅቁ", "start_practice": "ልምምድ ጀምር",
"continue_practice": "ልምምዱን ይቀጥሉ", "almost_there": "ሊጨርሱ ተቃርበዋል",
"end_session": "ክፍለ ጊዜውን ያብቁ ", "finish_session": "እድገትዎን ለማየት ክፍለ ጊዜውን ያጠናቅቁ",
"tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ", "continue_practice": "ልምምዱን ይቀጥሉ",
"practice_speaking": "ንግግርን ይለማመዱ ", "end_session": "ክፍለ ጊዜውን ያብቁ ",
"tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ", "tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ",
"reply": "እንደገና አዳምጥ", "practice_speaking": "ንግግርን ይለማመዱ",
"cancel": "ይቅር", "tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ",
"you_are_speaking": "እየተናገሩ ነው", "reply": "እንደገና አዳምጥ",
"practice_completed": "ልምምዱ ተጠናቅቋል", "cancel": "ይቅር",
"great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው", "you_are_speaking": "እየተናገሩ ነው",
"practice_again": "እንደገና ይለማመዱ", "practice_completed": "ልምምዱ ተጠናቅቋል",
"conversation_review": "የንግግር ግምገማ ", "great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው",
"result": "ውጤት", "practice_again": "እንደገና ይለማመዱ",
"quick_tip": "ጠቃሚ ምክር", "conversation_review": "የንግግር ግምገማ",
"retry": "እንደገና ይሞክሩ", "result": "ውጤት",
"completed_a1": "እንኳን ደስ አለዎት! A1 ደረጃን አጠናቅቀዋል", "quick_tip": "ጠቃሚ ምክር",
"analyzing_speaking": "የንግግር ችሎታዎን እየገመገምን ነው", "retry": "እንደገና ይሞክሩ",
"view_profile": "ፕሮፋይሎን ይመልከቱ ", "completed_a1": "እንኳን ደስ አለዎት! A1 ደረጃን አጠናቅቀዋል",
"hi": "ሰላም", "analyzing_speaking": "የንግግር ችሎታዎን እየገመገምን ነው",
"edit_profile": "መገለጫ ያስተካክሉ", "view_profile": "ፕሮፋይሎን ይመልከቱ ",
"first_name": "የመጀመሪያ ስም", "hi": "ሰላም",
"last_name": "የአባት ስም", "edit_profile": "መገለጫ ያስተካክሉ",
"gender": "ፆታ", "first_name": "የመጀመሪያ ስም",
"male": "ወንድ", "last_name": "የአባት ስም",
"female": "ሴት", "gender": "ፆታ",
"phone_number": "የስልክ ቁጥር", "male": "ወንድ",
"country": "ሀገር", "female": "ሴት",
"region": "ክልል", "phone_number": "የስልክ ቁጥር",
"occupation": "የስራ መስክ ", "country": "ሀገር",
"save_changes": "ለውጦችን ያስቀምጡ" "region": "ክልል",
}; "select_region": "ክልል ይምረጡ",
static const Map<String, dynamic> _en = { "enter_your_city": "ከተማዎን ያስገቡ",
"loading": "Loading", "occupation": "የስራ መስክ",
"welcome_back": "Welcome back", "select_occupation": "ሙያዎን ይምረጡ",
"checking_user_info": "Checking user info", "save_changes": "ለውጦችን ያስቀምጡ",
"dont_have_account": "Don't have an account? Register", "my_progress": "የእኔ እድገት",
"email": "Email", "track_your_achievement": "ስኬቶችዎን እና ተከታታይ የትምህርት ጉዞዎን ይከታተሉ",
"password": "Password", "account_and_privacy": "መለያ እና ግላዊነት",
"forgot_password": "Forgot password?", "manage_settings": "ቅንብሮችን እና የመተግበሪያ ምርጫዎችን ያስተዳድሩ",
"cont": "Continue", "support": "ድጋፍ",
"register": "Register", "get_help": "በስልክ ወይም በቴሌግራም እገዛ ያግኙ",
"login_with_google": "Login with Google", "logout": "ውጣ",
"or": "Or", "app_settings": "የመተግበሪያ ቅንብሮች",
"login_with_phone": "Login with phone number", "legal_and_information": "ሕጋዊ እና መረጃ",
"create_account": "Create an account", "change_language": "ቋንቋ ቀይር",
"already_have_account": "Already have an account?", "terms_and_conditions": "ውሎች እና ሁኔታዎች",
"login": "Login", "delete_account": "መለያ ሰርዝ",
"register_with_google": "Register with Google", "language_preference": "የቋንቋ ምርጫ",
"register_with_phone": "Register with phone number", "choose_your_language": "ለውጦችን አስቀምጥ",
"enter_phone_number": "switch_language_anytime": "ቋንቋዎችን በማንኛውም ጊዜ መቀየር ይችላሉ",
"Enter your phone number. We will send you a confirmation code there.", "need_help": "እገዛ ይፈልጋሉ?",
"login_with_email": "Login with email", "call_support": "የስልክ ድጋፍ",
"create_password": "Create password", "talk_with_support": "በቀጥታ ከድጋፍ ቡድናችን ጋር ይነጋገሩ",
"confirm_password": "Confirm password", "telegram_support": "የቴሌግራም ድጋፍ",
"eight_character_minimum": "8 characters minimum", "chat_via_telegram": "በቴሌግራም በፍጥነት ይወያዩ",
"password_math": "password match", "call_our_support": "ከ3 ጠዋት እስከ 12 ማታ ድረስ የድጋፍ ቡድናችንን ይደውሉ",
"sign_up_agreement": "tap_to_call": "ለመደወል ይንኩ"
"By clicking Sign Up, you agree to our Terms of Service and Privacy Policy", };
"terms_of_services": "Terms of Service", static const Map<String,dynamic> _en = {
"and": "and", "loading": "Loading",
"privacy_policy": "Privacy Policy", "welcome_back": "Welcome back",
"register_with_email": "Register with email", "checking_user_info": "Checking user info",
"verification_code": "Verification Code", "dont_have_account": "Don't have an account? Register",
"resend_code": "Resend Code", "email": "Email",
"code_sent_to_phone": "Code sent to your number", "password": "Password",
"code_sent_to_email": "Code sent to your email", "forgot_password": "Forgot password?",
"resend_code_in": "Resend code in", "cont": "Continue",
"reset_password": "Reset Password", "register": "Register",
"enter_email_reset_code": "login_with_google": "Login with Google",
"Enter your email. We will send you a reset code.", "or": "Or",
"please_wait": "Please wait", "login_with_phone": "Login with phone number",
"reset_code_sent": "Reset code sent successfully", "create_account": "Create an account",
"reset_code": "Reset code", "already_have_account": "Already have an account?",
"new_password": "New password", "login": "Login",
"logged_in_successfully": "Logged in successfully", "register_with_google": "Register with Google",
"view_course": "View course", "register_with_phone": "Register with phone number",
"take_practice": "Take practice", "enter_phone_number": "Enter your phone number. We will send you a confirmation code there.",
"your_current_level": "Your current level", "login_with_email": "Login with email",
"overall_progress": "Overall progress", "create_password": "Create password",
"great_work": "Keep up the great work! You're doing amazing", "confirm_password": "Confirm password",
"view_module": "View module", "eight_character_minimum": "8 characters minimum",
"progress": "Progress", "password_math": "password match",
"keep_going": "Let's keep going - you're more than half there", "sign_up_agreement": "By clicking Sign Up, you agree to our Terms of Service and Privacy Policy",
"lessons_in_module": "Lessons in this module", "terms_of_services": "Terms of Service",
"practice": "Practice", "and": "and",
"start": "Start", "privacy_policy": "Privacy Policy",
"in_progress": "In Progress", "register_with_email": "Register with email",
"hello": "Hello", "verification_code": "Verification Code",
"ready_to_learn": "Ready to keep learning English today", "resend_code": "Resend Code",
"learn": "Learn", "code_sent_to_phone": "Code sent to your number",
"course": "Course", "code_sent_to_email": "Code sent to your email",
"profile": "Profile", "resend_code_in": "Resend code in",
"speaking_partner": "Speaking partner", "reset_password": "Reset Password",
"practice_what_you_learned": "Let's practice what you just learnt", "enter_email_reset_code": "Enter your email. We will send you a reset code.",
"practice_questions": "I will ask you a few questions and you can respond", "please_wait": "Please wait",
"start_practice": "Start practice", "reset_code_sent": "Reset code sent successfully",
"almost_there": "You're almost there", "reset_code": "Reset code",
"finish_session": "Finish the session to see your progress", "new_password": "New password",
"continue_practice": "Continue practice", "logged_in_successfully": "Logged in successfully",
"end_session": "End session", "continue_learning": "Continue Learning",
"tap_start_to_listen": "Tap the start button to listen", "start_learning": "Start Learning",
"practice_speaking": "Practice speaking", "completed": "Completed",
"tap_microphone": "Tap the microphone to speak", "view_course": "View course",
"reply": "Reply", "take_practice": "Take practice",
"cancel": "Cancel", "your_current_level": "Your current level",
"you_are_speaking": "You're speaking", "overall_progress": "Overall progress",
"practice_completed": "Practice completed", "great_work": "Keep up the great work! You're doing amazing",
"great_improvement": "view_module": "View module",
"You sound more confident this time, great improvement", "progress": "Progress",
"practice_again": "Practice again", "keep_going": "Let's keep going - you're more than half there",
"conversation_review": "Conversation review", "lessons_in_module": "Lessons in this module",
"result": "Result", "practice": "Practice",
"quick_tip": "Quick tip", "start": "Start",
"retry": "Retry", "in_progress": "In Progress",
"completed_a1": "Yay, you've completed A1", "hello": "Hello",
"analyzing_speaking": "We're now analyzing your speaking skill", "ready_to_learn": "Ready to keep learning English today",
"view_profile": "View profile", "learn": "Learn",
"hi": "Hi", "course": "Course",
"edit_profile": "Edit profile", "profile": "Profile",
"first_name": "First name", "speaking_partner": "Speaking partner",
"last_name": "Last name", "practice_what_you_learned": "Let's practice what you just learnt",
"gender": "Gender", "practice_questions": "I will ask you a few questions and you can respond",
"male": "Male", "start_practice": "Start practice",
"female": "Female", "almost_there": "You're almost there",
"phone_number": "Phone number", "finish_session": "Finish the session to see your progress",
"country": "Country", "continue_practice": "Continue practice",
"region": "Region", "end_session": "End session",
"occupation": "Occupation", "tap_start_to_listen": "Tap the start button to listen",
"save_changes": "Save changes" "practice_speaking": "Practice speaking",
}; "tap_microphone": "Tap the microphone to speak",
static const Map<String, Map<String, dynamic>> mapLocales = { "reply": "Reply",
"am": _am, "cancel": "Cancel",
"en": _en "you_are_speaking": "You're speaking",
}; "practice_completed": "Practice completed",
"great_improvement": "You sound more confident this time, great improvement",
"practice_again": "Practice again",
"conversation_review": "Conversation review",
"result": "Result",
"quick_tip": "Quick tip",
"retry": "Retry",
"completed_a1": "Yay, you've completed A1",
"analyzing_speaking": "We're now analyzing your speaking skill",
"view_profile": "View profile",
"hi": "Hi",
"edit_profile": "Edit profile",
"first_name": "First name",
"last_name": "Last name",
"gender": "Gender",
"male": "Male",
"female": "Female",
"phone_number": "Phone number",
"country": "Country",
"region": "Region",
"select_region": "Select region",
"enter_your_city": "Enter your city",
"occupation": "Occupation",
"select_occupation": "Select occupation",
"save_changes": "Save changes",
"my_progress": "My progress",
"track_your_achievement": "Track your achievements and learning streak",
"account_and_privacy": "Account & Privacy",
"manage_settings": "Manage settings and app preference",
"support": "Support",
"get_help": "Get help through phone or Telegram",
"logout": "Logout",
"app_settings": "App settings",
"legal_and_information": "Legal & Information",
"change_language": "Change language",
"terms_and_conditions": "Terms & Conditions",
"delete_account": "Delete account",
"language_preference": "Language preference",
"choose_your_language": "Choose your language",
"switch_language_anytime": "You can switch languages anytime",
"need_help": "Need help?",
"call_support": "Call support",
"talk_with_support": "Talk with our support team directly",
"telegram_support": "Telegram support",
"chat_via_telegram": "Chat instantly via Telegram",
"call_our_support": "Call our support team between 9 AM - 6 PM",
"tap_to_call": "Tap to call"
};
static const Map<String, Map<String,dynamic>> mapLocales = {"am": _am, "en": _en};
} }

View File

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names
abstract class LocaleKeys { abstract class LocaleKeys {
static const loading = 'loading'; static const loading = 'loading';
static const welcome_back = 'welcome_back'; static const welcome_back = 'welcome_back';
static const checking_user_info = 'checking_user_info'; static const checking_user_info = 'checking_user_info';
@ -44,6 +44,9 @@ abstract class LocaleKeys {
static const new_password = 'new_password'; static const new_password = 'new_password';
static const logged_in_successfully = 'logged_in_successfully'; static const logged_in_successfully = 'logged_in_successfully';
static const view_course = 'view_course'; static const view_course = 'view_course';
static const continue_learning = 'continue_learning';
static const start_learning = 'start_learning';
static const completed = 'completed';
static const take_practice = 'take_practice'; static const take_practice = 'take_practice';
static const your_current_level = 'your_current_level'; static const your_current_level = 'your_current_level';
static const overall_progress = 'overall_progress'; static const overall_progress = 'overall_progress';
@ -92,6 +95,32 @@ abstract class LocaleKeys {
static const phone_number = 'phone_number'; static const phone_number = 'phone_number';
static const country = 'country'; static const country = 'country';
static const region = 'region'; static const region = 'region';
static const select_region = 'select_region';
static const enter_your_city = 'enter_your_city';
static const occupation = 'occupation'; static const occupation = 'occupation';
static const select_occupation = 'select_occupation';
static const save_changes = 'save_changes'; static const save_changes = 'save_changes';
static const my_progress = 'my_progress';
static const track_your_achievement = 'track_your_achievement';
static const account_and_privacy = 'account_and_privacy';
static const manage_settings = 'manage_settings';
static const support = 'support';
static const get_help = 'get_help';
static const logout = 'logout';
static const app_settings = 'app_settings';
static const legal_and_information = 'legal_and_information';
static const change_language = 'change_language';
static const terms_and_conditions = 'terms_and_conditions';
static const delete_account = 'delete_account';
static const language_preference = 'language_preference';
static const choose_your_language = 'choose_your_language';
static const switch_language_anytime = 'switch_language_anytime';
static const need_help = 'need_help';
static const call_support = 'call_support';
static const talk_with_support = 'talk_with_support';
static const telegram_support = 'telegram_support';
static const chat_via_telegram = 'chat_via_telegram';
static const call_our_support = 'call_our_support';
static const tap_to_call = 'tap_to_call';
} }

View File

@ -197,12 +197,8 @@ TextStyle style18W600 = const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style25W400 = const TextStyle( TextStyle style25W400 =
fontSize: 25, const TextStyle(fontSize: 25, color: kcWhite, fontWeight: FontWeight.w400);
color: kcWhite,
fontWeight: FontWeight.w400
);
TextStyle style25W600 = const TextStyle( TextStyle style25W600 = const TextStyle(
fontSize: 25, fontSize: 25,
@ -275,6 +271,12 @@ TextStyle style16P600 = const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style16P900 = const TextStyle(
fontSize: 16,
color: kcPrimaryColor,
fontWeight: FontWeight.w900,
);
TextStyle style16DG500 = const TextStyle( TextStyle style16DG500 = const TextStyle(
fontSize: 16, fontSize: 16,
color: kcDarkGrey, color: kcDarkGrey,

View File

@ -1,5 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/widgets/custom_list_tile.dart'; import 'package:yimaru_app/ui/widgets/custom_list_tile.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
@ -57,7 +59,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar( Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop, onPop: viewModel.pop,
title: 'Account Privacy', title: LocaleKeys.account_and_privacy.tr(),
); );
Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) => Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) =>
@ -92,12 +94,12 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
List<Widget> _buildMenuColumnChildren(AccountPrivacyViewModel viewModel) => [ List<Widget> _buildMenuColumnChildren(AccountPrivacyViewModel viewModel) => [
verticalSpaceLarge, verticalSpaceLarge,
_buildHeader('App Settings'), _buildHeader(LocaleKeys.app_settings.tr()),
verticalSpaceSmall, verticalSpaceSmall,
_buildLanguageMenu(viewModel), _buildLanguageMenu(viewModel),
_buildDividerWrapper(), _buildDividerWrapper(),
verticalSpaceMedium, verticalSpaceMedium,
_buildHeader('Legal & Information'), _buildHeader(LocaleKeys.legal_and_information.tr()),
verticalSpaceSmall, verticalSpaceSmall,
_buildTermsAndConditionsMenu(viewModel), _buildTermsAndConditionsMenu(viewModel),
_buildPrivacyPolicy(viewModel), _buildPrivacyPolicy(viewModel),
@ -112,23 +114,23 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) => Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
CustomListTile( CustomListTile(
isLanguage: true, isLanguage: true,
language: 'English',
icon: Icons.language, icon: Icons.language,
title: 'Change Language', title: LocaleKeys.change_language.tr(),
language: viewModel.selectedLanguage['language'],
onTap: () async => await viewModel.navigateToLanguage(), onTap: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTermsAndConditionsMenu(AccountPrivacyViewModel viewModel) => Widget _buildTermsAndConditionsMenu(AccountPrivacyViewModel viewModel) =>
CustomListTile( CustomListTile(
icon: Icons.handshake, icon: Icons.handshake,
title: 'Terms & Conditions', title: LocaleKeys.terms_and_conditions.tr(),
onTap: () async => await viewModel.navigateToTerms(), onTap: () async => await viewModel.navigateToTerms(),
); );
Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) => Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) =>
CustomListTile( CustomListTile(
title: 'Privacy Policy',
icon: Icons.shield_moon_outlined, icon: Icons.shield_moon_outlined,
title: LocaleKeys.privacy_policy.tr(),
onTap: () async => await viewModel.navigateToPrivacyPolicy(), onTap: () async => await viewModel.navigateToPrivacyPolicy(),
); );
@ -146,8 +148,8 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildDeleteButton() => CustomElevatedButton( Widget _buildDeleteButton() => CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'Delete Account',
foregroundColor: kcRed, foregroundColor: kcRed,
text: LocaleKeys.delete_account.tr(),
backgroundColor: kcRed.withOpacity(0.25), backgroundColor: kcRed.withOpacity(0.25),
); );
} }

View File

@ -3,11 +3,24 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../services/localization_service.dart';
class AccountPrivacyViewModel extends BaseViewModel { class AccountPrivacyViewModel extends ReactiveViewModel {
// Dependency injection // Dependency injection
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _localizationService = locator<LocalizationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[ _localizationService];
// Languages
Map<String, dynamic> get _selectedLanguage =>
_localizationService.selectedLanguage;
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();

View File

@ -1,6 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_constants.dart'; import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
@ -51,7 +53,7 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar( Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop, onPop: viewModel.pop,
title: 'Call Support', title: LocaleKeys.call_support.tr(),
); );
Widget _buildExpandedColumn(CallSupportViewModel viewModel) => Widget _buildExpandedColumn(CallSupportViewModel viewModel) =>
@ -91,7 +93,7 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor); const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'Call our support team between 9 AM - 6 PM', LocaleKeys.call_our_support.tr(),
style: style25DG600, style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
@ -111,10 +113,10 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'Tap to Call',
leadingIcon: Icons.call, leadingIcon: Icons.call,
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
text:LocaleKeys.tap_to_call.tr(),
onTap: () async => await viewModel.callSupport(), onTap: () async => await viewModel.callSupport(),
); );
} }

View File

@ -23,7 +23,12 @@ class CourseView extends StackedView<CourseViewModel> {
Widget _buildScaffoldWrapper(CourseViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(CourseViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffoldContainer(viewModel),
);
Widget _buildScaffoldContainer(CourseViewModel viewModel) => Container(
decoration: bgDecoration,
child: _buildScaffold(viewModel),
); );
Widget _buildScaffold(CourseViewModel viewModel) => Widget _buildScaffold(CourseViewModel viewModel) =>

View File

@ -33,7 +33,12 @@ class CourseCatalogView extends StackedView<CourseCatalogViewModel> {
Widget _buildScaffoldWrapper(CourseCatalogViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(CourseCatalogViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffoldContainer(viewModel),
);
Widget _buildScaffoldContainer(CourseCatalogViewModel viewModel) => Container(
decoration: bgDecoration,
child: _buildScaffold(viewModel),
); );
Widget _buildScaffold(CourseCatalogViewModel viewModel) => Widget _buildScaffold(CourseCatalogViewModel viewModel) =>

View File

@ -27,9 +27,6 @@ class CourseCatalogViewModel extends ReactiveViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToCoursePractice(int id) async =>
_navigationService.navigateToCoursePracticeView(id: id);
Future<void> navigateToCourseUnit(CourseCatalog catalog) async => Future<void> navigateToCourseUnit(CourseCatalog catalog) async =>
await _navigationService.navigateToCourseUnitView(catalog: catalog); await _navigationService.navigateToCourseUnitView(catalog: catalog);

View File

@ -1,119 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/ui/widgets/course_lesson_tile.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/small_app_bar.dart';
import 'course_lesson_viewmodel.dart';
class CourseLessonView extends StackedView<CourseLessonViewModel> {
final Course course;
const CourseLessonView({Key? key, required this.course}) : super(key: key);
@override
void onViewModelReady(CourseLessonViewModel viewModel) async {
await viewModel.getCourseLessons(course.id ?? 0);
super.onViewModelReady(viewModel);
}
@override
CourseLessonViewModel viewModelBuilder(BuildContext context) =>
CourseLessonViewModel();
@override
Widget builder(
BuildContext context,
CourseLessonViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CourseLessonViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CourseLessonViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CourseLessonViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CourseLessonViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildLessonColumnWrapper(viewModel),
],
);
Widget _buildAppBar(CourseLessonViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
title: 'Course Detail',
);
Widget _buildLessonColumnWrapper(CourseLessonViewModel viewModel) =>
Expanded(child: _buildLessonColumnScrollView(viewModel));
Widget _buildLessonColumnScrollView(CourseLessonViewModel viewModel) =>
SingleChildScrollView(
child: _buildLessonColumn(viewModel),
);
Widget _buildLessonColumn(CourseLessonViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLessonColumnChildren(viewModel),
);
List<Widget> _buildLessonColumnChildren(CourseLessonViewModel viewModel) =>
[_buildTitle(), verticalSpaceMedium, _buildListViewBuilder(viewModel)];
Widget _buildTitle() => Text(
'${course.title} course lessons',
style: style18DG700,
textAlign: TextAlign.center,
);
Widget _buildListViewBuilder(CourseLessonViewModel viewModel) =>
viewModel.busy(StateObjects.courseLessons)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(CourseLessonViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.courseLessons.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
lesson: viewModel.courseLessons[index],
onPracticeTap: () async =>
await viewModel.navigateToCoursePractice(course.id ?? 0),
onVideoTap: () async => await viewModel
.navigateToCourseLessonDetail(viewModel.courseLessons[index])),
);
Widget _buildTile({
required CourseLesson lesson,
required GestureTapCallback onVideoTap,
required GestureTapCallback onPracticeTap,
}) =>
CourseLessonTile(
lesson: lesson,
onVideoTap: onVideoTap,
onPracticeTap: onPracticeTap,
);
}

View File

@ -1,50 +1,73 @@
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_viewmodel.dart';
import '../../../models/course_lesson.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart'; import '../../widgets/custom_elevated_button.dart';
import '../../widgets/empty_video_player.dart'; import '../../widgets/empty_video_player.dart';
import '../../widgets/small_app_bar.dart'; import '../../widgets/small_app_bar.dart';
import 'course_lesson_detail_viewmodel.dart';
class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> { class CourseLessonDetailView extends StackedView<CourseLessonDetailViewmodel> {
final CourseLesson lesson; final CourseLesson lesson;
const CourseLessonDetailView({Key? key, required this.lesson}) const CourseLessonDetailView({
: super(key: key); Key? key,
required this.lesson,
}) : super(key: key);
Future<void> _navigate(CourseLessonDetailViewmodel viewModel) async {
await viewModel.pause();
// await viewModel.navigateToLearnPractice(lesson.id ?? 0);
}
@override @override
void onViewModelReady(CourseLessonDetailViewModel viewModel) async { void onViewModelReady(CourseLessonDetailViewmodel viewModel) async {
await viewModel.initializePlayer(lesson); await viewModel.initializePlayer(
lessonId: 0,
moduleId: 0,
url: lesson.videoUrl ?? '',
);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@override @override
CourseLessonDetailViewModel viewModelBuilder(BuildContext context) => void onDispose(CourseLessonDetailViewmodel viewModel) {
CourseLessonDetailViewModel(); viewModel.close();
super.onDispose(viewModel);
}
@override
CourseLessonDetailViewmodel viewModelBuilder(BuildContext context) =>
CourseLessonDetailViewmodel();
@override @override
Widget builder( Widget builder(
BuildContext context, BuildContext context,
CourseLessonDetailViewModel viewModel, CourseLessonDetailViewmodel viewModel,
Widget? child, Widget? child,
) => ) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CourseLessonDetailViewModel viewModel) => Widget _buildScaffoldWrapper(CourseLessonDetailViewmodel viewModel) =>
Scaffold( Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffoldContainer(viewModel),
); );
Widget _buildScaffold(CourseLessonDetailViewModel viewModel) => Widget _buildScaffoldContainer(CourseLessonDetailViewmodel viewModel) =>
Container(
decoration: bgDecoration,
child: _buildScaffold(viewModel),
);
Widget _buildScaffold(CourseLessonDetailViewmodel viewModel) =>
SafeArea(child: _buildColumn(viewModel)); SafeArea(child: _buildColumn(viewModel));
Widget _buildColumn(CourseLessonDetailViewModel viewModel) => Column( Widget _buildColumn(CourseLessonDetailViewmodel viewModel) => Column(
children: [ children: [
verticalSpaceMedium, verticalSpaceMedium,
_buildAppBarWrapper(viewModel), _buildAppBarWrapper(viewModel),
@ -52,57 +75,58 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
], ],
); );
Widget _buildAppBarWrapper(CourseLessonDetailViewModel viewModel) => Padding( Widget _buildAppBarWrapper(CourseLessonDetailViewmodel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppBar(viewModel)); child: _buildAppBar(viewModel));
Widget _buildAppBar(CourseLessonDetailViewModel viewModel) => SmallAppBar( Widget _buildAppBar(CourseLessonDetailViewmodel viewModel) => SmallAppBar(
onPop: viewModel.pop, onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
title: lesson.title ?? '',
); );
Widget _buildBodyColumnWrapper(CourseLessonDetailViewModel viewModel) => Widget _buildBodyColumnWrapper(CourseLessonDetailViewmodel viewModel) =>
Expanded( Expanded(
child: _buildBodyColumn(viewModel), child: _buildBodyColumn(viewModel),
); );
Widget _buildBodyColumn(CourseLessonDetailViewModel viewModel) => Column( Widget _buildBodyColumn(CourseLessonDetailViewmodel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel), children: _buildBodyColumnChildren(viewModel),
); );
List<Widget> _buildBodyColumnChildren( List<Widget> _buildBodyColumnChildren(
CourseLessonDetailViewModel viewModel) => CourseLessonDetailViewmodel viewModel) =>
[ [
_buildLevelsColumnWrapper(viewModel), _buildLevelsColumnWrapper(viewModel),
// _buildContinueButtonWrapper(viewModel) if (lesson.hasPractice ?? false) _buildPracticeButtonWrapper(viewModel)
]; ];
Widget _buildLevelsColumnWrapper(CourseLessonDetailViewModel viewModel) => Widget _buildLevelsColumnWrapper(CourseLessonDetailViewmodel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel)); Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(CourseLessonDetailViewModel viewModel) => Widget _buildLevelsColumnScrollView(CourseLessonDetailViewmodel viewModel) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildLevelsColumn(viewModel), child: _buildLevelsColumn(viewModel),
); );
Widget _buildLevelsColumn(CourseLessonDetailViewModel viewModel) => Column( Widget _buildLevelsColumn(CourseLessonDetailViewmodel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel), children: _buildLevelsColumnChildren(viewModel),
); );
List<Widget> _buildLevelsColumnChildren( List<Widget> _buildLevelsColumnChildren(
CourseLessonDetailViewModel viewModel) => CourseLessonDetailViewmodel viewModel) =>
[ [
verticalSpaceLarge, verticalSpaceMedium,
_buildVideoPlayerWrapper(viewModel), _buildVideoPlayerWrapper(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildTitleWrapper(),
verticalSpaceMedium,
_buildDescriptionWrapper(), _buildDescriptionWrapper(),
]; ];
Widget _buildVideoPlayerWrapper(CourseLessonDetailViewModel viewModel) => Widget _buildVideoPlayerWrapper(CourseLessonDetailViewmodel viewModel) =>
Container( Container(
height: 200, height: 200,
color: kcBlack, color: kcBlack,
@ -110,21 +134,32 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
child: _buildVideoPlayerState(viewModel), child: _buildVideoPlayerState(viewModel),
); );
Widget _buildVideoPlayerState(CourseLessonDetailViewModel viewModel) => Widget _buildVideoPlayerState(CourseLessonDetailViewmodel viewModel) =>
viewModel.chewieController != null && viewModel.chewieController != null &&
viewModel.videoPlayerController != null && viewModel.videoPlayerController != null &&
!viewModel.busy(StateObjects.loadCourseVideo) !viewModel.busy(StateObjects.loadLessonVideo)
? _buildVideoPlayer(viewModel) ? _buildVideoPlayer(viewModel)
: _buildEmptyVideoPlayer(); : _buildEmptyVideoPlayer();
Widget _buildVideoPlayer(CourseLessonDetailViewModel viewModel) => Widget _buildVideoPlayer(CourseLessonDetailViewmodel viewModel) =>
_buildChewiePlayer(viewModel); _buildChewiePlayer(viewModel);
Widget _buildChewiePlayer(CourseLessonDetailViewModel viewModel) => Widget _buildChewiePlayer(CourseLessonDetailViewmodel viewModel) => Chewie(
Chewie(controller: viewModel.chewieController!); controller: viewModel.chewieController!,
);
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer(); Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
Widget _buildTitleWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildTitle(),
);
Widget _buildTitle() => Text(
lesson.title ?? '',
style: style16DG600,
);
Widget _buildDescriptionWrapper() => Padding( Widget _buildDescriptionWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildDescription(), child: _buildDescription(),
@ -132,10 +167,10 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
Widget _buildDescription() => Text( Widget _buildDescription() => Text(
lesson.description ?? '', lesson.description ?? '',
style: style14DG600, style: style14DG400,
); );
Widget _buildPracticeButtonWrapper(CourseLessonDetailViewModel viewModel) => Widget _buildPracticeButtonWrapper(CourseLessonDetailViewmodel viewModel) =>
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 15, left: 15,
@ -145,14 +180,13 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
child: _buildPracticeButton(viewModel), child: _buildPracticeButton(viewModel),
); );
Widget _buildPracticeButton(CourseLessonDetailViewModel viewModel) => Widget _buildPracticeButton(CourseLessonDetailViewmodel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
text: 'Practice',
borderRadius: 12, borderRadius: 12,
text: 'Start Assessment',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () async => onTap: () async => await _navigate(viewModel),
await viewModel.navigateToCoursePractice(lesson.courseId ?? 0),
); );
} }

View File

@ -2,23 +2,27 @@ import 'package:chewie/chewie.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/services/api_service.dart'; import 'package:yimaru_app/services/api_service.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../common/app_constants.dart'; import '../../../services/status_checker_service.dart';
import '../../../services/vimeo_service.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
class CourseLessonDetailViewModel extends BaseViewModel { class CourseLessonDetailViewmodel extends ReactiveViewModel {
// Dependency injection // Dependency injection
final _apiService = locator<ApiService>(); final _apiService = locator<ApiService>();
final _vimeoService = locator<VimeoService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Video player config // Video player config
bool _isCompleted = false; bool _lessonCompleted = false;
ChewieController? _chewieController; ChewieController? _chewieController;
@ -29,82 +33,78 @@ class CourseLessonDetailViewModel extends BaseViewModel {
VideoPlayerController? get videoPlayerController => _videoPlayerController; VideoPlayerController? get videoPlayerController => _videoPlayerController;
// Video player // Video player
void close() {
@override
void dispose() {
_videoPlayerController?.dispose(); _videoPlayerController?.dispose();
_chewieController?.dispose(); _chewieController?.dispose();
super.dispose();
} }
Future<void> pause() async { Future<void> pause() async {
await _chewieController?.pause(); await _chewieController?.pause();
} }
Future<void> _videoListener(CourseLesson lesson) async { Future<void> initializePlayer(
final controller = _videoPlayerController; {required String url,
required int lessonId,
required int moduleId}) async =>
await runBusyFuture(
_initializePlayer(url: url, moduleId: moduleId, lessonId: lessonId),
busyObject: StateObjects.loadLessonVideo);
if (controller == null || !controller.value.isInitialized) return; Future<void> _initializePlayer(
{required String url,
required int lessonId,
required int moduleId}) async {
final playableUrl = await _vimeoService.getVideoUrl(url);
final position = controller.value.position; if (playableUrl == null) {
final duration = controller.value.duration; throw Exception("Unable to load video");
if (duration.inSeconds == 0) return;
double progress = position.inSeconds / duration.inSeconds;
print("Video progress: ${(progress * 100).toStringAsFixed(2)}%");
// Example: mark completion at 95%
if (progress >= 0.95) {
await _onVideoCompleted(lesson);
} }
}
Future<void> initializePlayer(CourseLesson lesson) async =>
await runBusyFuture(_initializePlayer(lesson),
busyObject: StateObjects.loadCourseVideo);
Future<void> _initializePlayer(CourseLesson lesson) async {
print('URL: $kSampleVideoUrl');
_videoPlayerController = _videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl)); VideoPlayerController.networkUrl(Uri.parse(playableUrl));
await _videoPlayerController?.initialize(); await _videoPlayerController?.initialize();
_videoPlayerController // Listen for video completion
?.addListener(() async => await _videoListener(lesson)); _videoPlayerController?.addListener(() async {
final controller = _videoPlayerController;
if (_videoPlayerController != null) { if (controller == null || _lessonCompleted) return;
_chewieController = ChewieController(
looping: true,
autoPlay: true,
showOptions: true,
showControls: true,
aspectRatio: 16 / 9,
autoInitialize: true,
allowedScreenSleep: false,
videoPlayerController: _videoPlayerController!,
materialProgressColors: buildChewieProgressIndicator,
);
}
rebuildUi(); final position = controller.value.position.inSeconds;
} final duration = controller.value.duration.inSeconds;
Future<void> _onVideoCompleted(CourseLesson lesson) async { if (duration <= 0) return;
if (_isCompleted) return;
_isCompleted = true; // Calculate watched percentage
final progress = position / duration;
print("Video completed!"); // Mark complete at 95%
if (progress >= 0.95) {
_lessonCompleted = true;
await _apiService.completeLesson(lesson.id ?? 0); /* await completeLearnLesson(
lessonId: lessonId,
moduleId: moduleId,
);*/
}
});
_chewieController = ChewieController(
looping: false,
autoPlay: true,
showOptions: true,
showControls: true,
aspectRatio: 16 / 9,
autoInitialize: true,
allowedScreenSleep: false,
videoPlayerController: _videoPlayerController!,
materialProgressColors: buildChewieProgressIndicator,
);
notifyListeners();
} }
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToCoursePractice(int id) =>
_navigationService.navigateToCoursePracticeView(id: id);
} }

View File

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_module_tile_large.dart';
import '../../../models/course_catalog.dart';
import '../../../models/course_module.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/small_app_bar.dart';
import 'course_module_viewmodel.dart';
class CourseModuleView extends StackedView<CourseModuleViewModel> {
final CourseModule? module;
final CourseCatalog catalog;
const CourseModuleView(
{Key? key, required this.module, required this.catalog})
: super(key: key);
@override
void onViewModelReady(CourseModuleViewModel viewModel) async {
await viewModel.getCourseLessons(module?.id ?? 0);
super.onViewModelReady(viewModel);
}
@override
CourseModuleViewModel viewModelBuilder(BuildContext context) =>
CourseModuleViewModel();
@override
Widget builder(
BuildContext context,
CourseModuleViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CourseModuleViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldContainer(viewModel),
);
Widget _buildScaffoldContainer(CourseModuleViewModel viewModel) => Container(
decoration: bgDecoration,
child: _buildScaffold(viewModel),
);
Widget _buildScaffold(CourseModuleViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CourseModuleViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CourseModuleViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildModulesColumnWrapper(viewModel),
],
);
Widget _buildAppBar(CourseModuleViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
title: 'Module Detail',
);
Widget _buildModulesColumnWrapper(CourseModuleViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(CourseModuleViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(CourseModuleViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(CourseModuleViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildListViewBuilder(viewModel)
];
Widget _buildTitle() => Text(
catalog.name ?? '',
style: style25DG600,
);
Widget _buildListViewBuilder(CourseModuleViewModel viewModel) =>
viewModel.busy(StateObjects.courseLessons)
? _buildProgressIndicator()
: _buildTile(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildTile(CourseModuleViewModel viewModel) => CourseModuleTileLarge(
module: module,
lessons: viewModel.lessons,
onContinueTap: () {},
);
}

View File

@ -1,45 +1,45 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../models/course_lesson.dart'; import '../../../services/course_service.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
class CourseLessonViewModel extends BaseViewModel { class CourseModuleViewModel extends ReactiveViewModel {
// Dependency injection // Dependency injection
final _apiService = locator<ApiService>(); final _courseService = locator<CourseService>();
final _statusChecker = locator<StatusCheckerService>(); final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Course lessons @override
List<CourseLesson> _courseLessons = []; List<ListenableServiceMixin> get listenableServices => [_courseService];
List<CourseLesson> get courseLessons => _courseLessons; // Course lessons
List<CourseLesson> get _lessons => _courseService.lessons;
List<CourseLesson> get lessons => _lessons;
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToCoursePractice(int id) =>
_navigationService.navigateToCoursePracticeView(id: id);
Future<void> navigateToCourseLessonDetail(CourseLesson lesson) async => Future<void> navigateToCourseLessonDetail(CourseLesson lesson) async =>
await _navigationService.navigateToCourseLessonDetailView(lesson: lesson); await _navigationService.navigateToCourseLessonDetailView(lesson: lesson);
// Remote api call // Remote api call
// Course lessons // Course modules
Future<void> getCourseLessons(int courseId) async => Future<void> getCourseLessons(int id) async =>
await runBusyFuture(_getCourseLessons(courseId), await runBusyFuture(_getCourseLessons(id),
busyObject: StateObjects.courseLessons); busyObject: StateObjects.courseLessons);
Future<void> _getCourseLessons(int courseId) async { Future<void> _getCourseLessons(int id) async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
_courseLessons = await _apiService.getCourseLessons(1); await _courseService.getCourseLessons(id);
} }
} }
} }

View File

@ -127,7 +127,7 @@ class CoursePaymentView extends StackedView<CoursePaymentViewModel> {
text: 'Subscribe Now', text: 'Subscribe Now',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.navigateToCourseLesson(course), onTap: () {},
); );
Widget _buildSecurePaymentWrapper() => Align( Widget _buildSecurePaymentWrapper() => Align(

View File

@ -1,7 +1,5 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/course.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
@ -11,7 +9,4 @@ class CoursePaymentViewModel extends BaseViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToCourseLesson(Course course) async =>
await _navigationService.navigateToCourseLessonView(course: course);
} }

View File

@ -1,116 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_practice_card.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'course_practice_viewmodel.dart';
class CoursePracticeView extends StackedView<CoursePracticeViewModel> {
final int id;
const CoursePracticeView({Key? key, required this.id}) : super(key: key);
@override
void onViewModelReady(CoursePracticeViewModel viewModel) async {
await viewModel.getCoursePractice(id);
super.onViewModelReady(viewModel);
}
@override
CoursePracticeViewModel viewModelBuilder(BuildContext context) =>
CoursePracticeViewModel();
@override
Widget builder(
BuildContext context,
CoursePracticeViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CoursePracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CoursePracticeViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CoursePracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CoursePracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildPracticeColumnWrapper(viewModel),
],
);
Widget _buildAppBar(CoursePracticeViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
);
Widget _buildPracticeColumnWrapper(CoursePracticeViewModel viewModel) =>
Expanded(child: _buildPracticeColumnScrollView(viewModel));
Widget _buildPracticeColumnScrollView(CoursePracticeViewModel viewModel) =>
SingleChildScrollView(
child: _buildPracticeColumn(viewModel),
);
Widget _buildPracticeColumn(CoursePracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildPracticeColumnChildren(viewModel),
);
List<Widget> _buildPracticeColumnChildren(
CoursePracticeViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildListView(viewModel)
];
Widget _buildTitle() => Text(
'Course Practices',
style: style18DG700,
);
Widget _buildSubtitle() => Text(
'Select a practice test your progress',
style: style14DG400,
);
Widget _buildListView(CoursePracticeViewModel viewModel) => GridView.builder(
shrinkWrap: true,
itemCount: viewModel.coursePractices.length,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 15,
crossAxisSpacing: 15,
childAspectRatio: 1.2,
),
itemBuilder: (context, index) => _buildCard(
title: viewModel.coursePractices[index].title ?? '',
onTap: () async => await viewModel.navigateToCoursePracticeQuestion(
viewModel.coursePractices[index].id ?? 0),
),
);
Widget _buildCard({
required String title,
GestureTapCallback? onTap,
}) =>
CoursePracticeCard(onTap: onTap, title: title);
}

View File

@ -1,42 +0,0 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/practice.dart';
import '../../../app/app.locator.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class CoursePracticeViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Course practices
List<Practice> _coursePractices = [];
List<Practice> get coursePractices => _coursePractices;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToCoursePracticeQuestion(int id) async =>
await _navigationService.navigateToCoursePracticeQuestionView(id: id);
// Remote api call
// Course practices
Future<void> getCoursePractice(int id) async =>
await runBusyFuture(_getCoursePractice(id),
busyObject: StateObjects.coursePractice);
Future<void> _getCoursePractice(int id) async {
if (await _statusChecker.checkConnection()) {
_coursePractices = await _apiService.getCoursePractices(id);
}
}
}

View File

@ -1,63 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_view.form.dart';
import 'package:yimaru_app/ui/views/course_practice_question/screens/practice_questions_screen.dart';
import 'package:yimaru_app/ui/views/course_practice_question/screens/practice_result_screen.dart';
import '../../common/validators/form_validator.dart';
import 'course_practice_question_viewmodel.dart';
@FormView(fields: [
FormTextField(name: 'answer', validator: FormValidator.validateForm),
])
class CoursePracticeQuestionView
extends StackedView<CoursePracticeQuestionViewModel>
with $CoursePracticeQuestionView {
final int id;
const CoursePracticeQuestionView({Key? key, required this.id})
: super(key: key);
@override
void onViewModelReady(CoursePracticeQuestionViewModel viewModel) async {
await viewModel.getCoursePracticeQuestions(id);
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@override
CoursePracticeQuestionViewModel viewModelBuilder(BuildContext context) =>
CoursePracticeQuestionViewModel();
@override
Widget builder(
BuildContext context,
CoursePracticeQuestionViewModel viewModel,
Widget? child,
) =>
_buildAssessmentScreensWrapper(viewModel);
Widget _buildAssessmentScreensWrapper(
CoursePracticeQuestionViewModel viewModel) =>
PopScope(
canPop: false,
onPopInvokedWithResult: (value, data) => viewModel.previousQuestion(),
child: _buildAssessmentScreens(viewModel));
Widget _buildAssessmentScreens(CoursePracticeQuestionViewModel viewModel) =>
IndexedStack(
index: viewModel.currentPage,
children: _buildScreens(),
);
List<Widget> _buildScreens() => [
_buildPracticeQuestionScreen(),
_buildPracticeResultScreen(),
];
Widget _buildPracticeQuestionScreen() =>
PracticeQuestionsScreen(id: id, answerController: answerController);
Widget _buildPracticeResultScreen() => const PracticeResultScreen();
}

View File

@ -1,181 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
// **************************************************************************
// 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 AnswerValueKey = 'answer';
final Map<String, TextEditingController>
_CoursePracticeQuestionViewTextEditingControllers = {};
final Map<String, FocusNode> _CoursePracticeQuestionViewFocusNodes = {};
final Map<String, String? Function(String?)?>
_CoursePracticeQuestionViewTextValidations = {
AnswerValueKey: FormValidator.validateForm,
};
mixin $CoursePracticeQuestionView {
TextEditingController get answerController =>
_getFormTextEditingController(AnswerValueKey);
FocusNode get answerFocusNode => _getFormFocusNode(AnswerValueKey);
TextEditingController _getFormTextEditingController(
String key, {
String? initialValue,
}) {
if (_CoursePracticeQuestionViewTextEditingControllers.containsKey(key)) {
return _CoursePracticeQuestionViewTextEditingControllers[key]!;
}
_CoursePracticeQuestionViewTextEditingControllers[key] =
TextEditingController(text: initialValue);
return _CoursePracticeQuestionViewTextEditingControllers[key]!;
}
FocusNode _getFormFocusNode(String key) {
if (_CoursePracticeQuestionViewFocusNodes.containsKey(key)) {
return _CoursePracticeQuestionViewFocusNodes[key]!;
}
_CoursePracticeQuestionViewFocusNodes[key] = FocusNode();
return _CoursePracticeQuestionViewFocusNodes[key]!;
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
answerController.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) {
answerController.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({
AnswerValueKey: answerController.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 _CoursePracticeQuestionViewTextEditingControllers.values) {
controller.dispose();
}
for (var focusNode in _CoursePracticeQuestionViewFocusNodes.values) {
focusNode.dispose();
}
_CoursePracticeQuestionViewTextEditingControllers.clear();
_CoursePracticeQuestionViewFocusNodes.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 answerValue => this.formValueMap[AnswerValueKey] as String?;
set answerValue(String? value) {
this.setData(
this.formValueMap..addAll({AnswerValueKey: value}),
);
if (_CoursePracticeQuestionViewTextEditingControllers.containsKey(
AnswerValueKey)) {
_CoursePracticeQuestionViewTextEditingControllers[AnswerValueKey]?.text =
value ?? '';
}
}
bool get hasAnswer =>
this.formValueMap.containsKey(AnswerValueKey) &&
(answerValue?.isNotEmpty ?? false);
bool get hasAnswerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey]?.isNotEmpty ?? false;
String? get answerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey];
}
extension Methods on FormStateHelper {
void setAnswerValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[AnswerValueKey] = validationMessage;
/// Clears text input fields on the Form
void clearForm() {
answerValue = '';
}
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
});
}
}
/// Returns the validation message for the given key
String? getValidationMessage(String key) {
final validatorForKey = _CoursePracticeQuestionViewTextValidations[key];
if (validatorForKey == null) return null;
String? validationMessageForKey = validatorForKey(
_CoursePracticeQuestionViewTextEditingControllers[key]?.text,
);
return validationMessageForKey;
}
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
});

View File

@ -1,202 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../models/option.dart';
import '../../../models/assessment_question.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
class CoursePracticeQuestionViewModel extends FormViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// In-app navigation
int _currentPage = 0;
int get currentPage => _currentPage;
int _previousPage = 0;
int get previousPage => _previousPage;
final PageController _pageController = PageController();
PageController get pageController => _pageController;
// Course practice questions
bool _focusAnswer = false;
bool get focusAnswer => _focusAnswer;
AssessmentQuestion? _currentQuestion;
AssessmentQuestion? get currentQuestion => _currentQuestion;
List<AssessmentQuestion> _coursePracticeQuestions = [];
List<AssessmentQuestion> get coursePracticeQuestions =>
_coursePracticeQuestions;
int _currentQuestionIndex = 0;
int get currentQuestionIndex => _currentQuestionIndex;
final Map<String, dynamic> _selectedAnswers = {};
Map<String, dynamic> get selectedAnswers => _selectedAnswers;
// Question navigation
void previousQuestion() {
if (_currentQuestionIndex != 0) {
_currentQuestionIndex--;
_pageController.previousPage(
duration: const Duration(microseconds: 100), curve: Curves.linear);
rebuildUi();
}
}
// In-app navigation
void goBack() {
if (_currentPage == 0) {
pop();
} else {
_currentPage = 0;
rebuildUi();
}
}
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
void next({int? page}) async {
if (page == null) {
if (_previousPage != 0) {
_currentPage = _previousPage;
} else {
_currentPage++;
}
} else {
_previousPage = _currentPage;
_currentPage = page;
}
rebuildUi();
}
// Answer
void reset() {
_selectedAnswers.clear();
rebuildUi();
}
void setAnswerFocus() {
_focusAnswer = true;
rebuildUi();
}
Future<void> abort() async {
bool? response = await showAbortDialog();
if (response != null && response) {
next(page: 1);
}
}
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(
cancelTitle: 'No',
buttonTitle: 'Yes',
title: 'Abort Practice',
barrierDismissible: true,
cancelTitleColor: kcDarkGrey,
buttonTitleColor: kcPrimaryColor,
description: 'Are you sure to abort the practice ?',
);
return response?.confirmed;
}
bool isSelectedAnswer({required int question, required String answer}) {
return _selectedAnswers[question.toString()]?['option'] == answer;
}
void setSelectedAnswer({required int question, required Option? option}) {
bool correct = false;
if (option?.isCorrect ?? false) {
correct = true;
}
final data = {
question.toString(): {
'correct': correct,
'option': option?.optionText,
'answer': _currentQuestion?.options
?.firstWhere((e) => e.isCorrect ?? false)
.optionText
}
};
_selectedAnswers.addAll(data);
rebuildUi();
}
// Navigation
void pop() => _navigationService.back();
// Remote api call
// Question navigation
Future<void> _nextQuestion(int id) async {
_currentQuestionIndex++;
if (_currentQuestionIndex == _coursePracticeQuestions.length) {
next();
} else {
if (await _statusChecker.checkConnection()) {
_currentQuestion = await _apiService.getCoursePracticeQuestion(id);
_pageController.jumpToPage(_currentQuestionIndex);
}
}
}
Future<void> nextQuestion(int id) async =>
await runBusyFuture(_nextQuestion(id),
busyObject: StateObjects.coursePractice);
// Course practice questions
Future<void> getCoursePracticeQuestions(int id) async =>
await runBusyFuture(_getCoursePracticeQuestions(id),
busyObject: StateObjects.coursePracticeQuestions);
Future<void> _getCoursePracticeQuestions(int id) async {
if (await _statusChecker.checkConnection()) {
_coursePracticeQuestions =
await _apiService.getCoursePracticeQuestions(id);
if (_coursePracticeQuestions.isNotEmpty) {
_currentQuestion = await _apiService
.getCoursePracticeQuestion(coursePracticeQuestions.first.id ?? 0);
}
}
}
// Course practice question
Future<void> getCoursePracticeQuestion(int id) async =>
await runBusyFuture(_getCoursePracticeQuestion(id),
busyObject: StateObjects.coursePractice);
Future<void> _getCoursePracticeQuestion(int id) async {
if (await _statusChecker.checkConnection()) {
_currentQuestion = await _apiService.getCoursePracticeQuestion(id);
}
}
}

View File

@ -1,107 +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/common/enmus.dart';
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import 'package:yimaru_app/ui/widgets/selectable_course_practice_question.dart';
import 'package:yimaru_app/ui/widgets/writing_course_practice_question.dart';
import 'question_loading_screen.dart';
class PracticeQuestionsScreen
extends ViewModelWidget<CoursePracticeQuestionViewModel> {
final int id;
final TextEditingController answerController;
const PracticeQuestionsScreen(
{super.key, required this.id, required this.answerController});
@override
Widget build(
BuildContext context, CoursePracticeQuestionViewModel viewModel) =>
_buildAssessmentScreens(viewModel);
Widget _buildAssessmentScreens(CoursePracticeQuestionViewModel viewModel) =>
viewModel.busy(StateObjects.coursePracticeQuestions) ||
viewModel.busy(StateObjects.coursePracticeQuestion) ||
viewModel.coursePracticeQuestions.isEmpty ||
viewModel.currentQuestion == null
? _buildPageLoadingIndicator(viewModel)
: _buildScaffoldWrapper(viewModel);
Widget _buildPageLoadingIndicator(
CoursePracticeQuestionViewModel viewModel) =>
QuestionLoadingScreen(
onPop: viewModel.coursePracticeQuestions.isEmpty ||
viewModel.currentQuestion == null
? viewModel.pop
: null,
isEmpty: viewModel.coursePracticeQuestions.isEmpty ||
viewModel.currentQuestion == null
? true
: false,
isLoading: viewModel.busy(StateObjects.coursePracticeQuestions) ||
viewModel.busy(StateObjects.coursePracticeQuestion),
onTap: () async => await viewModel.getCoursePracticeQuestions(id),
);
Widget _buildScaffoldWrapper(CoursePracticeQuestionViewModel viewModel) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CoursePracticeQuestionViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(
CoursePracticeQuestionViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(CoursePracticeQuestionViewModel viewModel) => LargeAppBar(
onClose: viewModel.abort,
showLanguageSelection: false,
onPop: viewModel.previousQuestion,
showBackButton: viewModel.currentQuestionIndex == 0 ? false : true,
);
Widget _buildExpandedBody(CoursePracticeQuestionViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(CoursePracticeQuestionViewModel viewModel) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildQuestion(viewModel),
);
Widget _buildQuestion(CoursePracticeQuestionViewModel viewModel) =>
PageView.builder(
controller: viewModel.pageController,
physics: const NeverScrollableScrollPhysics(),
itemCount: viewModel.coursePracticeQuestions.length,
itemBuilder: (cotext, index) =>
_buildQuestionType(index: index, viewModel: viewModel),
);
Widget _buildQuestionType(
{required int index,
required CoursePracticeQuestionViewModel viewModel}) =>
viewModel.currentQuestion?.questionType == 'SHORT_ANSWER'
? _buildWritingCoursePracticeQuestion(index)
: _buildCoursePracticeQuestionWrapper(index);
Widget _buildCoursePracticeQuestionWrapper(int index) =>
SingleChildScrollView(
child: _buildSelectableCoursePracticeQuestion(index),
);
Widget _buildSelectableCoursePracticeQuestion(int index) =>
SelectableCoursePracticeQuestion(index: index);
Widget _buildWritingCoursePracticeQuestion(int index) =>
WritingCoursePracticeQuestion(
index: index, answerController: answerController);
}

View File

@ -1,142 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_viewmodel.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
class PracticeResultScreen
extends ViewModelWidget<CoursePracticeQuestionViewModel> {
const PracticeResultScreen({super.key});
void _retake(CoursePracticeQuestionViewModel viewModel) {
viewModel.reset();
viewModel.goTo(0);
}
@override
Widget build(
BuildContext context, CoursePracticeQuestionViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CoursePracticeQuestionViewModel viewModel) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CoursePracticeQuestionViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(
CoursePracticeQuestionViewModel viewModel) =>
[
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildExpandedBody(viewModel)
];
Widget _buildExpandedBody(CoursePracticeQuestionViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(CoursePracticeQuestionViewModel viewModel) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(CoursePracticeQuestionViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(CoursePracticeQuestionViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildLowerColumn(viewModel)];
Widget _buildUpperColumn(CoursePracticeQuestionViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(
CoursePracticeQuestionViewModel viewModel) =>
[
verticalSpaceLarge,
_buildIcon(),
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
];
Widget _buildAppBar(CoursePracticeQuestionViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: false,
onPop: () => viewModel.pop(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/complete.svg',
);
Widget _buildTitle() => Text(
'Practice Completed',
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Youve finished this practice. Great work!',
style: style14MG400,
textAlign: TextAlign.center,
);
Widget _buildLowerColumn(CoursePracticeQuestionViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(
CoursePracticeQuestionViewModel viewModel) =>
[
_buildContinueButton(viewModel),
verticalSpaceSmall,
_buildSkipButtonWrapper(viewModel)
];
Widget _buildContinueButton(CoursePracticeQuestionViewModel viewModel) =>
CustomElevatedButton(
height: 55,
safe: false,
borderRadius: 12,
text: 'Practice Again',
foregroundColor: kcWhite,
onTap: () => _retake(viewModel),
backgroundColor: kcPrimaryColor,
);
Widget _buildSkipButtonWrapper(CoursePracticeQuestionViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildSkipButton(viewModel),
);
Widget _buildSkipButton(CoursePracticeQuestionViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
onTap: viewModel.pop,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
);
}

View File

@ -1,54 +0,0 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
import '../../../common/app_colors.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/refresh_button.dart';
class QuestionLoadingScreen extends StatelessWidget {
final bool isEmpty;
final bool isLoading;
final GestureTapCallback? onPop;
final GestureTapCallback? onTap;
const QuestionLoadingScreen(
{super.key,
this.onTap,
this.onPop,
required this.isEmpty,
required this.isLoading});
@override
Widget build(BuildContext context) => _buildScaffoldWrapper();
Widget _buildScaffoldWrapper() => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(),
);
Widget _buildScaffold() => Stack(
children: [
_buildColumn(),
if (isEmpty) _buildRefreshButton(),
if (isLoading) _buildPageIndicator()
],
);
Widget _buildColumn() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(),
);
List<Widget> _buildColumnChildren() => [_buildAppBar(), _buildBody()];
Widget _buildAppBar() => LargeAppBar(
onPop: onPop,
showBackButton: true,
showLanguageSelection: false,
);
Widget _buildBody() => Expanded(child: Container());
Widget _buildPageIndicator() => const PageLoadingIndicator();
Widget _buildRefreshButton() => RefreshButton(onTap: onTap);
}

View File

@ -39,7 +39,12 @@ class CourseUnitView extends StackedView<CourseUnitViewModel> {
Widget _buildScaffoldWrapper(CourseUnitViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(CourseUnitViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffoldContainer(viewModel),
);
Widget _buildScaffoldContainer(CourseUnitViewModel viewModel) => Container(
decoration: bgDecoration,
child: _buildScaffold(viewModel),
); );
Widget _buildScaffold(CourseUnitViewModel viewModel) => Widget _buildScaffold(CourseUnitViewModel viewModel) =>
@ -94,7 +99,7 @@ class CourseUnitView extends StackedView<CourseUnitViewModel> {
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
catalog.name ?? '', catalog.name ?? '',
style: style18P600, style: style25DG600,
); );
Widget _buildCourseModuleBanner() => const CourseModuleBanner(); Widget _buildCourseModuleBanner() => const CourseModuleBanner();
@ -130,19 +135,20 @@ class CourseUnitView extends StackedView<CourseUnitViewModel> {
index: index, index: index,
unit: viewModel.units[index], unit: viewModel.units[index],
onPracticeTap: () {}, onPracticeTap: () {},
onLessonTap: () {}), onViewTap: () {}),
); );
Widget _buildTile({ Widget _buildTile({
required int index, required int index,
required CourseUnit unit, required CourseUnit unit,
required GestureTapCallback onLessonTap, required GestureTapCallback onViewTap,
required GestureTapCallback onPracticeTap, required GestureTapCallback onPracticeTap,
}) => }) =>
CourseUnitTile( CourseUnitTile(
unit: unit, unit: unit,
index: index, index: index,
onLessonTap: onLessonTap, catalog: catalog,
onLessonTap: onViewTap,
onPracticeTap: onPracticeTap, onPracticeTap: onPracticeTap,
); );
} }

View File

@ -1,7 +1,10 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/course_catalog.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../models/course_module.dart';
import '../../../models/course_unit.dart'; import '../../../models/course_unit.dart';
import '../../../services/course_service.dart'; import '../../../services/course_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
@ -26,6 +29,12 @@ class CourseUnitViewModel extends ReactiveViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToCourseModule(
{required CourseModule? module,
required CourseCatalog catalog}) async =>
await _navigationService.navigateToCourseModuleView(
module: module, catalog: catalog);
// Remote api call // Remote api call
// Course units // Course units
@ -39,15 +48,13 @@ class CourseUnitViewModel extends ReactiveViewModel {
} }
} }
Future<void> getCourseUnitModules( Future<void> getCourseModules({required int id, required int index}) async =>
{required int id, required int index}) async => await runBusyFuture(_getCourseModules(id: id, index: index),
await runBusyFuture(_getCourseUnitModules(id: id, index: index), busyObject: index);
busyObject: StateObjects.courseModules);
Future<void> _getCourseUnitModules( Future<void> _getCourseModules({required int id, required int index}) async {
{required int id, required int index}) async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
await _courseService.getCourseUnitModule(id: id, index: index); await _courseService.getCourseModules(id: id, index: index);
} }
} }
} }

View File

@ -40,7 +40,7 @@ class FailureView extends StackedView<FailureViewModel> {
]; ];
Widget _buildBackground() => Image.asset( Widget _buildBackground() => Image.asset(
'assets/images/onboarding_1.png', 'assets/images/loading.png',
fit: BoxFit.fill, fit: BoxFit.fill,
width: double.maxFinite, width: double.maxFinite,
height: double.maxFinite, height: double.maxFinite,

View File

@ -1,6 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart'; import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
import 'package:yimaru_app/ui/views/profile/profile_view.dart'; import 'package:yimaru_app/ui/views/profile/profile_view.dart';
@ -44,18 +46,18 @@ class HomeView extends StackedView<HomeViewModel> {
]; ];
BottomNavigationBarItem _buildLearnItem() => BottomNavigationBarItem( BottomNavigationBarItem _buildLearnItem() => BottomNavigationBarItem(
label: 'Learn',
icon: _buildLearnIcon(), icon: _buildLearnIcon(),
label: LocaleKeys.learn.tr(),
); );
BottomNavigationBarItem _buildCourseItem() => BottomNavigationBarItem( BottomNavigationBarItem _buildCourseItem() => BottomNavigationBarItem(
label: 'Course',
icon: _buildCourseIcon(), icon: _buildCourseIcon(),
label: LocaleKeys.course.tr(),
); );
BottomNavigationBarItem _buildProfileItem() => BottomNavigationBarItem( BottomNavigationBarItem _buildProfileItem() => BottomNavigationBarItem(
label: 'Profile',
icon: _buildProfileIcon(), icon: _buildProfileIcon(),
label: LocaleKeys.profile.tr(),
); );
Widget _buildLearnIcon() => const Icon(Icons.school); Widget _buildLearnIcon() => const Icon(Icons.school);

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart'; import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/landing/screens/first_landing_screen.dart'; import 'package:yimaru_app/ui/views/landing/screens/first_landing_screen.dart';
import 'package:yimaru_app/ui/views/landing/screens/fourth_landing_screen.dart';
import 'package:yimaru_app/ui/views/landing/screens/second_landing_screen.dart'; import 'package:yimaru_app/ui/views/landing/screens/second_landing_screen.dart';
import 'package:yimaru_app/ui/views/landing/screens/third_landing_screen.dart'; import 'package:yimaru_app/ui/views/landing/screens/third_landing_screen.dart';
@ -10,11 +11,10 @@ import 'landing_viewmodel.dart';
class LandingView extends StackedView<LandingViewModel> { class LandingView extends StackedView<LandingViewModel> {
const LandingView({Key? key}) : super(key: key); const LandingView({Key? key}) : super(key: key);
@override @override
LandingViewModel viewModelBuilder( LandingViewModel viewModelBuilder(
BuildContext context, BuildContext context,
) => ) =>
LandingViewModel(); LandingViewModel();
@override @override
@ -22,25 +22,30 @@ class LandingView extends StackedView<LandingViewModel> {
BuildContext context, BuildContext context,
LandingViewModel viewModel, LandingViewModel viewModel,
Widget? child, Widget? child,
)=> _buildLandingScreens(viewModel); ) =>
_buildLandingScreens(viewModel);
Widget _buildLandingScreens(LandingViewModel viewModel) => FlutterCarousel( Widget _buildLandingScreens(LandingViewModel viewModel) => FlutterCarousel(
options: FlutterCarouselOptions( options: FlutterCarouselOptions(
autoPlay: true, autoPlay: true,
viewportFraction: 1, viewportFraction: 1,
showIndicator: true, showIndicator: true,
indicatorMargin: 40, indicatorMargin: 40,
height: double.maxFinite, height: double.maxFinite,
slideIndicator: CircularSlideIndicator( slideIndicator: CircularSlideIndicator(
slideIndicatorOptions: slideIndicatorOptions:
const SlideIndicatorOptions(indicatorRadius: 2.5), const SlideIndicatorOptions(indicatorRadius: 2.5),
), ),
), ),
items: _buildScreens(), items: _buildScreens(),
); );
List<Widget> _buildScreens() => List<Widget> _buildScreens() => [
[_buildFirstWelcome(), _buildSecondWelcome(), _buildThirdWelcome()]; _buildFirstWelcome(),
_buildSecondWelcome(),
_buildThirdWelcome(),
_buildFourthWelcome()
];
Widget _buildFirstWelcome() => const FirstLandingScreen(); Widget _buildFirstWelcome() => const FirstLandingScreen();
@ -48,5 +53,5 @@ class LandingView extends StackedView<LandingViewModel> {
Widget _buildThirdWelcome() => const ThirdLandingScreen(); Widget _buildThirdWelcome() => const ThirdLandingScreen();
Widget _buildFourthWelcome() => const FourthLandingScreen();
} }

View File

@ -19,22 +19,20 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
body: _buildScaffoldPadding(viewModel), body: _buildScaffoldPadding(viewModel),
); );
Widget _buildScaffoldPadding(LandingViewModel viewModel)=> Padding( Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(viewModel),); child: _buildScaffold(viewModel),
);
Widget _buildScaffold(LandingViewModel viewModel) => Column( Widget _buildScaffold(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(viewModel), children: _buildScaffoldChildren(viewModel),
); );
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) => List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
[ _buildUpperColumn(),_buildLowerColumnWrapper(viewModel)]; [_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
Widget _buildUpperColumn() => Column( Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -42,43 +40,38 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
children: _buildUpperColumnChildren(), children: _buildUpperColumnChildren(),
); );
List<Widget> _buildUpperColumnChildren() => [ List<Widget> _buildUpperColumnChildren() =>
verticalSpaceLarge, [verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
_buildIconWrapper(),
verticalSpaceLarge
]; Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
Widget _buildIconWrapper()=> Align(alignment: Alignment.topLeft,child: _buildIcon(),); child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg', 'assets/icons/logo.svg',
height: 25, height: 25,
); );
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded( Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
child: _buildLowerColumn(viewModel), child: _buildLowerColumn(viewModel),
); );
Widget _buildLowerColumn(LandingViewModel viewModel) => Column( Widget _buildLowerColumn(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(viewModel), children: _buildLowerColumnChildren(viewModel),
); );
List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [
_buildTitle(), _buildTitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildImageWrapper(), _buildImageWrapper(),
verticalSpaceMedium, verticalSpaceMedium,
_buildSafeWrapper(viewModel) _buildSafeWrapper(viewModel)
]; ];
Widget _buildTitle() => Widget _buildTitle() => Text.rich(
Text.rich(
TextSpan( TextSpan(
text: 'እንግሊዝኛ\n', text: 'እንግሊዝኛ\n',
style: style25W600, style: style25W600,
@ -90,29 +83,26 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
TextSpan( TextSpan(
text: ' ሰዓት ', text: ' ሰዓት ',
style: style25W600, style: style25W600,
), ),
TextSpan( TextSpan(
text: 'ይማሩ!', text: 'ይማሩ!',
style: style25W400, style: style25W400,
), ),
], ],
), ),
); );
Widget _buildImageWrapper()=> Expanded(child: _buildImageClipper()); Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper()=> ClipRRect( Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
child: _buildImage(), child: _buildImage(),
); );
Widget _buildImage()=> Image.asset('assets/images/profile.png',fit: BoxFit.cover,);
Widget _buildImage() => Image.asset(
'assets/images/landing_1.jpg',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper(LandingViewModel viewModel) => Widget _buildSafeWrapper(LandingViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel)); SafeArea(child: _buildContinueButtonWrapper(viewModel));

View File

@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
import '../landing_viewmodel.dart';
class FourthLandingScreen extends ViewModelWidget<LandingViewModel> {
const FourthLandingScreen({super.key});
@override
Widget build(BuildContext context, LandingViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LandingViewModel viewModel) => Scaffold(
backgroundColor: Colors.amber,
body: _buildScaffoldPadding(viewModel),
);
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(viewModel),
);
Widget _buildScaffold(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
color: kcPrimaryColor,
height: 25,
);
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
child: _buildLowerColumn(viewModel),
);
Widget _buildLowerColumn(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper(viewModel)
];
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25P600,
children: [
TextSpan(
text: 'በማንኛውም',
style: style25P400,
),
TextSpan(
text: ' እድሜ ',
style: style25P600,
),
TextSpan(
text: 'ይማሩ!',
style: style25P400,
),
],
),
);
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_2.jpg',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel));
Widget _buildContinueButtonWrapper(LandingViewModel viewModel) => Align(
alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel),
);
Widget _buildButtonContainer(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButtonState(viewModel),
);
Widget _buildContinueButtonState(LandingViewModel viewModel) =>
viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildContinueButton(LandingViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 25,
text: 'Get Started',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.setFirstTimeInstall(),
);
}

View File

@ -16,70 +16,63 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LandingViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(LandingViewModel viewModel) => Scaffold(
backgroundColor: Colors.amber, backgroundColor: Colors.amber,
body: _buildScaffoldPadding(viewModel), body: _buildScaffoldPadding(viewModel),
); );
Widget _buildScaffoldPadding(LandingViewModel viewModel)=> Padding( Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(viewModel),); child: _buildScaffold(viewModel),
);
Widget _buildScaffold(LandingViewModel viewModel) => Column( Widget _buildScaffold(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(viewModel), children: _buildScaffoldChildren(viewModel),
); );
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) => List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
[ _buildUpperColumn(),_buildLowerColumnWrapper(viewModel)]; [_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
Widget _buildUpperColumn() => Column( Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(), children: _buildUpperColumnChildren(),
); );
List<Widget> _buildUpperColumnChildren() => [ List<Widget> _buildUpperColumnChildren() =>
verticalSpaceLarge, [verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
_buildIconWrapper(),
verticalSpaceLarge
]; Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
Widget _buildIconWrapper()=> Align(alignment: Alignment.topLeft,child: _buildIcon(),); child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg', 'assets/icons/logo.svg',
color: kcPrimaryColor, color: kcPrimaryColor,
height: 25, height: 25,
); );
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded( Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
child: _buildLowerColumn(viewModel), child: _buildLowerColumn(viewModel),
); );
Widget _buildLowerColumn(LandingViewModel viewModel) => Column( Widget _buildLowerColumn(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(viewModel), children: _buildLowerColumnChildren(viewModel),
); );
List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [
_buildTitle(), _buildTitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildImageWrapper(), _buildImageWrapper(),
verticalSpaceMedium, verticalSpaceMedium,
_buildSafeWrapper(viewModel) _buildSafeWrapper(viewModel)
]; ];
Widget _buildTitle() => Widget _buildTitle() => Text.rich(
Text.rich(
TextSpan( TextSpan(
text: 'እንግሊዝኛ\n', text: 'እንግሊዝኛ\n',
style: style25P600, style: style25P600,
@ -91,42 +84,39 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
TextSpan( TextSpan(
text: ' እድሜ ', text: ' እድሜ ',
style: style25P600, style: style25P600,
), ),
TextSpan( TextSpan(
text: 'ይማሩ!', text: 'ይማሩ!',
style: style25P400, style: style25P400,
), ),
], ],
), ),
); );
Widget _buildImageWrapper()=> Expanded(child: _buildImageClipper()); Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper()=> ClipRRect( Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
child: _buildImage(), child: _buildImage(),
); );
Widget _buildImage()=> Image.asset('assets/images/profile.png',fit: BoxFit.cover,);
Widget _buildImage() => Image.asset(
'assets/images/landing_2.jpg',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper(LandingViewModel viewModel) => Widget _buildSafeWrapper(LandingViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel)); SafeArea(child: _buildContinueButtonWrapper(viewModel));
Widget _buildContinueButtonWrapper(LandingViewModel viewModel) => Align( Widget _buildContinueButtonWrapper(LandingViewModel viewModel) => Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel), child: _buildButtonContainer(viewModel),
); );
Widget _buildButtonContainer(LandingViewModel viewModel) => Padding( Widget _buildButtonContainer(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50), padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButtonState(viewModel), child: _buildContinueButtonState(viewModel),
); );
Widget _buildContinueButtonState(LandingViewModel viewModel) => Widget _buildContinueButtonState(LandingViewModel viewModel) =>
viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel); viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel);

View File

@ -16,70 +16,63 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LandingViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(LandingViewModel viewModel) => Scaffold(
backgroundColor: kcWhite, backgroundColor: kcWhite,
body: _buildScaffoldPadding(viewModel), body: _buildScaffoldPadding(viewModel),
); );
Widget _buildScaffoldPadding(LandingViewModel viewModel)=> Padding( Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(viewModel),); child: _buildScaffold(viewModel),
);
Widget _buildScaffold(LandingViewModel viewModel) => Column( Widget _buildScaffold(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(viewModel), children: _buildScaffoldChildren(viewModel),
); );
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) => List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
[ _buildUpperColumn(),_buildLowerColumnWrapper(viewModel)]; [_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
Widget _buildUpperColumn() => Column( Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(), children: _buildUpperColumnChildren(),
); );
List<Widget> _buildUpperColumnChildren() => [ List<Widget> _buildUpperColumnChildren() =>
verticalSpaceLarge, [verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
_buildIconWrapper(),
verticalSpaceLarge
]; Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
Widget _buildIconWrapper()=> Align(alignment: Alignment.topLeft,child: _buildIcon(),); child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg', 'assets/icons/logo.svg',
color: kcPrimaryColor, color: kcPrimaryColor,
height: 25, height: 25,
); );
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded( Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
child: _buildLowerColumn(viewModel), child: _buildLowerColumn(viewModel),
); );
Widget _buildLowerColumn(LandingViewModel viewModel) => Column( Widget _buildLowerColumn(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(viewModel), children: _buildLowerColumnChildren(viewModel),
); );
List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [
_buildTitle(), _buildTitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildImageWrapper(), _buildImageWrapper(),
verticalSpaceMedium, verticalSpaceMedium,
_buildSafeWrapper(viewModel) _buildSafeWrapper(viewModel)
]; ];
Widget _buildTitle() => Widget _buildTitle() => Text.rich(
Text.rich(
TextSpan( TextSpan(
text: 'እንግሊዝኛ\n', text: 'እንግሊዝኛ\n',
style: style25P600, style: style25P600,
@ -89,44 +82,41 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
style: style25P400, style: style25P400,
), ),
TextSpan( TextSpan(
text: ' እድሜ ', text: ' ቦታ ',
style: style25P600, style: style25P600,
), ),
TextSpan( TextSpan(
text: 'ይማሩ!', text: 'ይማሩ!',
style: style25P400, style: style25P400,
), ),
], ],
), ),
); );
Widget _buildImageWrapper()=> Expanded(child: _buildImageClipper()); Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper()=> ClipRRect( Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
child: _buildImage(), child: _buildImage(),
); );
Widget _buildImage()=> Image.asset('assets/images/profile.png',fit: BoxFit.cover,);
Widget _buildImage() => Image.asset(
'assets/images/landing_3.jpg',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper(LandingViewModel viewModel) => Widget _buildSafeWrapper(LandingViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel)); SafeArea(child: _buildContinueButtonWrapper(viewModel));
Widget _buildContinueButtonWrapper(LandingViewModel viewModel) => Align( Widget _buildContinueButtonWrapper(LandingViewModel viewModel) => Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel), child: _buildButtonContainer(viewModel),
); );
Widget _buildButtonContainer(LandingViewModel viewModel) => Padding( Widget _buildButtonContainer(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50), padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButtonState(viewModel), child: _buildContinueButtonState(viewModel),
); );
Widget _buildContinueButtonState(LandingViewModel viewModel) => Widget _buildContinueButtonState(LandingViewModel viewModel) =>
viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel); viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel);

View File

@ -1,7 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/translations/locale_keys.g.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
import '../../widgets/custom_small_radio_button.dart'; import '../../widgets/custom_small_radio_button.dart';
import '../../widgets/small_app_bar.dart'; import '../../widgets/small_app_bar.dart';
@ -106,16 +108,16 @@ class LanguageView extends StackedView<LanguageViewModel> {
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar( Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop, onPop: viewModel.pop,
title: 'Language Preference', title:LocaleKeys.language_preference.tr() ,
); );
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'Choose your language', LocaleKeys.choose_your_language.tr(),
style: style25DG600, style: style25DG600,
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'You can switch languages anytime', LocaleKeys.switch_language_anytime.tr() ,
style: style14MG400, style: style14MG400,
); );

View File

@ -122,7 +122,6 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile( itemBuilder: (context, index) => _buildTile(
module: viewModel.modules[index], module: viewModel.modules[index],
onLockTap: () async => await viewModel.navigateToLearnSubscription(),
onPracticeTap: () async => await viewModel.navigateToLearnPractice( onPracticeTap: () async => await viewModel.navigateToLearnPractice(
id: viewModel.modules[index].id ?? 0, id: viewModel.modules[index].id ?? 0,
module: viewModel.modules[index].name ?? ''), module: viewModel.modules[index].name ?? ''),
@ -133,13 +132,11 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
Widget _buildTile({ Widget _buildTile({
required LearnModule module, required LearnModule module,
required GestureTapCallback onLockTap,
required GestureTapCallback onModuleTap, required GestureTapCallback onModuleTap,
required GestureTapCallback onPracticeTap, required GestureTapCallback onPracticeTap,
}) => }) =>
LearnModuleTile( LearnModuleTile(
module: module, module: module,
onLockTap: onLockTap,
onModuleTap: onModuleTap, onModuleTap: onModuleTap,
onPracticeTap: onPracticeTap); onPracticeTap: onPracticeTap);
} }

View File

@ -27,8 +27,7 @@ class LearnModuleViewModel extends ReactiveViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnSubscription() async =>
await _navigationService.navigateToLearnSubscriptionView();
Future<void> navigateToLearnLesson(LearnModule module) async => Future<void> navigateToLearnLesson(LearnModule module) async =>
await _navigationService.navigateToLearnLessonView(module: module); await _navigationService.navigateToLearnLessonView(module: module);

View File

@ -89,12 +89,14 @@ class LearnProgramView extends StackedView<LearnProgramViewModel> {
program: viewModel.learnPrograms[index], program: viewModel.learnPrograms[index],
onTap: () async => await viewModel onTap: () async => await viewModel
.navigateToLearnCourse(viewModel.learnPrograms[index].id ?? 0), .navigateToLearnCourse(viewModel.learnPrograms[index].id ?? 0),
onLockTap: () async => await viewModel.navigateToLearnSubscription(),
), ),
); );
Widget _buildTile({ Widget _buildTile({
required LearnProgram program, required LearnProgram program,
required GestureTapCallback onTap, required GestureTapCallback onTap,
required GestureTapCallback onLockTap,
}) => }) =>
LearnProgramTile(onTap: onTap, program: program); LearnProgramTile(onTap: onTap, program: program,onLockTap: onLockTap,);
} }

View File

@ -39,6 +39,9 @@ class LearnProgramViewModel extends ReactiveViewModel {
Future<void> navigateToLearnCourse(int id) async => Future<void> navigateToLearnCourse(int id) async =>
_navigationService.navigateToLearnCourseView(id: id); _navigationService.navigateToLearnCourseView(id: id);
Future<void> navigateToLearnSubscription() async =>
await _navigationService.navigateToLearnSubscriptionView();
// Remote api call // Remote api call
// Learn programs // Learn programs

View File

@ -1,8 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_strings.dart'; import 'package:yimaru_app/ui/common/app_strings.dart';
import 'package:yimaru_app/ui/widgets/privacy_policy_tile.dart'; import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
@ -129,7 +130,7 @@ class PrivacyPolicyView extends StackedView<PrivacyPolicyViewModel> {
Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar( Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop, onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
title: 'Privacy Policy', title: LocaleKeys.privacy_policy.tr(),
); );
Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) => Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) =>

View File

@ -1,7 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.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_card.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart'; import 'package:yimaru_app/ui/widgets/profile_image.dart';
@ -138,7 +140,7 @@ class ProfileView extends StackedView<ProfileViewModel> {
); );
Widget _buildProfileName(ProfileViewModel viewModel) => Text( Widget _buildProfileName(ProfileViewModel viewModel) => Text(
'Hi, ${viewModel.user?.firstName ?? ''} 👋', '${LocaleKeys.hello.tr()}, ${viewModel.user?.firstName ?? ''} 👋',
style: style25DG600, style: style25DG600,
); );
@ -169,31 +171,31 @@ class ProfileView extends StackedView<ProfileViewModel> {
); );
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
title: 'My Progress',
icon: Icons.stacked_bar_chart, icon: Icons.stacked_bar_chart,
subtitle: 'Track your achievements and learning streak', title: LocaleKeys.my_progress.tr(),
onTap: () async => await viewModel.navigateToProgress(), subtitle: LocaleKeys.track_your_achievement.tr(),
// onTap: () async => await viewModel.navigateToProgress(),
); );
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Account & Privacy',
icon: Icons.privacy_tip_outlined, icon: Icons.privacy_tip_outlined,
subtitle: 'Manage setting and app preference', subtitle: LocaleKeys.manage_settings.tr(),
title: LocaleKeys.account_and_privacy.tr(),
onTap: () async => await viewModel.navigateToAccountPrivacy(), onTap: () async => await viewModel.navigateToAccountPrivacy(),
); );
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Support',
icon: Icons.headphones, icon: Icons.headphones,
subtitle: 'Get help through phone or Telegram', title: LocaleKeys.support.tr(),
subtitle: LocaleKeys.get_help.tr(),
onTap: () async => await viewModel.navigateToSupport(), onTap: () async => await viewModel.navigateToSupport(),
); );
Widget _buildLogOutButton(ProfileViewModel viewModel) => CustomElevatedButton( Widget _buildLogOutButton(ProfileViewModel viewModel) => CustomElevatedButton(
height: 55, height: 55,
text: 'Logout',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcRed, foregroundColor: kcRed,
text: LocaleKeys.logout.tr(),
backgroundColor: kcRed.withOpacity(0.25), backgroundColor: kcRed.withOpacity(0.25),
onTap: () async => await viewModel.logout(), onTap: () async => await viewModel.logout(),
); );

View File

@ -1,8 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart'; import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/widgets/custom_form_label.dart'; import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart'; import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
@ -160,7 +162,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar( Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop, onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
title: 'Edit Profile', title: LocaleKeys.edit_profile.tr(),
); );
Widget _buildColumnWrapper( Widget _buildColumnWrapper(
@ -270,8 +272,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
]; ];
Widget _buildFirstNameLabel() => CustomFormLabel( Widget _buildFirstNameLabel() => CustomFormLabel(
label: 'First Name',
style: style16DG600, style: style16DG600,
label: LocaleKeys.first_name.tr(),
); );
Widget _buildFirstNameFormField(ProfileDetailViewModel viewModel) => Widget _buildFirstNameFormField(ProfileDetailViewModel viewModel) =>
@ -317,8 +319,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
]; ];
Widget _buildLastNameLabel() => CustomFormLabel( Widget _buildLastNameLabel() => CustomFormLabel(
label: 'Last Name',
style: style16DG600, style: style16DG600,
label: LocaleKeys.last_name.tr(),
); );
Widget _buildLastNameFormField(ProfileDetailViewModel viewModel) => Widget _buildLastNameFormField(ProfileDetailViewModel viewModel) =>
@ -356,8 +358,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
]; ];
Widget _buildGenderLabel() => CustomFormLabel( Widget _buildGenderLabel() => CustomFormLabel(
label: 'Gender',
style: style16DG600, style: style16DG600,
label: LocaleKeys.gender.tr(),
); );
Widget _buildRadioButtonWrapper(ProfileDetailViewModel viewModel) => Row( Widget _buildRadioButtonWrapper(ProfileDetailViewModel viewModel) => Row(
@ -449,8 +451,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
]; ];
Widget _buildPhoneNumberLabel() => CustomFormLabel( Widget _buildPhoneNumberLabel() => CustomFormLabel(
label: 'Phone Number',
style: style16DG600, style: style16DG600,
label: LocaleKeys.phone_number.tr(),
); );
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) => Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
@ -496,8 +498,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
]; ];
Widget _buildEmailLabel() => CustomFormLabel( Widget _buildEmailLabel() => CustomFormLabel(
label: 'Email',
style: style16DG600, style: style16DG600,
label: LocaleKeys.email.tr(),
); );
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) => Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
@ -522,8 +524,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
); );
Widget _buildCountryDropdownLabel() => CustomFormLabel( Widget _buildCountryDropdownLabel() => CustomFormLabel(
label: 'Country',
style: style16DG600, style: style16DG600,
label: LocaleKeys.country.tr(),
); );
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) => Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
@ -559,8 +561,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
]; ];
Widget _buildRegionFormFieldLabel() => CustomFormLabel( Widget _buildRegionFormFieldLabel() => CustomFormLabel(
label: 'Region',
style: style16DG600, style: style16DG600,
label: LocaleKeys.region.tr(),
); );
Widget _buildRegionFormState(ProfileDetailViewModel viewModel) => Widget _buildRegionFormState(ProfileDetailViewModel viewModel) =>
@ -570,8 +572,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildRegionDropDown(ProfileDetailViewModel viewModel) => Widget _buildRegionDropDown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker( CustomDropdownPicker(
hint: 'Select region',
icon: _buildSearchIcon(), icon: _buildSearchIcon(),
hint:LocaleKeys.select_region.tr(),
selectedItem: viewModel.selectedRegion, selectedItem: viewModel.selectedRegion,
items: (value, props) => viewModel.getRegions(), items: (value, props) => viewModel.getRegions(),
onChanged: (value) => onChanged: (value) =>
@ -582,8 +584,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
controller: regionController, controller: regionController,
onTap: viewModel.setRegionFocus, onTap: viewModel.setRegionFocus,
decoration: inputDecoration( decoration: inputDecoration(
hint: 'Enter Your City',
focus: viewModel.focusRegion, focus: viewModel.focusRegion,
hint:LocaleKeys.enter_your_city.tr(),
filled: regionController.text.isNotEmpty), filled: regionController.text.isNotEmpty),
); );
@ -614,14 +616,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
]; ];
Widget _buildOccupationDropdownLabel() => CustomFormLabel( Widget _buildOccupationDropdownLabel() => CustomFormLabel(
label: 'Occupation',
style: style16DG600, style: style16DG600,
label: LocaleKeys.occupation.tr(),
); );
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) => Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker( CustomDropdownPicker(
hint: 'Select occupation',
icon: _buildSearchIcon(), icon: _buildSearchIcon(),
hint:LocaleKeys.select_occupation.tr(),
selectedItem: viewModel.selectedOccupation, selectedItem: viewModel.selectedOccupation,
items: (value, props) => viewModel.getOccupations(), items: (value, props) => viewModel.getOccupations(),
onChanged: (value) => viewModel.setSelectedOccupation( onChanged: (value) => viewModel.setSelectedOccupation(
@ -645,9 +647,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'Save Changes',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
text: LocaleKeys.save_changes.tr(),
onTap: () async => await _update(viewModel), onTap: () async => await _update(viewModel),
); );
@ -659,10 +661,10 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildCancelButton(ProfileDetailViewModel viewModel) => Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
text: 'Cancel',
borderRadius: 12, borderRadius: 12,
onTap: viewModel.pop, onTap: viewModel.pop,
backgroundColor: kcWhite, backgroundColor: kcWhite,
text:LocaleKeys.cancel.tr(),
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );

View File

@ -46,7 +46,7 @@ class StartupView extends StackedView<StartupViewModel> {
]; ];
Widget _buildBackground() => Image.asset( Widget _buildBackground() => Image.asset(
'assets/images/onboarding_1.png', 'assets/images/loading.png',
fit: BoxFit.fill, fit: BoxFit.fill,
width: double.maxFinite, width: double.maxFinite,
height: double.maxFinite, height: double.maxFinite,

View File

@ -7,6 +7,7 @@ import '../../../app/app.router.dart';
import '../../../models/user.dart'; import '../../../models/user.dart';
import '../../../services/api_service.dart'; import '../../../services/api_service.dart';
import '../../../services/image_downloader_service.dart'; import '../../../services/image_downloader_service.dart';
import '../../../services/localization_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
@ -15,6 +16,7 @@ class StartupViewModel extends ReactiveViewModel {
final _apiService = locator<ApiService>(); final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>(); final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _localizationService = locator<LocalizationService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
final _imageDownloaderService = locator<ImageDownloaderService>(); final _imageDownloaderService = locator<ImageDownloaderService>();
@ -29,6 +31,8 @@ class StartupViewModel extends ReactiveViewModel {
// Main startup and navigation logic // Main startup and navigation logic
Future runStartupLogic() async { Future runStartupLogic() async {
await _localizationService.loadSelectedLanguage();
final loggedIn = await _authenticationService.userLoggedIn(); final loggedIn = await _authenticationService.userLoggedIn();
final firstTimeInstall = await _authenticationService.isFirstTimeInstall(); final firstTimeInstall = await _authenticationService.isFirstTimeInstall();

View File

@ -1,5 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/widgets/support_card.dart'; import 'package:yimaru_app/ui/widgets/support_card.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
@ -51,10 +53,10 @@ class SupportView extends StackedView<SupportViewModel> {
); );
Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar( Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar(
title: 'Need Help?',
showBackButton: true, showBackButton: true,
onPop: viewModel.pop, onPop: viewModel.pop,
); title:LocaleKeys.need_help.tr(),
);
Widget _buildContentWrapper(SupportViewModel viewModel) => Widget _buildContentWrapper(SupportViewModel viewModel) =>
Expanded(child: _buildContentColumnWrapper(viewModel)); Expanded(child: _buildContentColumnWrapper(viewModel));
@ -85,16 +87,16 @@ class SupportView extends StackedView<SupportViewModel> {
Widget _buildCallSupport(SupportViewModel viewModel) => SupportCard( Widget _buildCallSupport(SupportViewModel viewModel) => SupportCard(
icon: Icons.call, icon: Icons.call,
color: kcPrimaryColor, color: kcPrimaryColor,
title: 'Call Support', title:LocaleKeys.call_support.tr(),
subtitle: 'Talk with our support team directly', subtitle: LocaleKeys.talk_with_support.tr(),
onTap: () async => await viewModel.navigateToCallSupport(), onTap: () async => await viewModel.navigateToCallSupport(),
); );
Widget _buildTelegramSupport(SupportViewModel viewModel) => SupportCard( Widget _buildTelegramSupport(SupportViewModel viewModel) => SupportCard(
color: kcSkyBlue, color: kcSkyBlue,
icon: Icons.telegram, icon: Icons.telegram,
title: 'Telegram Support', title: LocaleKeys.telegram_support.tr(),
subtitle: 'Chat Instantly via Telegram', subtitle: LocaleKeys.chat_via_telegram.tr(),
onTap: () async => await viewModel.navigateToTelegramSupport(), onTap: () async => await viewModel.navigateToTelegramSupport(),
); );
} }

View File

@ -1,11 +1,12 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_strings.dart'; import 'package:yimaru_app/ui/common/app_strings.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart'; import '../../widgets/small_app_bar.dart';
import 'terms_and_conditions_viewmodel.dart'; import 'terms_and_conditions_viewmodel.dart';
@ -59,7 +60,7 @@ class TermsAndConditionsView extends StackedView<TermsAndConditionsViewModel> {
Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar( Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop, onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
title: 'Terms and Conditions', title: LocaleKeys.terms_and_conditions.tr(),
); );
Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) => Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) =>

View File

@ -1,95 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
import '../welcome_viewmodel.dart';
class FirstWelcomeScreen extends ViewModelWidget<WelcomeViewModel> {
const FirstWelcomeScreen({super.key});
@override
Widget build(BuildContext context, WelcomeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(WelcomeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(WelcomeViewModel viewModel) => Stack(
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(WelcomeViewModel viewModel) =>
[_buildBackground(), _buildColumnWrapper(), _buildSafeWrapper(viewModel)];
Widget _buildBackground() => Image.asset(
'assets/images/onboarding_1.png',
fit: BoxFit.fill,
width: double.maxFinite,
height: double.maxFinite,
);
Widget _buildColumnWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(),
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() => [
verticalSpaceMassive,
_buildIcon(),
verticalSpaceMedium,
_buildTitle(),
];
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
height: 50,
);
Widget _buildTitle() => Text(
'Small daily practice. Big lifelong results.',
style: style25W600,
textAlign: TextAlign.center,
);
Widget _buildSafeWrapper(WelcomeViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel));
Widget _buildContinueButtonWrapper(WelcomeViewModel viewModel) => Align(
alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel),
);
Widget _buildButtonContainer(WelcomeViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 60, right: 50, left: 50),
child: _buildContinueButtonState(viewModel),
);
Widget _buildContinueButtonState(WelcomeViewModel viewModel) =>
viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildContinueButton(WelcomeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Start Learning',
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
trailingIcon: Icons.arrow_forward,
onTap: () async => await viewModel.setFirstTimeInstall(),
);
}

View File

@ -1,99 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
import '../welcome_viewmodel.dart';
class SecondWelcomeScreen extends ViewModelWidget<WelcomeViewModel> {
const SecondWelcomeScreen({super.key});
@override
Widget build(BuildContext context, WelcomeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(WelcomeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(WelcomeViewModel viewModel) => Stack(
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(WelcomeViewModel viewModel) =>
[_buildBackground(), _buildColumnWrapper(), _buildSafeWrapper(viewModel)];
Widget _buildBackground() => Image.asset(
'assets/images/onboarding_2.png',
fit: BoxFit.fill,
width: double.maxFinite,
height: double.maxFinite,
);
Widget _buildColumnWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(),
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() => [
verticalSpaceMassive,
_buildIcon(),
verticalSpaceMedium,
_buildTitle(),
];
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
height: 50,
);
Widget _buildTitle() => const Text(
'Start speaking, Confidence will follow.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30,
color: kcWhite,
fontWeight: FontWeight.w600,
),
);
Widget _buildSafeWrapper(WelcomeViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel));
Widget _buildContinueButtonWrapper(WelcomeViewModel viewModel) => Align(
alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel),
);
Widget _buildButtonContainer(WelcomeViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 60, right: 50, left: 50),
child: _buildContinueButtonState(viewModel),
);
Widget _buildContinueButtonState(WelcomeViewModel viewModel) =>
viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildContinueButton(WelcomeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Start Learning',
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
trailingIcon: Icons.arrow_forward,
onTap: () async => await viewModel.setFirstTimeInstall(),
);
}

View File

@ -1,99 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
import '../welcome_viewmodel.dart';
class ThirdWelcomeScreen extends ViewModelWidget<WelcomeViewModel> {
const ThirdWelcomeScreen({super.key});
@override
Widget build(BuildContext context, WelcomeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(WelcomeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(WelcomeViewModel viewModel) => Stack(
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(WelcomeViewModel viewModel) =>
[_buildBackground(), _buildColumnWrapper(), _buildSafeWrapper(viewModel)];
Widget _buildBackground() => Image.asset(
'assets/images/onboarding_3.png',
fit: BoxFit.fill,
width: double.maxFinite,
height: double.maxFinite,
);
Widget _buildColumnWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(),
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() => [
verticalSpaceMassive,
_buildIcon(),
verticalSpaceMedium,
_buildTitle(),
];
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: kcWhite,
fontWeight: FontWeight.w600,
),
);
Widget _buildSafeWrapper(WelcomeViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel));
Widget _buildContinueButtonWrapper(WelcomeViewModel viewModel) => Align(
alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel),
);
Widget _buildButtonContainer(WelcomeViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 60, right: 50, left: 50),
child: _buildContinueButtonState(viewModel),
);
Widget _buildContinueButtonState(WelcomeViewModel viewModel) =>
viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildContinueButton(WelcomeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Start Learning',
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
trailingIcon: Icons.arrow_forward,
onTap: () async => await viewModel.setFirstTimeInstall(),
);
}

View File

@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
import 'package:stacked/stacked.dart';
import 'screens/first_welcome_screen.dart';
import 'screens/second_welcome_screen.dart';
import 'screens/third_welcome_screen.dart';
import 'welcome_viewmodel.dart';
class WelcomeView extends StackedView<WelcomeViewModel> {
const WelcomeView({Key? key}) : super(key: key);
@override
WelcomeViewModel viewModelBuilder(BuildContext context) => WelcomeViewModel();
@override
Widget builder(
BuildContext context,
WelcomeViewModel viewModel,
Widget? child,
) =>
_buildWelcomeScreens(viewModel);
Widget _buildWelcomeScreens(WelcomeViewModel viewModel) => FlutterCarousel(
options: FlutterCarouselOptions(
autoPlay: true,
viewportFraction: 1,
showIndicator: true,
indicatorMargin: 40,
height: double.maxFinite,
slideIndicator: CircularSlideIndicator(
slideIndicatorOptions:
const SlideIndicatorOptions(indicatorRadius: 2.5),
),
),
items: _buildScreens(),
);
List<Widget> _buildScreens() =>
[_buildFirstWelcome(), _buildSecondWelcome(), _buildThirdWelcome()];
Widget _buildFirstWelcome() => const FirstWelcomeScreen();
Widget _buildSecondWelcome() => const SecondWelcomeScreen();
Widget _buildThirdWelcome() => const ThirdWelcomeScreen();
}

View File

@ -1,29 +0,0 @@
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/authentication_service.dart';
import '../../../app/app.locator.dart';
class WelcomeViewModel extends BaseViewModel {
// Dependency Injection
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
// Navigation
Future<void> navigateToLogin() async =>
await _navigationService.replaceWithLoginView();
// Remote api call
// First time install
Future<void> setFirstTimeInstall() async {
await runBusyFuture(_setFirstTimeInstall());
}
Future<void> _setFirstTimeInstall() async {
await _authenticationService.setFirstTimeInstall(false);
await navigateToLogin();
}
}

View File

@ -1,14 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course_lesson.dart'; import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/ui/views/course_lesson/course_lesson_viewmodel.dart'; import 'package:yimaru_app/ui/views/course_module/course_module_viewmodel.dart';
import '../common/app_colors.dart'; import '../common/app_colors.dart';
import '../common/enmus.dart';
import '../common/helper_functions.dart';
import '../common/ui_helpers.dart'; import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart'; import 'custom_elevated_button.dart';
import 'mini_thumbnail.dart'; import 'mini_thumbnail.dart';
class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> { class CourseLessonTile extends ViewModelWidget<CourseModuleViewModel> {
final CourseLesson lesson; final CourseLesson lesson;
final GestureTapCallback? onVideoTap; final GestureTapCallback? onVideoTap;
final GestureTapCallback? onPracticeTap; final GestureTapCallback? onPracticeTap;
@ -21,19 +23,16 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
}); });
@override @override
Widget build(BuildContext context, CourseLessonViewModel viewModel) => Widget build(BuildContext context, CourseModuleViewModel viewModel) =>
_buildExpansionTileCard(context: context, viewModel: viewModel); _buildExpansionTileCard(context: context, viewModel: viewModel);
Widget _buildExpansionTileCard( Widget _buildExpansionTileCard(
{required BuildContext context, {required BuildContext context,
required CourseLessonViewModel viewModel}) => required CourseModuleViewModel viewModel}) =>
Container( Container(
margin: const EdgeInsets.only(bottom: 15), margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
border: Border.all(
color: kcPrimaryColor.withValues(alpha: 0.25),
),
), ),
child: _buildColumn(), child: _buildColumn(),
); );
@ -44,7 +43,7 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
); );
List<Widget> _buildColumnChildren() => [ List<Widget> _buildColumnChildren() => [
// _buildDivider(), _buildDivider(),
verticalSpaceMedium, verticalSpaceMedium,
_buildTile(), _buildTile(),
verticalSpaceMedium, verticalSpaceMedium,
@ -52,11 +51,14 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
verticalSpaceSmall, verticalSpaceSmall,
]; ];
Widget _buildDivider() => const Divider(color: kcVeryLightGrey);
Widget _buildTile() => ListTile( Widget _buildTile() => ListTile(
minTileHeight: 0, minTileHeight: 0,
title: _buildTitle(), title: _buildTitle(),
subtitle: _buildSubtitle(), subtitle: _buildSubtitle(),
leading: _buildLeadingWrapper(), leading: _buildLeadingWrapper(),
trailing: _buildTrailingWrapper(),
titleAlignment: ListTileTitleAlignment.top, titleAlignment: ListTileTitleAlignment.top,
contentPadding: const EdgeInsets.symmetric(horizontal: 15), contentPadding: const EdgeInsets.symmetric(horizontal: 15),
); );
@ -67,12 +69,29 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'${((lesson.duration ?? 0) / 50).toInt()} min', '${((lesson.id ?? 0) / 50).toInt()} min',
style: style14MG400, style: style14MG400,
); );
Widget _buildLeadingWrapper() => Widget _buildLeadingWrapper() => MiniThumbnail(
const MiniThumbnail(thumbnail: 'assets/images/image_1.png'); thumbnail:
getReadableUrl(lesson.thumbnail ?? 'assets/images/image_1.png') ??
'assets/images/image_1.png');
Widget _buildTrailingWrapper() =>
ProgressStatuses.completed != ProgressStatuses.completed
? _buildCompletedTrailing()
: _buildPendingTrailing();
Widget _buildCompletedTrailing() => const Icon(
Icons.check_circle,
color: kcGreen,
);
Widget _buildPendingTrailing() => const Icon(
Icons.circle_outlined,
color: kcLightGrey,
);
Widget _buildActionButtonWrapper() => Container( Widget _buildActionButtonWrapper() => Container(
height: 40, height: 40,
@ -107,9 +126,9 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
Widget _buildPracticeButton() => CustomElevatedButton( Widget _buildPracticeButton() => CustomElevatedButton(
height: 15, height: 15,
text: 'Practice',
borderRadius: 12, borderRadius: 12,
onTap: onPracticeTap, onTap: onPracticeTap,
text: 'Practice Test',
backgroundColor: kcWhite, backgroundColor: kcWhite,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,

View File

@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/models/course_module.dart';
import 'package:yimaru_app/ui/views/course_module/course_module_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/course_lesson_tile.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseModuleTileLarge extends ViewModelWidget<CourseModuleViewModel> {
final CourseModule? module;
final List<CourseLesson> lessons;
final GestureTapCallback? onContinueTap;
const CourseModuleTileLarge(
{super.key,
this.onContinueTap,
required this.module,
required this.lessons});
Future<void> _showSheet(
{required BuildContext context,
required CourseModuleViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override
Widget build(BuildContext context, CourseModuleViewModel viewModel) =>
_buildExpansionTileCard(context: context, viewModel: viewModel);
Widget _buildExpansionTileCard(
{required BuildContext context,
required CourseModuleViewModel viewModel}) =>
Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(color: kcVeryLightGrey),
),
child: _buildTileStack(context: context, viewModel: viewModel),
);
Widget _buildTileStack(
{required BuildContext context,
required CourseModuleViewModel viewModel}) =>
Stack(
children: [
_buildExpansionTile(context: context, viewModel: viewModel),
// _buildContainerShaderState()
],
);
Widget _buildExpansionTile(
{required BuildContext context,
required CourseModuleViewModel viewModel}) =>
ExpansionTile(
enabled: true,
title: _buildTitle(),
textColor: kcDarkGrey,
showTrailingIcon: true,
initiallyExpanded: true,
subtitle: _buildSubtitle(),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
leading: _buildLeadingWrapper(),
backgroundColor: kcBackgroundColor,
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
collapsedBackgroundColor: kcBackgroundColor,
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
// enabled: status != ProgressStatuses.pending,
// showTrailingIcon: status != ProgressStatuses.pending ? true : false,
//initiallyExpanded: status == ProgressStatuses.started ? true : false,
children:
_buildExpansionTileChildren(context: context, viewModel: viewModel),
);
Widget _buildTitle() => Text(
module?.name ?? '',
maxLines: 1,
softWrap: false,
style: style16P600,
overflow: TextOverflow.ellipsis,
);
Widget _buildLeadingWrapper() => CircleAvatar(
backgroundColor: kcVeryLightGrey.withValues(alpha: 0.5),
child: _buildLeading(),
);
Widget _buildLeading() => const Icon(
Icons.book,
color: kcLightGrey,
);
Widget _buildSubtitle() => Text(
'0% completed',
style: style14DG500,
);
List<Widget> _buildExpansionTileChildren(
{required BuildContext context,
required CourseModuleViewModel viewModel}) =>
[_buildExpansionTileItem(context: context, viewModel: viewModel)];
Widget _buildExpansionTileItem(
{required BuildContext context,
required CourseModuleViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildExpansionTileItemChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildExpansionTileItemChildren(
{required BuildContext context,
required CourseModuleViewModel viewModel}) =>
[
_buildProgressRowWrapper(),
verticalSpaceSmall,
_buildActionButtonWrapper(context: context, viewModel: viewModel),
verticalSpaceMedium,
_buildCourseModules(viewModel)
];
Widget _buildProgressRowWrapper() => Padding(
padding: const EdgeInsets.only(left: 75, right: 15),
child: _buildProgressRow(),
);
Widget _buildProgressRow() => Row(
mainAxisSize: MainAxisSize.min,
children: _buildProgressChildren(),
);
List<Widget> _buildProgressChildren() =>
[_buildProgressStatusWrapper(), horizontalSpaceSmall, _buildProgress()];
Widget _buildProgressStatusWrapper() => Expanded(
child: _buildProgressStatus(),
);
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
progress: 0,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
Widget _buildProgress() => const Text(
'0/0',
style: TextStyle(color: kcDarkGrey),
);
Widget _buildActionButtonWrapper(
{required BuildContext context,
required CourseModuleViewModel viewModel}) =>
Container(
width: 175,
height: 40,
margin: const EdgeInsets.only(left: 75, right: 15),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(CourseModuleViewModel viewModel) =>
CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onContinueTap,
text: 'Continue Module',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
Widget _buildSheet(CourseModuleViewModel viewModel) => FinishPracticeSheet(
onTap: viewModel.pop,
);
Widget _buildCourseModules(CourseModuleViewModel viewModel) =>
ListView.builder(
shrinkWrap: true,
itemCount: lessons.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildCourseModuleCard(
lesson: lessons[index],
onVideoTap: () async =>
await viewModel.navigateToCourseLessonDetail(lessons[index]),
onPracticeTap: () {}),
);
Widget _buildCourseModuleCard({
required CourseLesson lesson,
required GestureTapCallback onVideoTap,
required GestureTapCallback onPracticeTap,
}) =>
CourseLessonTile(
lesson: lesson,
onVideoTap: onVideoTap,
onPracticeTap: onPracticeTap,
);
}

View File

@ -1,19 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yimaru_app/models/course_module.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
class CourseModuleTileSmall extends StatelessWidget { class CourseModuleTileSmall extends StatelessWidget {
final String title; final CourseModule? module;
final ProgressStatuses status; final GestureTapCallback? onTap;
const CourseModuleTileSmall( const CourseModuleTileSmall({super.key, this.onTap, required this.module});
{super.key, required this.title, required this.status});
@override @override
Widget build(BuildContext context) => _buildTile(); Widget build(BuildContext context) => _buildTile();
Widget _buildTile() => ListTile( Widget _buildTile() => ListTile(
onTap: onTap,
title: _buildTitle(), title: _buildTitle(),
leading: _buildLeadingWrapper(), leading: _buildLeadingWrapper(),
trailing: _buildTrailingWrapper(), trailing: _buildTrailingWrapper(),
@ -27,7 +28,7 @@ class CourseModuleTileSmall extends StatelessWidget {
); );
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
title, module?.name ?? '',
maxLines: 1, maxLines: 1,
softWrap: false, softWrap: false,
style: style14DG600, style: style14DG600,
@ -43,9 +44,10 @@ class CourseModuleTileSmall extends StatelessWidget {
color: kcLightGrey, color: kcLightGrey,
); );
Widget _buildTrailingWrapper() => status == ProgressStatuses.completed Widget _buildTrailingWrapper() =>
? _buildCompletedTrailing() ProgressStatuses.completed != ProgressStatuses.completed
: _buildPendingTrailing(); ? _buildCompletedTrailing()
: _buildPendingTrailing();
Widget _buildCompletedTrailing() => const Icon( Widget _buildCompletedTrailing() => const Icon(
Icons.check_circle, Icons.check_circle,

View File

@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course_module.dart';
import 'package:yimaru_app/models/course_unit.dart'; import 'package:yimaru_app/models/course_unit.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/course_module_tile_small.dart'; import 'package:yimaru_app/ui/widgets/course_module_tile_small.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart'; import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import '../../models/course_catalog.dart';
import '../common/app_colors.dart'; import '../common/app_colors.dart';
import '../common/ui_helpers.dart'; import '../common/ui_helpers.dart';
import '../views/course_unit/course_unit_viewmodel.dart'; import '../views/course_unit/course_unit_viewmodel.dart';
@ -15,16 +16,18 @@ import 'custom_elevated_button.dart';
class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> { class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
final int index; final int index;
final CourseUnit unit; final CourseUnit unit;
final CourseCatalog catalog;
final GestureTapCallback? onLessonTap; final GestureTapCallback? onLessonTap;
final GestureTapCallback? onPracticeTap; final GestureTapCallback? onPracticeTap;
const CourseUnitTile({ const CourseUnitTile(
super.key, {super.key,
this.onLessonTap, this.onLessonTap,
this.onPracticeTap, this.onPracticeTap,
required this.unit, required this.unit,
required this.index, required this.index,
}); required this.catalog});
Future<void> _getCourseModules({ Future<void> _getCourseModules({
required bool expanded, required bool expanded,
@ -35,7 +38,7 @@ class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
// Prevent duplicate API calls // Prevent duplicate API calls
if ((unit.modules?.isNotEmpty ?? false)) return; if ((unit.modules?.isNotEmpty ?? false)) return;
await viewModel.getCourseUnitModules(index: index, id: unit.id ?? 0); await viewModel.getCourseModules(index: index, id: unit.id ?? 0);
} }
Future<void> _showSheet( Future<void> _showSheet(
@ -83,14 +86,12 @@ class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
showTrailingIcon: true, showTrailingIcon: true,
initiallyExpanded: false, initiallyExpanded: false,
subtitle: _buildSubtitle(), subtitle: _buildSubtitle(),
// key: Key(unit.id.toString()),
collapsedIconColor: kcDarkGrey, collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey, collapsedTextColor: kcDarkGrey,
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
shape: Border.all(color: kcTransparent), shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft, expandedAlignment: Alignment.centerLeft,
collapsedBackgroundColor: kcBackgroundColor, collapsedBackgroundColor: kcBackgroundColor,
controlAffinity: ListTileControlAffinity.trailing, controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start, expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.symmetric(horizontal: 15), tilePadding: const EdgeInsets.symmetric(horizontal: 15),
@ -230,10 +231,15 @@ class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
); );
Widget _buildCourseModulesState(CourseUnitViewModel viewModel) => Widget _buildCourseModulesState(CourseUnitViewModel viewModel) =>
viewModel.busy(StateObjects.courseModules) viewModel.busy(index)
? _buildProgressIndicator() ? _buildProgressIndicatorWrapper()
: _buildCourseModules(viewModel); : _buildCourseModules(viewModel);
Widget _buildProgressIndicatorWrapper() => SizedBox(
height: 50,
width: double.maxFinite,
child: _buildProgressIndicator(),
);
Widget _buildProgressIndicator() => const Center( Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor), child: CustomCircularProgressIndicator(color: kcPrimaryColor),
); );
@ -242,12 +248,15 @@ class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
shrinkWrap: true, shrinkWrap: true,
itemCount: unit.modules?.length, itemCount: unit.modules?.length,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => itemBuilder: (context, index) => _buildCourseModuleCard(
_buildCourseModuleCard(unit.modules?[index].name ?? ''), module: unit.modules?[index],
onTap: () async => await viewModel.navigateToCourseModule(
catalog: catalog, module: unit.modules?[index])),
); );
Widget _buildCourseModuleCard(String title) => Widget _buildCourseModuleCard(
CourseModuleTileSmall(title: title, status: ProgressStatuses.completed); {required CourseModule? module, required GestureTapCallback onTap}) =>
CourseModuleTileSmall(onTap: onTap, module: module);
// Widget _buildContainerShaderState() => status == ProgressStatuses.pending // Widget _buildContainerShaderState() => status == ProgressStatuses.pending
// ? _buildContainerShaderWrapper() // ? _buildContainerShaderWrapper()

View File

@ -12,13 +12,11 @@ import 'custom_elevated_button.dart';
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> { class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
final LearnModule module; final LearnModule module;
final GestureTapCallback? onLockTap;
final GestureTapCallback? onModuleTap; final GestureTapCallback? onModuleTap;
final GestureTapCallback? onPracticeTap; final GestureTapCallback? onPracticeTap;
const LearnModuleTile( const LearnModuleTile(
{super.key, {super.key,
this.onLockTap,
this.onModuleTap, this.onModuleTap,
this.onPracticeTap, this.onPracticeTap,
required this.module}); required this.module});
@ -34,15 +32,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
@override @override
Widget build(BuildContext context, LearnModuleViewModel viewModel) => Widget build(BuildContext context, LearnModuleViewModel viewModel) =>
_buildExpansionTileWrapper(context: context, viewModel: viewModel); _buildExpansionTileCard(context: context, viewModel: viewModel);
Widget _buildExpansionTileWrapper(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
GestureDetector(
onTap: !(module.access?.isAccessible ?? false) ? onLockTap : null,
child: _buildExpansionTileCard(context: context, viewModel: viewModel),
);
Widget _buildExpansionTileCard( Widget _buildExpansionTileCard(
{required BuildContext context, {required BuildContext context,
required LearnModuleViewModel viewModel}) => required LearnModuleViewModel viewModel}) =>

View File

@ -1,5 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/progress_status.dart'; import 'package:yimaru_app/ui/widgets/progress_status.dart';
@ -11,12 +13,20 @@ import 'custom_elevated_button.dart';
class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> { class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
final LearnProgram program; final LearnProgram program;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
final GestureTapCallback? onLockTap;
const LearnProgramTile({super.key, this.onTap, required this.program}); const LearnProgramTile(
{super.key, this.onTap, this.onLockTap, required this.program});
@override @override
Widget build(BuildContext context, LearnProgramViewModel viewModel) => Widget build(BuildContext context, LearnProgramViewModel viewModel) =>
_buildExpansionTileCard(viewModel); _buildExpansionTileCardWrapper(viewModel);
Widget _buildExpansionTileCardWrapper(LearnProgramViewModel viewModel) =>
GestureDetector(
onTap: !(program.access?.isAccessible ?? false) ? onLockTap : null,
child: _buildExpansionTileCard(viewModel),
);
Widget _buildExpansionTileCard(LearnProgramViewModel viewModel) => Container( Widget _buildExpansionTileCard(LearnProgramViewModel viewModel) => Container(
margin: const EdgeInsets.only(bottom: 15), margin: const EdgeInsets.only(bottom: 15),
@ -99,8 +109,8 @@ class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
Widget _buildProgressStatus() => ProgressStatus( Widget _buildProgressStatus() => ProgressStatus(
color: kcPrimaryColor, color: kcPrimaryColor,
status: (program.access?.isCompleted ?? false) status: (program.access?.isCompleted ?? false)
? 'Completed' ?LocaleKeys.completed.tr()
: 'In Progress', : LocaleKeys.in_progress.tr(),
); );
Widget _buildContent() => Text( Widget _buildContent() => Text(
@ -123,7 +133,7 @@ class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
text: program.access?.progressPercent == 0 text: program.access?.progressPercent == 0
? 'Start Learning' ? LocaleKeys.start_learning.tr()
: 'Continue Learning', :LocaleKeys.continue_learning.tr() ,
); );
} }

View File

@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_constants.dart'; import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../common/app_colors.dart'; import '../common/app_colors.dart';
import '../common/translations/locale_keys.g.dart';
class ProfileAppBar extends StatelessWidget { class ProfileAppBar extends StatelessWidget {
final String? name; final String? name;
@ -72,7 +74,7 @@ class ProfileAppBar extends StatelessWidget {
[_buildGreetingTitle(), _buildSubtitle()]; [_buildGreetingTitle(), _buildSubtitle()];
Widget _buildGreetingTitle() => Text.rich( Widget _buildGreetingTitle() => Text.rich(
TextSpan(text: 'Hello,', style: style14DG600, children: [ TextSpan(text: '${LocaleKeys.hello.tr()},', style: style14DG600, children: [
TextSpan( TextSpan(
text: ' $name!', text: ' $name!',
style: style14P600, style: style14P600,
@ -81,7 +83,7 @@ class ProfileAppBar extends StatelessWidget {
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'Ready to keep learning English today?', LocaleKeys.ready_to_learn.tr(),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: style14DG400, style: style14DG400,
); );

View File

@ -1,113 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import '../views/course_practice_question/course_practice_question_viewmodel.dart';
import 'custom_elevated_button.dart';
import 'custom_small_radio_button.dart';
class SelectableCoursePracticeQuestion
extends ViewModelWidget<CoursePracticeQuestionViewModel> {
final int index;
const SelectableCoursePracticeQuestion({super.key, required this.index});
@override
Widget build(
BuildContext context, CoursePracticeQuestionViewModel viewModel) =>
_buildBodyScroller(viewModel);
Widget _buildBodyScroller(CoursePracticeQuestionViewModel viewModel) =>
SingleChildScrollView(
child: _buildBody(viewModel),
);
Widget _buildBody(CoursePracticeQuestionViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(CoursePracticeQuestionViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitle(viewModel),
verticalSpaceMedium,
_buildAnswers(viewModel),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildTitle(CoursePracticeQuestionViewModel viewModel) => Text(
'Q${index + 1}. ${viewModel.currentQuestion?.questionText} ',
style: style16DG600,
);
Widget _buildAnswers(CoursePracticeQuestionViewModel viewModel) =>
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: viewModel.currentQuestion?.options?.length,
itemBuilder: (context, inner) => _buildAnswer(
title: viewModel.currentQuestion?.options?[inner].optionText ?? '',
selected: viewModel.isSelectedAnswer(
question: index + 1,
answer:
viewModel.currentQuestion?.options?[inner].optionText ?? ''),
onTap: () => viewModel.setSelectedAnswer(
question: index + 1,
option: viewModel.currentQuestion?.options?[inner]),
),
);
Widget _buildAnswer(
{required String title,
required bool selected,
required GestureTapCallback onTap}) =>
CustomSmallRadioButton(
title: title,
onTap: onTap,
selected: selected,
);
Widget _buildContinueButtonWrapper(
CoursePracticeQuestionViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButtonState(viewModel),
);
Widget _buildContinueButtonState(CoursePracticeQuestionViewModel viewModel) =>
viewModel.busy(StateObjects.coursePracticeQuestion)
? _buildProgressIndicator()
: _buildContinueButton(viewModel);
Widget _buildProgressIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
Widget _buildContinueButton(CoursePracticeQuestionViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
foregroundColor: kcWhite,
text: viewModel.currentQuestionIndex ==
viewModel.coursePracticeQuestions.length - 1
? 'Finish'
: 'Continue',
backgroundColor:
viewModel.selectedAnswers.containsKey((index + 1).toString())
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.selectedAnswers.containsKey((index + 1).toString())
? () async => await viewModel.nextQuestion(viewModel
.coursePracticeQuestions[
index + 1 < viewModel.coursePracticeQuestions.length
? index + 1
: index]
.id ??
0)
: null,
);
}

View File

@ -1,5 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
class ViewProfileButton extends StatelessWidget { class ViewProfileButton extends StatelessWidget {
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
@ -21,10 +24,9 @@ class ViewProfileButton extends StatelessWidget {
List<Widget> _buildButtonRowChildren() => List<Widget> _buildButtonRowChildren() =>
[_buildButtonText(), const SizedBox(width: 10), _buildButtonIcon()]; [_buildButtonText(), const SizedBox(width: 10), _buildButtonIcon()];
Widget _buildButtonText() => const Text( Widget _buildButtonText() => Text(
'View Profile', LocaleKeys.view_profile.tr(),
style: TextStyle( style: style16P900,
color: kcPrimaryColor, fontSize: 16, fontWeight: FontWeight.w900),
); );
Widget _buildButtonIcon() => const Icon( Widget _buildButtonIcon() => const Icon(

View File

@ -1,124 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../common/app_colors.dart';
import '../common/enmus.dart';
import '../common/ui_helpers.dart';
import '../views/course_practice_question/course_practice_question_view.form.dart';
import '../views/course_practice_question/course_practice_question_viewmodel.dart';
import 'custom_circular_progress_indicator.dart';
import 'custom_elevated_button.dart';
class WritingCoursePracticeQuestion
extends ViewModelWidget<CoursePracticeQuestionViewModel> {
final int index;
final TextEditingController answerController;
const WritingCoursePracticeQuestion(
{super.key, required this.index, required this.answerController});
@override
Widget build(
BuildContext context, CoursePracticeQuestionViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(CoursePracticeQuestionViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(CoursePracticeQuestionViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(CoursePracticeQuestionViewModel viewModel) =>
SingleChildScrollView(
child: _buildUpperColumn(viewModel),
);
Widget _buildUpperColumn(CoursePracticeQuestionViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(
CoursePracticeQuestionViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitle(viewModel),
verticalSpaceLarge,
_buildQuestionFormField(viewModel),
if (viewModel.hasAnswerValidationMessage && viewModel.focusAnswer)
verticalSpaceTiny,
if (viewModel.hasAnswerValidationMessage && viewModel.focusAnswer)
_buildQuestionValidatorWrapper(viewModel),
];
Widget _buildTitle(CoursePracticeQuestionViewModel viewModel) => Text(
'Q${index + 1}. ${viewModel.coursePracticeQuestions[index].questionText} ',
style: style16DG600,
);
Widget _buildQuestionFormField(CoursePracticeQuestionViewModel viewModel) =>
TextFormField(
maxLines: 3,
controller: answerController,
onTap: viewModel.setAnswerFocus,
decoration: inputDecoration(
hint: 'Enter Your Answer',
focus: viewModel.focusAnswer,
filled: answerController.text.isNotEmpty),
);
Widget _buildQuestionValidatorWrapper(
CoursePracticeQuestionViewModel viewModel) =>
viewModel.hasAnswerValidationMessage
? _buildQuestionValidator(viewModel)
: Container();
Widget _buildQuestionValidator(CoursePracticeQuestionViewModel viewModel) =>
Text(
viewModel.answerValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildContinueButtonWrapper(
CoursePracticeQuestionViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButtonState(viewModel),
);
Widget _buildContinueButtonState(CoursePracticeQuestionViewModel viewModel) =>
viewModel.busy(StateObjects.coursePracticeQuestion)
? _buildProgressIndicator()
: _buildContinueButton(viewModel);
Widget _buildProgressIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
Widget _buildContinueButton(CoursePracticeQuestionViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: answerController.text.isNotEmpty
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: answerController.text.isNotEmpty
? () async => await viewModel.nextQuestion(
index + 1 < viewModel.coursePracticeQuestions.length
? index + 1
: index)
: null,
text: viewModel.currentQuestionIndex ==
viewModel.coursePracticeQuestions.length - 1
? 'Finish'
: 'Continue',
);
}

View File

@ -1,5 +1,5 @@
name: yimaru_app name: yimaru_app
version: 0.1.17+19 version: 0.1.18+20
publish_to: 'none' publish_to: 'none'
description: A new Flutter project. description: A new Flutter project.

View File

@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart'; import '../helpers/test_helpers.dart';
void main() { void main() {
group('CourseLessonViewModel Tests -', () { group('CourseModuleViewModel Tests -', () {
setUp(() => registerServices()); setUp(() => registerServices());
tearDown(() => locator.reset()); tearDown(() => locator.reset());
}); });

View File

@ -1,11 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('CoursePracticeQuestionViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -1,11 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('CoursePracticeViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -1,11 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('WelcomeViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}