Compare commits

...

3 Commits

Author SHA1 Message Date
64cad421e7 Merge tag '0.1.14' into develop
- fix: Apply UAT comments
2026-05-09 01:07:30 +03:00
a2a26c456d Merge branch 'release/0.1.14'
- fix: Apply UAT comments
2026-05-09 01:05:50 +03:00
e917209d10 fix: Apply UAT comments 2026-05-09 01:04:53 +03:00
41 changed files with 605 additions and 456 deletions

BIN
assets/images/pattern.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -510,8 +510,8 @@ class StackedRouter extends _i1.RouterBase {
_i22.LearnLessonDetailView: (data) {
final args = data.getArgs<LearnLessonDetailViewArguments>(nullOk: false);
return _i37.MaterialPageRoute<dynamic>(
builder: (context) =>
_i22.LearnLessonDetailView(key: args.key, lesson: args.lesson),
builder: (context) => _i22.LearnLessonDetailView(
key: args.key, lesson: args.lesson, hasPractice: args.hasPractice),
settings: data,
);
},
@ -519,7 +519,13 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<LearnPracticeViewArguments>(nullOk: false);
return _i37.MaterialPageRoute<dynamic>(
builder: (context) => _i23.LearnPracticeView(
key: args.key, id: args.id, practice: args.practice),
key: args.key,
level: args.level,
id: args.id,
label: args.label,
title: args.title,
practice: args.practice,
subtitle: args.subtitle),
settings: data,
);
},
@ -1098,56 +1104,85 @@ class LearnLessonDetailViewArguments {
const LearnLessonDetailViewArguments({
this.key,
required this.lesson,
required this.hasPractice,
});
final _i37.Key? key;
final _i40.LearnLesson lesson;
final bool hasPractice;
@override
String toString() {
return '{"key": "$key", "lesson": "$lesson"}';
return '{"key": "$key", "lesson": "$lesson", "hasPractice": "$hasPractice"}';
}
@override
bool operator ==(covariant LearnLessonDetailViewArguments other) {
if (identical(this, other)) return true;
return other.key == key && other.lesson == lesson;
return other.key == key &&
other.lesson == lesson &&
other.hasPractice == hasPractice;
}
@override
int get hashCode {
return key.hashCode ^ lesson.hashCode;
return key.hashCode ^ lesson.hashCode ^ hasPractice.hashCode;
}
}
class LearnPracticeViewArguments {
const LearnPracticeViewArguments({
this.key,
this.level,
required this.id,
required this.label,
required this.title,
required this.practice,
required this.subtitle,
});
final _i37.Key? key;
final String? level;
final int id;
final String label;
final String title;
final _i41.LearnPractices practice;
final String subtitle;
@override
String toString() {
return '{"key": "$key", "id": "$id", "practice": "$practice"}';
return '{"key": "$key", "level": "$level", "id": "$id", "label": "$label", "title": "$title", "practice": "$practice", "subtitle": "$subtitle"}';
}
@override
bool operator ==(covariant LearnPracticeViewArguments other) {
if (identical(this, other)) return true;
return other.key == key && other.id == id && other.practice == practice;
return other.key == key &&
other.level == level &&
other.id == id &&
other.label == label &&
other.title == title &&
other.practice == practice &&
other.subtitle == subtitle;
}
@override
int get hashCode {
return key.hashCode ^ id.hashCode ^ practice.hashCode;
return key.hashCode ^
level.hashCode ^
id.hashCode ^
label.hashCode ^
title.hashCode ^
practice.hashCode ^
subtitle.hashCode;
}
}
@ -1817,6 +1852,7 @@ extension NavigatorStateExtension on _i46.NavigationService {
Future<dynamic> navigateToLearnLessonDetailView({
_i37.Key? key,
required _i40.LearnLesson lesson,
required bool hasPractice,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -1824,7 +1860,8 @@ extension NavigatorStateExtension on _i46.NavigationService {
transition,
}) async {
return navigateTo<dynamic>(Routes.learnLessonDetailView,
arguments: LearnLessonDetailViewArguments(key: key, lesson: lesson),
arguments: LearnLessonDetailViewArguments(
key: key, lesson: lesson, hasPractice: hasPractice),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
@ -1833,8 +1870,12 @@ extension NavigatorStateExtension on _i46.NavigationService {
Future<dynamic> navigateToLearnPracticeView({
_i37.Key? key,
String? level,
required int id,
required String label,
required String title,
required _i41.LearnPractices practice,
required String subtitle,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -1842,8 +1883,14 @@ extension NavigatorStateExtension on _i46.NavigationService {
transition,
}) async {
return navigateTo<dynamic>(Routes.learnPracticeView,
arguments:
LearnPracticeViewArguments(key: key, id: id, practice: practice),
arguments: LearnPracticeViewArguments(
key: key,
level: level,
id: id,
label: label,
title: title,
practice: practice,
subtitle: subtitle),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
@ -2395,6 +2442,7 @@ extension NavigatorStateExtension on _i46.NavigationService {
Future<dynamic> replaceWithLearnLessonDetailView({
_i37.Key? key,
required _i40.LearnLesson lesson,
required bool hasPractice,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2402,7 +2450,8 @@ extension NavigatorStateExtension on _i46.NavigationService {
transition,
}) async {
return replaceWith<dynamic>(Routes.learnLessonDetailView,
arguments: LearnLessonDetailViewArguments(key: key, lesson: lesson),
arguments: LearnLessonDetailViewArguments(
key: key, lesson: lesson, hasPractice: hasPractice),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
@ -2411,8 +2460,12 @@ extension NavigatorStateExtension on _i46.NavigationService {
Future<dynamic> replaceWithLearnPracticeView({
_i37.Key? key,
String? level,
required int id,
required String label,
required String title,
required _i41.LearnPractices practice,
required String subtitle,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
@ -2420,8 +2473,14 @@ extension NavigatorStateExtension on _i46.NavigationService {
transition,
}) async {
return replaceWith<dynamic>(Routes.learnPracticeView,
arguments:
LearnPracticeViewArguments(key: key, id: id, practice: practice),
arguments: LearnPracticeViewArguments(
key: key,
level: level,
id: id,
label: label,
title: title,
practice: practice,
subtitle: subtitle),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,

View File

@ -158,6 +158,7 @@ class DioService {
return true;
} catch (e) {
print('REFRESH EXCEPTION: ${e.toString()}');
await _authenticationService.logout();
await _navigationService.replaceWithLoginView();
return false;

View File

@ -2,7 +2,5 @@ import 'package:flutter_phone_direct_caller/flutter_phone_direct_caller.dart';
class PhoneCallerService {
Future<void> call(String phone) async =>
await FlutterPhoneDirectCaller.callNumber(phone);
await FlutterPhoneDirectCaller.callNumber(phone);
}

View File

@ -88,4 +88,4 @@ String kServerClientId =
// Other
String kPhoneSupport = '+251946396655';
String kTelegramSupport = '@yimaruacademy2026';
String kTelegramSupport = '@yimaruacademy2026';

View File

@ -1,4 +1,3 @@
const String ksHomeBottomSheetTitle = 'Build Great Apps!';
const String ksSuggestion =

View File

@ -238,6 +238,12 @@ TextStyle style25P600 = const TextStyle(
fontWeight: FontWeight.w600,
);
TextStyle style40P900 = const TextStyle(
fontSize: 40,
color: kcPrimaryColor,
fontWeight: FontWeight.w900,
);
TextStyle style25DG600 = const TextStyle(
fontSize: 25,
color: kcDarkGrey,
@ -309,16 +315,14 @@ TextStyle style14LG400 = const TextStyle(
TextStyle style14MG400 = const TextStyle(
color: kcMediumGrey,
);
TextStyle style14DG400 = const TextStyle(color: kcDarkGrey);
TextStyle style14DG500 =
const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500);
TextStyle style18MG500 = const TextStyle(
fontSize: 18, color: kcMediumGrey, fontWeight: FontWeight.w500);
TextStyle style14DG400 = const TextStyle(
color: kcDarkGrey,
);
TextStyle style14DG600 = const TextStyle(
color: kcDarkGrey,
fontWeight: FontWeight.w600,

View File

@ -53,62 +53,61 @@ class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
controller: viewModel.pageController,
itemCount: viewModel.assessmentQuestions.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (cotext, index) =>
_buildBodyScroller( viewModel),
itemBuilder: (cotext, index) => _buildBodyScroller(viewModel),
);
Widget _buildBodyScroller(
AssessmentViewModel viewModel) =>
Widget _buildBodyScroller(AssessmentViewModel viewModel) =>
SingleChildScrollView(
child: _buildBody( viewModel),
child: _buildBody(viewModel),
);
Widget _buildBody(
AssessmentViewModel viewModel) =>
Column(
Widget _buildBody(AssessmentViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren( viewModel),
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(
AssessmentViewModel viewModel) =>[
List<Widget> _buildBodyChildren(AssessmentViewModel viewModel) => [
verticalSpaceMedium,
_buildTitleState( viewModel),
_buildTitleState(viewModel),
verticalSpaceMedium,
_buildAnswersState( viewModel),
_buildContinueButtonWrapper( viewModel)
_buildAnswersState(viewModel),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildTitleState( AssessmentViewModel viewModel)=> viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length ?Container(): _buildTitle(viewModel);
Widget _buildTitle(
AssessmentViewModel viewModel) =>
Text(
Widget _buildTitleState(AssessmentViewModel viewModel) =>
viewModel.currentQuestionIndex == viewModel.assessmentQuestions.length
? Container()
: _buildTitle(viewModel);
Widget _buildTitle(AssessmentViewModel viewModel) => Text(
'Q${viewModel.currentQuestionIndex + 1}. ${viewModel.assessmentQuestions[viewModel.currentQuestionIndex].questionText} ',
style: style16DG600,
);
Widget _buildAnswersState( AssessmentViewModel viewModel)=> viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length ?Container(): _buildAnswers(viewModel);
Widget _buildAnswersState(AssessmentViewModel viewModel) =>
viewModel.currentQuestionIndex == viewModel.assessmentQuestions.length
? Container()
: _buildAnswers(viewModel);
Widget _buildAnswers(
AssessmentViewModel viewModel) =>
ListView.builder(
Widget _buildAnswers(AssessmentViewModel viewModel) => ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?.length,
itemCount: viewModel.assessmentQuestions[viewModel.currentQuestionIndex]
.options?.length,
itemBuilder: (context, inner) => _buildAnswer(
onTap: () => viewModel.setSelectedAnswer(
question: viewModel.currentQuestionIndex + 1,
option: viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?[inner]),
title:
viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?[inner].optionText ??
'',
option: viewModel
.assessmentQuestions[viewModel.currentQuestionIndex]
.options?[inner]),
title: viewModel.assessmentQuestions[viewModel.currentQuestionIndex]
.options?[inner].optionText ??
'',
selected: viewModel.isSelectedAnswer(
question: viewModel.currentQuestionIndex + 1,
question: viewModel.currentQuestionIndex + 1,
answer: viewModel
.assessmentQuestions[viewModel.currentQuestionIndex ].options?[inner].optionText ??
.assessmentQuestions[viewModel.currentQuestionIndex]
.options?[inner]
.optionText ??
''),
),
);
@ -123,30 +122,27 @@ class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
selected: selected,
);
Widget _buildContinueButtonWrapper(
AssessmentViewModel viewModel) =>
Padding(
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton( viewModel),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(
AssessmentViewModel viewModel) =>
Widget _buildContinueButton(AssessmentViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
foregroundColor: kcWhite,
text: viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length - 1
viewModel.assessmentQuestions.length - 1
? 'Finish Level'
: 'Continue',
backgroundColor:
viewModel.selectedAnswers.containsKey('${viewModel.currentQuestionIndex + 1}')
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.selectedAnswers.containsKey('${viewModel.currentQuestionIndex + 1}')
backgroundColor: viewModel.selectedAnswers
.containsKey('${viewModel.currentQuestionIndex + 1}')
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.selectedAnswers
.containsKey('${viewModel.currentQuestionIndex + 1}')
? () => viewModel.nextQuestion()
: null,
);
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
@ -83,9 +84,7 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubtitle('+2519012345678'),
verticalSpaceSmall,
_buildSubtitle('+2519012345678'),
_buildSubtitle(kPhoneSupport),
];
Widget _buildIcon() =>
@ -109,12 +108,13 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
);
Widget _buildContinueButton(CallSupportViewModel viewModel) =>
const CustomElevatedButton(
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Tap to Call',
leadingIcon: Icons.call,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.callSupport(),
);
}

View File

@ -2,11 +2,18 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../services/phone_caller_service.dart';
import '../../common/app_constants.dart';
class CallSupportViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>();
final _phoneCallerService = locator<PhoneCallerService>();
// Call support
Future<void> callSupport() => _phoneCallerService.call(kPhoneSupport);
// Navigation
void pop() => _navigationService.back();
}

View File

@ -40,14 +40,6 @@ class ForgetPasswordViewModel extends FormViewModel {
bool get length => _length;
bool _number = false;
bool get number => _number;
bool _specialChar = false;
bool get specialChar => _specialChar;
bool _focusPassword = false;
bool get focusPassword => _focusPassword;
@ -105,11 +97,9 @@ class ForgetPasswordViewModel extends FormViewModel {
int completed = 0;
if (_length) completed++;
if (_number) completed++;
if (_specialChar) completed++;
if (_passwordMatch) completed++;
return completed / 4; // returns 0.0 1.0
return completed / 2; // returns 0.0 1.0
}
void validatePassword(
@ -120,17 +110,7 @@ class ForgetPasswordViewModel extends FormViewModel {
_length = false;
}
if (RegExp(r'\d').hasMatch(password)) {
_number = true;
} else {
_number = false;
}
if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
_specialChar = true;
} else {
_specialChar = false;
}
if (password == confirmPassword) {
_passwordMatch = true;
@ -156,8 +136,6 @@ class ForgetPasswordViewModel extends FormViewModel {
// Reset reset password screen
void resetResetPasswordScreen() {
_length = false;
_number = false;
_specialChar = false;
_passwordMatch = false;
_focusPassword = false;
_focusResetCode = false;

View File

@ -140,8 +140,6 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
_buildLinearProgressIndicator(viewModel),
verticalSpaceSmall,
_buildCharLengthValidator(viewModel),
_buildNumberValidator(viewModel),
_buildSymbolValidator(viewModel),
_buildPasswordMatchValidator(viewModel),
verticalSpaceSmall,
_buildSignUpButton(viewModel),
@ -256,16 +254,6 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey,
label: '8 characters minimum');
Widget _buildNumberValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey,
label: 'a number');
Widget _buildSymbolValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey,
label: 'one symbol minimum');
Widget _buildPasswordMatchValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor:
@ -281,20 +269,14 @@ class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
onTap: passwordController.text.isNotEmpty &&
confirmPasswordController.text.isNotEmpty &&
resetCodeController.text.isNotEmpty &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch
? () async => await _reset(viewModel)
: null,
backgroundColor: passwordController.text.isNotEmpty &&
confirmPasswordController.text.isNotEmpty &&
resetCodeController.text.isNotEmpty &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),

View File

@ -84,8 +84,9 @@ class LearnCourseView extends StackedView<LearnCourseViewModel> {
course: viewModel.learnCourses[index],
onViewTap: () async => await viewModel
.navigateToLearnModule(viewModel.learnCourses[index]),
onPracticeTap: () async => await viewModel
.navigateToLearnPractice(viewModel.learnCourses[index].id ?? 0),
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
id: viewModel.learnCourses[index].id ?? 0,
level: viewModel.learnCourses[index].name ?? ''),
),
separatorBuilder: (context, index) => verticalSpaceSmall,
);

View File

@ -27,8 +27,16 @@ class LearnCourseViewModel extends BaseViewModel {
Future<void> navigateToLearnModule(LearnCourse course) async =>
_navigationService.navigateToLearnModuleView(course: course);
Future<void> navigateToLearnPractice(int id) async => await _navigationService
.navigateToLearnPracticeView(id: id, practice: LearnPractices.course);
Future<void> navigateToLearnPractice(
{required int id, required String level}) async =>
await _navigationService.navigateToLearnPracticeView(
id: id,
level: level,
label: 'Begin Level Practice',
practice: LearnPractices.course,
title: 'Lets Practice Course $level',
subtitle: 'Lets quickly review what youve learned in this level!',
);
// Remote api call

View File

@ -115,9 +115,9 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
_buildSubtitle(),
verticalSpaceSmall,
_buildModuleProgress(),
verticalSpaceMedium,
verticalSpaceLarge,
_buildMotivationCard(),
verticalSpaceMedium,
verticalSpaceLarge,
_buildHeader(),
verticalSpaceMedium,
_buildListViewBuilder(viewModel),
@ -156,20 +156,25 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
itemCount: viewModel.lessons.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
index: index,
lesson: viewModel.lessons[index],
onLessonTap: () async => await viewModel
.navigateToLearnLessonDetail(viewModel.lessons[index]),
onLessonTap: () async => await viewModel.navigateToLearnLessonDetail(
lesson: viewModel.lessons[index],
hasPractice:
index != viewModel.lessons.length - 1 ? true : false),
onPracticeTap: () async => await viewModel
.navigateToLearnPractice(viewModel.lessons[index].id ?? 0),
),
);
Widget _buildTile({
required int index,
required LearnLesson lesson,
required GestureTapCallback? onLessonTap,
required GestureTapCallback? onPracticeTap,
}) =>
LearnLessonTile(
index: index,
lesson: lesson,
onLessonTap: onLessonTap,
onPracticeTap: onPracticeTap,

View File

@ -24,11 +24,20 @@ class LearnLessonViewModel extends BaseViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnPractice(int id) async => await _navigationService
.navigateToLearnPracticeView(id: id, practice: LearnPractices.lesson);
Future<void> navigateToLearnPractice(int id) async =>
await _navigationService.navigateToLearnPracticeView(
id: id,
label: 'Start Practice',
practice: LearnPractices.lesson,
title: 'Let\'s practice what you just learnt!',
subtitle:
'Ill ask you a few questions, and you can respond naturally.',
);
Future<void> navigateToLearnLessonDetail(LearnLesson lesson) async =>
await _navigationService.navigateToLearnLessonDetailView(lesson: lesson);
Future<void> navigateToLearnLessonDetail(
{required bool hasPractice, required LearnLesson lesson}) async =>
await _navigationService.navigateToLearnLessonDetailView(
lesson: lesson, hasPractice: hasPractice);
// Remote api call

View File

@ -13,9 +13,11 @@ import '../../widgets/small_app_bar.dart';
import 'learn_lesson_detail_viewmodel.dart';
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
final bool hasPractice;
final LearnLesson lesson;
const LearnLessonDetailView({Key? key, required this.lesson})
const LearnLessonDetailView(
{Key? key, required this.lesson, required this.hasPractice})
: super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel) async {
@ -86,7 +88,7 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
List<Widget> _buildBodyColumnChildren(LearnLessonDetailViewModel viewModel) =>
[
_buildLevelsColumnWrapper(viewModel),
_buildContinueButtonWrapper(viewModel)
if (hasPractice) _buildPracticeButtonWrapper(viewModel)
];
Widget _buildLevelsColumnWrapper(LearnLessonDetailViewModel viewModel) =>
@ -157,21 +159,21 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
style: style14DG400,
);
Widget _buildContinueButtonWrapper(LearnLessonDetailViewModel viewModel) =>
Widget _buildPracticeButtonWrapper(LearnLessonDetailViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(
left: 15,
right: 15,
bottom: 50,
),
child: _buildContinueButton(viewModel),
child: _buildPracticeButton(viewModel),
);
Widget _buildContinueButton(LearnLessonDetailViewModel viewModel) =>
Widget _buildPracticeButton(LearnLessonDetailViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Practices',
text: 'Take Practice',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await _navigate(viewModel),

View File

@ -67,6 +67,13 @@ class LearnLessonDetailViewModel extends BaseViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnPractice(int id) async => await _navigationService
.navigateToLearnPracticeView(id: id, practice: LearnPractices.lesson);
Future<void> navigateToLearnPractice(int id) async =>
await _navigationService.navigateToLearnPracticeView(
id: id,
label: 'Start Practice',
practice: LearnPractices.lesson,
title: 'Let\'s practice what you just learnt!',
subtitle:
'Ill ask you a few questions, and you can respond naturally.',
);
}

View File

@ -116,8 +116,9 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile(
module: viewModel.modules[index],
onPracticeTap: () async => await viewModel
.navigateToLearnPractice(viewModel.modules[index].id ?? 0),
onPracticeTap: () async => await viewModel.navigateToLearnPractice(
id: viewModel.modules[index].id ?? 0,
module: viewModel.modules[index].name ?? ''),
onModuleTap: () async =>
await viewModel.navigateToLearnLesson(viewModel.modules[index]),
),

View File

@ -27,8 +27,15 @@ class LearnModuleViewModel extends BaseViewModel {
Future<void> navigateToLearnLesson(LearnModule module) async =>
await _navigationService.navigateToLearnLessonView(module: module);
Future<void> navigateToLearnPractice(int id) async => await _navigationService
.navigateToLearnPracticeView(id: id, practice: LearnPractices.module);
Future<void> navigateToLearnPractice(
{required int id, required String module}) async =>
await _navigationService.navigateToLearnPracticeView(
id: id,
label: 'Begin Module Practice',
practice: LearnPractices.module,
title: 'Lets Practice $module',
subtitle: 'Lets quickly review what youve learned in this module! ',
);
// Remote api call

View File

@ -16,10 +16,21 @@ import 'learn_practice_viewmodel.dart';
class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
final int id;
final String label;
final String title;
final String? level;
final String subtitle;
final LearnPractices practice;
const LearnPracticeView({Key? key, required this.id, required this.practice})
: super(key: key);
const LearnPracticeView({
Key? key,
this.level,
required this.id,
required this.label,
required this.title,
required this.practice,
required this.subtitle,
}) : super(key: key);
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
await viewModel.stopRecording();
@ -108,10 +119,17 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
_buildLearnPracticeQuestionsScreen(),
_buildFinishLearnPracticeScreen(),
_buildLearnPracticeResultScreen(),
_buildLearnPracticeCompletionScreen()
if (practice == LearnPractices.course)
_buildLearnPracticeCompletionScreen()
];
Widget _buildLearnPracticeIntroScreen() => const LearnPracticeIntroScreen();
Widget _buildLearnPracticeIntroScreen() => LearnPracticeIntroScreen(
level: level,
title: title,
label: label,
practice: practice,
subtitle: subtitle,
);
Widget _buildLearnPracticeElementsScreen() =>
const LearnPracticeDescriptionScreen();
@ -121,8 +139,9 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
Widget _buildFinishLearnPracticeScreen() => const FinishLearnPracticeScreen();
Widget _buildLearnPracticeResultScreen() => const LearnPracticeResultScreen();
Widget _buildLearnPracticeResultScreen() =>
LearnPracticeResultScreen(practice: practice);
Widget _buildLearnPracticeCompletionScreen() =>
const LearnPracticeCompletionScreen();
LearnPracticeCompletionScreen(level: level ?? '');
}

View File

@ -225,7 +225,7 @@ class LearnPracticeViewModel extends ReactiveViewModel {
await stopRecording();
_answers.add({
'busy_object': question.id.toString(),
'sample_text_answer': question.audioCorrectAnswerText,
'question_text': question.questionText,
'sample_voice_answer': question.sampleAnswerVoicePrompt,
'recorded_voice_answer': await _voiceRecorderService.getRecordedAudio(),
});

View File

@ -9,7 +9,8 @@ import '../../../widgets/custom_elevated_button.dart';
class LearnPracticeCompletionScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeCompletionScreen({super.key});
final String level;
const LearnPracticeCompletionScreen({super.key, required this.level});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
@ -54,7 +55,7 @@ class LearnPracticeCompletionScreen
);
Widget _buildTitle() => Text(
'Yay, youve completed A1 ',
'Yay, youve completed $level ',
style: style25DG600,
textAlign: TextAlign.center,
);

View File

@ -3,6 +3,7 @@ import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/cancel_learn_practice_sheet.dart';
import '../../../widgets/custom_elevated_button.dart';
@ -10,7 +11,19 @@ import '../../../widgets/small_app_bar.dart';
import '../../../widgets/speaking_partner_image.dart';
class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeIntroScreen({super.key});
final String title;
final String label;
final String? level;
final String subtitle;
final LearnPractices practice;
const LearnPracticeIntroScreen(
{super.key,
this.level,
required this.label,
required this.title,
required this.subtitle,
required this.practice});
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
await viewModel.stopRecording();
@ -114,21 +127,44 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
List<Widget> _buildPracticeColumnChildren(LearnPracticeViewModel viewModel) =>
[
verticalSpaceMassive,
_buildImage(),
_buildImageState(),
verticalSpaceMedium,
_buildPartnerName(),
_buildPartnerNameState(),
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubtitle()
];
Widget _buildImage() => const SpeakingPartnerImage(
Widget _buildImageState() =>
level != null ? _buildCourseSpeakingPartnerImage() : _buildImage(75);
Widget _buildCourseSpeakingPartnerImage() => CircleAvatar(
radius: 75,
backgroundColor: kcPrimaryColorLight,
child: _buildCourseSpeakingPartnerText(),
);
Widget _buildCourseSpeakingPartnerText() => Text(
level?.toUpperCase() ?? '',
style: style40P900,
);
Widget _buildImage(double radius) => SpeakingPartnerImage(radius: radius);
Widget _buildPartnerNameState() =>
level != null ? _buildCourseSpeakingPartner() : _buildPartnerName();
Widget _buildCourseSpeakingPartner() => Row(
mainAxisSize: MainAxisSize.min,
children: _buildCourseSpeakingPartnerChildren(),
);
List<Widget> _buildCourseSpeakingPartnerChildren() =>
[_buildImage(15), horizontalSpaceTiny, _buildPartnerName()];
Widget _buildPartnerName() => Text.rich(
TextSpan(text: 'Dawit', style: style14DG600, children: [
TextSpan(text: 'Daniel', style: style14DG600, children: [
TextSpan(
text: ' - Your Speaking Partner',
style: style14MG400,
@ -137,13 +173,13 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
);
Widget _buildTitle() => Text(
'Let\'s practice what you just learnt!',
title,
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Ill ask you a few questions, and you can respond naturally.',
subtitle,
maxLines: 1,
style: style14DG400,
textAlign: TextAlign.center,
@ -158,8 +194,8 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: label,
borderRadius: 12,
text: 'Practice',
foregroundColor: kcWhite,
onTap: () => viewModel.goTo(1),
backgroundColor: kcPrimaryColor,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/learn_practice_tip_section.dart';
import 'package:yimaru_app/ui/widgets/learn_practice_results_wrapper.dart';
@ -12,9 +13,19 @@ import '../../../widgets/small_app_bar.dart';
class LearnPracticeResultScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeResultScreen({super.key});
final LearnPractices practice;
void _navigate(LearnPracticeViewModel viewModel) async =>
const LearnPracticeResultScreen({super.key, required this.practice});
Future<void> _navigate(LearnPracticeViewModel viewModel) async {
if (practice == LearnPractices.course) {
viewModel.goTo(5);
} else {
viewModel.pop();
}
}
Future<void> _retry(LearnPracticeViewModel viewModel) async =>
await viewModel.reset();
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
@ -145,8 +156,8 @@ class LearnPracticeResultScreen
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: () => viewModel.goTo(5),
backgroundColor: kcPrimaryColor,
onTap: () async => await _navigate(viewModel),
);
Widget _buildPracticeAgainButton(LearnPracticeViewModel viewModel) =>
@ -157,6 +168,6 @@ class LearnPracticeResultScreen
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
onTap: () => _navigate(viewModel),
onTap: () async => await _retry(viewModel),
);
}

View File

@ -98,9 +98,9 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
_buildCountryDropDown(viewModel),
verticalSpaceMedium,
_buildRegionFormState(viewModel),
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&
viewModel.focusRegion)
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&
viewModel.focusRegion)
verticalSpaceTiny,
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&

View File

@ -26,10 +26,6 @@ class ProfileViewModel extends ReactiveViewModel {
final _googleAuthService = locator<GoogleAuthService>();
final _phoneCallerService = locator<PhoneCallerService>();
final _urlLauncherService = locator<UrlLauncherService>();
final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>();
@ -68,13 +64,6 @@ class ProfileViewModel extends ReactiveViewModel {
}
}
// Launch telegram
Future<void> launchTelegram() =>
_urlLauncherService.launchUri(kTelegramSupport);
// Call support
Future<void> callSupport() => _phoneCallerService.call(kPhoneSupport);
// Dialog
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(

View File

@ -66,13 +66,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
content: _buildImagePicker(context: context, viewModel: viewModel),
);
void _checkRegion(ProfileDetailViewModel viewModel){
bool region = viewModel.checkRegion(region:viewModel.user?.region ?? 'Addis Ababa',country:viewModel.user?.country ?? 'Ethiopia' );
if(region){
void _checkRegion(ProfileDetailViewModel viewModel) {
bool region = viewModel.checkRegion(
region: viewModel.user?.region ?? 'Addis Ababa',
country: viewModel.user?.country ?? 'Ethiopia');
if (region) {
viewModel.setSelectedRegion(viewModel.user?.region ?? 'Addis Ababa');
}else{
} else {
regionController.text = viewModel.user?.region ?? '';
}
}
@ -549,10 +550,12 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
_buildRegionFormState(viewModel),
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&
viewModel.focusRegion) verticalSpaceTiny,
viewModel.focusRegion)
verticalSpaceTiny,
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&
viewModel.focusRegion) _buildRegionValidatorWrapper(viewModel),
viewModel.focusRegion)
_buildRegionValidatorWrapper(viewModel),
];
Widget _buildRegionFormFieldLabel() => CustomFormLabel(
@ -574,14 +577,15 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
onChanged: (value) =>
viewModel.setSelectedRegion(value ?? 'Addis Ababa'));
Widget _buildRegionFormField(ProfileDetailViewModel viewModel) => TextFormField(
controller: regionController,
onTap: viewModel.setRegionFocus,
decoration: inputDecoration(
hint: 'Enter Your City',
focus: viewModel.focusRegion,
filled: regionController.text.isNotEmpty),
);
Widget _buildRegionFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
controller: regionController,
onTap: viewModel.setRegionFocus,
decoration: inputDecoration(
hint: 'Enter Your City',
focus: viewModel.focusRegion,
filled: regionController.text.isNotEmpty),
);
Widget _buildRegionValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasRegionValidationMessage
@ -589,10 +593,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
: Container();
Widget _buildRegionValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.regionValidationMessage!,
style: style12R700,
);
viewModel.regionValidationMessage!,
style: style12R700,
);
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
Column(
@ -608,7 +611,6 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
_buildOccupationDropdownLabel(),
verticalSpaceSmall,
_buildOccupationDropdown(viewModel)
];
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
@ -625,9 +627,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
onChanged: (value) => viewModel.setSelectedOccupation(
value ?? 'Students (High school & University)'));
Icon _buildSearchIcon() => const Icon(
Icons.search,
color: kcPrimaryColor,
);
Icons.search,
color: kcPrimaryColor,
);
Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),

View File

@ -18,7 +18,6 @@ const String RegionValueKey = 'region';
const String PhoneNumberValueKey = 'phoneNumber';
const String LastNameValueKey = 'lastName';
const String FirstNameValueKey = 'firstName';
const String OccupationValueKey = 'occupation';
final Map<String, TextEditingController>
_ProfileDetailViewTextEditingControllers = {};
@ -32,7 +31,6 @@ final Map<String, String? Function(String?)?>
PhoneNumberValueKey: FormValidator.validatePhoneNumberForm,
LastNameValueKey: FormValidator.validateForm,
FirstNameValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm,
};
mixin $ProfileDetailView {
@ -46,15 +44,12 @@ mixin $ProfileDetailView {
_getFormTextEditingController(LastNameValueKey);
TextEditingController get firstNameController =>
_getFormTextEditingController(FirstNameValueKey);
TextEditingController get occupationController =>
_getFormTextEditingController(OccupationValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get regionFocusNode => _getFormFocusNode(RegionValueKey);
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey);
FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey);
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
TextEditingController _getFormTextEditingController(
String key, {
@ -85,7 +80,6 @@ mixin $ProfileDetailView {
phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
@ -102,7 +96,6 @@ mixin $ProfileDetailView {
phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
@ -117,7 +110,6 @@ mixin $ProfileDetailView {
PhoneNumberValueKey: phoneNumberController.text,
LastNameValueKey: lastNameController.text,
FirstNameValueKey: firstNameController.text,
OccupationValueKey: occupationController.text,
}),
);
@ -165,8 +157,6 @@ extension ValueProperties on FormStateHelper {
this.formValueMap[PhoneNumberValueKey] as String?;
String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?;
String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?;
String? get occupationValue =>
this.formValueMap[OccupationValueKey] as String?;
set emailValue(String? value) {
this.setData(
@ -226,18 +216,6 @@ extension ValueProperties on FormStateHelper {
}
}
set occupationValue(String? value) {
this.setData(
this.formValueMap..addAll({OccupationValueKey: value}),
);
if (_ProfileDetailViewTextEditingControllers.containsKey(
OccupationValueKey)) {
_ProfileDetailViewTextEditingControllers[OccupationValueKey]?.text =
value ?? '';
}
}
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
@ -253,9 +231,6 @@ extension ValueProperties on FormStateHelper {
bool get hasFirstName =>
this.formValueMap.containsKey(FirstNameValueKey) &&
(firstNameValue?.isNotEmpty ?? false);
bool get hasOccupation =>
this.formValueMap.containsKey(OccupationValueKey) &&
(occupationValue?.isNotEmpty ?? false);
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
@ -267,8 +242,6 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false;
bool get hasFirstNameValidationMessage =>
this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false;
bool get hasOccupationValidationMessage =>
this.fieldsValidationMessages[OccupationValueKey]?.isNotEmpty ?? false;
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
@ -280,8 +253,6 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey];
String? get firstNameValidationMessage =>
this.fieldsValidationMessages[FirstNameValueKey];
String? get occupationValidationMessage =>
this.fieldsValidationMessages[OccupationValueKey];
}
extension Methods on FormStateHelper {
@ -295,8 +266,6 @@ extension Methods on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey] = validationMessage;
void setFirstNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[FirstNameValueKey] = validationMessage;
void setOccupationValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[OccupationValueKey] = validationMessage;
/// Clears text input fields on the Form
void clearForm() {
@ -305,7 +274,6 @@ extension Methods on FormStateHelper {
phoneNumberValue = '';
lastNameValue = '';
firstNameValue = '';
occupationValue = '';
}
/// Validates text input fields on the Form
@ -316,7 +284,6 @@ extension Methods on FormStateHelper {
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
});
}
}
@ -341,5 +308,4 @@ void updateValidationData(FormStateHelper model) =>
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
});

View File

@ -57,13 +57,11 @@ class ProfileDetailViewModel extends ReactiveViewModel
bool get focusEmail => _focusEmail;
// Occupation
String _selectedOccupation = 'Students (High school & University)';
String get selectedOccupation => _selectedOccupation;
// Country
String _selectedCountry = 'Ethiopia';
@ -117,18 +115,16 @@ class ProfileDetailViewModel extends ReactiveViewModel
rebuildUi();
}
// Occupation
List<String> getOccupations() => [
'Students (High school & University)',
'Job Seekers / Fresh Graduates',
'Working Professionals (Corporate/Office)',
'Government & NGO Workers',
'Entrepreneurs & Small Business Owners',
'Hospitality & Tourism Workers',
'Freelancers / Remote Workers (Digital Economy)'
];
'Students (High school & University)',
'Job Seekers / Fresh Graduates',
'Working Professionals (Corporate/Office)',
'Government & NGO Workers',
'Entrepreneurs & Small Business Owners',
'Hospitality & Tourism Workers',
'Freelancers / Remote Workers (Digital Economy)'
];
void setSelectedOccupation(String value) {
_selectedOccupation = value;
@ -137,158 +133,158 @@ class ProfileDetailViewModel extends ReactiveViewModel
// Country
List<String> getCountries() => [
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Argentina",
"Armenia",
"Australia",
"Austria",
"Azerbaijan",
"Bahrain",
"Bangladesh",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bhutan",
"Bolivia",
"Bosnia and Herzegovina",
"Botswana",
"Brazil",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cambodia",
"Cameroon",
"Canada",
"Chad",
"Chile",
"China",
"Colombia",
"Comoros",
"Congo",
"Costa Rica",
"Croatia",
"Cuba",
"Cyprus",
"Czech Republic",
"Denmark",
"Djibouti",
"Dominican Republic",
"Ecuador",
"Egypt",
"El Salvador",
"Eritrea",
"Estonia",
"Eswatini",
"Ethiopia",
"Finland",
"France",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Greece",
"Guatemala",
"Guinea",
"Haiti",
"Honduras",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Israel",
"Italy",
"Jamaica",
"Japan",
"Jordan",
"Kazakhstan",
"Kenya",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Liberia",
"Libya",
"Lithuania",
"Luxembourg",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Mexico",
"Moldova",
"Monaco",
"Mongolia",
"Morocco",
"Mozambique",
"Myanmar",
"Namibia",
"Nepal",
"Netherlands",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"North Korea",
"Norway",
"Oman",
"Pakistan",
"Panama",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Qatar",
"Romania",
"Russia",
"Rwanda",
"Saudi Arabia",
"Senegal",
"Serbia",
"Singapore",
"Slovakia",
"Slovenia",
"Somalia",
"South Africa",
"South Korea",
"Spain",
"Sri Lanka",
"Sudan",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Tunisia",
"Turkey",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Venezuela",
"Vietnam",
"Yemen",
"Zambia",
"Zimbabwe"
];
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Argentina",
"Armenia",
"Australia",
"Austria",
"Azerbaijan",
"Bahrain",
"Bangladesh",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bhutan",
"Bolivia",
"Bosnia and Herzegovina",
"Botswana",
"Brazil",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cambodia",
"Cameroon",
"Canada",
"Chad",
"Chile",
"China",
"Colombia",
"Comoros",
"Congo",
"Costa Rica",
"Croatia",
"Cuba",
"Cyprus",
"Czech Republic",
"Denmark",
"Djibouti",
"Dominican Republic",
"Ecuador",
"Egypt",
"El Salvador",
"Eritrea",
"Estonia",
"Eswatini",
"Ethiopia",
"Finland",
"France",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Greece",
"Guatemala",
"Guinea",
"Haiti",
"Honduras",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Israel",
"Italy",
"Jamaica",
"Japan",
"Jordan",
"Kazakhstan",
"Kenya",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Liberia",
"Libya",
"Lithuania",
"Luxembourg",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Mexico",
"Moldova",
"Monaco",
"Mongolia",
"Morocco",
"Mozambique",
"Myanmar",
"Namibia",
"Nepal",
"Netherlands",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"North Korea",
"Norway",
"Oman",
"Pakistan",
"Panama",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Qatar",
"Romania",
"Russia",
"Rwanda",
"Saudi Arabia",
"Senegal",
"Serbia",
"Singapore",
"Slovakia",
"Slovenia",
"Somalia",
"South Africa",
"South Korea",
"Spain",
"Sri Lanka",
"Sudan",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Tunisia",
"Turkey",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Venezuela",
"Vietnam",
"Yemen",
"Zambia",
"Zimbabwe"
];
void setSelectedCountry(String value) {
_selectedCountry = value;
@ -305,24 +301,24 @@ class ProfileDetailViewModel extends ReactiveViewModel
// Region
List<String> getRegions() => [
'Addis Ababa',
'Afar',
'Amhara',
'Benishangul-Gumuz',
'Central Ethiopia',
'Dire Dawa',
'Gambela',
'Harari',
'Oromia',
'Sidama',
'Somali',
'South Ethiopia',
'South West Ethiopia Peoples',
'Tigray',
];
'Addis Ababa',
'Afar',
'Amhara',
'Benishangul-Gumuz',
'Central Ethiopia',
'Dire Dawa',
'Gambela',
'Harari',
'Oromia',
'Sidama',
'Somali',
'South Ethiopia',
'South West Ethiopia Peoples',
'Tigray',
];
bool checkRegion({required String region,required String country}){
if(country == 'Ethiopia'){
bool checkRegion({required String region, required String country}) {
if (country == 'Ethiopia') {
return getRegions().contains(region);
}
return false;

View File

@ -130,7 +130,6 @@ class RegisterViewModel extends ReactiveViewModel
_length = false;
}
if (password == confirmPassword) {
_passwordMatch = true;
} else {
@ -237,6 +236,7 @@ class RegisterViewModel extends ReactiveViewModel
}
void goBack() {
print('HERE');
if (_currentPage == 1) {
_currentPage = 0;
rebuildUi();

View File

@ -213,7 +213,6 @@ class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey,
label: '8 characters minimum');
Widget _buildPasswordMatchValidator(RegisterViewModel viewModel) =>
ValidatorListTile(
backgroundColor:

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/widgets/circular_icon.dart';
import '../../common/app_colors.dart';
@ -91,20 +92,16 @@ class TelegramSupportView extends StackedView<TelegramSupportViewModel> {
Widget _buildIcon() =>
const CircularIcon(icon: Icons.telegram, size: 50, color: kcSkyBlue);
Widget _buildTitle() => const Text(
Widget _buildTitle() => Text(
'Join Yimaru Academy on Telegram',
style: style25DG600,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
);
Widget _buildSubtitle() => const Text(
Widget _buildSubtitle() => Text(
'Connect with our support team instantly on Telegram for quick assistance and community updates',
style: style14MG400,
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
Widget _buildLowerColumn(TelegramSupportViewModel viewModel) => Column(
@ -123,31 +120,24 @@ class TelegramSupportView extends StackedView<TelegramSupportViewModel> {
];
Widget _buildContinueButton(TelegramSupportViewModel viewModel) =>
const CustomElevatedButton(
CustomElevatedButton(
height: 55,
borderRadius: 12,
leadingIcon: Icons.telegram,
text: 'Open in Telegram',
foregroundColor: kcWhite,
leadingIcon: Icons.telegram,
backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.launchTelegram(),
);
Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildSearchText() => const Text.rich(
TextSpan(
text: 'Search for',
style: TextStyle(
color: kcDarkGrey,
),
children: [
TextSpan(
text: ' @YimaruSupport',
style: TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
)
]),
Widget _buildSearchText() => Text.rich(
TextSpan(text: 'Search for', style: style14DG500, children: [
TextSpan(
style: style14P600,
text: ' $kTelegramSupport',
)
]),
);
}

View File

@ -2,8 +2,19 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../services/url_launcher_service.dart';
import '../../common/app_constants.dart';
class TelegramSupportViewModel extends BaseViewModel {
// Dependency injection
final _navigationService = locator<NavigationService>();
final _urlLauncherService = locator<UrlLauncherService>();
// Launch telegram
Future<void> launchTelegram() =>
_urlLauncherService.launchUri(kTelegramSupport);
// Navigation
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
class AppBarPattern extends StatelessWidget {
const AppBarPattern({
super.key,
});
@override
Widget build(BuildContext context) => _buildDecorationImageWrapper();
Widget _buildDecorationImageWrapper() => ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
child: _buildDecorationImage(),
);
Widget _buildDecorationImage() => SizedBox(
width: double.maxFinite,
height: double.maxFinite,
child: _buildPatternWrapper(),
);
Widget _buildPatternWrapper() => SizedBox(
width: double.maxFinite,
height: double.maxFinite,
child: _buildPatternMask(),
);
Widget _buildPatternMask() => ShaderMask(
shaderCallback: (Rect bounds) => const LinearGradient(
colors: [kcWhite, kcWhite],
).createShader(bounds),
blendMode: BlendMode.modulate,
child: _buildPattern(),
);
Widget _buildPattern() => Image.asset(
'assets/images/pattern.png',
fit: BoxFit.cover,
);
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/widgets/app_bar_pattern.dart';
import 'package:yimaru_app/ui/widgets/language_button.dart';
class LargeAppBar extends StatelessWidget {
@ -18,12 +19,11 @@ class LargeAppBar extends StatelessWidget {
required this.showLanguageSelection});
@override
Widget build(BuildContext context) => _buildAppBarWrapper();
Widget build(BuildContext context) => _buildStackWrapper();
Widget _buildAppBarWrapper() => Container(
Widget _buildStackWrapper() => Container(
height: 125,
width: double.maxFinite,
alignment: Alignment.bottomCenter,
decoration: const BoxDecoration(
color: kcPrimaryColor,
borderRadius: BorderRadius.only(
@ -31,6 +31,18 @@ class LargeAppBar extends StatelessWidget {
bottomRight: Radius.circular(24),
),
),
child: _buildStack(),
);
Widget _buildStack() => Stack(
children: [ _buildPattern(),_buildAppBarWrapper()],
);
Widget _buildAppBarWrapper() => Container(
color: kcTransparent,
width: double.maxFinite,
height: double.maxFinite,
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.only(bottom: 25, right: 15),
child: _buildAppBarItems(),
);
@ -74,4 +86,6 @@ class LargeAppBar extends StatelessWidget {
Icons.close,
color: kcWhite,
);
Widget _buildPattern() => const AppBarPattern();
}

View File

@ -143,7 +143,7 @@ class LearnCourseTile extends ViewModelWidget<LearnCourseViewModel> {
height: 15,
borderRadius: 12,
onTap: onViewTap,
text: 'View Courses',
text: 'View Course',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
@ -158,7 +158,7 @@ class LearnCourseTile extends ViewModelWidget<LearnCourseViewModel> {
height: 15,
borderRadius: 12,
onTap: onPracticeTap,
text: 'Take Practices',
text: 'Take Practice',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,

View File

@ -12,12 +12,16 @@ import 'custom_elevated_button.dart';
import 'custom_linear_progress_indicator.dart';
class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
final int index;
final LearnLesson lesson;
final GestureTapCallback? onLessonTap;
final GestureTapCallback? onPracticeTap;
const LearnLessonTile(
{super.key, this.onLessonTap, this.onPracticeTap, required this.lesson});
const LearnLessonTile({super.key,
this.onLessonTap,
this.onPracticeTap,
required this.index,
required this.lesson});
@override
Widget build(BuildContext context, LearnLessonViewModel viewModel) =>
@ -52,8 +56,8 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
expandedAlignment: Alignment.centerLeft,
backgroundColor: kcPrimaryColor.withOpacity(0.1),
controlAffinity: ListTileControlAffinity.trailing,
tilePadding: const EdgeInsets.fromLTRB(15, 15, 15, 0),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
tilePadding: const EdgeInsets.fromLTRB(15, 15, 15, 15),
collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1),
childrenPadding: const EdgeInsets.fromLTRB(15, 0, 15, 15),
// enabled: (lesson.access?.isAccessible ?? false),
@ -130,11 +134,12 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
Widget _buildActionButtons(LearnLessonViewModel viewModel) => Row(
mainAxisAlignment: MainAxisAlignment.end,
children: __buildActionButtonChildren(viewModel),
children: _buildActionButtonChildren(viewModel),
);
List<Widget> __buildActionButtonChildren(LearnLessonViewModel viewModel) => [
_buildPracticeButtonWrapper(viewModel),
List<Widget> _buildActionButtonChildren(LearnLessonViewModel viewModel) => [
if (index != viewModel.lessons.length - 1)
_buildPracticeButtonWrapper(viewModel),
horizontalSpaceSmall,
_buildLessonButtonWrapper(viewModel)
];

View File

@ -15,7 +15,7 @@ class LearnPracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> {
_buildColumnWrapper(viewModel);
Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => SizedBox(
height: 100,
height: 125,
width: double.maxFinite,
child: _buildColumn(viewModel),
);
@ -30,7 +30,8 @@ class LearnPracticeResultCard extends ViewModelWidget<LearnPracticeViewModel> {
[_buildQuestion(viewModel), verticalSpaceSmall, _buildRow()];
Widget _buildQuestion(LearnPracticeViewModel viewModel) => Text(
answer['sample_text_answer'],
answer['question_text'],
maxLines: 2,
style: style14DG400,
);

View File

@ -14,8 +14,7 @@ class MiniThumbnail extends StatelessWidget {
Widget build(BuildContext context) => _buildWrapper();
Widget _buildWrapper() => SizedBox(
width: 75,
height: double.maxFinite,
width: 80,
child: _buildLeadingClipper(),
);
@ -40,15 +39,17 @@ class MiniThumbnail extends StatelessWidget {
: _buildNetworkImage();
Widget _buildNetworkImage() => CachedNetworkImage(
fit: BoxFit.cover,
imageUrl: thumbnail,
fit: BoxFit.fill,
width: double.maxFinite,
height: double.maxFinite,
);
Widget _buildLocalImage() => Image.asset(
thumbnail,
fit: BoxFit.fill,
fit: BoxFit.cover,
width: double.maxFinite,
height: double.maxFinite,
);
Widget _buildPlayButtonWrapper() => Align(

View File

@ -1,5 +1,5 @@
name: yimaru_app
version: 0.1.13+15
version: 0.1.14+16
publish_to: 'none'
description: A new Flutter project.