refactor(auth): Apply code refactor for login, register and forget password functionalities

This commit is contained in:
BisratHailu 2026-02-12 18:01:00 +03:00
parent 94c0576a87
commit 51fe5dca40
51 changed files with 2093 additions and 314 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -36,6 +36,8 @@ import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/services/google_auth_service.dart'; import 'package:yimaru_app/services/google_auth_service.dart';
import 'package:yimaru_app/services/image_downloader_service.dart'; import 'package:yimaru_app/services/image_downloader_service.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'; import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -65,6 +67,8 @@ import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
MaterialRoute(page: LearnLessonView), MaterialRoute(page: LearnLessonView),
MaterialRoute(page: FailureView), MaterialRoute(page: FailureView),
MaterialRoute(page: ForgetPasswordView), MaterialRoute(page: ForgetPasswordView),
MaterialRoute(page: LearnLessonDetailView),
MaterialRoute(page: LearnPracticeView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [

View File

@ -5,10 +5,10 @@
// ************************************************************************** // **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i27; import 'package:flutter/material.dart' as _i29;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart' as _i1; import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i28; import 'package:stacked_services/stacked_services.dart' as _i30;
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
as _i10; as _i10;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
@ -23,9 +23,13 @@ import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14;
import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19; import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19;
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart' import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'
as _i24; as _i24;
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart'
as _i27;
import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20; import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20;
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart' import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
as _i21; as _i21;
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'
as _i28;
import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18; import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18;
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3;
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart' import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart'
@ -96,6 +100,10 @@ class Routes {
static const forgetPasswordView = '/forget-password-view'; static const forgetPasswordView = '/forget-password-view';
static const learnLessonDetailView = '/learn-lesson-detail-view';
static const learnPracticeView = '/learn-practice-view';
static const all = <String>{ static const all = <String>{
homeView, homeView,
onboardingView, onboardingView,
@ -122,6 +130,8 @@ class Routes {
learnLessonView, learnLessonView,
failureView, failureView,
forgetPasswordView, forgetPasswordView,
learnLessonDetailView,
learnPracticeView,
}; };
} }
@ -227,17 +237,25 @@ class StackedRouter extends _i1.RouterBase {
Routes.forgetPasswordView, Routes.forgetPasswordView,
page: _i26.ForgetPasswordView, page: _i26.ForgetPasswordView,
), ),
_i1.RouteDef(
Routes.learnLessonDetailView,
page: _i27.LearnLessonDetailView,
),
_i1.RouteDef(
Routes.learnPracticeView,
page: _i28.LearnPracticeView,
),
]; ];
final _pagesMap = <Type, _i1.StackedRouteFactory>{ final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) { _i2.HomeView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(), builder: (context) => const _i2.HomeView(),
settings: data, settings: data,
); );
}, },
_i3.OnboardingView: (data) { _i3.OnboardingView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(), builder: (context) => const _i3.OnboardingView(),
settings: data, settings: data,
); );
@ -246,147 +264,159 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>( final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(), orElse: () => const StartupViewArguments(),
); );
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label), builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data, settings: data,
); );
}, },
_i5.ProfileView: (data) { _i5.ProfileView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(), builder: (context) => const _i5.ProfileView(),
settings: data, settings: data,
); );
}, },
_i6.ProfileDetailView: (data) { _i6.ProfileDetailView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(), builder: (context) => const _i6.ProfileDetailView(),
settings: data, settings: data,
); );
}, },
_i7.DownloadsView: (data) { _i7.DownloadsView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(), builder: (context) => const _i7.DownloadsView(),
settings: data, settings: data,
); );
}, },
_i8.ProgressView: (data) { _i8.ProgressView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(), builder: (context) => const _i8.ProgressView(),
settings: data, settings: data,
); );
}, },
_i9.OngoingProgressView: (data) { _i9.OngoingProgressView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(), builder: (context) => const _i9.OngoingProgressView(),
settings: data, settings: data,
); );
}, },
_i10.AccountPrivacyView: (data) { _i10.AccountPrivacyView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(), builder: (context) => const _i10.AccountPrivacyView(),
settings: data, settings: data,
); );
}, },
_i11.SupportView: (data) { _i11.SupportView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(), builder: (context) => const _i11.SupportView(),
settings: data, settings: data,
); );
}, },
_i12.TelegramSupportView: (data) { _i12.TelegramSupportView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(), builder: (context) => const _i12.TelegramSupportView(),
settings: data, settings: data,
); );
}, },
_i13.CallSupportView: (data) { _i13.CallSupportView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(), builder: (context) => const _i13.CallSupportView(),
settings: data, settings: data,
); );
}, },
_i14.LanguageView: (data) { _i14.LanguageView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(), builder: (context) => const _i14.LanguageView(),
settings: data, settings: data,
); );
}, },
_i15.PrivacyPolicyView: (data) { _i15.PrivacyPolicyView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(), builder: (context) => const _i15.PrivacyPolicyView(),
settings: data, settings: data,
); );
}, },
_i16.TermsAndConditionsView: (data) { _i16.TermsAndConditionsView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(), builder: (context) => const _i16.TermsAndConditionsView(),
settings: data, settings: data,
); );
}, },
_i17.RegisterView: (data) { _i17.RegisterView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(), builder: (context) => const _i17.RegisterView(),
settings: data, settings: data,
); );
}, },
_i18.LoginView: (data) { _i18.LoginView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(), builder: (context) => const _i18.LoginView(),
settings: data, settings: data,
); );
}, },
_i19.LearnView: (data) { _i19.LearnView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(), builder: (context) => const _i19.LearnView(),
settings: data, settings: data,
); );
}, },
_i20.LearnLevelView: (data) { _i20.LearnLevelView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(), builder: (context) => const _i20.LearnLevelView(),
settings: data, settings: data,
); );
}, },
_i21.LearnModuleView: (data) { _i21.LearnModuleView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(), builder: (context) => const _i21.LearnModuleView(),
settings: data, settings: data,
); );
}, },
_i22.WelcomeView: (data) { _i22.WelcomeView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i22.WelcomeView(), builder: (context) => const _i22.WelcomeView(),
settings: data, settings: data,
); );
}, },
_i23.AssessmentView: (data) { _i23.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false); final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i23.AssessmentView(key: args.key, data: args.data), _i23.AssessmentView(key: args.key, data: args.data),
settings: data, settings: data,
); );
}, },
_i24.LearnLessonView: (data) { _i24.LearnLessonView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.LearnLessonView(), builder: (context) => const _i24.LearnLessonView(),
settings: data, settings: data,
); );
}, },
_i25.FailureView: (data) { _i25.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false); final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i25.FailureView(key: args.key, label: args.label), _i25.FailureView(key: args.key, label: args.label),
settings: data, settings: data,
); );
}, },
_i26.ForgetPasswordView: (data) { _i26.ForgetPasswordView: (data) {
return _i27.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i26.ForgetPasswordView(), builder: (context) => const _i26.ForgetPasswordView(),
settings: data, settings: data,
); );
}, },
_i27.LearnLessonDetailView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i27.LearnLessonDetailView(),
settings: data,
);
},
_i28.LearnPracticeView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i28.LearnPracticeView(),
settings: data,
);
},
}; };
@override @override
@ -402,7 +432,7 @@ class StartupViewArguments {
this.label = 'Loading', this.label = 'Loading',
}); });
final _i27.Key? key; final _i29.Key? key;
final String label; final String label;
@ -429,7 +459,7 @@ class AssessmentViewArguments {
required this.data, required this.data,
}); });
final _i27.Key? key; final _i29.Key? key;
final Map<String, dynamic> data; final Map<String, dynamic> data;
@ -456,7 +486,7 @@ class FailureViewArguments {
required this.label, required this.label,
}); });
final _i27.Key? key; final _i29.Key? key;
final String label; final String label;
@ -477,7 +507,7 @@ class FailureViewArguments {
} }
} }
extension NavigatorStateExtension on _i28.NavigationService { extension NavigatorStateExtension on _i30.NavigationService {
Future<dynamic> navigateToHomeView([ Future<dynamic> navigateToHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -507,7 +537,7 @@ extension NavigatorStateExtension on _i28.NavigationService {
} }
Future<dynamic> navigateToStartupView({ Future<dynamic> navigateToStartupView({
_i27.Key? key, _i29.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -776,7 +806,7 @@ extension NavigatorStateExtension on _i28.NavigationService {
} }
Future<dynamic> navigateToAssessmentView({ Future<dynamic> navigateToAssessmentView({
_i27.Key? key, _i29.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -807,7 +837,7 @@ extension NavigatorStateExtension on _i28.NavigationService {
} }
Future<dynamic> navigateToFailureView({ Future<dynamic> navigateToFailureView({
_i27.Key? key, _i29.Key? key,
required String label, required String label,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -837,6 +867,34 @@ extension NavigatorStateExtension on _i28.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> navigateToLearnLessonDetailView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnLessonDetailView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnPracticeView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnPracticeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([ Future<dynamic> replaceWithHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -866,7 +924,7 @@ extension NavigatorStateExtension on _i28.NavigationService {
} }
Future<dynamic> replaceWithStartupView({ Future<dynamic> replaceWithStartupView({
_i27.Key? key, _i29.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1135,7 +1193,7 @@ extension NavigatorStateExtension on _i28.NavigationService {
} }
Future<dynamic> replaceWithAssessmentView({ Future<dynamic> replaceWithAssessmentView({
_i27.Key? key, _i29.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1166,7 +1224,7 @@ extension NavigatorStateExtension on _i28.NavigationService {
} }
Future<dynamic> replaceWithFailureView({ Future<dynamic> replaceWithFailureView({
_i27.Key? key, _i29.Key? key,
required String label, required String label,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1195,4 +1253,32 @@ extension NavigatorStateExtension on _i28.NavigationService {
parameters: parameters, parameters: parameters,
transition: transition); transition: transition);
} }
Future<dynamic> replaceWithLearnLessonDetailView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnLessonDetailView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnPracticeView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnPracticeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
} }

View File

@ -12,8 +12,12 @@ class UserModel {
final String? country; final String? country;
final String? occupation; final String? occupation;
final bool? userInfoLoaded;
@JsonKey(name: 'user_id') @JsonKey(name: 'user_id')
final int? userId; final int? userId;
@ -51,6 +55,7 @@ class UserModel {
this.accessToken, this.accessToken,
this.refreshToken, this.refreshToken,
this.profilePicture, this.profilePicture,
this.userInfoLoaded ,
this.profileCompleted, this.profileCompleted,
}); });

View File

@ -20,6 +20,7 @@ UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
refreshToken: json['refresh_token'] as String?, refreshToken: json['refresh_token'] as String?,
profilePicture: json['profile_picture_url'] as String?, profilePicture: json['profile_picture_url'] as String?,
profileCompleted: json['profile_completed'] as bool?, profileCompleted: json['profile_completed'] as bool?,
userInfoLoaded: json['userInfoLoaded'] as bool? ?? false,
); );
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{ Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
@ -28,6 +29,7 @@ Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'region': instance.region, 'region': instance.region,
'country': instance.country, 'country': instance.country,
'occupation': instance.occupation, 'occupation': instance.occupation,
'userInfoLoaded': instance.userInfoLoaded,
'user_id': instance.userId, 'user_id': instance.userId,
'last_name': instance.lastName, 'last_name': instance.lastName,
'birth_day': instance.birthday, 'birth_day': instance.birthday,

View File

@ -11,7 +11,7 @@ class ApiService {
final _service = locator<DioService>(); final _service = locator<DioService>();
// Register // Register
Future<Map<String, dynamic>> register(Map<String, dynamic> data) async { Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kRegisterUrl', '$kBaseUrl/$kUserUrl/$kRegisterUrl',
@ -66,10 +66,10 @@ class ApiService {
} }
// Google login // Google login
Future<Map<String, dynamic>> googleLogin(Map<String, dynamic> data) async { Future<Map<String, dynamic>> googleAuth(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$kBaseUrl/$kGoogleLoginUrl', '$kBaseUrl/$kGoogleAuthUrl',
data: data, data: data,
); );

View File

@ -37,24 +37,6 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('refreshToken', refresh); await _secureService.setString('refreshToken', refresh);
} }
Future<void> saveUserName(Map<String, dynamic> data) async {
await _secureService.setString('firstName', data['firstName']);
_user = UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
userId: _user?.userId,
country: _user?.country,
lastName: _user?.lastName,
birthday: _user?.birthday,
occupation: _user?.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profilePicture: _user?.profilePicture,
profileCompleted: _user?.profileCompleted,
firstName: await _secureService.getString('firstName'),
);
}
Future<void> saveUserCredential(Map<String, dynamic> data) async { Future<void> saveUserCredential(Map<String, dynamic> data) async {
await _secureService.setInt('userId', data['userId']); await _secureService.setInt('userId', data['userId']);
@ -84,6 +66,7 @@ class AuthenticationService with ListenableServiceMixin {
accessToken: _user?.accessToken, accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken, refreshToken: _user?.refreshToken,
profilePicture: _user?.profilePicture, profilePicture: _user?.profilePicture,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profileCompleted: await _secureService.getBool('profileCompleted')); profileCompleted: await _secureService.getBool('profileCompleted'));
notifyListeners(); notifyListeners();
} }
@ -103,6 +86,7 @@ class AuthenticationService with ListenableServiceMixin {
accessToken: _user?.accessToken, accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken, refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted, profileCompleted: _user?.profileCompleted,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profileImage'), profilePicture: await _secureService.getString('profileImage'),
); );
@ -111,6 +95,7 @@ class AuthenticationService with ListenableServiceMixin {
Future<void> saveUserData( Future<void> saveUserData(
{required String image, required UserModel data}) async { {required String image, required UserModel data}) async {
await _secureService.setBool('userInfoLoaded', true);
await _secureService.setBool( await _secureService.setBool(
'profileCompleted', data.profileCompleted ?? false); 'profileCompleted', data.profileCompleted ?? false);
await _secureService.setString('profilePicture', image); await _secureService.setString('profilePicture', image);
@ -127,6 +112,7 @@ class AuthenticationService with ListenableServiceMixin {
email: data.email, email: data.email,
gender: data.gender, gender: data.gender,
region: data.region, region: data.region,
userInfoLoaded: true,
profilePicture: image, profilePicture: image,
userId: _user?.userId, userId: _user?.userId,
country: data.country, country: data.country,
@ -190,6 +176,7 @@ class AuthenticationService with ListenableServiceMixin {
accessToken: await _secureService.getString('accessToken'), accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'), refreshToken: await _secureService.getString('refreshToken'),
profilePicture: await _secureService.getString('profileImage'), profilePicture: await _secureService.getString('profileImage'),
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
profileCompleted: await _secureService.getBool('profileCompleted'), profileCompleted: await _secureService.getBool('profileCompleted'),
); );
return _user; return _user;

View File

@ -4,7 +4,7 @@ import 'package:yimaru_app/ui/common/app_constants.dart';
class GoogleAuthService { class GoogleAuthService {
final GoogleSignIn signIn = GoogleSignIn.instance; final GoogleSignIn signIn = GoogleSignIn.instance;
Future<GoogleSignInAccount?> googleSignIn() async { Future<GoogleSignInAccount?> googleAuth() async {
try { try {
GoogleSignInAccount? googleUser; GoogleSignInAccount? googleUser;
await signIn.initialize(serverClientId: kServerClientId).then((_) async { await signIn.initialize(serverClientId: kServerClientId).then((_) async {

View File

@ -5,6 +5,7 @@ const Color kcRed = Color(0xffFF4C4C);
const Color kcGreen = Color(0xFF1DE964); const Color kcGreen = Color(0xFF1DE964);
const Color kcBackgroundColor = kcWhite; const Color kcBackgroundColor = kcWhite;
const Color kcWhite = Color(0xFFFFFFFF); const Color kcWhite = Color(0xFFFFFFFF);
const Color kcViolet = Color(0x336A1B9A);
const Color kcIndigo = Color(0xff6A1B9A); const Color kcIndigo = Color(0xff6A1B9A);
const Color kcOrange = Color(0xFFF79400); const Color kcOrange = Color(0xFFF79400);
const Color kcSkyBlue = Color(0xFF28B4CD); const Color kcSkyBlue = Color(0xFF28B4CD);
@ -13,7 +14,6 @@ const Color kcMediumGrey = Color(0xFF474A54);
const Color kcAquamarine = Color(0xFF1DE9B6); const Color kcAquamarine = Color(0xFF1DE9B6);
const Color kcTransparent = Colors.transparent; const Color kcTransparent = Colors.transparent;
const Color kcPrimaryColor = Color(0xFF9E2891); const Color kcPrimaryColor = Color(0xFF9E2891);
const Color kcPrimaryAccent = Color(0xFF6A1B9A);
const Color kcVeryLightGrey = Color(0xFFE3E3E3); const Color kcVeryLightGrey = Color(0xFFE3E3E3);
const Color kcPrimaryColorDark = Color(0xFF300151); const Color kcPrimaryColorDark = Color(0xFF300151);
const Color kcPrimaryColorLight = Color(0x149E2891); const Color kcPrimaryColorLight = Color(0x149E2891);

View File

@ -23,9 +23,12 @@ String kLoginUrl = 'api/v1/auth/customer-login';
String kProfileStatusUrl = 'is-profile-completed'; String kProfileStatusUrl = 'is-profile-completed';
String kGoogleLoginUrl = 'api/v1/auth/google/android'; String kGoogleAuthUrl = 'api/v1/auth/google/android';
String kAssessmentsUrl = 'api/v1/assessment/questions'; String kAssessmentsUrl = 'api/v1/assessment/questions';
String kServerClientId = String kServerClientId =
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com'; '574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
String kSampleVideoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';

View File

@ -14,11 +14,13 @@ enum StateObjects {
verifyOtp, verifyOtp,
resendOtp, resendOtp,
profileImage, profileImage,
registration,
profileUpdate, profileUpdate,
resetPassword, resetPassword,
loginWithEmail, loginWithEmail,
loginWithGoogle, loginWithGoogle,
loadLessonVideo,
requestResetCode, requestResetCode,
registerWithEmail,
profileCompletion, profileCompletion,
registerWithGoogle,
} }

View File

@ -1,4 +1,5 @@
import 'dart:math'; import 'dart:math';
import 'package:chewie/chewie.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -183,6 +184,13 @@ TextStyle style18W600 = const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style25W600 = const TextStyle(
fontSize: 25,
color: kcWhite,
fontWeight: FontWeight.w600,
);
TextStyle style12R700 = const TextStyle( TextStyle style12R700 = const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.red, color: Colors.red,
@ -198,7 +206,7 @@ TextStyle style14P600 = const TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style25K600 = const TextStyle( TextStyle style25P600 = const TextStyle(
fontSize: 25, fontSize: 25,
color: kcPrimaryColor, color: kcPrimaryColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -279,6 +287,12 @@ Map<String, Style> htmlStyle = {
), ),
}; };
ChewieProgressColors buildChewieProgressIndicator = ChewieProgressColors(
bufferedColor: kcIndigo,
playedColor: kcPrimaryColor,
backgroundColor: kcBackgroundColor,
);
Widget buildToastDescription(String message) => Text( Widget buildToastDescription(String message) => Text(
message, message,
maxLines: 4, maxLines: 4,

View File

@ -29,10 +29,7 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
confirmPasswordController.clear(); confirmPasswordController.clear();
} }
void _inAppPop(ForgetPasswordViewModel viewModel) {
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) { void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
if (viewModel.currentPage == 0) { if (viewModel.currentPage == 0) {
@ -78,41 +75,11 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
canPop: true, canPop: true,
onPopInvokedWithResult: (value, data) => onPopInvokedWithResult: (value, data) =>
_pop(value: value, viewModel: viewModel), _pop(value: value, viewModel: viewModel),
child: _buildScaffoldWrapper(viewModel)); child: _buildBody(viewModel));
Widget _buildScaffoldWrapper(ForgetPasswordViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(ForgetPasswordViewModel viewModel) =>
Stack(children: [
_buildScaffold(viewModel),
_buildRequestResetCodeState(viewModel),
_buildResetPasswordState(viewModel)
]);
Widget _buildScaffold(ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(ForgetPasswordViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
Widget _buildExpandedBody(ForgetPasswordViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(ForgetPasswordViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(ForgetPasswordViewModel viewModel) => Widget _buildBody(ForgetPasswordViewModel viewModel) =>
IndexedStack(index: viewModel.currentPage, children: _buildScreens()); IndexedStack(index: viewModel.currentPage, children: _buildScreens());
@ -130,13 +97,5 @@ class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
resetCodeController: resetCodeController, resetCodeController: resetCodeController,
confirmPasswordController: confirmPasswordController); confirmPasswordController: confirmPasswordController);
Widget _buildRequestResetCodeState(ForgetPasswordViewModel viewModel) =>
viewModel.busy(StateObjects.requestResetCode)
? const PageLoadingIndicator()
: Container();
Widget _buildResetPasswordState(ForgetPasswordViewModel viewModel) =>
viewModel.busy(StateObjects.resetPassword)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -7,7 +7,9 @@ import 'package:yimaru_app/ui/widgets/login_account.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/option_text_divider.dart'; import '../../../widgets/option_text_divider.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../forget_password_view.form.dart'; import '../forget_password_view.form.dart';
class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> { class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
@ -18,6 +20,23 @@ class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
required this.emailController, required this.emailController,
}); });
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 375 - half,);
}
void _inAppPop(ForgetPasswordViewModel viewModel) {
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
emailController.clear();
viewModel.resetRequestResetCodeScreen();
}
Future<void> _addUserData(ForgetPasswordViewModel viewModel) async { Future<void> _addUserData(ForgetPasswordViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -29,24 +48,70 @@ class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
await viewModel.requestResetCode(); await viewModel.requestResetCode();
} }
@override @override
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) => Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
_buildBody(viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildBody(ForgetPasswordViewModel viewModel) => Column( Widget _buildScaffoldWrapper( {required BuildContext context,
crossAxisAlignment: CrossAxisAlignment.start, required ForgetPasswordViewModel viewModel}) => Scaffold(
mainAxisAlignment: MainAxisAlignment.spaceBetween, backgroundColor: kcBackgroundColor,
children: _buildBodyChildren(viewModel), body: _buildScaffoldStack(context: context, viewModel: viewModel),
); );
List<Widget> _buildBodyChildren(ForgetPasswordViewModel viewModel) => Widget _buildScaffoldStack( {required BuildContext context,
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; required ForgetPasswordViewModel viewModel}) => Stack(
children: [
_buildScaffold(context: context,viewModel: viewModel),
_buildRequestResetCodeState(viewModel),
],
);
Widget _buildColumnScroller(ForgetPasswordViewModel viewModel) => Widget _buildScaffold( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context, viewModel: viewModel)];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
Widget _buildExpandedBody( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildUpperColumn(viewModel), child: _buildBodyWrapper(context: context, viewModel: viewModel),
); );
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(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(ForgetPasswordViewModel viewModel) => Column( Widget _buildUpperColumn(ForgetPasswordViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -117,4 +182,9 @@ class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1), : kcPrimaryColor.withOpacity(0.1),
); );
Widget _buildRequestResetCodeState(ForgetPasswordViewModel viewModel) =>
viewModel.busy(StateObjects.requestResetCode)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/custom_form_label.dart'; import '../../../widgets/custom_form_label.dart';
import '../../../widgets/custom_linear_progress_indicator.dart'; import '../../../widgets/custom_linear_progress_indicator.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/obscure_password.dart'; import '../../../widgets/obscure_password.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../../../widgets/validator_list_tile.dart'; import '../../../widgets/validator_list_tile.dart';
import '../forget_password_viewmodel.dart'; import '../forget_password_viewmodel.dart';
import '../forget_password_view.form.dart'; import '../forget_password_view.form.dart';
@ -22,6 +25,20 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
required this.passwordController, required this.passwordController,
required this.confirmPasswordController}); required this.confirmPasswordController});
void _inAppPop(ForgetPasswordViewModel viewModel) {
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
passwordController.clear();
resetCodeController.clear();
confirmPasswordController.clear();
viewModel.resetResetPasswordScreen();
}
Future<void> _reset(ForgetPasswordViewModel viewModel) async { Future<void> _reset(ForgetPasswordViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -36,18 +53,58 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
@override @override
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) => Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
_buildBodyChildren(viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildBodyChildren(ForgetPasswordViewModel viewModel) => 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(
children: [
_buildScaffold(viewModel),
_buildResetPasswordState(viewModel),
],
);
Widget _buildScaffold( ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren( ForgetPasswordViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
Widget _buildExpandedBody( ForgetPasswordViewModel viewModel) =>
Expanded(child: _buildColumnScroller(viewModel));
Widget _buildColumnScroller( ForgetPasswordViewModel viewModel) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildBodyColumn(viewModel), child: _buildBodyWrapper(viewModel),
); );
Widget _buildBodyColumn(ForgetPasswordViewModel viewModel) => Column( Widget _buildBodyWrapper( ForgetPasswordViewModel viewModel) => Padding(
mainAxisSize: MainAxisSize.min, padding: const EdgeInsets.symmetric(horizontal: 15),
crossAxisAlignment: CrossAxisAlignment.start, child: _buildBody(viewModel),
children: _buildBodyColumnChildren(viewModel), );
);
Widget _buildBody(ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(ForgetPasswordViewModel viewModel) => [ List<Widget> _buildBodyColumnChildren(ForgetPasswordViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,
@ -241,4 +298,9 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1), : kcPrimaryColor.withOpacity(0.1),
); );
Widget _buildResetPasswordState(ForgetPasswordViewModel viewModel) =>
viewModel.busy(StateObjects.resetPassword)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -76,21 +76,27 @@ class HomeViewModel extends ReactiveViewModel {
Future<void> getProfileData() async => await runBusyFuture(_getProfileData()); Future<void> getProfileData() async => await runBusyFuture(_getProfileData());
Future<void> _getProfileData() async { Future<void> _getProfileData() async {
if (await _statusChecker.checkConnection()) { print('RESPONSE FOR USER DATA ${_user?.firstName}');
Map<String, dynamic> response = {};
if (_user?.profileCompleted != null && if (!(_user?.userInfoLoaded ?? false)) {
(_user?.profileCompleted ?? false)) { print('RESPONSE FOR USER DATA 1');
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileData(_user?.userId); Map<String, dynamic> response = {};
if (response['status'] == ResponseStatus.success) { if (_user?.profileCompleted != null &&
UserModel user = response['data'] as UserModel; (_user?.profileCompleted ?? false)) {
if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileData(_user?.userId);
String image = if (response['status'] == ResponseStatus.success) {
await _imageDownloaderService.downloader(user.profilePicture); UserModel user = response['data'] as UserModel;
await _authenticationService.saveUserData(image: image, data: user); String image =
await _imageDownloaderService.downloader(user.profilePicture);
await _authenticationService.saveUserData(
image: image, data: user);
}
} }
} }
} }
@ -117,7 +123,6 @@ class HomeViewModel extends ReactiveViewModel {
response = {'data': true, 'status': ResponseStatus.success}; response = {'data': true, 'status': ResponseStatus.success};
} }
if (response['status'] == ResponseStatus.success && !response['data']) { if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding(); await replaceWithOnboarding();
} else if (response['status'] == ResponseStatus.success && } else if (response['status'] == ResponseStatus.success &&

View File

@ -117,19 +117,24 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
itemCount: viewModel.lessons.length, itemCount: viewModel.lessons.length,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile( itemBuilder: (context, index) => _buildTile(
title: viewModel.lessons[index]['title'], title: viewModel.lessons[index]['title'],
status: viewModel.lessons[index]['status'], status: viewModel.lessons[index]['status'],
thumbnail: viewModel.lessons[index]['thumbnail']), thumbnail: viewModel.lessons[index]['thumbnail'],
onLessonTap: () async =>
await viewModel.navigateToLearnLessonDetail(),
),
); );
Widget _buildTile({ Widget _buildTile({
required String title, required String title,
required String thumbnail, required String thumbnail,
GestureTapCallback? onLessonTap,
required ProgressStatuses status, required ProgressStatuses status,
}) => }) =>
LearnLessonTile( LearnLessonTile(
title: title, title: title,
status: status, status: status,
thumbnail: thumbnail, thumbnail: thumbnail,
onLessonTap: onLessonTap,
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
@ -8,7 +9,6 @@ class LearnLessonViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Lessons // Lessons
// Downloads
final List<Map<String, dynamic>> _lessons = [ final List<Map<String, dynamic>> _lessons = [
{ {
'title': '1.1 Introducing Yourself', 'title': '1.1 Introducing Yourself',
@ -31,4 +31,7 @@ class LearnLessonViewModel extends BaseViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnLessonDetail() async =>
await _navigationService.navigateToLearnLessonDetailView();
} }

View File

@ -0,0 +1,178 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/empty_video_player.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_lesson_detail_viewmodel.dart';
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
const LearnLessonDetailView({Key? key}) : super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel)async{
await viewModel.pause();
await viewModel.navigateToLearnPractice();
}
// @override
// void onDispose(LearnLessonDetailViewModel viewModel) {
// print('DISPOSED');
// viewModel.dispose();
// super.onDispose(viewModel);
// }
@override
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
await viewModel.initializePlayer();
super.onViewModelReady(viewModel);
}
@override
LearnLessonDetailViewModel viewModelBuilder(BuildContext context) =>
LearnLessonDetailViewModel();
@override
Widget builder(
BuildContext context,
LearnLessonDetailViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnLessonDetailViewModel viewModel) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnLessonDetailViewModel viewModel) =>
SafeArea(child: _buildColumn(viewModel));
Widget _buildColumn(LearnLessonDetailViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
_buildBodyColumnWrapper(viewModel),
],
);
Widget _buildAppBarWrapper(LearnLessonDetailViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppBar(viewModel));
Widget _buildAppBar(LearnLessonDetailViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
);
Widget _buildBodyColumnWrapper(LearnLessonDetailViewModel viewModel) =>
Expanded(
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(LearnLessonDetailViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnLessonDetailViewModel viewModel) =>
[
_buildLevelsColumnWrapper(viewModel),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildLevelsColumnWrapper(LearnLessonDetailViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(LearnLessonDetailViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(LearnLessonDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(
LearnLessonDetailViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitleWrapper(),
verticalSpaceLarge,
_buildVideoPlayerWrapper(viewModel),
verticalSpaceMedium,
_buildDescriptionWrapper(),
];
Widget _buildTitleWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildTitle(),
);
Widget _buildTitle() => Text(
'1.3 Common Greetings',
style: style16DG600,
);
Widget _buildVideoPlayerWrapper(LearnLessonDetailViewModel viewModel) =>
Container(
height: 200,
color: kcBlack,
width: double.maxFinite,
child: _buildVideoPlayerState(viewModel),
);
Widget _buildVideoPlayerState(LearnLessonDetailViewModel viewModel) =>
viewModel.chewieController != null &&
viewModel.videoPlayerController != null &&
!viewModel.busy(StateObjects.loadLessonVideo)
? _buildVideoPlayer(viewModel)
: _buildEmptyVideoPlayer();
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
_buildChewiePlayer(viewModel);
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
Chewie(controller: viewModel.chewieController!);
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
Widget _buildDescriptionWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildDescription(),
);
Widget _buildDescription() => Text(
'In this lesson, youll explore how to start simple conversations by greeting others in polite and friendly ways. Youll practice different greetings for morning, afternoon, and evening, as well as casual and formal situations. By the end, youll know how to confidently say hello, ask how someone is, and respond naturally.',
style: style14DG600,
);
Widget _buildContinueButtonWrapper(LearnLessonDetailViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(
left: 15,
right: 15,
bottom: 50,
),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LearnLessonDetailViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Practice',
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: ()async => await _navigate(viewModel),
);
}

View File

@ -0,0 +1,72 @@
import 'package:chewie/chewie.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:video_player/video_player.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../../../app/app.locator.dart';
import '../../../services/status_checker_service.dart';
class LearnLessonDetailViewModel extends BaseViewModel {
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Video player config
ChewieController? _chewieController;
ChewieController? get chewieController => _chewieController;
VideoPlayerController? _videoPlayerController;
VideoPlayerController? get videoPlayerController => _videoPlayerController;
// Video player
Future<void> initializePlayer() async =>
await runBusyFuture(_initializePlayer(),
busyObject: StateObjects.loadLessonVideo);
Future<void> _initializePlayer() async {
_videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl));
await _videoPlayerController?.initialize();
if (_videoPlayerController != null) {
print('Initialized');
_chewieController = ChewieController(
looping: true,
autoPlay: true,
showOptions: true,
showControls: true,
aspectRatio: 16 / 9,
autoInitialize: true,
allowedScreenSleep: false,
videoPlayerController: _videoPlayerController!,
materialProgressColors: buildChewieProgressIndicator);
}
// rebuildUi();
}
Future<void> pause()async{
await _chewieController?.pause();
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnPractice() async=>await _navigationService.navigateToLearnPracticeView();
}

View File

@ -0,0 +1,72 @@
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/widgets/profile_image.dart';
import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_practice_viewmodel.dart';
class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
const LearnPracticeView({Key? key}) : super(key: key);
@override
LearnPracticeViewModel viewModelBuilder(BuildContext context) =>
LearnPracticeViewModel();
@override
Widget builder(
BuildContext context,
LearnPracticeViewModel viewModel,
Widget? child,
) =>
_buildPracticeScreensWrapper(viewModel);
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) => PopScope(
canPop: true,
onPopInvokedWithResult: (value, data) {
if (!value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(LearnPracticeViewModel viewModel) => Stack(children: [
_buildBody(viewModel),
//_buildLoginWithEmailState(viewModel),
//_buildLoginWithGoogleState(viewModel)
]);
Widget _buildBody(LearnPracticeViewModel viewModel) =>
IndexedStack(
index: viewModel.currentIndex, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildPracticeIntroScreen(),
_buildStartPracticeScreen(),
_buildListenSpeakerScreen()
];
Widget _buildPracticeIntroScreen() => const PracticeIntroScreen();
Widget _buildStartPracticeScreen() => const StartPracticeScreen();
Widget _buildListenSpeakerScreen() => const ListenSpeakerScreen();
}

View File

@ -0,0 +1,34 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class LearnPracticeViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// In-app navigation
int _currentIndex = 0;
int get currentIndex => _currentIndex;
// In-app navigation
void goTo(int page) {
_currentIndex = page;
rebuildUi();
}
void goBack() {
if(_currentIndex == 0){
pop();
}else{
_currentIndex--;
rebuildUi();
}
}
// Navigation
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,254 @@
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 ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
const ListenSpeakerScreen({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,
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(
'Daniel is speaking...',
style: style14P400,
textAlign: TextAlign.center,
);
Widget _buildSpeakingIndicator() => Container(
height: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
radius: 0.7,
stops: const [
0.2,
0.25,
0.45,
0.75,
1,
],
center: Alignment.center,
colors: [
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.15),
kcPrimaryColor.withOpacity(0.1),
kcPrimaryColor.withOpacity(0.05),
],
// quarterly spread
),
),
child: _buildSpinner(),
);
Widget _buildSpinner() => const SpinKitWave(
size: 20,
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}) =>
[
_buildReplyButtonWrapper(),
_buildMicButtonWrapper(),
_buildCancelButtonWrapper(context: context, viewModel: viewModel)
];
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
Widget _buildMicButton() => ElevatedButton(
onPressed: () {},
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
child: _buildMicIcon(),
);
Widget _buildMicIcon() => const Icon(
Icons.mic,
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

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
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});
@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: _buildColumnWrapper(viewModel));
Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildBodyColumnWrapper(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded(
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
_buildPracticeColumnWrapper(viewModel),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildPracticeColumnWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildPracticeColumnScrollView(viewModel));
Widget _buildPracticeColumnScrollView(LearnPracticeViewModel viewModel) =>
SingleChildScrollView(
child: _buildPracticeColumn(viewModel),
);
Widget _buildPracticeColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildPracticeColumnChildren(viewModel),
);
List<Widget> _buildPracticeColumnChildren(LearnPracticeViewModel viewModel) =>
[
verticalSpaceMassive,
_buildImage(),
verticalSpaceMedium,
_buildPartnerName(),
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubtitle()
];
Widget _buildImage() => const SpeakingPartnerImage(radius: 75,);
Widget _buildPartnerName() => Text.rich(
TextSpan(text: 'Daniel', style: style14DG600, children: [
TextSpan(
text: ' - Your Speaking Partner',
style: style14MG400,
)
]),
);
Widget _buildTitle() => Text(
'Let \'s practice what you just learnt!',
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Ill ask you a few questions, and you can respond naturally.',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Start Practice',
foregroundColor: kcWhite,
onTap: ()=> viewModel.goTo(1),
backgroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_column_button.dart';
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});
@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(LearnPracticeViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
_buildAppBarWrapper(viewModel),
_buildStartButtonWrapper(viewModel),
_buildLowerButtonsSectionWrapper(viewModel)
];
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildStartButtonWrapper(LearnPracticeViewModel viewModel) => Expanded(
child: _buildStartButtonContainer(viewModel),
);
Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) =>
GestureDetector(
onTap: () => viewModel.goTo(2),
child: _buildStartButton(),
);
Widget _buildStartButton() => Container(
width: 150,
height: 150,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: SweepGradient(
stops: const [
0.0,
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.8,
0.9,
1,
],
endAngle: 8,
startAngle: 0.0,
center: Alignment.center,
colors: [
kcPrimaryColor.withOpacity(0.3),
kcIndigo.withOpacity(0.2),
kcIndigo.withOpacity(0.3),
kcIndigo.withOpacity(0.4),
kcIndigo.withOpacity(0.5),
kcPrimaryColor.withOpacity(0.5),
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.3),
kcPrimaryColor.withOpacity(0.2),
kcPrimaryColor.withOpacity(0.5),
],
// quarterly spread
),
),
child: _buildStartText(),
);
Widget _buildStartText() => Text(
'Start',
style: style25W600,
);
Widget _buildLowerButtonsSectionWrapper(LearnPracticeViewModel viewMode) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: _buildLowerButtonsSection(viewMode),
);
Widget _buildLowerButtonsSection(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
LearnPracticeViewModel viewModel) =>
[
_buildActionLabel(),
verticalSpaceMedium,
_buildButtonsRowWrapper(),
verticalSpaceMedium,
];
Widget _buildActionLabel() => Text(
'Tap the microphone to speak',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildButtonsRowWrapper() => Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildButtonsRowChildren(),
);
List<Widget> _buildButtonsRowChildren() => [
_buildReplyButtonWrapper(),
_buildMicButtonWrapper(),
_buildEmptySpace()
];
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
Widget _buildMicButton() => ElevatedButton(
onPressed: () {},
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
child: _buildMicIcon(),
);
Widget _buildMicIcon() => const Icon(
Icons.mic,
size: 35,
color: kcWhite,
);
Widget _buildEmptySpace() => Expanded(child: Container());
}

View File

@ -54,38 +54,11 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
if (!value) return; if (!value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
}, },
child: _buildScaffoldWrapper(viewModel)); child: _buildBody(viewModel));
Widget _buildScaffoldWrapper(LoginViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(LoginViewModel viewModel) => Stack(children: [
_buildScaffold(viewModel),
_buildLoginWithEmailState(viewModel),
_buildLoginWithGoogleState(viewModel)
]);
Widget _buildScaffold(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(LoginViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(LoginViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(LoginViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(LoginViewModel viewModel) => Widget _buildBody(LoginViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
@ -106,13 +79,4 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
otpController: otpController, otpController: otpController,
phoneNumberController: phoneNumberController); phoneNumberController: phoneNumberController);
Widget _buildLoginWithEmailState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithEmail)
? const PageLoadingIndicator()
: Container();
Widget _buildLoginWithGoogleState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithGoogle)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -26,7 +26,7 @@ class LoginViewModel extends FormViewModel {
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
// Navigation // In-app navigation
int _currentIndex = 0; int _currentIndex = 0;
int get currentIndex => _currentIndex; int get currentIndex => _currentIndex;
@ -171,18 +171,18 @@ class LoginViewModel extends FormViewModel {
} }
} }
Future<void> googleLogin() async => await runBusyFuture(_googleLogin(), Future<void> signInWithGoogle() async => await runBusyFuture(_signInWithGoogle(),
busyObject: StateObjects.loginWithGoogle); busyObject: StateObjects.loginWithGoogle);
Future<void> _googleLogin() async { Future<void> _signInWithGoogle() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
GoogleSignInAccount? googleUser = await _googleAuthService.googleSignIn(); GoogleSignInAccount? googleUser = await _googleAuthService.googleAuth();
Map<String, dynamic> data = { Map<String, dynamic> data = {
'id_token': googleUser?.authentication.idToken ?? '', 'id_token': googleUser?.authentication.idToken ?? '',
}; };
Map<String, dynamic> response = await _apiService.googleLogin(data); Map<String, dynamic> response = await _apiService.googleAuth(data);
if (response['status'] == ResponseStatus.success) { if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel; UserModel user = response['data'] as UserModel;

View File

@ -8,6 +8,7 @@ import 'package:yimaru_app/ui/widgets/custom_cursor.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../login_viewmodel.dart'; import '../login_viewmodel.dart';
import '../login_view.form.dart'; import '../login_view.form.dart';
@ -20,29 +21,68 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
required this.otpController, required this.otpController,
required this.phoneNumberController}); required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 325 - half,);
}
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel); _buildScaffoldWrapper(context: context,viewModel: viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column( Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
crossAxisAlignment: CrossAxisAlignment.start, backgroundColor: kcBackgroundColor,
mainAxisAlignment: MainAxisAlignment.spaceBetween, body: _buildScaffold(context: context,viewModel: viewModel),
children: _buildBodyChildren(viewModel), );
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(LoginViewModel viewModel) =>
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildUpperColumn(viewModel), child: _buildBodyWrapper(context: context,viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButton(viewModel)];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel), children: _buildUpperColumnChildren(viewModel),
); );
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,

View File

@ -4,9 +4,12 @@ import 'package:yimaru_app/ui/views/login/login_view.form.dart';
import 'package:yimaru_app/ui/widgets/obscure_password.dart'; import 'package:yimaru_app/ui/widgets/obscure_password.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/option_text_divider.dart'; import '../../../widgets/option_text_divider.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../../../widgets/register_for_account.dart'; import '../../../widgets/register_for_account.dart';
import '../login_viewmodel.dart'; import '../login_viewmodel.dart';
@ -19,6 +22,12 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
required this.emailController, required this.emailController,
required this.passwordController}); required this.passwordController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 25 - half,);
}
Future<void> _login(LoginViewModel viewModel) async { Future<void> _login(LoginViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -33,22 +42,58 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel); _buildScaffoldWrapper(context: context,viewModel: viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column( Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
crossAxisAlignment: CrossAxisAlignment.start, backgroundColor: kcBackgroundColor,
mainAxisAlignment: MainAxisAlignment.spaceBetween, body: _buildScaffoldStack(context: context,viewModel: viewModel),
children: _buildBodyChildren(viewModel), );
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) => Widget _buildScaffoldStack({required BuildContext context,required LoginViewModel viewModel}) => Stack(children: [
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; _buildScaffold(context: context,viewModel: viewModel),
_buildLoginWithEmailState(viewModel),
_buildLoginWithGoogleState(viewModel)
]);
Widget _buildColumnScroller(LoginViewModel viewModel) => Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildUpperColumn(viewModel), child: _buildBodyWrapper(context: context,viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -184,7 +229,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
leadingImage: 'assets/icons/google.png', leadingImage: 'assets/icons/google.png',
onTap: () async => await viewModel.googleLogin(), onTap: () async => await viewModel.signInWithGoogle(),
); );
Widget _buildOptionTextDivider() => const OptionTextDivider(); Widget _buildOptionTextDivider() => const OptionTextDivider();
@ -200,4 +245,15 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
text: 'Login with Phone Number', text: 'Login with Phone Number',
); );
Widget _buildLoginWithEmailState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithEmail)
? const PageLoadingIndicator()
: Container();
Widget _buildLoginWithGoogleState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithGoogle)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -8,6 +8,7 @@ import 'package:yimaru_app/ui/widgets/register_for_account.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/phone_number_prefix.dart'; import '../../../widgets/phone_number_prefix.dart';
import '../login_view.form.dart'; import '../login_view.form.dart';
@ -17,29 +18,70 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
const LoginWithPhoneNumberScreen( const LoginWithPhoneNumberScreen(
{super.key, required this.phoneNumberController}); {super.key, required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 175 - half,);
}
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column( _buildScaffoldWrapper(context: context,viewModel: viewModel);
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
Widget _buildColumnScroller(LoginViewModel viewModel) =>
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildUpperColumn(viewModel), child: _buildBodyWrapper(context: context,viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel), children: _buildUpperColumnChildren(viewModel),
); );
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,

View File

@ -85,40 +85,9 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
canPop: false, canPop: false,
onPopInvokedWithResult: (value, data) => onPopInvokedWithResult: (value, data) =>
_pop(value: value, viewModel: viewModel), _pop(value: value, viewModel: viewModel),
child: _buildScaffoldWrapper(viewModel)); child: _buildBody(viewModel));
Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack(children: [
_buildScaffold(viewModel),
_buildRegistrationState(viewModel),
_buildVerityOtpState(viewModel)
]);
Widget _buildScaffold(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(RegisterViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
Widget _buildExpandedBody(RegisterViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(RegisterViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(RegisterViewModel viewModel) => Widget _buildBody(RegisterViewModel viewModel) =>
IndexedStack(index: viewModel.currentPage, children: _buildScreens()); IndexedStack(index: viewModel.currentPage, children: _buildScreens());
@ -146,13 +115,4 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
passwordController: passwordController, passwordController: passwordController,
confirmPasswordController: confirmPasswordController); confirmPasswordController: confirmPasswordController);
Widget _buildRegistrationState(RegisterViewModel viewModel) =>
viewModel.busy(StateObjects.registration)
? const PageLoadingIndicator()
: Container();
Widget _buildVerityOtpState(RegisterViewModel viewModel) =>
viewModel.busy(StateObjects.verifyOtp)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
@ -10,6 +11,7 @@ import 'package:yimaru_app/ui/views/home/home_view.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../models/user_model.dart'; import '../../../models/user_model.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
class RegisterViewModel extends FormViewModel { class RegisterViewModel extends FormViewModel {
@ -19,6 +21,9 @@ class RegisterViewModel extends FormViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _googleAuthService = locator<GoogleAuthService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
// Navigation // Navigation
@ -284,12 +289,12 @@ class RegisterViewModel extends FormViewModel {
// Remote api calls // Remote api calls
// Register // Register
Future<void> register() async => Future<void> registerWithEmail() async =>
await runBusyFuture(_register(), busyObject: StateObjects.registration); await runBusyFuture(_register(), busyObject: StateObjects.registerWithEmail);
Future<void> _register() async { Future<void> _register() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = await _apiService.register(_userData); Map<String, dynamic> response = await _apiService.registerWithEmail(_userData);
if (response['status'] == ResponseStatus.success) { if (response['status'] == ResponseStatus.success) {
goTo(page: 3); goTo(page: 3);
@ -300,6 +305,38 @@ class RegisterViewModel extends FormViewModel {
} }
} }
// Register with google
Future<void> registerWithGoogle() async => await runBusyFuture(_googleLogin(),
busyObject: StateObjects.registerWithGoogle);
Future<void> _googleLogin() async {
if (await _statusChecker.checkConnection()) {
GoogleSignInAccount? googleUser = await _googleAuthService.googleAuth();
Map<String, dynamic> data = {
'id_token': googleUser?.authentication.idToken ?? '',
};
Map<String, dynamic> response = await _apiService.googleAuth(data);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserCredential(data);
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
Future<void> verifyOtp() async => Future<void> verifyOtp() async =>
await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp); await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp);

View File

@ -7,9 +7,12 @@ import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/validator_list_tile.dart'; import 'package:yimaru_app/ui/widgets/validator_list_tile.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/obscure_password.dart'; import '../../../widgets/obscure_password.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../register_view.form.dart'; import '../register_view.form.dart';
class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> { class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
@ -31,21 +34,54 @@ class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
}; };
viewModel.addUserData(data); viewModel.addUserData(data);
await viewModel.register(); await viewModel.registerWithEmail();
} }
@override @override
Widget build(BuildContext context, RegisterViewModel viewModel) => Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildBodyChildren(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildBodyChildren(RegisterViewModel viewModel) => Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold(
SingleChildScrollView( backgroundColor: kcBackgroundColor,
child: _buildBodyColumn(viewModel), body: _buildScaffoldStack(viewModel),
); );
Widget _buildBodyColumn(RegisterViewModel viewModel) => Column( Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack(
mainAxisSize: MainAxisSize.min, children: [
_buildScaffold(viewModel),
_buildRegistrationState(viewModel),
],
);
Widget _buildScaffold(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(RegisterViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(RegisterViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(RegisterViewModel viewModel) =>
Expanded(child: _buildColumnScroller(viewModel));
Widget _buildColumnScroller(RegisterViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper(RegisterViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel), children: _buildBodyColumnChildren(viewModel),
); );
@ -248,4 +284,9 @@ class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1), : kcPrimaryColor.withOpacity(0.1),
); );
Widget _buildRegistrationState(RegisterViewModel viewModel) =>
viewModel.busy(StateObjects.registerWithEmail)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -6,7 +6,9 @@ import 'package:yimaru_app/ui/widgets/login_account.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/option_text_divider.dart'; import '../../../widgets/option_text_divider.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../register_viewmodel.dart'; import '../register_viewmodel.dart';
import '../register_view.form.dart'; import '../register_view.form.dart';
@ -28,23 +30,95 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
viewModel.goTo(page: 2, type: RegistrationType.email); viewModel.goTo(page: 2, type: RegistrationType.email);
} }
Widget getPadding(context) {
double half = screenHeight(context) / 2;
return SizedBox(
height: half + 155 - half,
);
}
@override @override
Widget build(BuildContext context, RegisterViewModel viewModel) => Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildBody(viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildBody(RegisterViewModel viewModel) => Column( Widget _buildScaffoldWrapper(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Stack(
children: [
_buildScaffold(context: context, viewModel: viewModel),
_buildRegisterWithGoogleState(viewModel)
],
);
Widget _buildScaffold(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
[
_buildAppBar(viewModel),
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(RegisterViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Expanded(
child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context, viewModel: viewModel),
);
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(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel), children: _buildBodyChildren(context: context, viewModel: viewModel),
); );
List<Widget> _buildBodyChildren(RegisterViewModel viewModel) => List<Widget> _buildBodyChildren(
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; {required BuildContext context,
required RegisterViewModel viewModel}) =>
Widget _buildColumnScroller(RegisterViewModel viewModel) => [
SingleChildScrollView( _buildUpperColumn(viewModel),
child: _buildUpperColumn(viewModel), getPadding(context),
); _buildLowerColumn(viewModel)
];
Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( Widget _buildUpperColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -108,6 +182,7 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
List<Widget> _buildLowerColumnChildren(RegisterViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(RegisterViewModel viewModel) => [
_buildContinueButton(viewModel), _buildContinueButton(viewModel),
_buildRegisterWithGoogleButton(viewModel),
_buildOptionTextDivider(), _buildOptionTextDivider(),
_buildRegisterWithEmailButton(viewModel), _buildRegisterWithEmailButton(viewModel),
verticalSpaceMedium verticalSpaceMedium
@ -116,6 +191,7 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
Widget _buildContinueButton(RegisterViewModel viewModel) => Widget _buildContinueButton(RegisterViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
safe: false,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite, foregroundColor: kcWhite,
@ -129,6 +205,18 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
: kcPrimaryColor.withOpacity(0.1), : kcPrimaryColor.withOpacity(0.1),
); );
Widget _buildRegisterWithGoogleButton(RegisterViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
text: 'Login with Google',
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
leadingImage: 'assets/icons/google.png',
onTap: () async => await viewModel.registerWithGoogle(),
);
Widget _buildOptionTextDivider() => const OptionTextDivider(); Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildRegisterWithEmailButton(RegisterViewModel viewModel) => Widget _buildRegisterWithEmailButton(RegisterViewModel viewModel) =>
@ -142,4 +230,11 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
text: 'Register with Phone Number', text: 'Register with Phone Number',
onTap: () => viewModel.goTo(page: 1), onTap: () => viewModel.goTo(page: 1),
); );
Widget _buildRegisterWithGoogleState(RegisterViewModel viewModel) =>
viewModel.busy(StateObjects.registerWithEmail)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -8,6 +8,7 @@ import '../../../common/app_colors.dart';
import '../../../common/enmus.dart'; import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/phone_number_prefix.dart'; import '../../../widgets/phone_number_prefix.dart';
import '../register_viewmodel.dart'; import '../register_viewmodel.dart';
import '../register_view.form.dart'; import '../register_view.form.dart';
@ -17,23 +18,83 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget<RegisterViewModel> {
const RegisterWithPhoneNumberScreen( const RegisterWithPhoneNumberScreen(
{super.key, required this.phoneNumberController}); {super.key, required this.phoneNumberController});
Widget getPadding(context) {
double half = screenHeight(context) / 2;
return SizedBox(height: half + 170 - half);
}
@override @override
Widget build(BuildContext context, RegisterViewModel viewModel) => Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildBody(viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildBody(RegisterViewModel viewModel) => Column( Widget _buildScaffoldWrapper(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:
_buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
[
_buildAppBar(viewModel),
_buildExpandedBody(context: context, viewModel: viewModel)
];
Widget _buildAppBar(RegisterViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Expanded(
child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context, viewModel: viewModel),
);
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(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel), children: _buildBodyChildren(context: context, viewModel: viewModel),
); );
List<Widget> _buildBodyChildren(RegisterViewModel viewModel) => List<Widget> _buildBodyChildren(
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; {required BuildContext context,
required RegisterViewModel viewModel}) =>
Widget _buildColumnScroller(RegisterViewModel viewModel) => [
SingleChildScrollView( _buildUpperColumn(viewModel),
child: _buildUpperColumn(viewModel), getPadding(context),
); _buildLowerColumn(viewModel)
];
Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( Widget _buildUpperColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -7,9 +7,12 @@ import 'package:yimaru_app/ui/views/register/register_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_cursor.dart'; import 'package:yimaru_app/ui/widgets/custom_cursor.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../register_view.form.dart'; import '../register_view.form.dart';
class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> { class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
@ -23,6 +26,12 @@ class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
required this.emailController, required this.emailController,
required this.phoneNumberController}); required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 325 - half,);
}
Future<void> _verifyOtp(RegisterViewModel viewModel) async { Future<void> _verifyOtp(RegisterViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -36,24 +45,71 @@ class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
await viewModel.verifyOtp(); await viewModel.verifyOtp();
} }
@override @override
Widget build(BuildContext context, RegisterViewModel viewModel) => Widget build(BuildContext context, RegisterViewModel viewModel) =>
_buildBody(viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildBody(RegisterViewModel viewModel) => Column( Widget _buildScaffoldWrapper( {required BuildContext context,
crossAxisAlignment: CrossAxisAlignment.start, required RegisterViewModel viewModel}) => Scaffold(
mainAxisAlignment: MainAxisAlignment.spaceBetween, backgroundColor: kcBackgroundColor,
children: _buildBodyChildren(viewModel), body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack(
{required BuildContext context,
required RegisterViewModel viewModel}) =>
Stack(
children: [
_buildScaffold(context: context, viewModel: viewModel),
_buildVerifyOtpState(viewModel)
],
); );
List<Widget> _buildBodyChildren(RegisterViewModel viewModel) => Widget _buildScaffold( {required BuildContext context,
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; required RegisterViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context, viewModel: viewModel),
);
Widget _buildColumnScroller(RegisterViewModel viewModel) => List<Widget> _buildScaffoldChildren( {required BuildContext context,
required RegisterViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context, viewModel: viewModel)];
Widget _buildAppBar(RegisterViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody( {required BuildContext context,
required RegisterViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller( {required BuildContext context,
required RegisterViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildUpperColumn(viewModel), child: _buildBodyWrapper(context: context, viewModel: viewModel),
); );
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(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren( {required BuildContext context,
required RegisterViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( Widget _buildUpperColumn(RegisterViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -174,4 +230,9 @@ class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
? () async => await _verifyOtp(viewModel) ? () async => await _verifyOtp(viewModel)
: null, : null,
); );
Widget _buildVerifyOtpState(RegisterViewModel viewModel) =>
viewModel.busy(StateObjects.verifyOtp)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_bottom_sheet.dart';
import 'custom_elevated_button.dart';
class CancelLearnPracticeSheet extends StatelessWidget {
final GestureTapCallback? onTap;
const CancelLearnPracticeSheet({super.key, this.onTap});
@override
Widget build(BuildContext context) => _buildSheetWrapper();
Widget _buildSheetWrapper() => CustomBottomSheet(
height: 500, onTap: onTap, child: _buildColumnWrapper());
Widget _buildColumnWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(),
);
Widget _buildColumn() => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildSheetChildren(),
);
List<Widget> _buildSheetChildren() => [
verticalSpaceLarge,
_buildImage(),
verticalSpaceMedium,
_buildMessage(),
_buildSubtitle(),
verticalSpaceLarge,
_buildContinueButton(),
_buildEndButton(),
];
Widget _buildImage() => const SpeakingPartnerImage(
radius: 45,
);
Widget _buildMessage() => Text.rich(
TextSpan(text: 'Youre almost there,', style: style18DG600, children: [
TextSpan(
text: ' Johnny!',
style: style18P600,
)
]),
);
Widget _buildSubtitle() => Text(
'Finish this session to see your progress.',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildContinueButton() => CustomElevatedButton(
height: 55,
onTap: onTap,
borderRadius: 12,
foregroundColor: kcWhite,
text: 'Continue Practice',
backgroundColor: kcPrimaryColor,
);
Widget _buildEndButton() => CustomElevatedButton(
height: 55,
onTap: onTap,
borderRadius: 12,
text: 'End Session',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
);
}

View File

@ -3,15 +3,17 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
class CustomBottomSheet extends StatelessWidget { class CustomBottomSheet extends StatelessWidget {
final Widget child; final Widget child;
final double height;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
const CustomBottomSheet({super.key, this.onTap, required this.child}); const CustomBottomSheet(
{super.key, this.onTap, required this.child, required this.height});
@override @override
Widget build(BuildContext context) => _buildStackWrapper(); Widget build(BuildContext context) => _buildStackWrapper();
Widget _buildStackWrapper() => Container( Widget _buildStackWrapper() => Container(
height: 400, height: height,
color: kcTransparent, color: kcTransparent,
width: double.maxFinite, width: double.maxFinite,
child: _buildStack(), child: _buildStack(),
@ -49,8 +51,8 @@ class CustomBottomSheet extends StatelessWidget {
); );
Widget _buildSheetWrapper() => Container( Widget _buildSheetWrapper() => Container(
height: double.maxFinite,
width: double.maxFinite, width: double.maxFinite,
height: double.maxFinite,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: kcBackgroundColor, color: kcBackgroundColor,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
class CustomColumnButton extends StatelessWidget {
final Color color;
final String label;
final IconData icon;
final GestureTapCallback? onTap;
const CustomColumnButton(
{super.key,
this.onTap,
required this.icon,
required this.label,
required this.color});
@override
Widget build(BuildContext context) => _buildColumnWrapper();
Widget _buildColumnWrapper() => GestureDetector(
onTap: onTap,
child: _buildColumn(),
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: _buildColumnChildren(),
);
List<Widget> _buildColumnChildren() => [
_buildIconWrapper(),
_buildLabel()
];
Widget _buildIconWrapper() => Container(
padding:const EdgeInsets.all(5),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color.withOpacity(0.1),
border: Border.all(color: color.withOpacity(0.75))
),
child: _buildIcon(),
);
Widget _buildLabel()=> Text(label,style: style14LG400.copyWith(color: color),);
Widget _buildIcon()=> Icon(icon,size: 14,color: color,);
}

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import '../common/app_colors.dart';
import 'custom_circular_progress_indicator.dart';
class EmptyVideoPlayer extends StatelessWidget {
const EmptyVideoPlayer({super.key});
@override
Widget build(BuildContext context) => _buildContainer();
Widget _buildContainer() => Container(
decoration: const BoxDecoration(color: kcLightGrey),
child: _buildStack(),
);
Widget _buildStack() => Stack(
children: [_buildProgressIndicatorWrapper()],
);
Widget _buildProgressIndicatorWrapper() => Align(
alignment: Alignment.center,
child: _buildProgressIndicator(),
);
Widget _buildProgressIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
}

View File

@ -13,8 +13,8 @@ class FinishPracticeSheet extends StatelessWidget {
@override @override
Widget build(BuildContext context) => _buildSheetWrapper(); Widget build(BuildContext context) => _buildSheetWrapper();
Widget _buildSheetWrapper() => Widget _buildSheetWrapper() => CustomBottomSheet(
CustomBottomSheet(onTap: onTap, child: _buildColumnWrapper()); height: 400, onTap: onTap, child: _buildColumnWrapper());
Widget _buildColumnWrapper() => Padding( Widget _buildColumnWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),

View File

@ -12,9 +12,11 @@ class LearnLessonTile extends StatelessWidget {
final String title; final String title;
final String thumbnail; final String thumbnail;
final ProgressStatuses status; final ProgressStatuses status;
final GestureTapCallback? onLessonTap;
const LearnLessonTile({ const LearnLessonTile({
super.key, super.key,
this.onLessonTap,
required this.title, required this.title,
required this.status, required this.status,
required this.thumbnail, required this.thumbnail,
@ -46,7 +48,6 @@ class LearnLessonTile extends StatelessWidget {
collapsedIconColor: kcDarkGrey, collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey, collapsedTextColor: kcDarkGrey,
leading: _buildLeadingWrapper(), leading: _buildLeadingWrapper(),
shape: Border.all(color: kcTransparent), shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft, expandedAlignment: Alignment.centerLeft,
enabled: status != ProgressStatuses.pending, enabled: status != ProgressStatuses.pending,
@ -180,7 +181,7 @@ class LearnLessonTile extends StatelessWidget {
_buildLessonButton(), _buildLessonButton(),
]; ];
Widget _buildLessonButton() => const CustomElevatedButton( Widget _buildPracticeButton() => const CustomElevatedButton(
height: 15, height: 15,
width: 135, width: 135,
text: 'Practice', text: 'Practice',
@ -191,10 +192,11 @@ class LearnLessonTile extends StatelessWidget {
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
Widget _buildPracticeButton() => CustomElevatedButton( Widget _buildLessonButton() => CustomElevatedButton(
height: 15, height: 15,
width: 135, width: 135,
borderRadius: 12, borderRadius: 12,
onTap: onLessonTap,
foregroundColor: kcWhite, foregroundColor: kcWhite,
trailingIcon: Icons.play_arrow, trailingIcon: Icons.play_arrow,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,

View File

@ -34,7 +34,7 @@ class ProfileImage extends StatelessWidget {
Widget _buildProfileImage() => CircleAvatar( Widget _buildProfileImage() => CircleAvatar(
radius: 50, radius: 50,
backgroundColor: kcPrimaryColor, backgroundColor: kcViolet,
backgroundImage: loading backgroundImage: loading
? null ? null
: profileImage != null || (profileImage?.contains('.') ?? false) : profileImage != null || (profileImage?.contains('.') ?? false)

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import '../common/app_colors.dart';
class SpeakingPartnerImage extends StatelessWidget {
final double radius;
const SpeakingPartnerImage({super.key,required this.radius});
@override
Widget build(BuildContext context) => _buildProfileImage();
Widget _buildProfileImage() => CircleAvatar(
radius: radius,
backgroundColor: kcViolet,
backgroundImage: _buildImageBuilder(),
);
AssetImage? _buildImageBuilder() => const AssetImage('assets/images/profile.png');
}

View File

@ -20,7 +20,7 @@ class SuggestionCard extends StatelessWidget {
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [kcPrimaryAccent, kcPrimaryColor]), colors: [kcIndigo, kcPrimaryColor]),
), ),
child: _buildRow(), child: _buildRow(),
); );

View File

@ -11,7 +11,10 @@ import file_selector_macos
import firebase_core import firebase_core
import flutter_secure_storage_darwin import flutter_secure_storage_darwin
import google_sign_in_ios import google_sign_in_ios
import package_info_plus
import sqflite_darwin import sqflite_darwin
import video_player_avfoundation
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
@ -20,5 +23,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
} }

View File

@ -177,6 +177,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.4" version: "2.0.4"
chewie:
dependency: "direct main"
description:
name: chewie
sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca"
url: "https://pub.dev"
source: hosted
version: "1.13.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -257,6 +265,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
cupertino_icons:
dependency: transitive
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -494,6 +510,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
flutter_spinkit:
dependency: "direct main"
description:
name: flutter_spinkit
sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f"
url: "https://pub.dev"
source: hosted
version: "5.2.2"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -952,6 +976,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
package_info_plus:
dependency: transitive
description:
name: package_info_plus
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev"
source: hosted
version: "9.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1413,6 +1453,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
video_player:
dependency: "direct main"
description:
name: video_player
sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: ee4fd520b0cafa02e4a867a0f882092e727cdaa1a2d24762171e787f8a502b0a
url: "https://pub.dev"
source: hosted
version: "2.9.1"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: f46e9e20f1fe429760cf4dc118761336320d1bec0f50d255930c2355f2defb5b
url: "https://pub.dev"
source: hosted
version: "2.9.1"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
url: "https://pub.dev"
source: hosted
version: "6.6.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -1421,6 +1501,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.0.2"
wakelock_plus:
dependency: transitive
description:
name: wakelock_plus
sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
wakelock_plus_platform_interface:
dependency: transitive
description:
name: wakelock_plus_platform_interface
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -15,6 +15,7 @@ dependencies:
pinput: ^6.0.1 pinput: ^6.0.1
stacked: ^3.4.0 stacked: ^3.4.0
iconsax: ^0.0.8 iconsax: ^0.0.8
chewie: ^1.13.0
flutter_svg: ^2.2.3 flutter_svg: ^2.2.3
stacked_shared: any stacked_shared: any
image_picker: ^1.2.1 image_picker: ^1.2.1
@ -22,6 +23,7 @@ dependencies:
storage_info: ^1.0.0 storage_info: ^1.0.0
flutter_html: ^3.0.0 flutter_html: ^3.0.0
email_validator: any email_validator: any
video_player: ^2.10.1
firebase_core: ^4.4.0 firebase_core: ^4.4.0
in_app_update: ^4.2.5 in_app_update: ^4.2.5
path_provider: ^2.1.5 path_provider: ^2.1.5
@ -29,6 +31,7 @@ dependencies:
toastification: ^3.0.3 toastification: ^3.0.3
dropdown_search: ^6.0.2 dropdown_search: ^6.0.2
json_annotation: ^4.9.0 json_annotation: ^4.9.0
flutter_spinkit: ^5.2.2
stacked_services: ^1.1.0 stacked_services: ^1.1.0
omni_datetime_picker: any omni_datetime_picker: any
json_serializable: ^6.8.0 json_serializable: ^6.8.0

View File

@ -952,7 +952,7 @@ class MockAuthenticationService extends _i1.Mock
/// See the documentation for Mockito's code generation for more information. /// See the documentation for Mockito's code generation for more information.
class MockApiService extends _i1.Mock implements _i12.ApiService { class MockApiService extends _i1.Mock implements _i12.ApiService {
@override @override
_i8.Future<Map<String, dynamic>> register(Map<String, dynamic>? data) => _i8.Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic>? data) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method( Invocation.method(
#register, #register,
@ -978,7 +978,7 @@ class MockApiService extends _i1.Mock implements _i12.ApiService {
) as _i8.Future<Map<String, dynamic>>); ) as _i8.Future<Map<String, dynamic>>);
@override @override
_i8.Future<Map<String, dynamic>> googleLogin(Map<String, dynamic>? data) => _i8.Future<Map<String, dynamic>> googleAuth(Map<String, dynamic>? data) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method( Invocation.method(
#googleLogin, #googleLogin,
@ -1384,7 +1384,7 @@ class MockGoogleAuthService extends _i1.Mock implements _i19.GoogleAuthService {
) as _i4.GoogleSignIn); ) as _i4.GoogleSignIn);
@override @override
_i8.Future<_i4.GoogleSignInAccount?> googleSignIn() => (super.noSuchMethod( _i8.Future<_i4.GoogleSignInAccount?> googleAuth() => (super.noSuchMethod(
Invocation.method( Invocation.method(
#googleSignIn, #googleSignIn,
[], [],

View File

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

View File

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