Compare commits

...

3 Commits

Author SHA1 Message Date
855aa25bc3 Merge tag '0.1.18' into develop
-fix: Apply UAT comments
2026-05-22 07:30:00 +03:00
2ac39558c7 -fix: Apply UAT comments
Merge branch 'release/0.1.18'
2026-05-22 07:29:37 +03:00
60016afee4 fix: Apply UAT comments 2026-05-22 07:28:37 +03:00
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_email": "ኮዱ ወደ ኢሜል ተልኳል",
"resend_code_in": "ኮዱን እንደገና ለመላክ የቀረው ጊዜ",
"reset_password": " የይለፍ ቃልን ይቀይሩ ",
"reset_password": " የይለፍ ቃልን ይቀይሩ",
"enter_email_reset_code": "ኢሜይልዎን ያስገቡ። የይለፍ ቃል መለወጫ ኮድ እንልክልዎታለን።" ,
"please_wait": "እባክዎ ይጠብቁ",
"reset_code_sent": "የመቀየሪያ ኮድ በተሳካ ሁኔታ ተልኳል" ,
"reset_code": " የመቀየሪያ ኮድ ",
"new_password": "አዲስ የይለፍ ቃል",
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
"view_course": " ኮርሱን ይመልከቱ ",
"take_practice": " ልምምድ ያድርጉ ",
"view_course": " ኮርሱን ይመልከቱ",
"continue_learning": "መማርን ይቀጥሉ",
"start_learning": "ትምህርትን ይጀምሩ",
"completed": "ተጠናቋል",
"take_practice": " ልምምድ ያድርጉ",
"your_current_level": "የአሁኑ ደረጃዎ",
"overall_progress": "አጠቃላይ እድገት",
"great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው ",
"great_work": "በርቱ! በጣም ጥሩ እየሰሩ ነው",
"view_module": "ሞጁሉን ይመልከቱ",
"progress": "እድገት",
"keep_going": " ይቀጥሉ - ከግማሽ በላይ ጨርሰዋል ",
@ -56,7 +59,7 @@
"learn": "ይማሩ ",
"course": "ኮርስ",
"profile": " ፕሮፋይል ",
"speaking_partner": "የንግግር ጓደኛ ",
"speaking_partner": "የንግግር ጓደኛ",
"practice_what_you_learned": "አሁን የተማሩትን እንለማመድ",
"practice_questions": "ጥቂት ጥያቄዎችን እጠይቃለሁ እና መልስ መስጠት ይችላሉ",
"start_practice": "ልምምድ ጀምር",
@ -65,7 +68,7 @@
"continue_practice": "ልምምዱን ይቀጥሉ",
"end_session": "ክፍለ ጊዜውን ያብቁ ",
"tap_start_to_listen": "ለማዳመጥ የጀምር ቁልፉን ይጫኑ",
"practice_speaking": "ንግግርን ይለማመዱ ",
"practice_speaking": "ንግግርን ይለማመዱ",
"tap_microphone": "ለመናገር ማይክሮፎኑን ይጫኑ",
"reply": "እንደገና አዳምጥ",
"cancel": "ይቅር",
@ -73,7 +76,7 @@
"practice_completed": "ልምምዱ ተጠናቅቋል",
"great_improvement": "በዚህኛው በራስ መተማመንዎ ጨምሯል፤ ትልቅ መሻሻል ነው",
"practice_again": "እንደገና ይለማመዱ",
"conversation_review": "የንግግር ግምገማ ",
"conversation_review": "የንግግር ግምገማ",
"result": "ውጤት",
"quick_tip": "ጠቃሚ ምክር",
"retry": "እንደገና ይሞክሩ",
@ -90,8 +93,33 @@
"phone_number": "የስልክ ቁጥር",
"country": "ሀገር",
"region": "ክልል",
"occupation": "የስራ መስክ ",
"save_changes": "ለውጦችን ያስቀምጡ"
"select_region": "ክልል ይምረጡ",
"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",
"new_password": "New password",
"logged_in_successfully": "Logged in successfully",
"continue_learning": "Continue Learning",
"start_learning": "Start Learning",
"completed": "Completed",
"view_course": "View course",
"take_practice": "Take practice",
"your_current_level": "Your current level",
@ -90,6 +93,32 @@
"phone_number": "Phone number",
"country": "Country",
"region": "Region",
"select_region": "Select region",
"enter_your_city": "Enter your city",
"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/dio_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/services/permission_handler_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/learn_lesson_detail/learn_lesson_detail_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/course_practice/course_practice_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/course_lesson/course_lesson_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/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/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart';
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_view.dart';
import 'package:yimaru_app/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_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/services/localization_service.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
@StackedApp(
@ -81,19 +78,15 @@ import 'package:yimaru_app/ui/views/landing/landing_view.dart';
MaterialRoute(page: RegisterView),
MaterialRoute(page: LoginView),
MaterialRoute(page: LearnModuleView),
MaterialRoute(page: WelcomeView),
MaterialRoute(page: LearnLessonView),
MaterialRoute(page: ForgetPasswordView),
MaterialRoute(page: LearnLessonDetailView),
MaterialRoute(page: LearnPracticeView),
MaterialRoute(page: CoursePracticeView),
MaterialRoute(page: CoursePaymentView),
MaterialRoute(page: FailureView),
MaterialRoute(page: CourseLessonView),
MaterialRoute(page: CourseLessonDetailView),
MaterialRoute(page: DuolingoView),
MaterialRoute(page: CourseView),
MaterialRoute(page: CoursePracticeQuestionView),
MaterialRoute(page: LearnProgramView),
MaterialRoute(page: LearnCourseView),
MaterialRoute(page: AssessmentView),
@ -102,6 +95,8 @@ import 'package:yimaru_app/ui/views/landing/landing_view.dart';
MaterialRoute(page: CourseCatalogView),
MaterialRoute(page: CourseUnitView),
MaterialRoute(page: LandingView),
MaterialRoute(page: CourseModuleView),
MaterialRoute(page: LearnCourseView),
// @stacked-route
],
dependencies: [

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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_practice.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/course_catalog.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/services/dio_service.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_question.dart';
import '../models/learn_subscription.dart';
import '../models/lesson.dart';
import '../models/module.dart';
import '../models/assessment.dart';
import '../models/submodule.dart';
import '../models/learn_subscription_request.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
Future<List<CourseLesson>> getCourseLessons(int id) async {
try {
List<CourseLesson> courseLessons = [];
List<CourseLesson> lessons = [];
final Response response = await _service.dio.get(
'$kBaseUrl/$kCourseBaseUrl/$kSubcoursesUrl/$id/$kPublishedVideos');
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kModulesUrl/$id/$kLessonsUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data'] 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;
var decodedData = data['data']['lessons'] as List;
lessons = decodedData.map(
(e) {
return Lesson.fromJson(e);
return CourseLesson.fromJson(e);
},
).toList();
return lessons;
@ -1085,52 +795,4 @@ class ApiService {
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/services/secure_storage_service.dart';
import 'localization_service.dart';
class AuthenticationService with ListenableServiceMixin {
// Dependency injection
final _secureService = locator<SecureStorageService>();
final _localizationService = locator<LocalizationService>();
// User data
User? _user;
@ -14,7 +18,7 @@ class AuthenticationService with ListenableServiceMixin {
// Initialization
AuthenticationService() {
listenToReactiveValues([_user]);
listenToReactiveValues([_user, _localizationService]);
}
// Check user logged in
@ -172,9 +176,12 @@ class AuthenticationService with ListenableServiceMixin {
// Logout
Future<void> logout() async {
bool firstTimeInstall = await isFirstTimeInstall();
String language = await _localizationService.selectedLanguage['code'];
_user = null;
await _secureService.clear();
await setFirstTimeInstall(firstTimeInstall);
await _secureService.setString('language', language);
notifyListeners();
}
}

View File

@ -1,10 +1,9 @@
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/course_progress.dart';
import 'package:yimaru_app/services/api_service.dart';
import '../models/course_catalog.dart';
import '../models/course_detail.dart';
import '../models/course_lesson.dart';
import '../models/course_module.dart';
import '../models/course_unit.dart';
@ -28,10 +27,15 @@ class CourseService with ListenableServiceMixin {
List<CourseUnit> get units => _units;
// Course modules
List<CourseModule> _modules = [];
final List<CourseModule> _modules = [];
List<CourseModule> get modules => _modules;
// Course lessons
List<CourseLesson> _lessons = [];
List<CourseLesson> get lessons => _lessons;
// Course catalogs
Future<void> getCourseCatalogs() async {
_catalogs = await _apiService.getCourseCatalogs();
@ -47,7 +51,7 @@ class CourseService with ListenableServiceMixin {
}
// Course modules
Future<void> getCourseUnitModule({
Future<void> getCourseModules({
required int id,
required int index,
}) async {
@ -66,26 +70,10 @@ class CourseService with ListenableServiceMixin {
notifyListeners();
}
Future<void> getCourseModules(int id) async {
_modules = await _apiService.getCourseModules(id);
_modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
// Course units
Future<void> getCourseLessons(int id) async {
_lessons = await _apiService.getCourseLessons(id);
_lessons.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
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:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
import '../app/app.locator.dart';
class LocalizationService with ListenableServiceMixin {
// Dependency injection
final _secureService = locator<SecureStorageService>();
// Initialization
localizationService() {
listenToReactiveValues([_selectedLanguage]);
@ -10,7 +16,7 @@ class LocalizationService with ListenableServiceMixin {
// Languages
Map<String, dynamic> _selectedLanguage = {
'code': 'EN',
'code': 'en',
'language': 'English'
};
@ -18,7 +24,7 @@ class LocalizationService with ListenableServiceMixin {
final List<Map<String, dynamic>> _languages = [
{'code': 'አማ', 'language': 'አማርኛ'},
{'code': 'EN', 'language': 'English'},
{'code': 'en', 'language': 'English'},
];
List<Map<String, dynamic>> get languages => _languages;
@ -34,14 +40,33 @@ class LocalizationService with ListenableServiceMixin {
if (title['code'] == 'አማ') {
await setAmharicLanguage(context);
} else {
await setAmharicLanguage(context);
await setEnglishLanguage(context);
}
notifyListeners();
}
Future<void> setAmharicLanguage(BuildContext context) async =>
await context.setLocale(const Locale('am'));
Future<void> loadSelectedLanguage() async {
String language = await _secureService.getString('language') ?? 'en';
Future<void> setEnglishLanguage(BuildContext context) async =>
await context.setLocale(const Locale('en'));
if (language == '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,
profileImage,
learnPrograms,
courseModules,
courseLessons,
profileUpdate,
resetPassword,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,9 +27,6 @@ class CourseCatalogViewModel extends ReactiveViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToCoursePractice(int id) async =>
_navigationService.navigateToCoursePracticeView(id: id);
Future<void> navigateToCourseUnit(CourseCatalog catalog) async =>
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:flutter/material.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/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/empty_video_player.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;
const CourseLessonDetailView({Key? key, required this.lesson})
: super(key: key);
const CourseLessonDetailView({
Key? key,
required this.lesson,
}) : super(key: key);
Future<void> _navigate(CourseLessonDetailViewmodel viewModel) async {
await viewModel.pause();
// await viewModel.navigateToLearnPractice(lesson.id ?? 0);
}
@override
void onViewModelReady(CourseLessonDetailViewModel viewModel) async {
await viewModel.initializePlayer(lesson);
void onViewModelReady(CourseLessonDetailViewmodel viewModel) async {
await viewModel.initializePlayer(
lessonId: 0,
moduleId: 0,
url: lesson.videoUrl ?? '',
);
super.onViewModelReady(viewModel);
}
@override
CourseLessonDetailViewModel viewModelBuilder(BuildContext context) =>
CourseLessonDetailViewModel();
void onDispose(CourseLessonDetailViewmodel viewModel) {
viewModel.close();
super.onDispose(viewModel);
}
@override
CourseLessonDetailViewmodel viewModelBuilder(BuildContext context) =>
CourseLessonDetailViewmodel();
@override
Widget builder(
BuildContext context,
CourseLessonDetailViewModel viewModel,
CourseLessonDetailViewmodel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CourseLessonDetailViewModel viewModel) =>
Widget _buildScaffoldWrapper(CourseLessonDetailViewmodel viewModel) =>
Scaffold(
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));
Widget _buildColumn(CourseLessonDetailViewModel viewModel) => Column(
Widget _buildColumn(CourseLessonDetailViewmodel viewModel) => Column(
children: [
verticalSpaceMedium,
_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),
child: _buildAppBar(viewModel));
Widget _buildAppBar(CourseLessonDetailViewModel viewModel) => SmallAppBar(
Widget _buildAppBar(CourseLessonDetailViewmodel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
title: lesson.title ?? '',
);
Widget _buildBodyColumnWrapper(CourseLessonDetailViewModel viewModel) =>
Widget _buildBodyColumnWrapper(CourseLessonDetailViewmodel viewModel) =>
Expanded(
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(CourseLessonDetailViewModel viewModel) => Column(
Widget _buildBodyColumn(CourseLessonDetailViewmodel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(
CourseLessonDetailViewModel viewModel) =>
CourseLessonDetailViewmodel viewModel) =>
[
_buildLevelsColumnWrapper(viewModel),
// _buildContinueButtonWrapper(viewModel)
if (lesson.hasPractice ?? false) _buildPracticeButtonWrapper(viewModel)
];
Widget _buildLevelsColumnWrapper(CourseLessonDetailViewModel viewModel) =>
Widget _buildLevelsColumnWrapper(CourseLessonDetailViewmodel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(CourseLessonDetailViewModel viewModel) =>
Widget _buildLevelsColumnScrollView(CourseLessonDetailViewmodel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(CourseLessonDetailViewModel viewModel) => Column(
Widget _buildLevelsColumn(CourseLessonDetailViewmodel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(
CourseLessonDetailViewModel viewModel) =>
CourseLessonDetailViewmodel viewModel) =>
[
verticalSpaceLarge,
verticalSpaceMedium,
_buildVideoPlayerWrapper(viewModel),
verticalSpaceMedium,
_buildTitleWrapper(),
verticalSpaceMedium,
_buildDescriptionWrapper(),
];
Widget _buildVideoPlayerWrapper(CourseLessonDetailViewModel viewModel) =>
Widget _buildVideoPlayerWrapper(CourseLessonDetailViewmodel viewModel) =>
Container(
height: 200,
color: kcBlack,
@ -110,21 +134,32 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
child: _buildVideoPlayerState(viewModel),
);
Widget _buildVideoPlayerState(CourseLessonDetailViewModel viewModel) =>
Widget _buildVideoPlayerState(CourseLessonDetailViewmodel viewModel) =>
viewModel.chewieController != null &&
viewModel.videoPlayerController != null &&
!viewModel.busy(StateObjects.loadCourseVideo)
!viewModel.busy(StateObjects.loadLessonVideo)
? _buildVideoPlayer(viewModel)
: _buildEmptyVideoPlayer();
Widget _buildVideoPlayer(CourseLessonDetailViewModel viewModel) =>
Widget _buildVideoPlayer(CourseLessonDetailViewmodel viewModel) =>
_buildChewiePlayer(viewModel);
Widget _buildChewiePlayer(CourseLessonDetailViewModel viewModel) =>
Chewie(controller: viewModel.chewieController!);
Widget _buildChewiePlayer(CourseLessonDetailViewmodel viewModel) => Chewie(
controller: viewModel.chewieController!,
);
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(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildDescription(),
@ -132,10 +167,10 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
Widget _buildDescription() => Text(
lesson.description ?? '',
style: style14DG600,
style: style14DG400,
);
Widget _buildPracticeButtonWrapper(CourseLessonDetailViewModel viewModel) =>
Widget _buildPracticeButtonWrapper(CourseLessonDetailViewmodel viewModel) =>
Padding(
padding: const EdgeInsets.only(
left: 15,
@ -145,14 +180,13 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
child: _buildPracticeButton(viewModel),
);
Widget _buildPracticeButton(CourseLessonDetailViewModel viewModel) =>
Widget _buildPracticeButton(CourseLessonDetailViewmodel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Practice',
borderRadius: 12,
text: 'Start Assessment',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async =>
await viewModel.navigateToCoursePractice(lesson.courseId ?? 0),
onTap: () async => await _navigate(viewModel),
);
}

View File

@ -2,23 +2,27 @@ import 'package:chewie/chewie.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.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 '../../../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/ui_helpers.dart';
class CourseLessonDetailViewModel extends BaseViewModel {
class CourseLessonDetailViewmodel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _vimeoService = locator<VimeoService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Video player config
bool _isCompleted = false;
bool _lessonCompleted = false;
ChewieController? _chewieController;
@ -29,82 +33,78 @@ class CourseLessonDetailViewModel extends BaseViewModel {
VideoPlayerController? get videoPlayerController => _videoPlayerController;
// Video player
@override
void dispose() {
void close() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
Future<void> pause() async {
await _chewieController?.pause();
}
Future<void> _videoListener(CourseLesson lesson) async {
final controller = _videoPlayerController;
Future<void> initializePlayer(
{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;
final duration = controller.value.duration;
if (duration.inSeconds == 0) return;
double progress = position.inSeconds / duration.inSeconds;
print("Video progress: ${(progress * 100).toStringAsFixed(2)}%");
// Example: mark completion at 95%
if (progress >= 0.95) {
await _onVideoCompleted(lesson);
if (playableUrl == null) {
throw Exception("Unable to load video");
}
}
Future<void> initializePlayer(CourseLesson lesson) async =>
await runBusyFuture(_initializePlayer(lesson),
busyObject: StateObjects.loadCourseVideo);
Future<void> _initializePlayer(CourseLesson lesson) async {
print('URL: $kSampleVideoUrl');
_videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl));
VideoPlayerController.networkUrl(Uri.parse(playableUrl));
await _videoPlayerController?.initialize();
_videoPlayerController
?.addListener(() async => await _videoListener(lesson));
// Listen for video completion
_videoPlayerController?.addListener(() async {
final controller = _videoPlayerController;
if (_videoPlayerController != null) {
_chewieController = ChewieController(
looping: true,
autoPlay: true,
showOptions: true,
showControls: true,
aspectRatio: 16 / 9,
autoInitialize: true,
allowedScreenSleep: false,
videoPlayerController: _videoPlayerController!,
materialProgressColors: buildChewieProgressIndicator,
);
}
if (controller == null || _lessonCompleted) return;
rebuildUi();
}
final position = controller.value.position.inSeconds;
final duration = controller.value.duration.inSeconds;
Future<void> _onVideoCompleted(CourseLesson lesson) async {
if (_isCompleted) return;
if (duration <= 0) 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
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_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import '../../../app/app.locator.dart';
import '../../../models/course_lesson.dart';
import '../../../services/api_service.dart';
import '../../../services/course_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class CourseLessonViewModel extends BaseViewModel {
class CourseModuleViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _courseService = locator<CourseService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Course lessons
List<CourseLesson> _courseLessons = [];
@override
List<ListenableServiceMixin> get listenableServices => [_courseService];
List<CourseLesson> get courseLessons => _courseLessons;
// Course lessons
List<CourseLesson> get _lessons => _courseService.lessons;
List<CourseLesson> get lessons => _lessons;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToCoursePractice(int id) =>
_navigationService.navigateToCoursePracticeView(id: id);
Future<void> navigateToCourseLessonDetail(CourseLesson lesson) async =>
await _navigationService.navigateToCourseLessonDetailView(lesson: lesson);
// Remote api call
// Course lessons
Future<void> getCourseLessons(int courseId) async =>
await runBusyFuture(_getCourseLessons(courseId),
// Course modules
Future<void> getCourseLessons(int id) async =>
await runBusyFuture(_getCourseLessons(id),
busyObject: StateObjects.courseLessons);
Future<void> _getCourseLessons(int courseId) async {
Future<void> _getCourseLessons(int id) async {
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',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.navigateToCourseLesson(course),
onTap: () {},
);
Widget _buildSecurePaymentWrapper() => Align(

View File

@ -1,7 +1,5 @@
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/course.dart';
import '../../../app/app.locator.dart';
@ -11,7 +9,4 @@ class CoursePaymentViewModel extends BaseViewModel {
// Navigation
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(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
body: _buildScaffoldContainer(viewModel),
);
Widget _buildScaffoldContainer(CourseUnitViewModel viewModel) => Container(
decoration: bgDecoration,
child: _buildScaffold(viewModel),
);
Widget _buildScaffold(CourseUnitViewModel viewModel) =>
@ -94,7 +99,7 @@ class CourseUnitView extends StackedView<CourseUnitViewModel> {
Widget _buildTitle() => Text(
catalog.name ?? '',
style: style18P600,
style: style25DG600,
);
Widget _buildCourseModuleBanner() => const CourseModuleBanner();
@ -130,19 +135,20 @@ class CourseUnitView extends StackedView<CourseUnitViewModel> {
index: index,
unit: viewModel.units[index],
onPracticeTap: () {},
onLessonTap: () {}),
onViewTap: () {}),
);
Widget _buildTile({
required int index,
required CourseUnit unit,
required GestureTapCallback onLessonTap,
required GestureTapCallback onViewTap,
required GestureTapCallback onPracticeTap,
}) =>
CourseUnitTile(
unit: unit,
index: index,
onLessonTap: onLessonTap,
catalog: catalog,
onLessonTap: onViewTap,
onPracticeTap: onPracticeTap,
);
}

View File

@ -1,7 +1,10 @@
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/course_catalog.dart';
import '../../../app/app.locator.dart';
import '../../../models/course_module.dart';
import '../../../models/course_unit.dart';
import '../../../services/course_service.dart';
import '../../../services/status_checker_service.dart';
@ -26,6 +29,12 @@ class CourseUnitViewModel extends ReactiveViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToCourseModule(
{required CourseModule? module,
required CourseCatalog catalog}) async =>
await _navigationService.navigateToCourseModuleView(
module: module, catalog: catalog);
// Remote api call
// Course units
@ -39,15 +48,13 @@ class CourseUnitViewModel extends ReactiveViewModel {
}
}
Future<void> getCourseUnitModules(
{required int id, required int index}) async =>
await runBusyFuture(_getCourseUnitModules(id: id, index: index),
busyObject: StateObjects.courseModules);
Future<void> getCourseModules({required int id, required int index}) async =>
await runBusyFuture(_getCourseModules(id: id, index: index),
busyObject: index);
Future<void> _getCourseUnitModules(
{required int id, required int index}) async {
Future<void> _getCourseModules({required int id, required int index}) async {
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(
'assets/images/onboarding_1.png',
'assets/images/loading.png',
fit: BoxFit.fill,
width: 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:stacked/stacked.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/profile/profile_view.dart';
@ -44,18 +46,18 @@ class HomeView extends StackedView<HomeViewModel> {
];
BottomNavigationBarItem _buildLearnItem() => BottomNavigationBarItem(
label: 'Learn',
icon: _buildLearnIcon(),
label: LocaleKeys.learn.tr(),
);
BottomNavigationBarItem _buildCourseItem() => BottomNavigationBarItem(
label: 'Course',
icon: _buildCourseIcon(),
label: LocaleKeys.course.tr(),
);
BottomNavigationBarItem _buildProfileItem() => BottomNavigationBarItem(
label: 'Profile',
icon: _buildProfileIcon(),
label: LocaleKeys.profile.tr(),
);
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:stacked/stacked.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/third_landing_screen.dart';
@ -10,11 +11,10 @@ import 'landing_viewmodel.dart';
class LandingView extends StackedView<LandingViewModel> {
const LandingView({Key? key}) : super(key: key);
@override
LandingViewModel viewModelBuilder(
BuildContext context,
) =>
BuildContext context,
) =>
LandingViewModel();
@override
@ -22,25 +22,30 @@ class LandingView extends StackedView<LandingViewModel> {
BuildContext context,
LandingViewModel viewModel,
Widget? child,
)=> _buildLandingScreens(viewModel);
) =>
_buildLandingScreens(viewModel);
Widget _buildLandingScreens(LandingViewModel 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(),
);
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()];
List<Widget> _buildScreens() => [
_buildFirstWelcome(),
_buildSecondWelcome(),
_buildThirdWelcome(),
_buildFourthWelcome()
];
Widget _buildFirstWelcome() => const FirstLandingScreen();
@ -48,5 +53,5 @@ class LandingView extends StackedView<LandingViewModel> {
Widget _buildThirdWelcome() => const ThirdLandingScreen();
Widget _buildFourthWelcome() => const FourthLandingScreen();
}

View File

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

View File

@ -16,70 +16,63 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LandingViewModel viewModel) => Scaffold(
backgroundColor: kcWhite,
body: _buildScaffoldPadding(viewModel),
);
Widget _buildScaffoldPadding(LandingViewModel viewModel)=> Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(viewModel),);
backgroundColor: kcWhite,
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),
);
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
[ _buildUpperColumn(),_buildLowerColumnWrapper(viewModel)];
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() => [
verticalSpaceLarge,
_buildIconWrapper(),
verticalSpaceLarge
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
];
Widget _buildIconWrapper()=> Align(alignment: Alignment.topLeft,child: _buildIcon(),);
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
color: kcPrimaryColor,
height: 25,
);
'assets/icons/logo.svg',
color: kcPrimaryColor,
height: 25,
);
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
child: _buildLowerColumn(viewModel),
);
child: _buildLowerColumn(viewModel),
);
Widget _buildLowerColumn(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(viewModel),
);
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper(viewModel)
];
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper(viewModel)
];
Widget _buildTitle() =>
Text.rich(
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25P600,
@ -89,44 +82,41 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
style: style25P400,
),
TextSpan(
text: ' እድሜ ',
text: ' ቦታ ',
style: style25P600,
),
TextSpan(
text: 'ይማሩ!',
style: style25P400,
),
],
),
);
Widget _buildImageWrapper()=> Expanded(child: _buildImageClipper());
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper()=> ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage()=> Image.asset('assets/images/profile.png',fit: BoxFit.cover,);
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_3.jpg',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel));
Widget _buildContinueButtonWrapper(LandingViewModel viewModel) => Align(
alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel),
);
alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel),
);
Widget _buildButtonContainer(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButtonState(viewModel),
);
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButtonState(viewModel),
);
Widget _buildContinueButtonState(LandingViewModel 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:stacked/stacked.dart';
import '../../common/app_colors.dart';
import '../../common/translations/locale_keys.g.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_small_radio_button.dart';
import '../../widgets/small_app_bar.dart';
@ -106,16 +108,16 @@ class LanguageView extends StackedView<LanguageViewModel> {
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
showBackButton: true,
onPop: viewModel.pop,
title: 'Language Preference',
title:LocaleKeys.language_preference.tr() ,
);
Widget _buildTitle() => Text(
'Choose your language',
LocaleKeys.choose_your_language.tr(),
style: style25DG600,
);
Widget _buildSubtitle() => Text(
'You can switch languages anytime',
LocaleKeys.switch_language_anytime.tr() ,
style: style14MG400,
);

View File

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

View File

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

View File

@ -89,12 +89,14 @@ class LearnProgramView extends StackedView<LearnProgramViewModel> {
program: viewModel.learnPrograms[index],
onTap: () async => await viewModel
.navigateToLearnCourse(viewModel.learnPrograms[index].id ?? 0),
onLockTap: () async => await viewModel.navigateToLearnSubscription(),
),
);
Widget _buildTile({
required LearnProgram program,
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 =>
_navigationService.navigateToLearnCourseView(id: id);
Future<void> navigateToLearnSubscription() async =>
await _navigationService.navigateToLearnSubscriptionView();
// Remote api call
// Learn programs

View File

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

View File

@ -1,7 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
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/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/profile_card.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart';
@ -138,7 +140,7 @@ class ProfileView extends StackedView<ProfileViewModel> {
);
Widget _buildProfileName(ProfileViewModel viewModel) => Text(
'Hi, ${viewModel.user?.firstName ?? ''} 👋',
'${LocaleKeys.hello.tr()}, ${viewModel.user?.firstName ?? ''} 👋',
style: style25DG600,
);
@ -169,31 +171,31 @@ class ProfileView extends StackedView<ProfileViewModel> {
);
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
title: 'My Progress',
icon: Icons.stacked_bar_chart,
subtitle: 'Track your achievements and learning streak',
onTap: () async => await viewModel.navigateToProgress(),
title: LocaleKeys.my_progress.tr(),
subtitle: LocaleKeys.track_your_achievement.tr(),
// onTap: () async => await viewModel.navigateToProgress(),
);
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Account & Privacy',
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(),
);
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Support',
icon: Icons.headphones,
subtitle: 'Get help through phone or Telegram',
title: LocaleKeys.support.tr(),
subtitle: LocaleKeys.get_help.tr(),
onTap: () async => await viewModel.navigateToSupport(),
);
Widget _buildLogOutButton(ProfileViewModel viewModel) => CustomElevatedButton(
height: 55,
text: 'Logout',
borderRadius: 12,
foregroundColor: kcRed,
text: LocaleKeys.logout.tr(),
backgroundColor: kcRed.withOpacity(0.25),
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/services.dart';
import 'package:intl/intl.dart';
import 'package:stacked/stacked.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/small_app_bar.dart';
@ -160,7 +162,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
title: 'Edit Profile',
title: LocaleKeys.edit_profile.tr(),
);
Widget _buildColumnWrapper(
@ -270,8 +272,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
];
Widget _buildFirstNameLabel() => CustomFormLabel(
label: 'First Name',
style: style16DG600,
label: LocaleKeys.first_name.tr(),
);
Widget _buildFirstNameFormField(ProfileDetailViewModel viewModel) =>
@ -317,8 +319,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
];
Widget _buildLastNameLabel() => CustomFormLabel(
label: 'Last Name',
style: style16DG600,
label: LocaleKeys.last_name.tr(),
);
Widget _buildLastNameFormField(ProfileDetailViewModel viewModel) =>
@ -356,8 +358,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
];
Widget _buildGenderLabel() => CustomFormLabel(
label: 'Gender',
style: style16DG600,
label: LocaleKeys.gender.tr(),
);
Widget _buildRadioButtonWrapper(ProfileDetailViewModel viewModel) => Row(
@ -449,8 +451,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
];
Widget _buildPhoneNumberLabel() => CustomFormLabel(
label: 'Phone Number',
style: style16DG600,
label: LocaleKeys.phone_number.tr(),
);
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
@ -496,8 +498,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
];
Widget _buildEmailLabel() => CustomFormLabel(
label: 'Email',
style: style16DG600,
label: LocaleKeys.email.tr(),
);
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
@ -522,8 +524,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
);
Widget _buildCountryDropdownLabel() => CustomFormLabel(
label: 'Country',
style: style16DG600,
label: LocaleKeys.country.tr(),
);
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
@ -559,8 +561,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
];
Widget _buildRegionFormFieldLabel() => CustomFormLabel(
label: 'Region',
style: style16DG600,
label: LocaleKeys.region.tr(),
);
Widget _buildRegionFormState(ProfileDetailViewModel viewModel) =>
@ -570,8 +572,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildRegionDropDown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select region',
icon: _buildSearchIcon(),
hint:LocaleKeys.select_region.tr(),
selectedItem: viewModel.selectedRegion,
items: (value, props) => viewModel.getRegions(),
onChanged: (value) =>
@ -582,8 +584,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
controller: regionController,
onTap: viewModel.setRegionFocus,
decoration: inputDecoration(
hint: 'Enter Your City',
focus: viewModel.focusRegion,
hint:LocaleKeys.enter_your_city.tr(),
filled: regionController.text.isNotEmpty),
);
@ -614,14 +616,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
];
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
label: 'Occupation',
style: style16DG600,
label: LocaleKeys.occupation.tr(),
);
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select occupation',
icon: _buildSearchIcon(),
hint:LocaleKeys.select_occupation.tr(),
selectedItem: viewModel.selectedOccupation,
items: (value, props) => viewModel.getOccupations(),
onChanged: (value) => viewModel.setSelectedOccupation(
@ -645,9 +647,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Save Changes',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
text: LocaleKeys.save_changes.tr(),
onTap: () async => await _update(viewModel),
);
@ -659,10 +661,10 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Cancel',
borderRadius: 12,
onTap: viewModel.pop,
backgroundColor: kcWhite,
text:LocaleKeys.cancel.tr(),
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
);

View File

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

View File

@ -7,6 +7,7 @@ import '../../../app/app.router.dart';
import '../../../models/user.dart';
import '../../../services/api_service.dart';
import '../../../services/image_downloader_service.dart';
import '../../../services/localization_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
@ -15,6 +16,7 @@ class StartupViewModel extends ReactiveViewModel {
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _localizationService = locator<LocalizationService>();
final _authenticationService = locator<AuthenticationService>();
final _imageDownloaderService = locator<ImageDownloaderService>();
@ -29,6 +31,8 @@ class StartupViewModel extends ReactiveViewModel {
// Main startup and navigation logic
Future runStartupLogic() async {
await _localizationService.loadSelectedLanguage();
final loggedIn = await _authenticationService.userLoggedIn();
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:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/widgets/support_card.dart';
import '../../common/app_colors.dart';
@ -51,10 +53,10 @@ class SupportView extends StackedView<SupportViewModel> {
);
Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar(
title: 'Need Help?',
showBackButton: true,
onPop: viewModel.pop,
);
title:LocaleKeys.need_help.tr(),
);
Widget _buildContentWrapper(SupportViewModel viewModel) =>
Expanded(child: _buildContentColumnWrapper(viewModel));
@ -85,16 +87,16 @@ class SupportView extends StackedView<SupportViewModel> {
Widget _buildCallSupport(SupportViewModel viewModel) => SupportCard(
icon: Icons.call,
color: kcPrimaryColor,
title: 'Call Support',
subtitle: 'Talk with our support team directly',
title:LocaleKeys.call_support.tr(),
subtitle: LocaleKeys.talk_with_support.tr(),
onTap: () async => await viewModel.navigateToCallSupport(),
);
Widget _buildTelegramSupport(SupportViewModel viewModel) => SupportCard(
color: kcSkyBlue,
icon: Icons.telegram,
title: 'Telegram Support',
subtitle: 'Chat Instantly via Telegram',
title: LocaleKeys.telegram_support.tr(),
subtitle: LocaleKeys.chat_via_telegram.tr(),
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_html/flutter_html.dart';
import 'package:stacked/stacked.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/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'terms_and_conditions_viewmodel.dart';
@ -59,7 +60,7 @@ class TermsAndConditionsView extends StackedView<TermsAndConditionsViewModel> {
Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar(
onPop: viewModel.pop,
showBackButton: true,
title: 'Terms and Conditions',
title: LocaleKeys.terms_and_conditions.tr(),
);
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:stacked/stacked.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/enmus.dart';
import '../common/helper_functions.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
import 'mini_thumbnail.dart';
class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
class CourseLessonTile extends ViewModelWidget<CourseModuleViewModel> {
final CourseLesson lesson;
final GestureTapCallback? onVideoTap;
final GestureTapCallback? onPracticeTap;
@ -21,19 +23,16 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
});
@override
Widget build(BuildContext context, CourseLessonViewModel viewModel) =>
Widget build(BuildContext context, CourseModuleViewModel viewModel) =>
_buildExpansionTileCard(context: context, viewModel: viewModel);
Widget _buildExpansionTileCard(
{required BuildContext context,
required CourseLessonViewModel viewModel}) =>
required CourseModuleViewModel viewModel}) =>
Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: kcPrimaryColor.withValues(alpha: 0.25),
),
),
child: _buildColumn(),
);
@ -44,7 +43,7 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
);
List<Widget> _buildColumnChildren() => [
// _buildDivider(),
_buildDivider(),
verticalSpaceMedium,
_buildTile(),
verticalSpaceMedium,
@ -52,11 +51,14 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
verticalSpaceSmall,
];
Widget _buildDivider() => const Divider(color: kcVeryLightGrey);
Widget _buildTile() => ListTile(
minTileHeight: 0,
title: _buildTitle(),
subtitle: _buildSubtitle(),
leading: _buildLeadingWrapper(),
trailing: _buildTrailingWrapper(),
titleAlignment: ListTileTitleAlignment.top,
contentPadding: const EdgeInsets.symmetric(horizontal: 15),
);
@ -67,12 +69,29 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
);
Widget _buildSubtitle() => Text(
'${((lesson.duration ?? 0) / 50).toInt()} min',
'${((lesson.id ?? 0) / 50).toInt()} min',
style: style14MG400,
);
Widget _buildLeadingWrapper() =>
const MiniThumbnail(thumbnail: 'assets/images/image_1.png');
Widget _buildLeadingWrapper() => MiniThumbnail(
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(
height: 40,
@ -107,9 +126,9 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
Widget _buildPracticeButton() => CustomElevatedButton(
height: 15,
text: 'Practice',
borderRadius: 12,
onTap: onPracticeTap,
text: 'Practice Test',
backgroundColor: kcWhite,
borderColor: 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:yimaru_app/models/course_module.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
class CourseModuleTileSmall extends StatelessWidget {
final String title;
final ProgressStatuses status;
final CourseModule? module;
final GestureTapCallback? onTap;
const CourseModuleTileSmall(
{super.key, required this.title, required this.status});
const CourseModuleTileSmall({super.key, this.onTap, required this.module});
@override
Widget build(BuildContext context) => _buildTile();
Widget _buildTile() => ListTile(
onTap: onTap,
title: _buildTitle(),
leading: _buildLeadingWrapper(),
trailing: _buildTrailingWrapper(),
@ -27,7 +28,7 @@ class CourseModuleTileSmall extends StatelessWidget {
);
Widget _buildTitle() => Text(
title,
module?.name ?? '',
maxLines: 1,
softWrap: false,
style: style14DG600,
@ -43,9 +44,10 @@ class CourseModuleTileSmall extends StatelessWidget {
color: kcLightGrey,
);
Widget _buildTrailingWrapper() => status == ProgressStatuses.completed
? _buildCompletedTrailing()
: _buildPendingTrailing();
Widget _buildTrailingWrapper() =>
ProgressStatuses.completed != ProgressStatuses.completed
? _buildCompletedTrailing()
: _buildPendingTrailing();
Widget _buildCompletedTrailing() => const Icon(
Icons.check_circle,

View File

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

View File

@ -12,13 +12,11 @@ import 'custom_elevated_button.dart';
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
final LearnModule module;
final GestureTapCallback? onLockTap;
final GestureTapCallback? onModuleTap;
final GestureTapCallback? onPracticeTap;
const LearnModuleTile(
{super.key,
this.onLockTap,
this.onModuleTap,
this.onPracticeTap,
required this.module});
@ -34,15 +32,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
@override
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(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>

View File

@ -1,5 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/progress_status.dart';
@ -11,12 +13,20 @@ import 'custom_elevated_button.dart';
class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
final LearnProgram program;
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
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(
margin: const EdgeInsets.only(bottom: 15),
@ -99,8 +109,8 @@ class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
Widget _buildProgressStatus() => ProgressStatus(
color: kcPrimaryColor,
status: (program.access?.isCompleted ?? false)
? 'Completed'
: 'In Progress',
?LocaleKeys.completed.tr()
: LocaleKeys.in_progress.tr(),
);
Widget _buildContent() => Text(
@ -123,7 +133,7 @@ class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
text: program.access?.progressPercent == 0
? 'Start Learning'
: 'Continue Learning',
? LocaleKeys.start_learning.tr()
:LocaleKeys.continue_learning.tr() ,
);
}

View File

@ -1,10 +1,12 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../common/app_colors.dart';
import '../common/translations/locale_keys.g.dart';
class ProfileAppBar extends StatelessWidget {
final String? name;
@ -72,7 +74,7 @@ class ProfileAppBar extends StatelessWidget {
[_buildGreetingTitle(), _buildSubtitle()];
Widget _buildGreetingTitle() => Text.rich(
TextSpan(text: 'Hello,', style: style14DG600, children: [
TextSpan(text: '${LocaleKeys.hello.tr()},', style: style14DG600, children: [
TextSpan(
text: ' $name!',
style: style14P600,
@ -81,7 +83,7 @@ class ProfileAppBar extends StatelessWidget {
);
Widget _buildSubtitle() => Text(
'Ready to keep learning English today?',
LocaleKeys.ready_to_learn.tr(),
textAlign: TextAlign.center,
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: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 {
final GestureTapCallback? onTap;
@ -21,10 +24,9 @@ class ViewProfileButton extends StatelessWidget {
List<Widget> _buildButtonRowChildren() =>
[_buildButtonText(), const SizedBox(width: 10), _buildButtonIcon()];
Widget _buildButtonText() => const Text(
'View Profile',
style: TextStyle(
color: kcPrimaryColor, fontSize: 16, fontWeight: FontWeight.w900),
Widget _buildButtonText() => Text(
LocaleKeys.view_profile.tr(),
style: style16P900,
);
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
version: 0.1.17+19
version: 0.1.18+20
publish_to: 'none'
description: A new Flutter project.

View File

@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('CourseLessonViewModel Tests -', () {
group('CourseModuleViewModel 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('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());
});
}