Merge branch 'release/0.1.5'

-fix(onboarding): Remove birthday and fix other fields
This commit is contained in:
BisratHailu 2026-04-25 11:42:46 +03:00
commit 2d5039486f
23 changed files with 663 additions and 590 deletions

View File

@ -14,11 +14,9 @@ class VoiceRecorderService with ListenableServiceMixin {
WaveformRecorderController get waveController => _waveController;
bool _isRecording = false;
bool get isRecording => _isRecording;
bool get isRecording => _isRecording;
// Start voice recording
Future<void> startRecording() async {
@ -41,5 +39,4 @@ class VoiceRecorderService with ListenableServiceMixin {
if (file == null) return null;
return file.path;
}
}

View File

@ -60,8 +60,8 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
required LearnPracticeViewModel viewModel}) =>
PopScope(
canPop: false,
onPopInvokedWithResult: (value, data)
async=>await _showSheet(context: context, viewModel: viewModel),
onPopInvokedWithResult: (value, data) async =>
await _showSheet(context: context, viewModel: viewModel),
child: _buildScaffoldWrapper(viewModel));
Widget _buildSheet(LearnPracticeViewModel viewModel) =>

View File

@ -83,7 +83,7 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Voice? _playing;
Voice? get playing =>_playing;
Voice? get playing => _playing;
// Learn practices
List<LearnPractice> _practices = [];
@ -126,7 +126,6 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Future<void> _startRecording() async =>
await _voiceRecorderService.startRecording();
// Play practice audio
void _listenToAudio() {
_audioPlayerService.durationStream.listen((dur) {
@ -151,45 +150,37 @@ class LearnPracticeViewModel extends ReactiveViewModel {
await _audioPlayerService.playUrl(question.voicePrompt ?? '');
}
Future<void> playResult({required Map<String,dynamic> answer,required Voice voice})async{
setBusyObject(
playing: voice,
object: answer['busy_object']);
await playAudio(voice: voice,answer: answer);
Future<void> playResult(
{required Map<String, dynamic> answer, required Voice voice}) async {
setBusyObject(playing: voice, object: answer['busy_object']);
await playAudio(voice: voice, answer: answer);
}
Future<void> playAudio({required Map<String,dynamic> answer,required Voice voice}) async =>
await runBusyFuture(_playAudio(voice:voice,answer:answer),
Future<void> playAudio(
{required Map<String, dynamic> answer, required Voice voice}) async =>
await runBusyFuture(_playAudio(voice: voice, answer: answer),
busyObject: answer['busy_object']);
Future<void> _playAudio({required Map<String,dynamic> answer,required Voice voice}) async {
if(voice == Voice.recorded){
await _audioPlayerService
.playLocal(answer['recorded_voice_answer']);
}else{
Future<void> _playAudio(
{required Map<String, dynamic> answer, required Voice voice}) async {
if (voice == Voice.recorded) {
await _audioPlayerService.playLocal(answer['recorded_voice_answer']);
} else {
await _audioPlayerService.playUrl(answer['sample_voice_answer']);
}
}
Future<void> pauseAudio() async {
await _audioPlayerService.pause();
}
// Set busy object
void setBusyObject({required String object,required Voice playing}) {
void setBusyObject({required String object, required Voice playing}) {
_playing = playing;
_busyObject = object;
rebuildUi();
}
// Dialogue
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(
@ -219,13 +210,14 @@ class LearnPracticeViewModel extends ReactiveViewModel {
}
}
Future<void> nextQuestion({required int index,required LearnQuestion question}) async {
Future<void> nextQuestion(
{required int index, required LearnQuestion question}) async {
await stopRecording();
_answers.add({
'busy_object': question.id.toString(),
'sample_text_answer': question.audioCorrectAnswerText,
'sample_voice_answer': question.sampleAnswerVoicePrompt,
'recorded_voice_answer' : _voiceRecorderService.getRecordedAudio() ,
'recorded_voice_answer': _voiceRecorderService.getRecordedAudio(),
});
if (index != _questions.length) {
_questionSetController.nextPage(
@ -266,6 +258,5 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Future<void> _getLearnPracticeQuestions(int id) async {
_questions = await _apiService.getLearnQuestions(id);
}
}

View File

@ -31,11 +31,10 @@ class InteractLearnPracticeScreen
}
void _start(LearnPracticeViewModel viewModel) =>
viewModel.playVoicePrompt(question);
viewModel.playVoicePrompt(question);
Future<void> _stop(LearnPracticeViewModel viewModel) async =>
await viewModel.nextQuestion(index: index,question: question);
await viewModel.nextQuestion(index: index, question: question);
Future<void> _showSheet(
{required BuildContext context,
@ -96,25 +95,30 @@ class InteractLearnPracticeScreen
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildAppBarWrapper(context: context,viewModel: viewModel),
_buildAppBarWrapper(context: context, viewModel: viewModel),
_buildSpeakingIndicatorWrapper(viewModel),
_buildLowerButtonsSectionWrapper(context: context, viewModel: viewModel)
];
Widget _buildAppBarWrapper( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => Column(
Widget _buildAppBarWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
children: [
verticalSpaceMedium,
_buildAppBar(context: context,viewModel: viewModel),
_buildAppBar(context: context, viewModel: viewModel),
verticalSpaceMedium,
],
);
Widget _buildAppBar( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => SmallAppBar(
showBackButton: true,
onTap: () async => await _showSheet(context: context,viewModel: viewModel),
title: 'Practice Speaking ($index/${viewModel.questions.length})');
Widget _buildAppBar(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SmallAppBar(
showBackButton: true,
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
title: 'Practice Speaking ($index/${viewModel.questions.length})');
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
Column(

View File

@ -17,12 +17,11 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
viewModel.pop();
viewModel.pop();
viewModel.stopRecording();
}
Future<void> _showSheet(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -32,41 +31,51 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(context: context,viewModel: viewModel);
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => Scaffold(
Widget _buildScaffoldWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold( {required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SafeArea(child: _buildColumnWrapper(context: context,viewModel: viewModel));
Widget _buildScaffold(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SafeArea(
child: _buildColumnWrapper(context: context, viewModel: viewModel));
Widget _buildColumnWrapper( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => Padding(
Widget _buildColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(context: context,viewModel: viewModel),
child: _buildColumn(context: context, viewModel: viewModel),
);
Widget _buildColumn( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => Column(
Widget _buildColumn(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
children: [
verticalSpaceMedium,
_buildAppBar(context: context,viewModel: viewModel),
_buildAppBar(context: context, viewModel: viewModel),
verticalSpaceMedium,
_buildBodyColumnWrapper(viewModel),
],
);
Widget _buildAppBar( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => SmallAppBar(
Widget _buildAppBar(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SmallAppBar(
showBackButton: true,
title: 'Practice Speaking',
onTap: () async => await _showSheet(context: context,viewModel: viewModel),
);
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
);
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
CancelLearnPracticeSheet(

View File

@ -38,7 +38,7 @@ class LearnPracticeQuestionsScreen
PageView(
controller: viewModel.questionController,
physics: const NeverScrollableScrollPhysics(),
children: _buildScreens(index:index,question: question),
children: _buildScreens(index: index, question: question),
);
List<Widget> _buildScreens({
@ -46,20 +46,25 @@ class LearnPracticeQuestionsScreen
required LearnQuestion question,
}) =>
[
_buildStartLearnPracticeScreen(index:index,question: question),
_buildInteractLearnPracticeScreen(index:index,question: question)
_buildStartLearnPracticeScreen(index: index, question: question),
_buildInteractLearnPracticeScreen(index: index, question: question)
];
Widget _buildStartLearnPracticeScreen({
required int index,
required LearnQuestion question,}
) => StartLearnPracticeScreen(
required int index,
required LearnQuestion question,
}) =>
StartLearnPracticeScreen(
index: index,
question: question,
question: question,
);
Widget _buildInteractLearnPracticeScreen({
required int index,
required LearnQuestion question,}) =>
InteractLearnPracticeScreen(index: index,question: question,);
required LearnQuestion question,
}) =>
InteractLearnPracticeScreen(
index: index,
question: question,
);
}

View File

@ -12,11 +12,9 @@ import '../../../widgets/small_app_bar.dart';
class LearnPracticeResultScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeResultScreen({super.key});
void _navigate(LearnPracticeViewModel viewModel){
void _navigate(LearnPracticeViewModel viewModel) {
viewModel.questionSetController.jumpToPage(0);
viewModel.goTo(0);
}
@ -25,12 +23,11 @@ class LearnPracticeResultScreen
viewModel.pop();
viewModel.pop();
viewModel.stopRecording();
}
Future<void> _showSheet(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -40,42 +37,57 @@ class LearnPracticeResultScreen
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(context: context,viewModel: viewModel);
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper({required BuildContext context,
required LearnPracticeViewModel viewModel}) => Scaffold(
Widget _buildScaffoldWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold({required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SafeArea(child: _buildBodyColumnWrapper(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(
Widget _buildBodyColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyColumn(context: context,viewModel: viewModel),
child: _buildBodyColumn(context: context, viewModel: viewModel),
);
Widget _buildBodyColumn({required BuildContext context,
required LearnPracticeViewModel viewModel}) => Column(
children: _buildBodyColumnChildren(context: context,viewModel: viewModel),
Widget _buildBodyColumn(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
children:
_buildBodyColumnChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyColumnChildren({required BuildContext context,
required LearnPracticeViewModel viewModel}) => [
List<Widget> _buildBodyColumnChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
verticalSpaceMedium,
_buildAppBar(context: context,viewModel: viewModel),
_buildAppBar(context: context, viewModel: viewModel),
verticalSpaceMedium,
_buildBodyWrapper(viewModel)
];
Widget _buildAppBar({required BuildContext context,
required LearnPracticeViewModel viewModel}) => SmallAppBar(
Widget _buildAppBar(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SmallAppBar(
title: 'Result',
showBackButton: true,
onTap: () async => await _showSheet(context: context,viewModel: viewModel),
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
);
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
@ -112,7 +124,7 @@ class LearnPracticeResultScreen
Widget _buildResultsSection(LearnPracticeViewModel viewModel) =>
const LearnPracticeResultsWrapper();
Widget _buildLearnPracticeTipSection() => const LearnPracticeTipSection();
Widget _buildLearnPracticeTipSection() => const LearnPracticeTipSection();
Widget _buildLowerButtonsSection(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
@ -148,6 +160,5 @@ class LearnPracticeResultScreen
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
onTap: () => _navigate(viewModel),
);
}

View File

@ -13,7 +13,8 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
final int index;
final LearnQuestion question;
const StartLearnPracticeScreen({super.key,required this.index,required this.question});
const StartLearnPracticeScreen(
{super.key, required this.index, required this.question});
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
viewModel.pop();
@ -26,8 +27,8 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
}
Future<void> _showSheet(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -37,51 +38,68 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(context: context,viewModel: viewModel);
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => Scaffold(
Widget _buildScaffoldWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold( {required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SafeArea(child: _buildBodyColumnWrapper(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(
Widget _buildBodyColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyColumn(context: context,viewModel: viewModel),
child: _buildBodyColumn(context: context, viewModel: viewModel),
);
Widget _buildBodyColumn( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => Column(
Widget _buildBodyColumn(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(context: context,viewModel: viewModel),
children:
_buildBodyColumnChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyColumnChildren( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => [
_buildAppBarWrapper(context: context,viewModel: viewModel),
List<Widget> _buildBodyColumnChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildAppBarWrapper(context: context, viewModel: viewModel),
_buildStartButtonWrapper(viewModel),
_buildLowerButtonsSectionWrapper(viewModel)
];
Widget _buildAppBarWrapper( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => Column(
Widget _buildAppBarWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
children: [
verticalSpaceMedium,
_buildAppBar(context: context,viewModel: viewModel),
_buildAppBar(context: context, viewModel: viewModel),
verticalSpaceMedium,
],
);
Widget _buildAppBar( {required BuildContext context,
required LearnPracticeViewModel viewModel}) => SmallAppBar(
showBackButton: true,
onTap: () async => await _showSheet(context: context,viewModel: viewModel),
title: 'Practice Speaking ($index/${viewModel.questions.length})');
Widget _buildAppBar(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SmallAppBar(
showBackButton: true,
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
title: 'Practice Speaking ($index/${viewModel.questions.length})');
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
CancelLearnPracticeSheet(

View File

@ -3,7 +3,6 @@ import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/age_group_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/birthday_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/challenge_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/country_region_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/educational_background_form_screen.dart';
@ -22,6 +21,7 @@ import 'onboarding_view.form.dart';
FormTextField(name: 'topic', validator: FormValidator.validateForm),
FormTextField(
name: 'fullName', validator: FormValidator.validateFullNameForm),
FormTextField(name: 'region', validator: FormValidator.validateForm),
FormTextField(name: 'challenge', validator: FormValidator.validateForm),
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
FormTextField(name: 'languageGoal', validator: FormValidator.validateForm),
@ -50,25 +50,23 @@ class OnboardingView extends StackedView<OnboardingViewModel>
} else if (viewModel.currentPage == 1) {
viewModel.resetGenderFormScreen();
} else if (viewModel.currentPage == 2) {
viewModel.resetBirthdayFormScreen();
} else if (viewModel.currentPage == 3) {
viewModel.resetAgeGroupFormScreen();
} else if (viewModel.currentPage == 4) {
} else if (viewModel.currentPage == 3) {
viewModel.resetEducationalBackgroundFormScreen();
} else if (viewModel.currentPage == 5) {
} else if (viewModel.currentPage == 4) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
} else if (viewModel.currentPage == 6) {
} else if (viewModel.currentPage == 5) {
viewModel.resetCountryRegionFormScreen();
} else if (viewModel.currentPage == 7) {
} else if (viewModel.currentPage == 6) {
viewModel.resetLearningGoalFormScreen();
} else if (viewModel.currentPage == 8) {
} else if (viewModel.currentPage == 7) {
languageGoalController.clear();
viewModel.resetLanguageGoalFormScreen();
} else if (viewModel.currentPage == 9) {
} else if (viewModel.currentPage == 8) {
challengeController.clear();
viewModel.resetChallengeFormScreen();
} else if (viewModel.currentPage == 10) {
} else if (viewModel.currentPage == 9) {
topicController.clear();
viewModel.resetTopicFormScreen();
}
@ -117,7 +115,6 @@ class OnboardingView extends StackedView<OnboardingViewModel>
List<Widget> _buildScreens() => [
_buildFullNameForm(),
_buildGenderForm(),
_buildBirthdayForm(),
_buildAgeGroupForm(),
_buildEducationalBackgroundForm(),
_buildOccupationForm(),
@ -133,8 +130,6 @@ class OnboardingView extends StackedView<OnboardingViewModel>
Widget _buildGenderForm() => const GenderFormScreen();
Widget _buildBirthdayForm() => const BirthdayFormScreen();
Widget _buildAgeGroupForm() => const AgeGroupFormScreen();
Widget _buildEducationalBackgroundForm() =>
@ -143,7 +138,9 @@ class OnboardingView extends StackedView<OnboardingViewModel>
Widget _buildOccupationForm() =>
OccupationFormScreen(occupationController: occupationController);
Widget _buildCountryRegionForm() => const CountryRegionFormScreen();
Widget _buildCountryRegionForm() => CountryRegionFormScreen(
regionController: regionController,
);
Widget _buildLearningGoalForm() => const LearningGoalFormScreen();

View File

@ -15,6 +15,7 @@ const bool _autoTextFieldValidation = true;
const String TopicValueKey = 'topic';
const String FullNameValueKey = 'fullName';
const String RegionValueKey = 'region';
const String ChallengeValueKey = 'challenge';
const String OccupationValueKey = 'occupation';
const String LanguageGoalValueKey = 'languageGoal';
@ -27,6 +28,7 @@ final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
TopicValueKey: FormValidator.validateForm,
FullNameValueKey: FormValidator.validateFullNameForm,
RegionValueKey: FormValidator.validateForm,
ChallengeValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm,
LanguageGoalValueKey: FormValidator.validateForm,
@ -37,6 +39,8 @@ mixin $OnboardingView {
_getFormTextEditingController(TopicValueKey);
TextEditingController get fullNameController =>
_getFormTextEditingController(FullNameValueKey);
TextEditingController get regionController =>
_getFormTextEditingController(RegionValueKey);
TextEditingController get challengeController =>
_getFormTextEditingController(ChallengeValueKey);
TextEditingController get occupationController =>
@ -46,6 +50,7 @@ mixin $OnboardingView {
FocusNode get topicFocusNode => _getFormFocusNode(TopicValueKey);
FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey);
FocusNode get regionFocusNode => _getFormFocusNode(RegionValueKey);
FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey);
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
FocusNode get languageGoalFocusNode =>
@ -77,6 +82,7 @@ mixin $OnboardingView {
void syncFormWithViewModel(FormStateHelper model) {
topicController.addListener(() => _updateFormData(model));
fullNameController.addListener(() => _updateFormData(model));
regionController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
languageGoalController.addListener(() => _updateFormData(model));
@ -93,6 +99,7 @@ mixin $OnboardingView {
void listenToFormUpdated(FormViewModel model) {
topicController.addListener(() => _updateFormData(model));
fullNameController.addListener(() => _updateFormData(model));
regionController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
languageGoalController.addListener(() => _updateFormData(model));
@ -107,6 +114,7 @@ mixin $OnboardingView {
..addAll({
TopicValueKey: topicController.text,
FullNameValueKey: fullNameController.text,
RegionValueKey: regionController.text,
ChallengeValueKey: challengeController.text,
OccupationValueKey: occupationController.text,
LanguageGoalValueKey: languageGoalController.text,
@ -153,6 +161,7 @@ extension ValueProperties on FormStateHelper {
String? get topicValue => this.formValueMap[TopicValueKey] as String?;
String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?;
String? get regionValue => this.formValueMap[RegionValueKey] as String?;
String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?;
String? get occupationValue =>
this.formValueMap[OccupationValueKey] as String?;
@ -180,6 +189,16 @@ extension ValueProperties on FormStateHelper {
}
}
set regionValue(String? value) {
this.setData(
this.formValueMap..addAll({RegionValueKey: value}),
);
if (_OnboardingViewTextEditingControllers.containsKey(RegionValueKey)) {
_OnboardingViewTextEditingControllers[RegionValueKey]?.text = value ?? '';
}
}
set challengeValue(String? value) {
this.setData(
this.formValueMap..addAll({ChallengeValueKey: value}),
@ -220,6 +239,9 @@ extension ValueProperties on FormStateHelper {
bool get hasFullName =>
this.formValueMap.containsKey(FullNameValueKey) &&
(fullNameValue?.isNotEmpty ?? false);
bool get hasRegion =>
this.formValueMap.containsKey(RegionValueKey) &&
(regionValue?.isNotEmpty ?? false);
bool get hasChallenge =>
this.formValueMap.containsKey(ChallengeValueKey) &&
(challengeValue?.isNotEmpty ?? false);
@ -234,6 +256,8 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false;
bool get hasFullNameValidationMessage =>
this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false;
bool get hasRegionValidationMessage =>
this.fieldsValidationMessages[RegionValueKey]?.isNotEmpty ?? false;
bool get hasChallengeValidationMessage =>
this.fieldsValidationMessages[ChallengeValueKey]?.isNotEmpty ?? false;
bool get hasOccupationValidationMessage =>
@ -245,6 +269,8 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[TopicValueKey];
String? get fullNameValidationMessage =>
this.fieldsValidationMessages[FullNameValueKey];
String? get regionValidationMessage =>
this.fieldsValidationMessages[RegionValueKey];
String? get challengeValidationMessage =>
this.fieldsValidationMessages[ChallengeValueKey];
String? get occupationValidationMessage =>
@ -258,6 +284,8 @@ extension Methods on FormStateHelper {
this.fieldsValidationMessages[TopicValueKey] = validationMessage;
void setFullNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[FullNameValueKey] = validationMessage;
void setRegionValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[RegionValueKey] = validationMessage;
void setChallengeValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ChallengeValueKey] = validationMessage;
void setOccupationValidationMessage(String? validationMessage) =>
@ -269,6 +297,7 @@ extension Methods on FormStateHelper {
void clearForm() {
topicValue = '';
fullNameValue = '';
regionValue = '';
challengeValue = '';
occupationValue = '';
languageGoalValue = '';
@ -279,6 +308,7 @@ extension Methods on FormStateHelper {
this.setValidationMessages({
TopicValueKey: getValidationMessage(TopicValueKey),
FullNameValueKey: getValidationMessage(FullNameValueKey),
RegionValueKey: getValidationMessage(RegionValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
LanguageGoalValueKey: getValidationMessage(LanguageGoalValueKey),
@ -303,6 +333,7 @@ void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
TopicValueKey: getValidationMessage(TopicValueKey),
FullNameValueKey: getValidationMessage(FullNameValueKey),
RegionValueKey: getValidationMessage(RegionValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
LanguageGoalValueKey: getValidationMessage(LanguageGoalValueKey),

View File

@ -63,11 +63,6 @@ class OnboardingViewModel extends ReactiveViewModel
String? get selectedGender => _selectedGender;
// Birthday
String? _selectedBirthday;
String? get selectedBirthday => _selectedBirthday;
// Age group
final List<Map<String, dynamic>> _ageGroups = [
{
@ -105,10 +100,10 @@ class OnboardingViewModel extends ReactiveViewModel
String get selectedCountry => _selectedCountry;
// Country
String _selectedRegion = 'Addis Ababa';
// Region
bool _focusRegion = false;
String get selectedRegion => _selectedRegion;
bool get focusRegion => _focusRegion;
// Learning goal
String? _selectedLearningGoal;
@ -247,12 +242,6 @@ class OnboardingViewModel extends ReactiveViewModel
bool isSelectedGender(String value) => _selectedGender == value;
// Birthday
void setBirthday(String value) {
_selectedBirthday = value;
rebuildUi();
}
// Age group
void setSelectedAgeGroup(Map<String, dynamic> value) {
_selectedAgeGroup = value;
@ -270,44 +259,168 @@ class OnboardingViewModel extends ReactiveViewModel
}
// Country
List<String> getCountries() => ['Ethiopia', 'Other'];
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"
];
void setSelectedCountry(String value) {
_selectedCountry = value;
if (selectedCountry != 'Ethiopia') {
_selectedRegion = 'Other';
} else {
_selectedRegion = 'Addis Ababa';
}
rebuildUi();
}
// Region
List<String> getRegions(String country) {
if (country == 'Ethiopia') {
return [
'Afar',
'SNNPR',
'Amhara',
'Harari',
'Oromia',
'Sidama',
'Somali',
'Tigray',
'Gambela',
'Dire Dawa',
'Addis Ababa',
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
} else {
return ['Other'];
}
}
void setSelectedRegion(String value) {
_selectedRegion = value;
void setRegionFocus() {
_focusRegion = true;
rebuildUi();
}
@ -414,12 +527,6 @@ class OnboardingViewModel extends ReactiveViewModel
rebuildUi();
}
// Reset birthday form screen
void resetBirthdayFormScreen() {
_selectedBirthday = null;
rebuildUi();
}
// Reset age group form screen
void resetAgeGroupFormScreen() {
_selectedAgeGroup = null;
@ -441,7 +548,6 @@ class OnboardingViewModel extends ReactiveViewModel
// Reset country region form screen
void resetCountryRegionFormScreen() {
_selectedCountry = 'Ethiopia';
_selectedRegion = 'Addis Ababa';
rebuildUi();
}

View File

@ -1,122 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../../../widgets/birthday_selector.dart';
class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
const BirthdayFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetBirthdayFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {'birth_day': viewModel.selectedBirthday};
viewModel.addUserData(data);
viewModel.next();
}
@override
Widget build(BuildContext context, OnboardingViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(OnboardingViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(OnboardingViewModel viewModel) => [
_buildAppBar(viewModel),
verticalSpaceMedium,
_buildExpandedBody(viewModel)
];
Widget _buildExpandedBody(OnboardingViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(OnboardingViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(OnboardingViewModel viewModel) =>
[_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(OnboardingViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubtitle(),
verticalSpaceMedium,
_buildBirthdayFormField(viewModel)
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);
Widget _buildTitle() => Text(
'Pick your birthday?',
style: style25DG600,
);
Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your birthday.',
style: style14MG400,
);
Widget _buildBirthdayFormField(OnboardingViewModel viewModel) =>
BirthdaySelector(
birthday: viewModel.selectedBirthday,
onSelected: (value) =>
viewModel.setBirthday(DateFormat('yyyy-MM-dd').format(value)),
);
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: viewModel.selectedBirthday != null
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap:
viewModel.selectedBirthday != null ? () => _next(viewModel) : null,
);
}

View File

@ -6,9 +6,11 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_dropdown.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../onboarding_view.form.dart';
class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
const CountryRegionFormScreen({super.key});
final TextEditingController regionController;
const CountryRegionFormScreen({super.key, required this.regionController});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetCountryRegionFormScreen();
@ -19,8 +21,8 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'region': regionController.text,
'country': viewModel.selectedCountry,
'region': viewModel.selectedRegion
};
viewModel.addUserData(data);
@ -83,8 +85,11 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildCountryDropDown(viewModel),
verticalSpaceMedium,
_buildRegionDropDown(viewModel),
verticalSpaceMedium,
_buildRegionFormField(viewModel),
if (viewModel.hasRegionValidationMessage && viewModel.focusRegion)
verticalSpaceTiny,
if (viewModel.hasRegionValidationMessage && viewModel.focusRegion)
_buildRegionValidatorWrapper(viewModel)
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
@ -112,16 +117,23 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
items: (value, props) => viewModel.getCountries(),
onChanged: (value) =>
viewModel.setSelectedCountry(value ?? 'Ethiopia'));
Widget _buildRegionFormField(OnboardingViewModel viewModel) => TextFormField(
controller: regionController,
onTap: viewModel.setRegionFocus,
decoration: inputDecoration(
hint: 'Enter Your City',
focus: viewModel.focusRegion,
filled: regionController.text.isNotEmpty),
);
Widget _buildRegionDropDown(OnboardingViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select region',
icon: _buildSearchIcon(),
selectedItem: viewModel.selectedRegion,
items: (value, props) =>
viewModel.getRegions(viewModel.selectedCountry),
onChanged: (value) =>
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
Widget _buildRegionValidatorWrapper(OnboardingViewModel viewModel) =>
viewModel.hasRegionValidationMessage
? _buildRegionValidator(viewModel)
: Container();
Widget _buildRegionValidator(OnboardingViewModel viewModel) => Text(
viewModel.regionValidationMessage!,
style: style12R700,
);
Icon _buildSearchIcon() => const Icon(
@ -140,7 +152,9 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: () => _next(viewModel),
backgroundColor: kcPrimaryColor,
onTap: regionController.text.isNotEmpty ? () => _next(viewModel) : null,
backgroundColor: regionController.text.isNotEmpty
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
@ -25,6 +26,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
Map<String, dynamic> data = {
'profile_completed': true,
'preferred_language': 'en',
'birth_day': DateFormat('yyyy-MM-dd').format(DateTime.now()),
'favoutite_topic': viewModel.selectedTopic ?? topicController.text,
};
viewModel.addUserData(data);

View File

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/widgets/birthday_selector.dart';
import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
@ -22,6 +21,7 @@ import 'profile_detail_view.form.dart';
@FormView(fields: [
FormTextField(name: 'email', validator: FormValidator.validateForm),
FormTextField(name: 'region', validator: FormValidator.validateForm),
FormTextField(
name: 'phoneNumber', validator: FormValidator.validatePhoneNumberForm),
FormTextField(name: 'lastName', validator: FormValidator.validateForm),
@ -34,13 +34,13 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Future<void> _update(ProfileDetailViewModel viewModel) async {
Map<String, dynamic> data = {
'region': viewModel.selectedRegion,
'region':regionController.text,
'gender': viewModel.selectedGender,
'last_name': lastNameController.text,
'country': viewModel.selectedCountry,
'first_name': firstNameController.text,
'occupation': occupationController.text,
'birth_day': viewModel.selectedBirthday,
'birth_day': DateFormat('d MMM, yyyy').format(DateTime.now()),
};
viewModel.addUserData(data);
@ -68,15 +68,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
void _onModelReady(ProfileDetailViewModel viewModel) {
phoneNumberController.text = '251900000000';
emailController.text = viewModel.user?.email ?? '';
regionController.text = viewModel.user?.region ?? '';
lastNameController.text = viewModel.user?.lastName ?? '';
firstNameController.text = viewModel.user?.firstName ?? '';
occupationController.text = viewModel.user?.occupation ?? '';
viewModel.clearUserData();
viewModel.setGender(viewModel.user?.gender ?? '');
viewModel.setSelectedCountry(viewModel.user?.country ?? 'Ethiopia');
viewModel.setSelectedRegion(viewModel.user?.region ?? 'Addis Ababa');
viewModel.setBirthday(viewModel.user?.birthday ??
DateFormat('d MMM, yyyy').format(DateTime.now()));
}
@override
@ -185,14 +184,17 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
verticalSpaceMedium,
_buildGenderFormFieldWrapper(viewModel),
verticalSpaceSmall,
_buildBirthdayColumn(viewModel),
verticalSpaceSmall,
_buildPhoneNumberFormFieldSection(viewModel),
verticalSpaceTiny,
_buildEmailFormFieldSection(viewModel),
verticalSpaceMedium,
_buildCountryRegionSection(viewModel),
_buildCountryDropdownLabel(),
verticalSpaceSmall,
_buildCountryDropdown(viewModel),
verticalSpaceMedium,
_buildRegionFormFieldWrapper(viewModel),
verticalSpaceMedium,
_buildOccupationDropdownWrapper(viewModel),
verticalSpaceLarge,
_buildLowerColumn(viewModel)
@ -415,30 +417,6 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
),
);
Widget _buildBirthdayColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBirthdayChildren(viewModel),
);
List<Widget> _buildBirthdayChildren(ProfileDetailViewModel viewModel) => [
_buildBirthdayLabel(),
verticalSpaceSmall,
_buildBirthdayFormField(viewModel),
];
Widget _buildBirthdayLabel() => CustomFormLabel(
label: 'Birthday',
style: style16DG600,
);
Widget _buildBirthdayFormField(ProfileDetailViewModel viewModel) =>
BirthdaySelector(
birthday: viewModel.selectedBirthday,
onSelected: (value) =>
viewModel.setBirthday(DateFormat('d MMM, yyyy').format(value)),
);
Widget _buildPhoneNumberFormFieldSection(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
@ -533,39 +511,6 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
style: validationStyle,
);
Widget _buildCountryRegionSection(ProfileDetailViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildCountryRegionChildren(viewModel),
);
List<Widget> _buildCountryRegionChildren(ProfileDetailViewModel viewModel) =>
[
_buildCountryDropdownColumnWrapper(viewModel),
const SizedBox(width: 20),
_buildRegionDropdownColumnWrapper(viewModel)
];
Widget _buildCountryDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
Expanded(
child: _buildCountryDropdownColumn(viewModel),
);
Widget _buildCountryDropdownColumn(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildCountryDropdownChildren(viewModel),
);
List<Widget> _buildCountryDropdownChildren(
ProfileDetailViewModel viewModel) =>
[
_buildCountryDropdownLabel(),
verticalSpaceSmall,
_buildCountryDropdown(viewModel)
];
Widget _buildCountryDropdownLabel() => CustomFormLabel(
label: 'Country',
style: style16DG600,
@ -579,38 +524,48 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
onChanged: (value) => viewModel.setSelectedCountry(value ?? 'Ethiopia'),
);
Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
Expanded(
child: _buildRegionDropdownColumn(viewModel),
);
Widget _buildRegionDropdownColumn(ProfileDetailViewModel viewModel) => Column(
Widget _buildRegionFormFieldWrapper(ProfileDetailViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildRegionDropdownChildren(viewModel),
children: _buildRegionFormFieldChildren(viewModel),
);
List<Widget> _buildRegionDropdownChildren(ProfileDetailViewModel viewModel) =>
List<Widget> _buildRegionFormFieldChildren(
ProfileDetailViewModel viewModel) =>
[
_buildRegionDropdownLabel(),
verticalSpaceSmall,
_buildRegionDropdown(viewModel)
_buildRegionFormFieldLabel(),verticalSpaceSmall,
_buildRegionFormField(viewModel),
if (viewModel.hasRegionValidationMessage && viewModel.focusRegion)
verticalSpaceTiny,
if (viewModel.hasRegionValidationMessage && viewModel.focusRegion)
_buildRegionValidatorWrapper(viewModel),
];
Widget _buildRegionDropdownLabel() => CustomFormLabel(
Widget _buildRegionFormFieldLabel() => CustomFormLabel(
label: 'Region',
style: style16DG600,
);
Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select region',
selectedItem: viewModel.selectedRegion,
items: (value, props) =>
viewModel.getRegions(viewModel.selectedCountry),
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 _buildRegionValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasRegionValidationMessage
? _buildRegionValidator(viewModel)
: Container();
Widget _buildRegionValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.regionValidationMessage!,
style: style12R700,
);
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>

View File

@ -14,6 +14,7 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String EmailValueKey = 'email';
const String RegionValueKey = 'region';
const String PhoneNumberValueKey = 'phoneNumber';
const String LastNameValueKey = 'lastName';
const String FirstNameValueKey = 'firstName';
@ -27,6 +28,7 @@ final Map<String, FocusNode> _ProfileDetailViewFocusNodes = {};
final Map<String, String? Function(String?)?>
_ProfileDetailViewTextValidations = {
EmailValueKey: FormValidator.validateForm,
RegionValueKey: FormValidator.validateForm,
PhoneNumberValueKey: FormValidator.validatePhoneNumberForm,
LastNameValueKey: FormValidator.validateForm,
FirstNameValueKey: FormValidator.validateForm,
@ -36,6 +38,8 @@ final Map<String, String? Function(String?)?>
mixin $ProfileDetailView {
TextEditingController get emailController =>
_getFormTextEditingController(EmailValueKey);
TextEditingController get regionController =>
_getFormTextEditingController(RegionValueKey);
TextEditingController get phoneNumberController =>
_getFormTextEditingController(PhoneNumberValueKey);
TextEditingController get lastNameController =>
@ -46,6 +50,7 @@ mixin $ProfileDetailView {
_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);
@ -76,6 +81,7 @@ mixin $ProfileDetailView {
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
emailController.addListener(() => _updateFormData(model));
regionController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model));
@ -92,6 +98,7 @@ mixin $ProfileDetailView {
)
void listenToFormUpdated(FormViewModel model) {
emailController.addListener(() => _updateFormData(model));
regionController.addListener(() => _updateFormData(model));
phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model));
@ -106,6 +113,7 @@ mixin $ProfileDetailView {
model.formValueMap
..addAll({
EmailValueKey: emailController.text,
RegionValueKey: regionController.text,
PhoneNumberValueKey: phoneNumberController.text,
LastNameValueKey: lastNameController.text,
FirstNameValueKey: firstNameController.text,
@ -152,6 +160,7 @@ extension ValueProperties on FormStateHelper {
}
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
String? get regionValue => this.formValueMap[RegionValueKey] as String?;
String? get phoneNumberValue =>
this.formValueMap[PhoneNumberValueKey] as String?;
String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?;
@ -170,6 +179,17 @@ extension ValueProperties on FormStateHelper {
}
}
set regionValue(String? value) {
this.setData(
this.formValueMap..addAll({RegionValueKey: value}),
);
if (_ProfileDetailViewTextEditingControllers.containsKey(RegionValueKey)) {
_ProfileDetailViewTextEditingControllers[RegionValueKey]?.text =
value ?? '';
}
}
set phoneNumberValue(String? value) {
this.setData(
this.formValueMap..addAll({PhoneNumberValueKey: value}),
@ -221,6 +241,9 @@ extension ValueProperties on FormStateHelper {
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
bool get hasRegion =>
this.formValueMap.containsKey(RegionValueKey) &&
(regionValue?.isNotEmpty ?? false);
bool get hasPhoneNumber =>
this.formValueMap.containsKey(PhoneNumberValueKey) &&
(phoneNumberValue?.isNotEmpty ?? false);
@ -236,6 +259,8 @@ extension ValueProperties on FormStateHelper {
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
bool get hasRegionValidationMessage =>
this.fieldsValidationMessages[RegionValueKey]?.isNotEmpty ?? false;
bool get hasPhoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false;
bool get hasLastNameValidationMessage =>
@ -247,6 +272,8 @@ extension ValueProperties on FormStateHelper {
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
String? get regionValidationMessage =>
this.fieldsValidationMessages[RegionValueKey];
String? get phoneNumberValidationMessage =>
this.fieldsValidationMessages[PhoneNumberValueKey];
String? get lastNameValidationMessage =>
@ -260,6 +287,8 @@ extension ValueProperties on FormStateHelper {
extension Methods on FormStateHelper {
void setEmailValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
void setRegionValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[RegionValueKey] = validationMessage;
void setPhoneNumberValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage;
void setLastNameValidationMessage(String? validationMessage) =>
@ -272,6 +301,7 @@ extension Methods on FormStateHelper {
/// Clears text input fields on the Form
void clearForm() {
emailValue = '';
regionValue = '';
phoneNumberValue = '';
lastNameValue = '';
firstNameValue = '';
@ -282,6 +312,7 @@ extension Methods on FormStateHelper {
void validateForm() {
this.setValidationMessages({
EmailValueKey: getValidationMessage(EmailValueKey),
RegionValueKey: getValidationMessage(RegionValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey),
@ -306,6 +337,7 @@ String? getValidationMessage(String key) {
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
EmailValueKey: getValidationMessage(EmailValueKey),
RegionValueKey: getValidationMessage(RegionValueKey),
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey),

View File

@ -47,10 +47,6 @@ class ProfileDetailViewModel extends ReactiveViewModel
String? get selectedGender => _selectedGender;
// Birthday
String? _selectedBirthday;
String? get selectedBirthday => _selectedBirthday;
// First name
bool _focusPhoneNumber = false;
@ -67,16 +63,17 @@ class ProfileDetailViewModel extends ReactiveViewModel
String get selectedCountry => _selectedCountry;
// Region
String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion;
// Occupation
bool _focusOccupation = false;
bool get focusOccupation => _focusOccupation;
// Region
bool _focusRegion = false;
bool get focusRegion => _focusRegion;
// User data
final Map<String, dynamic> _userData = {};
@ -100,11 +97,6 @@ class ProfileDetailViewModel extends ReactiveViewModel
rebuildUi();
}
// Birthday
void setBirthday(String value) {
_selectedBirthday = value;
rebuildUi();
}
// Phone number
void setPhoneNumberFocus() {
@ -119,47 +111,168 @@ class ProfileDetailViewModel extends ReactiveViewModel
}
// Country
List<String> getCountries() => ['Ethiopia', 'Other'];
// 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"
];
void setSelectedCountry(String value) {
_selectedCountry = value;
if (selectedCountry != 'Ethiopia') {
_selectedRegion = 'Other';
} else {
_selectedRegion = 'Addis Ababa';
}
rebuildUi();
}
// Region
List<String> getRegions(String country) {
if (country == 'Ethiopia') {
return [
'Afar',
'SNNPR',
'Amhara',
'Harari',
'Oromia',
'Sidama',
'Somali',
'Tigray',
'Gambela',
'Dire Dawa',
'Addis Ababa',
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
} else {
return ['Other'];
}
}
void setSelectedRegion(String value) {
_selectedRegion = value;
rebuildUi();
}
// Occupation
void setOccupationFocus() {
@ -167,6 +280,12 @@ class ProfileDetailViewModel extends ReactiveViewModel
rebuildUi();
}
// Region
void setRegionFocus() {
_focusRegion = true;
rebuildUi();
}
// User data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);

View File

@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'package:omni_datetime_picker/omni_datetime_picker.dart';
class BirthdaySelector extends StatelessWidget {
final String? birthday;
final void Function(DateTime)? onSelected;
const BirthdaySelector({super.key, this.birthday, this.onSelected});
DateTime _initialDate() {
try {
final parsedDate = format.parse(birthday ?? '');
return parsedDate.isAfter(DateTime.now()) ? DateTime.now() : parsedDate;
} catch (_) {
return DateTime.now();
}
}
Future<void> _pickDateTime(
BuildContext context,
) async {
DateTime? dateTime = await showOmniDateTimePicker(
context: context,
is24HourMode: false,
isShowSeconds: false,
title: _buildTitle(),
lastDate: DateTime.now(),
firstDate: DateTime(1900),
barrierDismissible: true,
initialDate: _initialDate(),
titleSeparator: const Divider(),
padding: const EdgeInsets.all(16),
type: OmniDateTimePickerType.date,
borderRadius: const BorderRadius.all(Radius.circular(15)),
insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
theme: ThemeData(
colorScheme:
const ColorScheme.light().copyWith(primary: kcPrimaryColor),
),
);
if (dateTime != null) {
// String formattedDateTime = DateFormat('d MMM, yyyy').format(dateTime);
if (onSelected != null) {
onSelected!(dateTime);
}
}
}
@override
Widget build(
BuildContext context,
) =>
_buildButtonWrapper(
context,
);
Widget _buildButtonWrapper(BuildContext context) => Container(
height: 50,
width: double.maxFinite,
margin: const EdgeInsets.only(bottom: 15),
child: _buildContainerWrapper(context),
);
Widget _buildContainerWrapper(BuildContext context) => GestureDetector(
onTap: () async => await _pickDateTime(
context,
),
child: _buildContainer(),
);
Widget _buildTitle() => Text('Birthday', style: style16DG600);
Widget _buildContainer() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: kcPrimaryColor.withOpacity(0.1),
border: Border.all(color: kcPrimaryColor),
),
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildButtonRowWrapper(),
);
Widget _buildButtonRowWrapper() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildButtonRowChildren(),
);
List<Widget> _buildButtonRowChildren() => [_buildText(), _buildIcon()];
Widget _buildText() => Text(
birthday ?? 'Pick birthday',
style: const TextStyle(color: kcDarkGrey),
);
Widget _buildIcon() => const Icon(
Icons.calendar_month,
color: kcPrimaryColor,
);
}

View File

@ -15,15 +15,17 @@ class CancelLearnPracticeSheet extends StatelessWidget {
final GestureTapCallback? onContinue;
const CancelLearnPracticeSheet(
{super.key, this.onClose, this.onCancel, this.onContinue,required this.user});
{super.key,
this.onClose,
this.onCancel,
this.onContinue,
required this.user});
@override
Widget build(BuildContext context) =>
_buildSheetWrapper();
Widget build(BuildContext context) => _buildSheetWrapper();
Widget _buildSheetWrapper() =>
CustomBottomSheet(
height: 500, onTap: onClose, child: _buildColumnWrapper());
Widget _buildSheetWrapper() => CustomBottomSheet(
height: 500, onTap: onClose, child: _buildColumnWrapper());
Widget _buildColumnWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),

View File

@ -29,11 +29,8 @@ class LearnPracticeResultsWrapper
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(LearnPracticeViewModel viewModel) => [
_buildTitle(),
verticalSpaceSmall,
_buildResults(viewModel)
];
List<Widget> _buildColumnChildren(LearnPracticeViewModel viewModel) =>
[_buildTitle(), verticalSpaceSmall, _buildResults(viewModel)];
Widget _buildTitle() => Text(
'Conversation Review',
@ -42,14 +39,14 @@ class LearnPracticeResultsWrapper
);
Widget _buildResults(LearnPracticeViewModel viewModel) => ListView.separated(
shrinkWrap: true,
itemCount: viewModel.answers.length,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => verticalSpaceSmall,
itemBuilder: (context, index) => _buildResult( viewModel.answers[index]),
);
shrinkWrap: true,
itemCount: viewModel.answers.length,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => verticalSpaceSmall,
itemBuilder: (context, index) => _buildResult(viewModel.answers[index]),
);
Widget _buildResult(Map<String,dynamic> answer) => LearnPracticeResultCard(answer: answer,);
Widget _buildResult(Map<String, dynamic> answer) => LearnPracticeResultCard(
answer: answer,
);
}

View File

@ -10,7 +10,8 @@ class LearnPracticeTipSection extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeTipSection({super.key});
@override
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildContainer(viewModel);
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildContainer(viewModel);
Widget _buildContainer(LearnPracticeViewModel viewModel) => Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 25),
@ -24,7 +25,7 @@ class LearnPracticeTipSection extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren( viewModel),
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(LearnPracticeViewModel viewModel) =>

View File

@ -1,5 +1,5 @@
name: yimaru_app
version: 0.1.4+6
version: 0.1.5+7
publish_to: 'none'
description: A new Flutter project.

View File

@ -1992,6 +1992,13 @@ class MockVoiceRecorderService extends _i1.Mock
),
) as _i5.WaveformRecorderController);
@override
bool get isRecording => (super.noSuchMethod(
Invocation.getter(#isRecording),
returnValue: false,
returnValueForMissingStub: false,
) as bool);
@override
int get listenersCount => (super.noSuchMethod(
Invocation.getter(#listenersCount),