From 0b4ff2fb7fca7226f625892932166ca849bf94fc Mon Sep 17 00:00:00 2001 From: Bossanyi Tibor Date: Wed, 16 Sep 2020 15:41:39 +0200 Subject: [PATCH] wt1.1.1 ExercisePlan + ExercisePlan detail, Trainee --- asset/image/lock.png | Bin 0 -> 87504 bytes i18n/en.json | 19 +- i18n/hu.json | 20 +- lib/bloc/account/account_bloc.dart | 15 + lib/bloc/account/account_event.dart | 12 + lib/bloc/exercise_add_by_plan_bloc.dart | 81 ++++++ .../exercise_by_plan_bloc.dart | 58 ++++ .../exercise_by_plan_event.dart | 21 ++ .../exercise_by_plan_state.dart | 31 +++ .../exercise_plan/exercise_plan_bloc.dart | 89 ++++++ .../exercise_plan/exercise_plan_event.dart | 49 ++++ .../exercise_plan/exercise_plan_state.dart | 37 +++ lib/bloc/exercise_plan_custom_form.dart | 94 +++++++ lib/bloc/settings/settings_bloc.dart | 9 +- lib/bloc/settings/settings_event.dart | 4 + lib/localization/app_language.dart | 13 +- lib/localization/app_localization.dart | 1 + lib/main.dart | 30 +- lib/model/cache.dart | 46 ++- lib/model/customer.dart | 4 + lib/model/exercise.dart | 2 + lib/model/exercise_plan.dart | 58 ++++ lib/model/exercise_plan_detail.dart | 36 +++ lib/model/workout_tree.dart | 23 +- lib/repository/customer_repository.dart | 58 +++- lib/repository/exercise_plan_repository.dart | 167 +++++++++++ lib/repository/exercise_repository.dart | 29 +- lib/repository/menu_tree_repository.dart | 76 ++++- lib/service/api.dart | 3 + lib/service/customer_service.dart | 83 ++++-- lib/service/exercise_plan_service.dart | 145 ++++++++++ lib/service/exercise_service.dart | 6 +- lib/util/not_found_exception.dart | 4 + lib/util/session.dart | 3 +- lib/view/account.dart | 152 +++++----- lib/view/exercise_add_by_plan_page.dart | 241 ++++++++++++++++ lib/view/exercise_execute_by_plan_page.dart | 187 +++++++++++++ lib/view/exercise_log_page.dart | 148 ++++++++++ lib/view/exercise_plan_custom_page.dart | 190 +++++++++++++ lib/view/exercise_plan_detail_add_page.dart | 144 ++++++++++ lib/view/mydevelopment_page.dart | 262 ++++++++++-------- lib/view/myexcercise_plan_page.dart | 196 +++++++++++++ lib/view/settings.dart | 7 + lib/widgets/app_bar.dart | 17 +- lib/widgets/app_bar_common.dart | 133 +++++++++ lib/widgets/bottom_nav.dart | 7 +- lib/widgets/home.dart | 5 +- lib/widgets/loading.dart | 4 +- lib/widgets/menu_page_widget.dart | 40 ++- pubspec.lock | 20 +- pubspec.yaml | 16 +- test/account_bloc_test.dart | 79 ++++++ test/customer_service_test.dart | 52 ++++ test/exercise_plan_test.dart | 72 +++++ 54 files changed, 2985 insertions(+), 313 deletions(-) create mode 100644 asset/image/lock.png create mode 100644 lib/bloc/exercise_add_by_plan_bloc.dart create mode 100644 lib/bloc/exercise_by_plan/exercise_by_plan_bloc.dart create mode 100644 lib/bloc/exercise_by_plan/exercise_by_plan_event.dart create mode 100644 lib/bloc/exercise_by_plan/exercise_by_plan_state.dart create mode 100644 lib/bloc/exercise_plan/exercise_plan_bloc.dart create mode 100644 lib/bloc/exercise_plan/exercise_plan_event.dart create mode 100644 lib/bloc/exercise_plan/exercise_plan_state.dart create mode 100644 lib/bloc/exercise_plan_custom_form.dart create mode 100644 lib/model/exercise_plan.dart create mode 100644 lib/model/exercise_plan_detail.dart create mode 100644 lib/repository/exercise_plan_repository.dart create mode 100644 lib/service/exercise_plan_service.dart create mode 100644 lib/util/not_found_exception.dart create mode 100644 lib/view/exercise_add_by_plan_page.dart create mode 100644 lib/view/exercise_execute_by_plan_page.dart create mode 100644 lib/view/exercise_log_page.dart create mode 100644 lib/view/exercise_plan_custom_page.dart create mode 100644 lib/view/exercise_plan_detail_add_page.dart create mode 100644 lib/view/myexcercise_plan_page.dart create mode 100644 lib/widgets/app_bar_common.dart create mode 100644 test/account_bloc_test.dart create mode 100644 test/customer_service_test.dart create mode 100644 test/exercise_plan_test.dart diff --git a/asset/image/lock.png b/asset/image/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..2e8c552a33fa6567ca0f85bbe7a7a775db29be98 GIT binary patch literal 87504 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 R*XIK05wIcTYq2kz|2J4c=1Kqn literal 0 HcmV?d00001 diff --git a/i18n/en.json b/i18n/en.json index 244ce60..c1300a9 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -52,6 +52,7 @@ "Sizes": "Sizes", "Save Exercise": "Save Exercise of:", "Save": "Save", + "Delete": "Delete", "Name": "Name", "Exercise": "Exercise", @@ -117,5 +118,21 @@ "3rd Control Exercise:": "3rd Control Exercise:", "My Development":"My Development", - "My Training Plan":"My Training Plan" + "My Training Plan":"My Training Plan", + + "Please add an exercise plan": "Please add an exercise plan", + "Serie": "Serie", + "Repeats": "Repeats", + "Save The Exercise To The Exercise Plan": "Save The Exercise To The Exercise Plan", + "The number of the serie done with":"The number of the serie done with", + "The number of the repeats of one serie":"The number of the repeats of one serie", + + "1. Chest": "1. Chest", + "2. Biceps": "2. Biceps", + "3. Triceps": "3. Triceps", + "4. Back": "4. Back", + "5. Shoulders": "5. Shoulders", + "6. Core": "6. Core", + "7. Thigh": "7. Thigh", + "8. Calf": "8. Calf" } \ No newline at end of file diff --git a/i18n/hu.json b/i18n/hu.json index f553264..5489c48 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -52,6 +52,7 @@ "Sizes": "Méretek", "Save Exercise": "Gyakorlat mentése:", "Save": "Mentés", + "Delete": "Törlés", "The number of the exercise": "Írd be a gyakorlat számát", "The number of the exercise done with": "Írd be, mennyivel csináltad a gyakorlatot", @@ -116,5 +117,22 @@ "3rd Control Exercise:": "3. kontrollgyakorlat:", "My Development":"Fejlődésem", - "My Training Plan":"Edzéstervem" + "My Training Plan":"Edzéstervem", + + "Please add an exercise plan": "Kérlek add meg az edzéstervet a gyakorlathoz", + + "Serie": "Széria", + "Repeats": "Ismétlés", + "Save The Exercise To The Exercise Plan": "Gyakorlat mentése az edzéstervhez", + "The number of the serie done with":"Mennyi szériát csinálsz", + "The number of the repeats of one serie":"Hány ismétlést csinálsz egy gyakorlaton belül", + + "1. Chest": "1. Mell", + "2. Biceps": "2. Bicepsz", + "3. Triceps": "3. Tricepsz", + "4. Back": "4. Hát", + "5. Shoulders": "5. Váll", + "6. Core": "6. Has", + "7. Thigh": "7. Comb", + "8. Calf": "8. Vádli" } \ No newline at end of file diff --git a/lib/bloc/account/account_bloc.dart b/lib/bloc/account/account_bloc.dart index 03779a8..0e33c7b 100644 --- a/lib/bloc/account/account_bloc.dart +++ b/lib/bloc/account/account_bloc.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/model/customer.dart'; import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; @@ -13,6 +14,7 @@ part 'account_state.dart'; class AccountBloc extends Bloc { final CustomerRepository customerRepository; bool loggedIn = false; + int traineeId = 0; AccountBloc({this.customerRepository}) : super(AccountInitial()) { if ( Cache().userLoggedIn != null ) { customerRepository.customer = Cache().userLoggedIn; @@ -36,8 +38,21 @@ class AccountBloc extends Bloc { } else if (event is AccountLogout) { await Cache().logout(); customerRepository.customer = null; + customerRepository.emptyTrainees(); loggedIn = false; yield AccountLoggedOut(); + } else if ( event is AccountGetTrainees) { + yield AccountLoading(); + await customerRepository.getTrainees(); + yield AccountReady(); + } else if ( event is AccountSelectTrainee ) { + yield AccountLoading(); + customerRepository.setTrainee(event.traineeId); + Cache().setTrainee(customerRepository.getTraineeById(event.traineeId)); + ExerciseRepository exerciseRepository = ExerciseRepository(); + await exerciseRepository.getExercisesByCustomer(event.traineeId); + this.traineeId = event.traineeId; + yield AccountReady(); } } on Exception catch(e) { yield AccountError(message: e.toString()); diff --git a/lib/bloc/account/account_event.dart b/lib/bloc/account/account_event.dart index a433b05..5259cfd 100644 --- a/lib/bloc/account/account_event.dart +++ b/lib/bloc/account/account_event.dart @@ -43,4 +43,16 @@ class AccountLogInFinished extends AccountEvent { @override List get props => [customer]; +} + +class AccountGetTrainees extends AccountEvent { + const AccountGetTrainees(); +} + +class AccountSelectTrainee extends AccountEvent { + final int traineeId; + const AccountSelectTrainee({this.traineeId}); + + @override + List get props => [traineeId]; } \ No newline at end of file diff --git a/lib/bloc/exercise_add_by_plan_bloc.dart b/lib/bloc/exercise_add_by_plan_bloc.dart new file mode 100644 index 0000000..3d72ae1 --- /dev/null +++ b/lib/bloc/exercise_add_by_plan_bloc.dart @@ -0,0 +1,81 @@ +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:aitrainer_app/repository/exercise_plan_repository.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class ExerciseAddByPlanFormBloc extends FormBloc { + final ExerciseRepository exerciseRepository; + final ExercisePlanRepository exercisePlanRepository; + final WorkoutTree workoutTree; + final customerId; + Customer customer; + int step = 1; + int countSteps = 1; + + final quantity1Field = TextFieldBloc( + ); + + final unitQuantity1Field = TextFieldBloc( + ); + + ExerciseAddByPlanFormBloc({this.exerciseRepository, this.exercisePlanRepository, this.customerId, this.workoutTree}) { + addFieldBlocs(fieldBlocs: [ + quantity1Field, + unitQuantity1Field, + ]); + + quantity1Field.onValueChanges(onData: (previous, current) async* { + exerciseRepository.setQuantity(current.valueToDouble); + }); + + unitQuantity1Field.onValueChanges(onData: (previous, current) async* { + exerciseRepository.setUnitQuantity(unitQuantity1Field.valueToDouble); + }); + + exerciseRepository.exerciseType = workoutTree.exerciseType; + if ( Cache().userLoggedIn.customerId == customerId) { + customer = Cache().userLoggedIn; + } else if ( Cache().getTrainee().customerId == customerId) { + customer = Cache().getTrainee(); + } + exercisePlanRepository.setActualPlanDetail(workoutTree.exerciseType); + exerciseRepository.customer = customer; + countSteps = exercisePlanRepository.actualPlanDetail.serie; + + unitQuantity1Field.updateInitialValue(exercisePlanRepository.actualPlanDetail.weightEquation); + quantity1Field.updateInitialValue(exercisePlanRepository.actualPlanDetail.repeats.toString()); + + } + + @override + void onSubmitting() async { + + try { + emitLoading(progress: 30); + step++; + + exerciseRepository.setUnitQuantity(unitQuantity1Field.valueToDouble); + exerciseRepository.setQuantity(quantity1Field.valueToDouble); + exerciseRepository.exercise.exercisePlanDetailId = + exercisePlanRepository.actualPlanDetail.exercisePlanDetailId; + exerciseRepository.exercise.unit = workoutTree.exerciseType.unit; + workoutTree.executed = true; + print("On Submitting Add Exercise By Plan " + exerciseRepository.exercise.toJson().toString()); + await exerciseRepository.addExercise(); + + emitSuccess(canSubmitAgain: true); + } on Exception catch (ex) { + emitFailure(failureResponse: ex.toString()); + } + } + + //@override + Future close() { + quantity1Field.close(); + unitQuantity1Field.close(); + + return super.close(); + } +} diff --git a/lib/bloc/exercise_by_plan/exercise_by_plan_bloc.dart b/lib/bloc/exercise_by_plan/exercise_by_plan_bloc.dart new file mode 100644 index 0000000..c852831 --- /dev/null +++ b/lib/bloc/exercise_by_plan/exercise_by_plan_bloc.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:aitrainer_app/repository/exercise_plan_repository.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/repository/menu_tree_repository.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +part 'exercise_by_plan_event.dart'; + +part 'exercise_by_plan_state.dart'; + +class ExerciseByPlanBloc extends Bloc { + final ExerciseRepository exerciseRepository; + final MenuTreeRepository menuTreeRepository; + final ExercisePlanRepository exercisePlanRepository = ExercisePlanRepository(); + int customerId; + @override + ExerciseByPlanBloc({this.exerciseRepository, this.menuTreeRepository}) : super(ExerciseByPlanStateInitial()); + + Future getData() async { + exercisePlanRepository.setCustomerId(customerId); + await exercisePlanRepository.getLastExercisePlan(); + await exercisePlanRepository.getExercisePlanDetails(); + menuTreeRepository.sortedTree = null; + menuTreeRepository.sortByMuscleType(); + + menuTreeRepository.sortedTree.forEach((key, value) { + List listWorkoutTree = value; + listWorkoutTree.forEach((workoutTree) { + workoutTree.selected = false; + if (exercisePlanRepository.exercisePlanDetails.length > 0) { + if (exercisePlanRepository.exercisePlanDetails[workoutTree.exerciseTypeId] != null) { + workoutTree.selected = true; + } + } + }); + }); + } + + @override + Stream mapEventToState(ExerciseByPlanEvent event) async* { + try { + if (event is ExerciseByPlanLoad) { + yield ExerciseByPlanLoading(); + await this.getData(); + yield ExerciseByPlanReady(); + } else if (event is AddExerciseByPlanEvent) { + yield ExerciseByPlanLoading(); + yield ExerciseByPlanReady(); + } + } on Exception catch (e) { + yield ExerciseByPlanError(message: e.toString()); + } + } +} diff --git a/lib/bloc/exercise_by_plan/exercise_by_plan_event.dart b/lib/bloc/exercise_by_plan/exercise_by_plan_event.dart new file mode 100644 index 0000000..97c1c14 --- /dev/null +++ b/lib/bloc/exercise_by_plan/exercise_by_plan_event.dart @@ -0,0 +1,21 @@ +part of 'exercise_by_plan_bloc.dart'; + +@immutable +abstract class ExerciseByPlanEvent extends Equatable { + const ExerciseByPlanEvent(); + + @override + List get props => []; +} + +class AddExerciseByPlanEvent extends ExerciseByPlanEvent { + final ExerciseType exerciseType; + const AddExerciseByPlanEvent({this.exerciseType}); + + @override + List get props => [exerciseType]; +} + +class ExerciseByPlanLoad extends ExerciseByPlanEvent { + const ExerciseByPlanLoad(); +} diff --git a/lib/bloc/exercise_by_plan/exercise_by_plan_state.dart b/lib/bloc/exercise_by_plan/exercise_by_plan_state.dart new file mode 100644 index 0000000..f9c89c7 --- /dev/null +++ b/lib/bloc/exercise_by_plan/exercise_by_plan_state.dart @@ -0,0 +1,31 @@ +part of 'exercise_by_plan_bloc.dart'; + +@immutable +abstract class ExerciseByPlanState extends Equatable { + const ExerciseByPlanState(); + + @override + List get props => []; +} + +class ExerciseByPlanStateInitial extends ExerciseByPlanState { + const ExerciseByPlanStateInitial(); +} + +class ExerciseByPlanLoading extends ExerciseByPlanState { + const ExerciseByPlanLoading(); +} + +// updated screen +class ExerciseByPlanReady extends ExerciseByPlanState { + const ExerciseByPlanReady(); +} + +// error splash screen +class ExerciseByPlanError extends ExerciseByPlanState { + final String message; + const ExerciseByPlanError({this.message}); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/bloc/exercise_plan/exercise_plan_bloc.dart b/lib/bloc/exercise_plan/exercise_plan_bloc.dart new file mode 100644 index 0000000..5c88210 --- /dev/null +++ b/lib/bloc/exercise_plan/exercise_plan_bloc.dart @@ -0,0 +1,89 @@ +import 'dart:async'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:aitrainer_app/repository/exercise_plan_repository.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/repository/menu_tree_repository.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +part 'exercise_plan_event.dart'; + +part 'exercise_plan_state.dart'; + +class ExercisePlanBloc extends Bloc { + final MenuTreeRepository menuTreeRepository; + final ExerciseRepository exerciseRepository; + final ExercisePlanRepository exercisePlanRepository = ExercisePlanRepository(); + int customerId; + + ExercisePlanBloc({this.exerciseRepository, this.menuTreeRepository}) : super(ExercisePlanInitial()); + + Future getData() async { + exercisePlanRepository.setCustomerId(customerId); + await exercisePlanRepository.getLastExercisePlan(); + await exercisePlanRepository.getExercisePlanDetails(); + menuTreeRepository.sortedTree = null; + menuTreeRepository.sortByMuscleType(); + + menuTreeRepository.sortedTree.forEach((key, value) { + List listWorkoutTree = value; + listWorkoutTree.forEach((workoutTree) { + workoutTree.selected = false; + if (exercisePlanRepository.exercisePlanDetails.length > 0) { + if (exercisePlanRepository.exercisePlanDetails[workoutTree.exerciseTypeId] != null) { + //print("bingo"); + workoutTree.selected = true; + } + } + }); + }); + } + + @override + Stream mapEventToState(ExercisePlanEvent event) async* { + try { + if (event is ExercisePlanLoad) { + yield ExercisePlanLoading(); + await this.getData(); + yield ExercisePlanReady(); + } + if (event is ExercisePlanUpdate) { + yield ExercisePlanLoading(); + WorkoutTree workoutTree = event.workoutTree; + if (workoutTree != null) { + exercisePlanRepository.addExerciseTypeToPlan(workoutTree.exerciseType); + workoutTree.selected = true; + } + yield ExercisePlanReady(); + } else if (event is ExercisePlanRemoveExercise) { + yield ExercisePlanLoading(); + ExercisePlanDetail planDetail = event.exercisePlanDetail; + exercisePlanRepository.removeExerciseTypeFromPlan(planDetail.exerciseType); + this.menuTreeRepository.sortedTree.forEach((key, value) { + List listTreeItem = value; + listTreeItem.forEach((element) { + if ( element.exerciseType.exerciseTypeId == planDetail.exerciseTypeId) { + element.selected = false; + } + }); + }); + yield ExercisePlanReady(); + } else if (event is ExercisePlanUpdateExercise) { + yield ExercisePlanReady(); + } else if (event is ExercisePlanSave) { + if (exercisePlanRepository.getExercisePlanDetailSize() == 0) { + throw Exception("Please select an exercise"); + } else { + yield ExercisePlanLoading(); + exercisePlanRepository.saveExercisePlan(); + yield ExercisePlanReady(); + } + } + } on Exception catch (e) { + yield ExercisePlanError(message: e.toString()); + } + } +} diff --git a/lib/bloc/exercise_plan/exercise_plan_event.dart b/lib/bloc/exercise_plan/exercise_plan_event.dart new file mode 100644 index 0000000..0faf92a --- /dev/null +++ b/lib/bloc/exercise_plan/exercise_plan_event.dart @@ -0,0 +1,49 @@ +part of 'exercise_plan_bloc.dart'; + +@immutable +abstract class ExercisePlanEvent extends Equatable { + const ExercisePlanEvent(); + @override + List get props => []; +} + +class ExercisePlanLoad extends ExercisePlanEvent { + const ExercisePlanLoad(); +} + +class ExercisePlanUpdate extends ExercisePlanEvent { + final WorkoutTree workoutTree; + const ExercisePlanUpdate({this.workoutTree}); + + @override + List get props => [workoutTree]; +} + +class ExercisePlanAddExercise extends ExercisePlanEvent { + final ExerciseType exerciseType; + const ExercisePlanAddExercise({this.exerciseType}); + + @override + List get props => [exerciseType]; +} + +class ExercisePlanRemoveExercise extends ExercisePlanEvent { + final ExercisePlanDetail exercisePlanDetail; + const ExercisePlanRemoveExercise({this.exercisePlanDetail}); + + @override + List get props => [exercisePlanDetail]; +} + +class ExercisePlanUpdateExercise extends ExercisePlanEvent { + final ExercisePlanDetail exercisePlanDetail; + const ExercisePlanUpdateExercise({this.exercisePlanDetail}); + + @override + List get props => [exercisePlanDetail]; +} + +class ExercisePlanSave extends ExercisePlanEvent { + const ExercisePlanSave(); +} + diff --git a/lib/bloc/exercise_plan/exercise_plan_state.dart b/lib/bloc/exercise_plan/exercise_plan_state.dart new file mode 100644 index 0000000..131a0e6 --- /dev/null +++ b/lib/bloc/exercise_plan/exercise_plan_state.dart @@ -0,0 +1,37 @@ +part of 'exercise_plan_bloc.dart'; + +@immutable +abstract class ExercisePlanState extends Equatable{ + const ExercisePlanState(); + + @override + List get props => []; +} + +// Display the last saved exercise plan +class ExercisePlanInitial extends ExercisePlanState { + const ExercisePlanInitial(); +} + +// Loading screen +class ExercisePlanLoading extends ExercisePlanState { + const ExercisePlanLoading(); +} + +// updated screen +class ExercisePlanReady extends ExercisePlanState { + const ExercisePlanReady(); +} + +// error splash screen +class ExercisePlanError extends ExercisePlanState { + final String message; + const ExercisePlanError({this.message}); + + @override + List get props => [message]; +} + + + + diff --git a/lib/bloc/exercise_plan_custom_form.dart b/lib/bloc/exercise_plan_custom_form.dart new file mode 100644 index 0000000..43e714e --- /dev/null +++ b/lib/bloc/exercise_plan_custom_form.dart @@ -0,0 +1,94 @@ +import 'package:aitrainer_app/bloc/exercise_plan/exercise_plan_bloc.dart'; +import 'package:aitrainer_app/repository/exercise_plan_repository.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class ExercisePlanCustomerFormBloc extends FormBloc { + final ExercisePlanRepository exercisePlanRepository; + final ExercisePlanBloc planBloc; + final serieField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + final quantityField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + final weightField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + final exerciseTypeField = TextFieldBloc( + validators: [ + FieldBlocValidators.required, + ], + ); + + ExercisePlanCustomerFormBloc({this.exercisePlanRepository, this.planBloc}) { + addFieldBlocs(fieldBlocs: [ + quantityField, + serieField, + weightField + ]); + + String repeatsInitial = exercisePlanRepository.actualPlanDetail != null && + exercisePlanRepository.actualPlanDetail.repeats != null ? + exercisePlanRepository.actualPlanDetail.repeats.toString() : "12"; + quantityField.updateInitialValue(repeatsInitial); + + String serieInitial = exercisePlanRepository.actualPlanDetail != null && + exercisePlanRepository.actualPlanDetail.serie != null ? + exercisePlanRepository.actualPlanDetail.serie.toString() : "3"; + serieField.updateInitialValue(serieInitial); + + String weightInitial = exercisePlanRepository.actualPlanDetail != null && + exercisePlanRepository.actualPlanDetail.weightEquation != null ? + exercisePlanRepository.actualPlanDetail.weightEquation : "30"; + weightField.updateInitialValue(weightInitial); + + quantityField.onValueChanges(onData: (previous, current) async* { + exercisePlanRepository.actualPlanDetail.repeats = current.valueToInt; + }); + serieField.onValueChanges(onData: (previous, current) async* { + exercisePlanRepository.actualPlanDetail.serie = current.valueToInt; + }); + weightField.onValueChanges(onData: (previous, current) async* { + exercisePlanRepository.actualPlanDetail.weightEquation = current.value; + }); + } + + @override + void onSubmitting() async { + print("On Submitting Custom Plan form"); + try { + emitLoading(progress: 30); + // Emit either Loaded or Error + + exercisePlanRepository.actualPlanDetail.repeats = quantityField.valueToInt; + exercisePlanRepository.actualPlanDetail.serie = serieField.valueToInt; + exercisePlanRepository.actualPlanDetail.weightEquation = weightField.value; + + exercisePlanRepository.addToPlan(); + planBloc.add(ExercisePlanUpdate()); + + emitSuccess(canSubmitAgain: false); + } on Exception catch (ex) { + emitFailure(failureResponse: ex.toString()); + } + } + + + //@override + Future close() { + quantityField.close(); + serieField.close(); + weightField.close(); + exerciseTypeField.close(); + return super.close(); + } +} diff --git a/lib/bloc/settings/settings_bloc.dart b/lib/bloc/settings/settings_bloc.dart index af2d4b5..fb6db71 100644 --- a/lib/bloc/settings/settings_bloc.dart +++ b/lib/bloc/settings/settings_bloc.dart @@ -34,6 +34,10 @@ class SettingsBloc extends Bloc { yield SettingsLoading(); await _changeLang( event.language); yield SettingsReady(_locale); + } else if ( event is SettingsGetLanguage) { + await AppLanguage().fetchLocale(); + _locale = AppLanguage().appLocal; + yield SettingsReady(_locale); } } @@ -50,12 +54,13 @@ class SettingsBloc extends Bloc { break; } this.language = lang; + final AppLanguage appLanguage = AppLanguage(); + appLanguage.changeLanguage(_locale); await loadLang(); } Future loadLang() async{ - final AppLanguage appLanguage = AppLanguage(); - appLanguage.changeLanguage(_locale); + print (" -- Loading lang $_locale"); if ( context != null ) { AppLocalizations.of(context).setLocale(_locale); await AppLocalizations.of(context).load(); diff --git a/lib/bloc/settings/settings_event.dart b/lib/bloc/settings/settings_event.dart index 31a8279..c0f741b 100644 --- a/lib/bloc/settings/settings_event.dart +++ b/lib/bloc/settings/settings_event.dart @@ -11,3 +11,7 @@ class SettingsChangeLanguage extends SettingsEvent { final String language; const SettingsChangeLanguage({this.language}); } + +class SettingsGetLanguage extends SettingsEvent { + const SettingsGetLanguage(); +} diff --git a/lib/localization/app_language.dart b/lib/localization/app_language.dart index 11fad94..49bf4f8 100644 --- a/lib/localization/app_language.dart +++ b/lib/localization/app_language.dart @@ -20,19 +20,24 @@ class AppLanguage{ Future fetchLocale() async { var prefs = await SharedPreferences.getInstance(); - if (prefs.getString('language_code') == null) { + String langCode = prefs.getString('language_code'); + print(" ---- lang code $langCode"); + if ( langCode == null) { _appLocale = Locale('en'); } else { - _appLocale = Locale(prefs.getString('language_code')); + _appLocale = Locale(langCode); } print(" ---- Fetched lang: " + _appLocale.toString()); } getLocale(SharedPreferences prefs) { - if (prefs.getString('language_code') == null) { + String langCode = prefs.getString('language_code'); + if ( langCode == null) { _appLocale = Locale('en'); } - _appLocale = Locale(prefs.getString('language_code')); + _appLocale = Locale(langCode); + print(" ---- Get lang: " + _appLocale.toString() + " lang code $langCode"); + } diff --git a/lib/localization/app_localization.dart b/lib/localization/app_localization.dart index 181ba08..ab032e3 100644 --- a/lib/localization/app_localization.dart +++ b/lib/localization/app_localization.dart @@ -28,6 +28,7 @@ class AppLocalizations { Future load() async { // Load the language JSON file from the "lang" folder + print(" -- load language pieces " + locale.languageCode); String jsonString = await rootBundle.loadString('i18n/${locale.languageCode}.json'); Map jsonMap = json.decode(jsonString); diff --git a/lib/main.dart b/lib/main.dart index 213155a..f82cbc5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; import 'package:aitrainer_app/repository/menu_tree_repository.dart'; import 'package:aitrainer_app/util/session.dart'; import 'package:aitrainer_app/view/account.dart'; @@ -9,13 +10,19 @@ import 'package:aitrainer_app/view/customer_fitness_page.dart'; import 'package:aitrainer_app/view/customer_goal_page.dart'; import 'package:aitrainer_app/view/customer_modify_page.dart'; import 'package:aitrainer_app/view/customer_welcome_page.dart'; +import 'package:aitrainer_app/view/exercise_add_by_plan_page.dart'; import 'package:aitrainer_app/view/exercise_control_page.dart'; +import 'package:aitrainer_app/view/exercise_execute_by_plan_page.dart'; +import 'package:aitrainer_app/view/exercise_log_page.dart'; +import 'package:aitrainer_app/view/exercise_plan_custom_page.dart'; +import 'package:aitrainer_app/view/exercise_plan_detail_add_page.dart'; import 'package:aitrainer_app/view/exercise_type_description.dart'; import 'package:aitrainer_app/view/gdpr.dart'; import 'package:aitrainer_app/view/login.dart'; import 'package:aitrainer_app/view/exercise_new_page.dart'; import 'package:aitrainer_app/view/menu_page.dart'; import 'package:aitrainer_app/view/mydevelopment_page.dart'; +import 'package:aitrainer_app/view/myexcercise_plan_page.dart'; import 'package:aitrainer_app/view/registration.dart'; import 'package:aitrainer_app/view/settings.dart'; import 'package:aitrainer_app/widgets/home.dart'; @@ -27,6 +34,8 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; import 'package:sentry/sentry.dart'; import 'bloc/account/account_bloc.dart'; +import 'bloc/exercise_by_plan/exercise_by_plan_bloc.dart'; +import 'bloc/exercise_plan/exercise_plan_bloc.dart'; import 'bloc/menu/menu_bloc.dart'; import 'bloc/session/session_bloc.dart'; import 'bloc/settings/settings_bloc.dart'; @@ -96,14 +105,15 @@ Future main() async { // - https://api.dartlang.org/stable/1.24.2/dart-async/Zone-class.html // - https://www.dartlang.org/articles/libraries/zones runZonedGuarded>(() async { + final MenuTreeRepository menuTreeRepository = MenuTreeRepository(); runApp( MultiBlocProvider( - providers: [ + providers: [ BlocProvider( create: (BuildContext context) => SessionBloc(session: Session()), ), BlocProvider( - create: (BuildContext context) => MenuBloc( menuTreeRepository: MenuTreeRepository()), + create: (BuildContext context) => MenuBloc( menuTreeRepository: menuTreeRepository), ), BlocProvider( create: (BuildContext context) => SettingsBloc(), @@ -111,6 +121,12 @@ Future main() async { BlocProvider( create: (BuildContext context) => AccountBloc(customerRepository: CustomerRepository()), ), + BlocProvider( + create: (BuildContext context) => ExercisePlanBloc(menuTreeRepository: menuTreeRepository), + ), + BlocProvider( + create: (BuildContext context) => ExerciseByPlanBloc(menuTreeRepository: menuTreeRepository, exerciseRepository: ExerciseRepository()), + ), ], child: AitrainerApp(), @@ -172,10 +188,16 @@ class AitrainerApp extends StatelessWidget { 'account': (context) => AccountPage(), 'settings': (context) => SettingsPage(), 'exerciseTypeDescription': (context) => ExerciseTypeDescription(), - 'mydevelopment': (context) => MyDevelopmentPage(), + 'myDevelopment': (context) => MyDevelopmentPage(), + 'myExercisePlan': (context) => MyExercisePlanPage(), + 'exerciseLogPage': (context) => ExerciseLogPage(), + 'exercisePlanCustomPage': (context) => ExercisePlanCustomPage(), + 'exercisePlanDetailAdd': (context) => ExercisePlanDetailAddPage(), + 'exerciseByPlanPage': (context) => ExerciseByPlanPage(), + 'exerciseAddByPlanPage': (context) => ExerciseAddByPlanPage(), }, initialRoute: 'home', - title: 'Aitrainer', + title: 'WorkoutTest', theme: ThemeData( brightness: Brightness.light, //primarySwatch: Colors.transparent, diff --git a/lib/model/cache.dart b/lib/model/cache.dart index af29450..e58ac1c 100644 --- a/lib/model/cache.dart +++ b/lib/model/cache.dart @@ -1,6 +1,7 @@ import 'dart:collection'; - import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/model/exercise_plan.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; import 'package:aitrainer_app/model/exercise_tree.dart'; import 'package:aitrainer_app/model/exercise.dart'; import 'package:aitrainer_app/model/workout_tree.dart'; @@ -55,10 +56,19 @@ class Cache { List _exerciseTypes; List _exerciseTree; + List _exercises; + ExercisePlan _myExercisePlan; + List _myExercisesPlanDetail; + LinkedHashMap _tree = LinkedHashMap(); double _percentExercises = -1; + Customer _trainee; + List _exercisesTrainee; + ExercisePlan _traineeExercisePlan; + List _traineeExercisesPlanDetail; + List deviceLanguages; String startPage; @@ -73,6 +83,10 @@ class Cache { } } + void setTestBaseUrl() { + baseUrl = 'http://aitrainer.app:8899/api/'; + } + String getAuthToken() { return this.authToken; } @@ -106,6 +120,12 @@ class Cache { logout() async { userLoggedIn = null; authToken = ""; + _trainee = null; + _percentExercises = -1; + _exercisesTrainee = null; + _traineeExercisePlan = null; + _exercises = List(); + print("Trainees is null? " + (_trainee == null).toString() ); //firstLoad = true; Future prefs = SharedPreferences.getInstance(); await setPreferences(prefs, SharePrefsChange.logout, 0); @@ -154,6 +174,10 @@ class Cache { this._exercises = exercises; } + void setExercisesTrainee( List exercises ) { + this._exercisesTrainee = exercises; + } + void setWorkoutTree( LinkedHashMap tree) { this._tree = tree; } @@ -170,6 +194,10 @@ class Cache { return this._exercises; } + List getExercisesTrainee() { + return this._exercisesTrainee; + } + LinkedHashMap getWorkoutTree() { return this._tree; } @@ -186,4 +214,20 @@ class Cache { _exercises.add(exercise); } + void addExerciseTrainee(Exercise exercise) { + _exercisesTrainee.add(exercise); + } + + Customer getTrainee() { + return this._trainee; + } + + void setTrainee(Customer trainee) { + this._trainee = trainee; + } + + void setTraineeExercisePlan(ExercisePlan exercisePlan) { + this._traineeExercisePlan = exercisePlan; + } + } \ No newline at end of file diff --git a/lib/model/customer.dart b/lib/model/customer.dart index 6063fb9..23444a6 100644 --- a/lib/model/customer.dart +++ b/lib/model/customer.dart @@ -13,6 +13,7 @@ class Customer { String fitnessLevel; String bodyType; int admin; + int trainer; int dataPolicyAllowed; @@ -30,6 +31,7 @@ class Customer { this.goal, this.weight, this.admin, + this.trainer, this.dataPolicyAllowed }); @@ -47,6 +49,7 @@ class Customer { this.goal = json['goal']; this.weight = json['weight']; this.admin = json['admin']; + this.trainer = json['trainer']; } Map toJson() => @@ -64,6 +67,7 @@ class Customer { "goal": goal, "weight": weight, "admin": admin, + "trainer": trainer, "dataPolicyAllowed": dataPolicyAllowed, }; } diff --git a/lib/model/exercise.dart b/lib/model/exercise.dart index 3e16abb..e3b7dd0 100644 --- a/lib/model/exercise.dart +++ b/lib/model/exercise.dart @@ -8,6 +8,7 @@ class Exercise { String unit; double unitQuantity; DateTime dateAdd; + int exercisePlanDetailId; @@ -30,5 +31,6 @@ class Exercise { "unit": unit, "unitQuantity": unitQuantity, "dateAdd": DateFormat('yyyy-MM-dd HH:mm:ss').format(this.dateAdd), + "exercisePlanDetailId": exercisePlanDetailId, }; } \ No newline at end of file diff --git a/lib/model/exercise_plan.dart b/lib/model/exercise_plan.dart new file mode 100644 index 0000000..b2b77d8 --- /dev/null +++ b/lib/model/exercise_plan.dart @@ -0,0 +1,58 @@ +import 'package:intl/intl.dart'; + +class ExercisePlan { + int exercisePlanId; + int customerId; + String name; + String description; + bool private; + DateTime dateAdd; + DateTime dateUpd; + + ExercisePlan(String name, int customerId) { + this.customerId = customerId; + this.name = name; + this.dateUpd = DateTime.now(); + } + + ExercisePlan.fromJson(Map json) { + this.exercisePlanId = json['exercisePlanId']; + this.customerId = json['customerId']; + this.name = json['name']; + this.private = json['private']; + this.description = json['description']; + this.dateAdd = json['dateAdd'] == null ? null : DateTime.parse( json['dateAdd'] ); + this.dateUpd = json['dateUpd'] == null ? null : DateTime.parse( json['dateUpd'] ); + } + + Map toJson() { + String formattedDateAdd; + if ( dateAdd != null) { + formattedDateAdd = DateFormat('yyyy-MM-dd HH:mm').format(dateAdd); + } + String formattedDateUpd = DateFormat('yyyy-MM-dd HH:mm').format(dateUpd); + + print("DateAdd $formattedDateAdd"); + if ( exercisePlanId == null ) { + return { + "customerId": customerId, + "name": name, + "description": description, + "private": private, + "dateAdd": formattedDateAdd, + "dateUpd": formattedDateUpd, + }; + } else { + return { + "exercisePlanId": exercisePlanId, + "customerId": customerId, + "name": name, + "description": description, + "private": private, + "dateAdd": formattedDateAdd, + "dateUpd": formattedDateUpd, + }; + } + } + +} \ No newline at end of file diff --git a/lib/model/exercise_plan_detail.dart b/lib/model/exercise_plan_detail.dart new file mode 100644 index 0000000..eac325d --- /dev/null +++ b/lib/model/exercise_plan_detail.dart @@ -0,0 +1,36 @@ +import 'package:aitrainer_app/model/exercise_plan.dart'; + +import 'exercise_type.dart'; + +class ExercisePlanDetail { + int exercisePlanDetailId; + int exercisePlanId; + int exerciseTypeId; + int serie; + int repeats; + String weightEquation; + + ExerciseType exerciseType; + + ExercisePlanDetail(int exerciseTypeId) { + this.exerciseTypeId = exerciseTypeId; + } + + ExercisePlanDetail.fromJson(Map json) { + this.exercisePlanDetailId = json['exercisePlanDetailId']; + this.exercisePlanId = json['exercisePlanId']; + this.exerciseTypeId = json['exerciseTypeId']; + this.serie = json['serie']; + this.repeats = json['repeats']; + this.weightEquation = json['weightEquation']; + } + + Map toJson() => + { + "exercisePlanId": exercisePlanId, + "exerciseTypeId": exerciseTypeId, + "serie": serie, + "repeats": repeats, + "weightEquation": weightEquation + }; +} \ No newline at end of file diff --git a/lib/model/workout_tree.dart b/lib/model/workout_tree.dart index d07bb92..dae5454 100644 --- a/lib/model/workout_tree.dart +++ b/lib/model/workout_tree.dart @@ -5,7 +5,7 @@ import 'exercise_type.dart'; class WorkoutTree { int id; int parent; - String name; // is also the key + String name; String imageName; Color color; double fontSize; @@ -15,7 +15,26 @@ class WorkoutTree { bool base; bool is1RM; + bool selected = false; + bool executed = false; + String exerciseDetail; + String nameEnglish; - WorkoutTree(this.id, this.parent, this.name, this.imageName, this.color, this.fontSize, this.child, this.exerciseTypeId, this.exerciseType, this.base, this.is1RM); + WorkoutTree(this.id, this.parent, this.name, this.imageName, this.color, this.fontSize, this.child, + this.exerciseTypeId, this.exerciseType, this.base, this.is1RM, this.nameEnglish); + Map toJson() { + return { + "id": id, + "parent": parent, + "name": name, + "imageName": imageName, + "color": color.toString(), + "fontSize": fontSize.toString(), + "child": child.toString(), + "exerciseTypeId": exerciseTypeId.toString(), + "base": base.toString(), + "is1RM": is1RM.toString(), + }; + } } diff --git a/lib/repository/customer_repository.dart b/lib/repository/customer_repository.dart index 4226a2c..4c0b520 100644 --- a/lib/repository/customer_repository.dart +++ b/lib/repository/customer_repository.dart @@ -1,3 +1,4 @@ +import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/model/customer.dart'; import 'package:aitrainer_app/service/customer_service.dart'; @@ -9,6 +10,9 @@ class GenderItem { class CustomerRepository { Customer customer; + Customer _trainee; + List _trainees; + //List customerList = List(); bool visibleDetails = false; List genders; @@ -131,13 +135,53 @@ class CustomerRepository { await CustomerApi().saveCustomer(modelCustomer); } - /* Future> getCustomers() async { - final results = await CustomerApi().getRealCustomers(""); - this.customerList = results.map((item) => CustomerRepository(customer: item)).toList(); - return this.customerList; + Future getTraineeAsCustomer() async { + this._trainee = await CustomerApi().getTrainee( + Cache().userLoggedIn.customerId + ); + return _trainee; } - addNewCustomerToList(CustomerRepository customerViewModel) { - customerList.add(customerViewModel); - }*/ + Future> getTrainees() async { + int trainerId = Cache().userLoggedIn.customerId; + final results = await CustomerApi().getTrainees(trainerId); + this._trainees = results; + return results; + } + + List getTraineesList() { + return _trainees; + } + + void setTrainee(int traineeId ) { + if ( _trainees == null ) { + return; + } + _trainees.forEach((element) { + if ( traineeId == element.customerId) { + this._trainee = element; + } + }); + } + + void emptyTrainees() { + _trainees = null; + _trainee = null; + } + + Customer getTrainee() { + return this._trainee; + } + + Customer getTraineeById(int customerId) { + if (_trainees == null) { + return null; + } + _trainees.forEach((element) { + if ( customerId == element.customerId) { + this._trainee = element; + } + }); + return _trainee; + } } diff --git a/lib/repository/exercise_plan_repository.dart b/lib/repository/exercise_plan_repository.dart new file mode 100644 index 0000000..61df4f4 --- /dev/null +++ b/lib/repository/exercise_plan_repository.dart @@ -0,0 +1,167 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/exercise_plan.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/service/exercise_plan_service.dart'; + +class ExercisePlanRepository { + bool newPlan = true; + ExercisePlan _exercisePlan; + LinkedHashMap exercisePlanDetails = + LinkedHashMap(); + LinkedHashMap _origExercisePlanDetails = + LinkedHashMap(); + int _customerId = 0; + ExercisePlanDetail actualPlanDetail; + + void setCustomerId( int customerId ) { + this._customerId = customerId; + } + + int getCustomerId() { + return this._customerId; + } + + void addExerciseTypeToPlan(ExerciseType exerciseType) { + setActualPlanDetail(exerciseType); + } + + void addToPlan() { + exercisePlanDetails[actualPlanDetail.exerciseTypeId] = actualPlanDetail; + } + + void setActualPlanDetail(ExerciseType exerciseType) { + ExercisePlanDetail detail = exercisePlanDetails[exerciseType.exerciseTypeId]; + if ( detail != null ) { + actualPlanDetail = detail; + } else { + actualPlanDetail = ExercisePlanDetail(exerciseType.exerciseTypeId); + } + actualPlanDetail.exerciseType = exerciseType; + } + + int getPlanDetailId(int exerciseTypeId) { + ExercisePlanDetail detail = exercisePlanDetails[exerciseTypeId]; + return detail.exercisePlanDetailId; + } + + String getPlanDetail(int exerciseTypeId) { + ExercisePlanDetail detail = exercisePlanDetails[exerciseTypeId]; + String detailString = ""; + if ( detail != null) { + detailString = + detail.serie.toString() + "x" + detail.repeats.toString() + " " + + detail.weightEquation + "kg"; + } + return detailString; + } + + int getExercisePlanDetailSize() { + return exercisePlanDetails.length; + } + + void updateExercisePlanDetail(ExerciseType exerciseType, int serie, int repeat, String weight) { + if ( exercisePlanDetails[exerciseType.exerciseTypeId] == null) { + return; + } + ExercisePlanDetail exercisePlanDetail = exercisePlanDetails[exerciseType.exerciseTypeId]; + exercisePlanDetail.serie = serie; + exercisePlanDetail.repeats = repeat; + exercisePlanDetail.weightEquation = weight; + + exercisePlanDetails[exerciseType.exerciseTypeId] = exercisePlanDetail; + } + + void removeExerciseTypeFromPlan(ExerciseType exerciseType) { + exercisePlanDetails.remove(exerciseType.exerciseTypeId); + } + + Future saveExercisePlan() async { + + if ( _exercisePlan == null ) { + if ( Cache().userLoggedIn == null ) { + throw Exception("please log in"); + } + + String exercisePlanName; + if ( this._customerId == Cache().userLoggedIn.customerId) { + exercisePlanName = Cache().userLoggedIn.name + " private"; + } else { + exercisePlanName = Cache().getTrainee().name + " " + Cache().getTrainee().firstname + " private"; + } + + _exercisePlan = ExercisePlan(exercisePlanName, this._customerId); + } + if ( newPlan ) { + _exercisePlan.dateAdd = DateTime.now(); + _exercisePlan.private = true; + ExercisePlan savedExercisePlan = + await ExercisePlanApi().saveExercisePlan(_exercisePlan); + + exercisePlanDetails.forEach((exerciseTypeId, exercisePlanDetail) async { + exercisePlanDetail.exercisePlanId = savedExercisePlan.exercisePlanId; + await ExercisePlanApi().saveExercisePlanDetail(exercisePlanDetail); + }); + } else { + + await ExercisePlanApi().updateExercisePlan(_exercisePlan, _exercisePlan.exercisePlanId); + + _origExercisePlanDetails.forEach((exerciseTypeId, exercisePlanDetail) async { + if (exercisePlanDetails[exercisePlanDetail.exercisePlanDetailId] == null) { + await ExercisePlanApi() + .deleteExercisePlanDetail(exercisePlanDetail.exercisePlanDetailId); + } else { + await ExercisePlanApi() + .updateExercisePlanDetail(exercisePlanDetail, exercisePlanDetail.exercisePlanDetailId); + } + }); + + exercisePlanDetails.forEach((exerciseTypeId, exercisePlanDetail) async{ + exercisePlanDetail.exercisePlanId = _exercisePlan.exercisePlanId; + if ( _origExercisePlanDetails[exercisePlanDetail.exercisePlanDetailId] == null) { + await ExercisePlanApi() + .saveExercisePlanDetail(exercisePlanDetail); + } + }); + } + } + + Future getLastExercisePlan() async { + if ( _customerId == 0) { + return null; + } + _exercisePlan = await ExercisePlanApi().getLastExercisePlan(_customerId); + newPlan = (_exercisePlan == null); + print("New plan: " + newPlan.toString()); + + return _exercisePlan; + } + + Future getExercisePlanDetails() async { + if (_exercisePlan == null) { + ExercisePlan exercisePlan = await this.getLastExercisePlan(); + if ( exercisePlan == null ) { + exercisePlanDetails = LinkedHashMap(); + _origExercisePlanDetails = LinkedHashMap(); + return; + } + } + exercisePlanDetails = LinkedHashMap(); + _origExercisePlanDetails = LinkedHashMap(); + + List list = + await ExercisePlanApi().getExercisePlanDetail(_exercisePlan.exercisePlanId); + + list.forEach((element) { + newPlan = false; + ExercisePlanDetail detail = element; + _origExercisePlanDetails[detail.exerciseTypeId] = detail; + exercisePlanDetails[detail.exerciseTypeId] = detail; + }); + + return; + } + +} \ No newline at end of file diff --git a/lib/repository/exercise_repository.dart b/lib/repository/exercise_repository.dart index 885305f..244d614 100644 --- a/lib/repository/exercise_repository.dart +++ b/lib/repository/exercise_repository.dart @@ -72,8 +72,12 @@ class ExerciseRepository { final Exercise modelExercise = this.exercise; modelExercise.customerId = this.customer.customerId; modelExercise.exerciseTypeId = this.exerciseType.exerciseTypeId; - await ExerciseApi().addExercise(modelExercise); - Cache().addExercise(exercise); + Exercise savedExercise = await ExerciseApi().addExercise(modelExercise); + if ( customer.customerId == Cache().userLoggedIn.customerId) { + Cache().addExercise(savedExercise); + } else if ( Cache().getTrainee() != null && customer.customerId == Cache().getTrainee().customerId ) { + Cache().addExerciseTrainee(savedExercise); + } } @@ -89,15 +93,26 @@ class ExerciseRepository { Future> getExercisesByCustomer( int customerId ) async { final results = await ExerciseApi().getExercisesByCustomer(customerId); this.exerciseList = results; - Cache().setExercises(exerciseList); + if ( customerId == Cache().userLoggedIn.customerId) { + Cache().setExercises(exerciseList); + } else if ( Cache().getTrainee() != null && customerId == Cache().getTrainee().customerId ) { + Cache().setExercisesTrainee(exerciseList); + } return this.exerciseList; } List getExerciseList() { - if ( this.exerciseList == null || this.exerciseList.length == 0 ) { - this.exerciseList = Cache().getExercises(); - } - return this.exerciseList; + //if ( this.exerciseList == null || this.exerciseList.length == 0 ) { + return this.exerciseList = Cache().getExercises(); + //} + //return this.exerciseList; + } + + List getExerciseListTrainee() { + //if ( this.exerciseList == null || this.exerciseList.length == 0 ) { + return this.exerciseList = Cache().getExercisesTrainee(); + //} + //return this.exerciseList; } void getBaseExerciseFinishedPercent() { diff --git a/lib/repository/menu_tree_repository.dart b/lib/repository/menu_tree_repository.dart index 0526e0c..1e0d584 100644 --- a/lib/repository/menu_tree_repository.dart +++ b/lib/repository/menu_tree_repository.dart @@ -9,12 +9,44 @@ import 'package:aitrainer_app/service/exercise_tree_service.dart'; import 'package:aitrainer_app/service/exercisetype_service.dart'; import 'package:flutter/material.dart'; +class Antagonist { + static String chest = "Chest"; + static int chestNr = 1; + static String biceps = "Biceps"; + static int bicepsNr = 2; + static String triceps = "Triceps"; + static int tricepsNr =3; + static String back = "Back"; + static int backNr = 4; + static String shoulder = "Shoulders"; + static int shoulderNr = 5; + static String core = "Core"; + static int coreNr = 6; + static String thigh = "Thigh"; + static int thighNr = 7; + static String calf = "Calf"; + static int calfNr = 8; +} + class MenuTreeRepository { final LinkedHashMap tree = LinkedHashMap(); + SplayTreeMap sortedTree = SplayTreeMap>(); + bool isEnglish; + + final Map _antagonist = { + Antagonist.chest: Antagonist.chestNr, + Antagonist.biceps: Antagonist.bicepsNr, + Antagonist.triceps: Antagonist.tricepsNr, + Antagonist.back: Antagonist.backNr, + Antagonist.shoulder: Antagonist.shoulderNr, + Antagonist.core: Antagonist.coreNr, + Antagonist.thigh: Antagonist.thighNr, + Antagonist.calf: Antagonist.calfNr + }; Future createTree() async { final AppLanguage appLanguage = AppLanguage(); - bool isEnglish = appLanguage.appLocal == Locale('en'); + isEnglish = appLanguage.appLocal == Locale('en'); print("** Start creating tree on lang: " + appLanguage.appLocal.toString()); List exerciseTree = Cache().getExerciseTree(); @@ -25,7 +57,7 @@ class MenuTreeRepository { exerciseTree.forEach( (treeItem) async { String treeName = isEnglish ? treeItem.name : treeItem.nameTranslation; String assetImage = 'asset/menu/' + treeItem.imageUrl.substring(7); - bool is1RM = treeItem.name == '1RM' ? true : false; + bool is1RM = treeItem.name == 'One Rep Max' ? true : false; if ( is1RM == false && treeItem.parentId != 0) { is1RM = isParent1RM(treeItem.parentId); } @@ -34,12 +66,13 @@ class MenuTreeRepository { treeItem.parentId, treeName, assetImage, Colors.white, - 32, + 24, false, 0, null, false, - is1RM + is1RM, + treeItem.name, ); }); @@ -65,7 +98,8 @@ class MenuTreeRepository { exerciseType.exerciseTypeId, exerciseType, exerciseType.base, - is1RM + is1RM, + exerciseType.name ); }); @@ -81,7 +115,7 @@ class MenuTreeRepository { WorkoutTree treeItem = value as WorkoutTree; if ( treeItem.id == treeId ) { isTreeItem1RM = treeItem.is1RM; - //print (treeItem.name + " 1RM " + treeItem.is1RM.toString() ); + print (treeItem.name + " 1RM " + treeItem.is1RM.toString() ); } }); @@ -99,4 +133,34 @@ class MenuTreeRepository { }); return branch; } + + List getBranchList(int parent) { + List branch = List(); + tree.forEach((key, value) { + WorkoutTree workoutTree = value as WorkoutTree; + if ( parent == workoutTree.parent) { + branch.add(workoutTree); + } + }); + return branch; + } + + void sortByMuscleType() { + sortedTree = SplayTreeMap>(); + tree.forEach((key, value) { + WorkoutTree workoutTree = value as WorkoutTree; + //print("treeitem: " + workoutTree.toJson().toString()); + /*if ( workoutTree.exerciseType != null) { + print("treeItem exerciseTye " + workoutTree.exerciseType.toJson().toString()); + } else { + print("treeItem exerciseType null " + workoutTree.toJson().toString()); + }*/ + if ( workoutTree.nameEnglish != 'One Rep Max' && workoutTree.is1RM && workoutTree.exerciseTypeId == 0) { + String treeName = _antagonist[workoutTree.nameEnglish].toString() + ". " + workoutTree.name; + sortedTree[treeName] = this.getBranchList(workoutTree.id); + } + }); + + return; + } } \ No newline at end of file diff --git a/lib/service/api.dart b/lib/service/api.dart index e172e96..3c750cc 100644 --- a/lib/service/api.dart +++ b/lib/service/api.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:aitrainer_app/util/common.dart'; +import 'package:aitrainer_app/util/not_found_exception.dart'; import 'package:http/http.dart' as http; import 'package:aitrainer_app/model/cache.dart'; @@ -22,6 +23,8 @@ class APIClient with Common { ); if(response.statusCode == 200) { return utf8.decode(response.bodyBytes); + } else if(response.statusCode == 404 ) { + throw NotFoundException(message: "Not Found"); } else { throw Exception("Unable to perform HTTP request!"); } diff --git a/lib/service/customer_service.dart b/lib/service/customer_service.dart index 39f04b6..69ad1b4 100644 --- a/lib/service/customer_service.dart +++ b/lib/service/customer_service.dart @@ -5,64 +5,65 @@ import 'package:aitrainer_app/service/api.dart'; import 'package:aitrainer_app/model/cache.dart'; class CustomerApi { - final APIClient _client=new APIClient(); + final APIClient _client = new APIClient(); Future> getRealCustomers(String param) async { final body = await _client.get("customers/", param); final Iterable json = jsonDecode(body); - final List customers = json.map( (customer) => Customer.fromJson(customer) ).toList(); + final List customers = json.map((customer) => + Customer.fromJson(customer)).toList(); return customers; } Future saveCustomer(Customer customer) async { String body = JsonEncoder().convert(customer.toJson()); - print(" ===== saving customer id: " + customer.customerId.toString() + ":" + body ); + print(" ===== saving customer id: " + customer.customerId.toString() + ":" + + body); await _client.post( - "customers/"+customer.customerId.toString(), - body); + "customers/" + customer.customerId.toString(), + body); } Future addCustomer(Customer customer) async { String body = JsonEncoder().convert(customer.toJson()); - print(" ===== add new customer: " + body ); + print(" ===== add new customer: " + body); await _client.post( - "customers", - body); + "customers", + body); } Future addUser(User user) async { String body = JsonEncoder().convert(user.toJson()); - print(" ===== register new user: " + body ); + print(" ===== register new user: " + body); final String responseBody = await _client.post( - "registration", - body); + "registration", + body); Customer customer; try { int status = jsonDecode(responseBody)['status']; - if ( status != null ) { + if (status != null) { throw new Exception(jsonDecode(responseBody)['error']); } else { customer = Customer.fromJson(jsonDecode(responseBody)); Cache().afterRegistration(customer); } - } on FormatException catch(exception) { + } on FormatException catch (exception) { throw new Exception(responseBody); } - } Future getUser(User user) async { String body = JsonEncoder().convert(user.toJson()); - print(" ===== login the user: " + body ); + print(" ===== login the user: " + body); final String responseBody = await _client.post( - "login", - body); + "login", + body); Customer customer; try { customer = Customer.fromJson(jsonDecode(responseBody)); await Cache().afterLogin(customer); - } on FormatException catch(exception) { + } on FormatException catch (exception) { throw new Exception(responseBody); } } @@ -70,18 +71,54 @@ class CustomerApi { Future getCustomer(int customerId) async { String body = ""; - print(" ===== get the customer by id: " + customerId.toString() ); + print(" ===== get the customer by id: " + customerId.toString()); try { final String responseBody = await _client.get( - "customers/"+customerId.toString(), - body); + "customers/" + customerId.toString(), + body); Customer customer = Customer.fromJson(jsonDecode(responseBody)); + print(" --- Customer: " + customer.toJson().toString()); Cache().afterRegistration(customer); } catch (exception) { - print ("Exception: " + exception.toString()); - print (" === go to registration "); + print("Exception: " + exception.toString()); + print(" === go to registration "); Cache().logout(); Cache().startPage = "registration"; } } + + Future getTrainee(int customerId) async { + String body = ""; + Customer customer; + print(" ===== get Trainee customer by id: " + customerId.toString()); + try { + final String responseBody = await _client.get( + "customers/" + customerId.toString(), + body); + customer = Customer.fromJson(jsonDecode(responseBody)); + print(" --- Trainee: " + customer.toJson().toString()); + } catch (exception) { + print("Exception: " + exception.toString()); + throw Exception(exception); + } + return customer; + } + + Future> getTrainees(int trainerId) async { + List trainees = List(); + print("Get trainees list"); + try { + String body = ""; + final String responseBody = await _client.get( + "customers/trainees/" + trainerId.toString(), + body + ); + final Iterable json = jsonDecode(responseBody); + trainees = json.map((customer) => Customer.fromJson(customer)).toList(); + } catch (exception) { + print("Exception: " + exception.toString()); + throw Exception(exception); + } + return trainees; + } } \ No newline at end of file diff --git a/lib/service/exercise_plan_service.dart b/lib/service/exercise_plan_service.dart new file mode 100644 index 0000000..6d61036 --- /dev/null +++ b/lib/service/exercise_plan_service.dart @@ -0,0 +1,145 @@ +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/exercise_plan.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/util/not_found_exception.dart'; +import 'dart:convert'; +import 'api.dart'; + +class ExercisePlanApi { + final APIClient _client = new APIClient(); + + Future saveExercisePlan(ExercisePlan exercisePlan) async { + String body = JsonEncoder().convert(exercisePlan.toJson()); + print(" ===== saving exercisePlan $exercisePlan"); + ExercisePlan savedExercisePlan; + try { + final String responseBody = await _client.post( + "exercise_plan", + body); + savedExercisePlan = ExercisePlan.fromJson(jsonDecode(responseBody)); + Cache().setTraineeExercisePlan(savedExercisePlan); + } on Exception catch(e) { + throw new Exception(e.toString()); + } + return savedExercisePlan; + } + + Future updateExercisePlan( + ExercisePlan exercisePlan, + int exercisePlanId) async { + String body = JsonEncoder().convert(exercisePlan.toJson()); + print(" ===== saving exercisePlan $exercisePlan"); + ExercisePlan updatedExercisePlan; + try { + final String responseBody = await _client.post( + "exercise_plan/" + exercisePlanId.toString(), + body); + updatedExercisePlan = ExercisePlan.fromJson(jsonDecode(responseBody)); + Cache().setTraineeExercisePlan(updatedExercisePlan); + } on Exception catch(e) { + throw new Exception(e.toString()); + } + return updatedExercisePlan; + } + + Future saveExercisePlanDetail(ExercisePlanDetail exercisePlanDetail) async { + String body = JsonEncoder().convert(exercisePlanDetail.toJson()); + print(" ===== update exercisePlanDetail $exercisePlanDetail"); + ExercisePlanDetail savedExercisePlanDetail; + try { + final String responseBody = await _client.post( + "exercise_plan_detail", + body); + savedExercisePlanDetail = ExercisePlanDetail.fromJson(jsonDecode(responseBody)); + } on Exception catch(e) { + throw new Exception(e.toString()); + } + return savedExercisePlanDetail; + } + + Future updateExercisePlanDetail(ExercisePlanDetail exercisePlanDetail, + int exercisePlanDetailId) async { + String body = JsonEncoder().convert(exercisePlanDetail.toJson()); + print(" ===== update exercisePlanDetail $exercisePlanDetail"); + ExercisePlanDetail savedExercisePlanDetail; + try { + final String responseBody = await _client.post( + "exercise_plan_detail/" + exercisePlanDetailId.toString(), + body); + savedExercisePlanDetail = ExercisePlanDetail.fromJson(jsonDecode(responseBody)); + } on Exception catch(e) { + throw new Exception(e.toString()); + } + return savedExercisePlanDetail; + } + + Future deleteExercisePlanDetail(int exercisePlanDetailId) async { + print(" ===== delete exercisePlanDetail $exercisePlanDetailId"); + String body = ""; + try { + await _client.post( + "exercise_plan_detail/delete/" + exercisePlanDetailId.toString(), + body); + + } on Exception catch(e) { + throw new Exception(e.toString()); + } + return; + } + + Future deleteExercisePlan(int exercisePlanId) async { + String body = ""; + print(" ===== delete exercisePlan $exercisePlanId"); + + try { + await _client.post( + "exercise_plan/delete/" + exercisePlanId.toString(), + body); + + } on Exception catch(e) { + throw new Exception(e.toString()); + } + return; + } + + Future getLastExercisePlan(int customerId) async { + String body = ""; + print(" ===== get last exercisePlan $customerId"); + ExercisePlan exercisePlan; + try { + final String responseBody = await _client.get( + "exercise_plan/last/" + customerId.toString(), + body); + exercisePlan = ExercisePlan.fromJson(jsonDecode(responseBody)); + } on Exception catch(e) { + if ( e is NotFoundException) { + print("ExercisePlan not found for " + customerId.toString()); + return exercisePlan; + } else { + throw new Exception(e.toString()); + } + } + return exercisePlan; + } + + Future> getExercisePlanDetail(int exercisePlanId) async { + String body = ""; + print(" ===== get exercisePlanDetail $exercisePlanId"); + List listExercisePlanDetail; + try { + final String responseBody = await _client.get( + "exercise_plan_detail/" + exercisePlanId.toString(), + body); + print("response body:" + responseBody); + final Iterable json = jsonDecode(responseBody); + listExercisePlanDetail = json.map( (planDetail) => ExercisePlanDetail.fromJson(planDetail) ) .toList(); + } on Exception catch(e) { + throw new Exception(e.toString()); + } + return listExercisePlanDetail; + } + + + + +} \ No newline at end of file diff --git a/lib/service/exercise_service.dart b/lib/service/exercise_service.dart index f08831e..6648f99 100644 --- a/lib/service/exercise_service.dart +++ b/lib/service/exercise_service.dart @@ -22,12 +22,14 @@ class ExerciseApi { body); } - Future addExercise(Exercise exercise) async { + Future addExercise(Exercise exercise) async { String body = JsonEncoder().convert(exercise.toJson()); print(" ===== add new exercise: " + body ); - await _client.post( + final String response = await _client.post( "exercises", body); + final Exercise savedExercise = Exercise.fromJson(jsonDecode(response)); + return savedExercise; } Future> getExercisesByCustomer(int customerId ) async { diff --git a/lib/util/not_found_exception.dart b/lib/util/not_found_exception.dart new file mode 100644 index 0000000..ab56186 --- /dev/null +++ b/lib/util/not_found_exception.dart @@ -0,0 +1,4 @@ +class NotFoundException implements Exception { + final String message; + const NotFoundException({this.message}); +} \ No newline at end of file diff --git a/lib/util/session.dart b/lib/util/session.dart index bcbe973..683610e 100644 --- a/lib/util/session.dart +++ b/lib/util/session.dart @@ -27,7 +27,7 @@ class Session { if ( Cache().firstLoad ) { print (" -- Session: fetch locale.."); - await appLanguage.fetchLocale(); + await appLanguage.getLocale(_sharedPreferences); await AppLocalizations.delegate.load(appLanguage.appLocal); print (" -- Session: fetch token.."); await _fetchToken(_sharedPreferences); @@ -95,7 +95,6 @@ class Session { //Navigator.of(context).pushNamed('login'); Cache().startPage = "login"; } else { - print("************** Store SharedPreferences"); // get API customer customerId = prefs.getInt(Cache.customerIdKey); await CustomerApi().getCustomer(customerId); diff --git a/lib/view/account.dart b/lib/view/account.dart index 485d53d..e70df1e 100644 --- a/lib/view/account.dart +++ b/lib/view/account.dart @@ -1,5 +1,7 @@ import 'package:aitrainer_app/bloc/account/account_bloc.dart'; +import 'package:aitrainer_app/localization/app_language.dart'; import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/model/customer.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:aitrainer_app/widgets/bottom_nav.dart'; import 'package:flutter/material.dart'; @@ -7,6 +9,7 @@ import 'package:flutter/cupertino.dart'; // ignore: must_be_immutable class AccountPage extends StatelessWidget { + // ignore: close_sinks AccountBloc accountBloc; @override @@ -93,7 +96,7 @@ class AccountPage extends StatelessWidget { ), ), loginOut( context, accountBloc ), - //exercises(exerciseChangingViewModel), + getMyTrainees(context, accountBloc), ]); } @@ -137,93 +140,72 @@ class AccountPage extends StatelessWidget { return element; } - /* ListTile exercises( ExerciseChangingViewModel model ) { - ListTile element = ListTile(); - if ( Auth().userLoggedIn == null ) { - return element; + Widget getMyTrainees( BuildContext context, AccountBloc accountBloc ) { + if ( accountBloc.customerRepository.customer == null ) { + return Container(); } - - element = ListTile( - title: Text(AppLocalizations.of(context).translate("Exercises")), - subtitle: Column( - children: [ - FutureBuilder>( - future: _exercises, - builder: (context, snapshot) { - if (snapshot.hasData) { - return getExercises( model );//CustomerListWidget(customers: _exerciseViewModel.exerciseList); - } else if (snapshot.hasError) { - return Text("${snapshot.error}"); - } - - // By default, show a loading spinner. - return CircularProgressIndicator(); - } - ),] - )); - - return element; - } -*/ - /* - Widget getExercises( ExerciseChangingViewModel model ) { - List exercises = model.exerciseList; - - Column element = Column(); - if (exercises.length > 0) { - List rows = List(); - - exercises.forEach((exercise) { - String exerciseName = AppLocalizations.of(context).translate( - Common.getExerciseType(exercise.getExercise().exerciseTypeId).name); - - String quantity = exercise.getExercise().quantity.toString() + " " + - AppLocalizations.of(context).translate(exercise.getExercise().unit); - - String unitQuantity = ""; - String unitQuantityUnit = ""; - String date = Common.getDateLocale(exercise.getExercise().dateAdd, false); - if (exercise.getExercise().unitQuantity != null) { - unitQuantity = exercise.getExercise().unitQuantity.toString(); - unitQuantityUnit = AppLocalizations.of(context).translate( - Common.getExerciseType(exercise.getExercise().exerciseTypeId).unitQuantityUnit); - } - - TableRow row = TableRow( - children: [ - Text(date), - Text(exerciseName), - Text(quantity), - - Text(unitQuantity + " " + unitQuantityUnit), - ] - ); - - Table table = Table( - defaultColumnWidth: FractionColumnWidth(0.28), - children: [row], - ); - - Column col = Column( - children: [ - table, - Row( - children: [ - Text(" "), - ] - ) - ], - ); - rows.add(col); - }); - - - element = Column( - children: rows, + if ( accountBloc.customerRepository.customer.trainer == 0 ) { + return ListTile( + title: Container(), ); } - return element; + if (accountBloc.customerRepository.getTraineesList() == null ) { + return ListTile( + leading: Icon(Icons.people), + title: RaisedButton( + color: Colors.white70, + onPressed: () => accountBloc.add(AccountGetTrainees()), + child: Text("See my trainees"), + ), + ); + + } + + List elements = List(); + accountBloc.customerRepository.getTraineesList().forEach((element) { + Customer trainee = element; + String name = trainee.name; + String firstName = trainee.firstname; + String nodeName = AppLanguage().appLocal == Locale("en") ? + firstName + " " + name : name + " " + firstName; + + bool selected = accountBloc.traineeId == trainee.customerId; + + Widget widget = FlatButton( + padding: EdgeInsets.all(10), + shape:RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + side: BorderSide(width: 2, color: selected ? Colors.blue : Colors.black26 ), + ), + onPressed: () { + accountBloc.add(AccountSelectTrainee(traineeId: trainee.customerId)); + //Navigator.of(context).pushNamed('login'); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(nodeName, style: + TextStyle( + color: selected ? Colors.blue : Colors.black54, + fontWeight: selected ? FontWeight.bold : FontWeight.normal + ), + ), + Icon(Icons.arrow_forward_ios), + ]), + ); + elements.add(widget); + }); + + return ListTile( + leading: Icon(Icons.people), + subtitle: Text("My Trainees"), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: elements, + ) + ); + } - } */ } diff --git a/lib/view/exercise_add_by_plan_page.dart b/lib/view/exercise_add_by_plan_page.dart new file mode 100644 index 0000000..91ebc3d --- /dev/null +++ b/lib/view/exercise_add_by_plan_page.dart @@ -0,0 +1,241 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/bloc/exercise_add_by_plan_bloc.dart'; +import 'package:aitrainer_app/bloc/exercise_by_plan/exercise_by_plan_bloc.dart'; +import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:aitrainer_app/repository/exercise_plan_repository.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/widgets/splash.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class ExerciseAddByPlanPage extends StatefulWidget{ + _ExerciseAddByPlanPage createState() => _ExerciseAddByPlanPage(); +} + +class _ExerciseAddByPlanPage extends State { + + @override + Widget build(BuildContext context) { + LinkedHashMap arguments = ModalRoute.of(context).settings.arguments; + // ignore: close_sinks + final ExerciseByPlanBloc bloc = arguments['blocExerciseByPlan']; + final int customerId = arguments['customerId']; + final WorkoutTree workoutTree = arguments['workoutTree']; + final ExerciseRepository exerciseRepository = ExerciseRepository(); + + return BlocProvider( + create: (context) => + ExerciseAddByPlanFormBloc( + exerciseRepository: exerciseRepository, + exercisePlanRepository: bloc.exercisePlanRepository, + customerId: customerId, + workoutTree: workoutTree), + child: BlocBuilder( + builder: (context, state) { + // ignore: close_sinks + final exerciseBloc = BlocProvider.of(context); + if ( state is FormBlocLoading ) { + return LoadingDialog(); + } else if ( state is FormBlocSuccess) { + return getControlForm(exerciseBloc); + } else { + return getControlForm(exerciseBloc); + } + } + )); + } + + Form getControlForm( ExerciseAddByPlanFormBloc exerciseBloc) { + String exerciseName = AppLanguage().appLocal == Locale("en") ? + exerciseBloc.exerciseRepository.exerciseType.name : + exerciseBloc.exerciseRepository.exerciseType.nameTranslation; + + return Form( + autovalidate: true, + child: Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + backgroundColor: Colors.black, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Add Exercise"), + Image.asset( + 'asset/image/WT_long_logo.png', + fit: BoxFit.cover, + height: 65.0, + ), + ], + ), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: Container( + width: MediaQuery + .of(context) + .size + .width, + height: MediaQuery + .of(context) + .size + .height, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.fill, + alignment: Alignment.center, + ), + ), + child: Container( + padding: const EdgeInsets.only (top: 25, left: 25, right: 25), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text(exerciseName, + style: TextStyle(fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.deepOrange), + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: true, + ), + Divider(color: Colors.transparent,), + + Divider(), + Column( + children: repeatExercises(exerciseBloc), + + ), + Divider(), + + + + ]), + ) + ) + ), + ), + ); + } + + List repeatExercises(ExerciseAddByPlanFormBloc exerciseBloc) { + List listColumns = List(); + for ( int i = 0; i < exerciseBloc.countSteps; i++) { + Column col = Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Divider(color: Colors.transparent,), + Text("Execute the " + (i+1).toString() + ". set!", + style: TextStyle(),), + TextFieldBlocBuilder( + readOnly: exerciseBloc.step != i+1, + textFieldBloc: exerciseBloc.quantity1Field, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.deepOrange, + fontWeight: FontWeight.bold), + inputFormatters: [ + WhitelistingTextInputFormatter(RegExp(r"[\d.]")) + ], + + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle( + fontSize: 12, color: Colors.black54, fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context) + .translate("The number of the exercise"), + labelStyle: TextStyle(fontSize: 12, color: Colors.deepOrange, fontWeight: FontWeight.normal), + labelText: "Please repeat with " + exerciseBloc.unitQuantity1Field.value + " " + + exerciseBloc.exerciseRepository.exerciseType.unitQuantityUnit + " " + + exerciseBloc.exercisePlanRepository.actualPlanDetail.repeats.toString() + " times!", + ), + ), + TextFieldBlocBuilder( + readOnly: exerciseBloc.step != i+1, + textFieldBloc: exerciseBloc.unitQuantity1Field, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: Colors.black54, + fontWeight: FontWeight.bold), + inputFormatters: [ + WhitelistingTextInputFormatter(RegExp(r"[\d.]")) + ], + + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle( + fontSize: 12, color: Colors.black54, fontWeight: FontWeight.w100), + labelStyle: TextStyle(fontSize: 12, color: Colors.deepOrange, fontWeight: FontWeight.normal), + labelText: exerciseBloc.exerciseRepository.exerciseType.unitQuantityUnit, + ), + ), + RaisedButton( + + padding: EdgeInsets.all(0), + textColor: Colors.white, + color: exerciseBloc.step == i+1 ? Colors.blue : Colors.black26, + focusColor: Colors.blueAccent, + onPressed: () => + { + print ("Submit step " + exerciseBloc.step.toString() + " (i) " + i.toString()), + if ( exerciseBloc.step == i+1 ) { + exerciseBloc.submit() + }, + if ( i+1 == exerciseBloc.countSteps) { + Navigator.of(context).pop() + } + }, + child: Text( + AppLocalizations.of(context).translate("Check"), + style: TextStyle(fontSize: 12),) + ), + Divider(color: Colors.transparent,), + ], + ); + listColumns.add(col); + } + return listColumns; + } + + String validateNumberInput(input) { + String error = AppLocalizations.of(context).translate( + "Please type the right quantity 0-10000"); + dynamic rc = (input != null && input.length > 0); + if (!rc) { + return null; + } + + Pattern pattern = r'^\d+(?:\.\d+)?$'; + RegExp regex = new RegExp(pattern); + if (!regex.hasMatch(input)) { + return error; + } + + rc = double.tryParse(input); + if (rc == null) { + return error; + } + + + if (!(double.parse(input) < 10000 && double.parse(input) > 0)) { + return error; + } + + return null; + } +} diff --git a/lib/view/exercise_execute_by_plan_page.dart b/lib/view/exercise_execute_by_plan_page.dart new file mode 100644 index 0000000..d900587 --- /dev/null +++ b/lib/view/exercise_execute_by_plan_page.dart @@ -0,0 +1,187 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/bloc/exercise_by_plan/exercise_by_plan_bloc.dart'; +import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:aitrainer_app/widgets/app_bar_common.dart'; +import 'package:aitrainer_app/widgets/splash.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_treeview/tree_view.dart'; + +class ExerciseByPlanPage extends StatefulWidget { + @override + _ExerciseByPlanPage createState() => _ExerciseByPlanPage(); +} + +class _ExerciseByPlanPage extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + // ignore: close_sinks + ExerciseByPlanBloc bloc; + + @override + void initState() { + super.initState(); + + /// We require the initializers to run after the loading screen is rendered + SchedulerBinding.instance.addPostFrameCallback((_) { + BlocProvider.of(context).add(ExerciseByPlanLoad()); + }); + } + + @override + Widget build(BuildContext context) { + LinkedHashMap arguments = ModalRoute.of(context).settings.arguments; + final int customerId = arguments['customerId']; + bloc = BlocProvider.of(context); + bloc.customerId = customerId; + + return Scaffold( + key: _scaffoldKey, + appBar: AppBarCommonNav(), + body: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + image: DecorationImage( + image: customerId == Cache().userLoggedIn.customerId ? AssetImage('asset/image/WT_light_background.png'): + AssetImage('asset/image/WT_menu_dark.png'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: BlocConsumer(listener: (context, state) { + if (state is ExerciseByPlanError) { + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + state.message, + ), + backgroundColor: Colors.orange, + )); + } else if (state is ExerciseByPlanLoading) { + LoadingDialog(); + } + }, + // ignore: missing_return + builder: (context, state) { + if (state is ExerciseByPlanStateInitial || state is ExerciseByPlanLoading) { + return Container(); + } else if (state is ExerciseByPlanReady) { + return exerciseWidget(bloc); + } else if (state is ExerciseByPlanError) { + return exerciseWidget(bloc); + } + }))); + } + + Widget exerciseWidget(ExerciseByPlanBloc bloc) { + final LinkedHashMap args = LinkedHashMap(); + TreeViewController _treeViewController = TreeViewController(children: nodeExercisePlan(bloc)); + + TreeViewTheme _treeViewTheme = TreeViewTheme( + expanderTheme: ExpanderThemeData( + type: ExpanderType.plusMinus, + modifier: ExpanderModifier.circleOutlined, + position: ExpanderPosition.start, + color: Colors.black26, + size: 10, + ), + labelStyle: TextStyle(fontSize: 14, letterSpacing: 0, color: Colors.blue.shade800), + parentLabelStyle: TextStyle( + fontSize: 14, + letterSpacing: 0.3, + fontWeight: FontWeight.w800, + color: Colors.orange.shade600, + ), + iconTheme: IconThemeData( + size: 20, + color: Colors.blue.shade800, + ), + colorScheme: bloc.customerId == Cache().userLoggedIn.customerId ? ColorScheme.light(background: Colors.transparent) : ColorScheme.dark(background: Colors.transparent), + ); + + return Scaffold( + backgroundColor: Colors.transparent, + body: TreeView( + controller: _treeViewController, + allowParentSelect: false, + supportParentDoubleTap: false, + //onExpansionChanged: _expandNodeHandler, + onNodeTap: (key) { + /* Node node = _treeViewController.getNode(key); + WorkoutTree workoutTree = node.data as WorkoutTree; + bloc.exercisePlanRepository.setActualPlanDetail(workoutTree.exerciseType); + print("change node " + node.label + " key " + key); + bloc.add(ExercisePlanUpdate(workoutTree: workoutTree)); + Navigator.of(context).pushNamed("exercisePlanDetailAdd", arguments: bloc); */ + + Node node = _treeViewController.getNode(key); + WorkoutTree workoutTree = node.data as WorkoutTree; + args['blocExerciseByPlan'] = bloc; + args['customerId'] = bloc.customerId; + args['workoutTree'] = workoutTree; + Navigator.of(context).pushNamed("exerciseAddByPlanPage", arguments: args); + }, + + theme: _treeViewTheme, + ), + //bottomNavigationBar: BottomNavigator(bottomNavIndex: 2), + floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, + ); + } + + List nodeExercisePlan(ExerciseByPlanBloc bloc) { + List nodes = List(); + Node actualNode; + bool isEnglish = AppLanguage().appLocal == Locale("en"); + + bloc.menuTreeRepository.sortedTree.forEach((name, list) { + List listWorkoutItem = list; + List listExerciseTypePerMuscle = List(); + NodeIcon icon; + listWorkoutItem.forEach((element) { + + WorkoutTree treeItem = element; + if ( treeItem.selected ) { + icon = + treeItem.executed == false ? NodeIcon(codePoint: Icons.bubble_chart.codePoint, color: "blueAccent") : + NodeIcon(codePoint: Icons.check_box.codePoint, color: "green"); + + String exerciseLabel = isEnglish + ? treeItem.name + : treeItem.exerciseType == null ? treeItem.name : treeItem.exerciseType.nameTranslation; + + List> planDetailList = List>(); + String planDetail = bloc.exercisePlanRepository.getPlanDetail(treeItem.exerciseTypeId); + + if (planDetail.length > 0) { + exerciseLabel += " (" + planDetail + ")"; + } + + actualNode = Node( + label: exerciseLabel, + key: treeItem.id.toString(), + data: treeItem, + expanded: planDetailList.length > 0 ? true : false, + children: [], + icon: icon); + listExerciseTypePerMuscle.add(actualNode); + } + }); + + if (name != null) { + actualNode = Node( + label: name, + key: name, + expanded: true, + children: listExerciseTypePerMuscle, + icon: NodeIcon(codePoint: Icons.perm_identity.codePoint, color: "orange")); + nodes.add(actualNode); + } + }); + + return nodes; + } +} diff --git a/lib/view/exercise_log_page.dart b/lib/view/exercise_log_page.dart new file mode 100644 index 0000000..3d1c3d6 --- /dev/null +++ b/lib/view/exercise_log_page.dart @@ -0,0 +1,148 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/exercise.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/widgets/app_bar_common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_treeview/tree_view.dart'; + +class ExerciseLogPage extends StatefulWidget { + @override + _ExerciseLogPage createState() => _ExerciseLogPage(); +} + +class _ExerciseLogPage extends State { + @override + Widget build(BuildContext context) { + LinkedHashMap arguments = ModalRoute.of(context).settings.arguments; + final ExerciseRepository exerciseRepository = arguments['exerciseRepository']; + final CustomerRepository customerRepository = arguments['customerRepository']; + final int customerId = arguments['customerId']; + + return Scaffold( + appBar: AppBarCommonNav(), + body: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + image: DecorationImage( + image: customerId == Cache().userLoggedIn.customerId ? AssetImage('asset/image/WT_light_background.png'): + AssetImage('asset/image/WT_menu_dark.png'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: exerciseWidget(exerciseRepository, customerId), + ) + ); + } + + Widget exerciseWidget(ExerciseRepository exerciseRepository, int customerId) { + TreeViewController _treeViewController = + TreeViewController(children: nodeExercises(exerciseRepository, customerId)); + + TreeViewTheme _treeViewTheme = TreeViewTheme( + expanderTheme: ExpanderThemeData( + type: ExpanderType.caret, + modifier: ExpanderModifier.none, + position: ExpanderPosition.start, + color: Colors.red.shade800, + size: 20, + ), + labelStyle: TextStyle( + fontSize: 12, + letterSpacing: 0.1, + ), + parentLabelStyle: TextStyle( + fontSize: 16, + letterSpacing: 0.1, + fontWeight: FontWeight.w800, + color: Colors.orange.shade600, + ), + iconTheme: IconThemeData( + size: 18, + color: Colors.grey.shade800, + ), + colorScheme: ColorScheme.light(background: Colors.transparent), + ); + + return TreeView( + controller: _treeViewController, + allowParentSelect: false, + supportParentDoubleTap: false, + //onExpansionChanged: _expandNodeHandler, + onNodeTap: (key) { + setState(() { + _treeViewController = _treeViewController.copyWith(selectedKey: key); + }); + }, + theme: _treeViewTheme, + ); + } + + List nodeExercises(ExerciseRepository exerciseRepository, int customerId) { + List nodes = List(); + List exercises; + if ( customerId == Cache().userLoggedIn.customerId ) { + exercises = exerciseRepository.getExerciseList(); + } else if ( Cache().getTrainee() != null && customerId == Cache().getTrainee().customerId ) { + exercises = exerciseRepository.getExerciseListTrainee(); + } + + + String prevDay = ""; + Node actualNode; + List listExercisesPerDay; + exercises.forEach((element) { + Exercise exercise = element; + ExerciseType exerciseType = + exerciseRepository.getExerciseTypeById(exercise.exerciseTypeId); + String actualDay = exercise.dateAdd.year.toString() + + "-" + + exercise.dateAdd.month.toString() + + "-" + + exercise.dateAdd.day.toString(); + + if (prevDay.compareTo(actualDay) != 0) { + listExercisesPerDay = List(); + actualNode = Node( + label: actualDay, + key: exercise.dateAdd.toString(), + expanded: true, + children: listExercisesPerDay, + icon: + NodeIcon(codePoint: Icons.date_range.codePoint, color: "blue")); + nodes.add(actualNode); + prevDay = actualDay; + } + + String exerciseName = AppLanguage().appLocal == Locale("en") + ? exerciseType.name + : exerciseType.nameTranslation; + String unitQuantity = exerciseType.unitQuantity == "1" + ? exercise.unitQuantity.toStringAsFixed(0) + + " " + + AppLocalizations.of(context) + .translate(exerciseType.unitQuantityUnit) + + " " + : ""; + + String labelExercise = exerciseName + + " " + + unitQuantity + + exercise.quantity.toStringAsFixed(0) + + " " + + AppLocalizations.of(context).translate(exercise.unit); + listExercisesPerDay.add(Node( + label: labelExercise, + key: exercise.exerciseId.toString(), + expanded: false, + icon: NodeIcon(codePoint: Icons.repeat.codePoint, color: "blue"))); + }); + return nodes; + } +} diff --git a/lib/view/exercise_plan_custom_page.dart b/lib/view/exercise_plan_custom_page.dart new file mode 100644 index 0000000..9beb3ef --- /dev/null +++ b/lib/view/exercise_plan_custom_page.dart @@ -0,0 +1,190 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/bloc/exercise_plan/exercise_plan_bloc.dart'; +import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/workout_tree.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/widgets/app_bar_common.dart'; +import 'package:aitrainer_app/widgets/splash.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_treeview/tree_view.dart'; + +class ExercisePlanCustomPage extends StatefulWidget { + @override + _ExercisePlanCustomPage createState() => _ExercisePlanCustomPage(); +} + +class _ExercisePlanCustomPage extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + // ignore: close_sinks + ExercisePlanBloc bloc; + + @override + void initState() { + super.initState(); + + /// We require the initializers to run after the loading screen is rendered + SchedulerBinding.instance.addPostFrameCallback((_) { + BlocProvider.of(context).add(ExercisePlanLoad()); + }); + } + + @override + Widget build(BuildContext context) { + LinkedHashMap arguments = ModalRoute.of(context).settings.arguments; + final ExerciseRepository exerciseRepository = arguments['exerciseRepository']; + final int customerId = arguments['customerId']; + bloc = BlocProvider.of(context); + bloc.customerId = customerId; + + return Scaffold( + key: _scaffoldKey, + appBar: AppBarCommonNav(), + body: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + image: DecorationImage( + image: customerId == Cache().userLoggedIn.customerId ? AssetImage('asset/image/WT_light_background.png'): + AssetImage('asset/image/WT_menu_dark.png'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: BlocConsumer(listener: (context, state) { + if (state is ExercisePlanError) { + //showInSnackBar(state.message); + //return exerciseWidget(bloc); + Scaffold.of(context).showSnackBar(SnackBar( + content: Text( + state.message, + ), + backgroundColor: Colors.orange, + )); + } else if (state is ExercisePlanLoading) { + LoadingDialog(); + } + }, + // ignore: missing_return + builder: (context, state) { + if (state is ExercisePlanInitial) { + return Container(); + } else if (state is ExercisePlanReady) { + return exerciseWidget(bloc); + } else if (state is ExercisePlanError) { + return exerciseWidget(bloc); + } else if (state is ExercisePlanLoading) { + return Container(); + } + }))); + } + + Widget exerciseWidget(ExercisePlanBloc bloc) { + TreeViewController _treeViewController = TreeViewController(children: nodeExercisePlan(bloc)); + + TreeViewTheme _treeViewTheme = TreeViewTheme( + expanderTheme: ExpanderThemeData( + type: ExpanderType.plusMinus, + modifier: ExpanderModifier.circleOutlined, + position: ExpanderPosition.start, + color: Colors.black26, + size: 10, + ), + labelStyle: TextStyle(fontSize: 14, letterSpacing: 0, color: Colors.blue.shade800), + parentLabelStyle: TextStyle( + fontSize: 14, + letterSpacing: 0.3, + fontWeight: FontWeight.w800, + color: Colors.orange.shade600, + ), + iconTheme: IconThemeData( + size: 20, + color: Colors.blue.shade800, + ), + colorScheme: bloc.customerId == Cache().userLoggedIn.customerId ? ColorScheme.light(background: Colors.transparent) : ColorScheme.dark(background: Colors.transparent), + ); + + return Scaffold( + backgroundColor: Colors.transparent, + body: TreeView( + controller: _treeViewController, + allowParentSelect: true, + supportParentDoubleTap: false, + //onExpansionChanged: _expandNodeHandler, + onNodeTap: (key) { + Node node = _treeViewController.getNode(key); + WorkoutTree workoutTree = node.data as WorkoutTree; + bloc.exercisePlanRepository.setActualPlanDetail(workoutTree.exerciseType); + print("change node " + node.label + " key " + key); + bloc.add(ExercisePlanUpdate(workoutTree: workoutTree)); + Navigator.of(context).pushNamed("exercisePlanDetailAdd", arguments: bloc); + }, + theme: _treeViewTheme, + ), + floatingActionButton: FloatingActionButton( + backgroundColor: Colors.blueAccent, + child: Icon(Icons.save_alt), + onPressed: () => { + bloc.add(ExercisePlanSave()), + if (bloc.exercisePlanRepository.getExercisePlanDetailSize() > 0) { + Navigator.of(context).pop() + } + } + ), + //bottomNavigationBar: BottomNavigator(bottomNavIndex: 2), + floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, + ); + } + + List nodeExercisePlan(ExercisePlanBloc bloc) { + List nodes = List(); + Node actualNode; + bool isEnglish = AppLanguage().appLocal == Locale("en"); + + bloc.menuTreeRepository.sortedTree.forEach((name, list) { + List listWorkoutItem = list; + List listExerciseTypePerMuscle = List(); + NodeIcon icon; + listWorkoutItem.forEach((element) { + WorkoutTree treeItem = element; + icon = + treeItem.selected == true ? NodeIcon(codePoint: Icons.bubble_chart.codePoint, color: "blueAccent") : null; + + String exerciseLabel = isEnglish + ? treeItem.name + : treeItem.exerciseType == null ? treeItem.name : treeItem.exerciseType.nameTranslation; + + List> planDetailList = List>(); + String planDetail = bloc.exercisePlanRepository.getPlanDetail(treeItem.exerciseTypeId); + + if (planDetail.length > 0) { + exerciseLabel += " (" + planDetail +")"; + } + + actualNode = Node( + label: exerciseLabel, + key: treeItem.id.toString(), + data: treeItem, + expanded: planDetailList.length > 0 ? true : false, + children: [], + icon: icon); + listExerciseTypePerMuscle.add(actualNode); + }); + //print ("Node name " + name); + if (name != null) { + actualNode = Node( + label: name, // AppLocalizations.of(context).translate(name), + key: name, + expanded: true, + children: listExerciseTypePerMuscle, + icon: NodeIcon(codePoint: Icons.perm_identity.codePoint, color: "orange")); + nodes.add(actualNode); + } + }); + + return nodes; + } +} diff --git a/lib/view/exercise_plan_detail_add_page.dart b/lib/view/exercise_plan_detail_add_page.dart new file mode 100644 index 0000000..1216b81 --- /dev/null +++ b/lib/view/exercise_plan_detail_add_page.dart @@ -0,0 +1,144 @@ + +import 'package:aitrainer_app/bloc/exercise_plan/exercise_plan_bloc.dart'; +import 'package:aitrainer_app/bloc/exercise_plan_custom_form.dart'; +import 'package:aitrainer_app/localization/app_language.dart'; +import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/repository/exercise_plan_repository.dart'; +import 'package:aitrainer_app/widgets/app_bar_common.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_form_bloc/flutter_form_bloc.dart'; + +class ExercisePlanDetailAddPage extends StatefulWidget { + @override + _ExercisePlanDetailAddPage createState() => _ExercisePlanDetailAddPage(); +} + +class _ExercisePlanDetailAddPage extends State { + @override + Widget build(BuildContext context) { + // ignore: close_sinks + final ExercisePlanBloc planBloc = ModalRoute.of(context).settings.arguments; + final ExercisePlanRepository exercisePlanRepository = planBloc.exercisePlanRepository; + + return BlocProvider( + create: (context) => ExercisePlanCustomerFormBloc(exercisePlanRepository: exercisePlanRepository, planBloc: planBloc), + child: Builder(builder: (context) { + // ignore: close_sinks + final bloc = BlocProvider.of(context); + + String exerciseName = ""; + if (bloc != null) { + exerciseName = AppLanguage().appLocal == Locale("en") + ? bloc.exercisePlanRepository.actualPlanDetail.exerciseType.name + : bloc.exercisePlanRepository.actualPlanDetail.exerciseType.nameTranslation; + } + + return Form( + autovalidate: true, + child: Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBarCommonNav(), + body: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.fill, + alignment: Alignment.center, + ), + ), + child: Container( + padding: const EdgeInsets.only(top: 25, left: 25, right: 25), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ + Text(AppLocalizations.of(context).translate('Save The Exercise To The Exercise Plan'), + style: TextStyle(fontSize: 14, color: Colors.blueAccent)), + Text( + exerciseName, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.deepOrange), + overflow: TextOverflow.fade, + maxLines: 1, + softWrap: true, + ), + TextFieldBlocBuilder( + textFieldBloc: bloc.serieField, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 30, color: Colors.lightBlue, fontWeight: FontWeight.bold), + inputFormatters: [WhitelistingTextInputFormatter(RegExp(r"[\d.]"))], + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context).translate("The number of the serie done with"), + labelStyle: TextStyle(fontSize: 16, color: Colors.lightBlue), + labelText: AppLocalizations.of(context).translate("Serie"), + ), + ), + TextFieldBlocBuilder( + textFieldBloc: bloc.quantityField, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 30, color: Colors.lightBlue, fontWeight: FontWeight.bold), + inputFormatters: [WhitelistingTextInputFormatter(RegExp(r"[\d.]"))], + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context).translate("The number of the repeats of one serie"), + labelStyle: TextStyle(fontSize: 16, color: Colors.lightBlue), + labelText: AppLocalizations.of(context).translate("Repeats"), + ), + ), + TextFieldBlocBuilder( + textFieldBloc: bloc.weightField, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 30, color: Colors.lightBlue, fontWeight: FontWeight.bold), + inputFormatters: [WhitelistingTextInputFormatter(RegExp(r"[\d.]"))], + decoration: InputDecoration( + fillColor: Colors.white, + filled: false, + hintStyle: TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w100), + hintText: AppLocalizations.of(context).translate("The weight"), + labelStyle: TextStyle(fontSize: 16, color: Colors.lightBlue), + labelText: AppLocalizations.of(context).translate("Weight"), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + RaisedButton( + textColor: Colors.white, + color: Colors.red.shade300, + focusColor: Colors.white, + onPressed: () => { + print("Remove " + bloc.exercisePlanRepository.actualPlanDetail.exerciseType.name), + planBloc.add(ExercisePlanRemoveExercise(exercisePlanDetail: bloc.exercisePlanRepository.actualPlanDetail)), + Navigator.of(context).pop(), + }, + child: Text("Delete"), //Text(AppLocalizations.of(context).translate("Delete"), style: TextStyle(fontSize: 16),) + ), + RaisedButton( + textColor: Colors.white, + color: Colors.blueAccent, + focusColor: Colors.white, + onPressed: () => { + bloc.submit(), + Navigator.of(context).pop(), + }, + child: Text( + AppLocalizations.of(context).translate("Save"), + style: TextStyle(fontSize: 16), + )), + ], + ), + ]), + ))), + ), + ); + })); + } +} diff --git a/lib/view/mydevelopment_page.dart b/lib/view/mydevelopment_page.dart index b6324e1..cdea462 100644 --- a/lib/view/mydevelopment_page.dart +++ b/lib/view/mydevelopment_page.dart @@ -1,12 +1,12 @@ -import 'package:aitrainer_app/localization/app_language.dart'; -import 'package:aitrainer_app/localization/app_localization.dart'; -import 'package:aitrainer_app/model/exercise.dart'; -import 'package:aitrainer_app/model/exercise_type.dart'; +import 'dart:collection'; + +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:aitrainer_app/repository/exercise_repository.dart'; -import 'package:aitrainer_app/widgets/app_bar.dart'; +import 'package:aitrainer_app/widgets/app_bar_common.dart'; import 'package:aitrainer_app/widgets/bottom_nav.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_treeview/tree_view.dart'; class MyDevelopmentPage extends StatefulWidget { @override @@ -17,131 +17,151 @@ class _MyDevelopmentPage extends State { @override Widget build(BuildContext context) { final ExerciseRepository exerciseRepository = ExerciseRepository(); + final CustomerRepository customerRepository = CustomerRepository(); + final LinkedHashMap args = LinkedHashMap(); return Scaffold( - appBar: AppBarNav(), + appBar: AppBarCommonNav(), body: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_light_background.png'), - fit: BoxFit.cover, - alignment: Alignment.center, - ), + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.cover, + alignment: Alignment.center, ), - child: Container( - padding: EdgeInsets.all(10), - child: - exerciseWidget(exerciseRepository), + ), + child: CustomScrollView( + scrollDirection: Axis.vertical, + slivers: + [ + SliverGrid( + delegate: SliverChildListDelegate( + [ + FlatButton( + padding: EdgeInsets.all(0), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + args['exerciseRepository'] = exerciseRepository, + args['customerRepository'] = customerRepository, + args['customerId'] = Cache().userLoggedIn.customerId, + Navigator.of(context).pushNamed('exerciseLogPage', + arguments: args) + }, + child: Text("My Exercise Logs", + style: TextStyle(fontSize: 18),) + ), + Stack( + fit: StackFit.passthrough, + overflow: Overflow.clip, + alignment: Alignment.topLeft, + children: [ + Image.asset('asset/image/lock.png', + height: 40, + width: 40, + ), + FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + }, + child: Text("My Whole Body Development", + style: TextStyle(fontSize: 18),) + ), - ) - ), + ], + ), + + Stack( + fit: StackFit.passthrough, + overflow: Overflow.clip, + children: [ + Image.asset('asset/image/lock.png', + height: 40, + width: 40, + ), + FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + }, + child: Text("Development Of Muscles", + style: TextStyle(fontSize: 18),) + ), + + ] + ), + Stack( + fit: StackFit.passthrough, + + overflow: Overflow.clip, + children: [ + Image.asset('asset/image/lock.png', + height: 40, + width: 40, + ), + FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + }, + child: Text("Predictions", + style: TextStyle(fontSize: 18),) + ), + ] + ), + hiddenWidget(customerRepository, exerciseRepository), + ] + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 20.0, + crossAxisSpacing: 20.0, + childAspectRatio: 1.2, + ), + ) + ] + ) + ), bottomNavigationBar: BottomNavigator(bottomNavIndex: 1)); } - Widget exerciseWidget(ExerciseRepository exerciseRepository) { - TreeViewController _treeViewController = TreeViewController(children: nodeExercises(exerciseRepository) ); - - TreeViewTheme _treeViewTheme = TreeViewTheme( - expanderTheme: ExpanderThemeData( - type: ExpanderType.caret, - modifier: ExpanderModifier.none, - position: ExpanderPosition.start, - color: Colors.red.shade800, - size: 20, - ), - labelStyle: TextStyle( - fontSize: 12, - letterSpacing: 0.1, - ), - parentLabelStyle: TextStyle( - fontSize: 16, - letterSpacing: 0.1, - fontWeight: FontWeight.w800, - color: Colors.orange.shade600, - ), - iconTheme: IconThemeData( - size: 18, - color: Colors.grey.shade800, - ), - colorScheme: ColorScheme.light( - background: Colors.transparent - ), - ); - - return TreeView( - controller: _treeViewController, - allowParentSelect: false, - supportParentDoubleTap: false, - //onExpansionChanged: _expandNodeHandler, - onNodeTap: (key) { - setState(() { - _treeViewController = _treeViewController.copyWith(selectedKey: key); - }); - }, - theme: _treeViewTheme, - ); - } - - List nodeExercises(ExerciseRepository exerciseRepository) { - List nodes = List(); - List exercises = exerciseRepository.getExerciseList(); - - String prevDay = ""; - Node actualNode; - List listExercisesPerDay; - exercises.forEach((element) { - Exercise exercise = element; - ExerciseType exerciseType = - exerciseRepository.getExerciseTypeById(exercise.exerciseTypeId); - String actualDay = exercise.dateAdd.year.toString()+"-"+ - exercise.dateAdd.month.toString()+"-"+ - exercise.dateAdd.day.toString(); - - if ( prevDay.compareTo(actualDay) != 0) { - listExercisesPerDay = List(); - actualNode = - Node( - label: actualDay, - key: exercise.dateAdd.toString(), - expanded: true, - children: listExercisesPerDay, - icon: NodeIcon( - codePoint: Icons.date_range.codePoint, - color: "blue" - ) - ); - nodes.add(actualNode); - prevDay = actualDay; - } - - String exerciseName = AppLanguage().appLocal == Locale("en") ? - exerciseType.name : - exerciseType.nameTranslation; - String unitQuantity = exerciseType.unitQuantity == "1" ? - exercise.unitQuantity.toStringAsFixed(0) - + " " + AppLocalizations.of(context).translate(exerciseType.unitQuantityUnit) + " " - : ""; - - String labelExercise = - exerciseName + " " + unitQuantity - + exercise.quantity.toStringAsFixed(0) + " " - + AppLocalizations.of(context).translate(exercise.unit); - listExercisesPerDay.add( - Node( - label: labelExercise, - key: exercise.exerciseId.toString(), - expanded: false, - icon: NodeIcon( - codePoint: Icons.repeat.codePoint, - color: "blue" - ) - ) + Widget hiddenWidget(CustomerRepository customerRepository, ExerciseRepository exerciseRepository) { + final LinkedHashMap args = LinkedHashMap(); + if (Cache().getTrainee() != null) { + return FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + args['exerciseRepository'] = exerciseRepository, + args['customerRepository'] = customerRepository, + args['customerId'] = Cache().getTrainee().customerId, + Navigator.of(context).pushNamed('exerciseLogPage', + arguments: args) + }, + child: Text("My Trainee's Exercise Logs", + style: TextStyle(fontSize: 18),) ); - - - }); - return nodes; + } else { + return Container(); + } } + } diff --git a/lib/view/myexcercise_plan_page.dart b/lib/view/myexcercise_plan_page.dart new file mode 100644 index 0000000..68351d4 --- /dev/null +++ b/lib/view/myexcercise_plan_page.dart @@ -0,0 +1,196 @@ +import 'dart:collection'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/widgets/app_bar_common.dart'; +import 'package:aitrainer_app/widgets/bottom_nav.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class MyExercisePlanPage extends StatefulWidget { + @override + _MyExercisePlanPage createState() => _MyExercisePlanPage(); +} + +class _MyExercisePlanPage extends State { + @override + Widget build(BuildContext context) { + final ExerciseRepository exerciseRepository = ExerciseRepository(); + final LinkedHashMap args = LinkedHashMap(); + + return Scaffold( + appBar: AppBarCommonNav(), + body: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.png'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: CustomScrollView( + scrollDirection: Axis.vertical, + slivers: + [ + SliverGrid( + delegate: SliverChildListDelegate( + [ + FlatButton( + padding: EdgeInsets.all(10), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + args['customerId'] = Cache().userLoggedIn.customerId, + Navigator.of(context).pushNamed('exerciseByPlanPage', + arguments: args) + }, + child: Text("Execute My Selected Training Plan", + style: TextStyle(fontSize: 18),) + ), + + FlatButton( + padding: EdgeInsets.all(0), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + args['exerciseRepository'] = exerciseRepository, + args['customerId'] = Cache().userLoggedIn.customerId, + Navigator.of(context).pushNamed('exercisePlanCustomPage', + arguments: args) + }, + child: Text("Edit My Custom Plan", + style: TextStyle(fontSize: 18),) + ), + FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + + }, + child: Text("Suggested Plan", + style: TextStyle(fontSize: 18),) + ), + Stack( + fit: StackFit.passthrough, + overflow: Overflow.clip, + alignment: Alignment.topLeft, + children: [ + Image.asset('asset/image/lock.png', + height: 40, + width: 40, + ), + FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + + }, + child: Text("My Special Plan", + style: TextStyle(fontSize: 18),) + ), + + ], + ), + + Stack( + fit: StackFit.passthrough, + overflow: Overflow.clip, + children: [ + Image.asset('asset/image/lock.png', + height: 40, + width: 40, + ), + FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + + }, + child: Text("My Arnold's Plan", + style: TextStyle(fontSize: 18),) + ), + + ] + ), + + hiddenPlanWidget(exerciseRepository), + hiddenTrainingWidget(), + + + ] + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 20.0, + crossAxisSpacing: 20.0, + childAspectRatio: 1.2, + ), + ) + ] + ) + ), + bottomNavigationBar: BottomNavigator(bottomNavIndex: 2)); + } + + Widget hiddenPlanWidget(ExerciseRepository exerciseRepository) { + final LinkedHashMap args = LinkedHashMap(); + if ( Cache().getTrainee() != null ) { + return FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + args['exerciseRepository'] = exerciseRepository, + args['customerId'] = Cache().getTrainee().customerId, + Navigator.of(context).pushNamed('exercisePlanCustomPage', + arguments: args) + }, + child: Text("My Trainee's Plan", + style: TextStyle(fontSize: 18),) + ); + } else { + return Container(); + } + } + + Widget hiddenTrainingWidget() { + final LinkedHashMap args = LinkedHashMap(); + if ( Cache().getTrainee() != null ) { + print ("!!Trainee: " + Cache().getTrainee().firstname + " " + Cache().getTrainee().name); + return FlatButton( + padding: EdgeInsets.all(20), + textColor: Colors.white, + color: Colors.black12, + focusColor: Colors.blueAccent, + onPressed: () => + { + args['customerId'] = Cache().getTrainee().customerId, + Navigator.of(context).pushNamed('exerciseByPlanPage', + arguments: args) + }, + child: Text("Execute My Trainee's Training Plan", + style: TextStyle(fontSize: 18),) + ); + } else { + return Container(); + } + } + +} + + diff --git a/lib/view/settings.dart b/lib/view/settings.dart index 03bad7d..3112dcd 100644 --- a/lib/view/settings.dart +++ b/lib/view/settings.dart @@ -109,6 +109,13 @@ class SettingsPage extends StatelessWidget{ ) ), + ListTile( + leading: Icon(Icons.get_app), + title: RaisedButton( + child: Text("Check lang", style: TextStyle(fontSize: 12),), + onPressed: () => settingsBloc.add(SettingsGetLanguage()), + ) + ) ] ); } diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index d20d300..1b273d7 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -56,14 +56,15 @@ class _AppBarNav extends State with SingleTickerProviderStateMixin { } }); */ colorAnim = RainbowColorTween([Colors.white70, - Colors.greenAccent, - Colors.lightGreen, - Colors.lightGreenAccent, - Colors.yellow, + Colors.blueGrey, + Colors.blueAccent, + Colors.lightBlue, + Colors.lightBlueAccent, Colors.yellowAccent, Colors.orange, Colors.orangeAccent, - Colors.white70]) + Colors.yellowAccent, + Color(0xffcce6ff)]) .animate(colorController) ..addListener(() { setState(() {}); }) ..addStatusListener((status) { @@ -103,7 +104,9 @@ class _AppBarNav extends State with SingleTickerProviderStateMixin { icon: Icon(Icons.arrow_back, color: Colors.white), onPressed: () => { - menuBloc.add(MenuTreeUp(parent: 0)) + if ( menuBloc != null ) { + menuBloc.add(MenuTreeUp(parent: 0)), + } }, ) ); @@ -151,7 +154,7 @@ class _AppBarNav extends State with SingleTickerProviderStateMixin { trailing: Icon(percent > 0.6 ? Icons.mood : Icons.mood_bad, color: colorAnim.value,), linearStrokeCap: LinearStrokeCap.roundAll, backgroundColor: colorAnim.value, - progressColor: Colors.blue, + progressColor: Color(0xff73e600), animation: true, ), ], diff --git a/lib/widgets/app_bar_common.dart b/lib/widgets/app_bar_common.dart new file mode 100644 index 0000000..f6c6c02 --- /dev/null +++ b/lib/widgets/app_bar_common.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'package:aitrainer_app/localization/app_localization.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; +import 'package:rainbow_color/rainbow_color.dart'; + + +class AppBarCommonNav extends StatefulWidget implements PreferredSizeWidget { + + @override + _AppBarCommonNav createState() => _AppBarCommonNav(); + + @override + Size get preferredSize => const Size.fromHeight(60); +} + +class _AppBarCommonNav extends State with SingleTickerProviderStateMixin { + Animation colorAnim; + AnimationController colorController; + + @override + void initState() { + + colorController = + AnimationController(duration: Duration(seconds: 4), vsync: this); + + colorAnim = RainbowColorTween([Colors.white70, + Colors.blueGrey, + Colors.blueAccent, + Colors.lightBlue, + Colors.lightBlueAccent, + Colors.yellowAccent, + Colors.orange, + Colors.orangeAccent, + Colors.yellowAccent, + Color(0xffcce6ff)]) + .animate(colorController) + ..addListener(() { setState(() {}); }) + ..addStatusListener((status) { + if (status == AnimationStatus.completed) { + Timer(Duration(seconds: 10), () { + //colorController.reset(); + if ( mounted ) { + colorController.forward(); + } + }); + } else if (status == AnimationStatus.dismissed) { + colorController.forward(); + } + }); + colorController.forward(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.black, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + getAnimatedWidget(), + Image.asset( + 'asset/image/WT_long_logo.png', + fit: BoxFit.cover, + height: 65.0, + ), + ], + ), + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => + { + Navigator.of(context).pop() + }, + ) + ); + } + + @override + void dispose() { + //sizeController.dispose(); + colorController.dispose(); + super.dispose(); + } + + Widget getAnimatedWidget() { + double percent = Cache().getPercentExercises(); + if ( percent == -1) { + ExerciseRepository exerciseRepository = ExerciseRepository(); + exerciseRepository.getBaseExerciseFinishedPercent(); + percent = Cache().getPercentExercises(); + } + int sizeExerciseList = Cache().getExercises() == null? 0 : Cache().getExercises().length; + if ( sizeExerciseList == 0 ) { + return Stack( + alignment: Alignment.topLeft, + children: [ + Text(AppLocalizations.of(context).translate("Make your first test"), + style: TextStyle(fontSize: 16, color: colorAnim.value, shadows: [Shadow(color: Colors.purple , blurRadius: 15)]), + + ), + //TestProgress(animation: sizeAnim), + ] + ); + } else { + + return Stack( + alignment: Alignment.topLeft, + children: [ + LinearPercentIndicator( + width: 120.0, + lineHeight: 14.0, + percent: percent, + center: Text( + (percent * 100).toStringAsFixed(0) + "% finished", + style: new TextStyle(fontSize: 12.0), + ), + trailing: Icon(percent > 0.6 ? Icons.mood : Icons.mood_bad, color: colorAnim.value,), + linearStrokeCap: LinearStrokeCap.roundAll, + backgroundColor: colorAnim.value, + progressColor: Color(0xff73e600), + animation: true, + ), + ], + ); + } + } +} diff --git a/lib/widgets/bottom_nav.dart b/lib/widgets/bottom_nav.dart index 4d9715c..50fad6b 100644 --- a/lib/widgets/bottom_nav.dart +++ b/lib/widgets/bottom_nav.dart @@ -91,12 +91,13 @@ class _NawDrawerWidget extends State { break; case 1: - Navigator.of(context).pop(); - Navigator.of(context).pushNamed('mydevelopment'); + //Navigator.of(context).pop(); + Navigator.of(context).pushNamed('myDevelopment'); break; case 2: - //throw new StateError('This is a Dart exception on event.'); + //Navigator.of(context).pop(); + Navigator.of(context).pushNamed('myExercisePlan'); break; case 3: diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index b613d38..5c578f5 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -24,7 +24,6 @@ class AitrainerHome extends StatefulWidget { class _HomePageState extends State { GlobalKey _scaffoldKey = new GlobalKey(); - final AppLanguage appLanguage = AppLanguage(); @override void initState() { @@ -45,7 +44,9 @@ class _HomePageState extends State { sessionBloc.add(SessionStart()); // ignore: close_sinks SettingsBloc settingsBloc = BlocProvider.of(context); - settingsBloc.loadLang(); + String lang = AppLanguage().appLocal.languageCode; + print (" -- Loading delayed lang $lang"); + settingsBloc.add(SettingsChangeLanguage(language: lang)); } } }); diff --git a/lib/widgets/loading.dart b/lib/widgets/loading.dart index 2dce838..974b39b 100644 --- a/lib/widgets/loading.dart +++ b/lib/widgets/loading.dart @@ -6,8 +6,8 @@ class LoadingScreenMain extends StatelessWidget { Widget build(BuildContext context) { Image _backgroundImage = Image.asset('asset/image/WT01_loading_layers.png', fit: BoxFit.cover, - //height: double.infinity, - //width: double.infinity, + height: double.infinity, + width: double.infinity, alignment: Alignment.center, ); return Scaffold( diff --git a/lib/widgets/menu_page_widget.dart b/lib/widgets/menu_page_widget.dart index 8dfd097..ac3c14c 100644 --- a/lib/widgets/menu_page_widget.dart +++ b/lib/widgets/menu_page_widget.dart @@ -37,7 +37,7 @@ class MenuPageWidget extends StatelessWidget { child: Center( child: Stack( alignment: Alignment.bottomLeft, - fit: StackFit.loose, + clipBehavior: Clip.antiAliasWithSaveLayer, children: [ FlatButton( child: _getButtonImage(workoutTree), @@ -45,21 +45,35 @@ class MenuPageWidget extends StatelessWidget { shape: getShape(workoutTree), onPressed: () => menuClick(workoutTree, menuBloc, context), ), - InkWell( - onTap:() => menuClick(workoutTree, menuBloc, context), - child: Text( - " " + workoutTree.name, - maxLines: 2, - style: TextStyle( - color: workoutTree.color, - fontSize: workoutTree.fontSize, - fontFamily: 'Arial', - fontWeight: FontWeight.w900), + Positioned( + top: workoutTree.name.length > 30 ? 140 : 150, + left: 5, + child: Container( + height: 300, + width: 280, + child: InkWell( + onTap:() => menuClick(workoutTree, menuBloc, context), + child: Text( + " " + workoutTree.name, + maxLines: 2, + style: TextStyle( + color: workoutTree.color, + fontSize: workoutTree.fontSize, + fontFamily: 'Arial', + fontWeight: FontWeight.w900), + ), + + highlightColor: workoutTree.color, + ), + color: Colors.transparent, ), - highlightColor: workoutTree.color, ), - ])))); + ] + ) + ) + ) + ); }); SliverList sliverList = diff --git a/pubspec.lock b/pubspec.lock index a77334a..7b7c8aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,7 +49,7 @@ packages: name: bloc_test url: "https://pub.dartlang.org" source: hosted - version: "7.0.3" + version: "7.0.4" boolean_selector: dependency: transitive description: @@ -175,7 +175,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.14.0" + version: "0.14.1" crypto: dependency: transitive description: @@ -196,7 +196,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.0.0" dart_style: dependency: transitive description: @@ -210,14 +210,14 @@ packages: name: devicelocale url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.2" equatable: dependency: "direct main" description: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "1.2.4" + version: "1.2.5" fake_async: dependency: transitive description: @@ -250,7 +250,7 @@ packages: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "6.0.4" + version: "6.0.5" flutter_driver: dependency: "direct dev" description: flutter @@ -283,7 +283,7 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.7.5" + version: "0.8.0" flutter_local_notifications: dependency: "direct main" description: @@ -541,7 +541,7 @@ packages: name: percent_indicator url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.6" petitparser: dependency: transitive description: @@ -578,12 +578,12 @@ packages: source: hosted version: "3.0.13" provider: - dependency: "direct dev" + dependency: transitive description: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.3.2+1" + version: "4.3.2+2" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0fef500..78a0b86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.1.0+12 +version: 1.1.0+20 environment: sdk: ">=2.7.0 <3.0.0" @@ -26,19 +26,19 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.3 - devicelocale: ^0.3.1 + cupertino_icons: ^1.0.0 + devicelocale: ^0.3.2 sentry: ^3.0.1 # firebase_messaging: ^6.0.16 flutter_local_notifications: 1.1.1 flutter_facebook_login: ^3.0.0 - flutter_bloc: ^6.0.4 + flutter_bloc: ^6.0.5 equatable: ^1.2.4 freezed: ^0.11.6 flutter_form_bloc: ^0.19.0 spider_chart: ^0.1.5 rainbow_color: ^0.1.1 - percent_indicator: ^2.1.5 + percent_indicator: ^2.1.6 gradient_bottom_navigation_bar: ^1.0.0+4 flutter_treeview: ^0.6.0+1 @@ -51,17 +51,16 @@ dev_dependencies: flutter_driver: sdk: flutter test: any - bloc_test: ^7.0.1 + bloc_test: ^7.0.3 build_runner: http: 0.12.1 - provider: ^4.3.2+1 intl: 0.16.1 shared_preferences: ^0.5.10 - flutter_launcher_icons: ^0.7.5 + flutter_launcher_icons: ^0.8.0 flutter_icons: android: "launcher_icon" @@ -94,6 +93,7 @@ flutter: - asset/image/WT_weight_loss.png - asset/image/WT_welcome.png - asset/image/login_fb.png + - asset/image/lock.png - asset/menu/1.cardio.png - asset/menu/1.1.aerob.png - asset/menu/1.2.anaerob.png diff --git a/test/account_bloc_test.dart b/test/account_bloc_test.dart new file mode 100644 index 0000000..7cfb618 --- /dev/null +++ b/test/account_bloc_test.dart @@ -0,0 +1,79 @@ +import 'package:aitrainer_app/bloc/account/account_bloc.dart'; +import 'package:aitrainer_app/repository/customer_repository.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart' as test; +import 'package:flutter_test/flutter_test.dart'; + +class MockCustomerRepository extends Mock implements CustomerRepository { + +} + +void main() { + MockCustomerRepository customerRepository; + AccountBloc accountBloc; + + TestWidgetsFlutterBinding.ensureInitialized(); + + test.setUp(() { + customerRepository = MockCustomerRepository(); + accountBloc = AccountBloc(customerRepository: customerRepository); + }); + + test.tearDown(() { + accountBloc?.close(); + }); + + test.test('initial state is correct', () { + expect(accountBloc.state, AccountInitial()); + }); + + group('Account', () { + test.test( + 'emits [loading, logged in] when the customer clicked login', + () { + final expectedResponse = [ + AccountLoading(), + AccountLoggedIn(), + ]; + + //verify(accountBloc.customerRepository.customer == null); + + expectLater( + accountBloc, emitsInOrder(expectedResponse), + ); + + accountBloc.add(AccountLogin()); + }); + }); + + test.test( + 'emits [loading, logged out] when the customer clicked logout', + () { + final expectedResponse = [ + AccountLoading(), + AccountLoggedOut(), + ]; + + expectLater( + accountBloc, emitsInOrder(expectedResponse), + ); + + accountBloc.add(AccountLogout()); + }); + + test.test( + 'emits [loading, logged out] when the customer data changed', + () { + final expectedResponse = [ + AccountLoading(), + AccountReady(), + ]; + + expectLater( + accountBloc, emitsInOrder(expectedResponse), + ); + + accountBloc.add(AccountChangeCustomer()); + }); + +} diff --git a/test/customer_service_test.dart b/test/customer_service_test.dart new file mode 100644 index 0000000..d2d7018 --- /dev/null +++ b/test/customer_service_test.dart @@ -0,0 +1,52 @@ +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/service/customer_service.dart'; +import 'package:aitrainer_app/util/env.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +//import 'package:mockito/mockito.dart'; + +// Create a MockClient using the Mock class provided by the Mockito package. +// Create new instances of this class in each test. +/* class MockClient extends Mock implements CustomerApi { + Future> getTrainees(int trainerId) async { + if (trainerId == 62) { + List list = List(); + Customer customer1 = Customer(firstname: "Zalán", name: "Boss"); + Customer customer2 = Customer(firstname: "Zétény", name: "Boss"); + list.add(customer1); + list.add(customer2); + return list; + } else { + throw Exception("No trainees found"); + } + } +} */ + +main() { + + + setUp(() { + Cache().setTestBaseUrl(); + }); + + group('fetchPost', () { + test('returns a List if the http call completes successfully', () async { + final client = CustomerApi(); + + // Use Mockito to return a successful response when it calls the + // provided http.Client. + List trainees = List(); + trainees = await client.getTrainees(62); + + expect(trainees.length, 2); + expect(trainees[0].firstname, "Zalán"); + }); + + test('throws an exception if the http call completes with an error', () async { + final client = CustomerApi(); + + expect(client.getTrainees(22), throwsException); + }); + }); +} \ No newline at end of file diff --git a/test/exercise_plan_test.dart b/test/exercise_plan_test.dart new file mode 100644 index 0000000..2836749 --- /dev/null +++ b/test/exercise_plan_test.dart @@ -0,0 +1,72 @@ +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/exercise_plan.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/service/exercise_plan_service.dart'; +import 'package:test/test.dart'; + +main() { + + setUp(() { + Cache().setTestBaseUrl(); + }); + + group('new Plan', () { + test('add new plan and plan details', () async { + final client = ExercisePlanApi(); + + ExercisePlan exercisePlan = ExercisePlan( + "Test plan " + DateTime.now().toIso8601String(), + 62 + ); + exercisePlan.dateAdd = DateTime.now(); + + ExercisePlan savedPlan = await client.saveExercisePlan(exercisePlan); + expect(savedPlan.name.substring(0, 9), "Test plan"); + expect(savedPlan.customerId, 62); + + + + int newPlanId = savedPlan.exercisePlanId; + //savedPlan.exercisePlanId = newPlanId; + + ExercisePlanDetail detail = ExercisePlanDetail( + 39 //exerciseTypeId + ); + detail.serie = 3; + detail.repeats = 12; + detail.weightEquation = "90"; + detail.exercisePlanId = newPlanId; + + ExercisePlanDetail savedDetail = await client.saveExercisePlanDetail(detail); + + expect(savedDetail.weightEquation, "90"); + expect(savedDetail.repeats, 12); + expect(savedDetail.exercisePlanId, newPlanId); + + await client.deleteExercisePlanDetail(savedDetail.exercisePlanDetailId); + await client.deleteExercisePlan(savedPlan.exercisePlanId); + + }); + }); + + test('get the last plan and change plan details', () async { + final client = ExercisePlanApi(); + + ExercisePlan exercisePlan = await client.getLastExercisePlan(61); + List list = await client.getExercisePlanDetail(exercisePlan.exercisePlanId); + + expect(list.length, 2); + ExercisePlanDetail detail = ExercisePlanDetail(3); + detail.serie = 4; + detail.repeats = 12; + detail.weightEquation = "10"; + detail.exercisePlanId = exercisePlan.exercisePlanId; + //list.add(detail); + ExercisePlanDetail newObjectToSave = await client.saveExercisePlanDetail(detail); + List list2 = await client.getExercisePlanDetail(exercisePlan.exercisePlanId); + expect(list2.length, 3); + expect(list2.last.weightEquation, "10"); + await client.deleteExercisePlanDetail(newObjectToSave.exercisePlanDetailId); + + }); +} \ No newline at end of file