Compare commits

..

3 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
1c8b54fe69 Merge tag '0.1.0-internal.v1' into develop
- refactor(auth): Apply code refactor for login, register and forget
  password functionalities
2026-02-12 18:03:27 +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/learn_lesson_detail/learn_lesson_detail_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/course/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
@StackedApp(
@ -69,6 +73,10 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
MaterialRoute(page: ForgetPasswordView),
MaterialRoute(page: LearnLessonDetailView),
MaterialRoute(page: LearnPracticeView),
MaterialRoute(page: CourseView),
MaterialRoute(page: CourseModuleView),
MaterialRoute(page: CoursePracticeView),
MaterialRoute(page: CoursePaymentView),
// @stacked-route
],
dependencies: [

View File

@ -5,15 +5,22 @@
// **************************************************************************
// 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' as _i33;
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'
as _i10;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'
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/failure/failure_view.dart' as _i25;
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 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>{
homeView,
onboardingView,
@ -132,6 +147,10 @@ class Routes {
forgetPasswordView,
learnLessonDetailView,
learnPracticeView,
courseView,
courseModuleView,
coursePracticeView,
coursePaymentView,
};
}
@ -245,17 +264,33 @@ class StackedRouter extends _i1.RouterBase {
Routes.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>{
_i2.HomeView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(),
settings: data,
);
},
_i3.OnboardingView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(),
settings: data,
);
@ -264,156 +299,185 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(),
);
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data,
);
},
_i5.ProfileView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(),
settings: data,
);
},
_i6.ProfileDetailView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(),
settings: data,
);
},
_i7.DownloadsView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(),
settings: data,
);
},
_i8.ProgressView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(),
settings: data,
);
},
_i9.OngoingProgressView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(),
settings: data,
);
},
_i10.AccountPrivacyView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(),
settings: data,
);
},
_i11.SupportView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(),
settings: data,
);
},
_i12.TelegramSupportView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(),
settings: data,
);
},
_i13.CallSupportView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(),
settings: data,
);
},
_i14.LanguageView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(),
settings: data,
);
},
_i15.PrivacyPolicyView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(),
settings: data,
);
},
_i16.TermsAndConditionsView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(),
settings: data,
);
},
_i17.RegisterView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(),
settings: data,
);
},
_i18.LoginView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(),
settings: data,
);
},
_i19.LearnView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(),
settings: data,
);
},
_i20.LearnLevelView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(),
settings: data,
);
},
_i21.LearnModuleView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(),
settings: data,
);
},
_i22.WelcomeView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i22.WelcomeView(),
settings: data,
);
},
_i23.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) =>
_i23.AssessmentView(key: args.key, data: args.data),
settings: data,
);
},
_i24.LearnLessonView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.LearnLessonView(),
settings: data,
);
},
_i25.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) =>
_i25.FailureView(key: args.key, label: args.label),
settings: data,
);
},
_i26.ForgetPasswordView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i26.ForgetPasswordView(),
settings: data,
);
},
_i27.LearnLessonDetailView: (data) {
return _i29.MaterialPageRoute<dynamic>(
return _i33.MaterialPageRoute<dynamic>(
builder: (context) => const _i27.LearnLessonDetailView(),
settings: data,
);
},
_i28.LearnPracticeView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i28.LearnPracticeView(),
final args = data.getArgs<LearnPracticeViewArguments>(nullOk: false);
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,
);
},
@ -432,7 +496,7 @@ class StartupViewArguments {
this.label = 'Loading',
});
final _i29.Key? key;
final _i33.Key? key;
final String label;
@ -459,7 +523,7 @@ class AssessmentViewArguments {
required this.data,
});
final _i29.Key? key;
final _i33.Key? key;
final Map<String, dynamic> data;
@ -486,7 +550,7 @@ class FailureViewArguments {
required this.label,
});
final _i29.Key? key;
final _i33.Key? key;
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([
int? routerId,
bool preventDuplicates = true,
@ -537,7 +640,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> navigateToStartupView({
_i29.Key? key,
_i33.Key? key,
String label = 'Loading',
int? routerId,
bool preventDuplicates = true,
@ -806,7 +909,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> navigateToAssessmentView({
_i29.Key? key,
_i33.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -837,7 +940,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> navigateToFailureView({
_i29.Key? key,
_i33.Key? key,
required String label,
int? routerId,
bool preventDuplicates = true,
@ -881,14 +984,79 @@ extension NavigatorStateExtension on _i30.NavigationService {
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,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) 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,
preventDuplicates: preventDuplicates,
parameters: parameters,
@ -924,7 +1092,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> replaceWithStartupView({
_i29.Key? key,
_i33.Key? key,
String label = 'Loading',
int? routerId,
bool preventDuplicates = true,
@ -1193,7 +1361,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> replaceWithAssessmentView({
_i29.Key? key,
_i33.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -1224,7 +1392,7 @@ extension NavigatorStateExtension on _i30.NavigationService {
}
Future<dynamic> replaceWithFailureView({
_i29.Key? key,
_i33.Key? key,
required String label,
int? routerId,
bool preventDuplicates = true,
@ -1268,14 +1436,79 @@ extension NavigatorStateExtension on _i30.NavigationService {
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,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) 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,
preventDuplicates: preventDuplicates,
parameters: parameters,

View File

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

View File

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

View File

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

View File

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

View File

@ -8,10 +8,12 @@ import '../app/app.locator.dart';
import '../ui/common/enmus.dart';
class ApiService {
// Dependency injection
final _service = locator<DioService>();
// Register
Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic> data) async {
Future<Map<String, dynamic>> registerWithEmail(
Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kRegisterUrl',
@ -37,7 +39,7 @@ class ApiService {
}
}
// Email Login
// Email login
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
try {
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';
class AuthenticationService with ListenableServiceMixin {
// Dependency injection
final _secureService = locator<SecureStorageService>();
AuthenticationService() {
listenToReactiveValues([_user]);
}
// User data
UserModel? _user;
UserModel? get user => _user;
// Initialization
AuthenticationService() {
listenToReactiveValues([_user]);
}
// Check user logged in
Future<bool> userLoggedIn() async {
if (await _secureService.getString('userId') != null) {
return true;
@ -21,14 +25,18 @@ class AuthenticationService with ListenableServiceMixin {
return false;
}
// Get access token
Future<String?> getAccessToken() async =>
await _secureService.getString('accessToken');
// Get refresh token
Future<String?> getRefreshToken() async =>
await _secureService.getString('refreshToken');
// Get user id
Future<int?> getUserId() async => await _secureService.getInt('userId');
// Save tokens
Future<void> saveTokens({
required String access,
required String refresh,
@ -37,7 +45,7 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('refreshToken', refresh);
}
// Save user credential
Future<void> saveUserCredential(Map<String, dynamic> data) async {
await _secureService.setInt('userId', data['userId']);
await _secureService.setString('accessToken', data['accessToken']);
@ -50,10 +58,16 @@ class AuthenticationService with ListenableServiceMixin {
);
}
// Save profile status
Future<void> saveProfileStatus(bool value) async {
await _secureService.setBool('profileCompleted', value);
_user = UserModel(
_user = _user?.copyWith(
userInfoLoaded: _user?.userInfoLoaded ?? false,
profileCompleted: await _secureService.getBool('profileCompleted'),
);
/* UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
@ -68,12 +82,18 @@ class AuthenticationService with ListenableServiceMixin {
profilePicture: _user?.profilePicture,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profileCompleted: await _secureService.getBool('profileCompleted'));
*/
notifyListeners();
}
Future<void> saveProfileImage(String image) async {
await _secureService.setString('profileImage', image);
_user = UserModel(
Future<void> saveProfilePicture(String image) async {
await _secureService.setString('profilePicture', image);
_user = _user?.copyWith(
userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profilePicture'),
);
/*UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
@ -87,9 +107,9 @@ class AuthenticationService with ListenableServiceMixin {
refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profileImage'),
profilePicture: await _secureService.getString('profilePicture'),
);
*/
notifyListeners();
}
@ -137,7 +157,17 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('firstName', data['first_name']);
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,
userId: _user?.userId,
accessToken: _user?.accessToken,
@ -151,7 +181,7 @@ class AuthenticationService with ListenableServiceMixin {
birthday: await _secureService.getString('birthday'),
firstName: await _secureService.getString('firstName'),
occupation: await _secureService.getString('occupation'),
);
);*/
notifyListeners();
}
@ -175,8 +205,8 @@ class AuthenticationService with ListenableServiceMixin {
occupation: await _secureService.getString('occupation'),
accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'),
profilePicture: await _secureService.getString('profileImage'),
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
profilePicture: await _secureService.getString('profilePicture'),
profileCompleted: await _secureService.getBool('profileCompleted'),
);
return _user;

View File

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

View File

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

View File

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

View File

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

View File

@ -15,8 +15,7 @@ extension BoolParsing on String {
}
class SecureStorageService {
// Create storage
// Initialization
late final FlutterSecureStorage _storage;
SecureStorageService() {
@ -31,33 +30,40 @@ class SecureStorageService {
);
}
// Clear storage data
Future<void> clear() async {
_storage.deleteAll();
}
// Get boolean data from storage
Future<bool?> getBool(String key) async {
String? result = await _storage.read(key: key);
return result?.parseBool();
}
// Get string data from storage
Future<String?> getString(String key) async {
return await _storage.read(key: key);
}
// Get integer data from storage
Future<int?> getInt(String key) async {
return await _storage.read(key: key) == null
? null
: int.parse(await _storage.read(key: key) ?? '0');
}
// Save string data to storage
Future<void> setString(String key, String value) async {
await _storage.write(key: key, value: value);
}
// Save integer data to storage
Future<void> setInt(String key, int value) async {
await _storage.write(key: key, value: value.toString());
}
// Save boolean data to storage
Future<void> setBool(String key, bool value) async {
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';
class StatusCheckerService {
// Dependency injection
final storage = locator<SecureStorageService>();
// Initialization
bool _previousConnection = true;
bool get previousConnection => _previousConnection;
// Get phone battery level
Future<int> getBatteryLevel() async {
final battery = Battery();
final batteryLevel = await battery.batteryLevel;
return batteryLevel;
}
Future<bool> userAuthenticated() async {
await checkAndUpdate();
if (await storage.getString('authenticated') != null) {
return true;
}
return false;
}
// Check internet connection
Future<bool> checkConnection() async {
if (await InternetConnection().hasInternetAccess) {
_previousConnection = true;
return true;
} else {
if (_previousConnection) {
// showErrorToast('Check your internet connection');
_previousConnection = false;
}
@ -43,6 +37,7 @@ class StatusCheckerService {
}
}
// Check phone available storage
Future<int> getAvailableStorage() async {
try {
final availableStorage =
@ -53,6 +48,7 @@ class StatusCheckerService {
}
}
// Check for latest update
Future<void> checkAndUpdate() async {
const requiredStorage = 500 * 1024 * 1024;
@ -62,16 +58,12 @@ class StatusCheckerService {
await getAvailableStorage(); // Implement getAvailableStorage
if (batteryLevel < 20 || storageAvailable < requiredStorage) {
if (batteryLevel < 20 || storageAvailable < requiredStorage) {
// KewedeConst().showErrorToast(
// 'Unable to update app, please charge your phone & free up space.');
} else if (batteryLevel < 20) {
// KewedeConst()
// .showErrorToast('Unable to update app, please charge your phone.');
} else if (storageAvailable < requiredStorage) {
// KewedeConst()
// .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
}
try {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
const Color kcBlack = Colors.black;
const Color kcRed = Color(0xffFF4C4C);
const Color kcBlue = Color(0xff135BEC);
const Color kcGreen = Color(0xFF1DE964);
const Color kcBackgroundColor = kcWhite;
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 kServerClientId =
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
String kSampleVideoUrl =
'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 =
"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 =
'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 = """
<p style="color:#9C2C91;font-size:13px;">
Last updated: October 26, 2025

View File

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

View File

@ -1,3 +1,5 @@
// Split full name
Map<String, String> splitFullName(String fullName) {
final parts = fullName.trim().split(RegExp(r'\s+'));

View File

@ -178,6 +178,12 @@ TextStyle style18P600 = const TextStyle(
fontWeight: FontWeight.w600,
);
TextStyle style16W600 = const TextStyle(
fontSize: 16,
color: kcWhite,
fontWeight: FontWeight.w600,
);
TextStyle style18W600 = const TextStyle(
fontSize: 18,
color: kcWhite,
@ -190,6 +196,11 @@ TextStyle style25W600 = const TextStyle(
fontWeight: FontWeight.w600,
);
TextStyle style12RP600 = const TextStyle(
fontSize: 12,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style12R700 = const TextStyle(
fontSize: 12,
@ -197,10 +208,19 @@ TextStyle style12R700 = const TextStyle(
fontWeight: FontWeight.w700,
);
TextStyle style12DG400 = const TextStyle(
fontSize: 12,
color: kcDarkGrey,
);
TextStyle style14P400 = const TextStyle(
color: kcPrimaryColor,
);
TextStyle style14B400 = const TextStyle(
color: kcBlue,
);
TextStyle style14P600 = const TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
@ -218,22 +238,45 @@ TextStyle style25DG600 = const TextStyle(
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(
fontSize: 16,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
);
TextStyle style16B600 = const TextStyle(
fontSize: 16,
color: kcBlue,
fontWeight: FontWeight.w600,
);
TextStyle style18DG500 = const TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w500,
);
TextStyle style18DG600 = const TextStyle(
TextStyle style18DG700 = const TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
);
TextStyle style20DG700 = const TextStyle(
fontSize: 20,
color: kcDarkGrey,
fontWeight: FontWeight.w700,
);
TextStyle style16DG400 = const TextStyle(

View File

@ -1,6 +1,7 @@
import 'package:email_validator/email_validator.dart';
class FormValidator {
// Form validator
static String? validateForm(String? value) {
if (value == null) {
return null;
@ -12,6 +13,37 @@ class FormValidator {
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) {
if (value == null) {
return null;
@ -35,30 +67,4 @@ class FormValidator {
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);
@override
AccountPrivacyViewModel viewModelBuilder(
BuildContext context,
) =>
AccountPrivacyViewModel viewModelBuilder(BuildContext context) =>
AccountPrivacyViewModel();
@override
@ -57,8 +55,9 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
);
Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar(
title: 'Account Privacy',
showBackButton: true,
onTap: viewModel.pop,
title: 'Account Privacy',
);
Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) =>
@ -107,7 +106,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildHeader(String title) => Text(
title,
style: style18DG600,
style: style18DG700,
);
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>
@ -146,8 +145,8 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
);
Widget _buildDeleteButton() => CustomElevatedButton(
height: 55,
text: 'Delete Account',
borderRadius: 12,
text: 'Delete Account',
foregroundColor: kcRed,
backgroundColor: kcRed.withOpacity(0.25),
);

View File

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

View File

@ -23,6 +23,9 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
super.onViewModelReady(viewModel);
}
@override
AssessmentViewModel viewModelBuilder(BuildContext context) => AssessmentViewModel();
@override
Widget builder(
BuildContext context,
@ -65,9 +68,5 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
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';
class AssessmentViewModel extends BaseViewModel {
// Dependency injection
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
@ -207,6 +208,7 @@ class AssessmentViewModel extends BaseViewModel {
}
}
// In-app navigation
void next({int? page}) async {
if (page == null) {
if (_previousPage != 0) {
@ -235,7 +237,7 @@ class AssessmentViewModel extends BaseViewModel {
await _navigationService.navigateToLanguageView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
await _navigationService.clearStackAndShow(Routes.homeView);
// Remote api call
Future<void> getAssessments() async => await runBusyFuture(_getAssessments());

View File

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

View File

@ -77,8 +77,9 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildSubtitle() => Text(
'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,
textAlign: TextAlign.center,
);
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
@ -97,9 +98,9 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55,
safe: false,
borderRadius: 12,
foregroundColor: kcWhite,
text: 'Continue Assessment',
onTap: () => viewModel.next(),
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);

View File

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

View File

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

View File

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

View File

@ -12,9 +12,7 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
const CallSupportView({Key? key}) : super(key: key);
@override
CallSupportViewModel viewModelBuilder(
BuildContext context,
) =>
CallSupportViewModel viewModelBuilder(BuildContext context) =>
CallSupportViewModel();
@override
@ -50,8 +48,9 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
);
Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar(
title: 'Call Support',
showBackButton: true,
onTap: viewModel.pop,
title: 'Call Support',
);
Widget _buildExpandedColumn(CallSupportViewModel viewModel) =>
@ -84,9 +83,9 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubTitle('+2519012345678'),
_buildSubtitle('+2519012345678'),
verticalSpaceSmall,
_buildSubTitle('+2519012345678'),
_buildSubtitle('+2519012345678'),
];
Widget _buildIcon() =>
@ -98,10 +97,10 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
textAlign: TextAlign.center,
);
Widget _buildSubTitle(String title) => Text(
Widget _buildSubtitle(String title) => Text(
title,
style: style14P400,
textAlign: TextAlign.center,
style: const TextStyle(color: kcPrimaryColor),
);
Widget _buildContinueButtonWrapper(CallSupportViewModel viewModel) => Padding(

View File

@ -4,6 +4,9 @@ import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class CallSupportViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>();
// Navigation
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);
@override
DownloadsViewModel viewModelBuilder(
BuildContext context,
) =>
DownloadsViewModel viewModelBuilder(BuildContext context) =>
DownloadsViewModel();
@override
@ -54,8 +52,9 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
];
Widget _buildAppbar(DownloadsViewModel viewModel) => SmallAppBar(
title: 'Offline Downloads',
onTap: viewModel.pop,
showBackButton: true,
title: 'Offline Downloads',
);
Widget _buildContentWrapper(DownloadsViewModel viewModel) =>
@ -94,20 +93,11 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
List<Widget> _buildStorageInfoChildren(DownloadsViewModel viewModel) =>
[_buildStorageInfo(), _buildManageButton(viewModel)];
Widget _buildStorageInfo() => const Text.rich(
TextSpan(
text: '1.2GB',
style: TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
children: [
Widget _buildStorageInfo() => Text.rich(
TextSpan(text: '1.2GB', style: style14P600, children: [
TextSpan(
text: ' used of 2GB',
style: TextStyle(
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
style: style14DG600,
)
]),
);
@ -115,12 +105,9 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
Widget _buildManageButton(DownloadsViewModel viewModel) => TextButton(
onPressed: viewModel.setShowDownload, child: _buildManageText());
Widget _buildManageText() => const Text(
Widget _buildManageText() => Text(
'Manage Storage',
style: TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
style: style14P600,
);
Widget _buildStorageIndicator() => const CustomLinearProgressIndicator(
@ -187,20 +174,16 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
color: kcPrimaryColor,
);
Widget _buildEmptyTitle() => const Text(
Widget _buildEmptyTitle() => Text(
'Looking for something to download?',
style: style25DG600,
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.',
style: style14MG400,
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildGoButtonWrapper(DownloadsViewModel viewModel) => Padding(
@ -210,9 +193,9 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
Widget _buildGoButton(DownloadsViewModel viewModel) => CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Go to Learn Section',
onTap: viewModel.setShowDownload,
foregroundColor: kcWhite,
text: 'Go to Learn Section',
backgroundColor: kcPrimaryColor,
onTap: viewModel.setShowDownload,
);
}

View File

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

View File

@ -29,8 +29,6 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
confirmPasswordController.clear();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
if (viewModel.currentPage == 0) {
emailController.clear();
@ -77,10 +75,6 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
_pop(value: value, viewModel: viewModel),
child: _buildBody(viewModel));
Widget _buildBody(ForgetPasswordViewModel viewModel) =>
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
@ -96,6 +90,4 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
passwordController: passwordController,
resetCodeController: resetCodeController,
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 '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
@ -190,7 +191,7 @@ class ForgetPasswordViewModel extends FormViewModel {
void pop() => _navigationService.back();
Future<void> replaceWithLogin() async =>
await _navigationService.clearStackAndShowView(const LoginView());
await _navigationService.clearStackAndShow(Routes.loginView);
// Remote api calls

View File

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

View File

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

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/course/course_view.dart';
import 'package:yimaru_app/ui/views/learn/learn_view.dart';
import 'package:yimaru_app/ui/views/profile/profile_view.dart';
import 'package:yimaru_app/ui/views/startup/startup_view.dart';
@ -16,17 +19,20 @@ class HomeView extends StackedView<HomeViewModel> {
@override
void onViewModelReady(HomeViewModel viewModel) async {
await viewModel.getProfileStatus();
await viewModel.getProfileData();
await _init(viewModel);
super.onViewModelReady(viewModel);
}
Future<void> _init(HomeViewModel viewModel) async =>
await viewModel.initialize();
@override
Widget builder(
BuildContext context, HomeViewModel viewModel, Widget? child) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy
Widget _buildScaffoldWrapper(HomeViewModel viewModel) =>
viewModel.busy(StateObjects.homeView)
? const StartupView(label: 'Checking user info')
: _buildScaffold(viewModel);
@ -71,7 +77,7 @@ Widget getViewForIndex(int index) {
case 0:
return const LearnView();
case 1:
return const ComingSoon();
return const CourseView();
default:
return const ProfileView();

View File

@ -37,6 +37,7 @@ class HomeViewModel extends ReactiveViewModel {
int get currentIndex => _currentIndex;
// Bottom navigation
void setCurrentIndex(int index) {
_currentIndex = index;
rebuildUi();
@ -63,25 +64,58 @@ class HomeViewModel extends ReactiveViewModel {
// Navigation
Future<void> replaceWithFailure() async =>
await _navigationService.clearStackAndShowView(
const FailureView(label: 'Check your internet connection to proceed'),
);
await _navigationService.clearStackAndShow(Routes.failureView,
arguments: 'Check your internet connection to proceed');
Future<void> replaceWithOnboarding() async =>
await _navigationService.replaceWithOnboardingView();
// 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
Future<void> getProfileData() async => await runBusyFuture(_getProfileData());
Future<void> _getProfileData() async {
print('RESPONSE FOR USER DATA ${_user?.firstName}');
if (!(_user?.userInfoLoaded ?? false)) {
print('RESPONSE FOR USER DATA 1');
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = {};
print('BREAK-POINT: Profile-Completed ${_user?.profileCompleted}');
if (_user?.profileCompleted != null &&
(_user?.profileCompleted ?? false)) {
@ -89,46 +123,17 @@ class HomeViewModel extends ReactiveViewModel {
response = await _apiService.getProfileData(_user?.userId);
if (response['status'] == ResponseStatus.success) {
print('BREAK-POINT 8');
UserModel user = response['data'] as UserModel;
String image =
await _imageDownloaderService.downloader(user.profilePicture);
await _authenticationService.saveUserData(
image: image, data: user);
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(
showBackButton: true,
onTap: viewModel.pop,
title: 'Language Preference',
);

View File

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

View File

@ -51,6 +51,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
Widget _buildAppBar(LearnLessonViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildLevelsColumnWrapper(LearnLessonViewModel viewModel) =>
@ -109,7 +110,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
Widget _buildHeader() => Text(
'Module 1: Greetings & Introductions',
style: style18DG600,
style: style18DG700,
);
Widget _buildListView(LearnLessonViewModel viewModel) => ListView.builder(
@ -122,6 +123,7 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
thumbnail: viewModel.lessons[index]['thumbnail'],
onLessonTap: () async =>
await viewModel.navigateToLearnLessonDetail(),
onPracticeTap: () async => await viewModel.navigateToLearnPractice(),
),
);
@ -130,11 +132,13 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
required String thumbnail,
GestureTapCallback? onLessonTap,
required ProgressStatuses status,
GestureTapCallback? onPracticeTap,
}) =>
LearnLessonTile(
title: title,
status: status,
thumbnail: thumbnail,
onLessonTap: onLessonTap,
onPracticeTap: onPracticeTap,
);
}

View File

@ -34,4 +34,12 @@ class LearnLessonViewModel extends BaseViewModel {
Future<void> navigateToLearnLessonDetail() async =>
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> {
const LearnLessonDetailView({Key? key}) : super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel)async{
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
await viewModel.pause();
await viewModel.navigateToLearnPractice();
}
// @override
// void onDispose(LearnLessonDetailViewModel viewModel) {
// print('DISPOSED');
@ -70,6 +68,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
Widget _buildAppBar(LearnLessonDetailViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildBodyColumnWrapper(LearnLessonDetailViewModel viewModel) =>
@ -173,6 +172,6 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: ()async => await _navigate(viewModel),
onTap: () async => await _navigate(viewModel),
);
}

View File

@ -53,7 +53,7 @@ class LearnLessonDetailViewModel extends BaseViewModel {
// rebuildUi();
}
Future<void> pause()async{
Future<void> pause() async {
await _chewieController?.pause();
}
@ -67,6 +67,11 @@ class LearnLessonDetailViewModel extends BaseViewModel {
// Navigation
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(
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildLevelsColumnWrapper(LearnLevelViewModel viewModel) =>

View File

@ -22,8 +22,16 @@ class LearnLevelViewModel extends BaseViewModel {
List<Map<String, dynamic>> get learnSubLevels => _learnSubLevels;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnModule() async =>
_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(
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildLevelsColumnWrapper(LearnModuleViewModel viewModel) =>
@ -66,7 +67,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
List<Widget> _buildLevelsColumnChildren(LearnModuleViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildOverallProgress(),
verticalSpaceMedium,
@ -78,7 +79,7 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
style: style18P600,
);
Widget _buildSubTitle() => Text(
Widget _buildSubtitle() => Text(
'Your Current Level',
style: style14DG400,
);

View File

@ -8,15 +8,16 @@ import '../../common/enmus.dart';
class LearnModuleViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// Modules
final List<Map<String, dynamic>> _modules = [
{
'status': ProgressStatuses.started,
'status': ProgressStatuses.completed,
'title': 'Module 1: Greetings & Introductions',
'subtitle':
'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.',
},
{
'status': ProgressStatuses.pending,
'status': ProgressStatuses.started,
'title': 'Module 2: Everyday Basics',
'subtitle': 'Learn numbers, colors, and common objects.',
},
@ -35,8 +36,16 @@ class LearnModuleViewModel extends BaseViewModel {
List<Map<String, dynamic>> get modules => _modules;
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnLesson() async =>
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: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/practice_intro_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/start_practice_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/learn_practice_completion_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/speaking_partner_image.dart';
@ -13,7 +17,16 @@ import '../../widgets/small_app_bar.dart';
import 'learn_practice_viewmodel.dart';
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
LearnPracticeViewModel viewModelBuilder(BuildContext context) =>
@ -27,13 +40,13 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
) =>
_buildPracticeScreensWrapper(viewModel);
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) => PopScope(
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) =>
PopScope(
canPop: true,
onPopInvokedWithResult: (value, data) {
if (!value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
WidgetsBinding.instance
.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel));
@ -42,31 +55,44 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(LearnPracticeViewModel viewModel) => Stack(children: [
Widget _buildScaffoldStack(LearnPracticeViewModel viewModel) =>
Stack(children: [
_buildBody(viewModel),
//_buildLoginWithEmailState(viewModel),
//_buildLoginWithGoogleState(viewModel)
]);
Widget _buildBody(LearnPracticeViewModel viewModel) =>
IndexedStack(
index: viewModel.currentIndex, children: _buildScreens());
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildPracticeIntroScreen(),
_buildStartPracticeScreen(),
_buildListenSpeakerScreen()
_buildLearnPracticeIntroScreen(),
_buildStartLearnPracticeScreen(),
_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;
// 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
void goTo(int page) {
_currentIndex = page;
rebuildUi();
}
void goBack() {
if(_currentIndex == 0){
if (_currentIndex == 0) {
pop();
}else{
} else {
_currentIndex--;
rebuildUi();
}
}
// Navigation
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,13 +9,20 @@ import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/small_app_bar.dart';
import '../../../widgets/speaking_partner_image.dart';
class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
const PracticeIntroScreen({super.key});
class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
final String title;
final String subtitle;
final String buttonLabel;
const LearnPracticeIntroScreen(
{super.key,
required this.title,
required this.subtitle,
required this.buttonLabel});
@override
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildScaffoldWrapper(viewModel);
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
@ -39,6 +46,7 @@ class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
showBackButton: true,
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
@ -83,7 +91,9 @@ class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
_buildSubtitle()
];
Widget _buildImage() => const SpeakingPartnerImage(radius: 75,);
Widget _buildImage() => const SpeakingPartnerImage(
radius: 75,
);
Widget _buildPartnerName() => Text.rich(
TextSpan(text: 'Daniel', style: style14DG600, children: [
@ -95,13 +105,13 @@ class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
);
Widget _buildTitle() => Text(
'Let \'s practice what you just learnt!',
title,
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Ill ask you a few questions, and you can respond naturally.',
subtitle,
style: style14DG400,
textAlign: TextAlign.center,
);
@ -116,9 +126,9 @@ class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Start Practice',
text: buttonLabel,
foregroundColor: kcWhite,
onTap: ()=> viewModel.goTo(1),
onTap: () => viewModel.goTo(1),
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/small_app_bar.dart';
class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
const ListenSpeakerScreen({super.key});
class ListenLearnPracticeSpeakerScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const ListenLearnPracticeSpeakerScreen({super.key});
Future<void> _showSheet(
{required BuildContext context,
@ -86,6 +87,7 @@ class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
showBackButton: true,
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
@ -190,7 +192,7 @@ class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
required LearnPracticeViewModel viewModel}) =>
[
_buildReplyButtonWrapper(),
_buildMicButtonWrapper(),
_buildMicButtonWrapper(viewModel),
_buildCancelButtonWrapper(context: context, viewModel: viewModel)
];
@ -199,10 +201,11 @@ class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildMicButton(viewModel));
Widget _buildMicButton() => ElevatedButton(
onPressed: () {},
Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton(
onPressed: () => viewModel.goTo(3),
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
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 '../../../widgets/small_app_bar.dart';
class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
const StartPracticeScreen({super.key});
class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
const StartLearnPracticeScreen({super.key});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
@ -47,6 +47,7 @@ class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
showBackButton: true,
title: 'Practice Speaking',
);
@ -122,7 +123,7 @@ class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
[
_buildActionLabel(),
verticalSpaceMedium,
_buildButtonsRowWrapper(),
_buildButtonsRowWrapper(viewModel),
verticalSpaceMedium,
];
@ -132,15 +133,15 @@ class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
textAlign: TextAlign.center,
);
Widget _buildButtonsRowWrapper() => Row(
Widget _buildButtonsRowWrapper(LearnPracticeViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildButtonsRowChildren(),
children: _buildButtonsRowChildren(viewModel),
);
List<Widget> _buildButtonsRowChildren() => [
List<Widget> _buildButtonsRowChildren(LearnPracticeViewModel viewModel) => [
_buildReplyButtonWrapper(),
_buildMicButtonWrapper(),
_buildMicButtonWrapper(viewModel),
_buildEmptySpace()
];
@ -149,10 +150,11 @@ class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
Widget _buildMicButtonWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildMicButton(viewModel));
Widget _buildMicButton() => ElevatedButton(
onPressed: () {},
Widget _buildMicButton(LearnPracticeViewModel viewModel) => ElevatedButton(
onPressed: () => viewModel.goTo(2),
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),

View File

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

View File

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

View File

@ -21,61 +21,77 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
required this.otpController,
required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 325 - half,);
Widget getPadding(context) {
double half = screenHeight(context) / 2;
return SizedBox(
height: half + 325 - half,
);
}
@override
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(
{required BuildContext context, required LoginViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
Widget _buildScaffold(
{required BuildContext context, required LoginViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(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 _buildExpandedBody(
{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(
child: _buildBodyWrapper(context: context,viewModel: viewModel),
child: _buildBodyWrapper(context: context, viewModel: viewModel),
);
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
Widget _buildBodyWrapper(
{required BuildContext context, required LoginViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
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,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context,viewModel: viewModel),
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButton(viewModel)];
List<Widget> _buildBodyChildren(
{required BuildContext context, required LoginViewModel viewModel}) =>
[
_buildUpperColumn(viewModel),
getPadding(context),
_buildContinueButton(viewModel)
];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
@ -83,7 +99,6 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),

View File

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

View File

@ -18,63 +18,76 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
const LoginWithPhoneNumberScreen(
{super.key, required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 175 - half,);
Widget getPadding(context) {
double half = screenHeight(context) / 2;
return SizedBox(
height: half + 175 - half,
);
}
@override
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(
{required BuildContext context, required LoginViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
Widget _buildScaffold(
{required BuildContext context, required LoginViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(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 _buildExpandedBody(
{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(
child: _buildBodyWrapper(context: context,viewModel: viewModel),
child: _buildBodyWrapper(context: context, viewModel: viewModel),
);
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
Widget _buildBodyWrapper(
{required BuildContext context, required LoginViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
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,
children: _buildBodyChildren(context: context,viewModel: viewModel),
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)];
List<Widget> _buildBodyChildren(
{required BuildContext context, required LoginViewModel viewModel}) =>
[
_buildUpperColumn(viewModel),
getPadding(context),
_buildLowerColumn(viewModel)
];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
@ -82,7 +95,6 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),

View File

@ -481,5 +481,5 @@ class OnboardingViewModel extends FormViewModel {
await _navigationService.navigateToAssessmentView(data: _userData);
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,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildAgeGroups(viewModel)
];
@ -91,7 +91,7 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
style: style25DG600,
);
Widget _buildSubTitle() => Text(
Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your age.',
style: style14DG400,
);

View File

@ -76,7 +76,7 @@ class EducationalBackgroundFormScreen
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_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.',
style: TextStyle(color: kcMediumGrey),
);

View File

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

View File

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

View File

@ -42,7 +42,7 @@ class ProfileViewModel extends ReactiveViewModel {
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
await _authenticationService.saveProfilePicture(image);
}
}
@ -54,7 +54,7 @@ class ProfileViewModel extends ReactiveViewModel {
pop();
if (image != null) {
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(
title: 'Edit Profile',
onTap: viewModel.pop,
showBackButton: true,
title: 'Edit Profile',
);
Widget _buildColumnWrapper(

View File

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

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.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/suggestion_card.dart';
@ -57,6 +57,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
Widget _buildAppbar(ProgressViewModel viewModel) => SmallAppBar(
title: 'My Progress',
onTap: viewModel.pop,
showBackButton: true,
);
Widget _buildContentWrapper(ProgressViewModel viewModel) =>
@ -114,7 +115,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
required String subtitle,
required bool isCompleted,
required ProgressViewModel viewModel}) =>
CourseLevelCard(
CourseProgressCard(
icon: icon,
title: title,
color: color,

View File

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

View File

@ -23,7 +23,6 @@ class RegisterViewModel extends FormViewModel {
final _googleAuthService = locator<GoogleAuthService>();
final _authenticationService = locator<AuthenticationService>();
// Navigation
@ -284,17 +283,18 @@ class RegisterViewModel extends FormViewModel {
await _navigationService.replaceWithLoginView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
await _navigationService.clearStackAndShow(Routes.homeView);
// Remote api calls
// Register
Future<void> registerWithEmail() async =>
await runBusyFuture(_register(), busyObject: StateObjects.registerWithEmail);
Future<void> registerWithEmail() async => await runBusyFuture(_register(),
busyObject: StateObjects.registerWithEmail);
Future<void> _register() async {
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) {
goTo(page: 3);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,6 @@ class CancelLearnPracticeSheet extends StatelessWidget {
verticalSpaceLarge,
_buildContinueButton(),
_buildEndButton(),
];
Widget _buildImage() => const SpeakingPartnerImage(
@ -45,7 +44,7 @@ class CancelLearnPracticeSheet extends StatelessWidget {
);
Widget _buildMessage() => Text.rich(
TextSpan(text: 'Youre almost there,', style: style18DG600, children: [
TextSpan(text: 'Youre almost there,', style: style18DG700, children: [
TextSpan(
text: ' Johnny!',
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 'custom_elevated_button.dart';
class CourseLevelCard extends StatelessWidget {
class CourseProgressCard extends StatelessWidget {
final Color color;
final String icon;
final String title;
@ -15,7 +15,7 @@ class CourseLevelCard extends StatelessWidget {
final bool isCompleted;
final GestureTapCallback? onTap;
const CourseLevelCard({
const CourseProgressCard({
super.key,
this.onTap,
required this.icon,
@ -56,7 +56,7 @@ class CourseLevelCard extends StatelessWidget {
verticalSpaceSmall,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceSmall,
_buildActionButton()
];
@ -89,7 +89,7 @@ class CourseLevelCard extends StatelessWidget {
),
);
Widget _buildSubTitle() => Expanded(
Widget _buildSubtitle() => Expanded(
child: Text(
subtitle,
maxLines: 3,

View File

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

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