fix: Apply UAT comments
BIN
assets/images/landing_1.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/images/landing_2.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/images/landing_3.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 448 KiB After Width: | Height: | Size: 448 KiB |
|
Before Width: | Height: | Size: 467 KiB |
|
Before Width: | Height: | Size: 442 KiB |
|
|
@ -40,6 +40,9 @@
|
||||||
"new_password": "አዲስ የይለፍ ቃል",
|
"new_password": "አዲስ የይለፍ ቃል",
|
||||||
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
|
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
|
||||||
"view_course": " ኮርሱን ይመልከቱ",
|
"view_course": " ኮርሱን ይመልከቱ",
|
||||||
|
"continue_learning": "መማርን ይቀጥሉ",
|
||||||
|
"start_learning": "ትምህርትን ይጀምሩ",
|
||||||
|
"completed": "ተጠናቋል",
|
||||||
"take_practice": " ልምምድ ያድርጉ",
|
"take_practice": " ልምምድ ያድርጉ",
|
||||||
"your_current_level": "የአሁኑ ደረጃዎ",
|
"your_current_level": "የአሁኑ ደረጃዎ",
|
||||||
"overall_progress": "አጠቃላይ እድገት",
|
"overall_progress": "አጠቃላይ እድገት",
|
||||||
|
|
@ -90,8 +93,33 @@
|
||||||
"phone_number": "የስልክ ቁጥር",
|
"phone_number": "የስልክ ቁጥር",
|
||||||
"country": "ሀገር",
|
"country": "ሀገር",
|
||||||
"region": "ክልል",
|
"region": "ክልል",
|
||||||
|
"select_region": "ክልል ይምረጡ",
|
||||||
|
"enter_your_city": "ከተማዎን ያስገቡ",
|
||||||
"occupation": "የስራ መስክ",
|
"occupation": "የስራ መስክ",
|
||||||
"save_changes": "ለውጦችን ያስቀምጡ"
|
"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": "ለመደወል ይንኩ"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@
|
||||||
"reset_code": "Reset code",
|
"reset_code": "Reset code",
|
||||||
"new_password": "New password",
|
"new_password": "New password",
|
||||||
"logged_in_successfully": "Logged in successfully",
|
"logged_in_successfully": "Logged in successfully",
|
||||||
|
"continue_learning": "Continue Learning",
|
||||||
|
"start_learning": "Start Learning",
|
||||||
|
"completed": "Completed",
|
||||||
"view_course": "View course",
|
"view_course": "View course",
|
||||||
"take_practice": "Take practice",
|
"take_practice": "Take practice",
|
||||||
"your_current_level": "Your current level",
|
"your_current_level": "Your current level",
|
||||||
|
|
@ -90,6 +93,32 @@
|
||||||
"phone_number": "Phone number",
|
"phone_number": "Phone number",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
|
"select_region": "Select region",
|
||||||
|
"enter_your_city": "Enter your city",
|
||||||
"occupation": "Occupation",
|
"occupation": "Occupation",
|
||||||
"save_changes": "Save changes"
|
"select_occupation": "Select occupation",
|
||||||
|
"save_changes": "Save changes",
|
||||||
|
"my_progress": "My progress",
|
||||||
|
"track_your_achievement": "Track your achievements and learning streak",
|
||||||
|
"account_and_privacy": "Account & Privacy",
|
||||||
|
"manage_settings": "Manage settings and app preference",
|
||||||
|
"support": "Support",
|
||||||
|
"get_help": "Get help through phone or Telegram",
|
||||||
|
"logout": "Logout",
|
||||||
|
"app_settings": "App settings",
|
||||||
|
"legal_and_information": "Legal & Information",
|
||||||
|
"change_language": "Change language",
|
||||||
|
"terms_and_conditions":"Terms & Conditions",
|
||||||
|
"delete_account": "Delete account",
|
||||||
|
"language_preference": "Language preference",
|
||||||
|
"choose_your_language": "Choose your language",
|
||||||
|
"switch_language_anytime": "You can switch languages anytime",
|
||||||
|
"need_help": "Need help?",
|
||||||
|
"call_support": "Call support",
|
||||||
|
"talk_with_support": "Talk with our support team directly",
|
||||||
|
"telegram_support": "Telegram support",
|
||||||
|
"chat_via_telegram" :"Chat instantly via Telegram",
|
||||||
|
"call_our_support": "Call our support team between 9 AM - 6 PM",
|
||||||
|
"tap_to_call": "Tap to call"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import 'package:yimaru_app/services/api_service.dart';
|
||||||
import 'package:yimaru_app/services/secure_storage_service.dart';
|
import 'package:yimaru_app/services/secure_storage_service.dart';
|
||||||
import 'package:yimaru_app/services/dio_service.dart';
|
import 'package:yimaru_app/services/dio_service.dart';
|
||||||
import 'package:yimaru_app/services/status_checker_service.dart';
|
import 'package:yimaru_app/services/status_checker_service.dart';
|
||||||
import 'package:yimaru_app/ui/views/welcome/welcome_view.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
|
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
|
||||||
import 'package:yimaru_app/services/permission_handler_service.dart';
|
import 'package:yimaru_app/services/permission_handler_service.dart';
|
||||||
import 'package:yimaru_app/services/image_picker_service.dart';
|
import 'package:yimaru_app/services/image_picker_service.dart';
|
||||||
|
|
@ -33,10 +32,8 @@ import 'package:yimaru_app/services/image_downloader_service.dart';
|
||||||
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
|
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
|
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_lesson/course_lesson_view.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
|
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
|
||||||
import 'package:yimaru_app/services/notification_service.dart';
|
import 'package:yimaru_app/services/notification_service.dart';
|
||||||
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
|
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
|
||||||
|
|
@ -45,7 +42,6 @@ import 'package:yimaru_app/services/course_service.dart';
|
||||||
import 'package:yimaru_app/ui/views/course/course_view.dart';
|
import 'package:yimaru_app/ui/views/course/course_view.dart';
|
||||||
import 'package:yimaru_app/services/audio_player_service.dart';
|
import 'package:yimaru_app/services/audio_player_service.dart';
|
||||||
import 'package:yimaru_app/services/voice_recorder_service.dart';
|
import 'package:yimaru_app/services/voice_recorder_service.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_view.dart';
|
|
||||||
import 'package:yimaru_app/services/in_app_update_service.dart';
|
import 'package:yimaru_app/services/in_app_update_service.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
|
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
|
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
|
||||||
|
|
@ -60,6 +56,7 @@ import 'package:yimaru_app/ui/views/course_catalog/course_catalog_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart';
|
import 'package:yimaru_app/ui/views/course_unit/course_unit_view.dart';
|
||||||
import 'package:yimaru_app/services/localization_service.dart';
|
import 'package:yimaru_app/services/localization_service.dart';
|
||||||
import 'package:yimaru_app/ui/views/landing/landing_view.dart';
|
import 'package:yimaru_app/ui/views/landing/landing_view.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_module/course_module_view.dart';
|
||||||
// @stacked-import
|
// @stacked-import
|
||||||
|
|
||||||
@StackedApp(
|
@StackedApp(
|
||||||
|
|
@ -81,19 +78,15 @@ import 'package:yimaru_app/ui/views/landing/landing_view.dart';
|
||||||
MaterialRoute(page: RegisterView),
|
MaterialRoute(page: RegisterView),
|
||||||
MaterialRoute(page: LoginView),
|
MaterialRoute(page: LoginView),
|
||||||
MaterialRoute(page: LearnModuleView),
|
MaterialRoute(page: LearnModuleView),
|
||||||
MaterialRoute(page: WelcomeView),
|
|
||||||
MaterialRoute(page: LearnLessonView),
|
MaterialRoute(page: LearnLessonView),
|
||||||
MaterialRoute(page: ForgetPasswordView),
|
MaterialRoute(page: ForgetPasswordView),
|
||||||
MaterialRoute(page: LearnLessonDetailView),
|
MaterialRoute(page: LearnLessonDetailView),
|
||||||
MaterialRoute(page: LearnPracticeView),
|
MaterialRoute(page: LearnPracticeView),
|
||||||
MaterialRoute(page: CoursePracticeView),
|
|
||||||
MaterialRoute(page: CoursePaymentView),
|
MaterialRoute(page: CoursePaymentView),
|
||||||
MaterialRoute(page: FailureView),
|
MaterialRoute(page: FailureView),
|
||||||
MaterialRoute(page: CourseLessonView),
|
|
||||||
MaterialRoute(page: CourseLessonDetailView),
|
MaterialRoute(page: CourseLessonDetailView),
|
||||||
MaterialRoute(page: DuolingoView),
|
MaterialRoute(page: DuolingoView),
|
||||||
MaterialRoute(page: CourseView),
|
MaterialRoute(page: CourseView),
|
||||||
MaterialRoute(page: CoursePracticeQuestionView),
|
|
||||||
MaterialRoute(page: LearnProgramView),
|
MaterialRoute(page: LearnProgramView),
|
||||||
MaterialRoute(page: LearnCourseView),
|
MaterialRoute(page: LearnCourseView),
|
||||||
MaterialRoute(page: AssessmentView),
|
MaterialRoute(page: AssessmentView),
|
||||||
|
|
@ -102,6 +95,8 @@ import 'package:yimaru_app/ui/views/landing/landing_view.dart';
|
||||||
MaterialRoute(page: CourseCatalogView),
|
MaterialRoute(page: CourseCatalogView),
|
||||||
MaterialRoute(page: CourseUnitView),
|
MaterialRoute(page: CourseUnitView),
|
||||||
MaterialRoute(page: LandingView),
|
MaterialRoute(page: LandingView),
|
||||||
|
MaterialRoute(page: CourseModuleView),
|
||||||
|
MaterialRoute(page: LearnCourseView),
|
||||||
// @stacked-route
|
// @stacked-route
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|
|
||||||
|
|
@ -4,51 +4,35 @@ part 'course_lesson.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class CourseLesson {
|
class CourseLesson {
|
||||||
int? id;
|
final int? id;
|
||||||
|
|
||||||
String? title;
|
final String? title;
|
||||||
|
|
||||||
int? duration;
|
final String? thumbnail;
|
||||||
|
|
||||||
String? status;
|
final String? description;
|
||||||
|
|
||||||
String? thumbnail;
|
|
||||||
|
|
||||||
String? resolution;
|
|
||||||
|
|
||||||
String? visibility;
|
|
||||||
|
|
||||||
String? description;
|
|
||||||
|
|
||||||
@JsonKey(name: 'video_url')
|
@JsonKey(name: 'video_url')
|
||||||
String? videoUrl;
|
final String? videoUrl;
|
||||||
|
|
||||||
@JsonKey(name: 'vimeo_status')
|
@JsonKey(name: 'sort_order')
|
||||||
String? vimeoStatus;
|
final int? sortOrder;
|
||||||
|
|
||||||
@JsonKey(name: 'instructor_id')
|
@JsonKey(name: 'has_practice')
|
||||||
int? instructorId;
|
final bool? hasPractice;
|
||||||
|
|
||||||
@JsonKey(name: 'sub_course_id')
|
@JsonKey(name: 'unit_module_id')
|
||||||
int? courseId;
|
final int? unitModuleId;
|
||||||
|
|
||||||
@JsonKey(name: 'display_order')
|
const CourseLesson(
|
||||||
int? displayOrder;
|
|
||||||
|
|
||||||
CourseLesson(
|
|
||||||
{this.id,
|
{this.id,
|
||||||
this.title,
|
this.title,
|
||||||
this.status,
|
|
||||||
this.courseId,
|
|
||||||
this.videoUrl,
|
this.videoUrl,
|
||||||
this.duration,
|
this.sortOrder,
|
||||||
this.thumbnail,
|
this.thumbnail,
|
||||||
this.visibility,
|
|
||||||
this.resolution,
|
|
||||||
this.vimeoStatus,
|
|
||||||
this.description,
|
this.description,
|
||||||
this.displayOrder,
|
this.hasPractice,
|
||||||
this.instructorId});
|
this.unitModuleId});
|
||||||
|
|
||||||
factory CourseLesson.fromJson(Map<String, dynamic> json) =>
|
factory CourseLesson.fromJson(Map<String, dynamic> json) =>
|
||||||
_$CourseLessonFromJson(json);
|
_$CourseLessonFromJson(json);
|
||||||
|
|
|
||||||
|
|
@ -9,32 +9,22 @@ part of 'course_lesson.dart';
|
||||||
CourseLesson _$CourseLessonFromJson(Map<String, dynamic> json) => CourseLesson(
|
CourseLesson _$CourseLessonFromJson(Map<String, dynamic> json) => CourseLesson(
|
||||||
id: (json['id'] as num?)?.toInt(),
|
id: (json['id'] as num?)?.toInt(),
|
||||||
title: json['title'] as String?,
|
title: json['title'] as String?,
|
||||||
status: json['status'] as String?,
|
|
||||||
courseId: (json['sub_course_id'] as num?)?.toInt(),
|
|
||||||
videoUrl: json['video_url'] as String?,
|
videoUrl: json['video_url'] as String?,
|
||||||
duration: (json['duration'] as num?)?.toInt(),
|
sortOrder: (json['sort_order'] as num?)?.toInt(),
|
||||||
thumbnail: json['thumbnail'] as String?,
|
thumbnail: json['thumbnail'] as String?,
|
||||||
visibility: json['visibility'] as String?,
|
|
||||||
resolution: json['resolution'] as String?,
|
|
||||||
vimeoStatus: json['vimeo_status'] as String?,
|
|
||||||
description: json['description'] as String?,
|
description: json['description'] as String?,
|
||||||
displayOrder: (json['display_order'] as num?)?.toInt(),
|
hasPractice: json['has_practice'] as bool?,
|
||||||
instructorId: (json['instructor_id'] as num?)?.toInt(),
|
unitModuleId: (json['unit_module_id'] as num?)?.toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$CourseLessonToJson(CourseLesson instance) =>
|
Map<String, dynamic> _$CourseLessonToJson(CourseLesson instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'title': instance.title,
|
'title': instance.title,
|
||||||
'duration': instance.duration,
|
|
||||||
'status': instance.status,
|
|
||||||
'thumbnail': instance.thumbnail,
|
'thumbnail': instance.thumbnail,
|
||||||
'resolution': instance.resolution,
|
|
||||||
'visibility': instance.visibility,
|
|
||||||
'description': instance.description,
|
'description': instance.description,
|
||||||
'video_url': instance.videoUrl,
|
'video_url': instance.videoUrl,
|
||||||
'vimeo_status': instance.vimeoStatus,
|
'sort_order': instance.sortOrder,
|
||||||
'instructor_id': instance.instructorId,
|
'has_practice': instance.hasPractice,
|
||||||
'sub_course_id': instance.courseId,
|
'unit_module_id': instance.unitModuleId,
|
||||||
'display_order': instance.displayOrder,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,9 @@ import 'package:dio/dio.dart';
|
||||||
import 'package:yimaru_app/models/learn_lesson.dart';
|
import 'package:yimaru_app/models/learn_lesson.dart';
|
||||||
import 'package:yimaru_app/models/learn_practice.dart';
|
import 'package:yimaru_app/models/learn_practice.dart';
|
||||||
import 'package:yimaru_app/models/learn_program.dart';
|
import 'package:yimaru_app/models/learn_program.dart';
|
||||||
import 'package:yimaru_app/models/level.dart';
|
|
||||||
import 'package:yimaru_app/models/assessment_question.dart';
|
import 'package:yimaru_app/models/assessment_question.dart';
|
||||||
import 'package:yimaru_app/models/course_catalog.dart';
|
import 'package:yimaru_app/models/course_catalog.dart';
|
||||||
import 'package:yimaru_app/models/course_lesson.dart';
|
import 'package:yimaru_app/models/course_lesson.dart';
|
||||||
import 'package:yimaru_app/models/course_progress.dart';
|
|
||||||
import 'package:yimaru_app/models/course.dart';
|
|
||||||
import 'package:yimaru_app/models/practice.dart';
|
|
||||||
import 'package:yimaru_app/models/user.dart';
|
import 'package:yimaru_app/models/user.dart';
|
||||||
import 'package:yimaru_app/services/dio_service.dart';
|
import 'package:yimaru_app/services/dio_service.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||||
|
|
@ -20,10 +16,7 @@ import '../models/learn_course.dart';
|
||||||
import '../models/learn_module.dart';
|
import '../models/learn_module.dart';
|
||||||
import '../models/learn_question.dart';
|
import '../models/learn_question.dart';
|
||||||
import '../models/learn_subscription.dart';
|
import '../models/learn_subscription.dart';
|
||||||
import '../models/lesson.dart';
|
|
||||||
import '../models/module.dart';
|
|
||||||
import '../models/assessment.dart';
|
import '../models/assessment.dart';
|
||||||
import '../models/submodule.dart';
|
|
||||||
import '../models/learn_subscription_request.dart';
|
import '../models/learn_subscription_request.dart';
|
||||||
import '../ui/common/enmus.dart';
|
import '../ui/common/enmus.dart';
|
||||||
|
|
||||||
|
|
@ -779,303 +772,20 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TO BE MODIFIED*/
|
|
||||||
|
|
||||||
// Get courses
|
|
||||||
// Future<List<Course>> getCourses(int id) async {
|
|
||||||
// try {
|
|
||||||
// List<Course> courses = [];
|
|
||||||
//
|
|
||||||
// final Response response = await _service.dio
|
|
||||||
// .get('$kBaseUrl/$kCourseBaseUrl/$kCoursesUrl/$id/$kSubcoursesUrl');
|
|
||||||
//
|
|
||||||
// if (response.statusCode == 200) {
|
|
||||||
// var data = response.data;
|
|
||||||
// var decodedData = data['data']['sub_courses'] as List;
|
|
||||||
// courses = decodedData.map(
|
|
||||||
// (e) {
|
|
||||||
// return Course.fromJson(e);
|
|
||||||
// },
|
|
||||||
// ).toList();
|
|
||||||
// return courses;
|
|
||||||
// }
|
|
||||||
// return [];
|
|
||||||
// } catch (e) {
|
|
||||||
// return [];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Get course progress
|
|
||||||
Future<List<CourseProgress>> getCourseProgress(int id) async {
|
|
||||||
try {
|
|
||||||
List<CourseProgress> courseProgress = [];
|
|
||||||
|
|
||||||
final Response response =
|
|
||||||
await _service.dio.get('$kBaseUrl/$kCourseProgressUrl/$id');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data'] as List;
|
|
||||||
courseProgress = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return CourseProgress.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return courseProgress;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get course lessons
|
// Get course lessons
|
||||||
Future<List<CourseLesson>> getCourseLessons(int id) async {
|
Future<List<CourseLesson>> getCourseLessons(int id) async {
|
||||||
try {
|
try {
|
||||||
List<CourseLesson> courseLessons = [];
|
List<CourseLesson> lessons = [];
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
final Response response = await _service.dio.get(
|
||||||
'$kBaseUrl/$kCourseBaseUrl/$kSubcoursesUrl/$id/$kPublishedVideos');
|
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kExamPrepUrl/$kModulesUrl/$id/$kLessonsUrl');
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
var data = response.data;
|
var data = response.data;
|
||||||
var decodedData = data['data'] as List;
|
var decodedData = data['data']['lessons'] as List;
|
||||||
courseLessons = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return CourseLesson.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return courseLessons;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete lesson
|
|
||||||
Future<Map<String, dynamic>> completeLesson(int id) async {
|
|
||||||
try {
|
|
||||||
Response response = await _service.dio.post(
|
|
||||||
'$kBaseUrl/$kLessonProgressUrl/$id/$kCompleteUrl',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
return {'status': ResponseStatus.success, 'message': 'Video completed'};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
'status': ResponseStatus.failure,
|
|
||||||
'message': 'Unknown Error Occurred'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} on DioException catch (e) {
|
|
||||||
return {
|
|
||||||
'status': ResponseStatus.failure,
|
|
||||||
'message': e.response?.data.toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Course practices
|
|
||||||
Future<List<Practice>> getCoursePractices(int id) async {
|
|
||||||
try {
|
|
||||||
List<Practice> coursePractices = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice?owner_type=SUB_COURSE&owner_id=$id');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data'] as List;
|
|
||||||
coursePractices = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return Practice.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return coursePractices;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get course practic questions
|
|
||||||
Future<List<AssessmentQuestion>> getCoursePracticeQuestions(int id) async {
|
|
||||||
try {
|
|
||||||
List<AssessmentQuestion> coursePracticeQuestions = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio
|
|
||||||
.get('$kBaseUrl/$kPracticeBaseUrl/$id/$kCoursePracticeQuestions');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data'] as List;
|
|
||||||
coursePracticeQuestions = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return AssessmentQuestion.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return coursePracticeQuestions;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get course practice question
|
|
||||||
Future<AssessmentQuestion?> getCoursePracticeQuestion(int id) async {
|
|
||||||
try {
|
|
||||||
final Response response =
|
|
||||||
await _service.dio.get('$kBaseUrl/$kCoursePracticeQuestion/$id');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
AssessmentQuestion question =
|
|
||||||
AssessmentQuestion.fromJson(response.data['data']);
|
|
||||||
|
|
||||||
return question;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get learn subcategories
|
|
||||||
Future<List<CourseCatalog>> getLearnSubcategories() async {
|
|
||||||
try {
|
|
||||||
List<CourseCatalog> learnSubcategories = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLearnSubcategoriesUrl');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data']['sub_categories'] as List;
|
|
||||||
learnSubcategories = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return CourseCatalog.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return learnSubcategories;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get courses
|
|
||||||
Future<List<Course>> getCourses(int id) async {
|
|
||||||
try {
|
|
||||||
List<Course> courses = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kSubcategoriesUrl/$id/$kCoursesUrl');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data']['courses'] as List;
|
|
||||||
courses = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return Course.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return courses;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get levels
|
|
||||||
Future<List<Level>> getLevels(int id) async {
|
|
||||||
try {
|
|
||||||
List<Level> levels = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kCoursesUrl/$id/$kLevelsUrl');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data']['levels'] as List;
|
|
||||||
levels = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return Level.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return levels;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get modules
|
|
||||||
Future<List<Module>> getModules(int id) async {
|
|
||||||
try {
|
|
||||||
List<Module> modules = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kLevelsUrl/$id/$kModulesUrl');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data']['modules'] as List;
|
|
||||||
modules = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return Module.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return modules;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get submodules
|
|
||||||
Future<List<Submodule>> getSubmodules(int id) async {
|
|
||||||
try {
|
|
||||||
List<Submodule> submodules = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kModulesUrl/$id/$kSubmodulesUrl');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data']['sub_modules'] as List;
|
|
||||||
submodules = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return Submodule.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return submodules;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get lessons
|
|
||||||
Future<List<Lesson>> getLessons(int id) async {
|
|
||||||
try {
|
|
||||||
List<Lesson> lessons = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kSubmodulesUrl/$id/$kLessonsUrl');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data'] as List;
|
|
||||||
lessons = decodedData.map(
|
lessons = decodedData.map(
|
||||||
(e) {
|
(e) {
|
||||||
return Lesson.fromJson(e);
|
return CourseLesson.fromJson(e);
|
||||||
},
|
},
|
||||||
).toList();
|
).toList();
|
||||||
return lessons;
|
return lessons;
|
||||||
|
|
@ -1085,52 +795,4 @@ class ApiService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Practices
|
|
||||||
Future<List<Practice>> getPractices(int id) async {
|
|
||||||
try {
|
|
||||||
List<Practice> coursePractices = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice?owner_type=SUB_MODULE&owner_id=$id');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data'] as List;
|
|
||||||
coursePractices = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return Practice.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return coursePractices;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Questions
|
|
||||||
Future<List<AssessmentQuestion>> getQuestions(int id) async {
|
|
||||||
try {
|
|
||||||
List<AssessmentQuestion> questions = [];
|
|
||||||
|
|
||||||
final Response response = await _service.dio.get(
|
|
||||||
'$kBaseUrl/api/$kApiVersionUrl/$kQuestionSetsUrl/$id/$kQuestionsUrl');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var data = response.data;
|
|
||||||
var decodedData = data['data'] as List;
|
|
||||||
questions = decodedData.map(
|
|
||||||
(e) {
|
|
||||||
return AssessmentQuestion.fromJson(e);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return questions;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ import 'package:yimaru_app/app/app.locator.dart';
|
||||||
import 'package:yimaru_app/models/user.dart';
|
import 'package:yimaru_app/models/user.dart';
|
||||||
import 'package:yimaru_app/services/secure_storage_service.dart';
|
import 'package:yimaru_app/services/secure_storage_service.dart';
|
||||||
|
|
||||||
|
import 'localization_service.dart';
|
||||||
|
|
||||||
class AuthenticationService with ListenableServiceMixin {
|
class AuthenticationService with ListenableServiceMixin {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _secureService = locator<SecureStorageService>();
|
final _secureService = locator<SecureStorageService>();
|
||||||
|
|
||||||
|
final _localizationService = locator<LocalizationService>();
|
||||||
|
|
||||||
// User data
|
// User data
|
||||||
User? _user;
|
User? _user;
|
||||||
|
|
||||||
|
|
@ -14,7 +18,7 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
AuthenticationService() {
|
AuthenticationService() {
|
||||||
listenToReactiveValues([_user]);
|
listenToReactiveValues([_user, _localizationService]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check user logged in
|
// Check user logged in
|
||||||
|
|
@ -172,9 +176,12 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
// Logout
|
// Logout
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
bool firstTimeInstall = await isFirstTimeInstall();
|
bool firstTimeInstall = await isFirstTimeInstall();
|
||||||
|
String language = await _localizationService.selectedLanguage['code'];
|
||||||
|
|
||||||
_user = null;
|
_user = null;
|
||||||
await _secureService.clear();
|
await _secureService.clear();
|
||||||
await setFirstTimeInstall(firstTimeInstall);
|
await setFirstTimeInstall(firstTimeInstall);
|
||||||
|
await _secureService.setString('language', language);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/app/app.locator.dart';
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
import 'package:yimaru_app/models/course_progress.dart';
|
|
||||||
import 'package:yimaru_app/services/api_service.dart';
|
import 'package:yimaru_app/services/api_service.dart';
|
||||||
|
|
||||||
import '../models/course_catalog.dart';
|
import '../models/course_catalog.dart';
|
||||||
import '../models/course_detail.dart';
|
import '../models/course_lesson.dart';
|
||||||
import '../models/course_module.dart';
|
import '../models/course_module.dart';
|
||||||
import '../models/course_unit.dart';
|
import '../models/course_unit.dart';
|
||||||
|
|
||||||
|
|
@ -28,10 +27,15 @@ class CourseService with ListenableServiceMixin {
|
||||||
List<CourseUnit> get units => _units;
|
List<CourseUnit> get units => _units;
|
||||||
|
|
||||||
// Course modules
|
// Course modules
|
||||||
List<CourseModule> _modules = [];
|
final List<CourseModule> _modules = [];
|
||||||
|
|
||||||
List<CourseModule> get modules => _modules;
|
List<CourseModule> get modules => _modules;
|
||||||
|
|
||||||
|
// Course lessons
|
||||||
|
List<CourseLesson> _lessons = [];
|
||||||
|
|
||||||
|
List<CourseLesson> get lessons => _lessons;
|
||||||
|
|
||||||
// Course catalogs
|
// Course catalogs
|
||||||
Future<void> getCourseCatalogs() async {
|
Future<void> getCourseCatalogs() async {
|
||||||
_catalogs = await _apiService.getCourseCatalogs();
|
_catalogs = await _apiService.getCourseCatalogs();
|
||||||
|
|
@ -47,7 +51,7 @@ class CourseService with ListenableServiceMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Course modules
|
// Course modules
|
||||||
Future<void> getCourseUnitModule({
|
Future<void> getCourseModules({
|
||||||
required int id,
|
required int id,
|
||||||
required int index,
|
required int index,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -66,26 +70,10 @@ class CourseService with ListenableServiceMixin {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getCourseModules(int id) async {
|
// Course units
|
||||||
_modules = await _apiService.getCourseModules(id);
|
Future<void> getCourseLessons(int id) async {
|
||||||
_modules.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
_lessons = await _apiService.getCourseLessons(id);
|
||||||
|
_lessons.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get course detail
|
|
||||||
Future<List<CourseDetail>> getCoursesDetail(int id) async {
|
|
||||||
final courses = await _apiService.getCourses(id);
|
|
||||||
final progress = await _apiService.getCourseProgress(id);
|
|
||||||
|
|
||||||
final progressMap = {
|
|
||||||
for (var p in progress.whereType<CourseProgress>()) p.courseId: p
|
|
||||||
};
|
|
||||||
|
|
||||||
return courses.map((course) {
|
|
||||||
return CourseDetail(
|
|
||||||
course: course,
|
|
||||||
courseProgress: progressMap[course.id],
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:yimaru_app/services/secure_storage_service.dart';
|
||||||
|
|
||||||
|
import '../app/app.locator.dart';
|
||||||
|
|
||||||
class LocalizationService with ListenableServiceMixin {
|
class LocalizationService with ListenableServiceMixin {
|
||||||
|
// Dependency injection
|
||||||
|
final _secureService = locator<SecureStorageService>();
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
localizationService() {
|
localizationService() {
|
||||||
listenToReactiveValues([_selectedLanguage]);
|
listenToReactiveValues([_selectedLanguage]);
|
||||||
|
|
@ -10,7 +16,7 @@ class LocalizationService with ListenableServiceMixin {
|
||||||
|
|
||||||
// Languages
|
// Languages
|
||||||
Map<String, dynamic> _selectedLanguage = {
|
Map<String, dynamic> _selectedLanguage = {
|
||||||
'code': 'EN',
|
'code': 'en',
|
||||||
'language': 'English'
|
'language': 'English'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18,7 +24,7 @@ class LocalizationService with ListenableServiceMixin {
|
||||||
|
|
||||||
final List<Map<String, dynamic>> _languages = [
|
final List<Map<String, dynamic>> _languages = [
|
||||||
{'code': 'አማ', 'language': 'አማርኛ'},
|
{'code': 'አማ', 'language': 'አማርኛ'},
|
||||||
{'code': 'EN', 'language': 'English'},
|
{'code': 'en', 'language': 'English'},
|
||||||
];
|
];
|
||||||
|
|
||||||
List<Map<String, dynamic>> get languages => _languages;
|
List<Map<String, dynamic>> get languages => _languages;
|
||||||
|
|
@ -34,14 +40,33 @@ class LocalizationService with ListenableServiceMixin {
|
||||||
if (title['code'] == 'አማ') {
|
if (title['code'] == 'አማ') {
|
||||||
await setAmharicLanguage(context);
|
await setAmharicLanguage(context);
|
||||||
} else {
|
} else {
|
||||||
await setAmharicLanguage(context);
|
await setEnglishLanguage(context);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setAmharicLanguage(BuildContext context) async =>
|
Future<void> loadSelectedLanguage() async {
|
||||||
await context.setLocale(const Locale('am'));
|
String language = await _secureService.getString('language') ?? 'en';
|
||||||
|
|
||||||
Future<void> setEnglishLanguage(BuildContext context) async =>
|
if (language == 'en') {
|
||||||
await context.setLocale(const Locale('en'));
|
_selectedLanguage = {'code': 'en', 'language': 'English'};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
_selectedLanguage = {'code': 'አማ', 'language': 'አማርኛ'};
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
print('SELECTED LANGUAGE: $language $_selectedLanguage');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setAmharicLanguage(BuildContext context) async {
|
||||||
|
await context.setLocale(const Locale('am'));
|
||||||
|
await _secureService.setString('language', 'am');
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEnglishLanguage(BuildContext context) async {
|
||||||
|
await context.setLocale(const Locale('en'));
|
||||||
|
await _secureService.setString('language', 'en');
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ enum StateObjects {
|
||||||
learnCourses,
|
learnCourses,
|
||||||
profileImage,
|
profileImage,
|
||||||
learnPrograms,
|
learnPrograms,
|
||||||
courseModules,
|
|
||||||
courseLessons,
|
courseLessons,
|
||||||
profileUpdate,
|
profileUpdate,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,7 @@ class CodegenLoader extends AssetLoader {
|
||||||
"confirm_password": "የይለፍ ቃል ያረጋግጡ",
|
"confirm_password": "የይለፍ ቃል ያረጋግጡ",
|
||||||
"eight_character_minimum": "ቢያንስ 8 ፊደላት",
|
"eight_character_minimum": "ቢያንስ 8 ፊደላት",
|
||||||
"password_match": "የይለፍ ቃሉ ተመሳስሏል",
|
"password_match": "የይለፍ ቃሉ ተመሳስሏል",
|
||||||
"sign_up_agreement":
|
"sign_up_agreement": "‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።",
|
||||||
"‘ይመዝገቡ’ የሚለውን ሲጫኑ በ‘አገልግሎት ውሎች’ እና ‘በግላዊነት ፖሊሲ’ ይስማማሉ።",
|
|
||||||
"terms_of_services": "የአገልግሎት ውሎች",
|
"terms_of_services": "የአገልግሎት ውሎች",
|
||||||
"and": "እና",
|
"and": "እና",
|
||||||
"privacy_policy": "የግላዊነት ፖሊሲ",
|
"privacy_policy": "የግላዊነት ፖሊሲ",
|
||||||
|
|
@ -57,6 +56,9 @@ class CodegenLoader extends AssetLoader {
|
||||||
"new_password": "አዲስ የይለፍ ቃል",
|
"new_password": "አዲስ የይለፍ ቃል",
|
||||||
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
|
"logged_in_successfully": "በተሳካ ሁኔታ ገብተዋል",
|
||||||
"view_course": " ኮርሱን ይመልከቱ",
|
"view_course": " ኮርሱን ይመልከቱ",
|
||||||
|
"continue_learning": "መማርን ይቀጥሉ",
|
||||||
|
"start_learning": "ትምህርትን ይጀምሩ",
|
||||||
|
"completed": "ተጠናቋል",
|
||||||
"take_practice": " ልምምድ ያድርጉ",
|
"take_practice": " ልምምድ ያድርጉ",
|
||||||
"your_current_level": "የአሁኑ ደረጃዎ",
|
"your_current_level": "የአሁኑ ደረጃዎ",
|
||||||
"overall_progress": "አጠቃላይ እድገት",
|
"overall_progress": "አጠቃላይ እድገት",
|
||||||
|
|
@ -107,8 +109,33 @@ class CodegenLoader extends AssetLoader {
|
||||||
"phone_number": "የስልክ ቁጥር",
|
"phone_number": "የስልክ ቁጥር",
|
||||||
"country": "ሀገር",
|
"country": "ሀገር",
|
||||||
"region": "ክልል",
|
"region": "ክልል",
|
||||||
|
"select_region": "ክልል ይምረጡ",
|
||||||
|
"enter_your_city": "ከተማዎን ያስገቡ",
|
||||||
"occupation": "የስራ መስክ",
|
"occupation": "የስራ መስክ",
|
||||||
"save_changes": "ለውጦችን ያስቀምጡ"
|
"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 = {
|
static const Map<String,dynamic> _en = {
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
|
|
@ -128,15 +155,13 @@ class CodegenLoader extends AssetLoader {
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"register_with_google": "Register with Google",
|
"register_with_google": "Register with Google",
|
||||||
"register_with_phone": "Register with phone number",
|
"register_with_phone": "Register with phone number",
|
||||||
"enter_phone_number":
|
"enter_phone_number": "Enter your phone number. We will send you a confirmation code there.",
|
||||||
"Enter your phone number. We will send you a confirmation code there.",
|
|
||||||
"login_with_email": "Login with email",
|
"login_with_email": "Login with email",
|
||||||
"create_password": "Create password",
|
"create_password": "Create password",
|
||||||
"confirm_password": "Confirm password",
|
"confirm_password": "Confirm password",
|
||||||
"eight_character_minimum": "8 characters minimum",
|
"eight_character_minimum": "8 characters minimum",
|
||||||
"password_math": "password match",
|
"password_math": "password match",
|
||||||
"sign_up_agreement":
|
"sign_up_agreement": "By clicking ‘Sign Up’, you agree to our ‘Terms of Service’ and ‘Privacy Policy’",
|
||||||
"By clicking ‘Sign Up’, you agree to our ‘Terms of Service’ and ‘Privacy Policy’",
|
|
||||||
"terms_of_services": "Terms of Service",
|
"terms_of_services": "Terms of Service",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"privacy_policy": "Privacy Policy",
|
"privacy_policy": "Privacy Policy",
|
||||||
|
|
@ -147,13 +172,15 @@ class CodegenLoader extends AssetLoader {
|
||||||
"code_sent_to_email": "Code sent to your email",
|
"code_sent_to_email": "Code sent to your email",
|
||||||
"resend_code_in": "Resend code in",
|
"resend_code_in": "Resend code in",
|
||||||
"reset_password": "Reset Password",
|
"reset_password": "Reset Password",
|
||||||
"enter_email_reset_code":
|
"enter_email_reset_code": "Enter your email. We will send you a reset code.",
|
||||||
"Enter your email. We will send you a reset code.",
|
|
||||||
"please_wait": "Please wait",
|
"please_wait": "Please wait",
|
||||||
"reset_code_sent": "Reset code sent successfully",
|
"reset_code_sent": "Reset code sent successfully",
|
||||||
"reset_code": "Reset code",
|
"reset_code": "Reset code",
|
||||||
"new_password": "New password",
|
"new_password": "New password",
|
||||||
"logged_in_successfully": "Logged in successfully",
|
"logged_in_successfully": "Logged in successfully",
|
||||||
|
"continue_learning": "Continue Learning",
|
||||||
|
"start_learning": "Start Learning",
|
||||||
|
"completed": "Completed",
|
||||||
"view_course": "View course",
|
"view_course": "View course",
|
||||||
"take_practice": "Take practice",
|
"take_practice": "Take practice",
|
||||||
"your_current_level": "Your current level",
|
"your_current_level": "Your current level",
|
||||||
|
|
@ -186,8 +213,7 @@ class CodegenLoader extends AssetLoader {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"you_are_speaking": "You're speaking",
|
"you_are_speaking": "You're speaking",
|
||||||
"practice_completed": "Practice completed",
|
"practice_completed": "Practice completed",
|
||||||
"great_improvement":
|
"great_improvement": "You sound more confident this time, great improvement",
|
||||||
"You sound more confident this time, great improvement",
|
|
||||||
"practice_again": "Practice again",
|
"practice_again": "Practice again",
|
||||||
"conversation_review": "Conversation review",
|
"conversation_review": "Conversation review",
|
||||||
"result": "Result",
|
"result": "Result",
|
||||||
|
|
@ -206,11 +232,33 @@ class CodegenLoader extends AssetLoader {
|
||||||
"phone_number": "Phone number",
|
"phone_number": "Phone number",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
|
"select_region": "Select region",
|
||||||
|
"enter_your_city": "Enter your city",
|
||||||
"occupation": "Occupation",
|
"occupation": "Occupation",
|
||||||
"save_changes": "Save changes"
|
"select_occupation": "Select occupation",
|
||||||
};
|
"save_changes": "Save changes",
|
||||||
static const Map<String, Map<String, dynamic>> mapLocales = {
|
"my_progress": "My progress",
|
||||||
"am": _am,
|
"track_your_achievement": "Track your achievements and learning streak",
|
||||||
"en": _en
|
"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};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,9 @@ abstract class LocaleKeys {
|
||||||
static const new_password = 'new_password';
|
static const new_password = 'new_password';
|
||||||
static const logged_in_successfully = 'logged_in_successfully';
|
static const logged_in_successfully = 'logged_in_successfully';
|
||||||
static const view_course = 'view_course';
|
static const view_course = 'view_course';
|
||||||
|
static const continue_learning = 'continue_learning';
|
||||||
|
static const start_learning = 'start_learning';
|
||||||
|
static const completed = 'completed';
|
||||||
static const take_practice = 'take_practice';
|
static const take_practice = 'take_practice';
|
||||||
static const your_current_level = 'your_current_level';
|
static const your_current_level = 'your_current_level';
|
||||||
static const overall_progress = 'overall_progress';
|
static const overall_progress = 'overall_progress';
|
||||||
|
|
@ -92,6 +95,32 @@ abstract class LocaleKeys {
|
||||||
static const phone_number = 'phone_number';
|
static const phone_number = 'phone_number';
|
||||||
static const country = 'country';
|
static const country = 'country';
|
||||||
static const region = 'region';
|
static const region = 'region';
|
||||||
|
static const select_region = 'select_region';
|
||||||
|
static const enter_your_city = 'enter_your_city';
|
||||||
static const occupation = 'occupation';
|
static const occupation = 'occupation';
|
||||||
|
static const select_occupation = 'select_occupation';
|
||||||
static const save_changes = 'save_changes';
|
static const save_changes = 'save_changes';
|
||||||
|
static const my_progress = 'my_progress';
|
||||||
|
static const track_your_achievement = 'track_your_achievement';
|
||||||
|
static const account_and_privacy = 'account_and_privacy';
|
||||||
|
static const manage_settings = 'manage_settings';
|
||||||
|
static const support = 'support';
|
||||||
|
static const get_help = 'get_help';
|
||||||
|
static const logout = 'logout';
|
||||||
|
static const app_settings = 'app_settings';
|
||||||
|
static const legal_and_information = 'legal_and_information';
|
||||||
|
static const change_language = 'change_language';
|
||||||
|
static const terms_and_conditions = 'terms_and_conditions';
|
||||||
|
static const delete_account = 'delete_account';
|
||||||
|
static const language_preference = 'language_preference';
|
||||||
|
static const choose_your_language = 'choose_your_language';
|
||||||
|
static const switch_language_anytime = 'switch_language_anytime';
|
||||||
|
static const need_help = 'need_help';
|
||||||
|
static const call_support = 'call_support';
|
||||||
|
static const talk_with_support = 'talk_with_support';
|
||||||
|
static const telegram_support = 'telegram_support';
|
||||||
|
static const chat_via_telegram = 'chat_via_telegram';
|
||||||
|
static const call_our_support = 'call_our_support';
|
||||||
|
static const tap_to_call = 'tap_to_call';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,12 +197,8 @@ TextStyle style18W600 = const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
TextStyle style25W400 = const TextStyle(
|
TextStyle style25W400 =
|
||||||
fontSize: 25,
|
const TextStyle(fontSize: 25, color: kcWhite, fontWeight: FontWeight.w400);
|
||||||
color: kcWhite,
|
|
||||||
fontWeight: FontWeight.w400
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
TextStyle style25W600 = const TextStyle(
|
TextStyle style25W600 = const TextStyle(
|
||||||
fontSize: 25,
|
fontSize: 25,
|
||||||
|
|
@ -275,6 +271,12 @@ TextStyle style16P600 = const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style16P900 = const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: kcPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style16DG500 = const TextStyle(
|
TextStyle style16DG500 = const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/custom_list_tile.dart';
|
import 'package:yimaru_app/ui/widgets/custom_list_tile.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
|
@ -57,7 +59,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar(
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
title: 'Account Privacy',
|
title: LocaleKeys.account_and_privacy.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) =>
|
Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) =>
|
||||||
|
|
@ -92,12 +94,12 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
|
|
||||||
List<Widget> _buildMenuColumnChildren(AccountPrivacyViewModel viewModel) => [
|
List<Widget> _buildMenuColumnChildren(AccountPrivacyViewModel viewModel) => [
|
||||||
verticalSpaceLarge,
|
verticalSpaceLarge,
|
||||||
_buildHeader('App Settings'),
|
_buildHeader(LocaleKeys.app_settings.tr()),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildLanguageMenu(viewModel),
|
_buildLanguageMenu(viewModel),
|
||||||
_buildDividerWrapper(),
|
_buildDividerWrapper(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildHeader('Legal & Information'),
|
_buildHeader(LocaleKeys.legal_and_information.tr()),
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildTermsAndConditionsMenu(viewModel),
|
_buildTermsAndConditionsMenu(viewModel),
|
||||||
_buildPrivacyPolicy(viewModel),
|
_buildPrivacyPolicy(viewModel),
|
||||||
|
|
@ -112,23 +114,23 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
|
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
|
||||||
CustomListTile(
|
CustomListTile(
|
||||||
isLanguage: true,
|
isLanguage: true,
|
||||||
language: 'English',
|
|
||||||
icon: Icons.language,
|
icon: Icons.language,
|
||||||
title: 'Change Language',
|
title: LocaleKeys.change_language.tr(),
|
||||||
|
language: viewModel.selectedLanguage['language'],
|
||||||
onTap: () async => await viewModel.navigateToLanguage(),
|
onTap: () async => await viewModel.navigateToLanguage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTermsAndConditionsMenu(AccountPrivacyViewModel viewModel) =>
|
Widget _buildTermsAndConditionsMenu(AccountPrivacyViewModel viewModel) =>
|
||||||
CustomListTile(
|
CustomListTile(
|
||||||
icon: Icons.handshake,
|
icon: Icons.handshake,
|
||||||
title: 'Terms & Conditions',
|
title: LocaleKeys.terms_and_conditions.tr(),
|
||||||
onTap: () async => await viewModel.navigateToTerms(),
|
onTap: () async => await viewModel.navigateToTerms(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) =>
|
Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) =>
|
||||||
CustomListTile(
|
CustomListTile(
|
||||||
title: 'Privacy Policy',
|
|
||||||
icon: Icons.shield_moon_outlined,
|
icon: Icons.shield_moon_outlined,
|
||||||
|
title: LocaleKeys.privacy_policy.tr(),
|
||||||
onTap: () async => await viewModel.navigateToPrivacyPolicy(),
|
onTap: () async => await viewModel.navigateToPrivacyPolicy(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -146,8 +148,8 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
|
||||||
Widget _buildDeleteButton() => CustomElevatedButton(
|
Widget _buildDeleteButton() => CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
text: 'Delete Account',
|
|
||||||
foregroundColor: kcRed,
|
foregroundColor: kcRed,
|
||||||
|
text: LocaleKeys.delete_account.tr(),
|
||||||
backgroundColor: kcRed.withOpacity(0.25),
|
backgroundColor: kcRed.withOpacity(0.25),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,24 @@ import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/app/app.router.dart';
|
import 'package:yimaru_app/app/app.router.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
import '../../../services/localization_service.dart';
|
||||||
|
|
||||||
class AccountPrivacyViewModel extends BaseViewModel {
|
class AccountPrivacyViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
|
final _localizationService = locator<LocalizationService>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
|
[ _localizationService];
|
||||||
|
|
||||||
|
// Languages
|
||||||
|
Map<String, dynamic> get _selectedLanguage =>
|
||||||
|
_localizationService.selectedLanguage;
|
||||||
|
|
||||||
|
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
|
|
@ -51,7 +53,7 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
|
||||||
Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar(
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
title: 'Call Support',
|
title: LocaleKeys.call_support.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildExpandedColumn(CallSupportViewModel viewModel) =>
|
Widget _buildExpandedColumn(CallSupportViewModel viewModel) =>
|
||||||
|
|
@ -91,7 +93,7 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
|
||||||
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
|
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
Widget _buildTitle() => Text(
|
||||||
'Call our support team between 9 AM - 6 PM',
|
LocaleKeys.call_our_support.tr(),
|
||||||
style: style25DG600,
|
style: style25DG600,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
);
|
);
|
||||||
|
|
@ -111,10 +113,10 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
text: 'Tap to Call',
|
|
||||||
leadingIcon: Icons.call,
|
leadingIcon: Icons.call,
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
|
text:LocaleKeys.tap_to_call.tr(),
|
||||||
onTap: () async => await viewModel.callSupport(),
|
onTap: () async => await viewModel.callSupport(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,12 @@ class CourseView extends StackedView<CourseViewModel> {
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(CourseViewModel viewModel) => Scaffold(
|
Widget _buildScaffoldWrapper(CourseViewModel viewModel) => Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
body: _buildScaffold(viewModel),
|
body: _buildScaffoldContainer(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffoldContainer(CourseViewModel viewModel) => Container(
|
||||||
|
decoration: bgDecoration,
|
||||||
|
child: _buildScaffold(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(CourseViewModel viewModel) =>
|
Widget _buildScaffold(CourseViewModel viewModel) =>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,12 @@ class CourseCatalogView extends StackedView<CourseCatalogViewModel> {
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(CourseCatalogViewModel viewModel) => Scaffold(
|
Widget _buildScaffoldWrapper(CourseCatalogViewModel viewModel) => Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
body: _buildScaffold(viewModel),
|
body: _buildScaffoldContainer(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffoldContainer(CourseCatalogViewModel viewModel) => Container(
|
||||||
|
decoration: bgDecoration,
|
||||||
|
child: _buildScaffold(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(CourseCatalogViewModel viewModel) =>
|
Widget _buildScaffold(CourseCatalogViewModel viewModel) =>
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,6 @@ class CourseCatalogViewModel extends ReactiveViewModel {
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToCoursePractice(int id) async =>
|
|
||||||
_navigationService.navigateToCoursePracticeView(id: id);
|
|
||||||
|
|
||||||
Future<void> navigateToCourseUnit(CourseCatalog catalog) async =>
|
Future<void> navigateToCourseUnit(CourseCatalog catalog) async =>
|
||||||
await _navigationService.navigateToCourseUnitView(catalog: catalog);
|
await _navigationService.navigateToCourseUnitView(catalog: catalog);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +1,73 @@
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:yimaru_app/models/course_lesson.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_viewmodel.dart';
|
||||||
|
|
||||||
import '../../../models/course_lesson.dart';
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
import '../../widgets/custom_elevated_button.dart';
|
import '../../widgets/custom_elevated_button.dart';
|
||||||
import '../../widgets/empty_video_player.dart';
|
import '../../widgets/empty_video_player.dart';
|
||||||
import '../../widgets/small_app_bar.dart';
|
import '../../widgets/small_app_bar.dart';
|
||||||
import 'course_lesson_detail_viewmodel.dart';
|
|
||||||
|
|
||||||
class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
|
class CourseLessonDetailView extends StackedView<CourseLessonDetailViewmodel> {
|
||||||
final CourseLesson lesson;
|
final CourseLesson lesson;
|
||||||
|
|
||||||
const CourseLessonDetailView({Key? key, required this.lesson})
|
const CourseLessonDetailView({
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
required this.lesson,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
Future<void> _navigate(CourseLessonDetailViewmodel viewModel) async {
|
||||||
|
await viewModel.pause();
|
||||||
|
// await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewModelReady(CourseLessonDetailViewModel viewModel) async {
|
void onViewModelReady(CourseLessonDetailViewmodel viewModel) async {
|
||||||
await viewModel.initializePlayer(lesson);
|
await viewModel.initializePlayer(
|
||||||
|
lessonId: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
url: lesson.videoUrl ?? '',
|
||||||
|
);
|
||||||
super.onViewModelReady(viewModel);
|
super.onViewModelReady(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CourseLessonDetailViewModel viewModelBuilder(BuildContext context) =>
|
void onDispose(CourseLessonDetailViewmodel viewModel) {
|
||||||
CourseLessonDetailViewModel();
|
viewModel.close();
|
||||||
|
super.onDispose(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
CourseLessonDetailViewmodel viewModelBuilder(BuildContext context) =>
|
||||||
|
CourseLessonDetailViewmodel();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget builder(
|
Widget builder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
CourseLessonDetailViewModel viewModel,
|
CourseLessonDetailViewmodel viewModel,
|
||||||
Widget? child,
|
Widget? child,
|
||||||
) =>
|
) =>
|
||||||
_buildScaffoldWrapper(viewModel);
|
_buildScaffoldWrapper(viewModel);
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildScaffoldWrapper(CourseLessonDetailViewmodel viewModel) =>
|
||||||
Scaffold(
|
Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
body: _buildScaffold(viewModel),
|
body: _buildScaffoldContainer(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildScaffoldContainer(CourseLessonDetailViewmodel viewModel) =>
|
||||||
|
Container(
|
||||||
|
decoration: bgDecoration,
|
||||||
|
child: _buildScaffold(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffold(CourseLessonDetailViewmodel viewModel) =>
|
||||||
SafeArea(child: _buildColumn(viewModel));
|
SafeArea(child: _buildColumn(viewModel));
|
||||||
|
|
||||||
Widget _buildColumn(CourseLessonDetailViewModel viewModel) => Column(
|
Widget _buildColumn(CourseLessonDetailViewmodel viewModel) => Column(
|
||||||
children: [
|
children: [
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildAppBarWrapper(viewModel),
|
_buildAppBarWrapper(viewModel),
|
||||||
|
|
@ -52,57 +75,58 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppBarWrapper(CourseLessonDetailViewModel viewModel) => Padding(
|
Widget _buildAppBarWrapper(CourseLessonDetailViewmodel viewModel) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
child: _buildAppBar(viewModel));
|
child: _buildAppBar(viewModel));
|
||||||
|
|
||||||
Widget _buildAppBar(CourseLessonDetailViewModel viewModel) => SmallAppBar(
|
Widget _buildAppBar(CourseLessonDetailViewmodel viewModel) => SmallAppBar(
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
title: lesson.title ?? '',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildBodyColumnWrapper(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildBodyColumnWrapper(CourseLessonDetailViewmodel viewModel) =>
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildBodyColumn(viewModel),
|
child: _buildBodyColumn(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildBodyColumn(CourseLessonDetailViewModel viewModel) => Column(
|
Widget _buildBodyColumn(CourseLessonDetailViewmodel viewModel) => Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: _buildBodyColumnChildren(viewModel),
|
children: _buildBodyColumnChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildBodyColumnChildren(
|
List<Widget> _buildBodyColumnChildren(
|
||||||
CourseLessonDetailViewModel viewModel) =>
|
CourseLessonDetailViewmodel viewModel) =>
|
||||||
[
|
[
|
||||||
_buildLevelsColumnWrapper(viewModel),
|
_buildLevelsColumnWrapper(viewModel),
|
||||||
// _buildContinueButtonWrapper(viewModel)
|
if (lesson.hasPractice ?? false) _buildPracticeButtonWrapper(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildLevelsColumnWrapper(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildLevelsColumnWrapper(CourseLessonDetailViewmodel viewModel) =>
|
||||||
Expanded(child: _buildLevelsColumnScrollView(viewModel));
|
Expanded(child: _buildLevelsColumnScrollView(viewModel));
|
||||||
|
|
||||||
Widget _buildLevelsColumnScrollView(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildLevelsColumnScrollView(CourseLessonDetailViewmodel viewModel) =>
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: _buildLevelsColumn(viewModel),
|
child: _buildLevelsColumn(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLevelsColumn(CourseLessonDetailViewModel viewModel) => Column(
|
Widget _buildLevelsColumn(CourseLessonDetailViewmodel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _buildLevelsColumnChildren(viewModel),
|
children: _buildLevelsColumnChildren(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildLevelsColumnChildren(
|
List<Widget> _buildLevelsColumnChildren(
|
||||||
CourseLessonDetailViewModel viewModel) =>
|
CourseLessonDetailViewmodel viewModel) =>
|
||||||
[
|
[
|
||||||
verticalSpaceLarge,
|
verticalSpaceMedium,
|
||||||
_buildVideoPlayerWrapper(viewModel),
|
_buildVideoPlayerWrapper(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
|
_buildTitleWrapper(),
|
||||||
|
verticalSpaceMedium,
|
||||||
_buildDescriptionWrapper(),
|
_buildDescriptionWrapper(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildVideoPlayerWrapper(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildVideoPlayerWrapper(CourseLessonDetailViewmodel viewModel) =>
|
||||||
Container(
|
Container(
|
||||||
height: 200,
|
height: 200,
|
||||||
color: kcBlack,
|
color: kcBlack,
|
||||||
|
|
@ -110,21 +134,32 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
|
||||||
child: _buildVideoPlayerState(viewModel),
|
child: _buildVideoPlayerState(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildVideoPlayerState(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildVideoPlayerState(CourseLessonDetailViewmodel viewModel) =>
|
||||||
viewModel.chewieController != null &&
|
viewModel.chewieController != null &&
|
||||||
viewModel.videoPlayerController != null &&
|
viewModel.videoPlayerController != null &&
|
||||||
!viewModel.busy(StateObjects.loadCourseVideo)
|
!viewModel.busy(StateObjects.loadLessonVideo)
|
||||||
? _buildVideoPlayer(viewModel)
|
? _buildVideoPlayer(viewModel)
|
||||||
: _buildEmptyVideoPlayer();
|
: _buildEmptyVideoPlayer();
|
||||||
|
|
||||||
Widget _buildVideoPlayer(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildVideoPlayer(CourseLessonDetailViewmodel viewModel) =>
|
||||||
_buildChewiePlayer(viewModel);
|
_buildChewiePlayer(viewModel);
|
||||||
|
|
||||||
Widget _buildChewiePlayer(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildChewiePlayer(CourseLessonDetailViewmodel viewModel) => Chewie(
|
||||||
Chewie(controller: viewModel.chewieController!);
|
controller: viewModel.chewieController!,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
|
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
|
||||||
|
|
||||||
|
Widget _buildTitleWrapper() => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: _buildTitle(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildTitle() => Text(
|
||||||
|
lesson.title ?? '',
|
||||||
|
style: style16DG600,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildDescriptionWrapper() => Padding(
|
Widget _buildDescriptionWrapper() => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
child: _buildDescription(),
|
child: _buildDescription(),
|
||||||
|
|
@ -132,10 +167,10 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
|
||||||
|
|
||||||
Widget _buildDescription() => Text(
|
Widget _buildDescription() => Text(
|
||||||
lesson.description ?? '',
|
lesson.description ?? '',
|
||||||
style: style14DG600,
|
style: style14DG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPracticeButtonWrapper(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildPracticeButtonWrapper(CourseLessonDetailViewmodel viewModel) =>
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 15,
|
left: 15,
|
||||||
|
|
@ -145,14 +180,13 @@ class CourseLessonDetailView extends StackedView<CourseLessonDetailViewModel> {
|
||||||
child: _buildPracticeButton(viewModel),
|
child: _buildPracticeButton(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPracticeButton(CourseLessonDetailViewModel viewModel) =>
|
Widget _buildPracticeButton(CourseLessonDetailViewmodel viewModel) =>
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Practice',
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
text: 'Start Assessment',
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
onTap: () async =>
|
onTap: () async => await _navigate(viewModel),
|
||||||
await viewModel.navigateToCoursePractice(lesson.courseId ?? 0),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,27 @@ import 'package:chewie/chewie.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
import 'package:yimaru_app/app/app.router.dart';
|
|
||||||
import 'package:yimaru_app/models/course_lesson.dart';
|
|
||||||
import 'package:yimaru_app/services/api_service.dart';
|
import 'package:yimaru_app/services/api_service.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../common/app_constants.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
|
import '../../../services/vimeo_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
|
|
||||||
class CourseLessonDetailViewModel extends BaseViewModel {
|
class CourseLessonDetailViewmodel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
|
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
final _vimeoService = locator<VimeoService>();
|
||||||
|
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
// Video player config
|
// Video player config
|
||||||
bool _isCompleted = false;
|
bool _lessonCompleted = false;
|
||||||
|
|
||||||
ChewieController? _chewieController;
|
ChewieController? _chewieController;
|
||||||
|
|
||||||
|
|
@ -29,55 +33,65 @@ class CourseLessonDetailViewModel extends BaseViewModel {
|
||||||
VideoPlayerController? get videoPlayerController => _videoPlayerController;
|
VideoPlayerController? get videoPlayerController => _videoPlayerController;
|
||||||
|
|
||||||
// Video player
|
// Video player
|
||||||
|
void close() {
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_videoPlayerController?.dispose();
|
_videoPlayerController?.dispose();
|
||||||
_chewieController?.dispose();
|
_chewieController?.dispose();
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pause() async {
|
Future<void> pause() async {
|
||||||
await _chewieController?.pause();
|
await _chewieController?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _videoListener(CourseLesson lesson) async {
|
Future<void> initializePlayer(
|
||||||
final controller = _videoPlayerController;
|
{required String url,
|
||||||
|
required int lessonId,
|
||||||
|
required int moduleId}) async =>
|
||||||
|
await runBusyFuture(
|
||||||
|
_initializePlayer(url: url, moduleId: moduleId, lessonId: lessonId),
|
||||||
|
busyObject: StateObjects.loadLessonVideo);
|
||||||
|
|
||||||
if (controller == null || !controller.value.isInitialized) return;
|
Future<void> _initializePlayer(
|
||||||
|
{required String url,
|
||||||
|
required int lessonId,
|
||||||
|
required int moduleId}) async {
|
||||||
|
final playableUrl = await _vimeoService.getVideoUrl(url);
|
||||||
|
|
||||||
final position = controller.value.position;
|
if (playableUrl == null) {
|
||||||
final duration = controller.value.duration;
|
throw Exception("Unable to load video");
|
||||||
|
|
||||||
if (duration.inSeconds == 0) return;
|
|
||||||
|
|
||||||
double progress = position.inSeconds / duration.inSeconds;
|
|
||||||
|
|
||||||
print("Video progress: ${(progress * 100).toStringAsFixed(2)}%");
|
|
||||||
|
|
||||||
// Example: mark completion at 95%
|
|
||||||
if (progress >= 0.95) {
|
|
||||||
await _onVideoCompleted(lesson);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initializePlayer(CourseLesson lesson) async =>
|
|
||||||
await runBusyFuture(_initializePlayer(lesson),
|
|
||||||
busyObject: StateObjects.loadCourseVideo);
|
|
||||||
|
|
||||||
Future<void> _initializePlayer(CourseLesson lesson) async {
|
|
||||||
print('URL: $kSampleVideoUrl');
|
|
||||||
_videoPlayerController =
|
_videoPlayerController =
|
||||||
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl));
|
VideoPlayerController.networkUrl(Uri.parse(playableUrl));
|
||||||
|
|
||||||
await _videoPlayerController?.initialize();
|
await _videoPlayerController?.initialize();
|
||||||
|
|
||||||
_videoPlayerController
|
// Listen for video completion
|
||||||
?.addListener(() async => await _videoListener(lesson));
|
_videoPlayerController?.addListener(() async {
|
||||||
|
final controller = _videoPlayerController;
|
||||||
|
|
||||||
|
if (controller == null || _lessonCompleted) return;
|
||||||
|
|
||||||
|
final position = controller.value.position.inSeconds;
|
||||||
|
final duration = controller.value.duration.inSeconds;
|
||||||
|
|
||||||
|
if (duration <= 0) return;
|
||||||
|
|
||||||
|
// Calculate watched percentage
|
||||||
|
final progress = position / duration;
|
||||||
|
|
||||||
|
// Mark complete at 95%
|
||||||
|
if (progress >= 0.95) {
|
||||||
|
_lessonCompleted = true;
|
||||||
|
|
||||||
|
/* await completeLearnLesson(
|
||||||
|
lessonId: lessonId,
|
||||||
|
moduleId: moduleId,
|
||||||
|
);*/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (_videoPlayerController != null) {
|
|
||||||
_chewieController = ChewieController(
|
_chewieController = ChewieController(
|
||||||
looping: true,
|
looping: false,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
showOptions: true,
|
showOptions: true,
|
||||||
showControls: true,
|
showControls: true,
|
||||||
|
|
@ -87,24 +101,10 @@ class CourseLessonDetailViewModel extends BaseViewModel {
|
||||||
videoPlayerController: _videoPlayerController!,
|
videoPlayerController: _videoPlayerController!,
|
||||||
materialProgressColors: buildChewieProgressIndicator,
|
materialProgressColors: buildChewieProgressIndicator,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
rebuildUi();
|
notifyListeners();
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onVideoCompleted(CourseLesson lesson) async {
|
|
||||||
if (_isCompleted) return;
|
|
||||||
|
|
||||||
_isCompleted = true;
|
|
||||||
|
|
||||||
print("Video completed!");
|
|
||||||
|
|
||||||
await _apiService.completeLesson(lesson.id ?? 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToCoursePractice(int id) =>
|
|
||||||
_navigationService.navigateToCoursePracticeView(id: id);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
113
lib/ui/views/course_module/course_module_view.dart
Normal 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: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,45 +1,45 @@
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/app/app.router.dart';
|
import 'package:yimaru_app/app/app.router.dart';
|
||||||
|
import 'package:yimaru_app/models/course_lesson.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../models/course_lesson.dart';
|
import '../../../services/course_service.dart';
|
||||||
import '../../../services/api_service.dart';
|
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
class CourseLessonViewModel extends BaseViewModel {
|
class CourseModuleViewModel extends ReactiveViewModel {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
final _apiService = locator<ApiService>();
|
final _courseService = locator<CourseService>();
|
||||||
|
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
// Course lessons
|
@override
|
||||||
List<CourseLesson> _courseLessons = [];
|
List<ListenableServiceMixin> get listenableServices => [_courseService];
|
||||||
|
|
||||||
List<CourseLesson> get courseLessons => _courseLessons;
|
// Course lessons
|
||||||
|
List<CourseLesson> get _lessons => _courseService.lessons;
|
||||||
|
|
||||||
|
List<CourseLesson> get lessons => _lessons;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToCoursePractice(int id) =>
|
|
||||||
_navigationService.navigateToCoursePracticeView(id: id);
|
|
||||||
|
|
||||||
Future<void> navigateToCourseLessonDetail(CourseLesson lesson) async =>
|
Future<void> navigateToCourseLessonDetail(CourseLesson lesson) async =>
|
||||||
await _navigationService.navigateToCourseLessonDetailView(lesson: lesson);
|
await _navigationService.navigateToCourseLessonDetailView(lesson: lesson);
|
||||||
|
|
||||||
// Remote api call
|
// Remote api call
|
||||||
|
|
||||||
// Course lessons
|
// Course modules
|
||||||
Future<void> getCourseLessons(int courseId) async =>
|
Future<void> getCourseLessons(int id) async =>
|
||||||
await runBusyFuture(_getCourseLessons(courseId),
|
await runBusyFuture(_getCourseLessons(id),
|
||||||
busyObject: StateObjects.courseLessons);
|
busyObject: StateObjects.courseLessons);
|
||||||
|
|
||||||
Future<void> _getCourseLessons(int courseId) async {
|
Future<void> _getCourseLessons(int id) async {
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
_courseLessons = await _apiService.getCourseLessons(1);
|
await _courseService.getCourseLessons(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +127,7 @@ class CoursePaymentView extends StackedView<CoursePaymentViewModel> {
|
||||||
text: 'Subscribe Now',
|
text: 'Subscribe Now',
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
onTap: () async => await viewModel.navigateToCourseLesson(course),
|
onTap: () {},
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSecurePaymentWrapper() => Align(
|
Widget _buildSecurePaymentWrapper() => Align(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
import 'package:yimaru_app/app/app.router.dart';
|
|
||||||
import 'package:yimaru_app/models/course.dart';
|
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
|
||||||
|
|
@ -11,7 +9,4 @@ class CoursePaymentViewModel extends BaseViewModel {
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToCourseLesson(Course course) async =>
|
|
||||||
await _navigationService.navigateToCourseLessonView(course: course);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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),
|
|
||||||
});
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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(
|
|
||||||
'You’ve 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -39,7 +39,12 @@ class CourseUnitView extends StackedView<CourseUnitViewModel> {
|
||||||
|
|
||||||
Widget _buildScaffoldWrapper(CourseUnitViewModel viewModel) => Scaffold(
|
Widget _buildScaffoldWrapper(CourseUnitViewModel viewModel) => Scaffold(
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
body: _buildScaffold(viewModel),
|
body: _buildScaffoldContainer(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildScaffoldContainer(CourseUnitViewModel viewModel) => Container(
|
||||||
|
decoration: bgDecoration,
|
||||||
|
child: _buildScaffold(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(CourseUnitViewModel viewModel) =>
|
Widget _buildScaffold(CourseUnitViewModel viewModel) =>
|
||||||
|
|
@ -94,7 +99,7 @@ class CourseUnitView extends StackedView<CourseUnitViewModel> {
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
Widget _buildTitle() => Text(
|
||||||
catalog.name ?? '',
|
catalog.name ?? '',
|
||||||
style: style18P600,
|
style: style25DG600,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCourseModuleBanner() => const CourseModuleBanner();
|
Widget _buildCourseModuleBanner() => const CourseModuleBanner();
|
||||||
|
|
@ -130,19 +135,20 @@ class CourseUnitView extends StackedView<CourseUnitViewModel> {
|
||||||
index: index,
|
index: index,
|
||||||
unit: viewModel.units[index],
|
unit: viewModel.units[index],
|
||||||
onPracticeTap: () {},
|
onPracticeTap: () {},
|
||||||
onLessonTap: () {}),
|
onViewTap: () {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTile({
|
Widget _buildTile({
|
||||||
required int index,
|
required int index,
|
||||||
required CourseUnit unit,
|
required CourseUnit unit,
|
||||||
required GestureTapCallback onLessonTap,
|
required GestureTapCallback onViewTap,
|
||||||
required GestureTapCallback onPracticeTap,
|
required GestureTapCallback onPracticeTap,
|
||||||
}) =>
|
}) =>
|
||||||
CourseUnitTile(
|
CourseUnitTile(
|
||||||
unit: unit,
|
unit: unit,
|
||||||
index: index,
|
index: index,
|
||||||
onLessonTap: onLessonTap,
|
catalog: catalog,
|
||||||
|
onLessonTap: onViewTap,
|
||||||
onPracticeTap: onPracticeTap,
|
onPracticeTap: onPracticeTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked_services/stacked_services.dart';
|
import 'package:stacked_services/stacked_services.dart';
|
||||||
|
import 'package:yimaru_app/app/app.router.dart';
|
||||||
|
import 'package:yimaru_app/models/course_catalog.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
import '../../../models/course_module.dart';
|
||||||
import '../../../models/course_unit.dart';
|
import '../../../models/course_unit.dart';
|
||||||
import '../../../services/course_service.dart';
|
import '../../../services/course_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
|
|
@ -26,6 +29,12 @@ class CourseUnitViewModel extends ReactiveViewModel {
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
|
Future<void> navigateToCourseModule(
|
||||||
|
{required CourseModule? module,
|
||||||
|
required CourseCatalog catalog}) async =>
|
||||||
|
await _navigationService.navigateToCourseModuleView(
|
||||||
|
module: module, catalog: catalog);
|
||||||
|
|
||||||
// Remote api call
|
// Remote api call
|
||||||
|
|
||||||
// Course units
|
// Course units
|
||||||
|
|
@ -39,15 +48,13 @@ class CourseUnitViewModel extends ReactiveViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getCourseUnitModules(
|
Future<void> getCourseModules({required int id, required int index}) async =>
|
||||||
{required int id, required int index}) async =>
|
await runBusyFuture(_getCourseModules(id: id, index: index),
|
||||||
await runBusyFuture(_getCourseUnitModules(id: id, index: index),
|
busyObject: index);
|
||||||
busyObject: StateObjects.courseModules);
|
|
||||||
|
|
||||||
Future<void> _getCourseUnitModules(
|
Future<void> _getCourseModules({required int id, required int index}) async {
|
||||||
{required int id, required int index}) async {
|
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
await _courseService.getCourseUnitModule(id: id, index: index);
|
await _courseService.getCourseModules(id: id, index: index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class FailureView extends StackedView<FailureViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildBackground() => Image.asset(
|
Widget _buildBackground() => Image.asset(
|
||||||
'assets/images/onboarding_1.png',
|
'assets/images/loading.png',
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
width: double.maxFinite,
|
width: double.maxFinite,
|
||||||
height: double.maxFinite,
|
height: double.maxFinite,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
|
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
|
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
|
||||||
|
|
||||||
|
|
@ -44,18 +46,18 @@ class HomeView extends StackedView<HomeViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
BottomNavigationBarItem _buildLearnItem() => BottomNavigationBarItem(
|
BottomNavigationBarItem _buildLearnItem() => BottomNavigationBarItem(
|
||||||
label: 'Learn',
|
|
||||||
icon: _buildLearnIcon(),
|
icon: _buildLearnIcon(),
|
||||||
|
label: LocaleKeys.learn.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
BottomNavigationBarItem _buildCourseItem() => BottomNavigationBarItem(
|
BottomNavigationBarItem _buildCourseItem() => BottomNavigationBarItem(
|
||||||
label: 'Course',
|
|
||||||
icon: _buildCourseIcon(),
|
icon: _buildCourseIcon(),
|
||||||
|
label: LocaleKeys.course.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
BottomNavigationBarItem _buildProfileItem() => BottomNavigationBarItem(
|
BottomNavigationBarItem _buildProfileItem() => BottomNavigationBarItem(
|
||||||
label: 'Profile',
|
|
||||||
icon: _buildProfileIcon(),
|
icon: _buildProfileIcon(),
|
||||||
|
label: LocaleKeys.profile.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLearnIcon() => const Icon(Icons.school);
|
Widget _buildLearnIcon() => const Icon(Icons.school);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
|
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/views/landing/screens/first_landing_screen.dart';
|
import 'package:yimaru_app/ui/views/landing/screens/first_landing_screen.dart';
|
||||||
|
import 'package:yimaru_app/ui/views/landing/screens/fourth_landing_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/landing/screens/second_landing_screen.dart';
|
import 'package:yimaru_app/ui/views/landing/screens/second_landing_screen.dart';
|
||||||
import 'package:yimaru_app/ui/views/landing/screens/third_landing_screen.dart';
|
import 'package:yimaru_app/ui/views/landing/screens/third_landing_screen.dart';
|
||||||
|
|
||||||
|
|
@ -10,7 +11,6 @@ import 'landing_viewmodel.dart';
|
||||||
class LandingView extends StackedView<LandingViewModel> {
|
class LandingView extends StackedView<LandingViewModel> {
|
||||||
const LandingView({Key? key}) : super(key: key);
|
const LandingView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
LandingViewModel viewModelBuilder(
|
LandingViewModel viewModelBuilder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
|
@ -22,7 +22,8 @@ class LandingView extends StackedView<LandingViewModel> {
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
LandingViewModel viewModel,
|
LandingViewModel viewModel,
|
||||||
Widget? child,
|
Widget? child,
|
||||||
)=> _buildLandingScreens(viewModel);
|
) =>
|
||||||
|
_buildLandingScreens(viewModel);
|
||||||
|
|
||||||
Widget _buildLandingScreens(LandingViewModel viewModel) => FlutterCarousel(
|
Widget _buildLandingScreens(LandingViewModel viewModel) => FlutterCarousel(
|
||||||
options: FlutterCarouselOptions(
|
options: FlutterCarouselOptions(
|
||||||
|
|
@ -39,8 +40,12 @@ class LandingView extends StackedView<LandingViewModel> {
|
||||||
items: _buildScreens(),
|
items: _buildScreens(),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildScreens() =>
|
List<Widget> _buildScreens() => [
|
||||||
[_buildFirstWelcome(), _buildSecondWelcome(), _buildThirdWelcome()];
|
_buildFirstWelcome(),
|
||||||
|
_buildSecondWelcome(),
|
||||||
|
_buildThirdWelcome(),
|
||||||
|
_buildFourthWelcome()
|
||||||
|
];
|
||||||
|
|
||||||
Widget _buildFirstWelcome() => const FirstLandingScreen();
|
Widget _buildFirstWelcome() => const FirstLandingScreen();
|
||||||
|
|
||||||
|
|
@ -48,5 +53,5 @@ class LandingView extends StackedView<LandingViewModel> {
|
||||||
|
|
||||||
Widget _buildThirdWelcome() => const ThirdLandingScreen();
|
Widget _buildThirdWelcome() => const ThirdLandingScreen();
|
||||||
|
|
||||||
|
Widget _buildFourthWelcome() => const FourthLandingScreen();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
|
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
child: _buildScaffold(viewModel),);
|
child: _buildScaffold(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(LandingViewModel viewModel) => Column(
|
Widget _buildScaffold(LandingViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -33,31 +34,25 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
|
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
|
||||||
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
|
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildUpperColumn() => Column(
|
Widget _buildUpperColumn() => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: _buildUpperColumnChildren(),
|
children: _buildUpperColumnChildren(),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren() => [
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
verticalSpaceLarge,
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
_buildIconWrapper(),
|
|
||||||
verticalSpaceLarge
|
|
||||||
|
|
||||||
];
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
Widget _buildIconWrapper()=> Align(alignment: Alignment.topLeft,child: _buildIcon(),);
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
'assets/icons/logo.svg',
|
'assets/icons/logo.svg',
|
||||||
height: 25,
|
height: 25,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
|
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
|
||||||
child: _buildLowerColumn(viewModel),
|
child: _buildLowerColumn(viewModel),
|
||||||
);
|
);
|
||||||
|
|
@ -76,9 +71,7 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
_buildSafeWrapper(viewModel)
|
_buildSafeWrapper(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() =>
|
Widget _buildTitle() => Text.rich(
|
||||||
|
|
||||||
Text.rich(
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'እንግሊዝኛ\n',
|
text: 'እንግሊዝኛ\n',
|
||||||
style: style25W600,
|
style: style25W600,
|
||||||
|
|
@ -90,13 +83,10 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' ሰዓት ',
|
text: ' ሰዓት ',
|
||||||
style: style25W600,
|
style: style25W600,
|
||||||
|
|
||||||
),
|
),
|
||||||
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'ይማሩ!',
|
text: 'ይማሩ!',
|
||||||
style: style25W400,
|
style: style25W400,
|
||||||
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -109,10 +99,10 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
child: _buildImage(),
|
child: _buildImage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_1.jpg',
|
||||||
Widget _buildImage()=> Image.asset('assets/images/profile.png',fit: BoxFit.cover,);
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
|
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
|
||||||
SafeArea(child: _buildContinueButtonWrapper(viewModel));
|
SafeArea(child: _buildContinueButtonWrapper(viewModel));
|
||||||
|
|
|
||||||
136
lib/ui/views/landing/screens/fourth_landing_screen.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,8 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
|
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
child: _buildScaffold(viewModel),);
|
child: _buildScaffold(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(LandingViewModel viewModel) => Column(
|
Widget _buildScaffold(LandingViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -33,23 +34,19 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
|
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
|
||||||
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
|
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildUpperColumn() => Column(
|
Widget _buildUpperColumn() => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: _buildUpperColumnChildren(),
|
children: _buildUpperColumnChildren(),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren() => [
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
verticalSpaceLarge,
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
_buildIconWrapper(),
|
|
||||||
verticalSpaceLarge
|
|
||||||
|
|
||||||
];
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
Widget _buildIconWrapper()=> Align(alignment: Alignment.topLeft,child: _buildIcon(),);
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
'assets/icons/logo.svg',
|
'assets/icons/logo.svg',
|
||||||
|
|
@ -57,8 +54,6 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
height: 25,
|
height: 25,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
|
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
|
||||||
child: _buildLowerColumn(viewModel),
|
child: _buildLowerColumn(viewModel),
|
||||||
);
|
);
|
||||||
|
|
@ -77,9 +72,7 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
_buildSafeWrapper(viewModel)
|
_buildSafeWrapper(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() =>
|
Widget _buildTitle() => Text.rich(
|
||||||
|
|
||||||
Text.rich(
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'እንግሊዝኛ\n',
|
text: 'እንግሊዝኛ\n',
|
||||||
style: style25P600,
|
style: style25P600,
|
||||||
|
|
@ -91,13 +84,10 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' እድሜ ',
|
text: ' እድሜ ',
|
||||||
style: style25P600,
|
style: style25P600,
|
||||||
|
|
||||||
),
|
),
|
||||||
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'ይማሩ!',
|
text: 'ይማሩ!',
|
||||||
style: style25P400,
|
style: style25P400,
|
||||||
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -110,10 +100,10 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
child: _buildImage(),
|
child: _buildImage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_2.jpg',
|
||||||
Widget _buildImage()=> Image.asset('assets/images/profile.png',fit: BoxFit.cover,);
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
|
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
|
||||||
SafeArea(child: _buildContinueButtonWrapper(viewModel));
|
SafeArea(child: _buildContinueButtonWrapper(viewModel));
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
);
|
);
|
||||||
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
|
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
child: _buildScaffold(viewModel),);
|
child: _buildScaffold(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildScaffold(LandingViewModel viewModel) => Column(
|
Widget _buildScaffold(LandingViewModel viewModel) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -33,23 +34,19 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
|
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
|
||||||
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
|
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildUpperColumn() => Column(
|
Widget _buildUpperColumn() => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: _buildUpperColumnChildren(),
|
children: _buildUpperColumnChildren(),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildUpperColumnChildren() => [
|
List<Widget> _buildUpperColumnChildren() =>
|
||||||
verticalSpaceLarge,
|
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
|
||||||
_buildIconWrapper(),
|
|
||||||
verticalSpaceLarge
|
|
||||||
|
|
||||||
];
|
Widget _buildIconWrapper() => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
Widget _buildIconWrapper()=> Align(alignment: Alignment.topLeft,child: _buildIcon(),);
|
child: _buildIcon(),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildIcon() => SvgPicture.asset(
|
Widget _buildIcon() => SvgPicture.asset(
|
||||||
'assets/icons/logo.svg',
|
'assets/icons/logo.svg',
|
||||||
|
|
@ -57,8 +54,6 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
height: 25,
|
height: 25,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
|
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
|
||||||
child: _buildLowerColumn(viewModel),
|
child: _buildLowerColumn(viewModel),
|
||||||
);
|
);
|
||||||
|
|
@ -77,9 +72,7 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
_buildSafeWrapper(viewModel)
|
_buildSafeWrapper(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle() =>
|
Widget _buildTitle() => Text.rich(
|
||||||
|
|
||||||
Text.rich(
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'እንግሊዝኛ\n',
|
text: 'እንግሊዝኛ\n',
|
||||||
style: style25P600,
|
style: style25P600,
|
||||||
|
|
@ -89,15 +82,12 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
style: style25P400,
|
style: style25P400,
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' እድሜ ',
|
text: ' ቦታ ',
|
||||||
style: style25P600,
|
style: style25P600,
|
||||||
|
|
||||||
),
|
),
|
||||||
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'ይማሩ!',
|
text: 'ይማሩ!',
|
||||||
style: style25P400,
|
style: style25P400,
|
||||||
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -110,10 +100,10 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
|
||||||
child: _buildImage(),
|
child: _buildImage(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget _buildImage() => Image.asset(
|
||||||
|
'assets/images/landing_3.jpg',
|
||||||
Widget _buildImage()=> Image.asset('assets/images/profile.png',fit: BoxFit.cover,);
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
|
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
|
||||||
SafeArea(child: _buildContinueButtonWrapper(viewModel));
|
SafeArea(child: _buildContinueButtonWrapper(viewModel));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
import '../../common/translations/locale_keys.g.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
import '../../widgets/custom_small_radio_button.dart';
|
import '../../widgets/custom_small_radio_button.dart';
|
||||||
import '../../widgets/small_app_bar.dart';
|
import '../../widgets/small_app_bar.dart';
|
||||||
|
|
@ -106,16 +108,16 @@ class LanguageView extends StackedView<LanguageViewModel> {
|
||||||
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
title: 'Language Preference',
|
title:LocaleKeys.language_preference.tr() ,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
Widget _buildTitle() => Text(
|
||||||
'Choose your language',
|
LocaleKeys.choose_your_language.tr(),
|
||||||
style: style25DG600,
|
style: style25DG600,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubtitle() => Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'You can switch languages anytime',
|
LocaleKeys.switch_language_anytime.tr() ,
|
||||||
style: style14MG400,
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) => _buildTile(
|
itemBuilder: (context, index) => _buildTile(
|
||||||
module: viewModel.modules[index],
|
module: viewModel.modules[index],
|
||||||
onLockTap: () async => await viewModel.navigateToLearnSubscription(),
|
|
||||||
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
|
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
|
||||||
id: viewModel.modules[index].id ?? 0,
|
id: viewModel.modules[index].id ?? 0,
|
||||||
module: viewModel.modules[index].name ?? ''),
|
module: viewModel.modules[index].name ?? ''),
|
||||||
|
|
@ -133,13 +132,11 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
|
||||||
|
|
||||||
Widget _buildTile({
|
Widget _buildTile({
|
||||||
required LearnModule module,
|
required LearnModule module,
|
||||||
required GestureTapCallback onLockTap,
|
|
||||||
required GestureTapCallback onModuleTap,
|
required GestureTapCallback onModuleTap,
|
||||||
required GestureTapCallback onPracticeTap,
|
required GestureTapCallback onPracticeTap,
|
||||||
}) =>
|
}) =>
|
||||||
LearnModuleTile(
|
LearnModuleTile(
|
||||||
module: module,
|
module: module,
|
||||||
onLockTap: onLockTap,
|
|
||||||
onModuleTap: onModuleTap,
|
onModuleTap: onModuleTap,
|
||||||
onPracticeTap: onPracticeTap);
|
onPracticeTap: onPracticeTap);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ class LearnModuleViewModel extends ReactiveViewModel {
|
||||||
// Navigation
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
Future<void> navigateToLearnSubscription() async =>
|
|
||||||
await _navigationService.navigateToLearnSubscriptionView();
|
|
||||||
|
|
||||||
Future<void> navigateToLearnLesson(LearnModule module) async =>
|
Future<void> navigateToLearnLesson(LearnModule module) async =>
|
||||||
await _navigationService.navigateToLearnLessonView(module: module);
|
await _navigationService.navigateToLearnLessonView(module: module);
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,14 @@ class LearnProgramView extends StackedView<LearnProgramViewModel> {
|
||||||
program: viewModel.learnPrograms[index],
|
program: viewModel.learnPrograms[index],
|
||||||
onTap: () async => await viewModel
|
onTap: () async => await viewModel
|
||||||
.navigateToLearnCourse(viewModel.learnPrograms[index].id ?? 0),
|
.navigateToLearnCourse(viewModel.learnPrograms[index].id ?? 0),
|
||||||
|
onLockTap: () async => await viewModel.navigateToLearnSubscription(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTile({
|
Widget _buildTile({
|
||||||
required LearnProgram program,
|
required LearnProgram program,
|
||||||
required GestureTapCallback onTap,
|
required GestureTapCallback onTap,
|
||||||
|
required GestureTapCallback onLockTap,
|
||||||
}) =>
|
}) =>
|
||||||
LearnProgramTile(onTap: onTap, program: program);
|
LearnProgramTile(onTap: onTap, program: program,onLockTap: onLockTap,);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ class LearnProgramViewModel extends ReactiveViewModel {
|
||||||
Future<void> navigateToLearnCourse(int id) async =>
|
Future<void> navigateToLearnCourse(int id) async =>
|
||||||
_navigationService.navigateToLearnCourseView(id: id);
|
_navigationService.navigateToLearnCourseView(id: id);
|
||||||
|
|
||||||
|
Future<void> navigateToLearnSubscription() async =>
|
||||||
|
await _navigationService.navigateToLearnSubscriptionView();
|
||||||
|
|
||||||
// Remote api call
|
// Remote api call
|
||||||
|
|
||||||
// Learn programs
|
// Learn programs
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_strings.dart';
|
import 'package:yimaru_app/ui/common/app_strings.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/privacy_policy_tile.dart';
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
|
|
@ -129,7 +130,7 @@ class PrivacyPolicyView extends StackedView<PrivacyPolicyViewModel> {
|
||||||
Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar(
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
title: 'Privacy Policy',
|
title: LocaleKeys.privacy_policy.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) =>
|
Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) =>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/profile_card.dart';
|
import 'package:yimaru_app/ui/widgets/profile_card.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/profile_image.dart';
|
import 'package:yimaru_app/ui/widgets/profile_image.dart';
|
||||||
|
|
@ -138,7 +140,7 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProfileName(ProfileViewModel viewModel) => Text(
|
Widget _buildProfileName(ProfileViewModel viewModel) => Text(
|
||||||
'Hi, ${viewModel.user?.firstName ?? ''} 👋',
|
'${LocaleKeys.hello.tr()}, ${viewModel.user?.firstName ?? ''} 👋',
|
||||||
style: style25DG600,
|
style: style25DG600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -169,31 +171,31 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
|
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
|
||||||
title: 'My Progress',
|
|
||||||
icon: Icons.stacked_bar_chart,
|
icon: Icons.stacked_bar_chart,
|
||||||
subtitle: 'Track your achievements and learning streak',
|
title: LocaleKeys.my_progress.tr(),
|
||||||
onTap: () async => await viewModel.navigateToProgress(),
|
subtitle: LocaleKeys.track_your_achievement.tr(),
|
||||||
|
// onTap: () async => await viewModel.navigateToProgress(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
|
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
|
||||||
title: 'Account & Privacy',
|
|
||||||
icon: Icons.privacy_tip_outlined,
|
icon: Icons.privacy_tip_outlined,
|
||||||
subtitle: 'Manage setting and app preference',
|
subtitle: LocaleKeys.manage_settings.tr(),
|
||||||
|
title: LocaleKeys.account_and_privacy.tr(),
|
||||||
onTap: () async => await viewModel.navigateToAccountPrivacy(),
|
onTap: () async => await viewModel.navigateToAccountPrivacy(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
|
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
|
||||||
title: 'Support',
|
|
||||||
icon: Icons.headphones,
|
icon: Icons.headphones,
|
||||||
subtitle: 'Get help through phone or Telegram',
|
title: LocaleKeys.support.tr(),
|
||||||
|
subtitle: LocaleKeys.get_help.tr(),
|
||||||
onTap: () async => await viewModel.navigateToSupport(),
|
onTap: () async => await viewModel.navigateToSupport(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLogOutButton(ProfileViewModel viewModel) => CustomElevatedButton(
|
Widget _buildLogOutButton(ProfileViewModel viewModel) => CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Logout',
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
foregroundColor: kcRed,
|
foregroundColor: kcRed,
|
||||||
|
text: LocaleKeys.logout.tr(),
|
||||||
backgroundColor: kcRed.withOpacity(0.25),
|
backgroundColor: kcRed.withOpacity(0.25),
|
||||||
onTap: () async => await viewModel.logout(),
|
onTap: () async => await viewModel.logout(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:stacked/stacked_annotations.dart';
|
import 'package:stacked/stacked_annotations.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
|
import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
|
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
|
||||||
|
|
||||||
|
|
@ -160,7 +162,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
title: 'Edit Profile',
|
title: LocaleKeys.edit_profile.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildColumnWrapper(
|
Widget _buildColumnWrapper(
|
||||||
|
|
@ -270,8 +272,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildFirstNameLabel() => CustomFormLabel(
|
Widget _buildFirstNameLabel() => CustomFormLabel(
|
||||||
label: 'First Name',
|
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
|
label: LocaleKeys.first_name.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildFirstNameFormField(ProfileDetailViewModel viewModel) =>
|
Widget _buildFirstNameFormField(ProfileDetailViewModel viewModel) =>
|
||||||
|
|
@ -317,8 +319,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildLastNameLabel() => CustomFormLabel(
|
Widget _buildLastNameLabel() => CustomFormLabel(
|
||||||
label: 'Last Name',
|
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
|
label: LocaleKeys.last_name.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLastNameFormField(ProfileDetailViewModel viewModel) =>
|
Widget _buildLastNameFormField(ProfileDetailViewModel viewModel) =>
|
||||||
|
|
@ -356,8 +358,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildGenderLabel() => CustomFormLabel(
|
Widget _buildGenderLabel() => CustomFormLabel(
|
||||||
label: 'Gender',
|
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
|
label: LocaleKeys.gender.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildRadioButtonWrapper(ProfileDetailViewModel viewModel) => Row(
|
Widget _buildRadioButtonWrapper(ProfileDetailViewModel viewModel) => Row(
|
||||||
|
|
@ -449,8 +451,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildPhoneNumberLabel() => CustomFormLabel(
|
Widget _buildPhoneNumberLabel() => CustomFormLabel(
|
||||||
label: 'Phone Number',
|
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
|
label: LocaleKeys.phone_number.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
|
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
|
||||||
|
|
@ -496,8 +498,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildEmailLabel() => CustomFormLabel(
|
Widget _buildEmailLabel() => CustomFormLabel(
|
||||||
label: 'Email',
|
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
|
label: LocaleKeys.email.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
|
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
|
||||||
|
|
@ -522,8 +524,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCountryDropdownLabel() => CustomFormLabel(
|
Widget _buildCountryDropdownLabel() => CustomFormLabel(
|
||||||
label: 'Country',
|
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
|
label: LocaleKeys.country.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
|
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
|
||||||
|
|
@ -559,8 +561,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildRegionFormFieldLabel() => CustomFormLabel(
|
Widget _buildRegionFormFieldLabel() => CustomFormLabel(
|
||||||
label: 'Region',
|
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
|
label: LocaleKeys.region.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildRegionFormState(ProfileDetailViewModel viewModel) =>
|
Widget _buildRegionFormState(ProfileDetailViewModel viewModel) =>
|
||||||
|
|
@ -570,8 +572,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
|
|
||||||
Widget _buildRegionDropDown(ProfileDetailViewModel viewModel) =>
|
Widget _buildRegionDropDown(ProfileDetailViewModel viewModel) =>
|
||||||
CustomDropdownPicker(
|
CustomDropdownPicker(
|
||||||
hint: 'Select region',
|
|
||||||
icon: _buildSearchIcon(),
|
icon: _buildSearchIcon(),
|
||||||
|
hint:LocaleKeys.select_region.tr(),
|
||||||
selectedItem: viewModel.selectedRegion,
|
selectedItem: viewModel.selectedRegion,
|
||||||
items: (value, props) => viewModel.getRegions(),
|
items: (value, props) => viewModel.getRegions(),
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
|
|
@ -582,8 +584,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
controller: regionController,
|
controller: regionController,
|
||||||
onTap: viewModel.setRegionFocus,
|
onTap: viewModel.setRegionFocus,
|
||||||
decoration: inputDecoration(
|
decoration: inputDecoration(
|
||||||
hint: 'Enter Your City',
|
|
||||||
focus: viewModel.focusRegion,
|
focus: viewModel.focusRegion,
|
||||||
|
hint:LocaleKeys.enter_your_city.tr(),
|
||||||
filled: regionController.text.isNotEmpty),
|
filled: regionController.text.isNotEmpty),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -614,14 +616,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
|
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
|
||||||
label: 'Occupation',
|
|
||||||
style: style16DG600,
|
style: style16DG600,
|
||||||
|
label: LocaleKeys.occupation.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
|
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
|
||||||
CustomDropdownPicker(
|
CustomDropdownPicker(
|
||||||
hint: 'Select occupation',
|
|
||||||
icon: _buildSearchIcon(),
|
icon: _buildSearchIcon(),
|
||||||
|
hint:LocaleKeys.select_occupation.tr(),
|
||||||
selectedItem: viewModel.selectedOccupation,
|
selectedItem: viewModel.selectedOccupation,
|
||||||
items: (value, props) => viewModel.getOccupations(),
|
items: (value, props) => viewModel.getOccupations(),
|
||||||
onChanged: (value) => viewModel.setSelectedOccupation(
|
onChanged: (value) => viewModel.setSelectedOccupation(
|
||||||
|
|
@ -645,9 +647,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
text: 'Save Changes',
|
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
|
text: LocaleKeys.save_changes.tr(),
|
||||||
onTap: () async => await _update(viewModel),
|
onTap: () async => await _update(viewModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -659,10 +661,10 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
|
Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
|
||||||
CustomElevatedButton(
|
CustomElevatedButton(
|
||||||
height: 55,
|
height: 55,
|
||||||
text: 'Cancel',
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
onTap: viewModel.pop,
|
onTap: viewModel.pop,
|
||||||
backgroundColor: kcWhite,
|
backgroundColor: kcWhite,
|
||||||
|
text:LocaleKeys.cancel.tr(),
|
||||||
borderColor: kcPrimaryColor,
|
borderColor: kcPrimaryColor,
|
||||||
foregroundColor: kcPrimaryColor,
|
foregroundColor: kcPrimaryColor,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class StartupView extends StackedView<StartupViewModel> {
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildBackground() => Image.asset(
|
Widget _buildBackground() => Image.asset(
|
||||||
'assets/images/onboarding_1.png',
|
'assets/images/loading.png',
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
width: double.maxFinite,
|
width: double.maxFinite,
|
||||||
height: double.maxFinite,
|
height: double.maxFinite,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '../../../app/app.router.dart';
|
||||||
import '../../../models/user.dart';
|
import '../../../models/user.dart';
|
||||||
import '../../../services/api_service.dart';
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/image_downloader_service.dart';
|
import '../../../services/image_downloader_service.dart';
|
||||||
|
import '../../../services/localization_service.dart';
|
||||||
import '../../../services/status_checker_service.dart';
|
import '../../../services/status_checker_service.dart';
|
||||||
import '../../common/enmus.dart';
|
import '../../common/enmus.dart';
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ class StartupViewModel extends ReactiveViewModel {
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
final _statusChecker = locator<StatusCheckerService>();
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
final _localizationService = locator<LocalizationService>();
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
final _imageDownloaderService = locator<ImageDownloaderService>();
|
final _imageDownloaderService = locator<ImageDownloaderService>();
|
||||||
|
|
||||||
|
|
@ -29,6 +31,8 @@ class StartupViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
// Main startup and navigation logic
|
// Main startup and navigation logic
|
||||||
Future runStartupLogic() async {
|
Future runStartupLogic() async {
|
||||||
|
await _localizationService.loadSelectedLanguage();
|
||||||
|
|
||||||
final loggedIn = await _authenticationService.userLoggedIn();
|
final loggedIn = await _authenticationService.userLoggedIn();
|
||||||
|
|
||||||
final firstTimeInstall = await _authenticationService.isFirstTimeInstall();
|
final firstTimeInstall = await _authenticationService.isFirstTimeInstall();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/support_card.dart';
|
import 'package:yimaru_app/ui/widgets/support_card.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
|
|
@ -51,9 +53,9 @@ class SupportView extends StackedView<SupportViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar(
|
||||||
title: 'Need Help?',
|
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
|
title:LocaleKeys.need_help.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContentWrapper(SupportViewModel viewModel) =>
|
Widget _buildContentWrapper(SupportViewModel viewModel) =>
|
||||||
|
|
@ -85,16 +87,16 @@ class SupportView extends StackedView<SupportViewModel> {
|
||||||
Widget _buildCallSupport(SupportViewModel viewModel) => SupportCard(
|
Widget _buildCallSupport(SupportViewModel viewModel) => SupportCard(
|
||||||
icon: Icons.call,
|
icon: Icons.call,
|
||||||
color: kcPrimaryColor,
|
color: kcPrimaryColor,
|
||||||
title: 'Call Support',
|
title:LocaleKeys.call_support.tr(),
|
||||||
subtitle: 'Talk with our support team directly',
|
subtitle: LocaleKeys.talk_with_support.tr(),
|
||||||
onTap: () async => await viewModel.navigateToCallSupport(),
|
onTap: () async => await viewModel.navigateToCallSupport(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTelegramSupport(SupportViewModel viewModel) => SupportCard(
|
Widget _buildTelegramSupport(SupportViewModel viewModel) => SupportCard(
|
||||||
color: kcSkyBlue,
|
color: kcSkyBlue,
|
||||||
icon: Icons.telegram,
|
icon: Icons.telegram,
|
||||||
title: 'Telegram Support',
|
title: LocaleKeys.telegram_support.tr(),
|
||||||
subtitle: 'Chat Instantly via Telegram',
|
subtitle: LocaleKeys.chat_via_telegram.tr(),
|
||||||
onTap: () async => await viewModel.navigateToTelegramSupport(),
|
onTap: () async => await viewModel.navigateToTelegramSupport(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_strings.dart';
|
import 'package:yimaru_app/ui/common/app_strings.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
|
|
||||||
import '../../common/app_colors.dart';
|
import '../../common/app_colors.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
import '../../widgets/custom_elevated_button.dart';
|
|
||||||
import '../../widgets/small_app_bar.dart';
|
import '../../widgets/small_app_bar.dart';
|
||||||
import 'terms_and_conditions_viewmodel.dart';
|
import 'terms_and_conditions_viewmodel.dart';
|
||||||
|
|
||||||
|
|
@ -59,7 +60,7 @@ class TermsAndConditionsView extends StackedView<TermsAndConditionsViewModel> {
|
||||||
Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar(
|
Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar(
|
||||||
onPop: viewModel.pop,
|
onPop: viewModel.pop,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
title: 'Terms and Conditions',
|
title: LocaleKeys.terms_and_conditions.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) =>
|
Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) =>
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
import 'package:yimaru_app/models/course_lesson.dart';
|
import 'package:yimaru_app/models/course_lesson.dart';
|
||||||
import 'package:yimaru_app/ui/views/course_lesson/course_lesson_viewmodel.dart';
|
import 'package:yimaru_app/ui/views/course_module/course_module_viewmodel.dart';
|
||||||
|
|
||||||
import '../common/app_colors.dart';
|
import '../common/app_colors.dart';
|
||||||
|
import '../common/enmus.dart';
|
||||||
|
import '../common/helper_functions.dart';
|
||||||
import '../common/ui_helpers.dart';
|
import '../common/ui_helpers.dart';
|
||||||
import 'custom_elevated_button.dart';
|
import 'custom_elevated_button.dart';
|
||||||
import 'mini_thumbnail.dart';
|
import 'mini_thumbnail.dart';
|
||||||
|
|
||||||
class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
|
class CourseLessonTile extends ViewModelWidget<CourseModuleViewModel> {
|
||||||
final CourseLesson lesson;
|
final CourseLesson lesson;
|
||||||
final GestureTapCallback? onVideoTap;
|
final GestureTapCallback? onVideoTap;
|
||||||
final GestureTapCallback? onPracticeTap;
|
final GestureTapCallback? onPracticeTap;
|
||||||
|
|
@ -21,19 +23,16 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, CourseLessonViewModel viewModel) =>
|
Widget build(BuildContext context, CourseModuleViewModel viewModel) =>
|
||||||
_buildExpansionTileCard(context: context, viewModel: viewModel);
|
_buildExpansionTileCard(context: context, viewModel: viewModel);
|
||||||
|
|
||||||
Widget _buildExpansionTileCard(
|
Widget _buildExpansionTileCard(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
required CourseLessonViewModel viewModel}) =>
|
required CourseModuleViewModel viewModel}) =>
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(bottom: 15),
|
margin: const EdgeInsets.only(bottom: 15),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
border: Border.all(
|
|
||||||
color: kcPrimaryColor.withValues(alpha: 0.25),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: _buildColumn(),
|
child: _buildColumn(),
|
||||||
);
|
);
|
||||||
|
|
@ -44,7 +43,7 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> _buildColumnChildren() => [
|
List<Widget> _buildColumnChildren() => [
|
||||||
// _buildDivider(),
|
_buildDivider(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildTile(),
|
_buildTile(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
|
|
@ -52,11 +51,14 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Widget _buildDivider() => const Divider(color: kcVeryLightGrey);
|
||||||
|
|
||||||
Widget _buildTile() => ListTile(
|
Widget _buildTile() => ListTile(
|
||||||
minTileHeight: 0,
|
minTileHeight: 0,
|
||||||
title: _buildTitle(),
|
title: _buildTitle(),
|
||||||
subtitle: _buildSubtitle(),
|
subtitle: _buildSubtitle(),
|
||||||
leading: _buildLeadingWrapper(),
|
leading: _buildLeadingWrapper(),
|
||||||
|
trailing: _buildTrailingWrapper(),
|
||||||
titleAlignment: ListTileTitleAlignment.top,
|
titleAlignment: ListTileTitleAlignment.top,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 15),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
);
|
);
|
||||||
|
|
@ -67,12 +69,29 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubtitle() => Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'${((lesson.duration ?? 0) / 50).toInt()} min',
|
'${((lesson.id ?? 0) / 50).toInt()} min',
|
||||||
style: style14MG400,
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLeadingWrapper() =>
|
Widget _buildLeadingWrapper() => MiniThumbnail(
|
||||||
const MiniThumbnail(thumbnail: 'assets/images/image_1.png');
|
thumbnail:
|
||||||
|
getReadableUrl(lesson.thumbnail ?? 'assets/images/image_1.png') ??
|
||||||
|
'assets/images/image_1.png');
|
||||||
|
|
||||||
|
Widget _buildTrailingWrapper() =>
|
||||||
|
ProgressStatuses.completed != ProgressStatuses.completed
|
||||||
|
? _buildCompletedTrailing()
|
||||||
|
: _buildPendingTrailing();
|
||||||
|
|
||||||
|
Widget _buildCompletedTrailing() => const Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: kcGreen,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildPendingTrailing() => const Icon(
|
||||||
|
Icons.circle_outlined,
|
||||||
|
color: kcLightGrey,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildActionButtonWrapper() => Container(
|
Widget _buildActionButtonWrapper() => Container(
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|
@ -107,9 +126,9 @@ class CourseLessonTile extends ViewModelWidget<CourseLessonViewModel> {
|
||||||
|
|
||||||
Widget _buildPracticeButton() => CustomElevatedButton(
|
Widget _buildPracticeButton() => CustomElevatedButton(
|
||||||
height: 15,
|
height: 15,
|
||||||
text: 'Practice',
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
onTap: onPracticeTap,
|
onTap: onPracticeTap,
|
||||||
|
text: 'Practice Test',
|
||||||
backgroundColor: kcWhite,
|
backgroundColor: kcWhite,
|
||||||
borderColor: kcPrimaryColor,
|
borderColor: kcPrimaryColor,
|
||||||
foregroundColor: kcPrimaryColor,
|
foregroundColor: kcPrimaryColor,
|
||||||
|
|
|
||||||
209
lib/ui/widgets/course_module_tile_large.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yimaru_app/models/course_module.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
|
|
||||||
class CourseModuleTileSmall extends StatelessWidget {
|
class CourseModuleTileSmall extends StatelessWidget {
|
||||||
final String title;
|
final CourseModule? module;
|
||||||
final ProgressStatuses status;
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
const CourseModuleTileSmall(
|
const CourseModuleTileSmall({super.key, this.onTap, required this.module});
|
||||||
{super.key, required this.title, required this.status});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => _buildTile();
|
Widget build(BuildContext context) => _buildTile();
|
||||||
|
|
||||||
Widget _buildTile() => ListTile(
|
Widget _buildTile() => ListTile(
|
||||||
|
onTap: onTap,
|
||||||
title: _buildTitle(),
|
title: _buildTitle(),
|
||||||
leading: _buildLeadingWrapper(),
|
leading: _buildLeadingWrapper(),
|
||||||
trailing: _buildTrailingWrapper(),
|
trailing: _buildTrailingWrapper(),
|
||||||
|
|
@ -27,7 +28,7 @@ class CourseModuleTileSmall extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTitle() => Text(
|
Widget _buildTitle() => Text(
|
||||||
title,
|
module?.name ?? '',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
style: style14DG600,
|
style: style14DG600,
|
||||||
|
|
@ -43,7 +44,8 @@ class CourseModuleTileSmall extends StatelessWidget {
|
||||||
color: kcLightGrey,
|
color: kcLightGrey,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTrailingWrapper() => status == ProgressStatuses.completed
|
Widget _buildTrailingWrapper() =>
|
||||||
|
ProgressStatuses.completed != ProgressStatuses.completed
|
||||||
? _buildCompletedTrailing()
|
? _buildCompletedTrailing()
|
||||||
: _buildPendingTrailing();
|
: _buildPendingTrailing();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:yimaru_app/models/course_module.dart';
|
||||||
import 'package:yimaru_app/models/course_unit.dart';
|
import 'package:yimaru_app/models/course_unit.dart';
|
||||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
|
||||||
import 'package:yimaru_app/ui/widgets/course_module_tile_small.dart';
|
import 'package:yimaru_app/ui/widgets/course_module_tile_small.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
|
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
|
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
|
||||||
|
|
||||||
|
import '../../models/course_catalog.dart';
|
||||||
import '../common/app_colors.dart';
|
import '../common/app_colors.dart';
|
||||||
import '../common/ui_helpers.dart';
|
import '../common/ui_helpers.dart';
|
||||||
import '../views/course_unit/course_unit_viewmodel.dart';
|
import '../views/course_unit/course_unit_viewmodel.dart';
|
||||||
|
|
@ -15,16 +16,18 @@ import 'custom_elevated_button.dart';
|
||||||
class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
|
class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
|
||||||
final int index;
|
final int index;
|
||||||
final CourseUnit unit;
|
final CourseUnit unit;
|
||||||
|
final CourseCatalog catalog;
|
||||||
|
|
||||||
final GestureTapCallback? onLessonTap;
|
final GestureTapCallback? onLessonTap;
|
||||||
final GestureTapCallback? onPracticeTap;
|
final GestureTapCallback? onPracticeTap;
|
||||||
|
|
||||||
const CourseUnitTile({
|
const CourseUnitTile(
|
||||||
super.key,
|
{super.key,
|
||||||
this.onLessonTap,
|
this.onLessonTap,
|
||||||
this.onPracticeTap,
|
this.onPracticeTap,
|
||||||
required this.unit,
|
required this.unit,
|
||||||
required this.index,
|
required this.index,
|
||||||
});
|
required this.catalog});
|
||||||
|
|
||||||
Future<void> _getCourseModules({
|
Future<void> _getCourseModules({
|
||||||
required bool expanded,
|
required bool expanded,
|
||||||
|
|
@ -35,7 +38,7 @@ class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
|
||||||
// Prevent duplicate API calls
|
// Prevent duplicate API calls
|
||||||
if ((unit.modules?.isNotEmpty ?? false)) return;
|
if ((unit.modules?.isNotEmpty ?? false)) return;
|
||||||
|
|
||||||
await viewModel.getCourseUnitModules(index: index, id: unit.id ?? 0);
|
await viewModel.getCourseModules(index: index, id: unit.id ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showSheet(
|
Future<void> _showSheet(
|
||||||
|
|
@ -83,14 +86,12 @@ class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
|
||||||
showTrailingIcon: true,
|
showTrailingIcon: true,
|
||||||
initiallyExpanded: false,
|
initiallyExpanded: false,
|
||||||
subtitle: _buildSubtitle(),
|
subtitle: _buildSubtitle(),
|
||||||
// key: Key(unit.id.toString()),
|
|
||||||
collapsedIconColor: kcDarkGrey,
|
collapsedIconColor: kcDarkGrey,
|
||||||
collapsedTextColor: kcDarkGrey,
|
collapsedTextColor: kcDarkGrey,
|
||||||
backgroundColor: kcBackgroundColor,
|
backgroundColor: kcBackgroundColor,
|
||||||
shape: Border.all(color: kcTransparent),
|
shape: Border.all(color: kcTransparent),
|
||||||
expandedAlignment: Alignment.centerLeft,
|
expandedAlignment: Alignment.centerLeft,
|
||||||
collapsedBackgroundColor: kcBackgroundColor,
|
collapsedBackgroundColor: kcBackgroundColor,
|
||||||
|
|
||||||
controlAffinity: ListTileControlAffinity.trailing,
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||||
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
|
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
|
@ -230,10 +231,15 @@ class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCourseModulesState(CourseUnitViewModel viewModel) =>
|
Widget _buildCourseModulesState(CourseUnitViewModel viewModel) =>
|
||||||
viewModel.busy(StateObjects.courseModules)
|
viewModel.busy(index)
|
||||||
? _buildProgressIndicator()
|
? _buildProgressIndicatorWrapper()
|
||||||
: _buildCourseModules(viewModel);
|
: _buildCourseModules(viewModel);
|
||||||
|
|
||||||
|
Widget _buildProgressIndicatorWrapper() => SizedBox(
|
||||||
|
height: 50,
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: _buildProgressIndicator(),
|
||||||
|
);
|
||||||
Widget _buildProgressIndicator() => const Center(
|
Widget _buildProgressIndicator() => const Center(
|
||||||
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
|
||||||
);
|
);
|
||||||
|
|
@ -242,12 +248,15 @@ class CourseUnitTile extends ViewModelWidget<CourseUnitViewModel> {
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: unit.modules?.length,
|
itemCount: unit.modules?.length,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) => _buildCourseModuleCard(
|
||||||
_buildCourseModuleCard(unit.modules?[index].name ?? ''),
|
module: unit.modules?[index],
|
||||||
|
onTap: () async => await viewModel.navigateToCourseModule(
|
||||||
|
catalog: catalog, module: unit.modules?[index])),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildCourseModuleCard(String title) =>
|
Widget _buildCourseModuleCard(
|
||||||
CourseModuleTileSmall(title: title, status: ProgressStatuses.completed);
|
{required CourseModule? module, required GestureTapCallback onTap}) =>
|
||||||
|
CourseModuleTileSmall(onTap: onTap, module: module);
|
||||||
|
|
||||||
// Widget _buildContainerShaderState() => status == ProgressStatuses.pending
|
// Widget _buildContainerShaderState() => status == ProgressStatuses.pending
|
||||||
// ? _buildContainerShaderWrapper()
|
// ? _buildContainerShaderWrapper()
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,11 @@ import 'custom_elevated_button.dart';
|
||||||
|
|
||||||
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
final LearnModule module;
|
final LearnModule module;
|
||||||
final GestureTapCallback? onLockTap;
|
|
||||||
final GestureTapCallback? onModuleTap;
|
final GestureTapCallback? onModuleTap;
|
||||||
final GestureTapCallback? onPracticeTap;
|
final GestureTapCallback? onPracticeTap;
|
||||||
|
|
||||||
const LearnModuleTile(
|
const LearnModuleTile(
|
||||||
{super.key,
|
{super.key,
|
||||||
this.onLockTap,
|
|
||||||
this.onModuleTap,
|
this.onModuleTap,
|
||||||
this.onPracticeTap,
|
this.onPracticeTap,
|
||||||
required this.module});
|
required this.module});
|
||||||
|
|
@ -34,15 +32,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, LearnModuleViewModel viewModel) =>
|
Widget build(BuildContext context, LearnModuleViewModel viewModel) =>
|
||||||
_buildExpansionTileWrapper(context: context, viewModel: viewModel);
|
_buildExpansionTileCard(context: context, viewModel: viewModel);
|
||||||
|
|
||||||
Widget _buildExpansionTileWrapper(
|
|
||||||
{required BuildContext context,
|
|
||||||
required LearnModuleViewModel viewModel}) =>
|
|
||||||
GestureDetector(
|
|
||||||
onTap: !(module.access?.isAccessible ?? false) ? onLockTap : null,
|
|
||||||
child: _buildExpansionTileCard(context: context, viewModel: viewModel),
|
|
||||||
);
|
|
||||||
Widget _buildExpansionTileCard(
|
Widget _buildExpansionTileCard(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
required LearnModuleViewModel viewModel}) =>
|
required LearnModuleViewModel viewModel}) =>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stacked/stacked.dart';
|
import 'package:stacked/stacked.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/progress_status.dart';
|
import 'package:yimaru_app/ui/widgets/progress_status.dart';
|
||||||
|
|
||||||
|
|
@ -11,12 +13,20 @@ import 'custom_elevated_button.dart';
|
||||||
class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
|
class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
|
||||||
final LearnProgram program;
|
final LearnProgram program;
|
||||||
final GestureTapCallback? onTap;
|
final GestureTapCallback? onTap;
|
||||||
|
final GestureTapCallback? onLockTap;
|
||||||
|
|
||||||
const LearnProgramTile({super.key, this.onTap, required this.program});
|
const LearnProgramTile(
|
||||||
|
{super.key, this.onTap, this.onLockTap, required this.program});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, LearnProgramViewModel viewModel) =>
|
Widget build(BuildContext context, LearnProgramViewModel viewModel) =>
|
||||||
_buildExpansionTileCard(viewModel);
|
_buildExpansionTileCardWrapper(viewModel);
|
||||||
|
|
||||||
|
Widget _buildExpansionTileCardWrapper(LearnProgramViewModel viewModel) =>
|
||||||
|
GestureDetector(
|
||||||
|
onTap: !(program.access?.isAccessible ?? false) ? onLockTap : null,
|
||||||
|
child: _buildExpansionTileCard(viewModel),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildExpansionTileCard(LearnProgramViewModel viewModel) => Container(
|
Widget _buildExpansionTileCard(LearnProgramViewModel viewModel) => Container(
|
||||||
margin: const EdgeInsets.only(bottom: 15),
|
margin: const EdgeInsets.only(bottom: 15),
|
||||||
|
|
@ -99,8 +109,8 @@ class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
|
||||||
Widget _buildProgressStatus() => ProgressStatus(
|
Widget _buildProgressStatus() => ProgressStatus(
|
||||||
color: kcPrimaryColor,
|
color: kcPrimaryColor,
|
||||||
status: (program.access?.isCompleted ?? false)
|
status: (program.access?.isCompleted ?? false)
|
||||||
? 'Completed'
|
?LocaleKeys.completed.tr()
|
||||||
: 'In Progress',
|
: LocaleKeys.in_progress.tr(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContent() => Text(
|
Widget _buildContent() => Text(
|
||||||
|
|
@ -123,7 +133,7 @@ class LearnProgramTile extends ViewModelWidget<LearnProgramViewModel> {
|
||||||
foregroundColor: kcWhite,
|
foregroundColor: kcWhite,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
text: program.access?.progressPercent == 0
|
text: program.access?.progressPercent == 0
|
||||||
? 'Start Learning'
|
? LocaleKeys.start_learning.tr()
|
||||||
: 'Continue Learning',
|
:LocaleKeys.continue_learning.tr() ,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_constants.dart';
|
import 'package:yimaru_app/ui/common/app_constants.dart';
|
||||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
|
|
||||||
import '../common/app_colors.dart';
|
import '../common/app_colors.dart';
|
||||||
|
import '../common/translations/locale_keys.g.dart';
|
||||||
|
|
||||||
class ProfileAppBar extends StatelessWidget {
|
class ProfileAppBar extends StatelessWidget {
|
||||||
final String? name;
|
final String? name;
|
||||||
|
|
@ -72,7 +74,7 @@ class ProfileAppBar extends StatelessWidget {
|
||||||
[_buildGreetingTitle(), _buildSubtitle()];
|
[_buildGreetingTitle(), _buildSubtitle()];
|
||||||
|
|
||||||
Widget _buildGreetingTitle() => Text.rich(
|
Widget _buildGreetingTitle() => Text.rich(
|
||||||
TextSpan(text: 'Hello,', style: style14DG600, children: [
|
TextSpan(text: '${LocaleKeys.hello.tr()},', style: style14DG600, children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' $name!',
|
text: ' $name!',
|
||||||
style: style14P600,
|
style: style14P600,
|
||||||
|
|
@ -81,7 +83,7 @@ class ProfileAppBar extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubtitle() => Text(
|
Widget _buildSubtitle() => Text(
|
||||||
'Ready to keep learning English today?',
|
LocaleKeys.ready_to_learn.tr(),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: style14DG400,
|
style: style14DG400,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
|
|
||||||
class ViewProfileButton extends StatelessWidget {
|
class ViewProfileButton extends StatelessWidget {
|
||||||
final GestureTapCallback? onTap;
|
final GestureTapCallback? onTap;
|
||||||
|
|
@ -21,10 +24,9 @@ class ViewProfileButton extends StatelessWidget {
|
||||||
List<Widget> _buildButtonRowChildren() =>
|
List<Widget> _buildButtonRowChildren() =>
|
||||||
[_buildButtonText(), const SizedBox(width: 10), _buildButtonIcon()];
|
[_buildButtonText(), const SizedBox(width: 10), _buildButtonIcon()];
|
||||||
|
|
||||||
Widget _buildButtonText() => const Text(
|
Widget _buildButtonText() => Text(
|
||||||
'View Profile',
|
LocaleKeys.view_profile.tr(),
|
||||||
style: TextStyle(
|
style: style16P900,
|
||||||
color: kcPrimaryColor, fontSize: 16, fontWeight: FontWeight.w900),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildButtonIcon() => const Icon(
|
Widget _buildButtonIcon() => const Icon(
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
name: yimaru_app
|
name: yimaru_app
|
||||||
version: 0.1.17+19
|
version: 0.1.18+20
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
|
||||||
import '../helpers/test_helpers.dart';
|
import '../helpers/test_helpers.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('CourseLessonViewModel Tests -', () {
|
group('CourseModuleViewModel Tests -', () {
|
||||||
setUp(() => registerServices());
|
setUp(() => registerServices());
|
||||||
tearDown(() => locator.reset());
|
tearDown(() => locator.reset());
|
||||||
});
|
});
|
||||||
|
|
@ -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());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -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());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -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());
|
|
||||||
});
|
|
||||||
}
|
|
||||||