Compare commits

..

No commits in common. "2050bd332c3359258a05ae85dde3a12cbdd5b62e" and "f8973c8752eda7d11e5ff89c8a3a0dd8d91d3347" have entirely different histories.

81 changed files with 1677 additions and 2063 deletions

View File

@ -1,49 +1,46 @@
{
"project_info": {
"project_number": "900714037062",
"project_id": "yimaru-academy-5e7e2",
"storage_bucket": "yimaru-academy-5e7e2.firebasestorage.app"
"project_number": "574860813475",
"project_id": "yimaru-lms-e834e",
"storage_bucket": "yimaru-lms-e834e.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:900714037062:android:f11e3b69315b05304e6f47",
"mobilesdk_app_id": "1:574860813475:android:cd7fa6cf3a0527d97acb16",
"android_client_info": {
"package_name": "com.yimaru.lms.app"
}
},
"oauth_client": [
{
"client_id": "900714037062-ngc0gc426sfnnjjr494g4vni46ne5uqv.apps.googleusercontent.com",
"client_id": "574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.yimaru.lms.app",
"certificate_hash": "139ee56ac9763191d1eee882efc440c10530e6e9"
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
}
},
{
"client_id": "900714037062-mtm9rps15br603nnn3a451rr2vllopro.apps.googleusercontent.com",
"client_type": 3
"client_id": "574860813475-m90u87plqaac4tb8oug32k41usossiod.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.yimaru.lms.app",
"certificate_hash": "29797902ad6a24212b9d9fad71562907956f6a6c"
}
}
],
"api_key": [
{
"current_key": "AIzaSyAi8e4PMFH8QQU11DWftHdNEu8WUP7i2ww"
"current_key": "AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "900714037062-24ria5fcfet71o3vde8f6gsvsj1n68ec.apps.googleusercontent.com",
"client_id": "574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "900714037062-35bg0hsou56hg37mbcbpiar9uti7tcku.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.yimaru.lms.app"
}
}
]
}

View File

@ -1 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-academy-5e7e2","appId":"1:900714037062:android:f11e3b69315b05304e6f47","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-academy-5e7e2","configurations":{"android":"1:900714037062:android:f11e3b69315b05304e6f47","ios":"1:900714037062:ios:1caf8f24a4333b8e4e6f47"}}}}}}
{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-lms-e834e","appId":"1:574860813475:android:cd7fa6cf3a0527d97acb16","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-lms-e834e","configurations":{"android":"1:574860813475:android:cd7fa6cf3a0527d97acb16","ios":"1:574860813475:ios:3ac9f7c4ae1771287acb16"}}}}}}

View File

@ -51,7 +51,6 @@ 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/ui/views/learn_subcategory/learn_subcategory_view.dart';
// @stacked-import
@StackedApp(
@ -91,7 +90,6 @@ import 'package:yimaru_app/ui/views/learn_subcategory/learn_subcategory_view.dar
MaterialRoute(page: CourseSubcategoryView),
MaterialRoute(page: CourseView),
MaterialRoute(page: CoursePracticeQuestionView),
MaterialRoute(page: LearnSubcategoryView),
// @stacked-route
],
dependencies: [

View File

@ -5,15 +5,14 @@
// **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i38;
import 'package:flutter/material.dart' as _i37;
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i44;
import 'package:yimaru_app/models/category.dart' as _i42;
import 'package:yimaru_app/models/course.dart' as _i40;
import 'package:yimaru_app/models/course_lesson.dart' as _i41;
import 'package:yimaru_app/models/level.dart' as _i39;
import 'package:yimaru_app/models/subcategory.dart' as _i43;
import 'package:stacked_services/stacked_services.dart' as _i42;
import 'package:yimaru_app/models/course.dart' as _i38;
import 'package:yimaru_app/models/course_category.dart' as _i40;
import 'package:yimaru_app/models/course_lesson.dart' as _i39;
import 'package:yimaru_app/models/course_subcategory.dart' as _i41;
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
as _i9;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i22;
@ -51,8 +50,6 @@ import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
as _i20;
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'
as _i26;
import 'package:yimaru_app/ui/views/learn_subcategory/learn_subcategory_view.dart'
as _i37;
import 'package:yimaru_app/ui/views/login/login_view.dart' as _i17;
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3;
import 'package:yimaru_app/ui/views/privacy_policy/privacy_policy_view.dart'
@ -141,8 +138,6 @@ class Routes {
static const coursePracticeQuestionView = '/course-practice-question-view';
static const learnSubcategoryView = '/learn-subcategory-view';
static const all = <String>{
homeView,
onboardingView,
@ -179,7 +174,6 @@ class Routes {
courseSubcategoryView,
courseView,
coursePracticeQuestionView,
learnSubcategoryView,
};
}
@ -325,21 +319,17 @@ class StackedRouter extends _i1.RouterBase {
Routes.coursePracticeQuestionView,
page: _i36.CoursePracticeQuestionView,
),
_i1.RouteDef(
Routes.learnSubcategoryView,
page: _i37.LearnSubcategoryView,
),
];
final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(),
settings: data,
);
},
_i3.OnboardingView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(),
settings: data,
);
@ -348,120 +338,116 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(),
);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data,
);
},
_i5.ProfileView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(),
settings: data,
);
},
_i6.ProfileDetailView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(),
settings: data,
);
},
_i7.DownloadsView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(),
settings: data,
);
},
_i8.ProgressView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(),
settings: data,
);
},
_i9.AccountPrivacyView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.AccountPrivacyView(),
settings: data,
);
},
_i10.SupportView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.SupportView(),
settings: data,
);
},
_i11.TelegramSupportView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.TelegramSupportView(),
settings: data,
);
},
_i12.CallSupportView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.CallSupportView(),
settings: data,
);
},
_i13.LanguageView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.LanguageView(),
settings: data,
);
},
_i14.PrivacyPolicyView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.PrivacyPolicyView(),
settings: data,
);
},
_i15.TermsAndConditionsView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.TermsAndConditionsView(),
settings: data,
);
},
_i16.RegisterView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.RegisterView(),
settings: data,
);
},
_i17.LoginView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.LoginView(),
settings: data,
);
},
_i18.LearnView: (data) {
final args = data.getArgs<LearnViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
builder: (context) => _i18.LearnView(key: args.key, id: args.id),
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LearnView(),
settings: data,
);
},
_i19.LearnLevelView: (data) {
final args = data.getArgs<LearnLevelViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
builder: (context) => _i19.LearnLevelView(key: args.key, id: args.id),
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnLevelView(),
settings: data,
);
},
_i20.LearnModuleView: (data) {
final args = data.getArgs<LearnModuleViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
builder: (context) =>
_i20.LearnModuleView(key: args.key, level: args.level),
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnModuleView(),
settings: data,
);
},
_i21.WelcomeView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.WelcomeView(),
settings: data,
);
},
_i22.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i22.AssessmentView(key: args.key, data: args.data),
settings: data,
@ -469,7 +455,7 @@ class StackedRouter extends _i1.RouterBase {
},
_i23.LearnLessonView: (data) {
final args = data.getArgs<LearnLessonViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i23.LearnLessonView(
key: args.key,
title: args.title,
@ -481,14 +467,14 @@ class StackedRouter extends _i1.RouterBase {
);
},
_i24.ForgetPasswordView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.ForgetPasswordView(),
settings: data,
);
},
_i25.LearnLessonDetailView: (data) {
final args = data.getArgs<LearnLessonDetailViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i25.LearnLessonDetailView(
key: args.key,
title: args.title,
@ -499,7 +485,7 @@ class StackedRouter extends _i1.RouterBase {
},
_i26.LearnPracticeView: (data) {
final args = data.getArgs<LearnPracticeViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i26.LearnPracticeView(
key: args.key,
title: args.title,
@ -511,7 +497,7 @@ class StackedRouter extends _i1.RouterBase {
},
_i27.CoursePracticeView: (data) {
final args = data.getArgs<CoursePracticeViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i27.CoursePracticeView(key: args.key, id: args.id),
settings: data,
@ -519,21 +505,21 @@ class StackedRouter extends _i1.RouterBase {
},
_i28.CoursePaymentView: (data) {
final args = data.getArgs<CoursePaymentViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i28.CoursePaymentView(key: args.key, course: args.course),
settings: data,
);
},
_i29.CourseCategoryView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i29.CourseCategoryView(),
settings: data,
);
},
_i30.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i30.FailureView(key: args.key, label: args.label),
settings: data,
@ -541,7 +527,7 @@ class StackedRouter extends _i1.RouterBase {
},
_i31.CourseLessonView: (data) {
final args = data.getArgs<CourseLessonViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i31.CourseLessonView(key: args.key, course: args.course),
settings: data,
@ -549,21 +535,21 @@ class StackedRouter extends _i1.RouterBase {
},
_i32.CourseLessonDetailView: (data) {
final args = data.getArgs<CourseLessonDetailViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i32.CourseLessonDetailView(key: args.key, lesson: args.lesson),
settings: data,
);
},
_i33.DuolingoView: (data) {
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i33.DuolingoView(),
settings: data,
);
},
_i34.CourseSubcategoryView: (data) {
final args = data.getArgs<CourseSubcategoryViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i34.CourseSubcategoryView(key: args.key, category: args.category),
settings: data,
@ -571,7 +557,7 @@ class StackedRouter extends _i1.RouterBase {
},
_i35.CourseView: (data) {
final args = data.getArgs<CourseViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i35.CourseView(key: args.key, subcategory: args.subcategory),
settings: data,
@ -580,18 +566,12 @@ class StackedRouter extends _i1.RouterBase {
_i36.CoursePracticeQuestionView: (data) {
final args =
data.getArgs<CoursePracticeQuestionViewArguments>(nullOk: false);
return _i38.MaterialPageRoute<dynamic>(
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i36.CoursePracticeQuestionView(key: args.key, id: args.id),
settings: data,
);
},
_i37.LearnSubcategoryView: (data) {
return _i38.MaterialPageRoute<dynamic>(
builder: (context) => const _i37.LearnSubcategoryView(),
settings: data,
);
},
};
@override
@ -607,7 +587,7 @@ class StartupViewArguments {
this.label = 'Loading',
});
final _i38.Key? key;
final _i37.Key? key;
final String label;
@ -628,94 +608,13 @@ class StartupViewArguments {
}
}
class LearnViewArguments {
const LearnViewArguments({
this.key,
required this.id,
});
final _i38.Key? key;
final int id;
@override
String toString() {
return '{"key": "$key", "id": "$id"}';
}
@override
bool operator ==(covariant LearnViewArguments other) {
if (identical(this, other)) return true;
return other.key == key && other.id == id;
}
@override
int get hashCode {
return key.hashCode ^ id.hashCode;
}
}
class LearnLevelViewArguments {
const LearnLevelViewArguments({
this.key,
required this.id,
});
final _i38.Key? key;
final int id;
@override
String toString() {
return '{"key": "$key", "id": "$id"}';
}
@override
bool operator ==(covariant LearnLevelViewArguments other) {
if (identical(this, other)) return true;
return other.key == key && other.id == id;
}
@override
int get hashCode {
return key.hashCode ^ id.hashCode;
}
}
class LearnModuleViewArguments {
const LearnModuleViewArguments({
this.key,
required this.level,
});
final _i38.Key? key;
final _i39.Level level;
@override
String toString() {
return '{"key": "$key", "level": "$level"}';
}
@override
bool operator ==(covariant LearnModuleViewArguments other) {
if (identical(this, other)) return true;
return other.key == key && other.level == level;
}
@override
int get hashCode {
return key.hashCode ^ level.hashCode;
}
}
class AssessmentViewArguments {
const AssessmentViewArguments({
this.key,
required this.data,
});
final _i38.Key? key;
final _i37.Key? key;
final Map<String, dynamic> data;
@ -746,7 +645,7 @@ class LearnLessonViewArguments {
required this.description,
});
final _i38.Key? key;
final _i37.Key? key;
final String title;
@ -793,7 +692,7 @@ class LearnLessonDetailViewArguments {
required this.description,
});
final _i38.Key? key;
final _i37.Key? key;
final String title;
@ -833,7 +732,7 @@ class LearnPracticeViewArguments {
required this.buttonLabel,
});
final _i38.Key? key;
final _i37.Key? key;
final String title;
@ -874,7 +773,7 @@ class CoursePracticeViewArguments {
required this.id,
});
final _i38.Key? key;
final _i37.Key? key;
final int id;
@ -901,9 +800,9 @@ class CoursePaymentViewArguments {
required this.course,
});
final _i38.Key? key;
final _i37.Key? key;
final _i40.Course course;
final _i38.Course course;
@override
String toString() {
@ -928,7 +827,7 @@ class FailureViewArguments {
required this.label,
});
final _i38.Key? key;
final _i37.Key? key;
final String label;
@ -955,9 +854,9 @@ class CourseLessonViewArguments {
required this.course,
});
final _i38.Key? key;
final _i37.Key? key;
final _i40.Course course;
final _i38.Course course;
@override
String toString() {
@ -982,9 +881,9 @@ class CourseLessonDetailViewArguments {
required this.lesson,
});
final _i38.Key? key;
final _i37.Key? key;
final _i41.CourseLesson lesson;
final _i39.CourseLesson lesson;
@override
String toString() {
@ -1009,9 +908,9 @@ class CourseSubcategoryViewArguments {
required this.category,
});
final _i38.Key? key;
final _i37.Key? key;
final _i42.Category category;
final _i40.CourseCategory category;
@override
String toString() {
@ -1036,9 +935,9 @@ class CourseViewArguments {
required this.subcategory,
});
final _i38.Key? key;
final _i37.Key? key;
final _i43.Subcategory subcategory;
final _i41.CourseSubcategory subcategory;
@override
String toString() {
@ -1063,7 +962,7 @@ class CoursePracticeQuestionViewArguments {
required this.id,
});
final _i38.Key? key;
final _i37.Key? key;
final int id;
@ -1084,7 +983,7 @@ class CoursePracticeQuestionViewArguments {
}
}
extension NavigatorStateExtension on _i44.NavigationService {
extension NavigatorStateExtension on _i42.NavigationService {
Future<dynamic> navigateToHomeView([
int? routerId,
bool preventDuplicates = true,
@ -1114,7 +1013,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToStartupView({
_i38.Key? key,
_i37.Key? key,
String label = 'Loading',
int? routerId,
bool preventDuplicates = true,
@ -1312,51 +1211,42 @@ extension NavigatorStateExtension on _i44.NavigationService {
transition: transition);
}
Future<dynamic> navigateToLearnView({
_i38.Key? key,
required int id,
Future<dynamic> navigateToLearnView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
]) async {
return navigateTo<dynamic>(Routes.learnView,
arguments: LearnViewArguments(key: key, id: id),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnLevelView({
_i38.Key? key,
required int id,
Future<dynamic> navigateToLearnLevelView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
]) async {
return navigateTo<dynamic>(Routes.learnLevelView,
arguments: LearnLevelViewArguments(key: key, id: id),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnModuleView({
_i38.Key? key,
required _i39.Level level,
Future<dynamic> navigateToLearnModuleView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
]) async {
return navigateTo<dynamic>(Routes.learnModuleView,
arguments: LearnModuleViewArguments(key: key, level: level),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
@ -1378,7 +1268,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToAssessmentView({
_i38.Key? key,
_i37.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -1395,7 +1285,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToLearnLessonView({
_i38.Key? key,
_i37.Key? key,
required String title,
required String topics,
required String subtitle,
@ -1436,7 +1326,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToLearnLessonDetailView({
_i38.Key? key,
_i37.Key? key,
required String title,
required List<Map<String, dynamic>> practices,
required String description,
@ -1459,7 +1349,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToLearnPracticeView({
_i38.Key? key,
_i37.Key? key,
required String title,
required String subtitle,
required List<Map<String, dynamic>> practices,
@ -1484,7 +1374,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToCoursePracticeView({
_i38.Key? key,
_i37.Key? key,
required int id,
int? routerId,
bool preventDuplicates = true,
@ -1501,8 +1391,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToCoursePaymentView({
_i38.Key? key,
required _i40.Course course,
_i37.Key? key,
required _i38.Course course,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -1532,7 +1422,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToFailureView({
_i38.Key? key,
_i37.Key? key,
required String label,
int? routerId,
bool preventDuplicates = true,
@ -1549,8 +1439,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToCourseLessonView({
_i38.Key? key,
required _i40.Course course,
_i37.Key? key,
required _i38.Course course,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -1566,8 +1456,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToCourseLessonDetailView({
_i38.Key? key,
required _i41.CourseLesson lesson,
_i37.Key? key,
required _i39.CourseLesson lesson,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -1597,8 +1487,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToCourseSubcategoryView({
_i38.Key? key,
required _i42.Category category,
_i37.Key? key,
required _i40.CourseCategory category,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -1614,8 +1504,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToCourseView({
_i38.Key? key,
required _i43.Subcategory subcategory,
_i37.Key? key,
required _i41.CourseSubcategory subcategory,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -1631,7 +1521,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> navigateToCoursePracticeQuestionView({
_i38.Key? key,
_i37.Key? key,
required int id,
int? routerId,
bool preventDuplicates = true,
@ -1647,20 +1537,6 @@ extension NavigatorStateExtension on _i44.NavigationService {
transition: transition);
}
Future<dynamic> navigateToLearnSubcategoryView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnSubcategoryView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([
int? routerId,
bool preventDuplicates = true,
@ -1690,7 +1566,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithStartupView({
_i38.Key? key,
_i37.Key? key,
String label = 'Loading',
int? routerId,
bool preventDuplicates = true,
@ -1888,51 +1764,42 @@ extension NavigatorStateExtension on _i44.NavigationService {
transition: transition);
}
Future<dynamic> replaceWithLearnView({
_i38.Key? key,
required int id,
Future<dynamic> replaceWithLearnView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
]) async {
return replaceWith<dynamic>(Routes.learnView,
arguments: LearnViewArguments(key: key, id: id),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnLevelView({
_i38.Key? key,
required int id,
Future<dynamic> replaceWithLearnLevelView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
]) async {
return replaceWith<dynamic>(Routes.learnLevelView,
arguments: LearnLevelViewArguments(key: key, id: id),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnModuleView({
_i38.Key? key,
required _i39.Level level,
Future<dynamic> replaceWithLearnModuleView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
]) async {
return replaceWith<dynamic>(Routes.learnModuleView,
arguments: LearnModuleViewArguments(key: key, level: level),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
@ -1954,7 +1821,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithAssessmentView({
_i38.Key? key,
_i37.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -1971,7 +1838,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithLearnLessonView({
_i38.Key? key,
_i37.Key? key,
required String title,
required String topics,
required String subtitle,
@ -2012,7 +1879,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithLearnLessonDetailView({
_i38.Key? key,
_i37.Key? key,
required String title,
required List<Map<String, dynamic>> practices,
required String description,
@ -2035,7 +1902,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithLearnPracticeView({
_i38.Key? key,
_i37.Key? key,
required String title,
required String subtitle,
required List<Map<String, dynamic>> practices,
@ -2060,7 +1927,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithCoursePracticeView({
_i38.Key? key,
_i37.Key? key,
required int id,
int? routerId,
bool preventDuplicates = true,
@ -2077,8 +1944,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithCoursePaymentView({
_i38.Key? key,
required _i40.Course course,
_i37.Key? key,
required _i38.Course course,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2108,7 +1975,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithFailureView({
_i38.Key? key,
_i37.Key? key,
required String label,
int? routerId,
bool preventDuplicates = true,
@ -2125,8 +1992,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithCourseLessonView({
_i38.Key? key,
required _i40.Course course,
_i37.Key? key,
required _i38.Course course,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2142,8 +2009,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithCourseLessonDetailView({
_i38.Key? key,
required _i41.CourseLesson lesson,
_i37.Key? key,
required _i39.CourseLesson lesson,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2173,8 +2040,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithCourseSubcategoryView({
_i38.Key? key,
required _i42.Category category,
_i37.Key? key,
required _i40.CourseCategory category,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2190,8 +2057,8 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithCourseView({
_i38.Key? key,
required _i43.Subcategory subcategory,
_i37.Key? key,
required _i41.CourseSubcategory subcategory,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2207,7 +2074,7 @@ extension NavigatorStateExtension on _i44.NavigationService {
}
Future<dynamic> replaceWithCoursePracticeQuestionView({
_i38.Key? key,
_i37.Key? key,
required int id,
int? routerId,
bool preventDuplicates = true,
@ -2222,18 +2089,4 @@ extension NavigatorStateExtension on _i44.NavigationService {
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnSubcategoryView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnSubcategoryView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
}

View File

@ -50,23 +50,21 @@ class DefaultFirebaseOptions {
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAi8e4PMFH8QQU11DWftHdNEu8WUP7i2ww',
appId: '1:900714037062:android:f11e3b69315b05304e6f47',
messagingSenderId: '900714037062',
projectId: 'yimaru-academy-5e7e2',
storageBucket: 'yimaru-academy-5e7e2.firebasestorage.app',
apiKey: 'AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk',
appId: '1:574860813475:android:cd7fa6cf3a0527d97acb16',
messagingSenderId: '574860813475',
projectId: 'yimaru-lms-e834e',
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDbaGD47oUJOyn9n3b0pbH6ozmbGyIlOKk',
appId: '1:900714037062:ios:1caf8f24a4333b8e4e6f47',
messagingSenderId: '900714037062',
projectId: 'yimaru-academy-5e7e2',
storageBucket: 'yimaru-academy-5e7e2.firebasestorage.app',
apiKey: 'AIzaSyBBcQ17JB6RBTjD7G7mh6Xf_FMUGxP5cC8',
appId: '1:574860813475:ios:3ac9f7c4ae1771287acb16',
messagingSenderId: '574860813475',
projectId: 'yimaru-lms-e834e',
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
androidClientId:
'900714037062-ngc0gc426sfnnjjr494g4vni46ne5uqv.apps.googleusercontent.com',
iosClientId:
'900714037062-35bg0hsou56hg37mbcbpiar9uti7tcku.apps.googleusercontent.com',
'574860813475-3p3k63lkrfd113sn6jscgvdj0aigsg5s.apps.googleusercontent.com',
iosBundleId: 'com.yimaru.lms.app',
);
}

View File

@ -1,23 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'category.g.dart';
@JsonSerializable()
class Category {
final int? id;
final String? name;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'total_count')
final int? totalCount;
const Category({this.id, this.name, this.isActive, this.totalCount});
factory Category.fromJson(Map<String, dynamic> json) =>
_$CategoryFromJson(json);
Map<String, dynamic> toJson() => _$CategoryToJson(this);
}

View File

@ -6,38 +6,32 @@ part 'course.g.dart';
class Course {
final int? id;
final String? level;
final String? title;
final String? thumbnail;
final String? description;
@JsonKey(name: 'course_id')
final int? courseId;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'category_id')
final int? categoryId;
@JsonKey(name: 'display_order')
final int? displayOrder;
@JsonKey(name: 'total_count')
final int? totalCount;
@JsonKey(name: 'intro_video_url')
final String? introVideoUrl;
@JsonKey(name: 'sub_category_id')
final int? subCategoryId;
const Course({
this.id,
this.title,
this.isActive,
this.thumbnail,
this.totalCount,
this.categoryId,
this.description,
this.introVideoUrl,
this.subCategoryId,
});
const Course(
{this.id,
this.level,
this.title,
this.isActive,
this.courseId,
this.thumbnail,
this.description,
this.displayOrder});
factory Course.fromJson(Map<String, dynamic> json) => _$CourseFromJson(json);

View File

@ -8,24 +8,22 @@ part of 'course.dart';
Course _$CourseFromJson(Map<String, dynamic> json) => Course(
id: (json['id'] as num?)?.toInt(),
level: json['level'] as String?,
title: json['title'] as String?,
isActive: json['is_active'] as bool?,
courseId: (json['course_id'] as num?)?.toInt(),
thumbnail: json['thumbnail'] as String?,
totalCount: (json['total_count'] as num?)?.toInt(),
categoryId: (json['category_id'] as num?)?.toInt(),
description: json['description'] as String?,
introVideoUrl: json['intro_video_url'] as String?,
subCategoryId: (json['sub_category_id'] as num?)?.toInt(),
displayOrder: (json['display_order'] as num?)?.toInt(),
);
Map<String, dynamic> _$CourseToJson(Course instance) => <String, dynamic>{
'id': instance.id,
'level': instance.level,
'title': instance.title,
'thumbnail': instance.thumbnail,
'description': instance.description,
'course_id': instance.courseId,
'is_active': instance.isActive,
'category_id': instance.categoryId,
'total_count': instance.totalCount,
'intro_video_url': instance.introVideoUrl,
'sub_category_id': instance.subCategoryId,
'display_order': instance.displayOrder,
};

View File

@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
part 'course_category.g.dart';
@JsonSerializable()
class CourseCategory {
final int? id;
final String? name;
@JsonKey(name: 'is_active')
final bool? isActive;
const CourseCategory({this.id, this.name, this.isActive});
factory CourseCategory.fromJson(Map<String, dynamic> json) =>
_$CourseCategoryFromJson(json);
Map<String, dynamic> toJson() => _$CourseCategoryToJson(this);
}

View File

@ -1,21 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'category.dart';
part of 'course_category.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Category _$CategoryFromJson(Map<String, dynamic> json) => Category(
CourseCategory _$CourseCategoryFromJson(Map<String, dynamic> json) =>
CourseCategory(
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
isActive: json['is_active'] as bool?,
totalCount: (json['total_count'] as num?)?.toInt(),
);
Map<String, dynamic> _$CategoryToJson(Category instance) => <String, dynamic>{
Map<String, dynamic> _$CourseCategoryToJson(CourseCategory instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'is_active': instance.isActive,
'total_count': instance.totalCount,
};

View File

@ -33,8 +33,8 @@ Map<String, dynamic> _$CourseLessonToJson(CourseLesson instance) =>
'visibility': instance.visibility,
'description': instance.description,
'video_url': instance.videoUrl,
'vimeo_status': instance.vimeoStatus,
'instructor_id': instance.instructorId,
'sub_course_id': instance.courseId,
'vimeo_status': instance.vimeoStatus,
'display_order': instance.displayOrder,
};

View File

@ -0,0 +1,33 @@
import 'package:json_annotation/json_annotation.dart';
part 'course_subcategory.g.dart';
@JsonSerializable()
class CourseSubcategory {
final int? id;
final String? title;
final String? thumbnail;
final String? description;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'category_id')
final int? categoryId;
const CourseSubcategory(
{this.id,
this.title,
this.isActive,
this.thumbnail,
this.categoryId,
this.description});
factory CourseSubcategory.fromJson(Map<String, dynamic> json) =>
_$CourseSubcategoryFromJson(json);
Map<String, dynamic> toJson() => _$CourseSubcategoryToJson(this);
}

View File

@ -1,29 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'level.dart';
part of 'course_subcategory.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Level _$LevelFromJson(Map<String, dynamic> json) => Level(
CourseSubcategory _$CourseSubcategoryFromJson(Map<String, dynamic> json) =>
CourseSubcategory(
id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?,
isActive: json['is_active'] as bool?,
cerfLevel: json['cefr_level'] as String?,
thumbnail: json['thumbnail'] as String?,
totalCount: (json['total_count'] as num?)?.toInt(),
categoryId: (json['category_id'] as num?)?.toInt(),
description: json['description'] as String?,
displayOrder: (json['display_order'] as num?)?.toInt(),
);
Map<String, dynamic> _$LevelToJson(Level instance) => <String, dynamic>{
Map<String, dynamic> _$CourseSubcategoryToJson(CourseSubcategory instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'thumbnail': instance.thumbnail,
'description': instance.description,
'is_active': instance.isActive,
'cefr_level': instance.cerfLevel,
'total_count': instance.totalCount,
'display_order': instance.displayOrder,
'category_id': instance.categoryId,
};

View File

@ -1,41 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'level.g.dart';
@JsonSerializable()
class Level {
final int? id;
final String? title;
final String? thumbnail;
final String? description;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'cefr_level')
final String? cerfLevel;
@JsonKey(name: 'total_count')
final int? totalCount;
@JsonKey(name: 'display_order')
final int? displayOrder;
const Level({
this.id,
this.title,
this.isActive,
this.cerfLevel,
this.thumbnail,
this.totalCount,
this.description,
this.displayOrder,
});
factory Level.fromJson(Map<String, dynamic> json) => _$LevelFromJson(json);
Map<String, dynamic> toJson() => _$LevelToJson(this);
}

View File

@ -1,38 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'module.g.dart';
@JsonSerializable()
class Module {
final int? id;
final String? title;
final String? description;
@JsonKey(name: 'icon_url')
final String? iconUrl;
@JsonKey(name: 'level_id')
final int? levelId;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'display_order')
final int? displayOrder;
const Module({
this.id,
this.title,
this.iconUrl,
this.levelId,
this.isActive,
this.description,
this.displayOrder,
});
factory Module.fromJson(Map<String, dynamic> json) => _$ModuleFromJson(json);
Map<String, dynamic> toJson() => _$ModuleToJson(this);
}

View File

@ -1,27 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'module.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Module _$ModuleFromJson(Map<String, dynamic> json) => Module(
id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?,
iconUrl: json['icon_url'] as String?,
levelId: (json['level_id'] as num?)?.toInt(),
isActive: json['is_active'] as bool?,
description: json['description'] as String?,
displayOrder: (json['display_order'] as num?)?.toInt(),
);
Map<String, dynamic> _$ModuleToJson(Module instance) => <String, dynamic>{
'id': instance.id,
'title': instance.title,
'description': instance.description,
'icon_url': instance.iconUrl,
'level_id': instance.levelId,
'is_active': instance.isActive,
'display_order': instance.displayOrder,
};

View File

@ -14,6 +14,6 @@ Option _$OptionFromJson(Map<String, dynamic> json) => Option(
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
'id': instance.id,
'is_correct': instance.isCorrect,
'option_text': instance.optionText,
'is_correct': instance.isCorrect,
};

View File

@ -1,42 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'subcategory.g.dart';
@JsonSerializable()
class Subcategory {
final int? id;
final String? name;
final String? description;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'total_count')
final int? totalCount;
@JsonKey(name: 'category_id')
final int? categoryId;
@JsonKey(name: 'category_name')
final String? categoryName;
@JsonKey(name: 'display_order')
final int? displayOrder;
const Subcategory(
{this.id,
this.name,
this.isActive,
this.totalCount,
this.categoryId,
this.description,
this.categoryName,
this.displayOrder});
factory Subcategory.fromJson(Map<String, dynamic> json) =>
_$SubcategoryFromJson(json);
Map<String, dynamic> toJson() => _$SubcategoryToJson(this);
}

View File

@ -1,30 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'subcategory.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Subcategory _$SubcategoryFromJson(Map<String, dynamic> json) => Subcategory(
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
isActive: json['is_active'] as bool?,
totalCount: (json['total_count'] as num?)?.toInt(),
categoryId: (json['category_id'] as num?)?.toInt(),
description: json['description'] as String?,
categoryName: json['category_name'] as String?,
displayOrder: (json['display_order'] as num?)?.toInt(),
);
Map<String, dynamic> _$SubcategoryToJson(Subcategory instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'description': instance.description,
'is_active': instance.isActive,
'total_count': instance.totalCount,
'category_id': instance.categoryId,
'category_name': instance.categoryName,
'display_order': instance.displayOrder,
};

View File

@ -1,9 +1,9 @@
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
part 'user_model.g.dart';
@JsonSerializable()
class User {
class UserModel {
final String? email;
final String? gender;
@ -40,7 +40,7 @@ class User {
@JsonKey(name: 'profile_picture_url')
final String? profilePicture;
const User({
const UserModel({
this.email,
this.region,
this.gender,
@ -57,7 +57,7 @@ class User {
this.profileCompleted,
});
User copyWith(
UserModel copyWith(
{int? userId,
String? email,
String? gender,
@ -72,7 +72,7 @@ class User {
bool? userInfoLoaded,
bool? profileCompleted,
String? profilePicture}) =>
User(
UserModel(
email: email ?? this.email,
userId: userId ?? this.userId,
gender: gender ?? this.gender,
@ -88,7 +88,8 @@ class User {
profilePicture: profilePicture ?? this.profilePicture,
profileCompleted: profileCompleted ?? this.profileCompleted);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
}

View File

@ -1,12 +1,12 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
part of 'user_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
User _$UserFromJson(Map<String, dynamic> json) => User(
UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
email: json['email'] as String?,
region: json['region'] as String?,
gender: json['gender'] as String?,
@ -23,7 +23,7 @@ User _$UserFromJson(Map<String, dynamic> json) => User(
profileCompleted: json['profile_completed'] as bool?,
);
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'email': instance.email,
'gender': instance.gender,
'region': instance.region,

View File

@ -1,19 +1,17 @@
import 'package:dio/dio.dart';
import 'package:yimaru_app/models/level.dart';
import 'package:yimaru_app/models/question.dart';
import 'package:yimaru_app/models/subcategory.dart';
import 'package:yimaru_app/models/category.dart';
import 'package:yimaru_app/models/course_subcategory.dart';
import 'package:yimaru_app/models/course_category.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/practice_question.dart';
import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import '../app/app.locator.dart';
import '../models/module.dart';
import '../ui/common/enmus.dart';
class ApiService {
@ -60,7 +58,7 @@ class ApiService {
return {
'status': ResponseStatus.success,
'message': 'Logged in successfully',
'data': User.fromJson(response.data['data']),
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
@ -88,7 +86,7 @@ class ApiService {
return {
'status': ResponseStatus.success,
'message': 'Logged in successfully',
'data': User.fromJson(response.data['data']),
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
@ -115,7 +113,7 @@ class ApiService {
return {
'status': ResponseStatus.success,
'message': 'Otp verified successfully',
'data': User.fromJson(response.data['data']),
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
@ -214,7 +212,7 @@ class ApiService {
}
// GEt profile completion status
Future<Map<String, dynamic>> getProfileStatus(User? user) async {
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
try {
Response response = await _service.dio.get(
'$kBaseUrl/$kUserBaseUrl/${user?.userId}/$kProfileStatusUrl',
@ -251,7 +249,7 @@ class ApiService {
return {
'status': ResponseStatus.success,
'message': 'Profile fetched successfully',
'data': User.fromJson(response.data['data']),
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
@ -371,20 +369,20 @@ class ApiService {
}
}
// Get categories
Future<List<Category>> getCategories() async {
// Get course categories
Future<List<CourseCategory>> getCourseCategories() async {
try {
List<Category> categories = [];
List<CourseCategory> categories = [];
final Response response = await _service.dio.get(
'$kBaseUrl/api/$kApiVersionUrl/$kCourseManagementUrl/$kCategoryUrl');
final Response response = await _service.dio
.get('$kBaseUrl/$kCourseBaseUrl/$kCourseCategoryUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['categories'] as List;
categories = decodedData.map(
(e) {
return Category.fromJson(e);
return CourseCategory.fromJson(e);
},
).toList();
return categories;
@ -396,19 +394,19 @@ class ApiService {
}
// Get course subcategory
Future<List<Subcategory>> getSubcategories(int id) async {
Future<List<CourseSubcategory>> getCourseSubcategories(int id) async {
try {
List<Subcategory> subcategories = [];
List<CourseSubcategory> subcategories = [];
final Response response = await _service.dio
.get('$kBaseUrl/$kCourseBaseUrl/$kCategoryUrl/$id/$kCoursesUrl');
final Response response = await _service.dio.get(
'$kBaseUrl/$kCourseBaseUrl/$kCourseCategoryUrl/$id/$kCoursesUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['courses'] as List;
subcategories = decodedData.map(
(e) {
return Subcategory.fromJson(e);
return CourseSubcategory.fromJson(e);
},
).toList();
return subcategories;
@ -420,28 +418,28 @@ class ApiService {
}
// 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 [];
// }
// }
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 {
@ -578,100 +576,4 @@ class ApiService {
return null;
}
}
// Get learn subcategories
Future<List<Subcategory>> getLearnSubcategories() async {
try {
List<Subcategory> 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 Subcategory.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 [];
}
}
}

View File

@ -1,6 +1,6 @@
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
class AuthenticationService with ListenableServiceMixin {
@ -8,9 +8,9 @@ class AuthenticationService with ListenableServiceMixin {
final _secureService = locator<SecureStorageService>();
// User data
User? _user;
UserModel? _user;
User? get user => _user;
UserModel? get user => _user;
// Initialization
AuthenticationService() {
@ -51,7 +51,7 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('accessToken', data['accessToken']);
await _secureService.setString('refreshToken', data['refreshToken']);
_user = User(
_user = UserModel(
userId: await _secureService.getInt('userId'),
accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'),
@ -67,6 +67,7 @@ class AuthenticationService with ListenableServiceMixin {
profileCompleted: await _secureService.getBool('profileCompleted'),
);
notifyListeners();
}
@ -82,7 +83,7 @@ class AuthenticationService with ListenableServiceMixin {
}
// Save user data
Future<void> saveUserData(User data) async {
Future<void> saveUserData(UserModel data) async {
await _secureService.setBool('userInfoLoaded', true);
await _secureService.setBool(
'profileCompleted', data.profileCompleted ?? false);
@ -95,7 +96,7 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('firstName', data.firstName ?? '');
await _secureService.setString('occupation', data.occupation ?? '');
_user = User(
_user = UserModel(
email: data.email,
gender: data.gender,
region: data.region,
@ -147,8 +148,8 @@ class AuthenticationService with ListenableServiceMixin {
}
// Get user data
Future<User?> getUser() async {
_user = User(
Future<UserModel?> getUser() async {
_user = UserModel(
userId: await _secureService.getInt('userId'),
email: await _secureService.getString('email'),
region: await _secureService.getString('region'),

View File

@ -2,7 +2,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import '../app/app.locator.dart';
@ -135,7 +135,7 @@ class DioService {
// Refresh token
Future<bool> _refreshToken() async {
final User? user = await _authenticationService.getUser();
final UserModel? user = await _authenticationService.getUser();
if (user?.refreshToken == null) return false;

View File

@ -1,19 +1,9 @@
String kBaseUrl = 'https://api.yimaruacademy.com';
String kApiUrl = 'api';
String kApiVersionUrl = 'v1';
String kLevelsUrl = 'levels';
String kCoursesUrl = 'courses';
String kModulesUrl = 'modules';
String kRegisterUrl = 'register';
String kCategoryUrl = 'categories';
String kCoursePractice = 'by-owner';
String kUserBaseUrl = 'api/v1/user';
@ -30,9 +20,9 @@ String kCompleteLessonUrl = 'complete';
String kResetPassword = 'resetPassword';
String kRequestResetCode = 'sendResetCode';
String kCourseCategoryUrl = 'categories';
String kSubcategoriesUrl = 'sub-categories';
String kRequestResetCode = 'sendResetCode';
String kPublishedVideos = 'videos/published';
@ -46,8 +36,6 @@ String kLoginUrl = 'api/v1/auth/customer-login';
String kPracticeBaseUrl = 'api/v1/question-sets';
String kCourseManagementUrl = 'course-management';
String kProfileStatusUrl = 'is-profile-completed';
String kCourseBaseUrl = 'api/v1/course-management';
@ -62,12 +50,10 @@ String kCourseProgressUrl = 'api/v1/progress/courses';
String kAssessmentsUrl = 'api/v1/assessment/questions';
String kLearnSubcategoriesUrl = 'human-language/sub-categories';
String kEmptyImagePath = '/data/user/0/com.yimaru.lms.app/app_flutter';
String kSampleVideoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
String kServerClientId =
'900714037062-24ria5fcfet71o3vde8f6gsvsj1n68ec.apps.googleusercontent.com';
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';

View File

@ -12,6 +12,8 @@ const String ksHomeBottomSheetDescription =
const String ksPrivacyPolicy =
'A brief, simple overview of Yimarus commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.';
const String ksTerms = """
<p style="color:#9C2C91;font-size:13px;">
Last updated: October 26, 2025

View File

@ -27,9 +27,6 @@ enum StateObjects {
register,
verifyOtp,
resendOtp,
learnLevels,
learnModules,
learnCourses,
profileImage,
courseLessons,
profileUpdate,
@ -44,7 +41,6 @@ enum StateObjects {
courseCategories,
profileCompletion,
registerWithGoogle,
learnSubcategories,
learnPracticeSample,
learnPracticeAnswer,
loginWithPhoneNumber,

View File

@ -2,7 +2,6 @@ import 'dart:math';
import 'dart:ui';
import 'app_colors.dart';
// Split full name
Map<String, String> splitFullName(String fullName) {
final parts = fullName.trim().split(RegExp(r'\s+'));

View File

@ -127,8 +127,8 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) =>
CustomListTile(
title: 'Privacy Policy',
icon: Icons.shield_moon_outlined,
title: 'Privacy Policy',
onTap: () async => await viewModel.navigateToPrivacyPolicy(),
);

View File

@ -11,12 +11,12 @@ class AccountPrivacyViewModel extends BaseViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToTerms() async =>
await _navigationService.navigateToTermsAndConditionsView();
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> navigateToPrivacyPolicy() async =>
await _navigationService.navigateToPrivacyPolicyView();
Future<void> navigateToTerms() async =>
await _navigationService.navigateToTermsAndConditionsView();
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/assessment/screens/assessment_questions_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/Assessment_form_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/assessment_intro_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/assessment_result_screen.dart';
import 'package:yimaru_app/ui/views/assessment/screens/start_lesson_screen.dart';
@ -51,7 +51,7 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
Widget _buildAssessmentIntro() => const AssessmentIntroScreen();
Widget _buildAssessment() => const AssessmentQuestionsScreen();
Widget _buildAssessment() => const AssessmentFormScreen();
Widget _buildAssessmentResult() => const AssessmentResultScreen();

View File

@ -37,14 +37,14 @@ class AssessmentViewModel extends BaseViewModel {
int get currentQuestion => _currentQuestion;
List<Question> _assessments = [];
List<Question> get assessments => _assessments;
ProficiencyLevels _proficiencyLevel = ProficiencyLevels.none;
ProficiencyLevels get proficiencyLevel => _proficiencyLevel;
List<Question> _assessments = [];
List<Question> get assessments => _assessments;
final Map<String, dynamic> _selectedAnswers = {};
Map<String, dynamic> get selectedAnswers => _selectedAnswers;
@ -55,6 +55,20 @@ class AssessmentViewModel extends BaseViewModel {
Map<String, dynamic> get userData => _userData;
// Assessment
int countCorrectAnswersUntil(int untilQuestion) {
int count = 0;
for (int i = 1; i <= untilQuestion; i++) {
final answer = _selectedAnswers[i.toString()];
if (answer is Map<String, dynamic> && answer['correct'] == true) {
count++;
}
}
return count;
}
Map<String, dynamic> evaluateAssessment() {
if (_currentQuestion == 5) {
// A1
@ -97,24 +111,6 @@ class AssessmentViewModel extends BaseViewModel {
}
}
int countCorrectAnswersUntil(int untilQuestion) {
int count = 0;
for (int i = 1; i <= untilQuestion; i++) {
final answer = _selectedAnswers[i.toString()];
if (answer is Map<String, dynamic> && answer['correct'] == true) {
count++;
}
}
return count;
}
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) {
@ -137,18 +133,43 @@ class AssessmentViewModel extends BaseViewModel {
rebuildUi();
}
// User data
void clearUserData() {
_userData.clear();
bool isSelectedAnswer({required int question, required String answer}) {
return _selectedAnswers[question.toString()]?['option'] == answer;
}
// Add user data
void initUserData(Map<String, dynamic> data) {
clearUserData();
_userData.addAll(data);
}
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void initUserData(Map<String, dynamic> data) {
clearUserData();
_userData.addAll(data);
void clearUserData() {
_userData.clear();
}
// Dialog
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(
cancelTitle: 'No',
buttonTitle: 'Yes',
barrierDismissible: true,
title: 'Abort Assessment',
cancelTitleColor: kcDarkGrey,
buttonTitleColor: kcPrimaryColor,
description: 'Are you sure to abort the assessment ?',
);
return response?.confirmed;
}
Future<void> abort() async {
bool? response = await showAbortDialog();
if (response != null && response) {
next(page: 3);
}
}
// Question navigation
@ -185,6 +206,19 @@ class AssessmentViewModel extends BaseViewModel {
}
// In-app navigation
void next({int? page}) async {
if (page == null) {
if (_previousPage != 0) {
_currentPage = _previousPage;
} else {
_currentPage++;
}
} else {
_previousPage = _currentPage;
_currentPage = page;
}
rebuildUi();
}
void goBack() {
if (_currentPage == 0) {
@ -202,51 +236,23 @@ class AssessmentViewModel extends BaseViewModel {
}
}
void next({int? page}) async {
if (page == null) {
if (_previousPage != 0) {
_currentPage = _previousPage;
} else {
_currentPage++;
}
} else {
_previousPage = _currentPage;
_currentPage = page;
}
rebuildUi();
}
// Dialog
Future<void> abort() async {
bool? response = await showAbortDialog();
if (response != null && response) {
next(page: 3);
}
}
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(
cancelTitle: 'No',
buttonTitle: 'Yes',
barrierDismissible: true,
title: 'Abort Assessment',
cancelTitleColor: kcDarkGrey,
buttonTitleColor: kcPrimaryColor,
description: 'Are you sure to abort the assessment ?',
);
return response?.confirmed;
}
// Navigation
void pop() => _navigationService.back();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShow(Routes.homeView);
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShow(Routes.homeView);
// Remote api call
Future<void> getAssessments() async => await runBusyFuture(_getAssessments());
Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) {
_assessments = await _apiService.getAssessments();
}
}
// Complete profile
Future<void> completeProfile() async =>
@ -266,13 +272,4 @@ class AssessmentViewModel extends BaseViewModel {
}
}
}
// Assessments
Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) {
_assessments = await _apiService.getAssessments();
}
}
Future<void> getAssessments() async => await runBusyFuture(_getAssessments());
}

View File

@ -9,8 +9,8 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../assessment_viewmodel.dart';
import 'assessment_loading_screen.dart';
class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
const AssessmentQuestionsScreen({super.key});
class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
const AssessmentFormScreen({super.key});
@override
Widget build(BuildContext context, AssessmentViewModel viewModel) =>
@ -109,14 +109,14 @@ class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
physics: const NeverScrollableScrollPhysics(),
itemCount: viewModel.assessments[index].options?.length,
itemBuilder: (context, inner) => _buildAnswer(
onTap: () => viewModel.setSelectedAnswer(
question: index + 1,
option: viewModel.assessments[index].options?[inner]),
title: viewModel.assessments[index].options?[inner].optionText ?? '',
selected: viewModel.isSelectedAnswer(
question: index + 1,
answer: viewModel.assessments[index].options?[inner].optionText ??
''),
onTap: () => viewModel.setSelectedAnswer(
question: index + 1,
option: viewModel.assessments[index].options?[inner]),
),
);

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../../models/course_detail.dart';
import '../../../models/subcategory.dart';
import '../../../models/course_subcategory.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
@ -12,7 +12,7 @@ import '../../widgets/small_app_bar.dart';
import 'course_viewmodel.dart';
class CourseView extends StackedView<CourseViewModel> {
final Subcategory subcategory;
final CourseSubcategory subcategory;
const CourseView({Key? key, required this.subcategory}) : super(key: key);
@ -83,12 +83,12 @@ class CourseView extends StackedView<CourseViewModel> {
];
Widget _buildTitle() => Text(
'${subcategory.name ?? ''} courses',
'${subcategory.title ?? ''} courses',
style: style18DG700,
);
Widget _buildSubtitle() => Text(
'Explore variety of courses on ${subcategory.name ?? ''}.',
'Explore variety of courses on ${subcategory.title ?? ''}.',
style: style14DG400,
);
@ -105,7 +105,6 @@ class CourseView extends StackedView<CourseViewModel> {
shrinkWrap: true,
itemCount: viewModel.courseDetail.length,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => verticalSpaceSmall,
itemBuilder: (context, index) => _buildTile(
courseDetail: viewModel.courseDetail[index],
onCourseTap: () async => await viewModel
@ -113,6 +112,7 @@ class CourseView extends StackedView<CourseViewModel> {
onPracticeTap: () async => await viewModel.navigateToCoursePractice(
viewModel.courseDetail[index].course?.id ?? 0),
),
separatorBuilder: (context, index) => verticalSpaceSmall,
);
Widget _buildTile({

View File

@ -11,6 +11,7 @@ import '../../common/enmus.dart';
class CourseViewModel extends BaseViewModel {
// Dependency injection
final _courseService = locator<CourseService>();
final _statusChecker = locator<StatusCheckerService>();
@ -25,15 +26,15 @@ class CourseViewModel extends BaseViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToCoursePractice(int id) =>
_navigationService.navigateToCoursePracticeView(id: id);
Future<void> navigateToCoursePayment(Course course) async =>
_navigationService.navigateToCoursePaymentView(course: course);
Future<void> navigateToCoursePractice(int id) =>
_navigationService.navigateToCoursePracticeView(id: id);
// Remote api call
// Course detail
// Sub course
Future<void> getCourseDetails(int id) async =>
await runBusyFuture(_getCourseDetails(id),
busyObject: StateObjects.courses);
@ -41,8 +42,8 @@ class CourseViewModel extends BaseViewModel {
Future<void> _getCourseDetails(int id) async {
if (await _statusChecker.checkConnection()) {
_courseDetail = await _courseService.getCoursesDetail(id);
// _courseDetail.sort((a, b) =>
// (a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0));
_courseDetail.sort((a, b) =>
(a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0));
}
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../../models/category.dart';
import '../../../models/course_category.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
@ -15,7 +15,7 @@ class CourseCategoryView extends StackedView<CourseCategoryViewModel> {
@override
void onViewModelReady(CourseCategoryViewModel viewModel) async {
await viewModel.getCategories();
await viewModel.getCourseCategories();
super.onViewModelReady(viewModel);
}
@ -114,7 +114,7 @@ class CourseCategoryView extends StackedView<CourseCategoryViewModel> {
//
Widget _buildTile({
required Category category,
required CourseCategory category,
required GestureTapCallback onTap,
}) =>
CourseCategoryCard(

View File

@ -3,8 +3,8 @@ import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/category.dart';
import '../../../models/user.dart';
import '../../../models/course_category.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/status_checker_service.dart';
@ -25,30 +25,31 @@ class CourseCategoryViewModel extends ReactiveViewModel {
[_authenticationService];
// Current user
User? get _user => _authenticationService.user;
UserModel? get _user => _authenticationService.user;
User? get user => _user;
UserModel? get user => _user;
// Course categories
List<Category> _categories = [];
List<CourseCategory> _categories = [];
List<Category> get categories => _categories;
List<CourseCategory> get categories => _categories;
// Navigation
Future<void> navigateToCourseCategory(Category category) async =>
Future<void> navigateToCourseCategory(CourseCategory category) async =>
_navigationService.navigateToCourseSubcategoryView(category: category);
// Remote api call
// Course categories
Future<void> getCategories() async =>
await runBusyFuture(_getCategories(),
Future<void> getCourseCategories() async =>
await runBusyFuture(_getCourseCategories(),
busyObject: StateObjects.courseCategories);
Future<void> _getCategories() async {
Future<void> _getCourseCategories() async {
if (categories.isEmpty) {
if (await _statusChecker.checkConnection()) {
_categories = await _apiService.getCategories();
_categories = await _apiService.getCourseCategories();
}
}
}

View File

@ -76,8 +76,12 @@ class CourseLessonView extends StackedView<CourseLessonViewModel> {
children: _buildLessonColumnChildren(viewModel),
);
List<Widget> _buildLessonColumnChildren(CourseLessonViewModel viewModel) =>
[_buildTitle(), verticalSpaceMedium, _buildListViewBuilder(viewModel)];
List<Widget> _buildLessonColumnChildren(CourseLessonViewModel viewModel) => [
// verticalSpaceLarge,
_buildTitle(),
verticalSpaceMedium,
_buildListViewBuilder(viewModel)
];
Widget _buildTitle() => Text(
'${course.title} course lessons',

View File

@ -29,38 +29,6 @@ class CourseLessonDetailViewModel extends BaseViewModel {
VideoPlayerController? get videoPlayerController => _videoPlayerController;
// Video player
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
Future<void> pause() async {
await _chewieController?.pause();
}
Future<void> _videoListener(CourseLesson lesson) async {
final controller = _videoPlayerController;
if (controller == null || !controller.value.isInitialized) return;
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);
}
}
Future<void> initializePlayer(CourseLesson lesson) async =>
await runBusyFuture(_initializePlayer(lesson),
busyObject: StateObjects.loadCourseVideo);
@ -91,6 +59,26 @@ class CourseLessonDetailViewModel extends BaseViewModel {
rebuildUi();
}
Future<void> _videoListener(CourseLesson lesson) async {
final controller = _videoPlayerController;
if (controller == null || !controller.value.isInitialized) return;
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);
}
}
Future<void> _onVideoCompleted(CourseLesson lesson) async {
if (_isCompleted) return;
@ -101,6 +89,17 @@ class CourseLessonDetailViewModel extends BaseViewModel {
await _apiService.completeLesson(lesson.id ?? 0);
}
Future<void> pause() async {
await _chewieController?.pause();
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
// Navigation
void pop() => _navigationService.back();

View File

@ -95,17 +95,17 @@ class CoursePracticeView extends StackedView<CoursePracticeViewModel> {
shrinkWrap: true,
itemCount: viewModel.coursePractices.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildCard(
title: viewModel.coursePractices[index].title ?? '',
onTap: () async =>
await viewModel.navigateToCoursePracticeQuestion(viewModel.coursePractices[index].id ?? 0),
),
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({

View File

@ -56,80 +56,13 @@ class CoursePracticeQuestionViewModel extends FormViewModel {
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();
}
// Question
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) {
@ -151,29 +84,81 @@ class CoursePracticeQuestionViewModel extends FormViewModel {
rebuildUi();
}
bool isSelectedAnswer({required int question, required String answer}) {
return _selectedAnswers[question.toString()]?['option'] == answer;
}
// Dialog
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;
}
Future<void> abort() async {
bool? response = await showAbortDialog();
if (response != null && response) {
next(page: 1);
}
}
// Reset practice
void reset() {
_selectedAnswers.clear();
rebuildUi();
}
// Question navigation
void previousQuestion() {
if (_currentQuestionIndex != 0) {
_currentQuestionIndex--;
_pageController.previousPage(
duration: const Duration(microseconds: 100), curve: Curves.linear);
rebuildUi();
}
}
// In-app navigation
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();
}
void goBack() {
if (_currentPage == 0) {
pop();
} else {
_currentPage = 0;
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),
@ -200,4 +185,25 @@ class CoursePracticeQuestionViewModel extends FormViewModel {
_currentQuestion = await _apiService.getCoursePracticeQuestion(id);
}
}
// Question navigation
Future<void> nextQuestion(int id) async =>
await runBusyFuture(_nextQuestion(id),
busyObject: StateObjects.coursePractice);
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);
}
}
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../../models/category.dart';
import '../../../models/subcategory.dart';
import '../../../models/course_category.dart';
import '../../../models/course_subcategory.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
@ -12,14 +12,14 @@ import '../../widgets/small_app_bar.dart';
import 'course_subcategory_viewmodel.dart';
class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
final Category category;
final CourseCategory category;
const CourseSubcategoryView({Key? key, required this.category})
: super(key: key);
@override
void onViewModelReady(CourseSubcategoryViewModel viewModel) async {
await viewModel.getSubcategories(category.id ?? 0);
await viewModel.getCourseSubcategories(category.id ?? 0);
super.onViewModelReady(viewModel);
}
@ -121,7 +121,7 @@ class CourseSubcategoryView extends StackedView<CourseSubcategoryViewModel> {
Widget _buildTile({
GestureTapCallback? onCourseTap,
GestureTapCallback? onPracticeTap,
required Subcategory subcategory,
required CourseSubcategory subcategory,
}) =>
CourseSubcategoryTile(
subcategory: subcategory,

View File

@ -3,7 +3,7 @@ import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/subcategory.dart';
import '../../../models/course_subcategory.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
@ -16,10 +16,10 @@ class CourseSubcategoryViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Course subcategories
List<Subcategory> _subcategories = [];
// Courses
List<CourseSubcategory> _subcategories = [];
List<Subcategory> get subcategories => _subcategories;
List<CourseSubcategory> get subcategories => _subcategories;
// Navigation
void pop() => _navigationService.back();
@ -27,19 +27,20 @@ class CourseSubcategoryViewModel extends BaseViewModel {
Future<void> navigateToCoursePractice(int id) async =>
_navigationService.navigateToCoursePracticeView(id: id);
Future<void> navigateToSubcourse(Subcategory subcategory) async =>
Future<void> navigateToSubcourse(CourseSubcategory subcategory) async =>
_navigationService.navigateToCourseView(subcategory: subcategory);
// Remote api call
// Course subcategories
Future<void> getSubcategories(int id) async =>
await runBusyFuture(_getSubcategories(id),
// Courses
Future<void> getCourseSubcategories(int id) async =>
await runBusyFuture(_getCourseSubcategories(id),
busyObject: StateObjects.subcategories);
Future<void> _getSubcategories(int id) async {
Future<void> _getCourseSubcategories(int id) async {
if (await _statusChecker.checkConnection()) {
_subcategories = await _apiService.getSubcategories(id);
_subcategories = await _apiService.getCourseSubcategories(id);
}
}
}

View File

@ -169,32 +169,41 @@ class DuolingoViewModel extends FormViewModel {
List<String> get listeningAssessments => _listeningAssessments;
// Assessment
void setSpeakingState() {
_isSpeaking = !_isSpeaking;
rebuildUi();
}
// Topics
void setAssessmentFocus() {
_assessmentFocus = true;
rebuildUi();
}
// Speaking state
void setSpeakingState() {
_isSpeaking = !_isSpeaking;
rebuildUi();
}
// Set assessment
void setSelectedAssessment(
{required int page, required Map<String, dynamic> assessment}) {
_selectedAssessment = assessment;
goTo(page);
}
bool isSelectedListeningAssessment(String value) =>
_selectedListeningAssessment == value;
// Listening assessment
void setSelectedListeningAssessment(String value) {
_selectedListeningAssessment = value;
rebuildUi();
}
bool isSelectedListeningAssessment(String value) =>
_selectedListeningAssessment == value;
// In-app navigation
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
void goBack() {
if (_currentPage == 0) {
pop();
@ -204,11 +213,6 @@ class DuolingoViewModel extends FormViewModel {
}
}
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
// Navigation
void pop() => _navigationService.back();
}

View File

@ -105,8 +105,8 @@ class DuolingoRetakeScreen extends ViewModelWidget<DuolingoViewModel> {
height: 55,
safe: false,
borderRadius: 12,
text: 'Practice Again',
foregroundColor: kcWhite,
text: 'Practice Again',
onTap: () => viewModel.goTo(0),
backgroundColor: kcPrimaryColor,
);

View File

@ -89,11 +89,14 @@ class DuolingoSpeakingAssessment1Question
[_buildTimer(), _buildCountdownTime()];
Widget _buildTimer() => const CircularProgressIndicator(
constraints: BoxConstraints(
minWidth: 50,
minHeight: 50,
),
value: 1.0,
strokeWidth: 5,
color: kcPrimaryColor,
padding: EdgeInsets.zero,
constraints: BoxConstraints(minWidth: 50, minHeight: 50),
color: kcPrimaryColor,
);
Widget _buildCountdownTime() => Text(

View File

@ -9,9 +9,9 @@ import '../../common/validators/form_validator.dart';
import 'forget_password_viewmodel.dart';
@FormView(fields: [
FormTextField(name: 'password', validator: FormValidator.validateForm),
FormTextField(name: 'resetCode', validator: FormValidator.validateForm),
FormTextField(name: 'email', validator: FormValidator.validateEmailForm),
FormTextField(name: 'resetCode', validator: FormValidator.validateForm),
FormTextField(name: 'password', validator: FormValidator.validateForm),
FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm)
])
class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>

View File

@ -12,9 +12,9 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String PasswordValueKey = 'password';
const String ResetCodeValueKey = 'resetCode';
const String EmailValueKey = 'email';
const String ResetCodeValueKey = 'resetCode';
const String PasswordValueKey = 'password';
const String ConfirmPasswordValueKey = 'confirmPassword';
final Map<String, TextEditingController>
@ -24,25 +24,25 @@ final Map<String, FocusNode> _ForgetPasswordViewFocusNodes = {};
final Map<String, String? Function(String?)?>
_ForgetPasswordViewTextValidations = {
PasswordValueKey: FormValidator.validateForm,
ResetCodeValueKey: FormValidator.validateForm,
EmailValueKey: FormValidator.validateEmailForm,
ResetCodeValueKey: FormValidator.validateForm,
PasswordValueKey: FormValidator.validateForm,
ConfirmPasswordValueKey: FormValidator.validateForm,
};
mixin $ForgetPasswordView {
TextEditingController get passwordController =>
_getFormTextEditingController(PasswordValueKey);
TextEditingController get resetCodeController =>
_getFormTextEditingController(ResetCodeValueKey);
TextEditingController get emailController =>
_getFormTextEditingController(EmailValueKey);
TextEditingController get resetCodeController =>
_getFormTextEditingController(ResetCodeValueKey);
TextEditingController get passwordController =>
_getFormTextEditingController(PasswordValueKey);
TextEditingController get confirmPasswordController =>
_getFormTextEditingController(ConfirmPasswordValueKey);
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
FocusNode get resetCodeFocusNode => _getFormFocusNode(ResetCodeValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get resetCodeFocusNode => _getFormFocusNode(ResetCodeValueKey);
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
FocusNode get confirmPasswordFocusNode =>
_getFormFocusNode(ConfirmPasswordValueKey);
@ -70,9 +70,9 @@ mixin $ForgetPasswordView {
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
passwordController.addListener(() => _updateFormData(model));
resetCodeController.addListener(() => _updateFormData(model));
emailController.addListener(() => _updateFormData(model));
resetCodeController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
confirmPasswordController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
@ -85,9 +85,9 @@ mixin $ForgetPasswordView {
'This feature was deprecated after 3.1.0.',
)
void listenToFormUpdated(FormViewModel model) {
passwordController.addListener(() => _updateFormData(model));
resetCodeController.addListener(() => _updateFormData(model));
emailController.addListener(() => _updateFormData(model));
resetCodeController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
confirmPasswordController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
@ -98,9 +98,9 @@ mixin $ForgetPasswordView {
model.setData(
model.formValueMap
..addAll({
PasswordValueKey: passwordController.text,
ResetCodeValueKey: resetCodeController.text,
EmailValueKey: emailController.text,
ResetCodeValueKey: resetCodeController.text,
PasswordValueKey: passwordController.text,
ConfirmPasswordValueKey: confirmPasswordController.text,
}),
);
@ -143,20 +143,19 @@ extension ValueProperties on FormStateHelper {
return !hasAnyValidationMessage;
}
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
String? get resetCodeValue => this.formValueMap[ResetCodeValueKey] as String?;
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
String? get resetCodeValue => this.formValueMap[ResetCodeValueKey] as String?;
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
String? get confirmPasswordValue =>
this.formValueMap[ConfirmPasswordValueKey] as String?;
set passwordValue(String? value) {
set emailValue(String? value) {
this.setData(
this.formValueMap..addAll({PasswordValueKey: value}),
this.formValueMap..addAll({EmailValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(
PasswordValueKey)) {
_ForgetPasswordViewTextEditingControllers[PasswordValueKey]?.text =
if (_ForgetPasswordViewTextEditingControllers.containsKey(EmailValueKey)) {
_ForgetPasswordViewTextEditingControllers[EmailValueKey]?.text =
value ?? '';
}
}
@ -173,13 +172,14 @@ extension ValueProperties on FormStateHelper {
}
}
set emailValue(String? value) {
set passwordValue(String? value) {
this.setData(
this.formValueMap..addAll({EmailValueKey: value}),
this.formValueMap..addAll({PasswordValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(EmailValueKey)) {
_ForgetPasswordViewTextEditingControllers[EmailValueKey]?.text =
if (_ForgetPasswordViewTextEditingControllers.containsKey(
PasswordValueKey)) {
_ForgetPasswordViewTextEditingControllers[PasswordValueKey]?.text =
value ?? '';
}
}
@ -196,64 +196,64 @@ extension ValueProperties on FormStateHelper {
}
}
bool get hasPassword =>
this.formValueMap.containsKey(PasswordValueKey) &&
(passwordValue?.isNotEmpty ?? false);
bool get hasResetCode =>
this.formValueMap.containsKey(ResetCodeValueKey) &&
(resetCodeValue?.isNotEmpty ?? false);
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
bool get hasResetCode =>
this.formValueMap.containsKey(ResetCodeValueKey) &&
(resetCodeValue?.isNotEmpty ?? false);
bool get hasPassword =>
this.formValueMap.containsKey(PasswordValueKey) &&
(passwordValue?.isNotEmpty ?? false);
bool get hasConfirmPassword =>
this.formValueMap.containsKey(ConfirmPasswordValueKey) &&
(confirmPasswordValue?.isNotEmpty ?? false);
bool get hasPasswordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
bool get hasResetCodeValidationMessage =>
this.fieldsValidationMessages[ResetCodeValueKey]?.isNotEmpty ?? false;
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
bool get hasResetCodeValidationMessage =>
this.fieldsValidationMessages[ResetCodeValueKey]?.isNotEmpty ?? false;
bool get hasPasswordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
bool get hasConfirmPasswordValidationMessage =>
this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ??
false;
String? get passwordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey];
String? get resetCodeValidationMessage =>
this.fieldsValidationMessages[ResetCodeValueKey];
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
String? get resetCodeValidationMessage =>
this.fieldsValidationMessages[ResetCodeValueKey];
String? get passwordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey];
String? get confirmPasswordValidationMessage =>
this.fieldsValidationMessages[ConfirmPasswordValueKey];
}
extension Methods on FormStateHelper {
setPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
setResetCodeValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ResetCodeValueKey] = validationMessage;
setEmailValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
setResetCodeValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ResetCodeValueKey] = validationMessage;
setPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
setConfirmPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ConfirmPasswordValueKey] =
validationMessage;
/// Clears text input fields on the Form
void clearForm() {
passwordValue = '';
resetCodeValue = '';
emailValue = '';
resetCodeValue = '';
passwordValue = '';
confirmPasswordValue = '';
}
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
PasswordValueKey: getValidationMessage(PasswordValueKey),
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
EmailValueKey: getValidationMessage(EmailValueKey),
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
});
}
@ -274,8 +274,8 @@ String? getValidationMessage(String key) {
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
PasswordValueKey: getValidationMessage(PasswordValueKey),
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
EmailValueKey: getValidationMessage(EmailValueKey),
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
});

View File

@ -20,7 +20,7 @@ class ForgetPasswordViewModel extends FormViewModel {
Map<String, dynamic> get userData => _userData;
// In-app navigation
// Navigation
int _currentPage = 0;
int get currentPage => _currentPage;
@ -69,6 +69,15 @@ class ForgetPasswordViewModel extends FormViewModel {
bool get obscureConfirmPassword => _obscureConfirmPassword;
// Add user data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void clearUserData() {
_userData.clear();
}
// Email
void setEmailFocus() {
_focusEmail = true;
@ -81,37 +90,12 @@ class ForgetPasswordViewModel extends FormViewModel {
rebuildUi();
}
// User data
void clearUserData() {
_userData.clear();
}
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
// Password
void setPasswordFocus() {
_focusPassword = true;
rebuildUi();
}
void setObscurePassword() {
_obscurePassword = !_obscurePassword;
rebuildUi();
}
double validationProgress() {
int completed = 0;
if (_length) completed++;
if (_number) completed++;
if (_specialChar) completed++;
if (_passwordMatch) completed++;
return completed / 4; // returns 0.0 1.0
}
void validatePassword(
{required String password, required String confirmPassword}) {
if (password.length > 8) {
@ -140,6 +124,22 @@ class ForgetPasswordViewModel extends FormViewModel {
rebuildUi();
}
double validationProgress() {
int completed = 0;
if (_length) completed++;
if (_number) completed++;
if (_specialChar) completed++;
if (_passwordMatch) completed++;
return completed / 4; // returns 0.0 1.0
}
void setObscurePassword() {
_obscurePassword = !_obscurePassword;
rebuildUi();
}
// Confirm password
void setConfirmPasswordFocus() {
_focusConfirmPassword = true;
@ -172,6 +172,11 @@ class ForgetPasswordViewModel extends FormViewModel {
}
// In-app navigation
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
void goBack() {
if (_currentPage == 1) {
_currentPage = 0;
@ -181,11 +186,6 @@ class ForgetPasswordViewModel extends FormViewModel {
}
}
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
// Navigation
void pop() => _navigationService.back();
@ -194,23 +194,6 @@ class ForgetPasswordViewModel extends FormViewModel {
// Remote api calls
// Request reset code
Future<void> resetPassword() async => await runBusyFuture(_resetPassword(),
busyObject: StateObjects.resetPassword);
Future<void> _resetPassword() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.resetPassword(_userData);
if (response['status'] == ResponseStatus.success) {
showSuccessToast(response['message']);
await replaceWithLogin();
} else {
showErrorToast(response['message']);
}
}
}
// Request reset code
Future<void> requestResetCode() async =>
await runBusyFuture(_requestResetCode(),
@ -228,4 +211,21 @@ class ForgetPasswordViewModel extends FormViewModel {
}
}
}
// Request reset code
Future<void> resetPassword() async => await runBusyFuture(_resetPassword(),
busyObject: StateObjects.resetPassword);
Future<void> _resetPassword() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.resetPassword(_userData);
if (response['status'] == ResponseStatus.success) {
showSuccessToast(response['message']);
await replaceWithLogin();
} else {
showErrorToast(response['message']);
}
}
}
}

View File

@ -20,7 +20,9 @@ class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
Widget getPadding(context) {
double half = screenHeight(context) / 2;
return SizedBox(height: half + 375 - half);
return SizedBox(
height: half + 375 - half,
);
}
void _inAppPop(ForgetPasswordViewModel viewModel) {

View File

@ -3,7 +3,7 @@ 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_category/course_category_view.dart';
import 'package:yimaru_app/ui/views/learn_subcategory/learn_subcategory_view.dart';
import 'package:yimaru_app/ui/views/learn/learn_view.dart';
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
import 'package:yimaru_app/ui/views/startup/startup_view.dart';
@ -18,7 +18,6 @@ class HomeView extends StackedView<HomeViewModel> {
@override
void onViewModelReady(HomeViewModel viewModel) async {
// Removable
print('HERE');
await _init(viewModel);
super.onViewModelReady(viewModel);
}
@ -83,7 +82,7 @@ Widget _buildProfileIcon() => const Icon(Icons.person);
Widget getViewForIndex(int index) {
switch (index) {
case 0:
return const LearnSubcategoryView();
return const LearnView();
case 1:
return const CourseCategoryView();

View File

@ -1,7 +1,8 @@
import 'package:yimaru_app/app/app.bottomsheets.dart';
import 'package:yimaru_app/app/app.dialogs.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/common/app_strings.dart';
import 'package:stacked/stacked.dart';
@ -15,6 +16,7 @@ import '../../common/ui_helpers.dart';
class HomeViewModel extends ReactiveViewModel {
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _bottomSheetService = locator<BottomSheetService>();
@ -26,9 +28,9 @@ class HomeViewModel extends ReactiveViewModel {
[_authenticationService];
// Current user
User? get _user => _authenticationService.user;
UserModel? get _user => _authenticationService.user;
User? get user => _user;
UserModel? get user => _user;
// Bottom navigation
int _currentPage = 0;
@ -36,6 +38,19 @@ class HomeViewModel extends ReactiveViewModel {
int get currentPage => _currentPage;
// Bottom navigation
void setCurrentIndex(int index) {
_currentPage = index;
rebuildUi();
}
void showDialog() {
_dialogService.showCustomDialog(
title: 'Stacked Rocks!',
variant: DialogType.infoAlert,
description: 'Give stacked stars on Github',
);
}
void showBottomSheet() {
_bottomSheetService.showCustomSheet(
title: ksHomeBottomSheetTitle,
@ -44,12 +59,6 @@ class HomeViewModel extends ReactiveViewModel {
);
}
void setCurrentIndex(int index) {
_currentPage = index;
rebuildUi();
}
// Save profile status
Future<void> saveProfileStatus(bool value) async =>
await _authenticationService.saveProfileStatus(value);
@ -71,34 +80,7 @@ class HomeViewModel extends ReactiveViewModel {
await _getProfileData();
}
// Get profile data
Future<void> _getProfileData() async {
if (!(_user?.userInfoLoaded ?? false)) {
Map<String, dynamic> response = {};
if (_user?.profileCompleted != null &&
(_user?.profileCompleted ?? false)) {
if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileData(_user?.userId);
if (response['status'] == ResponseStatus.success) {
User user = response['data'] as User;
await _authenticationService.saveUserData(user);
String image =
await _imageDownloaderService.downloader(user.profilePicture);
await _authenticationService.saveProfilePicture(image);
}
} else {
await replaceWithFailure();
}
}
}
}
// Get profile status
// Profile status
Future<void> _getProfileStatus() async {
Map<String, dynamic> response = {};
if (_user?.profileCompleted == null) {
@ -120,4 +102,32 @@ class HomeViewModel extends ReactiveViewModel {
await saveProfileStatus(response['data']);
}
}
// Profile data
Future<void> _getProfileData() async {
if (!(_user?.userInfoLoaded ?? false)) {
Map<String, dynamic> response = {};
if (_user?.profileCompleted != null &&
(_user?.profileCompleted ?? false)) {
if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileData(_user?.userId);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
await _authenticationService.saveUserData(user);
String image =
await _imageDownloaderService.downloader(user.profilePicture);
await _authenticationService.saveProfilePicture(image);
}
} else {
await replaceWithFailure();
}
}
}
}
}

View File

@ -7,6 +7,7 @@ class LanguageViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Languages
Map<String, dynamic> _selectedLanguage = {
'code': 'EN',
'language': 'English'
@ -22,14 +23,13 @@ class LanguageViewModel extends BaseViewModel {
List<Map<String, dynamic>> get languages => _languages;
// Languages
bool isSelectedLanguage(String title) =>
_selectedLanguage['language'] == title;
void setSelectedLanguage(Map<String, dynamic> title) {
_selectedLanguage = title;
rebuildUi();
}
bool isSelectedLanguage(String title) =>
_selectedLanguage['language'] == title;
// Navigation
void pop() => _navigationService.back();
}

View File

@ -1,25 +1,15 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/learn_tile.dart';
import 'package:yimaru_app/ui/widgets/profile_app_bar.dart';
import 'package:yimaru_app/ui/widgets/learn_level_tile.dart';
import '../../../models/course.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 'learn_viewmodel.dart';
class LearnView extends StackedView<LearnViewModel> {
final int id;
const LearnView({Key? key, required this.id}) : super(key: key);
@override
void onViewModelReady(LearnViewModel viewModel) async {
await viewModel.getCourses(id);
super.onViewModelReady(viewModel);
}
const LearnView({Key? key}) : super(key: key);
@override
LearnViewModel viewModelBuilder(BuildContext context) => LearnViewModel();
@ -50,50 +40,49 @@ class LearnView extends StackedView<LearnViewModel> {
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildLearnColumnWrapper(viewModel)
_buildLevelsColumnWrapper(viewModel)
],
);
Widget _buildAppBar(LearnViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
showBackButton: true,
Widget _buildAppBar(LearnViewModel viewModel) => ProfileAppBar(
name: viewModel.user?.firstName,
profileImage: viewModel.user?.profilePicture,
);
Widget _buildLearnColumnWrapper(LearnViewModel viewModel) =>
Expanded(child: _buildLearnColumnScrollView(viewModel));
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLearnColumnScrollView(LearnViewModel viewModel) =>
Widget _buildLevelsColumnScrollView(LearnViewModel viewModel) =>
SingleChildScrollView(
child: _buildListViewBuilder(viewModel),
child: _buildLevelsColumn(viewModel),
);
Widget _buildListViewBuilder(LearnViewModel viewModel) =>
viewModel.busy(StateObjects.learnCourses)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
Widget _buildLevelsColumn(LearnViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
Widget _buildListView(LearnViewModel viewModel) => ListView.separated(
List<Widget> _buildLevelsColumnChildren(LearnViewModel viewModel) =>
[verticalSpaceLarge, _buildListView(viewModel)];
Widget _buildListView(LearnViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.courses.length,
itemCount: viewModel.learnLevels.length,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => verticalSpaceSmall,
itemBuilder: (context, index) => _buildTile(
course: viewModel.courses[index],
onTap: () async => await viewModel
.navigateToLearnLevel(viewModel.courses[index].id ?? 0),
),
title: viewModel.learnLevels[index]['title'],
status: viewModel.learnLevels[index]['status'],
subtitle: viewModel.learnLevels[index]['subtitle']),
);
Widget _buildTile({
required Course course,
required GestureTapCallback onTap,
}) =>
LearnTile(
onTap: onTap,
course: course,
Widget _buildTile(
{required String title,
required String subtitle,
required ProgressStatuses status}) =>
LearnLevelTile(
title: title,
status: status,
subtitle: subtitle,
);
}

View File

@ -1,43 +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.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
class LearnViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
class LearnViewModel extends ReactiveViewModel {
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
// Learn courses
List<Course> _courses = [];
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
List<Course> get courses => _courses;
// Current user
UserModel? get _user => _authenticationService.user;
// Navigation
void pop() => _navigationService.back();
UserModel? get user => _user;
Future<void> navigateToLearnLevel(int id) async =>
_navigationService.navigateToLearnLevelView(id: id);
final List<Map<String, dynamic>> _learnLevels = [
{
'title': 'Beginner',
'status': ProgressStatuses.started,
'subtitle': 'Start your journey with the basics of English.',
},
// {
// 'title': 'Intermediate',
// 'status': ProgressStatuses.started,
// 'subtitle': 'Practice real conversations and expand vocabulary.',
// },
// {
// 'title': 'Advanced',
// 'status': ProgressStatuses.pending,
// 'subtitle': 'Achieve fluency and master complex topics.',
// },
];
// Remote api call
List<Map<String, dynamic>> get learnLevels => _learnLevels;
// Learn courses
Future<void> getCourses(int id) async => await runBusyFuture(_getCourses(id),
busyObject: StateObjects.learnCourses);
Future<void> _getCourses(int id) async {
if (_courses.isEmpty) {
if (await _statusChecker.checkConnection()) {
_courses = await _apiService.getCourses(id);
}
}
}
Future<void> navigateToLearnLevel() async =>
_navigationService.navigateToLearnLevelView();
}

View File

@ -25,15 +25,6 @@ class LearnLessonDetailViewModel extends BaseViewModel {
VideoPlayerController? get videoPlayerController => _videoPlayerController;
// Video player
void close() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
}
Future<void> pause() async {
await _chewieController?.pause();
}
Future<void> initializePlayer() async =>
await runBusyFuture(_initializePlayer(),
busyObject: StateObjects.loadLessonVideo);
@ -60,6 +51,15 @@ class LearnLessonDetailViewModel extends BaseViewModel {
// rebuildUi();
}
Future<void> pause() async {
await _chewieController?.pause();
}
void close() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
}
// Navigation
void pop() => _navigationService.back();

View File

@ -1,24 +1,14 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/learn_level_tile.dart';
import 'package:yimaru_app/ui/widgets/learn_sub_level_tile.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
import '../../../models/level.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import 'learn_level_viewmodel.dart';
class LearnLevelView extends StackedView<LearnLevelViewModel> {
final int id;
const LearnLevelView({Key? key, required this.id}) : super(key: key);
@override
void onViewModelReady(LearnLevelViewModel viewModel) async {
await viewModel.getLevels(id);
super.onViewModelReady(viewModel);
}
const LearnLevelView({Key? key}) : super(key: key);
@override
LearnLevelViewModel viewModelBuilder(BuildContext context) =>
@ -64,35 +54,36 @@ class LearnLevelView extends StackedView<LearnLevelViewModel> {
Widget _buildLevelsColumnScrollView(LearnLevelViewModel viewModel) =>
SingleChildScrollView(
child: _buildListViewBuilder(viewModel),
child: _buildLevelsColumn(viewModel),
);
Widget _buildListViewBuilder(LearnLevelViewModel viewModel) =>
viewModel.busy(StateObjects.learnLevels)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
Widget _buildLevelsColumn(LearnLevelViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
Widget _buildListView(LearnLevelViewModel viewModel) => ListView.separated(
List<Widget> _buildLevelsColumnChildren(LearnLevelViewModel viewModel) =>
[verticalSpaceLarge, _buildListView(viewModel)];
Widget _buildListView(LearnLevelViewModel viewModel) => ListView.builder(
shrinkWrap: true,
itemCount: viewModel.levels.length,
itemCount: viewModel.learnSubLevels.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
level: viewModel.levels[index],
onTap: () async => await viewModel.navigateToModule( viewModel.levels[index]),
),
separatorBuilder: (context, index) => verticalSpaceSmall,
title: viewModel.learnSubLevels[index]['title'],
current: viewModel.learnSubLevels[index]['current'],
subtitle: viewModel.learnSubLevels[index]['subtitle']),
);
Widget _buildTile({
required Level level,
required GestureTapCallback onTap,
required String title,
required bool current,
required String subtitle,
}) =>
LearnLevelTile(
onTap: onTap,
level: level,
LearnSubLevelTile(
title: title,
current: current,
subtitle: subtitle,
);
}

View File

@ -3,43 +3,28 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
import '../../../models/level.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class LearnLevelViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Learn levels
List<Level> _levels = [];
final List<Map<String, dynamic>> _learnSubLevels = [
{
'title': 'A1',
'current': true,
'subtitle': 'Start your journey with the basics of English.',
},
// {
// 'title': 'A2',
// 'current': false,
// 'subtitle': 'Build upon your foundational knowledge.',
// },
];
List<Level> get levels => _levels;
List<Map<String, dynamic>> get learnSubLevels => _learnSubLevels;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToModule(Level level) async =>
_navigationService.navigateToLearnModuleView(level: level);
// Remote api call
// Learn levels
Future<void> getLevels(int id) async =>
await runBusyFuture(_getLevels(id), busyObject: StateObjects.learnLevels);
Future<void> _getLevels(int id) async {
if (_levels.isEmpty) {
if (await _statusChecker.checkConnection()) {
_levels = await _apiService.getLevels(id);
_levels.sort(
(a, b) => (a.displayOrder ?? 0).compareTo(b.displayOrder ?? 0));
}
}
}
Future<void> navigateToLearnModule() async =>
_navigationService.navigateToLearnModuleView();
}

View File

@ -1,26 +1,16 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/level.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/learn_module_tile.dart';
import 'package:yimaru_app/ui/widgets/overall_learn_progress.dart';
import '../../../models/module.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_module_viewmodel.dart';
class LearnModuleView extends StackedView<LearnModuleViewModel> {
final Level level;
const LearnModuleView({Key? key, required this.level}) : super(key: key);
@override
void onViewModelReady(LearnModuleViewModel viewModel) async {
await viewModel.getModules(level.id ?? 0);
super.onViewModelReady(viewModel);
}
const LearnModuleView({Key? key}) : super(key: key);
@override
LearnModuleViewModel viewModelBuilder(BuildContext context) =>
@ -52,7 +42,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildModulesColumnWrapper(viewModel),
_buildLevelsColumnWrapper(viewModel),
],
);
@ -61,7 +51,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
showBackButton: true,
);
Widget _buildModulesColumnWrapper(LearnModuleViewModel viewModel) =>
Widget _buildLevelsColumnWrapper(LearnModuleViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(LearnModuleViewModel viewModel) =>
@ -71,7 +61,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
Widget _buildLevelsColumn(LearnModuleViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
@ -86,7 +76,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
];
Widget _buildTitle() => Text(
level.title ?? '',
'A1 - Beginner',
style: style18P600,
);
@ -102,19 +92,27 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
itemCount: viewModel.modules.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
module: viewModel.modules[index],
onLessonTap: () {},
onPracticeTap: () {}),
title: viewModel.modules[index]['title'],
status: viewModel.modules[index]['status'],
topics: viewModel.modules[index]['topics'],
subtitle: viewModel.modules[index]['subtitle'],
practices: viewModel.modules[index]['practices'],
description: viewModel.modules[index]['description']),
);
Widget _buildTile({
required Module module,
required GestureTapCallback onLessonTap,
required GestureTapCallback onPracticeTap,
}) =>
Widget _buildTile(
{required String title,
required String topics,
required String subtitle,
required String description,
required ProgressStatuses status,
required List<Map<String, dynamic>> practices}) =>
LearnModuleTile(
module: module,
onLessonTap: onLessonTap,
onPracticeTap: onPracticeTap,
title: title,
status: status,
topics: topics,
subtitle: subtitle,
practices: practices,
description: description,
);
}

View File

@ -1,25 +1,351 @@
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/module.dart';
import '../../../app/app.locator.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class LearnModuleViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Learn module
List<Module> _modules = [];
// Modules
final List<Map<String, dynamic>> _modules = [
{
'status': ProgressStatuses.started,
'title': 'Lesson 1.1',
'subtitle': 'Start Speaking English Today! Greetings & Introductions',
'topics':
"""👉 How to use "Good Morning," "Afternoon," and "Evening" at the right time.
👉 Why saying "I'm" is often better than "I am" in spoken English.
👉 Master "My," "Your," and the verb "To Be" without the headache.
👉 How to perfectly say the "Long I" sound in "Hi" and "My."
""",
'practices': [
{
'question_text': 'Good morning! How are you?',
'question_audio_url':
'https://drive.google.com/file/d/1El9hhmZvLnrTYtVreHR0EDXNrZyGphT9/view?usp=sharing',
'sample_answer':
'https://drive.google.com/file/d/1dUSahuj_VdunV293gEZr0XL9d4WV7_8G/view?usp=sharing'
},
{
'question_text': 'What\'s your name?',
'sample_answer':
'https://drive.google.com/file/d/14oAqcMRltXeQhQ-RTGizO2DZ4CkHmKdu/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1iAsOIXD4NcsctKuubnvWlnwXodTMlou5/view?usp=drive_link'
},
{
'question_text': 'Nice to meet you!',
'question_audio_url':
'https://drive.google.com/file/d/10bOaNCcpNFzxpJ4d2-5KYahMCCcWt668/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1h8V6lFuOiOf0zRRN-NvYjtLrmMdx0SeX/view?usp=drive_link'
},
{
'question_text':
'You are doing great! Tell me, are you happy to be here?',
'sample_answer':
'https://drive.google.com/file/d/1sjNFofDRr9KB8fQptdM12LDJ5DIaHujs/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1kcNIL5NCt_lfTzAbmCkM3yojD_UzjSx7/view?usp=drive_link',
},
],
'description':
"""Stop feeling nervous when you meet someone new! In this lesson, we break down the most common English greetings and show you exactly how to introduce yourself with confidence.
Whether you are at the market, in a classroom, or meeting someone online, the first 10 seconds of a conversation are the most important. Our teacher will guide you through the exact phrases you need to sound natural and friendly from the very first moment.
""",
},
{
'status': ProgressStatuses.started,
'title': 'Lesson 1.2',
'subtitle': 'Talk About Your Home! "Where Are You From?" & Locations',
'topics':
"""👉 The difference between "Where are you from?" and "Where do you live?"
👉 How to use "Am," "Is," and "Are" to talk about your home.
👉 Learn to say "Where-are-you" as one smooth sound (word linking).
👉 Practice a full conversation with Miss Alem.
👉 A fast-paced game to test your speed and memory.
""",
'practices': [
{
'question_text':
'Hey, It is a pleasure to meet you. Where are you from?',
'sample_answer':
'https://drive.google.com/file/d/1F98PdPqeDhkF5KMzFjgmUoy4HanIDHVs/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1vipFTk-DYgOCYhyDyWVaORec7g0JeidD/view?usp=drive_link',
},
{
'question_text': 'Ethiopia is beautiful! What city are you from?',
'sample_answer':
'https://drive.google.com/file/d/1nZJLV9lOgFGqYr-W3vlpt8rbay_bwARJ/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1T8JIYQ6T9Mq_7TazGr85ag4PCyQgHhzi/view?usp=drive_link'
},
{
'question_text': 'I see. And where does your family live now?',
'question_audio_url':
'https://drive.google.com/file/d/19XAbHL3HqTpPcolvOQUVPSGef-Ythusu/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1O8o11DNKYZBcm1-f8pjMFJjNIvGmawvg/view?usp=drive_link'
},
],
'description':
"""Are you ready to talk about your city, town, or country with pride? In Lesson 1.2, we move past basic greetings and learn how to share where you come from and where you live now.
Whether you are meeting a tourist, a new classmate, or a colleague, being able to say "Im from Ethiopia" or "I live in Addis Ababa" is the best way to build a connection. Miss Alem will show you how to link your words together so you sound like a natural English speaker!
""",
},
{
'title': 'Lesson 1.3',
'status': ProgressStatuses.started,
'subtitle': 'Talk About Your Family! Master "Have" vs. "Has"',
'topics':
"""👉 Master the names for parents, grandparents, and the secret word for brothers and sisters (Siblings).
👉 Never mix up "I have" and "She has" ever again.
👉 Why the letter "S" is the most important sound when talking about your brothers.
👉 A close-up look at how to pronounce "Mother" and "Father" like a native speaker.
👉 Test your eyes and ears by finding the hidden mistakes in our family game.
""",
'practices': [
{
'question_text':
'Hello! I want to know about your family. Do you have a brother or sister?',
'sample_answer':
'https://drive.google.com/file/d/1fred7Y5ocdD4codZuK7Pr349O38UkB0Z/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1tMoTA4D6joz0bkWS5Nfn-neUvbej6XDI/view?usp=drive_link',
},
{
'question_text': 'That is nice! How many people are in your family?',
'sample_answer':
'https://drive.google.com/file/d/1dhYhiycWwdtW0ndJUMdMXxxVyrXLXPCM/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1FwRxWvrxzKbhEIYImy2ONzd410pabkJa/view?usp=drive_link'
},
{
'question_text': 'Do you have grandparents?',
'question_audio_url':
'https://drive.google.com/file/d/1UBaQYiJINgOmZWDLFKeXZQ8Zfb7cOKNZ/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/14cnEJTO7GtGZcO8ag50JdupV2EkvDHes/view?usp=drive_link'
},
],
'description':
"""Do you have brothers and sisters? In Lesson 1.3, we dive into the most personal and important topic for building friendships: Your Family. Many beginners make mistakes with the words "have" and "has," but today, we will fix that! Our teacher will teach you the exact formulas to describe your parents, siblings, and grandparents with perfect grammar. By the end of this video, you will be able to introduce your entire family in English with confidence.""",
},
{
'status': ProgressStatuses.started,
'title': 'Lesson 1.4',
'subtitle': '"What Do You Do?" Talk About Your Job & Studies',
'topics':
"""👉 The clear difference between talking about work and academic life.
👉 Simple rules to decide between "Im a teacher" and "Im an accountant."
👉 How to use the "-ing" form if you are still training for your dream job.
👉 Perfecting the final "T" in "Student" and the "SH" sound in "Cashier."
👉 A high-energy game to test your grammar reflexes!""",
'practices': [
{
'question_text': 'It is good to see you again! What do you do?',
'question_audio_url':
'https://drive.google.com/file/d/171fh_iN0aXS0t95_5RzcxFiVmLCB7caP/view?usp=sharing',
'sample_answer':
'https://drive.google.com/file/d/1wDkbp23F2PsLZM9mWlRd-JjnwZa97T-8/view?usp=drive_link',
},
{
'question_text':
'That is great. Are you studying to be a professional? What are you studying?',
'sample_answer':
'https://drive.google.com/file/d/1egHUMIqGt9VAw5HMVMdjWR3zmamdSB6F/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1T_qrH_-KHgQFw5kX4Ti-UwXvnBFyJsWT/view?usp=drive_link'
},
{
'question_text':
'Your sister works in a restaurant. Is she a waitress?',
'question_audio_url':
'https://drive.google.com/file/d/1Okd-W2zRZCukQfnbLEp9A4r_5rt99U_6/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1t-_JHXel9dtSHRwYU_CO1Mc8-Pa1pOa0/view?usp=drive_link'
},
{
'question_text': 'My friend works in a bank. Is he an accountant?',
'question_audio_url':
'https://drive.google.com/file/d/1K4ahmIzuBdrsUoO-gVRp525ZFyP7b1Cq/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1ecpWSQTvU9pu8BYq3qRkYUjblX5wG4RZ/view?usp=drive_link'
},
],
'description':
"""Youve shared your name and your home—now its time to talk about your daily life! In Lesson 1.4, we master the most common question in English: "What do you do?" Whether you are working a full-time job, searching for a new career, or currently studying, this lesson gives you the exact phrases to describe your profession with perfect grammar.
Miss [Name] breaks down the tricky "A vs. An" rule so you never have to second-guess yourself again. Plus, learn how to sound more natural by "linking" your words like a native speaker!
"""
},
{
'title': 'Lesson 1.5',
'status': ProgressStatuses.started,
'subtitle': 'Talk About Your Day! Daily Routines & Time Words',
'topics':
"""👉 Master the 6 most common actions, from "Wake up" to "Go to bed."
👉 Learn why we say "I eat" but "She eats" (and how to never forget it!).
👉 Use "First," "Then," and "Finally" to tell a complete story about your day.
👉 How to perfectly say the tricky "TH" sound in the word "Teeth."
👉 A fast-paced game to test if your brain can handle the "S" rule under pressure!""",
'practices': [
{
'question_text':
'I want to hear about your day! What is the first thing you do in the morning?',
'question_audio_url':
'https://drive.google.com/file/d/1B_g45crvD0X2xfJJuCBdBhe7LNRJtKpa/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/13nV3uQ6Vftc0DznBJ8GQMi26dHxmMUwH/view?usp=drive_link',
},
{
'question_text':
'That is a great start. What do you do for your hygiene? I brush...',
'sample_answer':
'https://drive.google.com/file/d/1ijbKHO9A3PEOUb712ycCw-P3ZvWX-NTp/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1r92ixsWSD58TrV9LIArPRaKk1bVE5m44/view?usp=drive_link'
},
{
'question_text':
'Perfect. And then, what do you do before you leave the house?',
'question_audio_url':
'https://drive.google.com/file/d/1xEHuliUwyqFV35o_C2varAHcoI3QrOsS/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1RJgS3tp0ZKQ1YIt9fLRch-IqFialZR6Q/view?usp=drive_link'
},
{
'question_text':
'And finally, what is the last thing you do at night?',
'question_audio_url':
'https://drive.google.com/file/d/1njWyQExmgAPWgDTrcpqitEoNjiPfCbEw/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/140O0xAkYpVo5FLZQmbc1BaY_6agYWeki/view?usp=drive_link'
},
],
'description':
"""What do you do from the moment you wake up until you go to bed? In Lesson 1.5, we master the art of describing your Daily Routine at Home. This is the best way to practice the "Simple Present Tense" so you can talk about your habits, chores, and schedules like a pro.
Our teacher will show you the "S" secretthe most important grammar rule for talking about other peopleand teach you the "Time Order Words" that make your English flow naturally from one activity to the next.
"""
},
{
'title': 'Lesson 1.6',
'status': ProgressStatuses.started,
'subtitle': 'Express Your Feelings! Likes, Dislikes & Hobbies',
'topics':
"""👉 How to build perfect positive and negative sentences (I like vs. I don't like).
👉 Master the "Do you like...?" structure and the correct short answers.
👉 Learn why we switch from "Don't" to "Doesn't" for He and She.
👉 How to master the "L" sound and make your "Don't" sound natural and fast.
👉 A high-speed game to test if you can handle the grammar of likes and dislikes!""",
'practices': [
{
'question_text':
'I want to know about your hobbies. Do you like football?',
'question_audio_url':
'https://drive.google.com/file/d/1ombhL58UTSSMlVr4OkjEN2m1TXeIs3Jw/view?usp=sharing',
'sample_answer':
'https://drive.google.com/file/d/1E-NilYZkazwTyAFwwNunDQmWIxBHH_GL/view?usp=drive_link',
},
{
'question_text':
'Thats fun! Some people prefer quiet things. What do you like to do at home',
'sample_answer':
'https://drive.google.com/file/d/1HJqwHQId9MUhaQvjoyhuhk-ZQNCz5-RP/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1HJqwHQId9MUhaQvjoyhuhk-ZQNCz5-RP/view?usp=drive_link'
},
{
'question_text':
'Interesting. Many people like music. Do you like slow music?',
'question_audio_url':
'https://drive.google.com/file/d/13edPllK_dZYmmFIsue_A-sGmDe9fh89K/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1lA18zdiFbKKXaGwUvJv4JwBMW_Vxxkip/view?usp=drive_link'
},
{
'question_text': 'Do you like running?',
'question_audio_url':
'https://drive.google.com/file/d/1MSVceVXL-mAELGvv0_SN68t1-pCM-V6R/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1XkwoWINsoXDSe63kjJXQFczjtPKbBTY4/view?usp=drive_link'
},
],
'description':
"""What makes you happy? What do you avoid? In Lesson 1.6, we learn how to make your English conversations personal and fun by sharing your Likes and Dislikes. This is the final topic of Module 1, and it is the key to building real friendships by finding common interests!
We will see how to use the power of "Do" and "Don't," and show you how the "S" rule changes when we talk about what our friends like. Get ready to talk about football, coffee, music, and more!
"""
},
{
'title': 'Lesson 1.7',
'status': ProgressStatuses.started,
'subtitle': 'Module 1 Final Test! Speaking Review & Graduation',
'topics': """👉 Fast-paced practice for names, origins, and jobs.
Mastering "Have," "Has," and the plural "S" under pressure.
👉 Using the Simple Present Tense to describe your day and your likes.
👉 A bonus challenge to find and correct common beginner mistakes.
👉 Two speaking scripts to help you link your words and sound like a native speaker.""",
'practices': [
{
'question_text': 'Hello! What\'s your name?',
'question_audio_url':
'https://drive.google.com/file/d/11xsnZpqOXQREc2nku77u_qOXucUdncDq/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1OgvhumoPNdw33WfxEZfS1xtYZHshAM-k/view?usp=drive_link',
},
{
'question_text': 'It was a pleasure meeting you. Nice to meet you!',
'sample_answer':
'https://drive.google.com/file/d/1NmKeyx0mKmbFyZdmQ4sWGM6UMpwAIrHN/view?usp=drive_link',
'question_audio_url':
'https://drive.google.com/file/d/1nu1fiK4dLCjW9jjENvThshU48vFj-T3h/view?usp=drive_link'
},
{
'question_text': 'Where are you from?',
'question_audio_url':
'https://drive.google.com/file/d/1uM2L6-u0H-LHyDQL_Y0txjoO64n5lHkp/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1VXhHRG2CJR3lx07cAuiZrfCxzxdOcCIz/view?usp=drive_link'
},
{
'question_text': 'And where do you live now?',
'question_audio_url':
'https://drive.google.com/file/d/1sGJpaRQ1wArA2iDq7BZM-56abMZwiISO/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/10Cj8r2KeYUttBJ7W2wETP8XSdK2ZItom/view?usp=drive_link'
},
{
'question_text': 'Do you have a brother or sister?',
'question_audio_url':
'https://drive.google.com/file/d/1cA4wD7zY7WRgorWladvf1f8QRu54oBBw/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1IeiGyN1S5XaREfXgmzfl-zESap8A_k3H/view?usp=drive_link'
},
{
'question_text': 'I am a teacher. Are you a student?',
'question_audio_url':
'https://drive.google.com/file/d/1n5w3skHBvQ2hE_OA2HdeUnr0YeN4zL2_/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1j86TVmeNzZvL8RueZeIueCQO_40i9aMh/view?usp=drive_link'
},
{
'question_text': 'What is the first thing you do in the morning?',
'question_audio_url':
'https://drive.google.com/file/d/1oCrDDtYF4-1S2UYu6shilqmQSqyRG3v_/view?usp=drive_link',
'sample_answer':
'https://drive.google.com/file/d/1shpCr2XvV12cXaxuJ-AjW-0XWvBdtT8Q/view?usp=drive_link'
},
],
'description':
"""Congratulations! You have reached the end of Module 1. You started with simple greetings, and now you are ready to hold a full conversation in English! This is our Final Speaking Review, where we test everything youve learned so far.
From your name and family to your daily habits and hobbies, this lesson is designed to push your speed, accuracy, and confidence. We will lead you through three high-intensity speaking drills and a bonus Grammar Check to make sure you are 100% ready for Module 2!
"""
},
];
List<Module> get modules => _modules;
List<Map<String, dynamic>> get modules => _modules;
// Navigation
void pop() => _navigationService.back();
@ -45,20 +371,4 @@ class LearnModuleViewModel extends BaseViewModel {
buttonLabel: 'Begin Lesson Practice',
subtitle: 'Lets quickly review what youve learned in this lesson!',
);
// Remote api call
// Learn modules
Future<void> getModules(int id) async => await runBusyFuture(_getModules(id),
busyObject: StateObjects.learnModules);
Future<void> _getModules(int id) async {
if (_modules.isEmpty) {
if (await _statusChecker.checkConnection()) {
_modules = await _apiService.getModules(id);
_modules.sort(
(a, b) => (a.displayOrder ?? 0).compareTo(b.displayOrder ?? 0));
}
}
}
}

View File

@ -2,7 +2,7 @@ import 'package:audioplayers/audioplayers.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:waveform_recorder/waveform_recorder.dart';
import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
@ -32,9 +32,9 @@ class LearnPracticeViewModel extends ReactiveViewModel {
[_audioPlayerService, _voiceRecorderService, _authenticationService];
// User
User? get _user => _authenticationService.user;
UserModel? get _user => _authenticationService.user;
User? get user => _user;
UserModel? get user => _user;
// AudioPlayer
AudioPlayer get _player => _audioPlayerService.player;
@ -55,6 +55,7 @@ class LearnPracticeViewModel extends ReactiveViewModel {
}
// Voice recorder
WaveformRecorderController get _waveController =>
_voiceRecorderService.waveController;
@ -81,36 +82,22 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Map<String, dynamic> get selectedPractice => _selectedPractice;
// Voice recorder
Future<void> stopRecording() async =>
await _voiceRecorderService.stopRecording();
Future<void> startRecording() async => await runBusyFuture(_startRecording(),
busyObject: StateObjects.recordLearnPracticeAnswer);
Future<void> _startRecording() async =>
await _voiceRecorderService.startRecording();
// Sample audio
Future<void> playSampleAudio() async =>
await runBusyFuture(_playSampleAudio(),
busyObject: StateObjects.learnPracticeSample);
Future<void> _playSampleAudio() async {
setBusyObject(StateObjects.learnPracticeSample);
await _audioPlayerService.playUrl(_selectedPractice['sample_answer']);
}
Future<void> pauseSampleAudio() async =>
await runBusyFuture(_pauseSampleAudio(),
busyObject: StateObjects.learnPracticeSample);
Future<void> _pauseSampleAudio() async {
setBusyObject(StateObjects.learnPracticeSample);
await _audioPlayerService.pause();
// Practice
void setPractice(Map<String, dynamic> practice) {
_selectedPractice = practice;
goTo(1);
}
// Play practice audio
Future<void> playQuestionAudio() async =>
await runBusyFuture(_playQuestionAudio(),
busyObject: StateObjects.learnPracticeQuestion);
Future<void> _playQuestionAudio() async {
goTo(3);
await _audioPlayerService.playUrl(_selectedPractice['question_audio_url']);
}
void _listenToAudio() {
_audioPlayerService.durationStream.listen((dur) {
_duration = dur;
@ -125,13 +112,29 @@ class LearnPracticeViewModel extends ReactiveViewModel {
});
}
Future<void> playQuestionAudio() async =>
await runBusyFuture(_playQuestionAudio(),
busyObject: StateObjects.learnPracticeQuestion);
// Set busy object
Future<void> _playQuestionAudio() async {
goTo(3);
await _audioPlayerService.playUrl(_selectedPractice['question_audio_url']);
void setBusyObject(StateObjects object) {
_busyObject = object;
notifyListeners();
}
// Sample audio
Future<void> playSampleAudio() async =>
await runBusyFuture(_playSampleAudio(),
busyObject: StateObjects.learnPracticeSample);
Future<void> _playSampleAudio() async {
setBusyObject(StateObjects.learnPracticeSample);
await _audioPlayerService.playUrl(_selectedPractice['sample_answer']);
}
Future<void> pauseSampleAudio() async =>
await runBusyFuture(_pauseSampleAudio(),
busyObject: StateObjects.learnPracticeSample);
Future<void> _pauseSampleAudio() async {
setBusyObject(StateObjects.learnPracticeSample);
await _audioPlayerService.pause();
}
// Recorded audio
@ -154,17 +157,15 @@ class LearnPracticeViewModel extends ReactiveViewModel {
await _audioPlayerService.pause();
}
// Set busy object
void setBusyObject(StateObjects object) {
_busyObject = object;
notifyListeners();
}
// Voice recorder
Future<void> startRecording() async => await runBusyFuture(_startRecording(),
busyObject: StateObjects.recordLearnPracticeAnswer);
// Practice
void setPractice(Map<String, dynamic> practice) {
_selectedPractice = practice;
goTo(1);
}
Future<void> _startRecording() async =>
await _voiceRecorderService.startRecording();
Future<void> stopRecording() async =>
await _voiceRecorderService.stopRecording();
// Dialogue
Future<bool?> showAbortDialog() async {

View File

@ -1,100 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/learn_subcategory_card.dart';
import '../../../models/subcategory.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/profile_app_bar.dart';
import 'learn_subcategory_viewmodel.dart';
class LearnSubcategoryView extends StackedView<LearnSubcategoryViewModel> {
const LearnSubcategoryView({Key? key}) : super(key: key);
@override
void onViewModelReady(LearnSubcategoryViewModel viewModel) async {
await viewModel.getLearnSubcategories();
super.onViewModelReady(viewModel);
}
@override
LearnSubcategoryViewModel viewModelBuilder(BuildContext context) =>
LearnSubcategoryViewModel();
@override
Widget builder(
BuildContext context,
LearnSubcategoryViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnSubcategoryViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnSubcategoryViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(LearnSubcategoryViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnSubcategoryViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildSubcategoryColumnWrapper(viewModel)
],
);
Widget _buildAppBar(LearnSubcategoryViewModel viewModel) => ProfileAppBar(
name: viewModel.user?.firstName,
profileImage: viewModel.user?.profilePicture,
);
Widget _buildSubcategoryColumnWrapper(LearnSubcategoryViewModel viewModel) =>
Expanded(child: _buildSubcategoryColumnScrollView(viewModel));
Widget _buildSubcategoryColumnScrollView(
LearnSubcategoryViewModel viewModel) =>
SingleChildScrollView(
child: _buildListViewBuilder(viewModel),
);
Widget _buildListViewBuilder(LearnSubcategoryViewModel viewModel) =>
viewModel.busy(StateObjects.learnSubcategories)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(LearnSubcategoryViewModel viewModel) =>
ListView.separated(
shrinkWrap: true,
itemCount: viewModel.subcategories.length,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => verticalSpaceSmall,
itemBuilder: (context, index) => _buildTile(
subcategory: viewModel.subcategories[index],
onTap: () async => await viewModel
.navigateToLearn(viewModel.subcategories[index].id ?? 0),
),
);
Widget _buildTile({
required Subcategory subcategory,
required GestureTapCallback onTap,
}) =>
LearnSubcategoryCard(
onTap: onTap,
subcategory: subcategory,
);
}

View File

@ -1,55 +0,0 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/subcategory.dart';
import '../../../models/user.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class LearnSubcategoryViewModel extends ReactiveViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
User? get _user => _authenticationService.user;
User? get user => _user;
// Learn subcategories
List<Subcategory> _subcategories = [];
List<Subcategory> get subcategories => _subcategories;
// Navigation
Future<void> navigateToLearn(int id) async =>
_navigationService.navigateToLearnView(id: id);
// Remote api call
// Learn subcategories
Future<void> getLearnSubcategories() async =>
await runBusyFuture(_getLearnSubcategories(),
busyObject: StateObjects.learnSubcategories);
Future<void> _getLearnSubcategories() async {
if (_subcategories.isEmpty) {
if (await _statusChecker.checkConnection()) {
_subcategories = await _apiService.getLearnSubcategories();
}
}
}
}

View File

@ -11,8 +11,8 @@ import 'login_viewmodel.dart';
@FormView(fields: [
FormTextField(name: 'otp', validator: FormValidator.validateForm),
FormTextField(name: 'password', validator: FormValidator.validateForm),
FormTextField(name: 'email', validator: FormValidator.validateEmailForm),
FormTextField(name: 'password', validator: FormValidator.validateForm),
FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm)
])
class LoginView extends StackedView<LoginViewModel> with $LoginView {

View File

@ -13,8 +13,8 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String OtpValueKey = 'otp';
const String PasswordValueKey = 'password';
const String EmailValueKey = 'email';
const String PasswordValueKey = 'password';
const String PhoneNumberValueKey = 'phoneNumber';
final Map<String, TextEditingController> _LoginViewTextEditingControllers = {};
@ -23,24 +23,24 @@ final Map<String, FocusNode> _LoginViewFocusNodes = {};
final Map<String, String? Function(String?)?> _LoginViewTextValidations = {
OtpValueKey: FormValidator.validateForm,
PasswordValueKey: FormValidator.validateForm,
EmailValueKey: FormValidator.validateEmailForm,
PasswordValueKey: FormValidator.validateForm,
PhoneNumberValueKey: FormValidator.validateForm,
};
mixin $LoginView {
TextEditingController get otpController =>
_getFormTextEditingController(OtpValueKey);
TextEditingController get passwordController =>
_getFormTextEditingController(PasswordValueKey);
TextEditingController get emailController =>
_getFormTextEditingController(EmailValueKey);
TextEditingController get passwordController =>
_getFormTextEditingController(PasswordValueKey);
TextEditingController get phoneNumberController =>
_getFormTextEditingController(PhoneNumberValueKey);
FocusNode get otpFocusNode => _getFormFocusNode(OtpValueKey);
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
TextEditingController _getFormTextEditingController(
@ -68,8 +68,8 @@ mixin $LoginView {
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
otpController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
emailController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
@ -83,8 +83,8 @@ mixin $LoginView {
)
void listenToFormUpdated(FormViewModel model) {
otpController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
emailController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
@ -96,8 +96,8 @@ mixin $LoginView {
model.formValueMap
..addAll({
OtpValueKey: otpController.text,
PasswordValueKey: passwordController.text,
EmailValueKey: emailController.text,
PasswordValueKey: passwordController.text,
PhoneNumberValueKey: phoneNumberController.text,
}),
);
@ -141,8 +141,8 @@ extension ValueProperties on FormStateHelper {
}
String? get otpValue => this.formValueMap[OtpValueKey] as String?;
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
String? get phoneNumberValue =>
this.formValueMap[PhoneNumberValueKey] as String?;
@ -156,16 +156,6 @@ extension ValueProperties on FormStateHelper {
}
}
set passwordValue(String? value) {
this.setData(
this.formValueMap..addAll({PasswordValueKey: value}),
);
if (_LoginViewTextEditingControllers.containsKey(PasswordValueKey)) {
_LoginViewTextEditingControllers[PasswordValueKey]?.text = value ?? '';
}
}
set emailValue(String? value) {
this.setData(
this.formValueMap..addAll({EmailValueKey: value}),
@ -176,6 +166,16 @@ extension ValueProperties on FormStateHelper {
}
}
set passwordValue(String? value) {
this.setData(
this.formValueMap..addAll({PasswordValueKey: value}),
);
if (_LoginViewTextEditingControllers.containsKey(PasswordValueKey)) {
_LoginViewTextEditingControllers[PasswordValueKey]?.text = value ?? '';
}
}
set phoneNumberValue(String? value) {
this.setData(
this.formValueMap..addAll({PhoneNumberValueKey: value}),
@ -189,31 +189,31 @@ extension ValueProperties on FormStateHelper {
bool get hasOtp =>
this.formValueMap.containsKey(OtpValueKey) &&
(otpValue?.isNotEmpty ?? false);
bool get hasPassword =>
this.formValueMap.containsKey(PasswordValueKey) &&
(passwordValue?.isNotEmpty ?? false);
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
bool get hasPassword =>
this.formValueMap.containsKey(PasswordValueKey) &&
(passwordValue?.isNotEmpty ?? false);
bool get hasPhoneNumber =>
this.formValueMap.containsKey(PhoneNumberValueKey) &&
(phoneNumberValue?.isNotEmpty ?? false);
bool get hasOtpValidationMessage =>
this.fieldsValidationMessages[OtpValueKey]?.isNotEmpty ?? false;
bool get hasPasswordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
bool get hasPasswordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
bool get hasPhoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false;
String? get otpValidationMessage =>
this.fieldsValidationMessages[OtpValueKey];
String? get passwordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey];
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
String? get passwordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey];
String? get phoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey];
}
@ -221,18 +221,18 @@ extension ValueProperties on FormStateHelper {
extension Methods on FormStateHelper {
setOtpValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[OtpValueKey] = validationMessage;
setPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
setEmailValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
setPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
setPhoneNumberValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage;
/// Clears text input fields on the Form
void clearForm() {
otpValue = '';
passwordValue = '';
emailValue = '';
passwordValue = '';
phoneNumberValue = '';
}
@ -240,8 +240,8 @@ extension Methods on FormStateHelper {
void validateForm() {
this.setValidationMessages({
OtpValueKey: getValidationMessage(OtpValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
EmailValueKey: getValidationMessage(EmailValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
});
}
@ -263,7 +263,7 @@ String? getValidationMessage(String key) {
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
OtpValueKey: getValidationMessage(OtpValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
EmailValueKey: getValidationMessage(EmailValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
});

View File

@ -4,7 +4,7 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
@ -29,6 +29,8 @@ class LoginViewModel extends ReactiveViewModel
@override
List<ListenableServiceMixin> get listenableServices => [_googleAuthService];
// Sms
// Google user
GoogleSignInAccount? get _googleUser => _googleAuthService.googleUser;
@ -126,16 +128,21 @@ class LoginViewModel extends ReactiveViewModel
_resendTime = DateTime.now().add(const Duration(minutes: 3, seconds: 0));
}
// User data
void clearUserData() {
_userData.clear();
}
// Add user data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void clearUserData() {
_userData.clear();
}
// In app navigation
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
void goBack() {
if (_currentPage == 1) {
_currentPage = 0;
@ -148,21 +155,16 @@ class LoginViewModel extends ReactiveViewModel
}
}
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
// Navigation
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShow(Routes.homeView);
Future<void> navigateToRegister() async =>
await _navigationService.navigateToRegisterView();
Future<void> navigateToForgetPassword() async =>
await _navigationService.navigateToForgetPasswordView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShow(Routes.homeView);
// Remote api calls
// Login with email
@ -173,7 +175,7 @@ class LoginViewModel extends ReactiveViewModel
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = await _apiService.login(_userData);
if (response['status'] == ResponseStatus.success) {
User user = response['data'] as User;
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
@ -190,37 +192,6 @@ class LoginViewModel extends ReactiveViewModel
}
}
// Sign-in with google
Future<void> _googleAuth() async {
if (await _statusChecker.checkConnection()) {
await _googleAuthService.googleAuth();
Map<String, dynamic> data = {
'id_token': _googleUser?.authentication.idToken ?? '',
};
Map<String, dynamic> response = await _apiService.googleAuth(data);
if (response['status'] == ResponseStatus.success) {
User user = response['data'] as User;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserCredential(data);
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
Future<void> signInWithGoogle() async => await runBusyFuture(_googleAuth(),
busyObject: StateObjects.loginWithGoogle);
// Login with phone
Future<void> loginWithPhoneNumber() async =>
await runBusyFuture(_loginWithPhoneNumber(),
@ -238,12 +209,46 @@ class LoginViewModel extends ReactiveViewModel
}
}
// Sign-in with google
Future<void> signInWithGoogle() async => await runBusyFuture(_googleAuth(),
busyObject: StateObjects.loginWithGoogle);
Future<void> _googleAuth() async {
if (await _statusChecker.checkConnection()) {
await _googleAuthService.googleAuth();
Map<String, dynamic> data = {
'id_token': _googleUser?.authentication.idToken ?? '',
};
Map<String, dynamic> response = await _apiService.googleAuth(data);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserCredential(data);
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
// Verify otp
Future<void> verifyOtp() async =>
await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp);
Future<void> _verifyOtp() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = await _apiService.verifyOtp(_userData);
if (response['status'] == ResponseStatus.success) {
User user = response['data'] as User;
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
@ -259,10 +264,10 @@ class LoginViewModel extends ReactiveViewModel
}
}
Future<void> verifyOtp() async =>
await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp);
// Resend otp
Future<void> resendOtp() async =>
await runBusyFuture(_resendOtp(), busyObject: StateObjects.resendOtp);
Future<void> _resendOtp() async {
if (await _statusChecker.checkConnection()) {
resetButton();
@ -276,7 +281,4 @@ class LoginViewModel extends ReactiveViewModel
}
}
}
Future<void> resendOtp() async =>
await runBusyFuture(_resendOtp(), busyObject: StateObjects.resendOtp);
}

View File

@ -5,7 +5,7 @@ import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../models/user.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/google_auth_service.dart';
@ -32,9 +32,9 @@ class ProfileViewModel extends ReactiveViewModel {
[_authenticationService];
// Current user
User? get _user => _authenticationService.user;
UserModel? get _user => _authenticationService.user;
User? get user => _user;
UserModel? get user => _user;
// Image picker
Future<void> openCamera() async =>

View File

@ -2,7 +2,7 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../models/user.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/image_picker_service.dart';
@ -28,9 +28,9 @@ class ProfileDetailViewModel extends ReactiveViewModel
[_authenticationService];
// Current user
User? get _user => _authenticationService.user;
UserModel? get _user => _authenticationService.user;
User? get user => _user;
UserModel? get user => _user;
// First name
bool _focusFirstName = false;

View File

@ -9,7 +9,7 @@ import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../../../app/app.locator.dart';
import '../../../models/user.dart';
import '../../../models/user_model.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/smart_auth_service.dart';
import '../../../services/status_checker_service.dart';
@ -336,7 +336,7 @@ class RegisterViewModel extends ReactiveViewModel
Map<String, dynamic> response = await _apiService.googleAuth(data);
if (response['status'] == ResponseStatus.success) {
User user = response['data'] as User;
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
@ -359,7 +359,7 @@ class RegisterViewModel extends ReactiveViewModel
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = await _apiService.verifyOtp(_userData);
if (response['status'] == ResponseStatus.success) {
User user = response['data'] as User;
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/models/category.dart';
import 'package:yimaru_app/models/course_category.dart';
import 'package:yimaru_app/ui/common/helper_functions.dart';
import '../common/app_colors.dart';
@ -8,7 +8,7 @@ import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseCategoryCard extends StatelessWidget {
final Category category;
final CourseCategory category;
final GestureTapCallback? onTap;
const CourseCategoryCard({super.key, this.onTap, required this.category});

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import '../../models/subcategory.dart';
import '../../models/course_subcategory.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseSubcategoryTile extends StatelessWidget {
final Subcategory subcategory;
final CourseSubcategory subcategory;
final GestureTapCallback? onCourseTap;
final GestureTapCallback? onPracticeTap;
@ -57,9 +57,9 @@ class CourseSubcategoryTile extends StatelessWidget {
];
Widget _buildTitle() => Text(
(subcategory.name == null || subcategory.name!.isEmpty)
(subcategory.title == null || subcategory.title!.isEmpty)
? 'Course ${subcategory.id}'
: subcategory.name!,
: subcategory.title!,
style: style16P600,
);

View File

@ -1,69 +1,67 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_level/learn_level_viewmodel.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/learn/learn_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/progress_status.dart';
import '../../models/level.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class LearnLevelTile extends ViewModelWidget<LearnLevelViewModel> {
final Level level;
final GestureTapCallback? onTap;
class LearnLevelTile extends ViewModelWidget<LearnViewModel> {
final String title;
final String subtitle;
final ProgressStatuses status;
const LearnLevelTile({
super.key,
this.onTap,
required this.level,
required this.title,
required this.status,
required this.subtitle,
});
@override
Widget build(BuildContext context, LearnLevelViewModel viewModel) =>
Widget build(BuildContext context, LearnViewModel viewModel) =>
_buildExpansionTileCard(viewModel);
Widget _buildExpansionTileCard(LearnLevelViewModel viewModel) => Container(
Widget _buildExpansionTileCard(LearnViewModel viewModel) => Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: kcPrimaryColor.withOpacity(0.2),
// color:
// current ? kcPrimaryColor.withOpacity(0.2) : kcVeryLightGrey,
),
color: status == ProgressStatuses.started
? kcPrimaryColor.withOpacity(0.2)
: kcVeryLightGrey),
),
child: _buildExpansionTile(viewModel),
);
Widget _buildExpansionTile(LearnLevelViewModel viewModel) => ExpansionTile(
enabled: true,
Widget _buildExpansionTile(LearnViewModel viewModel) => ExpansionTile(
textColor: kcDarkGrey,
showTrailingIcon: true,
title: _buildTitleRow(),
initiallyExpanded: false,
subtitle: _buildContent(),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
backgroundColor: kcPrimaryColor.withOpacity(0.1),
childrenPadding: const EdgeInsets.all(15),
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
backgroundColor: status != ProgressStatuses.pending
? kcPrimaryColor.withOpacity(0.1)
: kcBackgroundColor,
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1),
childrenPadding: const EdgeInsets.only(left: 15, right: 15, bottom: 15),
//enabled: current,
// showTrailingIcon: current,
//initiallyExpanded: current,
// collapsedBackgroundColor:
// current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor,
// backgroundColor:
// current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor,
enabled: status != ProgressStatuses.pending ? true : false,
collapsedBackgroundColor: status != ProgressStatuses.pending
? kcPrimaryColor.withOpacity(0.1)
: kcBackgroundColor,
showTrailingIcon: status != ProgressStatuses.pending ? true : false,
initiallyExpanded: status == ProgressStatuses.started ? true : false,
children: _buildExpansionTileChildren(viewModel),
);
List<Widget> _buildExpansionTileChildren(LearnLevelViewModel viewModel) =>
[_buildViewButton(viewModel)];
List<Widget> _buildExpansionTileChildren(LearnViewModel viewModel) =>
[_buildActionButton(viewModel)];
Widget _buildTitleRow() => Row(
mainAxisSize: MainAxisSize.min,
@ -72,27 +70,42 @@ class LearnLevelTile extends ViewModelWidget<LearnLevelViewModel> {
List<Widget> _buildTitleChildren() => [
_buildTitle(),
// if (current) horizontalSpaceSmall,
// if (current) _buildProgressStatus()
if (status != ProgressStatuses.pending) horizontalSpaceSmall,
if (status != ProgressStatuses.pending) _buildProgressStatus()
];
Widget _buildTitle() => Text(
level.title ?? '',
style: style16P600,
title,
style: const TextStyle(
fontSize: 16,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
);
Widget _buildProgressStatus() => const ProgressStatus(
Widget _buildProgressStatus() => ProgressStatus(
status: status.name.substring(0, 1).toUpperCase() +
status.name.substring(1, status.name.length),
color: kcPrimaryColor,
status: 'Current Level',
);
Widget _buildViewButton(LearnLevelViewModel viewModel) =>
CustomElevatedButton(
Widget _buildContent() => Text(
subtitle,
style: const TextStyle(
color: kcDarkGrey,
),
);
Widget _buildActionButton(LearnViewModel viewModel) => CustomElevatedButton(
height: 15,
onTap: onTap,
borderRadius: 12,
text: 'View Level',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
text: status == ProgressStatuses.completed
? 'Review Course'
: status == ProgressStatuses.pending
? 'Start Learning'
: 'Continue Learning',
onTap: () async => await viewModel.navigateToLearnLevel(),
);
}

View File

@ -4,19 +4,27 @@ import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import '../../models/module.dart';
import '../common/app_colors.dart';
import '../common/enmus.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
final Module module;
final GestureTapCallback? onLessonTap;
final GestureTapCallback? onPracticeTap;
final String title;
final String topics;
final String subtitle;
final String description;
final ProgressStatuses status;
final List<Map<String, dynamic>> practices;
const LearnModuleTile(
{super.key, this.onLessonTap, this.onPracticeTap, required this.module});
{super.key,
required this.title,
required this.topics,
required this.status,
required this.subtitle,
required this.practices,
required this.description});
Future<void> _showSheet(
{required BuildContext context,
@ -49,7 +57,7 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
Stack(
children: [
_buildExpansionTile(context: context, viewModel: viewModel),
// _buildContainerShaderState()
_buildContainerShaderState()
],
);
@ -57,11 +65,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
ExpansionTile(
enabled: true,
title: _buildTitle(),
textColor: kcDarkGrey,
showTrailingIcon: true,
initiallyExpanded: true,
title: _buildTitle(),
subtitle: _buildContent(),
leading: _buildIconWrapper(),
collapsedIconColor: kcDarkGrey,
@ -70,13 +75,13 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
collapsedBackgroundColor: kcBackgroundColor,
enabled: status != ProgressStatuses.pending,
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
childrenPadding: const EdgeInsets.fromLTRB(70, 15, 15, 15),
// enabled: status != ProgressStatuses.pending,
// showTrailingIcon: status != ProgressStatuses.pending ? true : false,
//initiallyExpanded: status == ProgressStatuses.started ? true : false,
showTrailingIcon: status != ProgressStatuses.pending ? true : false,
initiallyExpanded: status == ProgressStatuses.started ? true : false,
children:
_buildExpansionTileChildren(context: context, viewModel: viewModel),
);
@ -92,19 +97,13 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
);
Widget _buildTitle() => Text(
module.title ?? '',
maxLines: 1,
softWrap: false,
title,
style: style16P600,
overflow: TextOverflow.ellipsis,
);
Widget _buildContent() => Text(
module.description ?? '',
maxLines: 1,
softWrap: false,
subtitle,
style: style14DG400,
overflow: TextOverflow.ellipsis,
);
List<Widget> _buildExpansionTileChildren(
@ -180,16 +179,15 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onLessonTap,
text: 'View Module',
text: 'View Lessons',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
// onTap: () async => await viewModel.navigateToLearnLesson(
// title: title,
// topics: topics,
// subtitle: subtitle,
// practices: practices,
// description: description),
onTap: () async => await viewModel.navigateToLearnLesson(
title: title,
topics: topics,
subtitle: subtitle,
practices: practices,
description: description),
);
Widget _buildPracticeButtonWrapper(
@ -205,21 +203,20 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
CustomElevatedButton(
height: 15,
borderRadius: 12,
onTap: onPracticeTap,
text: 'View Practices',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
// onTap: () async => await viewModel.navigateToLearnPractice(practices),
onTap: () async => await viewModel.navigateToLearnPractice(practices),
);
Widget _buildSheet(LearnModuleViewModel viewModel) => FinishPracticeSheet(
onTap: viewModel.pop,
);
// Widget _buildContainerShaderState() => status == ProgressStatuses.pending
// ? _buildContainerShaderWrapper()
// : Container();
Widget _buildContainerShaderState() => status == ProgressStatuses.pending
? _buildContainerShaderWrapper()
: Container();
Widget _buildContainerShaderWrapper() => Positioned.fill(
child: _buildContainerShader(),

View File

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_level/learn_level_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/progress_status.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class LearnSubLevelTile extends ViewModelWidget<LearnLevelViewModel> {
final bool current;
final String title;
final String subtitle;
const LearnSubLevelTile({
super.key,
required this.title,
required this.current,
required this.subtitle,
});
@override
Widget build(BuildContext context, LearnLevelViewModel viewModel) =>
_buildExpansionTileCard(viewModel);
Widget _buildExpansionTileCard(LearnLevelViewModel viewModel) => Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(
color:
current ? kcPrimaryColor.withOpacity(0.2) : kcVeryLightGrey),
),
child: _buildExpansionTile(viewModel),
);
Widget _buildExpansionTile(LearnLevelViewModel viewModel) => ExpansionTile(
enabled: current,
textColor: kcDarkGrey,
title: _buildTitleRow(),
showTrailingIcon: current,
subtitle: _buildContent(),
initiallyExpanded: current,
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
childrenPadding: const EdgeInsets.all(15),
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
backgroundColor:
current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor,
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
collapsedBackgroundColor:
current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor,
children: _buildExpansionTileChildren(viewModel),
);
List<Widget> _buildExpansionTileChildren(LearnLevelViewModel viewModel) =>
[_buildActionButtonWrapper(viewModel)];
Widget _buildTitleRow() => Row(
mainAxisSize: MainAxisSize.min,
children: _buildTitleChildren(),
);
List<Widget> _buildTitleChildren() => [
_buildTitle(),
if (current) horizontalSpaceSmall,
if (current) _buildProgressStatus()
];
Widget _buildTitle() => Text(
title,
style: style16P600,
);
Widget _buildProgressStatus() => const ProgressStatus(
color: kcPrimaryColor,
status: 'Current Level',
);
Widget _buildContent() => Text(
subtitle,
style: style14DG400,
);
Widget _buildActionButtonWrapper(LearnLevelViewModel viewModel) => SizedBox(
height: 40,
child: _buildActionButtons(viewModel),
);
Widget _buildActionButtons(LearnLevelViewModel viewModel) => Row(
children: [
_buildViewButtonWrapper(viewModel),
horizontalSpaceSmall,
_buildPracticeButtonWrapper(viewModel)
],
);
Widget _buildViewButtonWrapper(LearnLevelViewModel viewModel) => Expanded(
child: _buildViewButton(viewModel),
);
Widget _buildViewButton(LearnLevelViewModel viewModel) =>
CustomElevatedButton(
height: 15,
borderRadius: 12,
text: 'View Course',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.navigateToLearnModule(),
);
Widget _buildPracticeButtonWrapper(LearnLevelViewModel viewModel) => Expanded(
child: Container(),
);
Widget _buildPracticeButton(LearnLevelViewModel viewModel) =>
const CustomElevatedButton(
height: 15,
text: 'Practice',
borderRadius: 12,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor
// onTap: () async => await viewModel.navigateToLearnPractice()
);
}

View File

@ -1,70 +0,0 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/models/subcategory.dart';
import '../common/app_colors.dart';
import '../common/app_strings.dart';
import '../common/helper_functions.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class LearnSubcategoryCard extends StatelessWidget {
final Subcategory subcategory;
final GestureTapCallback? onTap;
const LearnSubcategoryCard(
{super.key, this.onTap, required this.subcategory});
@override
Widget build(BuildContext context) => _buildContainer();
Widget _buildContainer() => Container(
height: 200,
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: getColor(),
borderRadius: BorderRadius.circular(5),
),
child: _buildColumn(),
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(),
);
List<Widget> _buildColumnChildren() => [
_buildTitle(),
verticalSpaceTiny,
_buildSubtitle(),
verticalSpaceMedium,
__buildStartButtonWrapper(),
];
Widget _buildTitle() => Text(
subcategory.name ?? '',
style: style18DG700,
);
Widget _buildSubtitle() => Text(
subcategory.description ?? ksCategorySubtitle,
maxLines: 3,
style: style16DG400,
);
Widget __buildStartButtonWrapper() => SizedBox(
height: 40,
child: _buildStartButton(),
);
Widget _buildStartButton() => CustomElevatedButton(
height: 50,
width: 200,
onTap: onTap,
borderRadius: 12,
text: 'Select Course',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
}

View File

@ -1,123 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/course.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/learn/learn_viewmodel.dart';
import '../common/app_colors.dart';
import 'custom_elevated_button.dart';
class LearnTile extends ViewModelWidget<LearnViewModel> {
final Course course;
final GestureTapCallback? onTap;
const LearnTile({
super.key,
this.onTap,
required this.course,
});
@override
Widget build(BuildContext context, LearnViewModel viewModel) =>
_buildExpansionTileCard(viewModel);
Widget _buildExpansionTileCard(LearnViewModel viewModel) => Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(color: kcPrimaryColor.withOpacity(0.2)
// color: status == ProgressStatuses.started
// ? kcPrimaryColor.withOpacity(0.2)
// : kcVeryLightGrey,
),
),
child: _buildExpansionTile(viewModel),
);
Widget _buildExpansionTile(LearnViewModel viewModel) => ExpansionTile(
enabled: true,
textColor: kcDarkGrey,
showTrailingIcon: true,
title: _buildTitleRow(),
initiallyExpanded: false,
subtitle: _buildContent(),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
backgroundColor: kcPrimaryColor.withOpacity(0.1),
controlAffinity: ListTileControlAffinity.trailing,
childrenPadding: const EdgeInsets.only(bottom: 15),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.symmetric(horizontal: 15),
collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1),
// enabled: status != ProgressStatuses.pending ? true : false,
// status != ProgressStatuses.pending
// ? kcPrimaryColor.withOpacity(0.1)
// : kcBackgroundColor,
// collapsedBackgroundColor: status != ProgressStatuses.pending
// ? kcPrimaryColor.withOpacity(0.1)
// : kcBackgroundColor,
// showTrailingIcon: status != ProgressStatuses.pending ? true : false,
// initiallyExpanded: status == ProgressStatuses.started ? true : false,
children: _buildExpansionTileChildren(viewModel),
);
List<Widget> _buildExpansionTileChildren(LearnViewModel viewModel) => [
_buildDivider(),
verticalSpaceTiny,
_buildActionButtonWrapper(viewModel)
];
Widget _buildTitleRow() => Row(
mainAxisSize: MainAxisSize.min,
children: _buildTitleChildren(),
);
List<Widget> _buildTitleChildren() => [
_buildTitle(),
// if (status != ProgressStatuses.pending) horizontalSpaceSmall,
// if (status != ProgressStatuses.pending) _buildProgressStatus()
];
Widget _buildTitle() => Text(
course.title ?? '',
style: const TextStyle(
fontSize: 16,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
);
// Widget _buildProgressStatus() => ProgressStatus(
// status: status.name.substring(0, 1).toUpperCase() +
// status.name.substring(1, status.name.length),
// color: kcPrimaryColor,
// );
Widget _buildContent() => Text(
course.description ?? '',
style: style14DG400,
);
Widget _buildDivider() => const Divider(color: kcVeryLightGrey);
Widget _buildActionButtonWrapper(LearnViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildActionButton(viewModel),
);
Widget _buildActionButton(LearnViewModel viewModel) => CustomElevatedButton(
height: 15,
onTap: onTap,
borderRadius: 12,
text: 'Start Course',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
// text: status == ProgressStatuses.completed
// ? 'Review Course'
// : status == ProgressStatuses.pending
// ? 'Start Learning'
// : 'Continue Learning',
);
}

View File

@ -84,8 +84,7 @@ class SelectableCoursePracticeQuestion
? _buildProgressIndicator()
: _buildContinueButton(viewModel);
Widget _buildProgressIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
Widget _buildProgressIndicator()=>const CustomCircularProgressIndicator(color: kcPrimaryColor);
Widget _buildContinueButton(CoursePracticeQuestionViewModel viewModel) =>
CustomElevatedButton(
@ -93,7 +92,7 @@ class SelectableCoursePracticeQuestion
borderRadius: 12,
foregroundColor: kcWhite,
text: viewModel.currentQuestionIndex ==
viewModel.coursePracticeQuestions.length - 1
viewModel.coursePracticeQuestions.length - 1
? 'Finish'
: 'Continue',
backgroundColor:
@ -101,7 +100,7 @@ class SelectableCoursePracticeQuestion
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.selectedAnswers.containsKey((index + 1).toString())
? () async => await viewModel.nextQuestion(viewModel
? ()async =>await viewModel.nextQuestion(viewModel
.coursePracticeQuestions[
index + 1 < viewModel.coursePracticeQuestions.length
? index + 1
@ -109,5 +108,6 @@ class SelectableCoursePracticeQuestion
.questionId ??
0)
: null,
);
}

View File

@ -99,8 +99,7 @@ class WritingCoursePracticeQuestion
? _buildProgressIndicator()
: _buildContinueButton(viewModel);
Widget _buildProgressIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
Widget _buildProgressIndicator()=>const CustomCircularProgressIndicator(color: kcPrimaryColor);
Widget _buildContinueButton(CoursePracticeQuestionViewModel viewModel) =>
CustomElevatedButton(
@ -111,7 +110,7 @@ class WritingCoursePracticeQuestion
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: answerController.text.isNotEmpty
? () async => await viewModel.nextQuestion(
? ()async =>await viewModel.nextQuestion(
index + 1 < viewModel.coursePracticeQuestions.length
? index + 1
: index)

View File

@ -1,7 +1,7 @@
name: yimaru_app
description: A new Flutter project.
publish_to: 'none'
version: 0.1.3+4
version: 0.1.2+3
environment:
sdk: '>=3.0.3 <4.0.0'

View File

@ -8,40 +8,38 @@ import 'dart:ui' as _i10;
import 'package:audioplayers/audioplayers.dart' as _i4;
import 'package:dio/dio.dart' as _i2;
import 'package:firebase_messaging/firebase_messaging.dart' as _i32;
import 'package:firebase_messaging/firebase_messaging.dart' as _i30;
import 'package:flutter/material.dart' as _i8;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i7;
import 'package:permission_handler/permission_handler.dart' as _i27;
import 'package:permission_handler/permission_handler.dart' as _i25;
import 'package:stacked_services/stacked_services.dart' as _i6;
import 'package:waveform_recorder/waveform_recorder.dart' as _i5;
import 'package:yimaru_app/models/category.dart' as _i15;
import 'package:yimaru_app/models/course.dart' as _i21;
import 'package:yimaru_app/models/course_detail.dart' as _i35;
import 'package:yimaru_app/models/course_lesson.dart' as _i18;
import 'package:yimaru_app/models/course_progress.dart' as _i17;
import 'package:yimaru_app/models/level.dart' as _i22;
import 'package:yimaru_app/models/module.dart' as _i23;
import 'package:yimaru_app/models/practice.dart' as _i19;
import 'package:yimaru_app/models/practice_question.dart' as _i20;
import 'package:yimaru_app/models/course.dart' as _i17;
import 'package:yimaru_app/models/course_category.dart' as _i15;
import 'package:yimaru_app/models/course_detail.dart' as _i33;
import 'package:yimaru_app/models/course_lesson.dart' as _i19;
import 'package:yimaru_app/models/course_progress.dart' as _i18;
import 'package:yimaru_app/models/course_subcategory.dart' as _i16;
import 'package:yimaru_app/models/practice.dart' as _i20;
import 'package:yimaru_app/models/practice_question.dart' as _i21;
import 'package:yimaru_app/models/question.dart' as _i14;
import 'package:yimaru_app/models/subcategory.dart' as _i16;
import 'package:yimaru_app/models/user.dart' as _i12;
import 'package:yimaru_app/models/user_model.dart' as _i12;
import 'package:yimaru_app/services/api_service.dart' as _i13;
import 'package:yimaru_app/services/audio_player_service.dart' as _i36;
import 'package:yimaru_app/services/audio_player_service.dart' as _i34;
import 'package:yimaru_app/services/authentication_service.dart' as _i11;
import 'package:yimaru_app/services/course_service.dart' as _i34;
import 'package:yimaru_app/services/dio_service.dart' as _i24;
import 'package:yimaru_app/services/google_auth_service.dart' as _i29;
import 'package:yimaru_app/services/image_downloader_service.dart' as _i30;
import 'package:yimaru_app/services/image_picker_service.dart' as _i28;
import 'package:yimaru_app/services/notification_service.dart' as _i31;
import 'package:yimaru_app/services/permission_handler_service.dart' as _i26;
import 'package:yimaru_app/services/course_service.dart' as _i32;
import 'package:yimaru_app/services/dio_service.dart' as _i22;
import 'package:yimaru_app/services/google_auth_service.dart' as _i27;
import 'package:yimaru_app/services/image_downloader_service.dart' as _i28;
import 'package:yimaru_app/services/image_picker_service.dart' as _i26;
import 'package:yimaru_app/services/notification_service.dart' as _i29;
import 'package:yimaru_app/services/permission_handler_service.dart' as _i24;
import 'package:yimaru_app/services/secure_storage_service.dart' as _i3;
import 'package:yimaru_app/services/smart_auth_service.dart' as _i33;
import 'package:yimaru_app/services/status_checker_service.dart' as _i25;
import 'package:yimaru_app/services/voice_recorder_service.dart' as _i37;
import 'package:yimaru_app/ui/common/enmus.dart' as _i38;
import 'package:yimaru_app/services/smart_auth_service.dart' as _i31;
import 'package:yimaru_app/services/status_checker_service.dart' as _i23;
import 'package:yimaru_app/services/voice_recorder_service.dart' as _i35;
import 'package:yimaru_app/ui/common/enmus.dart' as _i36;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
@ -859,7 +857,7 @@ class MockAuthenticationService extends _i1.Mock
) as _i9.Future<void>);
@override
_i9.Future<void> saveUserData(_i12.User? data) => (super.noSuchMethod(
_i9.Future<void> saveUserData(_i12.UserModel? data) => (super.noSuchMethod(
Invocation.method(
#saveUserData,
[data],
@ -900,14 +898,14 @@ class MockAuthenticationService extends _i1.Mock
) as _i9.Future<void>);
@override
_i9.Future<_i12.User?> getUser() => (super.noSuchMethod(
_i9.Future<_i12.UserModel?> getUser() => (super.noSuchMethod(
Invocation.method(
#getUser,
[],
),
returnValue: _i9.Future<_i12.User?>.value(),
returnValueForMissingStub: _i9.Future<_i12.User?>.value(),
) as _i9.Future<_i12.User?>);
returnValue: _i9.Future<_i12.UserModel?>.value(),
returnValueForMissingStub: _i9.Future<_i12.UserModel?>.value(),
) as _i9.Future<_i12.UserModel?>);
@override
_i9.Future<void> logout() => (super.noSuchMethod(
@ -1055,7 +1053,7 @@ class MockApiService extends _i1.Mock implements _i13.ApiService {
) as _i9.Future<Map<String, dynamic>>);
@override
_i9.Future<Map<String, dynamic>> getProfileStatus(_i12.User? user) =>
_i9.Future<Map<String, dynamic>> getProfileStatus(_i12.UserModel? user) =>
(super.noSuchMethod(
Invocation.method(
#getProfileStatus,
@ -1126,54 +1124,68 @@ class MockApiService extends _i1.Mock implements _i13.ApiService {
) as _i9.Future<List<_i14.Question>>);
@override
_i9.Future<List<_i15.Category>> getCategories() => (super.noSuchMethod(
_i9.Future<List<_i15.CourseCategory>> getCourseCategories() =>
(super.noSuchMethod(
Invocation.method(
#getCourseCategories,
[],
),
returnValue: _i9.Future<List<_i15.Category>>.value(<_i15.Category>[]),
returnValueForMissingStub:
_i9.Future<List<_i15.Category>>.value(<_i15.Category>[]),
) as _i9.Future<List<_i15.Category>>);
returnValue: _i9.Future<List<_i15.CourseCategory>>.value(
<_i15.CourseCategory>[]),
returnValueForMissingStub: _i9.Future<List<_i15.CourseCategory>>.value(
<_i15.CourseCategory>[]),
) as _i9.Future<List<_i15.CourseCategory>>);
@override
_i9.Future<List<_i16.Subcategory>> getSubcategories(int? id) =>
_i9.Future<List<_i16.CourseSubcategory>> getCourseSubcategories(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCourseSubcategories,
[id],
),
returnValue:
_i9.Future<List<_i16.Subcategory>>.value(<_i16.Subcategory>[]),
returnValue: _i9.Future<List<_i16.CourseSubcategory>>.value(
<_i16.CourseSubcategory>[]),
returnValueForMissingStub:
_i9.Future<List<_i16.Subcategory>>.value(<_i16.Subcategory>[]),
) as _i9.Future<List<_i16.Subcategory>>);
_i9.Future<List<_i16.CourseSubcategory>>.value(
<_i16.CourseSubcategory>[]),
) as _i9.Future<List<_i16.CourseSubcategory>>);
@override
_i9.Future<List<_i17.CourseProgress>> getCourseProgress(int? id) =>
_i9.Future<List<_i17.Course>> getCourses(int? id) => (super.noSuchMethod(
Invocation.method(
#getCourses,
[id],
),
returnValue: _i9.Future<List<_i17.Course>>.value(<_i17.Course>[]),
returnValueForMissingStub:
_i9.Future<List<_i17.Course>>.value(<_i17.Course>[]),
) as _i9.Future<List<_i17.Course>>);
@override
_i9.Future<List<_i18.CourseProgress>> getCourseProgress(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCourseProgress,
[id],
),
returnValue: _i9.Future<List<_i17.CourseProgress>>.value(
<_i17.CourseProgress>[]),
returnValueForMissingStub: _i9.Future<List<_i17.CourseProgress>>.value(
<_i17.CourseProgress>[]),
) as _i9.Future<List<_i17.CourseProgress>>);
returnValue: _i9.Future<List<_i18.CourseProgress>>.value(
<_i18.CourseProgress>[]),
returnValueForMissingStub: _i9.Future<List<_i18.CourseProgress>>.value(
<_i18.CourseProgress>[]),
) as _i9.Future<List<_i18.CourseProgress>>);
@override
_i9.Future<List<_i18.CourseLesson>> getCourseLessons(int? id) =>
_i9.Future<List<_i19.CourseLesson>> getCourseLessons(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCourseLessons,
[id],
),
returnValue:
_i9.Future<List<_i18.CourseLesson>>.value(<_i18.CourseLesson>[]),
_i9.Future<List<_i19.CourseLesson>>.value(<_i19.CourseLesson>[]),
returnValueForMissingStub:
_i9.Future<List<_i18.CourseLesson>>.value(<_i18.CourseLesson>[]),
) as _i9.Future<List<_i18.CourseLesson>>);
_i9.Future<List<_i19.CourseLesson>>.value(<_i19.CourseLesson>[]),
) as _i9.Future<List<_i19.CourseLesson>>);
@override
_i9.Future<Map<String, dynamic>> completeLesson(int? id) =>
@ -1189,87 +1201,30 @@ class MockApiService extends _i1.Mock implements _i13.ApiService {
) as _i9.Future<Map<String, dynamic>>);
@override
_i9.Future<List<_i19.Practice>> getCoursePractices(int? id) =>
_i9.Future<List<_i20.Practice>> getCoursePractices(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCoursePractices,
[id],
),
returnValue: _i9.Future<List<_i19.Practice>>.value(<_i19.Practice>[]),
returnValue: _i9.Future<List<_i20.Practice>>.value(<_i20.Practice>[]),
returnValueForMissingStub:
_i9.Future<List<_i19.Practice>>.value(<_i19.Practice>[]),
) as _i9.Future<List<_i19.Practice>>);
_i9.Future<List<_i20.Practice>>.value(<_i20.Practice>[]),
) as _i9.Future<List<_i20.Practice>>);
@override
_i9.Future<List<_i20.PracticeQuestion>> getCoursePracticeQuestions(int? id) =>
_i9.Future<List<_i21.PracticeQuestion>> getCoursePracticeQuestions(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCoursePracticeQuestions,
[id],
),
returnValue: _i9.Future<List<_i20.PracticeQuestion>>.value(
<_i20.PracticeQuestion>[]),
returnValue: _i9.Future<List<_i21.PracticeQuestion>>.value(
<_i21.PracticeQuestion>[]),
returnValueForMissingStub:
_i9.Future<List<_i20.PracticeQuestion>>.value(
<_i20.PracticeQuestion>[]),
) as _i9.Future<List<_i20.PracticeQuestion>>);
@override
_i9.Future<_i14.Question?> getCoursePracticeQuestion(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCoursePracticeQuestion,
[id],
),
returnValue: _i9.Future<_i14.Question?>.value(),
returnValueForMissingStub: _i9.Future<_i14.Question?>.value(),
) as _i9.Future<_i14.Question?>);
@override
_i9.Future<List<_i16.Subcategory>> getLearnSubcategories() =>
(super.noSuchMethod(
Invocation.method(
#getLearnSubcategories,
[],
),
returnValue:
_i9.Future<List<_i16.Subcategory>>.value(<_i16.Subcategory>[]),
returnValueForMissingStub:
_i9.Future<List<_i16.Subcategory>>.value(<_i16.Subcategory>[]),
) as _i9.Future<List<_i16.Subcategory>>);
@override
_i9.Future<List<_i21.Course>> getCourses(int? id) => (super.noSuchMethod(
Invocation.method(
#getCourses,
[id],
),
returnValue: _i9.Future<List<_i21.Course>>.value(<_i21.Course>[]),
returnValueForMissingStub:
_i9.Future<List<_i21.Course>>.value(<_i21.Course>[]),
) as _i9.Future<List<_i21.Course>>);
@override
_i9.Future<List<_i22.Level>> getLevels(int? id) => (super.noSuchMethod(
Invocation.method(
#getLevels,
[id],
),
returnValue: _i9.Future<List<_i22.Level>>.value(<_i22.Level>[]),
returnValueForMissingStub:
_i9.Future<List<_i22.Level>>.value(<_i22.Level>[]),
) as _i9.Future<List<_i22.Level>>);
@override
_i9.Future<List<_i23.Module>> getModules(int? id) => (super.noSuchMethod(
Invocation.method(
#getModules,
[id],
),
returnValue: _i9.Future<List<_i23.Module>>.value(<_i23.Module>[]),
returnValueForMissingStub:
_i9.Future<List<_i23.Module>>.value(<_i23.Module>[]),
) as _i9.Future<List<_i23.Module>>);
_i9.Future<List<_i21.PracticeQuestion>>.value(
<_i21.PracticeQuestion>[]),
) as _i9.Future<List<_i21.PracticeQuestion>>);
}
/// A class which mocks [SecureStorageService].
@ -1372,7 +1327,7 @@ class MockSecureStorageService extends _i1.Mock
/// A class which mocks [DioService].
///
/// See the documentation for Mockito's code generation for more information.
class MockDioService extends _i1.Mock implements _i24.DioService {
class MockDioService extends _i1.Mock implements _i22.DioService {
@override
_i2.Dio get dio => (super.noSuchMethod(
Invocation.getter(#dio),
@ -1391,7 +1346,7 @@ class MockDioService extends _i1.Mock implements _i24.DioService {
///
/// See the documentation for Mockito's code generation for more information.
class MockStatusCheckerService extends _i1.Mock
implements _i25.StatusCheckerService {
implements _i23.StatusCheckerService {
@override
_i3.SecureStorageService get storage => (super.noSuchMethod(
Invocation.getter(#storage),
@ -1457,40 +1412,40 @@ class MockStatusCheckerService extends _i1.Mock
///
/// See the documentation for Mockito's code generation for more information.
class MockPermissionHandlerService extends _i1.Mock
implements _i26.PermissionHandlerService {
implements _i24.PermissionHandlerService {
@override
_i9.Future<_i27.PermissionStatus> requestPermission(
_i27.Permission? requestedPermission) =>
_i9.Future<_i25.PermissionStatus> requestPermission(
_i25.Permission? requestedPermission) =>
(super.noSuchMethod(
Invocation.method(
#requestPermission,
[requestedPermission],
),
returnValue: _i9.Future<_i27.PermissionStatus>.value(
_i27.PermissionStatus.denied),
returnValueForMissingStub: _i9.Future<_i27.PermissionStatus>.value(
_i27.PermissionStatus.denied),
) as _i9.Future<_i27.PermissionStatus>);
returnValue: _i9.Future<_i25.PermissionStatus>.value(
_i25.PermissionStatus.denied),
returnValueForMissingStub: _i9.Future<_i25.PermissionStatus>.value(
_i25.PermissionStatus.denied),
) as _i9.Future<_i25.PermissionStatus>);
@override
_i9.Future<_i27.PermissionStatus> request(_i27.Permission? permission) =>
_i9.Future<_i25.PermissionStatus> request(_i25.Permission? permission) =>
(super.noSuchMethod(
Invocation.method(
#request,
[permission],
),
returnValue: _i9.Future<_i27.PermissionStatus>.value(
_i27.PermissionStatus.denied),
returnValueForMissingStub: _i9.Future<_i27.PermissionStatus>.value(
_i27.PermissionStatus.denied),
) as _i9.Future<_i27.PermissionStatus>);
returnValue: _i9.Future<_i25.PermissionStatus>.value(
_i25.PermissionStatus.denied),
returnValueForMissingStub: _i9.Future<_i25.PermissionStatus>.value(
_i25.PermissionStatus.denied),
) as _i9.Future<_i25.PermissionStatus>);
}
/// A class which mocks [ImagePickerService].
///
/// See the documentation for Mockito's code generation for more information.
class MockImagePickerService extends _i1.Mock
implements _i28.ImagePickerService {
implements _i26.ImagePickerService {
@override
_i9.Future<String?> gallery() => (super.noSuchMethod(
Invocation.method(
@ -1515,7 +1470,7 @@ class MockImagePickerService extends _i1.Mock
/// A class which mocks [GoogleAuthService].
///
/// See the documentation for Mockito's code generation for more information.
class MockGoogleAuthService extends _i1.Mock implements _i29.GoogleAuthService {
class MockGoogleAuthService extends _i1.Mock implements _i27.GoogleAuthService {
@override
int get listenersCount => (super.noSuchMethod(
Invocation.getter(#listenersCount),
@ -1585,7 +1540,7 @@ class MockGoogleAuthService extends _i1.Mock implements _i29.GoogleAuthService {
///
/// See the documentation for Mockito's code generation for more information.
class MockImageDownloaderService extends _i1.Mock
implements _i30.ImageDownloaderService {
implements _i28.ImageDownloaderService {
@override
_i9.Future<String> downloader(String? networkImage) => (super.noSuchMethod(
Invocation.method(
@ -1614,7 +1569,7 @@ class MockImageDownloaderService extends _i1.Mock
///
/// See the documentation for Mockito's code generation for more information.
class MockNotificationService extends _i1.Mock
implements _i31.NotificationService {
implements _i29.NotificationService {
@override
_i9.Future<void> initialize() => (super.noSuchMethod(
Invocation.method(
@ -1636,7 +1591,7 @@ class MockNotificationService extends _i1.Mock
) as _i9.Future<void>);
@override
_i9.Future<void> showNotification(_i32.RemoteMessage? message) =>
_i9.Future<void> showNotification(_i30.RemoteMessage? message) =>
(super.noSuchMethod(
Invocation.method(
#showNotification,
@ -1670,7 +1625,7 @@ class MockNotificationService extends _i1.Mock
/// A class which mocks [SmartAuthService].
///
/// See the documentation for Mockito's code generation for more information.
class MockSmartAuthService extends _i1.Mock implements _i33.SmartAuthService {
class MockSmartAuthService extends _i1.Mock implements _i31.SmartAuthService {
@override
bool get listenForMultipleSms => (super.noSuchMethod(
Invocation.getter(#listenForMultipleSms),
@ -1702,26 +1657,26 @@ class MockSmartAuthService extends _i1.Mock implements _i33.SmartAuthService {
/// A class which mocks [CourseService].
///
/// See the documentation for Mockito's code generation for more information.
class MockCourseService extends _i1.Mock implements _i34.CourseService {
class MockCourseService extends _i1.Mock implements _i32.CourseService {
@override
_i9.Future<List<_i35.CourseDetail>> getCoursesDetail(int? id) =>
_i9.Future<List<_i33.CourseDetail>> getCoursesDetail(int? id) =>
(super.noSuchMethod(
Invocation.method(
#getCoursesDetail,
[id],
),
returnValue:
_i9.Future<List<_i35.CourseDetail>>.value(<_i35.CourseDetail>[]),
_i9.Future<List<_i33.CourseDetail>>.value(<_i33.CourseDetail>[]),
returnValueForMissingStub:
_i9.Future<List<_i35.CourseDetail>>.value(<_i35.CourseDetail>[]),
) as _i9.Future<List<_i35.CourseDetail>>);
_i9.Future<List<_i33.CourseDetail>>.value(<_i33.CourseDetail>[]),
) as _i9.Future<List<_i33.CourseDetail>>);
}
/// A class which mocks [AudioPlayerService].
///
/// See the documentation for Mockito's code generation for more information.
class MockAudioPlayerService extends _i1.Mock
implements _i36.AudioPlayerService {
implements _i34.AudioPlayerService {
@override
_i4.AudioPlayer get player => (super.noSuchMethod(
Invocation.getter(#player),
@ -1845,13 +1800,13 @@ class MockAudioPlayerService extends _i1.Mock
///
/// See the documentation for Mockito's code generation for more information.
class MockVoiceRecorderService extends _i1.Mock
implements _i37.VoiceRecorderService {
implements _i35.VoiceRecorderService {
@override
_i38.VoiceRecordingState get recordingState => (super.noSuchMethod(
_i36.VoiceRecordingState get recordingState => (super.noSuchMethod(
Invocation.getter(#recordingState),
returnValue: _i38.VoiceRecordingState.pending,
returnValueForMissingStub: _i38.VoiceRecordingState.pending,
) as _i38.VoiceRecordingState);
returnValue: _i36.VoiceRecordingState.pending,
returnValueForMissingStub: _i36.VoiceRecordingState.pending,
) as _i36.VoiceRecordingState);
@override
_i5.WaveformRecorderController get waveController => (super.noSuchMethod(

View File

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