Merge branch 'release/0.1.1'

- feat(course): Finalize course practice integration.
This commit is contained in:
BisratHailu 2026-04-08 18:16:21 +03:00
commit a00c39d4e5
41 changed files with 1899 additions and 723 deletions

View File

@ -50,6 +50,7 @@ import 'package:yimaru_app/ui/views/course_subcategory/course_subcategory_view.d
import 'package:yimaru_app/ui/views/course/course_view.dart'; import 'package:yimaru_app/ui/views/course/course_view.dart';
import 'package:yimaru_app/services/audio_player_service.dart'; import 'package:yimaru_app/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart'; import 'package:yimaru_app/services/voice_recorder_service.dart';
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_view.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -88,6 +89,7 @@ import 'package:yimaru_app/services/voice_recorder_service.dart';
MaterialRoute(page: DuolingoView), MaterialRoute(page: DuolingoView),
MaterialRoute(page: CourseSubcategoryView), MaterialRoute(page: CourseSubcategoryView),
MaterialRoute(page: CourseView), MaterialRoute(page: CourseView),
MaterialRoute(page: CoursePracticeQuestionView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [

View File

@ -5,14 +5,14 @@
// ************************************************************************** // **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i36; import 'package:flutter/material.dart' as _i37;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart' as _i1; import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i41; import 'package:stacked_services/stacked_services.dart' as _i42;
import 'package:yimaru_app/models/course.dart' as _i37; import 'package:yimaru_app/models/course.dart' as _i38;
import 'package:yimaru_app/models/course_category.dart' as _i39; import 'package:yimaru_app/models/course_category.dart' as _i40;
import 'package:yimaru_app/models/course_lesson.dart' as _i38; import 'package:yimaru_app/models/course_lesson.dart' as _i39;
import 'package:yimaru_app/models/course_subcategory.dart' as _i40; import 'package:yimaru_app/models/course_subcategory.dart' as _i41;
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
as _i9; as _i9;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i22; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i22;
@ -29,6 +29,8 @@ import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart'
as _i28; as _i28;
import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart' import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart'
as _i27; as _i27;
import 'package:yimaru_app/ui/views/course_practice_question/course_practice_question_view.dart'
as _i36;
import 'package:yimaru_app/ui/views/course_subcategory/course_subcategory_view.dart' import 'package:yimaru_app/ui/views/course_subcategory/course_subcategory_view.dart'
as _i34; as _i34;
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7;
@ -134,6 +136,8 @@ class Routes {
static const courseView = '/course-view'; static const courseView = '/course-view';
static const coursePracticeQuestionView = '/course-practice-question-view';
static const all = <String>{ static const all = <String>{
homeView, homeView,
onboardingView, onboardingView,
@ -169,6 +173,7 @@ class Routes {
duolingoView, duolingoView,
courseSubcategoryView, courseSubcategoryView,
courseView, courseView,
coursePracticeQuestionView,
}; };
} }
@ -310,17 +315,21 @@ class StackedRouter extends _i1.RouterBase {
Routes.courseView, Routes.courseView,
page: _i35.CourseView, page: _i35.CourseView,
), ),
_i1.RouteDef(
Routes.coursePracticeQuestionView,
page: _i36.CoursePracticeQuestionView,
),
]; ];
final _pagesMap = <Type, _i1.StackedRouteFactory>{ final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) { _i2.HomeView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(), builder: (context) => const _i2.HomeView(),
settings: data, settings: data,
); );
}, },
_i3.OnboardingView: (data) { _i3.OnboardingView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(), builder: (context) => const _i3.OnboardingView(),
settings: data, settings: data,
); );
@ -329,116 +338,116 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>( final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(), orElse: () => const StartupViewArguments(),
); );
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label), builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data, settings: data,
); );
}, },
_i5.ProfileView: (data) { _i5.ProfileView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(), builder: (context) => const _i5.ProfileView(),
settings: data, settings: data,
); );
}, },
_i6.ProfileDetailView: (data) { _i6.ProfileDetailView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(), builder: (context) => const _i6.ProfileDetailView(),
settings: data, settings: data,
); );
}, },
_i7.DownloadsView: (data) { _i7.DownloadsView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(), builder: (context) => const _i7.DownloadsView(),
settings: data, settings: data,
); );
}, },
_i8.ProgressView: (data) { _i8.ProgressView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(), builder: (context) => const _i8.ProgressView(),
settings: data, settings: data,
); );
}, },
_i9.AccountPrivacyView: (data) { _i9.AccountPrivacyView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.AccountPrivacyView(), builder: (context) => const _i9.AccountPrivacyView(),
settings: data, settings: data,
); );
}, },
_i10.SupportView: (data) { _i10.SupportView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.SupportView(), builder: (context) => const _i10.SupportView(),
settings: data, settings: data,
); );
}, },
_i11.TelegramSupportView: (data) { _i11.TelegramSupportView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.TelegramSupportView(), builder: (context) => const _i11.TelegramSupportView(),
settings: data, settings: data,
); );
}, },
_i12.CallSupportView: (data) { _i12.CallSupportView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.CallSupportView(), builder: (context) => const _i12.CallSupportView(),
settings: data, settings: data,
); );
}, },
_i13.LanguageView: (data) { _i13.LanguageView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.LanguageView(), builder: (context) => const _i13.LanguageView(),
settings: data, settings: data,
); );
}, },
_i14.PrivacyPolicyView: (data) { _i14.PrivacyPolicyView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.PrivacyPolicyView(), builder: (context) => const _i14.PrivacyPolicyView(),
settings: data, settings: data,
); );
}, },
_i15.TermsAndConditionsView: (data) { _i15.TermsAndConditionsView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.TermsAndConditionsView(), builder: (context) => const _i15.TermsAndConditionsView(),
settings: data, settings: data,
); );
}, },
_i16.RegisterView: (data) { _i16.RegisterView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.RegisterView(), builder: (context) => const _i16.RegisterView(),
settings: data, settings: data,
); );
}, },
_i17.LoginView: (data) { _i17.LoginView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.LoginView(), builder: (context) => const _i17.LoginView(),
settings: data, settings: data,
); );
}, },
_i18.LearnView: (data) { _i18.LearnView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LearnView(), builder: (context) => const _i18.LearnView(),
settings: data, settings: data,
); );
}, },
_i19.LearnLevelView: (data) { _i19.LearnLevelView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnLevelView(), builder: (context) => const _i19.LearnLevelView(),
settings: data, settings: data,
); );
}, },
_i20.LearnModuleView: (data) { _i20.LearnModuleView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnModuleView(), builder: (context) => const _i20.LearnModuleView(),
settings: data, settings: data,
); );
}, },
_i21.WelcomeView: (data) { _i21.WelcomeView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.WelcomeView(), builder: (context) => const _i21.WelcomeView(),
settings: data, settings: data,
); );
}, },
_i22.AssessmentView: (data) { _i22.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false); final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i22.AssessmentView(key: args.key, data: args.data), _i22.AssessmentView(key: args.key, data: args.data),
settings: data, settings: data,
@ -446,7 +455,7 @@ class StackedRouter extends _i1.RouterBase {
}, },
_i23.LearnLessonView: (data) { _i23.LearnLessonView: (data) {
final args = data.getArgs<LearnLessonViewArguments>(nullOk: false); final args = data.getArgs<LearnLessonViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i23.LearnLessonView( builder: (context) => _i23.LearnLessonView(
key: args.key, key: args.key,
title: args.title, title: args.title,
@ -458,14 +467,14 @@ class StackedRouter extends _i1.RouterBase {
); );
}, },
_i24.ForgetPasswordView: (data) { _i24.ForgetPasswordView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.ForgetPasswordView(), builder: (context) => const _i24.ForgetPasswordView(),
settings: data, settings: data,
); );
}, },
_i25.LearnLessonDetailView: (data) { _i25.LearnLessonDetailView: (data) {
final args = data.getArgs<LearnLessonDetailViewArguments>(nullOk: false); final args = data.getArgs<LearnLessonDetailViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i25.LearnLessonDetailView( builder: (context) => _i25.LearnLessonDetailView(
key: args.key, key: args.key,
title: args.title, title: args.title,
@ -476,7 +485,7 @@ class StackedRouter extends _i1.RouterBase {
}, },
_i26.LearnPracticeView: (data) { _i26.LearnPracticeView: (data) {
final args = data.getArgs<LearnPracticeViewArguments>(nullOk: false); final args = data.getArgs<LearnPracticeViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i26.LearnPracticeView( builder: (context) => _i26.LearnPracticeView(
key: args.key, key: args.key,
title: args.title, title: args.title,
@ -488,7 +497,7 @@ class StackedRouter extends _i1.RouterBase {
}, },
_i27.CoursePracticeView: (data) { _i27.CoursePracticeView: (data) {
final args = data.getArgs<CoursePracticeViewArguments>(nullOk: false); final args = data.getArgs<CoursePracticeViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i27.CoursePracticeView(key: args.key, id: args.id), _i27.CoursePracticeView(key: args.key, id: args.id),
settings: data, settings: data,
@ -496,21 +505,21 @@ class StackedRouter extends _i1.RouterBase {
}, },
_i28.CoursePaymentView: (data) { _i28.CoursePaymentView: (data) {
final args = data.getArgs<CoursePaymentViewArguments>(nullOk: false); final args = data.getArgs<CoursePaymentViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i28.CoursePaymentView(key: args.key, course: args.course), _i28.CoursePaymentView(key: args.key, course: args.course),
settings: data, settings: data,
); );
}, },
_i29.CourseCategoryView: (data) { _i29.CourseCategoryView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i29.CourseCategoryView(), builder: (context) => const _i29.CourseCategoryView(),
settings: data, settings: data,
); );
}, },
_i30.FailureView: (data) { _i30.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false); final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i30.FailureView(key: args.key, label: args.label), _i30.FailureView(key: args.key, label: args.label),
settings: data, settings: data,
@ -518,7 +527,7 @@ class StackedRouter extends _i1.RouterBase {
}, },
_i31.CourseLessonView: (data) { _i31.CourseLessonView: (data) {
final args = data.getArgs<CourseLessonViewArguments>(nullOk: false); final args = data.getArgs<CourseLessonViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i31.CourseLessonView(key: args.key, course: args.course), _i31.CourseLessonView(key: args.key, course: args.course),
settings: data, settings: data,
@ -526,21 +535,21 @@ class StackedRouter extends _i1.RouterBase {
}, },
_i32.CourseLessonDetailView: (data) { _i32.CourseLessonDetailView: (data) {
final args = data.getArgs<CourseLessonDetailViewArguments>(nullOk: false); final args = data.getArgs<CourseLessonDetailViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i32.CourseLessonDetailView(key: args.key, lesson: args.lesson), _i32.CourseLessonDetailView(key: args.key, lesson: args.lesson),
settings: data, settings: data,
); );
}, },
_i33.DuolingoView: (data) { _i33.DuolingoView: (data) {
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => const _i33.DuolingoView(), builder: (context) => const _i33.DuolingoView(),
settings: data, settings: data,
); );
}, },
_i34.CourseSubcategoryView: (data) { _i34.CourseSubcategoryView: (data) {
final args = data.getArgs<CourseSubcategoryViewArguments>(nullOk: false); final args = data.getArgs<CourseSubcategoryViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i34.CourseSubcategoryView(key: args.key, category: args.category), _i34.CourseSubcategoryView(key: args.key, category: args.category),
settings: data, settings: data,
@ -548,12 +557,21 @@ class StackedRouter extends _i1.RouterBase {
}, },
_i35.CourseView: (data) { _i35.CourseView: (data) {
final args = data.getArgs<CourseViewArguments>(nullOk: false); final args = data.getArgs<CourseViewArguments>(nullOk: false);
return _i36.MaterialPageRoute<dynamic>( return _i37.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i35.CourseView(key: args.key, subcategory: args.subcategory), _i35.CourseView(key: args.key, subcategory: args.subcategory),
settings: data, settings: data,
); );
}, },
_i36.CoursePracticeQuestionView: (data) {
final args =
data.getArgs<CoursePracticeQuestionViewArguments>(nullOk: false);
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i36.CoursePracticeQuestionView(key: args.key, id: args.id),
settings: data,
);
},
}; };
@override @override
@ -569,7 +587,7 @@ class StartupViewArguments {
this.label = 'Loading', this.label = 'Loading',
}); });
final _i36.Key? key; final _i37.Key? key;
final String label; final String label;
@ -596,7 +614,7 @@ class AssessmentViewArguments {
required this.data, required this.data,
}); });
final _i36.Key? key; final _i37.Key? key;
final Map<String, dynamic> data; final Map<String, dynamic> data;
@ -627,7 +645,7 @@ class LearnLessonViewArguments {
required this.description, required this.description,
}); });
final _i36.Key? key; final _i37.Key? key;
final String title; final String title;
@ -674,7 +692,7 @@ class LearnLessonDetailViewArguments {
required this.description, required this.description,
}); });
final _i36.Key? key; final _i37.Key? key;
final String title; final String title;
@ -714,7 +732,7 @@ class LearnPracticeViewArguments {
required this.buttonLabel, required this.buttonLabel,
}); });
final _i36.Key? key; final _i37.Key? key;
final String title; final String title;
@ -755,7 +773,7 @@ class CoursePracticeViewArguments {
required this.id, required this.id,
}); });
final _i36.Key? key; final _i37.Key? key;
final int id; final int id;
@ -782,9 +800,9 @@ class CoursePaymentViewArguments {
required this.course, required this.course,
}); });
final _i36.Key? key; final _i37.Key? key;
final _i37.Course course; final _i38.Course course;
@override @override
String toString() { String toString() {
@ -809,7 +827,7 @@ class FailureViewArguments {
required this.label, required this.label,
}); });
final _i36.Key? key; final _i37.Key? key;
final String label; final String label;
@ -836,9 +854,9 @@ class CourseLessonViewArguments {
required this.course, required this.course,
}); });
final _i36.Key? key; final _i37.Key? key;
final _i37.Course course; final _i38.Course course;
@override @override
String toString() { String toString() {
@ -863,9 +881,9 @@ class CourseLessonDetailViewArguments {
required this.lesson, required this.lesson,
}); });
final _i36.Key? key; final _i37.Key? key;
final _i38.CourseLesson lesson; final _i39.CourseLesson lesson;
@override @override
String toString() { String toString() {
@ -890,9 +908,9 @@ class CourseSubcategoryViewArguments {
required this.category, required this.category,
}); });
final _i36.Key? key; final _i37.Key? key;
final _i39.CourseCategory category; final _i40.CourseCategory category;
@override @override
String toString() { String toString() {
@ -917,9 +935,9 @@ class CourseViewArguments {
required this.subcategory, required this.subcategory,
}); });
final _i36.Key? key; final _i37.Key? key;
final _i40.CourseSubcategory subcategory; final _i41.CourseSubcategory subcategory;
@override @override
String toString() { String toString() {
@ -938,7 +956,34 @@ class CourseViewArguments {
} }
} }
extension NavigatorStateExtension on _i41.NavigationService { class CoursePracticeQuestionViewArguments {
const CoursePracticeQuestionViewArguments({
this.key,
required this.id,
});
final _i37.Key? key;
final int id;
@override
String toString() {
return '{"key": "$key", "id": "$id"}';
}
@override
bool operator ==(covariant CoursePracticeQuestionViewArguments other) {
if (identical(this, other)) return true;
return other.key == key && other.id == id;
}
@override
int get hashCode {
return key.hashCode ^ id.hashCode;
}
}
extension NavigatorStateExtension on _i42.NavigationService {
Future<dynamic> navigateToHomeView([ Future<dynamic> navigateToHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -968,7 +1013,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToStartupView({ Future<dynamic> navigateToStartupView({
_i36.Key? key, _i37.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1223,7 +1268,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToAssessmentView({ Future<dynamic> navigateToAssessmentView({
_i36.Key? key, _i37.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1240,7 +1285,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToLearnLessonView({ Future<dynamic> navigateToLearnLessonView({
_i36.Key? key, _i37.Key? key,
required String title, required String title,
required String topics, required String topics,
required String subtitle, required String subtitle,
@ -1281,7 +1326,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToLearnLessonDetailView({ Future<dynamic> navigateToLearnLessonDetailView({
_i36.Key? key, _i37.Key? key,
required String title, required String title,
required List<Map<String, dynamic>> practices, required List<Map<String, dynamic>> practices,
required String description, required String description,
@ -1304,7 +1349,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToLearnPracticeView({ Future<dynamic> navigateToLearnPracticeView({
_i36.Key? key, _i37.Key? key,
required String title, required String title,
required String subtitle, required String subtitle,
required List<Map<String, dynamic>> practices, required List<Map<String, dynamic>> practices,
@ -1329,7 +1374,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToCoursePracticeView({ Future<dynamic> navigateToCoursePracticeView({
_i36.Key? key, _i37.Key? key,
required int id, required int id,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1346,8 +1391,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToCoursePaymentView({ Future<dynamic> navigateToCoursePaymentView({
_i36.Key? key, _i37.Key? key,
required _i37.Course course, required _i38.Course course,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1377,7 +1422,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToFailureView({ Future<dynamic> navigateToFailureView({
_i36.Key? key, _i37.Key? key,
required String label, required String label,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1394,8 +1439,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToCourseLessonView({ Future<dynamic> navigateToCourseLessonView({
_i36.Key? key, _i37.Key? key,
required _i37.Course course, required _i38.Course course,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1411,8 +1456,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToCourseLessonDetailView({ Future<dynamic> navigateToCourseLessonDetailView({
_i36.Key? key, _i37.Key? key,
required _i38.CourseLesson lesson, required _i39.CourseLesson lesson,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1442,8 +1487,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToCourseSubcategoryView({ Future<dynamic> navigateToCourseSubcategoryView({
_i36.Key? key, _i37.Key? key,
required _i39.CourseCategory category, required _i40.CourseCategory category,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1459,8 +1504,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> navigateToCourseView({ Future<dynamic> navigateToCourseView({
_i36.Key? key, _i37.Key? key,
required _i40.CourseSubcategory subcategory, required _i41.CourseSubcategory subcategory,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1475,6 +1520,23 @@ extension NavigatorStateExtension on _i41.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> navigateToCoursePracticeQuestionView({
_i37.Key? key,
required int id,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
return navigateTo<dynamic>(Routes.coursePracticeQuestionView,
arguments: CoursePracticeQuestionViewArguments(key: key, id: id),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([ Future<dynamic> replaceWithHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1504,7 +1566,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithStartupView({ Future<dynamic> replaceWithStartupView({
_i36.Key? key, _i37.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1759,7 +1821,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithAssessmentView({ Future<dynamic> replaceWithAssessmentView({
_i36.Key? key, _i37.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1776,7 +1838,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithLearnLessonView({ Future<dynamic> replaceWithLearnLessonView({
_i36.Key? key, _i37.Key? key,
required String title, required String title,
required String topics, required String topics,
required String subtitle, required String subtitle,
@ -1817,7 +1879,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithLearnLessonDetailView({ Future<dynamic> replaceWithLearnLessonDetailView({
_i36.Key? key, _i37.Key? key,
required String title, required String title,
required List<Map<String, dynamic>> practices, required List<Map<String, dynamic>> practices,
required String description, required String description,
@ -1840,7 +1902,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithLearnPracticeView({ Future<dynamic> replaceWithLearnPracticeView({
_i36.Key? key, _i37.Key? key,
required String title, required String title,
required String subtitle, required String subtitle,
required List<Map<String, dynamic>> practices, required List<Map<String, dynamic>> practices,
@ -1865,7 +1927,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithCoursePracticeView({ Future<dynamic> replaceWithCoursePracticeView({
_i36.Key? key, _i37.Key? key,
required int id, required int id,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1882,8 +1944,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithCoursePaymentView({ Future<dynamic> replaceWithCoursePaymentView({
_i36.Key? key, _i37.Key? key,
required _i37.Course course, required _i38.Course course,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1913,7 +1975,7 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithFailureView({ Future<dynamic> replaceWithFailureView({
_i36.Key? key, _i37.Key? key,
required String label, required String label,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1930,8 +1992,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithCourseLessonView({ Future<dynamic> replaceWithCourseLessonView({
_i36.Key? key, _i37.Key? key,
required _i37.Course course, required _i38.Course course,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1947,8 +2009,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithCourseLessonDetailView({ Future<dynamic> replaceWithCourseLessonDetailView({
_i36.Key? key, _i37.Key? key,
required _i38.CourseLesson lesson, required _i39.CourseLesson lesson,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1978,8 +2040,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithCourseSubcategoryView({ Future<dynamic> replaceWithCourseSubcategoryView({
_i36.Key? key, _i37.Key? key,
required _i39.CourseCategory category, required _i40.CourseCategory category,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -1995,8 +2057,8 @@ extension NavigatorStateExtension on _i41.NavigationService {
} }
Future<dynamic> replaceWithCourseView({ Future<dynamic> replaceWithCourseView({
_i36.Key? key, _i37.Key? key,
required _i40.CourseSubcategory subcategory, required _i41.CourseSubcategory subcategory,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
@ -2010,4 +2072,21 @@ extension NavigatorStateExtension on _i41.NavigationService {
parameters: parameters, parameters: parameters,
transition: transition); transition: transition);
} }
Future<dynamic> replaceWithCoursePracticeQuestionView({
_i37.Key? key,
required int id,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
return replaceWith<dynamic>(Routes.coursePracticeQuestionView,
arguments: CoursePracticeQuestionViewArguments(key: key, id: id),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
} }

View File

@ -23,15 +23,15 @@ class CourseLesson {
@JsonKey(name: 'video_url') @JsonKey(name: 'video_url')
String? videoUrl; String? videoUrl;
@JsonKey(name: 'vimeo_status')
String? vimeoStatus;
@JsonKey(name: 'instructor_id') @JsonKey(name: 'instructor_id')
int? instructorId; int? instructorId;
@JsonKey(name: 'sub_course_id') @JsonKey(name: 'sub_course_id')
int? courseId; int? courseId;
@JsonKey(name: 'vimeo_status')
String? vimeoStatus;
@JsonKey(name: 'display_order') @JsonKey(name: 'display_order')
int? displayOrder; int? displayOrder;

View File

@ -5,12 +5,12 @@ part 'option.g.dart';
class Option { class Option {
final int? id; final int? id;
@JsonKey(name: 'option_text')
final String? optionText;
@JsonKey(name: 'is_correct') @JsonKey(name: 'is_correct')
final bool? isCorrect; final bool? isCorrect;
@JsonKey(name: 'option_text')
final String? optionText;
const Option({this.id, this.optionText, this.isCorrect}); const Option({this.id, this.optionText, this.isCorrect});
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json); factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);

View File

@ -1,9 +1,9 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/option.dart'; import 'package:yimaru_app/models/option.dart';
part 'assessment.g.dart'; part 'question.g.dart';
@JsonSerializable() @JsonSerializable()
class Assessment { class Question {
final int? id; final int? id;
final int? points; final int? points;
@ -21,7 +21,7 @@ class Assessment {
@JsonKey(name: 'difficulty_level') @JsonKey(name: 'difficulty_level')
final String? difficultyLevel; final String? difficultyLevel;
const Assessment({ const Question({
this.id, this.id,
this.points, this.points,
this.status, this.status,
@ -31,8 +31,8 @@ class Assessment {
this.difficultyLevel, this.difficultyLevel,
}); });
factory Assessment.fromJson(Map<String, dynamic> json) => factory Question.fromJson(Map<String, dynamic> json) =>
_$AssessmentFromJson(json); _$QuestionFromJson(json);
Map<String, dynamic> toJson() => _$AssessmentToJson(this); Map<String, dynamic> toJson() => _$QuestionToJson(this);
} }

View File

@ -1,12 +1,12 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'assessment.dart'; part of 'question.dart';
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Assessment _$AssessmentFromJson(Map<String, dynamic> json) => Assessment( Question _$QuestionFromJson(Map<String, dynamic> json) => Question(
id: (json['id'] as num?)?.toInt(), id: (json['id'] as num?)?.toInt(),
points: (json['points'] as num?)?.toInt(), points: (json['points'] as num?)?.toInt(),
status: json['status'] as String?, status: json['status'] as String?,
@ -18,8 +18,7 @@ Assessment _$AssessmentFromJson(Map<String, dynamic> json) => Assessment(
difficultyLevel: json['difficulty_level'] as String?, difficultyLevel: json['difficulty_level'] as String?,
); );
Map<String, dynamic> _$AssessmentToJson(Assessment instance) => Map<String, dynamic> _$QuestionToJson(Question instance) => <String, dynamic>{
<String, dynamic>{
'id': instance.id, 'id': instance.id,
'points': instance.points, 'points': instance.points,
'status': instance.status, 'status': instance.status,

View File

@ -1,5 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:yimaru_app/models/assessment.dart'; import 'package:yimaru_app/models/question.dart';
import 'package:yimaru_app/models/course_subcategory.dart'; import 'package:yimaru_app/models/course_subcategory.dart';
import 'package:yimaru_app/models/course_category.dart'; import 'package:yimaru_app/models/course_category.dart';
import 'package:yimaru_app/models/course_lesson.dart'; import 'package:yimaru_app/models/course_lesson.dart';
@ -18,7 +18,7 @@ class ApiService {
// Dependency injection // Dependency injection
final _service = locator<DioService>(); final _service = locator<DioService>();
// Register // Register with email
Future<Map<String, dynamic>> registerWithEmail( Future<Map<String, dynamic>> registerWithEmail(
Map<String, dynamic> data) async { Map<String, dynamic> data) async {
try { try {
@ -46,7 +46,7 @@ class ApiService {
} }
} }
// Email login // Login
Future<Map<String, dynamic>> login(Map<String, dynamic> data) async { Future<Map<String, dynamic>> login(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
@ -74,7 +74,7 @@ class ApiService {
} }
} }
// Google login // Google auth
Future<Map<String, dynamic>> googleAuth(Map<String, dynamic> data) async { Future<Map<String, dynamic>> googleAuth(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
@ -211,7 +211,7 @@ class ApiService {
} }
} }
// Profile completed // GEt profile completion status
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async { Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
try { try {
Response response = await _service.dio.get( Response response = await _service.dio.get(
@ -238,7 +238,7 @@ class ApiService {
} }
} }
// Get profile // Get profile data
Future<Map<String, dynamic>> getProfileData(int? userId) async { Future<Map<String, dynamic>> getProfileData(int? userId) async {
try { try {
Response response = await _service.dio.get( Response response = await _service.dio.get(
@ -345,10 +345,10 @@ class ApiService {
} }
} }
// Assessments // Get assessments
Future<List<Assessment>> getAssessments() async { Future<List<Question>> getAssessments() async {
try { try {
List<Assessment> assessments = []; List<Question> assessments = [];
final Response response = final Response response =
await _service.dio.get('$kBaseUrl/$kAssessmentsUrl'); await _service.dio.get('$kBaseUrl/$kAssessmentsUrl');
@ -358,7 +358,7 @@ class ApiService {
var decodedData = data['data'] as List; var decodedData = data['data'] as List;
assessments = decodedData.map( assessments = decodedData.map(
(e) { (e) {
return Assessment.fromJson(e); return Question.fromJson(e);
}, },
).toList(); ).toList();
return assessments; return assessments;
@ -369,7 +369,7 @@ class ApiService {
} }
} }
// Course categories // Get course categories
Future<List<CourseCategory>> getCourseCategories() async { Future<List<CourseCategory>> getCourseCategories() async {
try { try {
List<CourseCategory> categories = []; List<CourseCategory> categories = [];
@ -393,7 +393,7 @@ class ApiService {
} }
} }
// Course subcategory // Get course subcategory
Future<List<CourseSubcategory>> getCourseSubcategories(int id) async { Future<List<CourseSubcategory>> getCourseSubcategories(int id) async {
try { try {
List<CourseSubcategory> subcategories = []; List<CourseSubcategory> subcategories = [];
@ -417,7 +417,7 @@ class ApiService {
} }
} }
// Sub-courses // Get courses
Future<List<Course>> getCourses(int id) async { Future<List<Course>> getCourses(int id) async {
try { try {
List<Course> courses = []; List<Course> courses = [];
@ -441,7 +441,7 @@ class ApiService {
} }
} }
// Course progress // Get course progress
Future<List<CourseProgress>> getCourseProgress(int id) async { Future<List<CourseProgress>> getCourseProgress(int id) async {
try { try {
List<CourseProgress> courseProgress = []; List<CourseProgress> courseProgress = [];
@ -465,7 +465,7 @@ class ApiService {
} }
} }
// Course videos // Get course lessons
Future<List<CourseLesson>> getCourseLessons(int id) async { Future<List<CourseLesson>> getCourseLessons(int id) async {
try { try {
List<CourseLesson> courseLessons = []; List<CourseLesson> courseLessons = [];
@ -513,12 +513,12 @@ class ApiService {
} }
// Course practices // Course practices
Future<List<Practice>> getCoursePractices(Map<String, dynamic> data) async { Future<List<Practice>> getCoursePractices(int id) async {
try { try {
List<Practice> coursePractices = []; List<Practice> coursePractices = [];
final Response response = await _service.dio final Response response = await _service.dio.get(
.get('$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice', data: data); '$kBaseUrl/$kPracticeBaseUrl/$kCoursePractice?owner_type=SUB_COURSE&owner_id=$id');
if (response.statusCode == 200) { if (response.statusCode == 200) {
var data = response.data; var data = response.data;
@ -536,7 +536,7 @@ class ApiService {
} }
} }
// Course practic questions // Get course practic questions
Future<List<PracticeQuestion>> getCoursePracticeQuestions(int id) async { Future<List<PracticeQuestion>> getCoursePracticeQuestions(int id) async {
try { try {
List<PracticeQuestion> coursePracticeQuestions = []; List<PracticeQuestion> coursePracticeQuestions = [];
@ -559,4 +559,21 @@ class ApiService {
return []; return [];
} }
} }
// Get course practice question
Future<Question?> getCoursePracticeQuestion(int id) async {
try {
final Response response =
await _service.dio.get('$kBaseUrl/$kCoursePracticeQuestion/$id');
if (response.statusCode == 200) {
Question question = Question.fromJson(response.data['data']);
return question;
}
return null;
} catch (e) {
return null;
}
}
} }

View File

@ -4,6 +4,7 @@ import 'package:stacked/stacked.dart';
import '../ui/common/helper_functions.dart'; import '../ui/common/helper_functions.dart';
class AudioPlayerService with ListenableServiceMixin { class AudioPlayerService with ListenableServiceMixin {
// Player initialization
final AudioPlayer _player = AudioPlayer(); final AudioPlayer _player = AudioPlayer();
AudioPlayer get player => _player; AudioPlayer get player => _player;
@ -30,8 +31,6 @@ class AudioPlayerService with ListenableServiceMixin {
} }
Future<void> playLocal(String url) async { Future<void> playLocal(String url) async {
await _player.play(UrlSource(url)); await _player.play(UrlSource(url));
} }

View File

@ -67,25 +67,11 @@ class AuthenticationService with ListenableServiceMixin {
profileCompleted: await _secureService.getBool('profileCompleted'), profileCompleted: await _secureService.getBool('profileCompleted'),
); );
/* UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
userId: _user?.userId,
country: _user?.country,
lastName: _user?.lastName,
birthday: _user?.birthday,
firstName: _user?.firstName,
occupation: _user?.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profilePicture: _user?.profilePicture,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profileCompleted: await _secureService.getBool('profileCompleted'));
*/
notifyListeners(); notifyListeners();
} }
// Save profile picture
Future<void> saveProfilePicture(String image) async { Future<void> saveProfilePicture(String image) async {
await _secureService.setString('profilePicture', image); await _secureService.setString('profilePicture', image);
_user = _user?.copyWith( _user = _user?.copyWith(
@ -93,26 +79,10 @@ class AuthenticationService with ListenableServiceMixin {
profilePicture: await _secureService.getString('profilePicture'), profilePicture: await _secureService.getString('profilePicture'),
); );
/*UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
userId: _user?.userId,
country: _user?.country,
lastName: _user?.lastName,
birthday: _user?.birthday,
firstName: _user?.firstName,
occupation: _user?.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profilePicture'),
);
*/
notifyListeners(); notifyListeners();
} }
// Save user data
Future<void> saveUserData(UserModel data) async { Future<void> saveUserData(UserModel data) async {
await _secureService.setBool('userInfoLoaded', true); await _secureService.setBool('userInfoLoaded', true);
await _secureService.setBool( await _secureService.setBool(
@ -145,6 +115,7 @@ class AuthenticationService with ListenableServiceMixin {
notifyListeners(); notifyListeners();
} }
// Update user data
Future<void> updateUserData(Map<String, dynamic> data) async { Future<void> updateUserData(Map<String, dynamic> data) async {
await _secureService.setString('region', data['region']); await _secureService.setString('region', data['region']);
await _secureService.setString('gender', data['gender']); await _secureService.setString('gender', data['gender']);
@ -164,31 +135,19 @@ class AuthenticationService with ListenableServiceMixin {
occupation: await _secureService.getString('occupation'), occupation: await _secureService.getString('occupation'),
); );
/*UserModel(
email: _user?.email,
userId: _user?.userId,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profilePicture: _user?.profilePicture,
profileCompleted: _user?.profileCompleted,
region: await _secureService.getString('region'),
gender: await _secureService.getString('gender'),
country: await _secureService.getString('country'),
lastName: await _secureService.getString('lastName'),
birthday: await _secureService.getString('birthday'),
firstName: await _secureService.getString('firstName'),
occupation: await _secureService.getString('occupation'),
);*/
notifyListeners(); notifyListeners();
} }
// Check first time install
Future<bool> isFirstTimeInstall() async => Future<bool> isFirstTimeInstall() async =>
await _secureService.getBool('firstTimeInstall') ?? true; await _secureService.getBool('firstTimeInstall') ?? true;
// Set first time install
Future<void> setFirstTimeInstall(bool value) async { Future<void> setFirstTimeInstall(bool value) async {
await _secureService.setBool('firstTimeInstall', value); await _secureService.setBool('firstTimeInstall', value);
} }
// Get user data
Future<UserModel?> getUser() async { Future<UserModel?> getUser() async {
_user = UserModel( _user = UserModel(
userId: await _secureService.getInt('userId'), userId: await _secureService.getInt('userId'),
@ -209,6 +168,7 @@ class AuthenticationService with ListenableServiceMixin {
return _user; return _user;
} }
// Logout
Future<void> logout() async { Future<void> logout() async {
bool firstTimeInstall = await isFirstTimeInstall(); bool firstTimeInstall = await isFirstTimeInstall();
_user = null; _user = null;

View File

@ -5,8 +5,10 @@ import 'package:yimaru_app/services/api_service.dart';
import '../models/course_detail.dart'; import '../models/course_detail.dart';
class CourseService { class CourseService {
// Dependency injection
final _apiService = locator<ApiService>(); final _apiService = locator<ApiService>();
// Get course detail
Future<List<CourseDetail>> getCoursesDetail(int id) async { Future<List<CourseDetail>> getCoursesDetail(int id) async {
final courses = await _apiService.getCourses(id); final courses = await _apiService.getCourses(id);
final progress = await _apiService.getCourseProgress(id); final progress = await _apiService.getCourseProgress(id);

View File

@ -2,11 +2,14 @@ import 'package:pinput/pinput.dart';
import 'package:smart_auth/smart_auth.dart'; import 'package:smart_auth/smart_auth.dart';
class SmartAuthService implements SmsRetriever { class SmartAuthService implements SmsRetriever {
// Instance initialization
final SmartAuth _smartAuth = SmartAuth.instance; final SmartAuth _smartAuth = SmartAuth.instance;
// Dispose listener
@override @override
Future<void> dispose() => _smartAuth.removeUserConsentApiListener(); Future<void> dispose() => _smartAuth.removeUserConsentApiListener();
// Get sms code
@override @override
Future<String?> getSmsCode() async { Future<String?> getSmsCode() async {
final res = await _smartAuth.getSmsWithUserConsentApi(); final res = await _smartAuth.getSmsWithUserConsentApi();
@ -21,6 +24,7 @@ class SmartAuthService implements SmsRetriever {
} }
} }
// Listen multiple sms
@override @override
bool get listenForMultipleSms => true; bool get listenForMultipleSms => true;
} }

View File

@ -3,29 +3,32 @@ import 'package:waveform_recorder/waveform_recorder.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
class VoiceRecorderService with ListenableServiceMixin { class VoiceRecorderService with ListenableServiceMixin {
// Recording states
VoiceRecordingState _recordingState = VoiceRecordingState.pending; VoiceRecordingState _recordingState = VoiceRecordingState.pending;
VoiceRecordingState get recordingState => _recordingState; VoiceRecordingState get recordingState => _recordingState;
// Voice recorder controller
final WaveformRecorderController _waveController = final WaveformRecorderController _waveController =
WaveformRecorderController(); WaveformRecorderController();
WaveformRecorderController get waveController => _waveController; WaveformRecorderController get waveController => _waveController;
// Start voice recording
Future<void> startRecording() async { Future<void> startRecording() async {
await _waveController.startRecording(); await _waveController.startRecording();
_recordingState = VoiceRecordingState.recording; _recordingState = VoiceRecordingState.recording;
notifyListeners(); notifyListeners();
} }
// Stop voice recording
Future<void> stopRecording() async { Future<void> stopRecording() async {
await _waveController.stopRecording(); await _waveController.stopRecording();
_recordingState = VoiceRecordingState.pending; _recordingState = VoiceRecordingState.pending;
notifyListeners(); notifyListeners();
} }
// Get recorded audio
Future<String?> getRecordedAudio() async { Future<String?> getRecordedAudio() async {
final file = _waveController.file; final file = _waveController.file;
print('RECORDED $file'); print('RECORDED $file');

View File

@ -1,5 +1,4 @@
String kBaseUrl = 'https://api.yimaruacademy.com'; String kBaseUrl = 'https://api.yimaruacademy.com';
//String baseUrl = 'https://api.yimaru.yaltopia.com';
String kCoursesUrl = 'courses'; String kCoursesUrl = 'courses';
@ -41,6 +40,8 @@ String kProfileStatusUrl = 'is-profile-completed';
String kCourseBaseUrl = 'api/v1/course-management'; String kCourseBaseUrl = 'api/v1/course-management';
String kCoursePracticeQuestion = 'api/v1/questions';
String kLessonProgressUrl = 'api/v1/progress/videos'; String kLessonProgressUrl = 'api/v1/progress/videos';
String kGoogleAuthUrl = 'api/v1/auth/google/android'; String kGoogleAuthUrl = 'api/v1/auth/google/android';

View File

@ -3,14 +3,16 @@ const String ksHomeBottomSheetTitle = 'Build Great Apps!';
const String ksSuggestion = const String ksSuggestion =
"15 minutes a day can make you 3x more fluent in 3 month"; "15 minutes a day can make you 3x more fluent in 3 month";
const String ksCategorySubtitle =
'Watch expert-led videos and reinforce your knowledge through guided practice activities.';
const String ksHomeBottomSheetDescription = const String ksHomeBottomSheetDescription =
'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more'; 'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more';
const String ksPrivacyPolicy = 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.'; '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 ksCategorySubtitle =
'Watch expert-led videos and reinforce your knowledge through guided practice activities.';
const String ksTerms = """ const String ksTerms = """
<p style="color:#9C2C91;font-size:13px;"> <p style="color:#9C2C91;font-size:13px;">

View File

@ -1,9 +1,9 @@
// Login method
enum LoginMethod { phone, email, google }
// Response status // Response status
enum ResponseStatus { success, failure } enum ResponseStatus { success, failure }
// Login method
enum LoginMethod { phone, email, google }
// Sign-up method // Sign-up method
enum SignUpMethod { phone, email, google } enum SignUpMethod { phone, email, google }
@ -45,6 +45,7 @@ enum StateObjects {
learnPracticeAnswer, learnPracticeAnswer,
loginWithPhoneNumber, loginWithPhoneNumber,
learnPracticeQuestion, learnPracticeQuestion,
coursePracticeQuestion,
coursePracticeQuestions,
recordLearnPracticeAnswer, recordLearnPracticeAnswer,
} }

View File

@ -1,9 +1,8 @@
// Split full name
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'app_colors.dart'; import 'app_colors.dart';
// Split full name
Map<String, String> splitFullName(String fullName) { Map<String, String> splitFullName(String fullName) {
final parts = fullName.trim().split(RegExp(r'\s+')); final parts = fullName.trim().split(RegExp(r'\s+'));
@ -22,6 +21,7 @@ Map<String, String> splitFullName(String fullName) {
}; };
} }
// Get random color
Color getColor() { Color getColor() {
final generator = Random(); final generator = Random();
int random = generator.nextInt(8); int random = generator.nextInt(8);
@ -44,6 +44,7 @@ Color getColor() {
} }
} }
// Get playable url
String? getPlayableUrl(String url) { String? getPlayableUrl(String url) {
try { try {
// Case 1: /file/d/FILE_ID/view // Case 1: /file/d/FILE_ID/view

View File

@ -14,7 +14,7 @@ class FormValidator {
return null; return null;
} }
// Form validator // Full name validator
static String? validateFullNameForm(String? value) { static String? validateFullNameForm(String? value) {
if (value == null) { if (value == null) {
return null; return null;

View File

@ -45,12 +45,6 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
List<Widget> _buildScreens() => [ List<Widget> _buildScreens() => [
_buildAssessmentIntro(), _buildAssessmentIntro(),
_buildAssessment(), _buildAssessment(),
/*
_buildAssessmentFailure(),
_buildRetakeAssessment(),
_buildResultAnalysis(),
_buildAssessmentCompletion(),
*/
_buildAssessmentResult(), _buildAssessmentResult(),
_buildStartLesson(), _buildStartLesson(),
]; ];

View File

@ -7,7 +7,7 @@ import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../app/app.router.dart'; import '../../../app/app.router.dart';
import '../../../models/assessment.dart'; import '../../../models/question.dart';
import '../../../services/api_service.dart'; import '../../../services/api_service.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
@ -24,14 +24,14 @@ class AssessmentViewModel extends BaseViewModel {
int get currentPage => _currentPage; int get currentPage => _currentPage;
final PageController _pageController = PageController();
PageController get pageController => _pageController;
int _previousPage = 0; int _previousPage = 0;
int get previousPage => _previousPage; int get previousPage => _previousPage;
final PageController _pageController = PageController();
PageController get pageController => _pageController;
// Assessment // Assessment
int _currentQuestion = 0; int _currentQuestion = 0;
@ -41,9 +41,9 @@ class AssessmentViewModel extends BaseViewModel {
ProficiencyLevels get proficiencyLevel => _proficiencyLevel; ProficiencyLevels get proficiencyLevel => _proficiencyLevel;
List<Assessment> _assessments = []; List<Question> _assessments = [];
List<Assessment> get assessments => _assessments; List<Question> get assessments => _assessments;
final Map<String, dynamic> _selectedAnswers = {}; final Map<String, dynamic> _selectedAnswers = {};
@ -251,13 +251,6 @@ class AssessmentViewModel extends BaseViewModel {
Future<void> _getAssessments() async { Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
_assessments = await _apiService.getAssessments(); _assessments = await _apiService.getAssessments();
/*
for (int i = 0; i < 6; i++) {
final generator = Random();
int random = generator.nextInt(15);
response.add(response[random]);
}
*/
} }
} }

View File

@ -44,7 +44,6 @@ class CourseViewModel extends BaseViewModel {
_courseDetail = await _courseService.getCoursesDetail(id); _courseDetail = await _courseService.getCoursesDetail(id);
_courseDetail.sort((a, b) => _courseDetail.sort((a, b) =>
(a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0)); (a.course?.displayOrder ?? 0).compareTo(b.course?.displayOrder ?? 0));
rebuildUi();
} }
} }
} }

View File

@ -50,7 +50,6 @@ class CourseCategoryViewModel extends ReactiveViewModel {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
_categories = await _apiService.getCourseCategories(); _categories = await _apiService.getCourseCategories();
rebuildUi();
} }
} }
} }

View File

@ -40,10 +40,6 @@ class CourseLessonViewModel extends BaseViewModel {
Future<void> _getCourseLessons(int courseId) async { Future<void> _getCourseLessons(int courseId) async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
_courseLessons = await _apiService.getCourseLessons(1); _courseLessons = await _apiService.getCourseLessons(1);
if (_courseLessons.isNotEmpty) {
rebuildUi();
}
} }
} }
} }

View File

@ -92,19 +92,25 @@ class CoursePracticeView extends StackedView<CoursePracticeViewModel> {
); );
Widget _buildListView(CoursePracticeViewModel viewModel) => GridView.builder( Widget _buildListView(CoursePracticeViewModel viewModel) => GridView.builder(
itemCount: 6,
shrinkWrap: true, shrinkWrap: true,
itemCount: viewModel.coursePractices.length,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => itemBuilder: (context, index) => _buildCard(
_buildCard(title: viewModel.coursePractices[index].title ?? ''), title: viewModel.coursePractices[index].title ?? '',
onTap: () async =>
await viewModel.navigateToCoursePracticeQuestion(viewModel.coursePractices[index].id ?? 0),
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, crossAxisCount: 2,
mainAxisSpacing: 15, mainAxisSpacing: 15,
crossAxisSpacing: 15, crossAxisSpacing: 15,
childAspectRatio: 1.45, childAspectRatio: 1.2,
), ),
); );
Widget _buildCard({required String title}) => Widget _buildCard({
CoursePracticeCard(title: title); required String title,
GestureTapCallback? onTap,
}) =>
CoursePracticeCard(onTap: onTap, title: title);
} }

View File

@ -1,5 +1,6 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/practice.dart'; import 'package:yimaru_app/models/practice.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
@ -23,19 +24,19 @@ class CoursePracticeViewModel extends BaseViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToCoursePracticeQuestion(int id) async =>
await _navigationService.navigateToCoursePracticeQuestionView(id: id);
// Remote api call // Remote api call
// Courses // Course practices
Future<void> getCoursePractice(int id) async => Future<void> getCoursePractice(int id) async =>
await runBusyFuture(_getCoursePractice(id), await runBusyFuture(_getCoursePractice(id),
busyObject: StateObjects.coursePractice); busyObject: StateObjects.coursePractice);
Future<void> _getCoursePractice(int id) async { Future<void> _getCoursePractice(int id) async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
Map<String, dynamic> data = {'owner_id': id, 'owner_type': 'SUB_COURSE'}; _coursePractices = await _apiService.getCoursePractices(id);
_coursePractices = await _apiService.getCoursePractices(data);
rebuildUi();
} }
} }
} }

View File

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

View File

@ -0,0 +1,180 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// StackedFormGenerator
// **************************************************************************
// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String AnswerValueKey = 'answer';
final Map<String, TextEditingController>
_CoursePracticeQuestionViewTextEditingControllers = {};
final Map<String, FocusNode> _CoursePracticeQuestionViewFocusNodes = {};
final Map<String, String? Function(String?)?>
_CoursePracticeQuestionViewTextValidations = {
AnswerValueKey: FormValidator.validateForm,
};
mixin $CoursePracticeQuestionView {
TextEditingController get answerController =>
_getFormTextEditingController(AnswerValueKey);
FocusNode get answerFocusNode => _getFormFocusNode(AnswerValueKey);
TextEditingController _getFormTextEditingController(
String key, {
String? initialValue,
}) {
if (_CoursePracticeQuestionViewTextEditingControllers.containsKey(key)) {
return _CoursePracticeQuestionViewTextEditingControllers[key]!;
}
_CoursePracticeQuestionViewTextEditingControllers[key] =
TextEditingController(text: initialValue);
return _CoursePracticeQuestionViewTextEditingControllers[key]!;
}
FocusNode _getFormFocusNode(String key) {
if (_CoursePracticeQuestionViewFocusNodes.containsKey(key)) {
return _CoursePracticeQuestionViewFocusNodes[key]!;
}
_CoursePracticeQuestionViewFocusNodes[key] = FocusNode();
return _CoursePracticeQuestionViewFocusNodes[key]!;
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
answerController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
@Deprecated(
'Use syncFormWithViewModel instead.'
'This feature was deprecated after 3.1.0.',
)
void listenToFormUpdated(FormViewModel model) {
answerController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Updates the formData on the FormViewModel
void _updateFormData(FormStateHelper model, {bool forceValidate = false}) {
model.setData(
model.formValueMap
..addAll({
AnswerValueKey: answerController.text,
}),
);
if (_autoTextFieldValidation || forceValidate) {
updateValidationData(model);
}
}
bool validateFormFields(FormViewModel model) {
_updateFormData(model, forceValidate: true);
return model.isFormValid;
}
/// Calls dispose on all the generated controllers and focus nodes
void disposeForm() {
// The dispose function for a TextEditingController sets all listeners to null
for (var controller
in _CoursePracticeQuestionViewTextEditingControllers.values) {
controller.dispose();
}
for (var focusNode in _CoursePracticeQuestionViewFocusNodes.values) {
focusNode.dispose();
}
_CoursePracticeQuestionViewTextEditingControllers.clear();
_CoursePracticeQuestionViewFocusNodes.clear();
}
}
extension ValueProperties on FormStateHelper {
bool get hasAnyValidationMessage => this
.fieldsValidationMessages
.values
.any((validation) => validation != null);
bool get isFormValid {
if (!_autoTextFieldValidation) this.validateForm();
return !hasAnyValidationMessage;
}
String? get answerValue => this.formValueMap[AnswerValueKey] as String?;
set answerValue(String? value) {
this.setData(
this.formValueMap..addAll({AnswerValueKey: value}),
);
if (_CoursePracticeQuestionViewTextEditingControllers.containsKey(
AnswerValueKey)) {
_CoursePracticeQuestionViewTextEditingControllers[AnswerValueKey]?.text =
value ?? '';
}
}
bool get hasAnswer =>
this.formValueMap.containsKey(AnswerValueKey) &&
(answerValue?.isNotEmpty ?? false);
bool get hasAnswerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey]?.isNotEmpty ?? false;
String? get answerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey];
}
extension Methods on FormStateHelper {
setAnswerValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[AnswerValueKey] = validationMessage;
/// Clears text input fields on the Form
void clearForm() {
answerValue = '';
}
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
});
}
}
/// Returns the validation message for the given key
String? getValidationMessage(String key) {
final validatorForKey = _CoursePracticeQuestionViewTextValidations[key];
if (validatorForKey == null) return null;
String? validationMessageForKey = validatorForKey(
_CoursePracticeQuestionViewTextEditingControllers[key]!.text,
);
return validationMessageForKey;
}
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
});

View File

@ -0,0 +1,209 @@
import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/models/practice_question.dart';
import '../../../app/app.locator.dart';
import '../../../models/option.dart';
import '../../../models/question.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
class CoursePracticeQuestionViewModel extends FormViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// In-app navigation
int _currentPage = 0;
int get currentPage => _currentPage;
int _previousPage = 0;
int get previousPage => _previousPage;
final PageController _pageController = PageController();
PageController get pageController => _pageController;
// Course practice questions
bool _focusAnswer = false;
bool get focusAnswer => _focusAnswer;
Question? _currentQuestion;
Question? get currentQuestion => _currentQuestion;
List<PracticeQuestion> _coursePracticeQuestions = [];
List<PracticeQuestion> get coursePracticeQuestions =>
_coursePracticeQuestions;
int _currentQuestionIndex = 0;
int get currentQuestionIndex => _currentQuestionIndex;
final Map<String, dynamic> _selectedAnswers = {};
Map<String, dynamic> get selectedAnswers => _selectedAnswers;
// Question
void setAnswerFocus() {
_focusAnswer = true;
rebuildUi();
}
void setSelectedAnswer({required int question, required Option? option}) {
bool correct = false;
if (option?.isCorrect ?? false) {
correct = true;
}
final data = {
question.toString(): {
'correct': correct,
'option': option?.optionText,
'answer': _currentQuestion?.options
?.firstWhere((e) => e.isCorrect ?? false)
.optionText
}
};
_selectedAnswers.addAll(data);
rebuildUi();
}
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
// Course practice questions
Future<void> getCoursePracticeQuestions(int id) async =>
await runBusyFuture(_getCoursePracticeQuestions(id),
busyObject: StateObjects.coursePracticeQuestions);
Future<void> _getCoursePracticeQuestions(int id) async {
if (await _statusChecker.checkConnection()) {
_coursePracticeQuestions =
await _apiService.getCoursePracticeQuestions(id);
if (_coursePracticeQuestions.isNotEmpty) {
_currentQuestion = await _apiService.getCoursePracticeQuestion(
coursePracticeQuestions.first.questionId ?? 0);
}
}
}
// Course practice question
Future<void> getCoursePracticeQuestion(int id) async =>
await runBusyFuture(_getCoursePracticeQuestion(id),
busyObject: StateObjects.coursePractice);
Future<void> _getCoursePracticeQuestion(int id) async {
if (await _statusChecker.checkConnection()) {
_currentQuestion = await _apiService.getCoursePracticeQuestion(id);
}
}
// 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

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

View File

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

View File

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

View File

@ -41,7 +41,6 @@ class CourseSubcategoryViewModel extends BaseViewModel {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
_subcategories = await _apiService.getCourseSubcategories(id); _subcategories = await _apiService.getCourseSubcategories(id);
rebuildUi();
} }
} }
} }

View File

@ -21,7 +21,6 @@ class LearnPracticeViewModel extends ReactiveViewModel {
final _voiceRecorderService = locator<VoiceRecorderService>(); final _voiceRecorderService = locator<VoiceRecorderService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
LearnPracticeViewModel() { LearnPracticeViewModel() {
@ -30,7 +29,7 @@ class LearnPracticeViewModel extends ReactiveViewModel {
@override @override
List<ListenableServiceMixin> get listenableServices => List<ListenableServiceMixin> get listenableServices =>
[_audioPlayerService, _voiceRecorderService,_authenticationService]; [_audioPlayerService, _voiceRecorderService, _authenticationService];
// User // User
UserModel? get _user => _authenticationService.user; UserModel? get _user => _authenticationService.user;
@ -68,7 +67,6 @@ class LearnPracticeViewModel extends ReactiveViewModel {
VoiceRecordingState get recordingState => _recordingState; VoiceRecordingState get recordingState => _recordingState;
// Busy object // Busy object
StateObjects _busyObject = StateObjects.none; StateObjects _busyObject = StateObjects.none;
@ -84,18 +82,15 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Map<String, dynamic> get selectedPractice => _selectedPractice; Map<String, dynamic> get selectedPractice => _selectedPractice;
// Practice // Practice
void setPractice(Map<String, dynamic> practice) { void setPractice(Map<String, dynamic> practice) {
_selectedPractice = practice; _selectedPractice = practice;
goTo(1); goTo(1);
} }
// Play practice audio // Play practice audio
Future<void> playQuestionAudio() async => await runBusyFuture(_playQuestionAudio(), Future<void> playQuestionAudio() async =>
await runBusyFuture(_playQuestionAudio(),
busyObject: StateObjects.learnPracticeQuestion); busyObject: StateObjects.learnPracticeQuestion);
Future<void> _playQuestionAudio() async { Future<void> _playQuestionAudio() async {
@ -115,25 +110,26 @@ class LearnPracticeViewModel extends ReactiveViewModel {
print('POSITION: $_position'); print('POSITION: $_position');
rebuildUi(); rebuildUi();
}); });
} }
// Set busy object // Set busy object
void setBusyObject(StateObjects object){ void setBusyObject(StateObjects object) {
_busyObject = object; _busyObject = object;
notifyListeners(); notifyListeners();
} }
// Sample audio // Sample audio
Future<void> playSampleAudio() async => await runBusyFuture(_playSampleAudio(), Future<void> playSampleAudio() async =>
await runBusyFuture(_playSampleAudio(),
busyObject: StateObjects.learnPracticeSample); busyObject: StateObjects.learnPracticeSample);
Future<void> _playSampleAudio() async { Future<void> _playSampleAudio() async {
setBusyObject(StateObjects.learnPracticeSample); setBusyObject(StateObjects.learnPracticeSample);
await _audioPlayerService.playUrl(_selectedPractice['sample_answer']); await _audioPlayerService.playUrl(_selectedPractice['sample_answer']);
} }
Future<void> pauseSampleAudio()async=> await runBusyFuture(_pauseSampleAudio(),
Future<void> pauseSampleAudio() async =>
await runBusyFuture(_pauseSampleAudio(),
busyObject: StateObjects.learnPracticeSample); busyObject: StateObjects.learnPracticeSample);
Future<void> _pauseSampleAudio() async { Future<void> _pauseSampleAudio() async {
@ -141,31 +137,32 @@ class LearnPracticeViewModel extends ReactiveViewModel {
await _audioPlayerService.pause(); await _audioPlayerService.pause();
} }
// Recorded audio // Recorded audio
Future<void> playRecordedAudio() async => await runBusyFuture(_playRecordedAudio(), Future<void> playRecordedAudio() async =>
await runBusyFuture(_playRecordedAudio(),
busyObject: StateObjects.learnPracticeAnswer); busyObject: StateObjects.learnPracticeAnswer);
Future<void> _playRecordedAudio() async { Future<void> _playRecordedAudio() async {
setBusyObject(StateObjects.learnPracticeAnswer); setBusyObject(StateObjects.learnPracticeAnswer);
await _audioPlayerService.playLocal(await _voiceRecorderService.getRecordedAudio() ?? ''); await _audioPlayerService
.playLocal(await _voiceRecorderService.getRecordedAudio() ?? '');
} }
Future<void> pauseRecordedAudio()async=> await runBusyFuture(_pauseRecordedAudio(), Future<void> pauseRecordedAudio() async =>
await runBusyFuture(_pauseRecordedAudio(),
busyObject: StateObjects.learnPracticeAnswer); busyObject: StateObjects.learnPracticeAnswer);
Future<void> _pauseRecordedAudio() async { Future<void> _pauseRecordedAudio() async {
setBusyObject(StateObjects.learnPracticeAnswer); setBusyObject(StateObjects.learnPracticeAnswer);
await _audioPlayerService.pause(); await _audioPlayerService.pause();
} }
// Voice recorder // Voice recorder
Future<void> startRecording() async => await runBusyFuture(_startRecording(),busyObject: StateObjects.recordLearnPracticeAnswer ); Future<void> startRecording() async => await runBusyFuture(_startRecording(),
busyObject: StateObjects.recordLearnPracticeAnswer);
Future<void> _startRecording() async => await _voiceRecorderService.startRecording();
Future<void> _startRecording() async =>
await _voiceRecorderService.startRecording();
Future<void> stopRecording() async => Future<void> stopRecording() async =>
await _voiceRecorderService.stopRecording(); await _voiceRecorderService.stopRecording();

View File

@ -60,7 +60,6 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
child: _buildStartButtonContainer(viewModel), child: _buildStartButtonContainer(viewModel),
); );
Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) => Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) =>
GestureDetector( GestureDetector(
onTap: () => _start(viewModel), onTap: () => _start(viewModel),
@ -170,7 +169,6 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
child: _buildMicIcon(), child: _buildMicIcon(),
); );
Widget _buildMicIcon() => const Icon( Widget _buildMicIcon() => const Icon(
Icons.mic, Icons.mic,
size: 35, size: 35,

View File

@ -6,14 +6,15 @@ import 'custom_elevated_button.dart';
class CoursePracticeCard extends StatelessWidget { class CoursePracticeCard extends StatelessWidget {
final String title; final String title;
final GestureTapCallback? onTap;
const CoursePracticeCard({super.key, required this.title}); const CoursePracticeCard({super.key, this.onTap, required this.title});
@override @override
Widget build(BuildContext context) => _buildContainer(); Widget build(BuildContext context) => _buildContainer();
Widget _buildContainer() => Container( Widget _buildContainer() => Container(
height: 200, // height: 250,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
color: kcPrimaryColor.withValues(alpha: 0.25), color: kcPrimaryColor.withValues(alpha: 0.25),
@ -27,14 +28,19 @@ class CoursePracticeCard extends StatelessWidget {
List<Widget> _buildColumnChildren() => [ List<Widget> _buildColumnChildren() => [
verticalSpaceTiny, verticalSpaceTiny,
_buildTitle(), _buildTitleWrapper(),
verticalSpaceSmall,
_buildStartButtonWrapper(),
verticalSpaceSmall, verticalSpaceSmall,
_buildStartButtonWrapper()
]; ];
Widget _buildTitleWrapper() => Expanded(child: _buildTitle());
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
title, title,
maxLines: 2,
style: style18DG700, style: style18DG700,
overflow: TextOverflow.fade,
); );
Widget _buildStartButtonWrapper() => SizedBox( Widget _buildStartButtonWrapper() => SizedBox(
@ -42,11 +48,12 @@ class CoursePracticeCard extends StatelessWidget {
child: _buildStartButton(), child: _buildStartButton(),
); );
Widget _buildStartButton() => const CustomElevatedButton( Widget _buildStartButton() => CustomElevatedButton(
height: 50, height: 50,
width: 200, width: 200,
onTap: onTap,
borderRadius: 8, borderRadius: 8,
text: 'Start Test', text: 'Start Practice',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );

View File

@ -5,14 +5,13 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart'; import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/learn_practice_answer_card.dart'; import 'package:yimaru_app/ui/widgets/learn_practice_answer_card.dart';
class LearnPracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> { class LearnPracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> {
final Map<String, dynamic> data; final Map<String, dynamic> data;
const LearnPracticeResultCard( const LearnPracticeResultCard({super.key, required this.data});
{super.key, required this.data});
@override @override
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildColumnWrapper(); Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildColumnWrapper();
Widget _buildColumnWrapper() => SizedBox( Widget _buildColumnWrapper() => SizedBox(
height: 100, height: 100,
@ -47,15 +46,16 @@ class LearnPracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildSampleResponseWrapper() => Widget _buildSampleResponseWrapper() =>
Expanded(child: _buildSampleResponse()); Expanded(child: _buildSampleResponse());
Widget _buildSampleResponse() => Widget _buildSampleResponse() => const LearnPracticeAnswerCard(
const LearnPracticeAnswerCard(title: 'Sample Answer' ,object: StateObjects.learnPracticeSample,); title: 'Sample Answer',
object: StateObjects.learnPracticeSample,
);
Widget _buildActualResponseWrapper() => Widget _buildActualResponseWrapper() =>
Expanded(child: _buildActualResponse()); Expanded(child: _buildActualResponse());
Widget _buildActualResponse() => Widget _buildActualResponse() => const LearnPracticeAnswerCard(
const LearnPracticeAnswerCard(title: 'Your Answer',object: StateObjects.learnPracticeAnswer,); title: 'Your Answer',
object: StateObjects.learnPracticeAnswer,
);
} }

View File

@ -6,11 +6,11 @@ import 'package:yimaru_app/ui/widgets/custom_response_card.dart';
class PracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> { class PracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> {
final Map<String, dynamic> data; final Map<String, dynamic> data;
const PracticeResultCard( const PracticeResultCard({super.key, required this.data});
{super.key, required this.data});
@override @override
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildColumnWrapper(); Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildColumnWrapper();
Widget _buildColumnWrapper() => SizedBox( Widget _buildColumnWrapper() => SizedBox(
height: 100, height: 100,

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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