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