feat(course): Polish course ui

This commit is contained in:
BisratHailu 2026-02-20 15:15:23 +03:00
parent d75ed8c7c7
commit 4eb6e9d6c3
75 changed files with 2852 additions and 274 deletions

View File

@ -38,6 +38,10 @@ import 'package:yimaru_app/services/image_downloader_service.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'; import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart'; import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'; import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/course/course_view.dart';
import 'package:yimaru_app/ui/views/course_module/course_module_view.dart';
import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart';
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -69,6 +73,10 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
MaterialRoute(page: ForgetPasswordView), MaterialRoute(page: ForgetPasswordView),
MaterialRoute(page: LearnLessonDetailView), MaterialRoute(page: LearnLessonDetailView),
MaterialRoute(page: LearnPracticeView), MaterialRoute(page: LearnPracticeView),
MaterialRoute(page: CourseView),
MaterialRoute(page: CourseModuleView),
MaterialRoute(page: CoursePracticeView),
MaterialRoute(page: CoursePaymentView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [

View File

@ -5,15 +5,22 @@
// ************************************************************************** // **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i29;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/material.dart' as _i33;
import 'package:stacked/stacked.dart' as _i1; import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i30; import 'package:stacked_services/stacked_services.dart' as _i34;
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 _i10; as _i10;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart' import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'
as _i13; as _i13;
import 'package:yimaru_app/ui/views/course/course_view.dart' as _i29;
import 'package:yimaru_app/ui/views/course_module/course_module_view.dart'
as _i30;
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart'
as _i32;
import 'package:yimaru_app/ui/views/course_practice/course_practice_view.dart'
as _i31;
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7;
import 'package:yimaru_app/ui/views/failure/failure_view.dart' as _i25; import 'package:yimaru_app/ui/views/failure/failure_view.dart' as _i25;
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart' import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'
@ -104,6 +111,14 @@ class Routes {
static const learnPracticeView = '/learn-practice-view'; static const learnPracticeView = '/learn-practice-view';
static const courseView = '/course-view';
static const courseModuleView = '/course-module-view';
static const coursePracticeView = '/course-practice-view';
static const coursePaymentView = '/course-payment-view';
static const all = <String>{ static const all = <String>{
homeView, homeView,
onboardingView, onboardingView,
@ -132,6 +147,10 @@ class Routes {
forgetPasswordView, forgetPasswordView,
learnLessonDetailView, learnLessonDetailView,
learnPracticeView, learnPracticeView,
courseView,
courseModuleView,
coursePracticeView,
coursePaymentView,
}; };
} }
@ -245,17 +264,33 @@ class StackedRouter extends _i1.RouterBase {
Routes.learnPracticeView, Routes.learnPracticeView,
page: _i28.LearnPracticeView, page: _i28.LearnPracticeView,
), ),
_i1.RouteDef(
Routes.courseView,
page: _i29.CourseView,
),
_i1.RouteDef(
Routes.courseModuleView,
page: _i30.CourseModuleView,
),
_i1.RouteDef(
Routes.coursePracticeView,
page: _i31.CoursePracticeView,
),
_i1.RouteDef(
Routes.coursePaymentView,
page: _i32.CoursePaymentView,
),
]; ];
final _pagesMap = <Type, _i1.StackedRouteFactory>{ final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) { _i2.HomeView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(), builder: (context) => const _i2.HomeView(),
settings: data, settings: data,
); );
}, },
_i3.OnboardingView: (data) { _i3.OnboardingView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(), builder: (context) => const _i3.OnboardingView(),
settings: data, settings: data,
); );
@ -264,156 +299,156 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>( final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(), orElse: () => const StartupViewArguments(),
); );
return _i29.MaterialPageRoute<dynamic>( return _i33.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 _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(), builder: (context) => const _i5.ProfileView(),
settings: data, settings: data,
); );
}, },
_i6.ProfileDetailView: (data) { _i6.ProfileDetailView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(), builder: (context) => const _i6.ProfileDetailView(),
settings: data, settings: data,
); );
}, },
_i7.DownloadsView: (data) { _i7.DownloadsView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(), builder: (context) => const _i7.DownloadsView(),
settings: data, settings: data,
); );
}, },
_i8.ProgressView: (data) { _i8.ProgressView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(), builder: (context) => const _i8.ProgressView(),
settings: data, settings: data,
); );
}, },
_i9.OngoingProgressView: (data) { _i9.OngoingProgressView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(), builder: (context) => const _i9.OngoingProgressView(),
settings: data, settings: data,
); );
}, },
_i10.AccountPrivacyView: (data) { _i10.AccountPrivacyView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(), builder: (context) => const _i10.AccountPrivacyView(),
settings: data, settings: data,
); );
}, },
_i11.SupportView: (data) { _i11.SupportView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(), builder: (context) => const _i11.SupportView(),
settings: data, settings: data,
); );
}, },
_i12.TelegramSupportView: (data) { _i12.TelegramSupportView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(), builder: (context) => const _i12.TelegramSupportView(),
settings: data, settings: data,
); );
}, },
_i13.CallSupportView: (data) { _i13.CallSupportView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(), builder: (context) => const _i13.CallSupportView(),
settings: data, settings: data,
); );
}, },
_i14.LanguageView: (data) { _i14.LanguageView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(), builder: (context) => const _i14.LanguageView(),
settings: data, settings: data,
); );
}, },
_i15.PrivacyPolicyView: (data) { _i15.PrivacyPolicyView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(), builder: (context) => const _i15.PrivacyPolicyView(),
settings: data, settings: data,
); );
}, },
_i16.TermsAndConditionsView: (data) { _i16.TermsAndConditionsView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(), builder: (context) => const _i16.TermsAndConditionsView(),
settings: data, settings: data,
); );
}, },
_i17.RegisterView: (data) { _i17.RegisterView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(), builder: (context) => const _i17.RegisterView(),
settings: data, settings: data,
); );
}, },
_i18.LoginView: (data) { _i18.LoginView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(), builder: (context) => const _i18.LoginView(),
settings: data, settings: data,
); );
}, },
_i19.LearnView: (data) { _i19.LearnView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(), builder: (context) => const _i19.LearnView(),
settings: data, settings: data,
); );
}, },
_i20.LearnLevelView: (data) { _i20.LearnLevelView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(), builder: (context) => const _i20.LearnLevelView(),
settings: data, settings: data,
); );
}, },
_i21.LearnModuleView: (data) { _i21.LearnModuleView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(), builder: (context) => const _i21.LearnModuleView(),
settings: data, settings: data,
); );
}, },
_i22.WelcomeView: (data) { _i22.WelcomeView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i22.WelcomeView(), builder: (context) => const _i22.WelcomeView(),
settings: data, settings: data,
); );
}, },
_i23.AssessmentView: (data) { _i23.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false); final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i23.AssessmentView(key: args.key, data: args.data), _i23.AssessmentView(key: args.key, data: args.data),
settings: data, settings: data,
); );
}, },
_i24.LearnLessonView: (data) { _i24.LearnLessonView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.LearnLessonView(), builder: (context) => const _i24.LearnLessonView(),
settings: data, settings: data,
); );
}, },
_i25.FailureView: (data) { _i25.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false); final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i25.FailureView(key: args.key, label: args.label), _i25.FailureView(key: args.key, label: args.label),
settings: data, settings: data,
); );
}, },
_i26.ForgetPasswordView: (data) { _i26.ForgetPasswordView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i26.ForgetPasswordView(), builder: (context) => const _i26.ForgetPasswordView(),
settings: data, settings: data,
); );
}, },
_i27.LearnLessonDetailView: (data) { _i27.LearnLessonDetailView: (data) {
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i27.LearnLessonDetailView(), builder: (context) => const _i27.LearnLessonDetailView(),
settings: data, settings: data,
); );
}, },
_i28.LearnPracticeView: (data) { _i28.LearnPracticeView: (data) {
final args = data.getArgs<LearnPracticeViewArguments>(nullOk: false); final args = data.getArgs<LearnPracticeViewArguments>(nullOk: false);
return _i29.MaterialPageRoute<dynamic>( return _i33.MaterialPageRoute<dynamic>(
builder: (context) => _i28.LearnPracticeView( builder: (context) => _i28.LearnPracticeView(
key: args.key, key: args.key,
title: args.title, title: args.title,
@ -422,6 +457,30 @@ class StackedRouter extends _i1.RouterBase {
settings: data, settings: data,
); );
}, },
_i29.CourseView: (data) {
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i29.CourseView(),
settings: data,
);
},
_i30.CourseModuleView: (data) {
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i30.CourseModuleView(),
settings: data,
);
},
_i31.CoursePracticeView: (data) {
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i31.CoursePracticeView(),
settings: data,
);
},
_i32.CoursePaymentView: (data) {
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i32.CoursePaymentView(),
settings: data,
);
},
}; };
@override @override
@ -437,7 +496,7 @@ class StartupViewArguments {
this.label = 'Loading', this.label = 'Loading',
}); });
final _i29.Key? key; final _i33.Key? key;
final String label; final String label;
@ -464,7 +523,7 @@ class AssessmentViewArguments {
required this.data, required this.data,
}); });
final _i29.Key? key; final _i33.Key? key;
final Map<String, dynamic> data; final Map<String, dynamic> data;
@ -491,7 +550,7 @@ class FailureViewArguments {
required this.label, required this.label,
}); });
final _i29.Key? key; final _i33.Key? key;
final String label; final String label;
@ -520,7 +579,7 @@ class LearnPracticeViewArguments {
required this.buttonLabel, required this.buttonLabel,
}); });
final _i29.Key? key; final _i33.Key? key;
final String title; final String title;
@ -551,7 +610,7 @@ class LearnPracticeViewArguments {
} }
} }
extension NavigatorStateExtension on _i30.NavigationService { extension NavigatorStateExtension on _i34.NavigationService {
Future<dynamic> navigateToHomeView([ Future<dynamic> navigateToHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -581,7 +640,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
} }
Future<dynamic> navigateToStartupView({ Future<dynamic> navigateToStartupView({
_i29.Key? key, _i33.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -850,7 +909,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
} }
Future<dynamic> navigateToAssessmentView({ Future<dynamic> navigateToAssessmentView({
_i29.Key? key, _i33.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -881,7 +940,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
} }
Future<dynamic> navigateToFailureView({ Future<dynamic> navigateToFailureView({
_i29.Key? key, _i33.Key? key,
required String label, required String label,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -926,7 +985,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
} }
Future<dynamic> navigateToLearnPracticeView({ Future<dynamic> navigateToLearnPracticeView({
_i29.Key? key, _i33.Key? key,
required String title, required String title,
required String subtitle, required String subtitle,
required String buttonLabel, required String buttonLabel,
@ -948,6 +1007,62 @@ extension NavigatorStateExtension on _i30.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> navigateToCourseView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.courseView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToCourseModuleView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.courseModuleView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToCoursePracticeView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.coursePracticeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToCoursePaymentView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.coursePaymentView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([ Future<dynamic> replaceWithHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -977,7 +1092,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
} }
Future<dynamic> replaceWithStartupView({ Future<dynamic> replaceWithStartupView({
_i29.Key? key, _i33.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1246,7 +1361,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
} }
Future<dynamic> replaceWithAssessmentView({ Future<dynamic> replaceWithAssessmentView({
_i29.Key? key, _i33.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1277,7 +1392,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
} }
Future<dynamic> replaceWithFailureView({ Future<dynamic> replaceWithFailureView({
_i29.Key? key, _i33.Key? key,
required String label, required String label,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1322,7 +1437,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
} }
Future<dynamic> replaceWithLearnPracticeView({ Future<dynamic> replaceWithLearnPracticeView({
_i29.Key? key, _i33.Key? key,
required String title, required String title,
required String subtitle, required String subtitle,
required String buttonLabel, required String buttonLabel,
@ -1343,4 +1458,60 @@ extension NavigatorStateExtension on _i30.NavigationService {
parameters: parameters, parameters: parameters,
transition: transition); transition: transition);
} }
Future<dynamic> replaceWithCourseView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.courseView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithCourseModuleView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.courseModuleView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithCoursePracticeView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.coursePracticeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithCoursePaymentView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.coursePaymentView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
} }

View File

@ -10,6 +10,9 @@ class Assessment {
final String? status; final String? status;
final List<Option>? options;
@JsonKey(name: 'question_type') @JsonKey(name: 'question_type')
final String? questionType; final String? questionType;
@ -19,7 +22,6 @@ class Assessment {
@JsonKey(name: 'difficulty_level') @JsonKey(name: 'difficulty_level')
final String? difficultyLevel; final String? difficultyLevel;
final List<Option>? options;
const Assessment({ const Assessment({
this.id, this.id,

View File

@ -23,8 +23,8 @@ Map<String, dynamic> _$AssessmentToJson(Assessment instance) =>
'id': instance.id, 'id': instance.id,
'points': instance.points, 'points': instance.points,
'status': instance.status, 'status': instance.status,
'options': instance.options,
'question_type': instance.questionType, 'question_type': instance.questionType,
'question_text': instance.questionText, 'question_text': instance.questionText,
'difficulty_level': instance.difficultyLevel, 'difficulty_level': instance.difficultyLevel,
'options': instance.options,
}; };

View File

@ -57,6 +57,37 @@ class UserModel {
this.profileCompleted, this.profileCompleted,
}); });
UserModel copyWith(
{int? userId,
String? email,
String? gender,
String? region,
String? country,
String? lastName,
String? birthday,
String? firstName,
String? occupation,
String? accessToken,
String? refreshToken,
bool? userInfoLoaded,
bool? profileCompleted,
String? profilePicture}) =>
UserModel(
email: email ?? this.email,
userId: userId ?? this.userId,
gender: gender ?? this.gender,
region: region ?? this.region,
country: country ?? this.country,
lastName: lastName ?? this.lastName,
birthday: birthday ?? this.birthday,
firstName: firstName ?? this.firstName,
occupation: occupation ?? this.occupation,
accessToken: accessToken ?? this.accessToken,
refreshToken: refreshToken ?? this.refreshToken,
userInfoLoaded: userInfoLoaded ?? this.userInfoLoaded,
profilePicture: profilePicture ?? this.profilePicture,
profileCompleted: profileCompleted ?? this.profileCompleted);
factory UserModel.fromJson(Map<String, dynamic> json) => factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json); _$UserModelFromJson(json);

View File

@ -8,6 +8,7 @@ import '../app/app.locator.dart';
import '../ui/common/enmus.dart'; import '../ui/common/enmus.dart';
class ApiService { class ApiService {
// Dependency injection
final _service = locator<DioService>(); final _service = locator<DioService>();
// Register // Register
@ -38,7 +39,7 @@ class ApiService {
} }
} }
// Email Login // Email login
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async { Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(

View File

@ -4,16 +4,20 @@ import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/secure_storage_service.dart'; import 'package:yimaru_app/services/secure_storage_service.dart';
class AuthenticationService with ListenableServiceMixin { class AuthenticationService with ListenableServiceMixin {
// Dependency injection
final _secureService = locator<SecureStorageService>(); final _secureService = locator<SecureStorageService>();
AuthenticationService() { // User data
listenToReactiveValues([_user]);
}
UserModel? _user; UserModel? _user;
UserModel? get user => _user; UserModel? get user => _user;
// Initialization
AuthenticationService() {
listenToReactiveValues([_user]);
}
// Check user logged in
Future<bool> userLoggedIn() async { Future<bool> userLoggedIn() async {
if (await _secureService.getString('userId') != null) { if (await _secureService.getString('userId') != null) {
return true; return true;
@ -21,14 +25,18 @@ class AuthenticationService with ListenableServiceMixin {
return false; return false;
} }
// Get access token
Future<String?> getAccessToken() async => Future<String?> getAccessToken() async =>
await _secureService.getString('accessToken'); await _secureService.getString('accessToken');
// Get refresh token
Future<String?> getRefreshToken() async => Future<String?> getRefreshToken() async =>
await _secureService.getString('refreshToken'); await _secureService.getString('refreshToken');
// Get user id
Future<int?> getUserId() async => await _secureService.getInt('userId'); Future<int?> getUserId() async => await _secureService.getInt('userId');
// Save tokens
Future<void> saveTokens({ Future<void> saveTokens({
required String access, required String access,
required String refresh, required String refresh,
@ -37,6 +45,7 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('refreshToken', refresh); await _secureService.setString('refreshToken', refresh);
} }
// Save user credential
Future<void> saveUserCredential(Map<String, dynamic> data) async { Future<void> saveUserCredential(Map<String, dynamic> data) async {
await _secureService.setInt('userId', data['userId']); await _secureService.setInt('userId', data['userId']);
await _secureService.setString('accessToken', data['accessToken']); await _secureService.setString('accessToken', data['accessToken']);
@ -49,10 +58,16 @@ class AuthenticationService with ListenableServiceMixin {
); );
} }
// Save profile status
Future<void> saveProfileStatus(bool value) async { Future<void> saveProfileStatus(bool value) async {
await _secureService.setBool('profileCompleted', value); await _secureService.setBool('profileCompleted', value);
_user = UserModel( _user = _user?.copyWith(
userInfoLoaded: _user?.userInfoLoaded ?? false,
profileCompleted: await _secureService.getBool('profileCompleted'),
);
/* UserModel(
email: _user?.email, email: _user?.email,
gender: _user?.gender, gender: _user?.gender,
region: _user?.region, region: _user?.region,
@ -67,12 +82,18 @@ class AuthenticationService with ListenableServiceMixin {
profilePicture: _user?.profilePicture, profilePicture: _user?.profilePicture,
userInfoLoaded: _user?.userInfoLoaded ?? false, userInfoLoaded: _user?.userInfoLoaded ?? false,
profileCompleted: await _secureService.getBool('profileCompleted')); profileCompleted: await _secureService.getBool('profileCompleted'));
*/
notifyListeners(); notifyListeners();
} }
Future<void> saveProfilePicture(String image) async { Future<void> saveProfilePicture(String image) async {
await _secureService.setString('profilePicture', image); await _secureService.setString('profilePicture', image);
_user = UserModel( _user = _user?.copyWith(
userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profilePicture'),
);
/*UserModel(
email: _user?.email, email: _user?.email,
gender: _user?.gender, gender: _user?.gender,
region: _user?.region, region: _user?.region,
@ -88,7 +109,7 @@ class AuthenticationService with ListenableServiceMixin {
userInfoLoaded: _user?.userInfoLoaded ?? false, userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profilePicture'), profilePicture: await _secureService.getString('profilePicture'),
); );
*/
notifyListeners(); notifyListeners();
} }
@ -136,7 +157,17 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('firstName', data['first_name']); await _secureService.setString('firstName', data['first_name']);
await _secureService.setString('occupation', data['occupation']); await _secureService.setString('occupation', data['occupation']);
_user = UserModel( _user = _user?.copyWith(
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'),
);
/*UserModel(
email: _user?.email, email: _user?.email,
userId: _user?.userId, userId: _user?.userId,
accessToken: _user?.accessToken, accessToken: _user?.accessToken,
@ -150,7 +181,7 @@ class AuthenticationService with ListenableServiceMixin {
birthday: await _secureService.getString('birthday'), birthday: await _secureService.getString('birthday'),
firstName: await _secureService.getString('firstName'), firstName: await _secureService.getString('firstName'),
occupation: await _secureService.getString('occupation'), occupation: await _secureService.getString('occupation'),
); );*/
notifyListeners(); notifyListeners();
} }

View File

@ -9,15 +9,21 @@ import '../app/app.locator.dart';
import '../ui/common/app_constants.dart'; import '../ui/common/app_constants.dart';
class DioService { class DioService {
// Dependency injection
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
// Initialization
final Dio _dio = Dio(); final Dio _dio = Dio();
Dio get dio => _dio;
final Dio _refreshDio = Dio(); // separate instance final Dio _refreshDio = Dio(); // separate instance
bool _isRefreshing = false; bool _isRefreshing = false;
final List<void Function()> _retryQueue = []; final List<void Function()> _retryQueue = [];
// Initialization
DioService() { DioService() {
_dio.options _dio.options
..baseUrl = kBaseUrl ..baseUrl = kBaseUrl
@ -33,6 +39,7 @@ class DioService {
); );
} }
// Response logger
void _onResponse( void _onResponse(
Response response, Response response,
ResponseInterceptorHandler handler, ResponseInterceptorHandler handler,
@ -69,6 +76,7 @@ class DioService {
handler.next(options); handler.next(options);
} }
// Error logger
Future<void> _onError( Future<void> _onError(
DioException error, DioException error,
ErrorInterceptorHandler handler, ErrorInterceptorHandler handler,
@ -125,6 +133,7 @@ class DioService {
} }
} }
// Refresh token
Future<bool> _refreshToken() async { Future<bool> _refreshToken() async {
final UserModel? user = await _authenticationService.getUser(); final UserModel? user = await _authenticationService.getUser();
@ -155,9 +164,8 @@ class DioService {
} }
} }
// Check request if immediately after token refreshed
bool _isRefreshRequest(RequestOptions options) { bool _isRefreshRequest(RequestOptions options) {
return options.path.contains(kRefreshTokenUrl); return options.path.contains(kRefreshTokenUrl);
} }
Dio get dio => _dio;
} }

View File

@ -2,8 +2,10 @@ import 'package:google_sign_in/google_sign_in.dart';
import 'package:yimaru_app/ui/common/app_constants.dart'; import 'package:yimaru_app/ui/common/app_constants.dart';
class GoogleAuthService { class GoogleAuthService {
// Initialization
final GoogleSignIn signIn = GoogleSignIn.instance; final GoogleSignIn signIn = GoogleSignIn.instance;
// Google authentication
Future<GoogleSignInAccount?> googleAuth() async { Future<GoogleSignInAccount?> googleAuth() async {
try { try {
GoogleSignInAccount? googleUser; GoogleSignInAccount? googleUser;

View File

@ -9,8 +9,10 @@ import '../ui/common/app_constants.dart';
import 'dio_service.dart'; import 'dio_service.dart';
class ImageDownloaderService { class ImageDownloaderService {
// Dependency injection
final _service = locator<DioService>(); final _service = locator<DioService>();
// Image downloader
Future<String> downloader(String? networkImage) async { Future<String> downloader(String? networkImage) async {
late File image; late File image;

View File

@ -6,10 +6,13 @@ import '../app/app.locator.dart';
import '../ui/common/ui_helpers.dart'; import '../ui/common/ui_helpers.dart';
class ImagePickerService { class ImagePickerService {
// Dependency injection
final _permissionHandler = locator<PermissionHandlerService>(); final _permissionHandler = locator<PermissionHandlerService>();
// Initialization
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
// Pick image from gallery
Future<String?> gallery() async { Future<String?> gallery() async {
try { try {
PermissionStatus status = PermissionStatus status =
@ -32,6 +35,7 @@ class ImagePickerService {
} }
} }
// Pick image from camera
Future<String?> camera() async { Future<String?> camera() async {
try { try {
PermissionStatus status = PermissionStatus status =

View File

@ -3,6 +3,8 @@ import 'package:permission_handler/permission_handler.dart';
import '../ui/common/ui_helpers.dart'; import '../ui/common/ui_helpers.dart';
class PermissionHandlerService { class PermissionHandlerService {
// Check permission category
Future<PermissionStatus> requestPermission( Future<PermissionStatus> requestPermission(
Permission requestedPermission) async { Permission requestedPermission) async {
if (requestedPermission == Permission.camera) { if (requestedPermission == Permission.camera) {
@ -17,6 +19,7 @@ class PermissionHandlerService {
return PermissionStatus.denied; return PermissionStatus.denied;
} }
// Request permission
Future<PermissionStatus> request(Permission permission) async { Future<PermissionStatus> request(Permission permission) async {
if (await permission.isDenied) { if (await permission.isDenied) {
final PermissionStatus status = await permission.request(); final PermissionStatus status = await permission.request();

View File

@ -15,8 +15,7 @@ extension BoolParsing on String {
} }
class SecureStorageService { class SecureStorageService {
// Create storage // Initialization
late final FlutterSecureStorage _storage; late final FlutterSecureStorage _storage;
SecureStorageService() { SecureStorageService() {
@ -31,33 +30,40 @@ class SecureStorageService {
); );
} }
// Clear storage data
Future<void> clear() async { Future<void> clear() async {
_storage.deleteAll(); _storage.deleteAll();
} }
// Get boolean data from storage
Future<bool?> getBool(String key) async { Future<bool?> getBool(String key) async {
String? result = await _storage.read(key: key); String? result = await _storage.read(key: key);
return result?.parseBool(); return result?.parseBool();
} }
// Get string data from storage
Future<String?> getString(String key) async { Future<String?> getString(String key) async {
return await _storage.read(key: key); return await _storage.read(key: key);
} }
// Get integer data from storage
Future<int?> getInt(String key) async { Future<int?> getInt(String key) async {
return await _storage.read(key: key) == null return await _storage.read(key: key) == null
? null ? null
: int.parse(await _storage.read(key: key) ?? '0'); : int.parse(await _storage.read(key: key) ?? '0');
} }
// Save string data to storage
Future<void> setString(String key, String value) async { Future<void> setString(String key, String value) async {
await _storage.write(key: key, value: value); await _storage.write(key: key, value: value);
} }
// Save integer data to storage
Future<void> setInt(String key, int value) async { Future<void> setInt(String key, int value) async {
await _storage.write(key: key, value: value.toString()); await _storage.write(key: key, value: value.toString());
} }
// Save boolean data to storage
Future<void> setBool(String key, bool value) async { Future<void> setBool(String key, bool value) async {
await _storage.write(key: key, value: value.toString()); await _storage.write(key: key, value: value.toString());
} }

View File

@ -8,34 +8,28 @@ import 'package:yimaru_app/services/secure_storage_service.dart';
import '../app/app.locator.dart'; import '../app/app.locator.dart';
class StatusCheckerService { class StatusCheckerService {
// Dependency injection
final storage = locator<SecureStorageService>(); final storage = locator<SecureStorageService>();
// Initialization
bool _previousConnection = true; bool _previousConnection = true;
bool get previousConnection => _previousConnection; bool get previousConnection => _previousConnection;
// Get phone battery level
Future<int> getBatteryLevel() async { Future<int> getBatteryLevel() async {
final battery = Battery(); final battery = Battery();
final batteryLevel = await battery.batteryLevel; final batteryLevel = await battery.batteryLevel;
return batteryLevel; return batteryLevel;
} }
Future<bool> userAuthenticated() async { // Check internet connection
await checkAndUpdate();
if (await storage.getString('authenticated') != null) {
return true;
}
return false;
}
Future<bool> checkConnection() async { Future<bool> checkConnection() async {
if (await InternetConnection().hasInternetAccess) { if (await InternetConnection().hasInternetAccess) {
_previousConnection = true; _previousConnection = true;
return true; return true;
} else { } else {
if (_previousConnection) { if (_previousConnection) {
// showErrorToast('Check your internet connection');
_previousConnection = false; _previousConnection = false;
} }
@ -43,6 +37,7 @@ class StatusCheckerService {
} }
} }
// Check phone available storage
Future<int> getAvailableStorage() async { Future<int> getAvailableStorage() async {
try { try {
final availableStorage = final availableStorage =
@ -53,6 +48,7 @@ class StatusCheckerService {
} }
} }
// Check for latest update
Future<void> checkAndUpdate() async { Future<void> checkAndUpdate() async {
const requiredStorage = 500 * 1024 * 1024; const requiredStorage = 500 * 1024 * 1024;
@ -62,16 +58,12 @@ class StatusCheckerService {
await getAvailableStorage(); // Implement getAvailableStorage await getAvailableStorage(); // Implement getAvailableStorage
if (batteryLevel < 20 || storageAvailable < requiredStorage) { if (batteryLevel < 20 || storageAvailable < requiredStorage) {
if (batteryLevel < 20 || storageAvailable < requiredStorage) { if (batteryLevel < 20 || storageAvailable < requiredStorage) {
// KewedeConst().showErrorToast(
// 'Unable to update app, please charge your phone & free up space.'); // 'Unable to update app, please charge your phone & free up space.');
} else if (batteryLevel < 20) { } else if (batteryLevel < 20) {
// KewedeConst()
// .showErrorToast('Unable to update app, please charge your phone.'); // .showErrorToast('Unable to update app, please charge your phone.');
} else if (storageAvailable < requiredStorage) { } else if (storageAvailable < requiredStorage) {
// KewedeConst()
// .showErrorToast('Unable to update app, please free up space.'); // .showErrorToast('Unable to update app, please free up space.');
} }
// Show user-friendly message explaining why update failed and suggesting solutions (e.g., charge device, free up space)
return; // Prevent update from starting return; // Prevent update from starting
} }
try { try {

View File

@ -27,8 +27,9 @@ String kGoogleAuthUrl = 'api/v1/auth/google/android';
String kAssessmentsUrl = 'api/v1/assessment/questions'; String kAssessmentsUrl = 'api/v1/assessment/questions';
String kServerClientId =
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
String kSampleVideoUrl = String kSampleVideoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'; 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
String kServerClientId =
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';

View File

@ -1,11 +1,13 @@
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 ksHomeBottomSheetTitle = 'Build Great Apps!';
const String ksPrivacyPolicy =
'A brief, simple overview of Yimarus commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.';
const String 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 =
'A brief, simple overview of Yimarus commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.';
const String ksTerms = """ const String ksTerms = """
<p style="color:#9C2C91;font-size:13px;"> <p style="color:#9C2C91;font-size:13px;">
Last updated: October 26, 2025 Last updated: October 26, 2025

View File

@ -1,14 +1,15 @@
// Registration type // Registration type
enum RegistrationType { phone, email } enum RegistrationType { phone, email }
// Report status // Response status
enum ResponseStatus { success, failure } enum ResponseStatus { success, failure }
enum ProgressStatuses { pending, started, completed }
// Levels // Levels
enum ProficiencyLevels { a1, a2, b1, b2, none } enum ProficiencyLevels { a1, a2, b1, b2, none }
// Progress status
enum ProgressStatuses { pending, started, completed }
// State object // State object
enum StateObjects { enum StateObjects {
homeView, homeView,

View File

@ -1,3 +1,5 @@
// 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+'));

View File

@ -202,7 +202,6 @@ TextStyle style12RP600 = const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style12R700 = const TextStyle( TextStyle style12R700 = const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.red, color: Colors.red,
@ -239,6 +238,17 @@ TextStyle style25DG600 = const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style16P600 = const TextStyle(
fontSize: 16,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style16DG500 = const TextStyle(
fontSize: 16,
color: kcDarkGrey,
);
TextStyle style16DG600 = const TextStyle( TextStyle style16DG600 = const TextStyle(
fontSize: 16, fontSize: 16,
color: kcDarkGrey, color: kcDarkGrey,
@ -257,10 +267,16 @@ TextStyle style18DG500 = const TextStyle(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
); );
TextStyle style18DG600 = const TextStyle( TextStyle style18DG700 = const TextStyle(
fontSize: 18, fontSize: 18,
color: kcDarkGrey, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
);
TextStyle style20DG700 = const TextStyle(
fontSize: 20,
color: kcDarkGrey,
fontWeight: FontWeight.w700,
); );
TextStyle style16DG400 = const TextStyle( TextStyle style16DG400 = const TextStyle(

View File

@ -1,6 +1,7 @@
import 'package:email_validator/email_validator.dart'; import 'package:email_validator/email_validator.dart';
class FormValidator { class FormValidator {
// Form validator
static String? validateForm(String? value) { static String? validateForm(String? value) {
if (value == null) { if (value == null) {
return null; return null;
@ -12,6 +13,37 @@ class FormValidator {
return null; return null;
} }
// Email validator
static String? validateEmail(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
if (!EmailValidator.validate(value)) {
return 'Invalid email format';
}
return null;
}
// Password validator
static String? validatePassword(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
return null;
}
// Phone number validator
static String? validatePhoneNumber(String? value) { static String? validatePhoneNumber(String? value) {
if (value == null) { if (value == null) {
return null; return null;
@ -35,30 +67,4 @@ class FormValidator {
return null; return null;
} }
static String? validateEmail(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
if (!EmailValidator.validate(value)) {
return 'Invalid email format';
}
return null;
}
static String? validatePassword(String? value) {
if (value == null) {
return null;
}
if (value.isEmpty) {
return 'The field is required';
}
return null;
}
} }

View File

@ -12,9 +12,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
const AccountPrivacyView({Key? key}) : super(key: key); const AccountPrivacyView({Key? key}) : super(key: key);
@override @override
AccountPrivacyViewModel viewModelBuilder( AccountPrivacyViewModel viewModelBuilder(BuildContext context) =>
BuildContext context,
) =>
AccountPrivacyViewModel(); AccountPrivacyViewModel();
@override @override
@ -108,7 +106,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildHeader(String title) => Text( Widget _buildHeader(String title) => Text(
title, title,
style: style18DG600, style: style18DG700,
); );
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) => Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
@ -147,8 +145,8 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
); );
Widget _buildDeleteButton() => CustomElevatedButton( Widget _buildDeleteButton() => CustomElevatedButton(
height: 55, height: 55,
text: 'Delete Account',
borderRadius: 12, borderRadius: 12,
text: 'Delete Account',
foregroundColor: kcRed, foregroundColor: kcRed,
backgroundColor: kcRed.withOpacity(0.25), backgroundColor: kcRed.withOpacity(0.25),
); );

View File

@ -5,6 +5,7 @@ import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
class AccountPrivacyViewModel extends BaseViewModel { class AccountPrivacyViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Navigation // Navigation

View File

@ -23,6 +23,9 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@override
AssessmentViewModel viewModelBuilder(BuildContext context) => AssessmentViewModel();
@override @override
Widget builder( Widget builder(
BuildContext context, BuildContext context,
@ -65,9 +68,5 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
Widget _buildStartLesson() => const StartLessonScreen(); Widget _buildStartLesson() => const StartLessonScreen();
@override
AssessmentViewModel viewModelBuilder(
BuildContext context,
) =>
AssessmentViewModel();
} }

View File

@ -16,6 +16,7 @@ import '../../common/ui_helpers.dart';
import '../home/home_view.dart'; import '../home/home_view.dart';
class AssessmentViewModel extends BaseViewModel { class AssessmentViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>(); final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>(); final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>(); final _statusChecker = locator<StatusCheckerService>();
@ -207,6 +208,7 @@ class AssessmentViewModel extends BaseViewModel {
} }
} }
// In-app navigation
void next({int? page}) async { void next({int? page}) async {
if (page == null) { if (page == null) {
if (_previousPage != 0) { if (_previousPage != 0) {

View File

@ -76,8 +76,8 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'Were now analyzing your speaking skills', 'Were now analyzing your speaking skills',
textAlign: TextAlign.center,
style: style14MG400, style: style14MG400,
textAlign: TextAlign.center,
); );
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(

View File

@ -77,9 +77,10 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ', 'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
textAlign: TextAlign.center,
style: style14MG400, style: style14MG400,
); textAlign: TextAlign.center,
);
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -97,9 +98,9 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55, height: 55,
safe: false, safe: false,
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite,
text: 'Continue Assessment', text: 'Continue Assessment',
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );

View File

@ -155,9 +155,6 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite, foregroundColor: kcWhite,
text: viewModel.currentQuestion == viewModel.assessments.length - 1
? 'Finish'
: 'Continue',
backgroundColor: backgroundColor:
viewModel.selectedAnswers.containsKey(question.toString()) viewModel.selectedAnswers.containsKey(question.toString())
? kcPrimaryColor ? kcPrimaryColor
@ -165,5 +162,8 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
onTap: viewModel.selectedAnswers.containsKey(question.toString()) onTap: viewModel.selectedAnswers.containsKey(question.toString())
? () => viewModel.nextQuestion() ? () => viewModel.nextQuestion()
: null, : null,
text: viewModel.currentQuestion == viewModel.assessments.length - 1
? 'Finish'
: 'Continue',
); );
} }

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart'; import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';

View File

@ -78,8 +78,8 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildPrimarySubtitle() => Text( Widget _buildPrimarySubtitle() => Text(
'Great Job! Heres your next step to keep improving.', 'Great Job! Heres your next step to keep improving.',
textAlign: TextAlign.center,
style: style14MG400, style: style14MG400,
textAlign: TextAlign.center,
); );
Widget _buildIconWrapper(AssessmentViewModel viewModel) => Widget _buildIconWrapper(AssessmentViewModel viewModel) =>

View File

@ -100,9 +100,9 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55, height: 55,
safe: false, safe: false,
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite,
text: 'Retake Assessment', text: 'Retake Assessment',
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
@ -116,9 +116,9 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55, height: 55,
text: 'Skip', text: 'Skip',
borderRadius: 12, borderRadius: 12,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -12,9 +12,7 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
const CallSupportView({Key? key}) : super(key: key); const CallSupportView({Key? key}) : super(key: key);
@override @override
CallSupportViewModel viewModelBuilder( CallSupportViewModel viewModelBuilder(BuildContext context) =>
BuildContext context,
) =>
CallSupportViewModel(); CallSupportViewModel();
@override @override
@ -85,9 +83,9 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildSubTitle('+2519012345678'), _buildSubtitle('+2519012345678'),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle('+2519012345678'), _buildSubtitle('+2519012345678'),
]; ];
Widget _buildIcon() => Widget _buildIcon() =>
@ -99,10 +97,10 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildSubTitle(String title) => Text( Widget _buildSubtitle(String title) => Text(
title, title,
style: style14P400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(color: kcPrimaryColor),
); );
Widget _buildContinueButtonWrapper(CallSupportViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(CallSupportViewModel viewModel) => Padding(

View File

@ -4,6 +4,9 @@ import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
class CallSupportViewModel extends BaseViewModel { class CallSupportViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
} }

View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_card.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/learn_app_bar.dart';
import 'course_viewmodel.dart';
class CourseView extends StackedView<CourseViewModel> {
const CourseView({Key? key}) : super(key: key);
@override
CourseViewModel viewModelBuilder(BuildContext context) => CourseViewModel();
@override
Widget builder(
BuildContext context,
CourseViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CourseViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CourseViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CourseViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CourseViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildCourseColumnWrapper(viewModel)
],
);
Widget _buildAppBar(CourseViewModel viewModel) => LearnAppBar(
name: viewModel.user?.firstName,
profileImage: viewModel.user?.profilePicture,
);
Widget _buildCourseColumnWrapper(CourseViewModel viewModel) =>
Expanded(child: _buildCourseColumnScrollView(viewModel));
Widget _buildCourseColumnScrollView(CourseViewModel viewModel) =>
SingleChildScrollView(
child: _buildCourseColumn(viewModel),
);
Widget _buildCourseColumn(CourseViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(CourseViewModel viewModel) => [
verticalSpaceLarge,
_buildTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildListView(viewModel)
];
Widget _buildTitle() => Text(
'Courses',
style: style20DG700,
);
Widget _buildSubtitle() => Text(
'Choose a course to improve your professional or exam skills.',
style: style14DG400,
);
Widget _buildListView(CourseViewModel viewModel) => ListView.separated(
shrinkWrap: true,
itemCount: viewModel.courses.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
title: viewModel.courses[index]['title'],
subtitle: viewModel.courses[index]['subtitle'],
onTap: () async => await viewModel.navigateToCourseModule(),
),
separatorBuilder: (context, index) => verticalSpaceSmall,
);
//
Widget _buildTile({
required String title,
required String subtitle,
required GestureTapCallback onTap,
}) =>
CourseCard(
title: title,
onTap: onTap,
subtitle: subtitle,
);
}

View File

@ -0,0 +1,42 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
import '../../../models/user_model.dart';
import '../../../services/authentication_service.dart';
class CourseViewModel extends ReactiveViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
// Courses
final List<Map<String, dynamic>> _courses = [
{
'title': 'English Proficiency Exams',
'subtitle':
'Prepare for IELTS, TOEFL, or Duolingo with structured practice.',
},
{
'title': 'Skill-Based Courses',
'subtitle':
'Learn English for the workplace, travel, and real-life communication.',
},
];
List<Map<String, dynamic>> get courses => _courses;
// Navigation
Future<void> navigateToCourseModule() async =>
_navigationService.navigateToCourseModuleView();
}

View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_module_tile.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'course_module_viewmodel.dart';
class CourseModuleView extends StackedView<CourseModuleViewModel> {
const CourseModuleView({Key? key}) : super(key: key);
@override
CourseModuleViewModel viewModelBuilder(BuildContext context) =>
CourseModuleViewModel();
@override
Widget builder(
BuildContext context,
CourseModuleViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CourseModuleViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CourseModuleViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CourseModuleViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CourseModuleViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildLevelsColumnWrapper(viewModel),
],
);
Widget _buildAppBar(CourseModuleViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildLevelsColumnWrapper(CourseModuleViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(CourseModuleViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(CourseModuleViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(CourseModuleViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildListView(viewModel)
];
Widget _buildTitle() => Text(
'English Proficiency Exams',
style: style20DG700,
);
Widget _buildSubtitle() => Text(
'Select your target exam and start preparing',
style: style14DG400,
);
Widget _buildListView(CourseModuleViewModel viewModel) => ListView.separated(
shrinkWrap: true,
itemCount: viewModel.modules.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
title: viewModel.modules[index]['title'],
onPracticeTap: () async =>
await viewModel.navigateToCoursePractice(),
onCourseTap: () async => await viewModel.navigateToCoursePayment()),
separatorBuilder: (context, index) => verticalSpaceMedium,
);
Widget _buildTile({
required String title,
GestureTapCallback? onCourseTap,
GestureTapCallback? onPracticeTap,
}) =>
CourseModuleTile(
title: title,
onCourseTap: onCourseTap,
onPracticeTap: onPracticeTap,
);
}

View File

@ -0,0 +1,31 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
class CourseModuleViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>();
// Course modules
final List<Map<String, dynamic>> _modules = [
{
'title': 'Duolingo English Test',
},
{
'title': 'IELTS',
},
];
List<Map<String, dynamic>> get modules => _modules;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToCoursePractice() async =>
_navigationService.navigateToCoursePracticeView();
Future<void> navigateToCoursePayment() async =>
_navigationService.navigateToCoursePaymentView();
}

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_payment_card.dart';
import 'package:yimaru_app/ui/widgets/custom_list_tile.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'course_payment_viewmodel.dart';
class CoursePaymentView extends StackedView<CoursePaymentViewModel> {
const CoursePaymentView({Key? key}) : super(key: key);
@override
CoursePaymentViewModel viewModelBuilder(BuildContext context) =>
CoursePaymentViewModel();
@override
Widget builder(
BuildContext context,
CoursePaymentViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CoursePaymentViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CoursePaymentViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CoursePaymentViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CoursePaymentViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildPracticeColumnWrapper(viewModel),
],
);
Widget _buildAppBar(CoursePaymentViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildPracticeColumnWrapper(CoursePaymentViewModel viewModel) =>
Expanded(child: _buildPracticeColumnScrollView(viewModel));
Widget _buildPracticeColumnScrollView(CoursePaymentViewModel viewModel) =>
SingleChildScrollView(
child: _buildPracticeColumn(viewModel),
);
Widget _buildPracticeColumn(CoursePaymentViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildPracticeColumnChildren(viewModel),
);
List<Widget> _buildPracticeColumnChildren(CoursePaymentViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildFirstCard(),
verticalSpaceMedium,
_buildSecondCard()
];
Widget _buildTitle() => Text(
'Unlock All Lessons & Practices',
style: style20DG700,
);
Widget _buildFirstCard() => const CoursePaymentCard(
icon: Icons.school,
title: '50+ New Lessons',
subtitle: 'Access fresh, advanced content',
);
Widget _buildSecondCard() => const CoursePaymentCard(
icon: Icons.developer_board,
title: 'Mastery Through Practice',
subtitle: 'Practice All Question Types & Take Mock Exams',
);
}

View File

@ -0,0 +1,12 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class CoursePaymentViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>();
// Navigation
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_practice_card.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/small_app_bar.dart';
import 'course_practice_viewmodel.dart';
class CoursePracticeView extends StackedView<CoursePracticeViewModel> {
const CoursePracticeView({Key? key}) : super(key: key);
@override
CoursePracticeViewModel viewModelBuilder(BuildContext context) =>
CoursePracticeViewModel();
@override
Widget builder(
BuildContext context,
CoursePracticeViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(CoursePracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(CoursePracticeViewModel viewModel) =>
SafeArea(child: _buildBody(viewModel));
Widget _buildBody(CoursePracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(CoursePracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildPracticeColumnWrapper(viewModel),
],
);
Widget _buildAppBar(CoursePracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildPracticeColumnWrapper(CoursePracticeViewModel viewModel) =>
Expanded(child: _buildPracticeColumnScrollView(viewModel));
Widget _buildPracticeColumnScrollView(CoursePracticeViewModel viewModel) =>
SingleChildScrollView(
child: _buildPracticeColumn(viewModel),
);
Widget _buildPracticeColumn(CoursePracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildPracticeColumnChildren(viewModel),
);
List<Widget> _buildPracticeColumnChildren(
CoursePracticeViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildListView(viewModel)
];
Widget _buildTitle() => Text(
'Duolingo Mock Tests',
style: style20DG700,
);
Widget _buildSubtitle() => Text(
'Select your target exam and start preparing',
style: style14DG400,
);
Widget _buildListView(CoursePracticeViewModel viewModel) => GridView.builder(
itemCount: 6,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) =>
_buildCard(title: viewModel.practices[index]['title']),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 15,
crossAxisSpacing: 15,
childAspectRatio: 1.45,
),
);
Widget _buildCard({
required String title,
}) =>
CoursePracticeCard(title: title);
}

View File

@ -0,0 +1,36 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class CoursePracticeViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>();
// Practices
final List<Map<String, dynamic>> _practices = [
{
'title': 'Test 01',
},
{
'title': 'Test 02',
},
{
'title': 'Test 03',
},
{
'title': 'Test 04',
},
{
'title': 'Test 05',
},
{
'title': 'Test 06',
},
];
List<Map<String, dynamic>> get practices => _practices;
// Navigation
void pop() => _navigationService.back();
}

View File

@ -13,9 +13,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
const DownloadsView({Key? key}) : super(key: key); const DownloadsView({Key? key}) : super(key: key);
@override @override
DownloadsViewModel viewModelBuilder( DownloadsViewModel viewModelBuilder(BuildContext context) =>
BuildContext context,
) =>
DownloadsViewModel(); DownloadsViewModel();
@override @override
@ -95,33 +93,21 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
List<Widget> _buildStorageInfoChildren(DownloadsViewModel viewModel) => List<Widget> _buildStorageInfoChildren(DownloadsViewModel viewModel) =>
[_buildStorageInfo(), _buildManageButton(viewModel)]; [_buildStorageInfo(), _buildManageButton(viewModel)];
Widget _buildStorageInfo() => const Text.rich( Widget _buildStorageInfo() => Text.rich(
TextSpan( TextSpan(text: '1.2GB', style: style14P600, children: [
text: '1.2GB', TextSpan(
style: TextStyle( text: ' used of 2GB',
color: kcPrimaryColor, style: style14DG600,
fontWeight: FontWeight.w600, )
), ]),
children: [
TextSpan(
text: ' used of 2GB',
style: TextStyle(
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
)
]),
); );
Widget _buildManageButton(DownloadsViewModel viewModel) => TextButton( Widget _buildManageButton(DownloadsViewModel viewModel) => TextButton(
onPressed: viewModel.setShowDownload, child: _buildManageText()); onPressed: viewModel.setShowDownload, child: _buildManageText());
Widget _buildManageText() => const Text( Widget _buildManageText() => Text(
'Manage Storage', 'Manage Storage',
style: TextStyle( style: style14P600,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
); );
Widget _buildStorageIndicator() => const CustomLinearProgressIndicator( Widget _buildStorageIndicator() => const CustomLinearProgressIndicator(
@ -188,20 +174,16 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
color: kcPrimaryColor, color: kcPrimaryColor,
); );
Widget _buildEmptyTitle() => const Text( Widget _buildEmptyTitle() => Text(
'Looking for something to download?', 'Looking for something to download?',
style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildEmptySubtitle() => const Text( Widget _buildEmptySubtitle() => Text(
'Start by exploring your learning materials and save them for offline access.', 'Start by exploring your learning materials and save them for offline access.',
style: style14MG400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
); );
Widget _buildGoButtonWrapper(DownloadsViewModel viewModel) => Padding( Widget _buildGoButtonWrapper(DownloadsViewModel viewModel) => Padding(
@ -211,9 +193,9 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
Widget _buildGoButton(DownloadsViewModel viewModel) => CustomElevatedButton( Widget _buildGoButton(DownloadsViewModel viewModel) => CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'Go to Learn Section',
onTap: viewModel.setShowDownload,
foregroundColor: kcWhite, foregroundColor: kcWhite,
text: 'Go to Learn Section',
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: viewModel.setShowDownload,
); );
} }

View File

@ -4,11 +4,14 @@ import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
class DownloadsViewModel extends BaseViewModel { class DownloadsViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Show download
bool _showDownload = false; bool _showDownload = false;
bool get showDownload => _showDownload; bool get showDownload => _showDownload;
// Downloads // Downloads
final List<Map<String, dynamic>> _downloads = [ final List<Map<String, dynamic>> _downloads = [
{ {
@ -33,10 +36,12 @@ class DownloadsViewModel extends BaseViewModel {
List<Map<String, dynamic>> get downloads => _downloads; List<Map<String, dynamic>> get downloads => _downloads;
// Show download
void setShowDownload() { void setShowDownload() {
_showDownload = !_showDownload; _showDownload = !_showDownload;
rebuildUi(); rebuildUi();
} }
// Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter/scheduler.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/course/course_view.dart';
import 'package:yimaru_app/ui/views/learn/learn_view.dart'; import 'package:yimaru_app/ui/views/learn/learn_view.dart';
import 'package:yimaru_app/ui/views/profile/profile_view.dart'; import 'package:yimaru_app/ui/views/profile/profile_view.dart';
import 'package:yimaru_app/ui/views/startup/startup_view.dart'; import 'package:yimaru_app/ui/views/startup/startup_view.dart';
@ -76,7 +77,7 @@ Widget getViewForIndex(int index) {
case 0: case 0:
return const LearnView(); return const LearnView();
case 1: case 1:
return const ComingSoon(); return const CourseView();
default: default:
return const ProfileView(); return const ProfileView();

View File

@ -16,7 +16,9 @@ class LearnViewModel extends ReactiveViewModel {
[_authenticationService]; [_authenticationService];
// Current user // Current user
UserModel? get user => _authenticationService.user; UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
final List<Map<String, dynamic>> _learnLevels = [ final List<Map<String, dynamic>> _learnLevels = [
{ {

View File

@ -110,7 +110,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
Widget _buildHeader() => Text( Widget _buildHeader() => Text(
'Module 1: Greetings & Introductions', 'Module 1: Greetings & Introductions',
style: style18DG600, style: style18DG700,
); );
Widget _buildListView(LearnLessonViewModel viewModel) => ListView.builder( Widget _buildListView(LearnLessonViewModel viewModel) => ListView.builder(
@ -124,7 +124,6 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
onLessonTap: () async => onLessonTap: () async =>
await viewModel.navigateToLearnLessonDetail(), await viewModel.navigateToLearnLessonDetail(),
onPracticeTap: () async => await viewModel.navigateToLearnPractice(), onPracticeTap: () async => await viewModel.navigateToLearnPractice(),
), ),
); );

View File

@ -40,6 +40,6 @@ class LearnLessonViewModel extends BaseViewModel {
buttonLabel: 'Start Practice', buttonLabel: 'Start Practice',
title: 'Let \'s practice what you just learnt!', title: 'Let \'s practice what you just learnt!',
subtitle: subtitle:
'Ill ask you a few questions, and you can respond naturally.', 'Ill ask you a few questions, and you can respond naturally.',
); );
} }

View File

@ -32,7 +32,6 @@ class LearnLevelViewModel extends BaseViewModel {
await _navigationService.navigateToLearnPracticeView( await _navigationService.navigateToLearnPracticeView(
title: 'Lets Practice Level 1', title: 'Lets Practice Level 1',
buttonLabel: 'Begin Level Practice', buttonLabel: 'Begin Level Practice',
subtitle: subtitle: 'Lets quickly review what youve learned in this level! ',
'Lets quickly review what youve learned in this level! ',
); );
} }

View File

@ -67,7 +67,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
List<Widget> _buildLevelsColumnChildren(LearnModuleViewModel viewModel) => [ List<Widget> _buildLevelsColumnChildren(LearnModuleViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildOverallProgress(), _buildOverallProgress(),
verticalSpaceMedium, verticalSpaceMedium,
@ -79,7 +79,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
style: style18P600, style: style18P600,
); );
Widget _buildSubTitle() => Text( Widget _buildSubtitle() => Text(
'Your Current Level', 'Your Current Level',
style: style14DG400, style: style14DG400,
); );

View File

@ -8,6 +8,7 @@ import '../../common/enmus.dart';
class LearnModuleViewModel extends BaseViewModel { class LearnModuleViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Modules
final List<Map<String, dynamic>> _modules = [ final List<Map<String, dynamic>> _modules = [
{ {
'status': ProgressStatuses.completed, 'status': ProgressStatuses.completed,
@ -35,6 +36,7 @@ class LearnModuleViewModel extends BaseViewModel {
List<Map<String, dynamic>> get modules => _modules; List<Map<String, dynamic>> get modules => _modules;
// Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnLesson() async => Future<void> navigateToLearnLesson() async =>
@ -44,7 +46,6 @@ class LearnModuleViewModel extends BaseViewModel {
await _navigationService.navigateToLearnPracticeView( await _navigationService.navigateToLearnPracticeView(
title: 'Lets Practice Module 1', title: 'Lets Practice Module 1',
buttonLabel: 'Begin Module Practice', buttonLabel: 'Begin Module Practice',
subtitle: subtitle: 'Lets quickly review what youve learned in this module! ',
'Lets quickly review what youve learned in this module! ',
); );
} }

View File

@ -72,7 +72,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
_buildSpeakToLearnPracticeListenerScreen(), _buildSpeakToLearnPracticeListenerScreen(),
_buildFinishLearnPracticeScreen(), _buildFinishLearnPracticeScreen(),
_buildLearnPracticeResultScreen(), _buildLearnPracticeResultScreen(),
_buildLearnPracticeCompletionScreen() _buildLearnPracticeCompletionScreen()
]; ];
Widget _buildLearnPracticeIntroScreen() => LearnPracticeIntroScreen( Widget _buildLearnPracticeIntroScreen() => LearnPracticeIntroScreen(
@ -93,6 +93,6 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
Widget _buildLearnPracticeResultScreen() => const LearnPracticeResultScreen(); Widget _buildLearnPracticeResultScreen() => const LearnPracticeResultScreen();
Widget _buildLearnPracticeCompletionScreen() => const LearnPracticeCompletionScreen(); Widget _buildLearnPracticeCompletionScreen() =>
const LearnPracticeCompletionScreen();
} }

View File

@ -8,7 +8,8 @@ import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
class LearnPracticeCompletionScreen extends ViewModelWidget<LearnPracticeViewModel> { class LearnPracticeCompletionScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeCompletionScreen({super.key}); const LearnPracticeCompletionScreen({super.key});
@override @override
@ -16,61 +17,60 @@ class LearnPracticeCompletionScreen extends ViewModelWidget<LearnPracticeViewMod
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildBodyWrapper(viewModel), body: _buildBodyWrapper(viewModel),
); );
Widget _buildBodyWrapper(LearnPracticeViewModel viewModel) => Padding( Widget _buildBodyWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel), child: _buildBody(viewModel),
); );
Widget _buildBody(LearnPracticeViewModel viewModel) => Column( Widget _buildBody(LearnPracticeViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel), children: _buildBodyChildren(viewModel),
); );
List<Widget> _buildBodyChildren(LearnPracticeViewModel viewModel) => List<Widget> _buildBodyChildren(LearnPracticeViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(LearnPracticeViewModel viewModel) => Column( Widget _buildUpperColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(viewModel), children: _buildUpperColumnChildren(viewModel),
); );
List<Widget> _buildUpperColumnChildren(LearnPracticeViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LearnPracticeViewModel viewModel) => [
verticalSpaceMassive, verticalSpaceMassive,
_buildIcon(), _buildIcon(),
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubtitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/complete.svg', 'assets/icons/complete.svg',
); );
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'Yay, youve completed A1 ', 'Yay, youve completed A1 ',
style: style25DG600, style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'Were now analyzing your speaking skills', 'Were now analyzing your speaking skills',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: style14MG400, style: style14MG400,
); );
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
padding: const EdgeInsets.only(bottom: 50), Padding(
child: _buildContinueButton(viewModel), padding: const EdgeInsets.only(bottom: 50),
); child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LearnPracticeViewModel viewModel) => Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(

View File

@ -105,13 +105,13 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
); );
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
title, title,
style: style25DG600, style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
subtitle, subtitle,
style: style14DG400, style: style14DG400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );

View File

@ -95,7 +95,8 @@ class LearnPracticeResultScreen
verticalSpaceMedium, verticalSpaceMedium,
]; ];
Widget _buildContinueButton(LearnPracticeViewModel viewModel) => CustomElevatedButton( Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55, height: 55,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,

View File

@ -74,7 +74,7 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildAgeGroups(viewModel) _buildAgeGroups(viewModel)
]; ];
@ -91,7 +91,7 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
style: style25DG600, style: style25DG600,
); );
Widget _buildSubTitle() => Text( Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your age.', 'Well personalize your learning experience based on your age.',
style: style14DG400, style: style14DG400,
); );

View File

@ -76,7 +76,7 @@ class EducationalBackgroundFormScreen
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildEducationalLevels(viewModel) _buildEducationalLevels(viewModel)
]; ];
@ -97,7 +97,7 @@ class EducationalBackgroundFormScreen
), ),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => const Text(
'This helps us tailor your lessons to your experience.', 'This helps us tailor your lessons to your experience.',
style: TextStyle(color: kcMediumGrey), style: TextStyle(color: kcMediumGrey),
); );

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/course_level_card.dart'; import 'package:yimaru_app/ui/widgets/course_progress_card.dart';
import 'package:yimaru_app/ui/widgets/skill_progress.dart'; import 'package:yimaru_app/ui/widgets/skill_progress.dart';
import 'package:yimaru_app/ui/widgets/suggestion_card.dart'; import 'package:yimaru_app/ui/widgets/suggestion_card.dart';
@ -115,7 +115,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
required String subtitle, required String subtitle,
required bool isCompleted, required bool isCompleted,
required ProgressViewModel viewModel}) => required ProgressViewModel viewModel}) =>
CourseLevelCard( CourseProgressCard(
icon: icon, icon: icon,
title: title, title: title,
color: color, color: color,

View File

@ -85,7 +85,7 @@ class TelegramSupportView extends StackedView<TelegramSupportViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => Widget _buildIcon() =>
@ -101,7 +101,7 @@ class TelegramSupportView extends StackedView<TelegramSupportViewModel> {
), ),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => const Text(
'Connect with our support team instantly on Telegram for quick assistance and community updates', 'Connect with our support team instantly on Telegram for quick assistance and community updates',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), style: TextStyle(color: kcMediumGrey),

View File

@ -25,6 +25,7 @@ class BirthdaySelector extends StatelessWidget {
context: context, context: context,
is24HourMode: false, is24HourMode: false,
isShowSeconds: false, isShowSeconds: false,
title: _buildTitle(),
lastDate: DateTime.now(), lastDate: DateTime.now(),
firstDate: DateTime(1900), firstDate: DateTime(1900),
barrierDismissible: true, barrierDismissible: true,
@ -34,7 +35,6 @@ class BirthdaySelector extends StatelessWidget {
type: OmniDateTimePickerType.date, type: OmniDateTimePickerType.date,
borderRadius: const BorderRadius.all(Radius.circular(15)), borderRadius: const BorderRadius.all(Radius.circular(15)),
insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24), insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
title: const Text('Birthday', style: TextStyle(fontSize: 16)),
theme: ThemeData( theme: ThemeData(
colorScheme: colorScheme:
const ColorScheme.light().copyWith(primary: kcPrimaryColor), const ColorScheme.light().copyWith(primary: kcPrimaryColor),
@ -72,6 +72,8 @@ class BirthdaySelector extends StatelessWidget {
child: _buildContainer(), child: _buildContainer(),
); );
Widget _buildTitle() => Text('Birthday', style: style16DG600);
Widget _buildContainer() => Container( Widget _buildContainer() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),

View File

@ -44,7 +44,7 @@ class CancelLearnPracticeSheet extends StatelessWidget {
); );
Widget _buildMessage() => Text.rich( Widget _buildMessage() => Text.rich(
TextSpan(text: 'Youre almost there,', style: style18DG600, children: [ TextSpan(text: 'Youre almost there,', style: style18DG700, children: [
TextSpan( TextSpan(
text: ' Johnny!', text: ' Johnny!',
style: style18P600, style: style18P600,

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseCard extends StatelessWidget {
final String title;
final String subtitle;
final GestureTapCallback? onTap;
const CourseCard(
{super.key, this.onTap, required this.title, required this.subtitle});
Color _getColor() {
if (title == 'English Proficiency Exams') {
return kcRed.withValues(alpha: 0.2);
} else {
return kcAquamarine.withValues(alpha: 0.2);
}
}
@override
Widget build(BuildContext context) => _buildContainer();
Widget _buildContainer() => Container(
height: 200,
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: _getColor(),
borderRadius: BorderRadius.circular(5),
),
child: _buildColumn(),
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(),
);
List<Widget> _buildColumnChildren() => [
_buildTitle(),
verticalSpaceTiny,
_buildSubtitle(),
verticalSpaceMedium,
__buildStartButtonWrapper(),
];
Widget _buildTitle() => Text(
title,
style: style18DG700,
);
Widget _buildSubtitle() => Text(
subtitle,
style: style16DG400,
);
Widget __buildStartButtonWrapper() => SizedBox(
height: 40,
child: _buildStartButton(),
);
Widget _buildStartButton() => CustomElevatedButton(
height: 50,
width: 200,
onTap: onTap,
borderRadius: 12,
text: 'Start Course',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:iconsax/iconsax.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/course_module/course_module_viewmodel.dart';
import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import 'package:yimaru_app/ui/widgets/progress_status.dart';
import '../common/app_colors.dart';
import '../common/enmus.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseModuleTile extends StatelessWidget {
final String title;
final GestureTapCallback? onCourseTap;
final GestureTapCallback? onPracticeTap;
const CourseModuleTile({
super.key,
this.onCourseTap,
this.onPracticeTap,
required this.title,
});
@override
Widget build(BuildContext context) => _buildExpansionTileCard();
Widget _buildExpansionTileCard() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: kcPrimaryColor.withOpacity(0.2),
),
),
child: _buildExpansionTile(),
);
Widget _buildExpansionTile() => ExpansionTile(
enabled: true,
title: _buildTitle(),
textColor: kcDarkGrey,
showTrailingIcon: false,
initiallyExpanded: false,
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
backgroundColor: kcPrimaryColor.withOpacity(0.1),
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1),
childrenPadding: const EdgeInsets.symmetric(horizontal: 15),
children: _buildExpansionTileChildren(),
);
List<Widget> _buildExpansionTileChildren() => [
_buildProgressRow(),
verticalSpaceSmall,
_buildActionButtonWrapper(),
verticalSpaceSmall
];
Widget _buildTitle() => Text(
title,
style: style16P600,
);
Widget _buildProgressRow() => Row(
mainAxisSize: MainAxisSize.min,
children: _buildProgressChildren(),
);
List<Widget> _buildProgressChildren() =>
[_buildProgressStatusWrapper(), horizontalSpaceSmall, _buildProgress()];
Widget _buildProgressStatusWrapper() => Expanded(
child: _buildProgressStatus(),
);
Widget _buildProgressStatus() => const CustomLinearProgressIndicator(
progress: 0.75,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
Widget _buildProgress() => const Text(
'75%',
style: TextStyle(color: kcDarkGrey),
);
Widget _buildActionButtonWrapper() => SizedBox(
height: 40,
width: 300,
child: _buildActionButtons(),
);
Widget _buildActionButtons() => Row(
children: [
_buildStartButtonWrapper(),
horizontalSpaceSmall,
_buildExamButtonWrapper()
],
);
Widget _buildStartButtonWrapper() => Expanded(
child: _buildStartButton(),
);
Widget _buildStartButton() => CustomElevatedButton(
height: 15,
borderRadius: 8,
onTap: onCourseTap,
text: 'Start Course',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
Widget _buildExamButtonWrapper() => Expanded(
child: _buildExamButton(),
);
Widget _buildExamButton() => CustomElevatedButton(
height: 15,
borderRadius: 8,
onTap: onPracticeTap,
text: 'Take Mock Exam',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import '../common/ui_helpers.dart';
class CoursePaymentCard extends StatelessWidget {
final String title;
final IconData icon;
final String subtitle;
const CoursePaymentCard(
{super.key,
required this.icon,
required this.title,
required this.subtitle});
@override
Widget build(BuildContext context) => _buildListTile();
Widget _buildListTile() => ListTile(
title: _buildTitle(),
leading: _buildLeading(),
subtitle: _buildSubtitle(),
tileColor: kcPrimaryColor.withValues(alpha: 0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: kcPrimaryColor.withValues(alpha: 0.25)),
),
);
Widget _buildTitle() => Text(
title,
style: style16DG600,
);
Widget _buildSubtitle() => Text(
subtitle,
style: style14DG400,
);
Widget _buildLeading() => CircleAvatar(
radius: 25,
backgroundColor: kcPrimaryColor.withValues(alpha: 0.25),
child: _buildIcon(),
);
Widget _buildIcon() => Icon(
icon,
size: 25,
color: kcPrimaryColor,
);
}

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CoursePracticeCard extends StatelessWidget {
final String title;
const CoursePracticeCard({super.key, required this.title});
@override
Widget build(BuildContext context) => _buildContainer();
Widget _buildContainer() => Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: kcPrimaryColor.withValues(alpha: 0.25),
),
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
child: _buildColumn(),
);
Widget _buildColumn() =>
Column(mainAxisSize: MainAxisSize.min, children: _buildColumnChildren());
List<Widget> _buildColumnChildren() => [
verticalSpaceTiny,
_buildTitle(),
verticalSpaceSmall,
_buildStartButtonWrapper()
];
Widget _buildTitle() => Text(
title,
style: style18DG700,
);
Widget _buildStartButtonWrapper() => SizedBox(
height: 40,
child: _buildStartButton(),
);
Widget _buildStartButton() => CustomElevatedButton(
height: 50,
width: 200,
borderRadius: 8,
text: 'Start Test',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
}

View File

@ -6,7 +6,7 @@ import '../common/app_colors.dart';
import '../common/ui_helpers.dart'; import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart'; import 'custom_elevated_button.dart';
class CourseLevelCard extends StatelessWidget { class CourseProgressCard extends StatelessWidget {
final Color color; final Color color;
final String icon; final String icon;
final String title; final String title;
@ -15,7 +15,7 @@ class CourseLevelCard extends StatelessWidget {
final bool isCompleted; final bool isCompleted;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
const CourseLevelCard({ const CourseProgressCard({
super.key, super.key,
this.onTap, this.onTap,
required this.icon, required this.icon,
@ -56,7 +56,7 @@ class CourseLevelCard extends StatelessWidget {
verticalSpaceSmall, verticalSpaceSmall,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildActionButton() _buildActionButton()
]; ];
@ -89,7 +89,7 @@ class CourseLevelCard extends StatelessWidget {
), ),
); );
Widget _buildSubTitle() => Expanded( Widget _buildSubtitle() => Expanded(
child: Text( child: Text(
subtitle, subtitle,
maxLines: 3, maxLines: 3,

View File

@ -68,7 +68,7 @@ class CustomLargeRadioButton extends StatelessWidget {
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
title, title,
style: style18DG600, style: style18DG700,
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(

View File

@ -183,7 +183,7 @@ class LearnLessonTile extends StatelessWidget {
_buildLessonButton(), _buildLessonButton(),
]; ];
Widget _buildPracticeButton() => CustomElevatedButton( Widget _buildPracticeButton() => CustomElevatedButton(
height: 15, height: 15,
width: 135, width: 135,
text: 'Practice', text: 'Practice',

View File

@ -27,24 +27,19 @@ class LearnPracticeTipSection extends StatelessWidget {
List<Widget> _buildColumnChildren() => List<Widget> _buildColumnChildren() =>
[_buildTitleWrapper(), verticalSpaceTiny, _buildContent()]; [_buildTitleWrapper(), verticalSpaceTiny, _buildContent()];
Widget _buildTitleWrapper() => Widget _buildTitleWrapper() => Row(
Row( children: [_buildLeading(), horizontalSpaceSmall, _buildTitle()],
children: [
_buildLeading(),horizontalSpaceSmall,_buildTitle()
],
); );
Widget _buildLeading() => const Icon( Widget _buildLeading() => const Icon(
Icons.lightbulb_outline_rounded, Icons.lightbulb_outline_rounded,
color: kcBlue, color: kcBlue,
); );
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'Quick Tip', 'Quick Tip',
style: style16B600, style: style16B600,
); );
Widget _buildContent() => Text( Widget _buildContent() => Text(
"You can always do better!\nSpeak in full sentences instead of short phrases.\nMaintain a steady pace, not too fast, not too slow.\nUse varied vocabulary to make your answers richer.\nPause naturally instead of using fillers like “um” or “uh”.", "You can always do better!\nSpeak in full sentences instead of short phrases.\nMaintain a steady pace, not too fast, not too slow.\nUse varied vocabulary to make your answers richer.\nPause naturally instead of using fillers like “um” or “uh”.",

View File

@ -123,14 +123,14 @@ class LearnSubLevelTile extends ViewModelWidget<LearnLevelViewModel> {
); );
Widget _buildPracticeButton(LearnLevelViewModel viewModel) => Widget _buildPracticeButton(LearnLevelViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 15, height: 15,
text: 'Practice', text: 'Practice',
borderRadius: 12, borderRadius: 12,
backgroundColor: kcWhite, backgroundColor: kcWhite,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
onTap: () async => await viewModel.navigateToLearnPractice() onTap: () async => await viewModel.navigateToLearnPractice()
// onTap: () async => await viewModel.navigateToLearnLevel(), // onTap: () async => await viewModel.navigateToLearnLevel(),
); );
} }

View File

@ -22,10 +22,7 @@ class PracticeResponseCard extends StatelessWidget {
); );
Widget _buildRow() => Row( Widget _buildRow() => Row(
children: [ children: [_buildPlayButton(), _buildColumnWrapper()],
_buildPlayButton(),
_buildColumnWrapper()
],
); );
Widget _buildPlayButton() => ElevatedButton( Widget _buildPlayButton() => ElevatedButton(

View File

@ -11,7 +11,11 @@ class PracticeResultCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) => _buildColumnWrapper(); Widget build(BuildContext context) => _buildColumnWrapper();
Widget _buildColumnWrapper() => SizedBox(height: 100,width: double.maxFinite,child: _buildColumn(),); Widget _buildColumnWrapper() => SizedBox(
height: 100,
width: double.maxFinite,
child: _buildColumn(),
);
Widget _buildColumn() => Column( Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -19,7 +23,8 @@ class PracticeResultCard extends StatelessWidget {
children: _buildColumnChildren(), children: _buildColumnChildren(),
); );
List<Widget> _buildColumnChildren() => [_buildQuestion(),verticalSpaceSmall, _buildRow()]; List<Widget> _buildColumnChildren() =>
[_buildQuestion(), verticalSpaceSmall, _buildRow()];
Widget _buildQuestion() => Text( Widget _buildQuestion() => Text(
'$index. ${data['question']}', '$index. ${data['question']}',
@ -30,8 +35,11 @@ class PracticeResultCard extends StatelessWidget {
children: _buildRowChildren(), children: _buildRowChildren(),
); );
List<Widget> _buildRowChildren() => List<Widget> _buildRowChildren() => [
[_buildSampleResponseWrapper(),horizontalSpaceSmall, _buildActualResponseWrapper()]; _buildSampleResponseWrapper(),
horizontalSpaceSmall,
_buildActualResponseWrapper()
];
Widget _buildSampleResponseWrapper() => Widget _buildSampleResponseWrapper() =>
Expanded(child: _buildSampleResponse()); Expanded(child: _buildSampleResponse());

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('CourseModuleViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

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('CoursePaymentViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

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('CoursePracticeViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

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('CourseViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}