Compare commits

...

2 Commits

Author SHA1 Message Date
4eb6e9d6c3 feat(course): Polish course ui 2026-02-20 15:15:23 +03:00
d75ed8c7c7 - fix(auth): Fix how user data in fetched from both local and remote
storage.
- feat(learn): Complete all learn module UI
2026-02-19 10:43:31 +03:00
117 changed files with 2956 additions and 757 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,185 @@ 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) {
return _i29.MaterialPageRoute<dynamic>( final args = data.getArgs<LearnPracticeViewArguments>(nullOk: false);
builder: (context) => const _i28.LearnPracticeView(), return _i33.MaterialPageRoute<dynamic>(
builder: (context) => _i28.LearnPracticeView(
key: args.key,
title: args.title,
subtitle: args.subtitle,
buttonLabel: args.buttonLabel),
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, settings: data,
); );
}, },
@ -432,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;
@ -459,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;
@ -486,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;
@ -507,7 +571,46 @@ class FailureViewArguments {
} }
} }
extension NavigatorStateExtension on _i30.NavigationService { class LearnPracticeViewArguments {
const LearnPracticeViewArguments({
this.key,
required this.title,
required this.subtitle,
required this.buttonLabel,
});
final _i33.Key? key;
final String title;
final String subtitle;
final String buttonLabel;
@override
String toString() {
return '{"key": "$key", "title": "$title", "subtitle": "$subtitle", "buttonLabel": "$buttonLabel"}';
}
@override
bool operator ==(covariant LearnPracticeViewArguments other) {
if (identical(this, other)) return true;
return other.key == key &&
other.title == title &&
other.subtitle == subtitle &&
other.buttonLabel == buttonLabel;
}
@override
int get hashCode {
return key.hashCode ^
title.hashCode ^
subtitle.hashCode ^
buttonLabel.hashCode;
}
}
extension NavigatorStateExtension on _i34.NavigationService {
Future<dynamic> navigateToHomeView([ Future<dynamic> navigateToHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -537,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,
@ -806,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,
@ -837,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,
@ -881,14 +984,79 @@ extension NavigatorStateExtension on _i30.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> navigateToLearnPracticeView([ Future<dynamic> navigateToLearnPracticeView({
_i33.Key? key,
required String title,
required String subtitle,
required String buttonLabel,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
return navigateTo<dynamic>(Routes.learnPracticeView,
arguments: LearnPracticeViewArguments(
key: key,
title: title,
subtitle: subtitle,
buttonLabel: buttonLabel),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToCourseView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)? Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition, transition,
]) async { ]) async {
return navigateTo<dynamic>(Routes.learnPracticeView, 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, id: routerId,
preventDuplicates: preventDuplicates, preventDuplicates: preventDuplicates,
parameters: parameters, parameters: parameters,
@ -924,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,
@ -1193,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,
@ -1224,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,
@ -1268,14 +1436,79 @@ extension NavigatorStateExtension on _i30.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> replaceWithLearnPracticeView([ Future<dynamic> replaceWithLearnPracticeView({
_i33.Key? key,
required String title,
required String subtitle,
required String buttonLabel,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
return replaceWith<dynamic>(Routes.learnPracticeView,
arguments: LearnPracticeViewArguments(
key: key,
title: title,
subtitle: subtitle,
buttonLabel: buttonLabel),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithCourseView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
Map<String, String>? parameters, Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)? Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition, transition,
]) async { ]) async {
return replaceWith<dynamic>(Routes.learnPracticeView, 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, id: routerId,
preventDuplicates: preventDuplicates, preventDuplicates: preventDuplicates,
parameters: parameters, parameters: parameters,

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

@ -12,12 +12,10 @@ class UserModel {
final String? country; final String? country;
final String? occupation; final String? occupation;
final bool? userInfoLoaded; final bool? userInfoLoaded;
@JsonKey(name: 'user_id') @JsonKey(name: 'user_id')
final int? userId; final int? userId;
@ -55,10 +53,41 @@ class UserModel {
this.accessToken, this.accessToken,
this.refreshToken, this.refreshToken,
this.profilePicture, this.profilePicture,
this.userInfoLoaded , this.userInfoLoaded,
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

@ -19,8 +19,8 @@ UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
accessToken: json['access_token'] as String?, accessToken: json['access_token'] as String?,
refreshToken: json['refresh_token'] as String?, refreshToken: json['refresh_token'] as String?,
profilePicture: json['profile_picture_url'] as String?, profilePicture: json['profile_picture_url'] as String?,
userInfoLoaded: json['userInfoLoaded'] as bool?,
profileCompleted: json['profile_completed'] as bool?, profileCompleted: json['profile_completed'] as bool?,
userInfoLoaded: json['userInfoLoaded'] as bool? ?? false,
); );
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{ Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{

View File

@ -8,10 +8,12 @@ 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
Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic> data) async { Future<Map<String, dynamic>> registerWithEmail(
Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kRegisterUrl', '$kBaseUrl/$kUserUrl/$kRegisterUrl',
@ -37,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,7 +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']);
@ -50,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,
@ -68,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> saveProfileImage(String image) async { Future<void> saveProfilePicture(String image) async {
await _secureService.setString('profileImage', 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,
@ -87,9 +107,9 @@ class AuthenticationService with ListenableServiceMixin {
refreshToken: _user?.refreshToken, refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted, profileCompleted: _user?.profileCompleted,
userInfoLoaded: _user?.userInfoLoaded ?? false, userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profileImage'), profilePicture: await _secureService.getString('profilePicture'),
); );
*/
notifyListeners(); notifyListeners();
} }
@ -137,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,
@ -151,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();
} }
@ -175,8 +205,8 @@ class AuthenticationService with ListenableServiceMixin {
occupation: await _secureService.getString('occupation'), occupation: await _secureService.getString('occupation'),
accessToken: await _secureService.getString('accessToken'), accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'), refreshToken: await _secureService.getString('refreshToken'),
profilePicture: await _secureService.getString('profileImage'),
userInfoLoaded: await _secureService.getBool('userInfoLoaded'), userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
profilePicture: await _secureService.getString('profilePicture'),
profileCompleted: await _secureService.getBool('profileCompleted'), profileCompleted: await _secureService.getBool('profileCompleted'),
); );
return _user; return _user;

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

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
const Color kcBlack = Colors.black; const Color kcBlack = Colors.black;
const Color kcRed = Color(0xffFF4C4C); const Color kcRed = Color(0xffFF4C4C);
const Color kcBlue = Color(0xff135BEC);
const Color kcGreen = Color(0xFF1DE964); const Color kcGreen = Color(0xFF1DE964);
const Color kcBackgroundColor = kcWhite; const Color kcBackgroundColor = kcWhite;
const Color kcWhite = Color(0xFFFFFFFF); const Color kcWhite = Color(0xFFFFFFFF);

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,16 +1,18 @@
// 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,
verifyOtp, verifyOtp,
resendOtp, resendOtp,
profileImage, profileImage,

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

@ -178,6 +178,12 @@ TextStyle style18P600 = const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style16W600 = const TextStyle(
fontSize: 16,
color: kcWhite,
fontWeight: FontWeight.w600,
);
TextStyle style18W600 = const TextStyle( TextStyle style18W600 = const TextStyle(
fontSize: 18, fontSize: 18,
color: kcWhite, color: kcWhite,
@ -190,6 +196,11 @@ TextStyle style25W600 = const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style12RP600 = const TextStyle(
fontSize: 12,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style12R700 = const TextStyle( TextStyle style12R700 = const TextStyle(
fontSize: 12, fontSize: 12,
@ -197,10 +208,19 @@ TextStyle style12R700 = const TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
); );
TextStyle style12DG400 = const TextStyle(
fontSize: 12,
color: kcDarkGrey,
);
TextStyle style14P400 = const TextStyle( TextStyle style14P400 = const TextStyle(
color: kcPrimaryColor, color: kcPrimaryColor,
); );
TextStyle style14B400 = const TextStyle(
color: kcBlue,
);
TextStyle style14P600 = const TextStyle( TextStyle style14P600 = const TextStyle(
color: kcPrimaryColor, color: kcPrimaryColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -218,22 +238,45 @@ 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,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style16B600 = const TextStyle(
fontSize: 16,
color: kcBlue,
fontWeight: FontWeight.w600,
);
TextStyle style18DG500 = const TextStyle( TextStyle style18DG500 = const TextStyle(
fontSize: 18, fontSize: 18,
color: kcDarkGrey, color: kcDarkGrey,
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
@ -57,8 +55,9 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
); );
Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar( Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar(
title: 'Account Privacy', showBackButton: true,
onTap: viewModel.pop, onTap: viewModel.pop,
title: 'Account Privacy',
); );
Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) => Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) =>
@ -107,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) =>
@ -146,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) {
@ -235,7 +237,7 @@ class AssessmentViewModel extends BaseViewModel {
await _navigationService.navigateToLanguageView(); await _navigationService.navigateToLanguageView();
Future<void> replaceWithHome() async => Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView()); await _navigationService.clearStackAndShow(Routes.homeView);
// Remote api call // Remote api call
Future<void> getAssessments() async => await runBusyFuture(_getAssessments()); Future<void> getAssessments() async => await runBusyFuture(_getAssessments());

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
@ -50,8 +48,9 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
); );
Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar( Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar(
title: 'Call Support', showBackButton: true,
onTap: viewModel.pop, onTap: viewModel.pop,
title: 'Call Support',
); );
Widget _buildExpandedColumn(CallSupportViewModel viewModel) => Widget _buildExpandedColumn(CallSupportViewModel viewModel) =>
@ -84,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() =>
@ -98,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
@ -54,8 +52,9 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
]; ];
Widget _buildAppbar(DownloadsViewModel viewModel) => SmallAppBar( Widget _buildAppbar(DownloadsViewModel viewModel) => SmallAppBar(
title: 'Offline Downloads',
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
title: 'Offline Downloads',
); );
Widget _buildContentWrapper(DownloadsViewModel viewModel) => Widget _buildContentWrapper(DownloadsViewModel viewModel) =>
@ -94,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(
@ -187,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(
@ -210,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

@ -29,8 +29,6 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
confirmPasswordController.clear(); confirmPasswordController.clear();
} }
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) { void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
if (viewModel.currentPage == 0) { if (viewModel.currentPage == 0) {
emailController.clear(); emailController.clear();
@ -77,10 +75,6 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
_pop(value: value, viewModel: viewModel), _pop(value: value, viewModel: viewModel),
child: _buildBody(viewModel)); child: _buildBody(viewModel));
Widget _buildBody(ForgetPasswordViewModel viewModel) => Widget _buildBody(ForgetPasswordViewModel viewModel) =>
IndexedStack(index: viewModel.currentPage, children: _buildScreens()); IndexedStack(index: viewModel.currentPage, children: _buildScreens());
@ -96,6 +90,4 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
passwordController: passwordController, passwordController: passwordController,
resetCodeController: resetCodeController, resetCodeController: resetCodeController,
confirmPasswordController: confirmPasswordController); confirmPasswordController: confirmPasswordController);
} }

View File

@ -3,6 +3,7 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/ui/views/login/login_view.dart'; import 'package:yimaru_app/ui/views/login/login_view.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../services/api_service.dart'; import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
@ -190,7 +191,7 @@ class ForgetPasswordViewModel extends FormViewModel {
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> replaceWithLogin() async => Future<void> replaceWithLogin() async =>
await _navigationService.clearStackAndShowView(const LoginView()); await _navigationService.clearStackAndShow(Routes.loginView);
// Remote api calls // Remote api calls

View File

@ -20,10 +20,11 @@ class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
required this.emailController, required this.emailController,
}); });
Widget getPadding(context) {
Widget getPadding(context){ double half = screenHeight(context) / 2;
double half = screenHeight(context)/2; return SizedBox(
return SizedBox(height: half + 375 - half,); height: half + 375 - half,
);
} }
void _inAppPop(ForgetPasswordViewModel viewModel) { void _inAppPop(ForgetPasswordViewModel viewModel) {
@ -32,9 +33,8 @@ class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
} }
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) { void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
emailController.clear(); emailController.clear();
viewModel.resetRequestResetCodeScreen(); viewModel.resetRequestResetCodeScreen();
} }
Future<void> _addUserData(ForgetPasswordViewModel viewModel) async { Future<void> _addUserData(ForgetPasswordViewModel viewModel) async {
@ -48,69 +48,89 @@ class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
await viewModel.requestResetCode(); await viewModel.requestResetCode();
} }
@override @override
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) => Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context, Widget _buildScaffoldWrapper(
required ForgetPasswordViewModel viewModel}) => Scaffold( {required BuildContext context,
backgroundColor: kcBackgroundColor, required ForgetPasswordViewModel viewModel}) =>
body: _buildScaffoldStack(context: context, viewModel: viewModel), Scaffold(
); backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack( {required BuildContext context, Widget _buildScaffoldStack(
required ForgetPasswordViewModel viewModel}) => Stack( {required BuildContext context,
children: [ required ForgetPasswordViewModel viewModel}) =>
_buildScaffold(context: context,viewModel: viewModel), Stack(
_buildRequestResetCodeState(viewModel), children: [
], _buildScaffold(context: context, viewModel: viewModel),
); _buildRequestResetCodeState(viewModel),
],
);
Widget _buildScaffold( {required BuildContext context, Widget _buildScaffold(
required ForgetPasswordViewModel viewModel}) => Column( {required BuildContext context,
crossAxisAlignment: CrossAxisAlignment.start, required ForgetPasswordViewModel viewModel}) =>
children: _buildScaffoldChildren(context: context, viewModel: viewModel), Column(
); crossAxisAlignment: CrossAxisAlignment.start,
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren( {required BuildContext context, List<Widget> _buildScaffoldChildren(
required ForgetPasswordViewModel viewModel}) => {required BuildContext context,
[_buildAppBar(viewModel), _buildExpandedBody(context: context, viewModel: viewModel)]; required ForgetPasswordViewModel viewModel}) =>
[
_buildAppBar(viewModel),
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar( Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _inAppPop(viewModel), onPop: () => _inAppPop(viewModel),
); );
Widget _buildExpandedBody( {required BuildContext context, Widget _buildExpandedBody(
required ForgetPasswordViewModel viewModel}) => {required BuildContext context,
Expanded(child: _buildColumnScroller(context: context, viewModel: viewModel)); required ForgetPasswordViewModel viewModel}) =>
Expanded(
child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller( {required BuildContext context, Widget _buildColumnScroller(
required ForgetPasswordViewModel viewModel}) => {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildBodyWrapper(context: context, viewModel: viewModel), child: _buildBodyWrapper(context: context, viewModel: viewModel),
); );
Widget _buildBodyWrapper( {required BuildContext context, Widget _buildBodyWrapper(
required ForgetPasswordViewModel viewModel}) => Padding( {required BuildContext context,
padding: const EdgeInsets.symmetric(horizontal: 15), required ForgetPasswordViewModel viewModel}) =>
child: _buildBody(context: context, viewModel: viewModel), Padding(
); padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context, viewModel: viewModel),
Widget _buildBody( {required BuildContext context, );
required ForgetPasswordViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButtonWrapper(viewModel)];
Widget _buildBody(
{required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren(
{required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
[
_buildUpperColumn(viewModel),
getPadding(context),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildUpperColumn(ForgetPasswordViewModel viewModel) => Column( Widget _buildUpperColumn(ForgetPasswordViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -31,12 +31,10 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
} }
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) { void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
passwordController.clear();
passwordController.clear(); resetCodeController.clear();
resetCodeController.clear(); confirmPasswordController.clear();
confirmPasswordController.clear(); viewModel.resetResetPasswordScreen();
viewModel.resetResetPasswordScreen();
} }
Future<void> _reset(ForgetPasswordViewModel viewModel) async { Future<void> _reset(ForgetPasswordViewModel viewModel) async {
@ -55,56 +53,56 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) => Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context, Widget _buildScaffoldWrapper(
required ForgetPasswordViewModel viewModel}) => Scaffold( {required BuildContext context,
backgroundColor: kcBackgroundColor, required ForgetPasswordViewModel viewModel}) =>
body: _buildScaffoldStack(context: context, viewModel: viewModel), Scaffold(
); backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack( {required BuildContext context, Widget _buildScaffoldStack(
required ForgetPasswordViewModel viewModel}) => Stack( {required BuildContext context,
children: [ required ForgetPasswordViewModel viewModel}) =>
_buildScaffold(viewModel), Stack(
_buildResetPasswordState(viewModel), children: [
], _buildScaffold(viewModel),
); _buildResetPasswordState(viewModel),
],
);
Widget _buildScaffold( ForgetPasswordViewModel viewModel) => Column( Widget _buildScaffold(ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel), children: _buildScaffoldChildren(viewModel),
); );
List<Widget> _buildScaffoldChildren( ForgetPasswordViewModel viewModel) => List<Widget> _buildScaffoldChildren(ForgetPasswordViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar( Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _inAppPop(viewModel), onPop: () => _inAppPop(viewModel),
); );
Widget _buildExpandedBody( ForgetPasswordViewModel viewModel) => Widget _buildExpandedBody(ForgetPasswordViewModel viewModel) =>
Expanded(child: _buildColumnScroller(viewModel)); Expanded(child: _buildColumnScroller(viewModel));
Widget _buildColumnScroller( ForgetPasswordViewModel viewModel) => Widget _buildColumnScroller(ForgetPasswordViewModel viewModel) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildBodyWrapper(viewModel), child: _buildBodyWrapper(viewModel),
); );
Widget _buildBodyWrapper( ForgetPasswordViewModel viewModel) => Padding( Widget _buildBodyWrapper(ForgetPasswordViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel), child: _buildBody(viewModel),
); );
Widget _buildBody(ForgetPasswordViewModel viewModel) => Column( Widget _buildBody(ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel), children: _buildBodyColumnChildren(viewModel),
); );
List<Widget> _buildBodyColumnChildren(ForgetPasswordViewModel viewModel) => [ List<Widget> _buildBodyColumnChildren(ForgetPasswordViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
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/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';
@ -16,19 +19,22 @@ class HomeView extends StackedView<HomeViewModel> {
@override @override
void onViewModelReady(HomeViewModel viewModel) async { void onViewModelReady(HomeViewModel viewModel) async {
await viewModel.getProfileStatus(); await _init(viewModel);
await viewModel.getProfileData();
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
Future<void> _init(HomeViewModel viewModel) async =>
await viewModel.initialize();
@override @override
Widget builder( Widget builder(
BuildContext context, HomeViewModel viewModel, Widget? child) => BuildContext context, HomeViewModel viewModel, Widget? child) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy Widget _buildScaffoldWrapper(HomeViewModel viewModel) =>
? const StartupView(label: 'Checking user info') viewModel.busy(StateObjects.homeView)
: _buildScaffold(viewModel); ? const StartupView(label: 'Checking user info')
: _buildScaffold(viewModel);
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold( Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(
body: getViewForIndex(viewModel.currentIndex), body: getViewForIndex(viewModel.currentIndex),
@ -71,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

@ -37,6 +37,7 @@ class HomeViewModel extends ReactiveViewModel {
int get currentIndex => _currentIndex; int get currentIndex => _currentIndex;
// Bottom navigation
void setCurrentIndex(int index) { void setCurrentIndex(int index) {
_currentIndex = index; _currentIndex = index;
rebuildUi(); rebuildUi();
@ -63,72 +64,76 @@ class HomeViewModel extends ReactiveViewModel {
// Navigation // Navigation
Future<void> replaceWithFailure() async => Future<void> replaceWithFailure() async =>
await _navigationService.clearStackAndShowView( await _navigationService.clearStackAndShow(Routes.failureView,
const FailureView(label: 'Check your internet connection to proceed'), arguments: 'Check your internet connection to proceed');
);
Future<void> replaceWithOnboarding() async => Future<void> replaceWithOnboarding() async =>
await _navigationService.replaceWithOnboardingView(); await _navigationService.replaceWithOnboardingView();
// Remote api calls // Remote api calls
// Initialize user data
Future<void> initialize() async =>
await runBusyFuture(_initialize(), busyObject: StateObjects.homeView);
Future<void> _initialize() async {
await _getProfileStatus();
await _getProfileData();
}
// Profile status
Future<void> _getProfileStatus() async {
Map<String, dynamic> response = {};
if (_user?.profileCompleted == null) {
if (await _statusChecker.checkConnection()) {
print('BREAK-POINT 1');
response = await _apiService.getProfileStatus(_user);
} else {
print('BREAK-POINT 2');
await replaceWithFailure();
}
} else if (!(_user?.profileCompleted ?? false)) {
print('BREAK-POINT 3');
response = {'data': false, 'status': ResponseStatus.success};
} else {
print('BREAK-POINT 4');
response = {'data': true, 'status': ResponseStatus.success};
}
if (response['status'] == ResponseStatus.success && !response['data']) {
print('BREAK-POINT 5');
await replaceWithOnboarding();
} else if (response['status'] == ResponseStatus.success &&
response['data']) {
print('BREAK-POINT 6');
await saveProfileStatus(response['data']);
}
}
// Profile data // Profile data
Future<void> getProfileData() async => await runBusyFuture(_getProfileData());
Future<void> _getProfileData() async { Future<void> _getProfileData() async {
print('RESPONSE FOR USER DATA ${_user?.firstName}');
if (!(_user?.userInfoLoaded ?? false)) { if (!(_user?.userInfoLoaded ?? false)) {
print('RESPONSE FOR USER DATA 1'); Map<String, dynamic> response = {};
if (await _statusChecker.checkConnection()) { print('BREAK-POINT: Profile-Completed ${_user?.profileCompleted}');
Map<String, dynamic> response = {};
if (_user?.profileCompleted != null && if (_user?.profileCompleted != null &&
(_user?.profileCompleted ?? false)) { (_user?.profileCompleted ?? false)) {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileData(_user?.userId); response = await _apiService.getProfileData(_user?.userId);
if (response['status'] == ResponseStatus.success) { if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel; print('BREAK-POINT 8');
String image = UserModel user = response['data'] as UserModel;
await _imageDownloaderService.downloader(user.profilePicture);
await _authenticationService.saveUserData( String image =
image: image, data: user); await _imageDownloaderService.downloader(user.profilePicture);
}
await _authenticationService.saveUserData(image: image, data: user);
} }
} }
} }
} }
} }
// Profile status
Future<void> getProfileStatus() async =>
await runBusyFuture(_getProfileStatus());
Future<void> _getProfileStatus() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = {};
if (_user?.profileCompleted == null) {
if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileStatus(_user);
} else {
await replaceWithFailure();
}
} else if (!(_user?.profileCompleted ?? false)) {
response = {'data': false, 'status': ResponseStatus.success};
} else {
response = {'data': true, 'status': ResponseStatus.success};
}
if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding();
} else if (response['status'] == ResponseStatus.success &&
response['data']) {
await saveProfileStatus(response['data']);
}
}
}
} }

View File

@ -72,6 +72,7 @@ class LanguageView extends StackedView<LanguageViewModel> {
); );
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar( Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
showBackButton: true,
onTap: viewModel.pop, onTap: viewModel.pop,
title: 'Language Preference', title: 'Language Preference',
); );

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

@ -51,6 +51,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
Widget _buildAppBar(LearnLessonViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnLessonViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
); );
Widget _buildLevelsColumnWrapper(LearnLessonViewModel viewModel) => Widget _buildLevelsColumnWrapper(LearnLessonViewModel viewModel) =>
@ -109,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(
@ -122,6 +123,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
thumbnail: viewModel.lessons[index]['thumbnail'], thumbnail: viewModel.lessons[index]['thumbnail'],
onLessonTap: () async => onLessonTap: () async =>
await viewModel.navigateToLearnLessonDetail(), await viewModel.navigateToLearnLessonDetail(),
onPracticeTap: () async => await viewModel.navigateToLearnPractice(),
), ),
); );
@ -130,11 +132,13 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
required String thumbnail, required String thumbnail,
GestureTapCallback? onLessonTap, GestureTapCallback? onLessonTap,
required ProgressStatuses status, required ProgressStatuses status,
GestureTapCallback? onPracticeTap,
}) => }) =>
LearnLessonTile( LearnLessonTile(
title: title, title: title,
status: status, status: status,
thumbnail: thumbnail, thumbnail: thumbnail,
onLessonTap: onLessonTap, onLessonTap: onLessonTap,
onPracticeTap: onPracticeTap,
); );
} }

View File

@ -34,4 +34,12 @@ class LearnLessonViewModel extends BaseViewModel {
Future<void> navigateToLearnLessonDetail() async => Future<void> navigateToLearnLessonDetail() async =>
await _navigationService.navigateToLearnLessonDetailView(); await _navigationService.navigateToLearnLessonDetailView();
Future<void> navigateToLearnPractice() async =>
await _navigationService.navigateToLearnPracticeView(
buttonLabel: 'Start Practice',
title: 'Let \'s practice what you just learnt!',
subtitle:
'Ill ask you a few questions, and you can respond naturally.',
);
} }

View File

@ -15,13 +15,11 @@ import 'learn_lesson_detail_viewmodel.dart';
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> { class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
const LearnLessonDetailView({Key? key}) : super(key: key); const LearnLessonDetailView({Key? key}) : super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
Future<void> _navigate(LearnLessonDetailViewModel viewModel)async{
await viewModel.pause(); await viewModel.pause();
await viewModel.navigateToLearnPractice(); await viewModel.navigateToLearnPractice();
} }
// @override // @override
// void onDispose(LearnLessonDetailViewModel viewModel) { // void onDispose(LearnLessonDetailViewModel viewModel) {
// print('DISPOSED'); // print('DISPOSED');
@ -70,6 +68,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
Widget _buildAppBar(LearnLessonDetailViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnLessonDetailViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
); );
Widget _buildBodyColumnWrapper(LearnLessonDetailViewModel viewModel) => Widget _buildBodyColumnWrapper(LearnLessonDetailViewModel viewModel) =>
@ -173,6 +172,6 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: ()async => await _navigate(viewModel), onTap: () async => await _navigate(viewModel),
); );
} }

View File

@ -53,7 +53,7 @@ class LearnLessonDetailViewModel extends BaseViewModel {
// rebuildUi(); // rebuildUi();
} }
Future<void> pause()async{ Future<void> pause() async {
await _chewieController?.pause(); await _chewieController?.pause();
} }
@ -67,6 +67,11 @@ class LearnLessonDetailViewModel extends BaseViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnPractice() async=>await _navigationService.navigateToLearnPracticeView(); Future<void> navigateToLearnPractice() async =>
await _navigationService.navigateToLearnPracticeView(
buttonLabel: 'Start Practice',
title: 'Let \'s practice what you just learnt!',
subtitle:
'Ill ask you a few questions, and you can respond naturally.',
);
} }

View File

@ -45,6 +45,7 @@ class LearnLevelView extends StackedView<LearnLevelViewModel> {
Widget _buildAppBar(LearnLevelViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnLevelViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
); );
Widget _buildLevelsColumnWrapper(LearnLevelViewModel viewModel) => Widget _buildLevelsColumnWrapper(LearnLevelViewModel viewModel) =>

View File

@ -22,8 +22,16 @@ class LearnLevelViewModel extends BaseViewModel {
List<Map<String, dynamic>> get learnSubLevels => _learnSubLevels; List<Map<String, dynamic>> get learnSubLevels => _learnSubLevels;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnModule() async => Future<void> navigateToLearnModule() async =>
_navigationService.navigateToLearnModuleView(); _navigationService.navigateToLearnModuleView();
void pop() => _navigationService.back(); Future<void> navigateToLearnPractice() async =>
await _navigationService.navigateToLearnPracticeView(
title: 'Lets Practice Level 1',
buttonLabel: 'Begin Level Practice',
subtitle: 'Lets quickly review what youve learned in this level! ',
);
} }

View File

@ -47,6 +47,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
Widget _buildAppBar(LearnModuleViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnModuleViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
); );
Widget _buildLevelsColumnWrapper(LearnModuleViewModel viewModel) => Widget _buildLevelsColumnWrapper(LearnModuleViewModel viewModel) =>
@ -66,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,
@ -78,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,15 +8,16 @@ 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.started, 'status': ProgressStatuses.completed,
'title': 'Module 1: Greetings & Introductions', 'title': 'Module 1: Greetings & Introductions',
'subtitle': 'subtitle':
'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.', 'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.',
}, },
{ {
'status': ProgressStatuses.pending, 'status': ProgressStatuses.started,
'title': 'Module 2: Everyday Basics', 'title': 'Module 2: Everyday Basics',
'subtitle': 'Learn numbers, colors, and common objects.', 'subtitle': 'Learn numbers, colors, and common objects.',
}, },
@ -35,8 +36,16 @@ 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 =>
await _navigationService.navigateToLearnLessonView(); await _navigationService.navigateToLearnLessonView();
Future<void> navigateToLearnPractice() async =>
await _navigationService.navigateToLearnPracticeView(
title: 'Lets Practice Module 1',
buttonLabel: 'Begin Module Practice',
subtitle: 'Lets quickly review what youve learned in this module! ',
);
} }

View File

@ -1,8 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/listen_speaker_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/finish_learn_practice_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/practice_intro_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_completion_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/start_practice_screen.dart'; import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_result_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/listen_learn_practice_speaker_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/learn_practice_intro_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/speak_to_learn_practice_listener_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/start_learn_practice_screen.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart'; import 'package:yimaru_app/ui/widgets/profile_image.dart';
import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart'; import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart';
@ -13,7 +17,16 @@ import '../../widgets/small_app_bar.dart';
import 'learn_practice_viewmodel.dart'; import 'learn_practice_viewmodel.dart';
class LearnPracticeView extends StackedView<LearnPracticeViewModel> { class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
const LearnPracticeView({Key? key}) : super(key: key); final String title;
final String subtitle;
final String buttonLabel;
const LearnPracticeView(
{Key? key,
required this.title,
required this.subtitle,
required this.buttonLabel})
: super(key: key);
@override @override
LearnPracticeViewModel viewModelBuilder(BuildContext context) => LearnPracticeViewModel viewModelBuilder(BuildContext context) =>
@ -27,46 +40,59 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
) => ) =>
_buildPracticeScreensWrapper(viewModel); _buildPracticeScreensWrapper(viewModel);
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) =>
PopScope(
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) => PopScope( canPop: true,
canPop: true, onPopInvokedWithResult: (value, data) {
onPopInvokedWithResult: (value, data) { if (!value) return;
if (!value) return; WidgetsBinding.instance
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); .addPostFrameCallback((_) => viewModel.goBack());
}, },
child: _buildScaffoldWrapper(viewModel)); child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel), body: _buildScaffoldStack(viewModel),
); );
Widget _buildScaffoldStack(LearnPracticeViewModel viewModel) => Stack(children: [
_buildBody(viewModel),
//_buildLoginWithEmailState(viewModel),
//_buildLoginWithGoogleState(viewModel)
]);
Widget _buildScaffoldStack(LearnPracticeViewModel viewModel) =>
Stack(children: [
_buildBody(viewModel),
//_buildLoginWithEmailState(viewModel),
//_buildLoginWithGoogleState(viewModel)
]);
Widget _buildBody(LearnPracticeViewModel viewModel) => Widget _buildBody(LearnPracticeViewModel viewModel) =>
IndexedStack( IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
index: viewModel.currentIndex, children: _buildScreens());
List<Widget> _buildScreens() => [ List<Widget> _buildScreens() => [
_buildPracticeIntroScreen(), _buildLearnPracticeIntroScreen(),
_buildStartPracticeScreen(), _buildStartLearnPracticeScreen(),
_buildListenSpeakerScreen() _buildListenLearnPracticeSpeakerScreen(),
]; _buildSpeakToLearnPracticeListenerScreen(),
_buildFinishLearnPracticeScreen(),
_buildLearnPracticeResultScreen(),
_buildLearnPracticeCompletionScreen()
];
Widget _buildPracticeIntroScreen() => const PracticeIntroScreen(); Widget _buildLearnPracticeIntroScreen() => LearnPracticeIntroScreen(
title: title,
subtitle: subtitle,
buttonLabel: buttonLabel,
);
Widget _buildStartPracticeScreen() => const StartPracticeScreen(); Widget _buildStartLearnPracticeScreen() => const StartLearnPracticeScreen();
Widget _buildListenSpeakerScreen() => const ListenSpeakerScreen(); Widget _buildListenLearnPracticeSpeakerScreen() =>
const ListenLearnPracticeSpeakerScreen();
Widget _buildSpeakToLearnPracticeListenerScreen() =>
const SpeakToLearnPracticeListenerScreen();
Widget _buildFinishLearnPracticeScreen() => const FinishLearnPracticeScreen();
Widget _buildLearnPracticeResultScreen() => const LearnPracticeResultScreen();
Widget _buildLearnPracticeCompletionScreen() =>
const LearnPracticeCompletionScreen();
} }

View File

@ -11,24 +11,30 @@ class LearnPracticeViewModel extends BaseViewModel {
int get currentIndex => _currentIndex; int get currentIndex => _currentIndex;
// Practice results
final List<Map<String, dynamic>> _practiceResults = [
{'question': 'What is your name?'},
{'question': 'Where are you from?'},
{'question': 'Where are you from?'}
];
List<Map<String, dynamic>> get practiceResults => _practiceResults;
// In-app navigation // In-app navigation
void goTo(int page) { void goTo(int page) {
_currentIndex = page; _currentIndex = page;
rebuildUi(); rebuildUi();
} }
void goBack() { void goBack() {
if(_currentIndex == 0){ if (_currentIndex == 0) {
pop(); pop();
}else{ } else {
_currentIndex--; _currentIndex--;
rebuildUi(); rebuildUi();
} }
} }
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
} }

View File

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_column_button.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/small_app_bar.dart';
class FinishLearnPracticeScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const FinishLearnPracticeScreen({super.key});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
SafeArea(child: _buildBodyColumnWrapper(viewModel));
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
_buildAppBarWrapper(viewModel),
_buildSpeakingIndicatorWrapper(viewModel),
_buildLowerButtonsSectionWrapper(viewModel)
];
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
showBackButton: false,
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildSpeakingIndicatorChildren(),
);
List<Widget> _buildSpeakingIndicatorChildren() => [
_buildIcon(),
verticalSpaceMedium,
_buildTitle(),
_buildSubtitle(),
];
Widget _buildIcon() => SvgPicture.asset('assets/icons/success.svg');
Widget _buildTitle() => Text(
'Practice Completed!',
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'You sound more confident this time - great improvement!',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildLowerButtonsSectionWrapper(LearnPracticeViewModel viewModel) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: _buildLowerButtonsSection(viewModel),
);
Widget _buildLowerButtonsSection(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
LearnPracticeViewModel viewModel) =>
[
_buildContinueButton(viewModel),
verticalSpaceSmall,
_buildPracticeAgainButton(viewModel),
verticalSpaceMedium,
];
Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
foregroundColor: kcWhite,
text: 'Continue Practice',
onTap: () => viewModel.goTo(5),
backgroundColor: kcPrimaryColor,
);
Widget _buildPracticeAgainButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Practice Again',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
onTap: () => viewModel.goTo(1),
foregroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
class LearnPracticeCompletionScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeCompletionScreen({super.key});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(LearnPracticeViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LearnPracticeViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(LearnPracticeViewModel viewModel) => [
verticalSpaceMassive,
_buildIcon(),
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
];
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/complete.svg',
);
Widget _buildTitle() => Text(
'Yay, youve completed A1 ',
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Were now analyzing your speaking skills',
textAlign: TextAlign.center,
style: style14MG400,
);
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Continue',
foregroundColor: kcWhite,
onTap: () => viewModel.pop(),
backgroundColor: kcPrimaryColor,
);
}

View File

@ -9,53 +9,61 @@ import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/small_app_bar.dart'; import '../../../widgets/small_app_bar.dart';
import '../../../widgets/speaking_partner_image.dart'; import '../../../widgets/speaking_partner_image.dart';
class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> { class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
const PracticeIntroScreen({super.key}); final String title;
final String subtitle;
final String buttonLabel;
const LearnPracticeIntroScreen(
{super.key,
required this.title,
required this.subtitle,
required this.buttonLabel});
@override @override
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildScaffoldWrapper(viewModel); Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffold(viewModel),
); );
Widget _buildScaffold(LearnPracticeViewModel viewModel) => Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
SafeArea(child: _buildColumnWrapper(viewModel)); SafeArea(child: _buildColumnWrapper(viewModel));
Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => Padding( Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel), child: _buildColumn(viewModel),
); );
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column( Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
children: [ children: [
verticalSpaceMedium, verticalSpaceMedium,
_buildAppBar(viewModel), _buildAppBar(viewModel),
_buildBodyColumnWrapper(viewModel), _buildBodyColumnWrapper(viewModel),
], ],
); );
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack, showBackButton: true,
title: 'Practice Speaking', onTap: viewModel.goBack,
); title: 'Practice Speaking',
);
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded( Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded(
child: _buildBodyColumn(viewModel), child: _buildBodyColumn(viewModel),
); );
Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column( Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel), children: _buildBodyColumnChildren(viewModel),
); );
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [ List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
_buildPracticeColumnWrapper(viewModel), _buildPracticeColumnWrapper(viewModel),
_buildContinueButtonWrapper(viewModel) _buildContinueButtonWrapper(viewModel)
]; ];
Widget _buildPracticeColumnWrapper(LearnPracticeViewModel viewModel) => Widget _buildPracticeColumnWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildPracticeColumnScrollView(viewModel)); Expanded(child: _buildPracticeColumnScrollView(viewModel));
@ -66,10 +74,10 @@ class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
); );
Widget _buildPracticeColumn(LearnPracticeViewModel viewModel) => Column( Widget _buildPracticeColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: _buildPracticeColumnChildren(viewModel), children: _buildPracticeColumnChildren(viewModel),
); );
List<Widget> _buildPracticeColumnChildren(LearnPracticeViewModel viewModel) => List<Widget> _buildPracticeColumnChildren(LearnPracticeViewModel viewModel) =>
[ [
@ -83,28 +91,30 @@ class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
_buildSubtitle() _buildSubtitle()
]; ];
Widget _buildImage() => const SpeakingPartnerImage(radius: 75,); Widget _buildImage() => const SpeakingPartnerImage(
radius: 75,
);
Widget _buildPartnerName() => Text.rich( Widget _buildPartnerName() => Text.rich(
TextSpan(text: 'Daniel', style: style14DG600, children: [ TextSpan(text: 'Daniel', style: style14DG600, children: [
TextSpan( TextSpan(
text: ' - Your Speaking Partner', text: ' - Your Speaking Partner',
style: style14MG400, style: style14MG400,
) )
]), ]),
); );
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
'Let \'s practice what you just learnt!', title,
style: style25DG600, style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildSubtitle() => Text( Widget _buildSubtitle() => Text(
'Ill ask you a few questions, and you can respond naturally.', subtitle,
style: style14DG400, style: style14DG400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) => Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
Padding( Padding(
@ -116,9 +126,9 @@ class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'Start Practice', text: buttonLabel,
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: ()=> viewModel.goTo(1), onTap: () => viewModel.goTo(1),
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
} }

View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/learn_practice_tip_section.dart';
import 'package:yimaru_app/ui/widgets/practice_results_wrapper.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_column_button.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/small_app_bar.dart';
class LearnPracticeResultScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeResultScreen({super.key});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
SafeArea(child: _buildBodyColumnWrapper(viewModel));
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(viewModel) => Column(
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildBodyWrapper(viewModel)
];
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
title: 'Result',
showBackButton: true,
onTap: viewModel.goBack,
);
Widget _buildBodyWrapper(LearnPracticeViewModel viewMode) => Expanded(
child: _buildBodyScroller(viewMode),
);
Widget _buildBodyScroller(LearnPracticeViewModel viewModel) =>
SingleChildScrollView(
child: _buildBody(viewModel),
);
Widget _buildBody(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LearnPracticeViewModel viewModel) => [
_buildResultsSection(viewModel),
verticalSpaceMedium,
_buildLearnPracticeTipSection(),
verticalSpaceLarge,
_buildLowerButtonsSection(viewModel)
];
Widget _buildResultsSection(LearnPracticeViewModel viewModel) =>
PracticeResultsWrapper(data: viewModel.practiceResults);
Widget _buildLearnPracticeTipSection() => const LearnPracticeTipSection();
Widget _buildLowerButtonsSection(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
LearnPracticeViewModel viewModel) =>
[
_buildContinueButton(viewModel),
verticalSpaceSmall,
_buildPracticeAgainButton(viewModel),
verticalSpaceMedium,
];
Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: () => viewModel.goTo(6),
backgroundColor: kcPrimaryColor,
);
Widget _buildPracticeAgainButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Retry',
borderRadius: 12,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
onTap: () => viewModel.goTo(1),
foregroundColor: kcPrimaryColor,
);
}

View File

@ -10,8 +10,9 @@ import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_column_button.dart'; import '../../../widgets/custom_column_button.dart';
import '../../../widgets/small_app_bar.dart'; import '../../../widgets/small_app_bar.dart';
class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> { class ListenLearnPracticeSpeakerScreen
const ListenSpeakerScreen({super.key}); extends ViewModelWidget<LearnPracticeViewModel> {
const ListenLearnPracticeSpeakerScreen({super.key});
Future<void> _showSheet( Future<void> _showSheet(
{required BuildContext context, {required BuildContext context,
@ -86,6 +87,7 @@ class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
); );
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
showBackButton: true,
onTap: viewModel.goBack, onTap: viewModel.goBack,
title: 'Practice Speaking', title: 'Practice Speaking',
); );
@ -190,7 +192,7 @@ class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
required LearnPracticeViewModel viewModel}) => required LearnPracticeViewModel viewModel}) =>
[ [
_buildReplyButtonWrapper(), _buildReplyButtonWrapper(),
_buildMicButtonWrapper(), _buildMicButtonWrapper(viewModel),
_buildCancelButtonWrapper(context: context, viewModel: viewModel) _buildCancelButtonWrapper(context: context, viewModel: viewModel)
]; ];
@ -199,10 +201,11 @@ class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildReplyButton() => const CustomColumnButton( Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor); icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton()); Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildMicButton(viewModel));
Widget _buildMicButton() => ElevatedButton( Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton(
onPressed: () {}, onPressed: () => viewModel.goTo(3),
style: const ButtonStyle( style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()), shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)), padding: WidgetStatePropertyAll(EdgeInsets.all(15)),

View File

@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_column_button.dart';
import '../../../widgets/small_app_bar.dart';
class SpeakToLearnPracticeListenerScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const SpeakToLearnPracticeListenerScreen({super.key});
Future<void> _showSheet(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SafeArea(
child:
_buildBodyColumnWrapper(context: context, viewModel: viewModel));
Widget _buildBodyColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyStack(context: context, viewModel: viewModel),
);
Widget _buildBodyStack(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Stack(
children: [
_buildBodyColumn(context: context, viewModel: viewModel),
_buildProgressIndicatorWrapper()
],
);
Widget _buildBodyColumn(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
_buildBodyColumnChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyColumnChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildAppBarWrapper(viewModel),
_buildSpeakingIndicatorWrapper(viewModel),
_buildLowerButtonsSectionWrapper(context: context, viewModel: viewModel)
];
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
showBackButton: true,
title: 'Practice Speaking',
);
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildSpeakingIndicatorChildren(),
);
List<Widget> _buildSpeakingIndicatorChildren() =>
[_buildSpeakerLabel(), verticalSpaceMedium, _buildSpeakingIndicator()];
Widget _buildSpeakerLabel() => Text(
'You are speaking...',
style: style14P400,
textAlign: TextAlign.center,
);
Widget _buildSpeakingIndicator() => Container(
height: 100,
alignment: Alignment.center,
child: _buildSpinner(),
);
Widget _buildSpinner() => const SpinKitWave(
size: 75,
color: kcPrimaryColor,
type: SpinKitWaveType.center,
);
Widget _buildLowerButtonsSectionWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child:
_buildLowerButtonsSection(context: context, viewModel: viewModel),
);
Widget _buildLowerButtonsSection(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildActionLabel(),
verticalSpaceMedium,
_buildButtonsRowWrapper(context: context, viewModel: viewModel),
verticalSpaceMedium,
];
Widget _buildActionLabel() => Text(
'Tap the microphone to speak',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildButtonsRowWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
_buildButtonsRowChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildButtonsRowChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildEmptySpace(),
_buildCheckButtonWrapper(viewModel),
_buildCancelButtonWrapper(context: context, viewModel: viewModel)
];
Widget _buildEmptySpace() => Expanded(child: Container());
Widget _buildCheckButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildCheckButton(viewModel));
Widget _buildCheckButton(LearnPracticeViewModel viewModel) => ElevatedButton(
onPressed: () => viewModel.goTo(4),
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
child: _buildCheckIcon(),
);
Widget _buildCheckIcon() => const Icon(
Icons.check,
size: 35,
color: kcWhite,
);
Widget _buildCancelButtonWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Expanded(
child: _buildCancelButton(context: context, viewModel: viewModel));
Widget _buildCancelButton(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
CustomColumnButton(
color: kcRed,
label: 'Cancel',
icon: Icons.close,
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
);
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
CancelLearnPracticeSheet(
onTap: viewModel.pop,
);
Widget _buildProgressIndicatorWrapper() => Positioned(
top: 75,
left: 0,
right: 0,
child: _buildProgressIndicator(),
);
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
progress: 0.7,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
}

View File

@ -7,8 +7,8 @@ import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/small_app_bar.dart'; import '../../../widgets/small_app_bar.dart';
class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> { class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
const StartPracticeScreen({super.key}); const StartLearnPracticeScreen({super.key});
@override @override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) => Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
@ -47,6 +47,7 @@ class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar( Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack, onTap: viewModel.goBack,
showBackButton: true,
title: 'Practice Speaking', title: 'Practice Speaking',
); );
@ -122,7 +123,7 @@ class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
[ [
_buildActionLabel(), _buildActionLabel(),
verticalSpaceMedium, verticalSpaceMedium,
_buildButtonsRowWrapper(), _buildButtonsRowWrapper(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
]; ];
@ -132,15 +133,15 @@ class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildButtonsRowWrapper() => Row( Widget _buildButtonsRowWrapper(LearnPracticeViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildButtonsRowChildren(), children: _buildButtonsRowChildren(viewModel),
); );
List<Widget> _buildButtonsRowChildren() => [ List<Widget> _buildButtonsRowChildren(LearnPracticeViewModel viewModel) => [
_buildReplyButtonWrapper(), _buildReplyButtonWrapper(),
_buildMicButtonWrapper(), _buildMicButtonWrapper(viewModel),
_buildEmptySpace() _buildEmptySpace()
]; ];
@ -149,10 +150,11 @@ class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildReplyButton() => const CustomColumnButton( Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor); icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton()); Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildMicButton(viewModel));
Widget _buildMicButton() => ElevatedButton( Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton(
onPressed: () {}, onPressed: () => viewModel.goTo(2),
style: const ButtonStyle( style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()), shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)), padding: WidgetStatePropertyAll(EdgeInsets.all(15)),

View File

@ -56,10 +56,6 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
}, },
child: _buildBody(viewModel)); child: _buildBody(viewModel));
Widget _buildBody(LoginViewModel viewModel) => Widget _buildBody(LoginViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
@ -78,5 +74,4 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
Widget _buildLoginOtpScreen() => LoginOtpScreen( Widget _buildLoginOtpScreen() => LoginOtpScreen(
otpController: otpController, otpController: otpController,
phoneNumberController: phoneNumberController); phoneNumberController: phoneNumberController);
} }

View File

@ -142,7 +142,7 @@ class LoginViewModel extends FormViewModel {
await _navigationService.navigateToForgetPasswordView(); await _navigationService.navigateToForgetPasswordView();
Future<void> replaceWithHome() async => Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView()); await _navigationService.clearStackAndShow(Routes.homeView);
// Remote api calls // Remote api calls
@ -171,8 +171,9 @@ class LoginViewModel extends FormViewModel {
} }
} }
Future<void> signInWithGoogle() async => await runBusyFuture(_signInWithGoogle(), Future<void> signInWithGoogle() async =>
busyObject: StateObjects.loginWithGoogle); await runBusyFuture(_signInWithGoogle(),
busyObject: StateObjects.loginWithGoogle);
Future<void> _signInWithGoogle() async { Future<void> _signInWithGoogle() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {

View File

@ -21,68 +21,83 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
required this.otpController, required this.otpController,
required this.phoneNumberController}); required this.phoneNumberController});
Widget getPadding(context){ Widget getPadding(context) {
double half = screenHeight(context)/2; double half = screenHeight(context) / 2;
return SizedBox(height: half + 325 - half,); return SizedBox(
height: half + 325 - half,
);
} }
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildScaffoldWrapper(context: context,viewModel: viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold( Widget _buildScaffoldWrapper(
backgroundColor: kcBackgroundColor, {required BuildContext context, required LoginViewModel viewModel}) =>
body: _buildScaffold(context: context,viewModel: viewModel), Scaffold(
); backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context, viewModel: viewModel),
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context,viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding( Widget _buildScaffold(
padding: const EdgeInsets.symmetric(horizontal: 15), {required BuildContext context, required LoginViewModel viewModel}) =>
child: _buildBody(context: context,viewModel: viewModel), Column(
); crossAxisAlignment: CrossAxisAlignment.start,
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column( List<Widget> _buildScaffoldChildren(
crossAxisAlignment: CrossAxisAlignment.start, {required BuildContext context, required LoginViewModel viewModel}) =>
mainAxisAlignment: MainAxisAlignment.spaceBetween, [
children: _buildBodyChildren(context: context,viewModel: viewModel), _buildAppBar(viewModel),
); _buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(
{required BuildContext context, required LoginViewModel viewModel}) =>
Expanded(
child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller(
{required BuildContext context, required LoginViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) => Widget _buildBodyWrapper(
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButton(viewModel)]; {required BuildContext context, required LoginViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context, viewModel: viewModel),
);
Widget _buildBody(
{required BuildContext context, required LoginViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren(
{required BuildContext context, required LoginViewModel viewModel}) =>
[
_buildUpperColumn(viewModel),
getPadding(context),
_buildContinueButton(viewModel)
];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel), children: _buildUpperColumnChildren(viewModel),
); );
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,

View File

@ -22,10 +22,11 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
required this.emailController, required this.emailController,
required this.passwordController}); required this.passwordController});
Widget getPadding(context) {
Widget getPadding(context){ double half = screenHeight(context) / 2;
double half = screenHeight(context)/2; return SizedBox(
return SizedBox(height: half + 25 - half,); height: half + 25 - half,
);
} }
Future<void> _login(LoginViewModel viewModel) async { Future<void> _login(LoginViewModel viewModel) async {
@ -42,57 +43,75 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildScaffoldWrapper(context: context,viewModel: viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold( Widget _buildScaffoldWrapper(
backgroundColor: kcBackgroundColor, {required BuildContext context, required LoginViewModel viewModel}) =>
body: _buildScaffoldStack(context: context,viewModel: viewModel), Scaffold(
); backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack({required BuildContext context,required LoginViewModel viewModel}) => Stack(children: [ Widget _buildScaffoldStack(
_buildScaffold(context: context,viewModel: viewModel), {required BuildContext context, required LoginViewModel viewModel}) =>
_buildLoginWithEmailState(viewModel), Stack(children: [
_buildLoginWithGoogleState(viewModel) _buildScaffold(context: context, viewModel: viewModel),
]); _buildLoginWithEmailState(viewModel),
_buildLoginWithGoogleState(viewModel)
]);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column( Widget _buildScaffold(
crossAxisAlignment: CrossAxisAlignment.start, {required BuildContext context, required LoginViewModel viewModel}) =>
children: _buildScaffoldChildren(context: context,viewModel: viewModel), Column(
); crossAxisAlignment: CrossAxisAlignment.start,
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) => List<Widget> _buildScaffoldChildren(
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)]; {required BuildContext context, required LoginViewModel viewModel}) =>
[
_buildAppBar(viewModel),
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar( Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false, showBackButton: false,
showLanguageSelection: true, showLanguageSelection: true,
); );
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) => Widget _buildExpandedBody(
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel)); {required BuildContext context, required LoginViewModel viewModel}) =>
Expanded(
child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) => Widget _buildColumnScroller(
{required BuildContext context, required LoginViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildBodyWrapper(context: context,viewModel: viewModel), child: _buildBodyWrapper(context: context, viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding( Widget _buildBodyWrapper(
padding: const EdgeInsets.symmetric(horizontal: 15), {required BuildContext context, required LoginViewModel viewModel}) =>
child: _buildBody(context: context,viewModel: viewModel), Padding(
); padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context, viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column( Widget _buildBody(
{required BuildContext context, required LoginViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context,viewModel: viewModel), children: _buildBodyChildren(context: context, viewModel: viewModel),
); );
List<Widget> _buildBodyChildren(
{required BuildContext context, required LoginViewModel viewModel}) =>
[
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) => _buildUpperColumn(viewModel),
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)]; getPadding(context),
_buildLowerColumn(viewModel)
];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -246,7 +265,6 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
text: 'Login with Phone Number', text: 'Login with Phone Number',
); );
Widget _buildLoginWithEmailState(LoginViewModel viewModel) => Widget _buildLoginWithEmailState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithEmail) viewModel.busy(StateObjects.loginWithEmail)
? const PageLoadingIndicator() ? const PageLoadingIndicator()

View File

@ -18,70 +18,82 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
const LoginWithPhoneNumberScreen( const LoginWithPhoneNumberScreen(
{super.key, required this.phoneNumberController}); {super.key, required this.phoneNumberController});
Widget getPadding(context){ Widget getPadding(context) {
double half = screenHeight(context)/2; double half = screenHeight(context) / 2;
return SizedBox(height: half + 175 - half,); return SizedBox(
height: half + 175 - half,
);
} }
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
_buildScaffoldWrapper(context: context,viewModel: viewModel); Widget _buildScaffoldWrapper(
{required BuildContext context, required LoginViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold( body: _buildScaffold(context: context, viewModel: viewModel),
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context,viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding( Widget _buildScaffold(
padding: const EdgeInsets.symmetric(horizontal: 15), {required BuildContext context, required LoginViewModel viewModel}) =>
child: _buildBody(context: context,viewModel: viewModel), Column(
); crossAxisAlignment: CrossAxisAlignment.start,
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column( List<Widget> _buildScaffoldChildren(
crossAxisAlignment: CrossAxisAlignment.start, {required BuildContext context, required LoginViewModel viewModel}) =>
children: _buildBodyChildren(context: context,viewModel: viewModel), [
); _buildAppBar(viewModel),
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(
{required BuildContext context, required LoginViewModel viewModel}) =>
Expanded(
child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller(
{required BuildContext context, required LoginViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) => Widget _buildBodyWrapper(
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)]; {required BuildContext context, required LoginViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context, viewModel: viewModel),
);
Widget _buildBody(
{required BuildContext context, required LoginViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren(
{required BuildContext context, required LoginViewModel viewModel}) =>
[
_buildUpperColumn(viewModel),
getPadding(context),
_buildLowerColumn(viewModel)
];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel), children: _buildUpperColumnChildren(viewModel),
); );
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,

View File

@ -481,5 +481,5 @@ class OnboardingViewModel extends FormViewModel {
await _navigationService.navigateToAssessmentView(data: _userData); await _navigationService.navigateToAssessmentView(data: _userData);
Future<void> replaceWithHome() async => Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView()); await _navigationService.clearStackAndShow(Routes.homeView);
} }

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

@ -58,6 +58,7 @@ class OngoingProgressView extends StackedView<OngoingProgressViewModel> {
Widget _buildAppbar(OngoingProgressViewModel viewModel) => SmallAppBar( Widget _buildAppbar(OngoingProgressViewModel viewModel) => SmallAppBar(
title: 'My Progress', title: 'My Progress',
showBackButton: true,
onTap: viewModel.pop, onTap: viewModel.pop,
); );

View File

@ -54,8 +54,9 @@ class PrivacyPolicyView extends StackedView<PrivacyPolicyViewModel> {
); );
Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar( Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar(
title: 'Privacy Policy', showBackButton: true,
onTap: viewModel.pop, onTap: viewModel.pop,
title: 'Privacy Policy',
); );
Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) => Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) =>

View File

@ -42,7 +42,7 @@ class ProfileViewModel extends ReactiveViewModel {
pop(); pop();
if (image != null) { if (image != null) {
await updateProfilePicture(image); await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image); await _authenticationService.saveProfilePicture(image);
} }
} }
@ -54,7 +54,7 @@ class ProfileViewModel extends ReactiveViewModel {
pop(); pop();
if (image != null) { if (image != null) {
await updateProfilePicture(image); await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image); await _authenticationService.saveProfilePicture(image);
} }
} }

View File

@ -148,8 +148,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
]; ];
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar( Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
title: 'Edit Profile',
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
title: 'Edit Profile',
); );
Widget _buildColumnWrapper( Widget _buildColumnWrapper(

View File

@ -185,7 +185,7 @@ class ProfileDetailViewModel extends ReactiveViewModel
pop(); pop();
if (image != null) { if (image != null) {
await updateProfilePicture(image); await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image); await _authenticationService.saveProfilePicture(image);
} }
} }
@ -197,7 +197,7 @@ class ProfileDetailViewModel extends ReactiveViewModel
pop(); pop();
if (image != null) { if (image != null) {
await updateProfilePicture(image); await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image); await _authenticationService.saveProfilePicture(image);
} }
} }

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';
@ -57,6 +57,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
Widget _buildAppbar(ProgressViewModel viewModel) => SmallAppBar( Widget _buildAppbar(ProgressViewModel viewModel) => SmallAppBar(
title: 'My Progress', title: 'My Progress',
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
); );
Widget _buildContentWrapper(ProgressViewModel viewModel) => Widget _buildContentWrapper(ProgressViewModel viewModel) =>
@ -114,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

@ -87,8 +87,6 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
_pop(value: value, viewModel: viewModel), _pop(value: value, viewModel: viewModel),
child: _buildBody(viewModel)); child: _buildBody(viewModel));
Widget _buildBody(RegisterViewModel viewModel) => Widget _buildBody(RegisterViewModel viewModel) =>
IndexedStack(index: viewModel.currentPage, children: _buildScreens()); IndexedStack(index: viewModel.currentPage, children: _buildScreens());
@ -114,5 +112,4 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
Widget _buildCreatePasswordScreen() => CreatePasswordScreen( Widget _buildCreatePasswordScreen() => CreatePasswordScreen(
passwordController: passwordController, passwordController: passwordController,
confirmPasswordController: confirmPasswordController); confirmPasswordController: confirmPasswordController);
} }

View File

@ -23,7 +23,6 @@ class RegisterViewModel extends FormViewModel {
final _googleAuthService = locator<GoogleAuthService>(); final _googleAuthService = locator<GoogleAuthService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
// Navigation // Navigation
@ -284,17 +283,18 @@ class RegisterViewModel extends FormViewModel {
await _navigationService.replaceWithLoginView(); await _navigationService.replaceWithLoginView();
Future<void> replaceWithHome() async => Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView()); await _navigationService.clearStackAndShow(Routes.homeView);
// Remote api calls // Remote api calls
// Register // Register
Future<void> registerWithEmail() async => Future<void> registerWithEmail() async => await runBusyFuture(_register(),
await runBusyFuture(_register(), busyObject: StateObjects.registerWithEmail); busyObject: StateObjects.registerWithEmail);
Future<void> _register() async { Future<void> _register() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = await _apiService.registerWithEmail(_userData); Map<String, dynamic> response =
await _apiService.registerWithEmail(_userData);
if (response['status'] == ResponseStatus.success) { if (response['status'] == ResponseStatus.success) {
goTo(page: 3); goTo(page: 3);

View File

@ -231,8 +231,6 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
onTap: () => viewModel.goTo(page: 1), onTap: () => viewModel.goTo(page: 1),
); );
Widget _buildRegisterWithGoogleState(RegisterViewModel viewModel) => Widget _buildRegisterWithGoogleState(RegisterViewModel viewModel) =>
viewModel.busy(StateObjects.registerWithEmail) viewModel.busy(StateObjects.registerWithEmail)
? const PageLoadingIndicator() ? const PageLoadingIndicator()

View File

@ -26,10 +26,11 @@ class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
required this.emailController, required this.emailController,
required this.phoneNumberController}); required this.phoneNumberController});
Widget getPadding(context) {
Widget getPadding(context){ double half = screenHeight(context) / 2;
double half = screenHeight(context)/2; return SizedBox(
return SizedBox(height: half + 325 - half,); height: half + 325 - half,
);
} }
Future<void> _verifyOtp(RegisterViewModel viewModel) async { Future<void> _verifyOtp(RegisterViewModel viewModel) async {
@ -45,20 +46,21 @@ class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
await viewModel.verifyOtp(); await viewModel.verifyOtp();
} }
@override @override
Widget build(BuildContext context, RegisterViewModel viewModel) => Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context, Widget _buildScaffoldWrapper(
required RegisterViewModel viewModel}) => Scaffold( {required BuildContext context,
backgroundColor: kcBackgroundColor, required RegisterViewModel viewModel}) =>
body: _buildScaffoldStack(context: context, viewModel: viewModel), Scaffold(
); backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack( Widget _buildScaffoldStack(
{required BuildContext context, {required BuildContext context,
required RegisterViewModel viewModel}) => required RegisterViewModel viewModel}) =>
Stack( Stack(
children: [ children: [
_buildScaffold(context: context, viewModel: viewModel), _buildScaffold(context: context, viewModel: viewModel),
@ -66,49 +68,66 @@ class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
], ],
); );
Widget _buildScaffold( {required BuildContext context, Widget _buildScaffold(
required RegisterViewModel viewModel}) => Column( {required BuildContext context,
crossAxisAlignment: CrossAxisAlignment.start, required RegisterViewModel viewModel}) =>
children: _buildScaffoldChildren(context: context, viewModel: viewModel), Column(
); crossAxisAlignment: CrossAxisAlignment.start,
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren( {required BuildContext context, List<Widget> _buildScaffoldChildren(
required RegisterViewModel viewModel}) => {required BuildContext context,
[_buildAppBar(viewModel), _buildExpandedBody(context: context, viewModel: viewModel)]; required RegisterViewModel viewModel}) =>
[
_buildAppBar(viewModel),
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(RegisterViewModel viewModel) => const LargeAppBar( Widget _buildAppBar(RegisterViewModel viewModel) => const LargeAppBar(
showBackButton: false, showBackButton: false,
showLanguageSelection: true, showLanguageSelection: true,
); );
Widget _buildExpandedBody( {required BuildContext context, Widget _buildExpandedBody(
required RegisterViewModel viewModel}) => {required BuildContext context,
Expanded(child: _buildColumnScroller(context: context, viewModel: viewModel)); required RegisterViewModel viewModel}) =>
Expanded(
child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller( {required BuildContext context, Widget _buildColumnScroller(
required RegisterViewModel viewModel}) => {required BuildContext context,
required RegisterViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildBodyWrapper(context: context, viewModel: viewModel), child: _buildBodyWrapper(context: context, viewModel: viewModel),
); );
Widget _buildBodyWrapper( {required BuildContext context, Widget _buildBodyWrapper(
required RegisterViewModel viewModel}) => Padding( {required BuildContext context,
padding: const EdgeInsets.symmetric(horizontal: 15), required RegisterViewModel viewModel}) =>
child: _buildBody(context: context, viewModel: viewModel), Padding(
); padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context, viewModel: viewModel),
Widget _buildBody( {required BuildContext context, );
required RegisterViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren( {required BuildContext context,
required RegisterViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButtonWrapper(viewModel)];
Widget _buildBody(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
[
_buildUpperColumn(viewModel),
getPadding(context),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( Widget _buildUpperColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -70,8 +70,7 @@ class StartupView extends StackedView<StartupViewModel> {
_buildIndicatorWrapper(), _buildIndicatorWrapper(),
]; ];
Widget _buildLoadingText() => Widget _buildLoadingText() => Text('$label ...', style: style16W600);
Text('$label ...', style: const TextStyle(color: kcWhite, fontSize: 16));
Widget _buildIndicatorWrapper() => SizedBox( Widget _buildIndicatorWrapper() => SizedBox(
width: 16, width: 16,
@ -83,7 +82,7 @@ class StartupView extends StackedView<StartupViewModel> {
const CustomCircularProgressIndicator(color: kcWhite); const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildIconWrapper() => Padding( Widget _buildIconWrapper() => Padding(
padding: const EdgeInsets.only(top: 100), padding: const EdgeInsets.only(top: 120),
child: _buildIcon(), child: _buildIcon(),
); );

View File

@ -52,6 +52,7 @@ class SupportView extends StackedView<SupportViewModel> {
Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar( Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar(
title: 'Need Help?', title: 'Need Help?',
showBackButton: true,
onTap: viewModel.pop, onTap: viewModel.pop,
); );

View File

@ -49,8 +49,9 @@ class TelegramSupportView extends StackedView<TelegramSupportViewModel> {
); );
Widget _buildAppbar(TelegramSupportViewModel viewModel) => SmallAppBar( Widget _buildAppbar(TelegramSupportViewModel viewModel) => SmallAppBar(
title: 'Telegram Support',
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
title: 'Telegram Support',
); );
Widget _buildExpandedColumn(TelegramSupportViewModel viewModel) => Widget _buildExpandedColumn(TelegramSupportViewModel viewModel) =>
@ -84,7 +85,7 @@ class TelegramSupportView extends StackedView<TelegramSupportViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => Widget _buildIcon() =>
@ -100,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

@ -57,8 +57,9 @@ class TermsAndConditionsView extends StackedView<TermsAndConditionsViewModel> {
); );
Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar( Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar(
title: 'Terms and Conditions',
onTap: viewModel.pop, onTap: viewModel.pop,
showBackButton: true,
title: 'Terms and Conditions',
); );
Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) => Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) =>

View File

@ -59,14 +59,10 @@ class FirstWelcomeScreen extends ViewModelWidget<WelcomeViewModel> {
height: 50, height: 50,
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Small daily practice. Big lifelong results.', 'Small daily practice. Big lifelong results.',
style: style25W600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30,
color: kcWhite,
fontWeight: FontWeight.w600,
),
); );
Widget _buildContinueButtonWrapper(WelcomeViewModel viewModel) => Align( Widget _buildContinueButtonWrapper(WelcomeViewModel viewModel) => Align(
@ -85,8 +81,8 @@ class FirstWelcomeScreen extends ViewModelWidget<WelcomeViewModel> {
borderRadius: 12, borderRadius: 12,
text: 'Start Learning', text: 'Start Learning',
backgroundColor: kcWhite, backgroundColor: kcWhite,
trailingIcon: Icons.arrow_forward,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
trailingIcon: Icons.arrow_forward,
); );
} }

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

@ -37,15 +37,14 @@ class CancelLearnPracticeSheet extends StatelessWidget {
verticalSpaceLarge, verticalSpaceLarge,
_buildContinueButton(), _buildContinueButton(),
_buildEndButton(), _buildEndButton(),
];
];
Widget _buildImage() => const SpeakingPartnerImage( Widget _buildImage() => const SpeakingPartnerImage(
radius: 45, radius: 45,
); );
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

@ -9,6 +9,6 @@ class CustomCircularProgressIndicator extends StatelessWidget {
Widget _buildIndicator() => CircularProgressIndicator( Widget _buildIndicator() => CircularProgressIndicator(
color: color, color: color,
strokeWidth: 6, strokeWidth: 5,
); );
} }

Some files were not shown because too many files have changed in this diff Show More