diff --git a/asset/image/WT_cup_victory400.png b/asset/image/WT_cup_victory400.png new file mode 100644 index 0000000..55a5b9d Binary files /dev/null and b/asset/image/WT_cup_victory400.png differ diff --git a/asset/menu/weight_test.jpg b/asset/menu/free_weight_test.jpg similarity index 100% rename from asset/menu/weight_test.jpg rename to asset/menu/free_weight_test.jpg diff --git a/asset/menu/lower_body_test.jpg b/asset/menu/lower_body_test.jpg new file mode 100644 index 0000000..cfb1cab Binary files /dev/null and b/asset/menu/lower_body_test.jpg differ diff --git a/asset/menu/no_equipment_test.jpg b/asset/menu/no_equipment_test.jpg new file mode 100644 index 0000000..e5a2910 Binary files /dev/null and b/asset/menu/no_equipment_test.jpg differ diff --git a/asset/menu/test_on_machines.jpg b/asset/menu/test_on_machines.jpg new file mode 100644 index 0000000..83c54ab Binary files /dev/null and b/asset/menu/test_on_machines.jpg differ diff --git a/asset/menu/upper_body_test.jpg b/asset/menu/upper_body_test.jpg new file mode 100644 index 0000000..e7aca76 Binary files /dev/null and b/asset/menu/upper_body_test.jpg differ diff --git a/asset/menu/weight_free_test.jpg b/asset/menu/weight_free_test.jpg new file mode 100644 index 0000000..7b74edc Binary files /dev/null and b/asset/menu/weight_free_test.jpg differ diff --git a/i18n/en.json b/i18n/en.json index 8cd6372..7e6994f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -391,6 +391,22 @@ "Change the weight to":"Change the weight to", "Search Exercises...":"Search Exercises...", - "No exercise found":"No exercise found" + "No exercise found":"No exercise found", + + "Edit Your Training Test Set":"Edit Your Training Test Set", + "Start training":"Start training", + "Enjoy the exercises, good luck with the testing!":"Enjoy the exercises, good luck with the testing!", + "Please continue with the next exercise in the queue:":"Please continue with the next exercise in the queue:", + "Or, you can redifine this exercise queue in the Compact Test menu":"Or, you can redifine this exercise queue in the Compact Test menu", + "you are able to do 12-20 repeats with":"you are able to do 12-20 repeats with", + "You have an active Test Set!":"You have an active Test Set!", + "Press OK to continue":"Press OK to continue", + "Continue":"Continue", + " your ":" your ", + "\nyour plan is available for 24 hours":"\nyour plan is available for 24 hours", + "Start":"Start", + "Compact Test":"Compact Test", + "Custom Test":"Custom Test", + "Set": "Set" } \ No newline at end of file diff --git a/i18n/hu.json b/i18n/hu.json index 3d40e15..cdac4f0 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -387,6 +387,23 @@ "Change the weight to":"Súly változtatása", "Search Exercises...":"Gyakorlat keresése...", - "No exercise found":"Nincs ilyen gyakorlat" + "No exercise found":"Nincs ilyen gyakorlat", + + "Edit Your Training Test Set":"Válaszd ki a gyakorlatokat", + "Start training":"Edzés kezdése", + "Enjoy the exercises, good luck with the testing!":"Élvezd a gyakorlatokat, sok sikert a teszteléshez!", + "Please continue with the next exercise in the queue:":"Kérlek folytasd a következő gyakorlattal:", + "Or, you can redifine this exercise queue in the Compact Test menu":"Vagy változtatsd meg a gyakorlatokat a Tesztközpontban", + "you are able to do 12-20 repeats with":"amivel képes vagy 12-20 ismétlésre", + "You have an active Test Set!":"Van egy aktiv tesztköröd!", + "Press OK to continue":"Nyomd meg az OK-t a folytatáshoz", + "Continue":"Folytatsd", + " your ":": ", + "\nyour plan is available for 24 hours":"\na teszt 24 óráig aktív", + "Start":"Kezdd el", + "Compact Test":"Kompakt teszt", + "Custom Test":"Egyedi teszt", + "Set": "Széria" + } \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 60937e8..7c37065 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -531,7 +531,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -566,7 +566,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/lib/bloc/exercise_control/exercise_control_bloc.dart b/lib/bloc/exercise_control/exercise_control_bloc.dart index 744788a..636af70 100644 --- a/lib/bloc/exercise_control/exercise_control_bloc.dart +++ b/lib/bloc/exercise_control/exercise_control_bloc.dart @@ -13,7 +13,6 @@ class ExerciseControlBloc extends Bloc with Logg exerciseRepository.exerciseType = exerciseType; exerciseRepository.setUnit(exerciseType.unit); exerciseRepository.setUnitQuantity(unitQuantity); + exerciseRepository.setQuantity(quantity); exerciseRepository.exercise.exercisePlanDetailId = 0; exerciseRepository.start = DateTime.now(); if (Cache().userLoggedIn != null) { @@ -74,30 +74,6 @@ class ExerciseNewBloc extends Bloc with Logg if (exerciseType.unit == "second") { stopWatchTimer.rawTime.listen((value) => {timerValue = value, this.setQuantity((value / 1000).toDouble())}); } - this.setExerciseTask(init: true); - } - - String setExerciseTask({bool init = false}) { - if (this.exerciseRepository.exerciseType == null) { - print("WTF, exerciseType is null"); - return ""; - } - if (this.exerciseRepository.exerciseType.unit != "second") { - if (menuBloc.ability.toString() == ExerciseAbility.oneRepMax.toString()) { - this.exerciseTask = "Please take a relative bigger weight and repeat 12-20 times"; - if (init == true) { - this.setQuantity(12); - } - } else if (this.exerciseRepository.exerciseType.isEndurance() && - menuBloc.ability.toString() == ExerciseAbility.endurance.toString() && - exerciseRepository.exerciseType.unitQuantity == "1") { - this.exerciseTask = "Please take a medium weight and repeat 20-30 times"; - if (init == true) { - this.setQuantity(20); - } - } - } - return this.exerciseTask; } void setQuantity(double quantity) { @@ -171,10 +147,11 @@ class ExerciseNewBloc extends Bloc with Logg yield ExerciseNewLoading(); exerciseRepository.end = DateTime.now(); await exerciseRepository.addExercise(); + exerciseRepository.initExercise(); menuBloc.add(MenuTreeDown(parent: 0)); Cache().initBadges(); Track().track(TrackingEvent.exercise_new, eventValue: exerciseRepository.exerciseType.name); - yield ExerciseNewReady(); + yield ExerciseNewSaved(); } else if (event is ExerciseNewBMIAnimate) { yield ExerciseNewLoading(); yield ExerciseNewReady(); diff --git a/lib/bloc/exercise_new/exercise_new_state.dart b/lib/bloc/exercise_new/exercise_new_state.dart index cdfc059..db081fd 100644 --- a/lib/bloc/exercise_new/exercise_new_state.dart +++ b/lib/bloc/exercise_new/exercise_new_state.dart @@ -20,10 +20,14 @@ class ExerciseNewReady extends ExerciseNewState { const ExerciseNewReady(); } +class ExerciseNewSaved extends ExerciseNewState { + const ExerciseNewSaved(); +} + class ExerciseNewError extends ExerciseNewState { final String message; const ExerciseNewError({this.message}); @override List get props => [message]; -} \ No newline at end of file +} diff --git a/lib/bloc/menu/menu_bloc.dart b/lib/bloc/menu/menu_bloc.dart index 3c26bf0..1b01783 100644 --- a/lib/bloc/menu/menu_bloc.dart +++ b/lib/bloc/menu/menu_bloc.dart @@ -166,7 +166,7 @@ class MenuBloc extends Bloc with Trans, Logging { void setAbility(String name) { switch (name) { - case "One Rep Max": + case "Muscle Build / Shape Toning": ability = ExerciseAbility.oneRepMax; break; case "Endurance": @@ -176,13 +176,13 @@ class MenuBloc extends Bloc with Trans, Logging { ability = ExerciseAbility.running; break; case "Test Center": - ability = ExerciseAbility.mini_test; + ability = ExerciseAbility.mini_test_set; break; case "My Body": ability = ExerciseAbility.none; break; } - log("Ability: " + ability.toString() + " name:" + name); + log("Ability: " + ability.toString() + " name: " + name); } Future getImages(LinkedHashMap branch) async { @@ -231,7 +231,6 @@ class MenuBloc extends Bloc with Trans, Logging { } } }); - return filtered; } } diff --git a/lib/bloc/settings/settings_bloc.dart b/lib/bloc/settings/settings_bloc.dart index 25553b2..18aa73a 100644 --- a/lib/bloc/settings/settings_bloc.dart +++ b/lib/bloc/settings/settings_bloc.dart @@ -37,7 +37,6 @@ class SettingsBloc extends Bloc with Logging { if (event is SettingsChangeLanguage) { yield SettingsLoading(); await _changeLang(event.language); - Track().track(TrackingEvent.settings_lang); yield SettingsReady(_locale); } else if (event is SettingsGetLanguage) { await AppLanguage().fetchLocale(); @@ -47,7 +46,7 @@ class SettingsBloc extends Bloc with Logging { yield SettingsLoading(); final bool live = event.live; Cache().setServer(live); - Track().track(TrackingEvent.settings_server); + Track().track(TrackingEvent.settings_server, eventValue: live.toString()); yield SettingsReady(_locale); } else if (event is SettingsSetHardware) { yield SettingsLoading(); diff --git a/lib/bloc/test_set_control/test_set_control_bloc.dart b/lib/bloc/test_set_control/test_set_control_bloc.dart new file mode 100644 index 0000000..e3ecc0c --- /dev/null +++ b/lib/bloc/test_set_control/test_set_control_bloc.dart @@ -0,0 +1,69 @@ +import 'dart:async'; + +import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/exercise.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'test_set_control_event.dart'; +part 'test_set_control_state.dart'; + +class TestSetControlBloc extends Bloc { + final ExercisePlanDetail exercisePlanDetail; + final TestSetExecuteBloc executeBloc; + final ExerciseType exerciseType; + final ExerciseRepository exerciseRepository = ExerciseRepository(); + TestSetControlBloc({this.exercisePlanDetail, this.executeBloc, this.exerciseType}) : super(TestSetControlInitial()) { + initBloc(); + } + + void initBloc() { + if (exercisePlanDetail.exerciseType.unitQuantity != null) { + oneRepMax = executeBloc.calculate1RM(exercisePlanDetail.exercises.last.unitQuantity, exercisePlanDetail.exercises.last.quantity); + initQuantity = 12; + quantity = initQuantity; + initUnitQuantity = oneRepMax * 0.75; + unitQuantity = initUnitQuantity; + } + step = exercisePlanDetail.exercises.length; + exerciseRepository.customer = Cache().userLoggedIn; + exerciseRepository.exerciseType = exerciseType; + } + + double initQuantity; + double initUnitQuantity; + double quantity; + double unitQuantity; + double oneRepMax; + int step; + + @override + Stream mapEventToState( + TestSetControlEvent event, + ) async* { + try { + if (event is TestSetControlQuantityChange) { + quantity = event.quantity; + } else if (event is TestSetControlUnitQuantityChange) { + unitQuantity = event.quantity; + } else if (event is TestSetControlSubmit) { + final Exercise exercise = Exercise(); + exercise.quantity = quantity; + exercise.unit = exerciseType.unit; + exercise.unitQuantity = unitQuantity; + exercise.dateAdd = DateTime.now(); + exerciseRepository.exercise = exercise; + + await exerciseRepository.addExercise(); + executeBloc.add( + TestSetExecuteExerciseFinished(exerciseTypeId: exerciseType.exerciseTypeId, quantity: quantity, unitQuantity: unitQuantity)); + } + } on Exception catch (e) { + yield TestSetControlError(message: e.toString()); + } + } +} diff --git a/lib/bloc/test_set_control/test_set_control_event.dart b/lib/bloc/test_set_control/test_set_control_event.dart new file mode 100644 index 0000000..0eb4620 --- /dev/null +++ b/lib/bloc/test_set_control/test_set_control_event.dart @@ -0,0 +1,32 @@ +part of 'test_set_control_bloc.dart'; + +abstract class TestSetControlEvent extends Equatable { + const TestSetControlEvent(); + + @override + List get props => []; +} + +class TestSetControlLoad extends TestSetControlEvent { + const TestSetControlLoad(); +} + +class TestSetControlQuantityChange extends TestSetControlEvent { + final double quantity; + const TestSetControlQuantityChange({this.quantity}); + + @override + List get props => [quantity]; +} + +class TestSetControlUnitQuantityChange extends TestSetControlEvent { + final double quantity; + const TestSetControlUnitQuantityChange({this.quantity}); + + @override + List get props => [quantity]; +} + +class TestSetControlSubmit extends TestSetControlEvent { + const TestSetControlSubmit(); +} diff --git a/lib/bloc/test_set_control/test_set_control_state.dart b/lib/bloc/test_set_control/test_set_control_state.dart new file mode 100644 index 0000000..c3e6f98 --- /dev/null +++ b/lib/bloc/test_set_control/test_set_control_state.dart @@ -0,0 +1,28 @@ +part of 'test_set_control_bloc.dart'; + +abstract class TestSetControlState extends Equatable { + const TestSetControlState(); + + @override + List get props => []; +} + +class TestSetControlInitial extends TestSetControlState { + const TestSetControlInitial(); +} + +class TestSetControlLoading extends TestSetControlState { + const TestSetControlLoading(); +} + +class TestSetControlReady extends TestSetControlState { + const TestSetControlReady(); +} + +class TestSetControlError extends TestSetControlState { + final String message; + const TestSetControlError({this.message}); + + @override + List get props => [message]; +} diff --git a/lib/bloc/test_set_edit/test_set_edit_bloc.dart b/lib/bloc/test_set_edit/test_set_edit_bloc.dart index b3143d4..a381265 100644 --- a/lib/bloc/test_set_edit/test_set_edit_bloc.dart +++ b/lib/bloc/test_set_edit/test_set_edit_bloc.dart @@ -10,6 +10,8 @@ import 'package:aitrainer_app/model/exercise_plan_template.dart'; import 'package:aitrainer_app/model/exercise_type.dart'; import 'package:aitrainer_app/repository/workout_tree_repository.dart'; import 'package:aitrainer_app/service/exercise_plan_service.dart'; +import 'package:aitrainer_app/util/enums.dart'; +import 'package:aitrainer_app/util/track.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; @@ -18,12 +20,15 @@ part 'test_set_edit_state.dart'; class TestSetEditBloc extends Bloc { final String templateName; + final String templateNameTranslation; final WorkoutTreeRepository workoutTreeRepository; final MenuBloc menuBloc; final List _exerciseTypes = List(); + final List _actualExerciseTypes = List(); final HashMap _exercisePlanDetails = HashMap(); - TestSetEditBloc({this.templateName, this.workoutTreeRepository, this.menuBloc}) : super(TestSetEditInitial()) { + TestSetEditBloc({this.templateName, this.templateNameTranslation, this.workoutTreeRepository, this.menuBloc}) + : super(TestSetEditInitial()) { if (Cache().exercisePlanTemplates.isNotEmpty) { Cache().exercisePlanTemplates.forEach((element) { final ExercisePlanTemplate template = element as ExercisePlanTemplate; @@ -31,6 +36,7 @@ class TestSetEditBloc extends Bloc { template.exerciseTypes.forEach((id) { final ExerciseType exerciseType = Cache().getExerciseTypeById(id); _exerciseTypes.add(exerciseType); + _actualExerciseTypes.add(exerciseType); _exercisePlanDetails[exerciseType.exerciseTypeId] = exerciseType; }); } @@ -42,22 +48,36 @@ class TestSetEditBloc extends Bloc { Stream mapEventToState(TestSetEditEvent event) async* { try { if (event is TestSetEditChangeExerciseType) { + yield TestSetEditLoading(); final List alternatives = workoutTreeRepository.getExerciseTypeAlternatives(event.exerciseTypeId); final ExerciseType exerciseType = Cache().getExerciseTypeById(event.exerciseTypeId); - if (event.index > alternatives.length) { - /// skip - _exercisePlanDetails[exerciseType.exerciseTypeId] = null; - } else if (event.index == 0) { + + if (_exercisePlanDetails[event.exerciseTypeId] == null) { + /// it was skipped _exercisePlanDetails[exerciseType.exerciseTypeId] = exerciseType; } else { - final changedExerciseType = alternatives[event.index - 1]; - _exercisePlanDetails[exerciseType.exerciseTypeId] = changedExerciseType; + if (event.index == 0) { + _exercisePlanDetails[exerciseType.exerciseTypeId] = exerciseType; + } else { + final changedExerciseType = alternatives[event.index - 1]; + _exercisePlanDetails[exerciseType.exerciseTypeId] = changedExerciseType; + } } + + // to keep the slider accurate + refreshActualPlan(); + yield TestSetEditReady(); + } else if (event is TestSetEditDeleteExerciseType) { + yield TestSetEditLoading(); + final ExerciseType exerciseType = Cache().getExerciseTypeById(event.exerciseTypeId); + _exercisePlanDetails[exerciseType.exerciseTypeId] = null; + refreshActualPlan(); + yield TestSetEditReady(); } else if (event is TestSetEditSubmit) { yield TestSetEditLoading(); - ExercisePlan exercisePlan = ExercisePlan(templateName, Cache().userLoggedIn.customerId); + ExercisePlan exercisePlan = ExercisePlan(templateNameTranslation, Cache().userLoggedIn.customerId); exercisePlan.private = true; - exercisePlan.type = ExerciseAbility.mini_test.toString(); + exercisePlan.type = ExerciseAbility.mini_test_set.toString(); exercisePlan.dateAdd = DateTime.now(); ExercisePlan savedExercisePlan = await ExercisePlanApi().saveExercisePlan(exercisePlan); @@ -67,11 +87,15 @@ class TestSetEditBloc extends Bloc { ExercisePlanDetail exercisePlanDetail = ExercisePlanDetail(entry.value.exerciseTypeId); exercisePlanDetail.exercisePlanId = savedExercisePlan.exercisePlanId; exercisePlanDetail.serie = 1; + ExercisePlanDetail savedDetail = await ExercisePlanApi().saveExercisePlanDetail(exercisePlanDetail); - details.add(savedDetail); + exercisePlanDetail.exercisePlanDetailId = savedDetail.exercisePlanDetailId; + exercisePlanDetail.exercises = List(); + details.add(exercisePlanDetail); } } Cache().saveActiveExercisePlan(exercisePlan, details); + Track().track(TrackingEvent.test_set_edit, eventValue: templateName); yield TestSetEditSaved(); } } on Exception catch (e) { @@ -80,4 +104,14 @@ class TestSetEditBloc extends Bloc { } List get exerciseTypes => this._exerciseTypes; + List get actualExerciseTypes => this._actualExerciseTypes; + HashMap get exercisePlanDetails => this._exercisePlanDetails; + + void refreshActualPlan() { + _actualExerciseTypes.removeRange(0, _actualExerciseTypes.length - 1); + _exercisePlanDetails.keys.forEach((element) { + final ExerciseType exerciseType = Cache().getExerciseTypeById(element); + _actualExerciseTypes.add(exerciseType); + }); + } } diff --git a/lib/bloc/test_set_edit/test_set_edit_event.dart b/lib/bloc/test_set_edit/test_set_edit_event.dart index 2db990b..8b33f73 100644 --- a/lib/bloc/test_set_edit/test_set_edit_event.dart +++ b/lib/bloc/test_set_edit/test_set_edit_event.dart @@ -20,6 +20,14 @@ class TestSetEditChangeExerciseType extends TestSetEditEvent { List get props => [index, exerciseTypeId]; } +class TestSetEditDeleteExerciseType extends TestSetEditEvent { + final int exerciseTypeId; + const TestSetEditDeleteExerciseType({this.exerciseTypeId}); + + @override + List get props => [exerciseTypeId]; +} + class TestSetEditSkipExerciseType extends TestSetEditEvent { final int exerciseTypeId; const TestSetEditSkipExerciseType({this.exerciseTypeId}); diff --git a/lib/bloc/test_set_execute/test_set_execute_bloc.dart b/lib/bloc/test_set_execute/test_set_execute_bloc.dart new file mode 100644 index 0000000..7ff9a99 --- /dev/null +++ b/lib/bloc/test_set_execute/test_set_execute_bloc.dart @@ -0,0 +1,358 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/exercise.dart'; +import 'package:aitrainer_app/model/exercise_ability.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/model/workout_menu_tree.dart'; +import 'package:aitrainer_app/service/exercise_plan_service.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'test_set_execute_event.dart'; +part 'test_set_execute_state.dart'; + +class TestSetExecuteBloc extends Bloc { + // ignore: close_sinks + MenuBloc menuBloc; + int exerciseTypeId; + String testName; + String testType = ""; + bool miniTestSet = false; + bool paralellTest = false; + double scrollOffset = 0; + ExercisePlan exercisePlan; + bool isDone100 = false; + + List exercisePlanDetails; + + TestSetExecuteBloc() : super(TestSetExecuteInitial()); + + void setExerciseTypeId(int id) => exerciseTypeId = id; + + void initExercisePlan() { + exercisePlan = Cache().activeExercisePlan; + if (exercisePlan != null) { + testName = exercisePlan.name; + this.miniTestSet = exercisePlan.type != null && ExerciseAbility.mini_test_set.equalsStringTo(exercisePlan.type); + this.paralellTest = exercisePlan.type != null && ExerciseAbility.paralell_test.equalsStringTo(exercisePlan.type); + + testType = ExerciseAbility.mini_test_set.equalsStringTo(exercisePlan.type) + ? ExerciseAbility.mini_test_set.description + : ExerciseAbility.paralell_test.description; + print("exercisePlan: $testName type: $testType"); + } + exercisePlanDetails = Cache().activeExercisePlanDetails; + if (exercisePlanDetails != null) { + exercisePlanDetails.forEach((element) { + final ExerciseType exerciseType = Cache().getExerciseTypeById(element.exerciseTypeId); + if (exerciseType != null) { + element.exerciseType = exerciseType; + } + this.setPlanDetailState(element); + print("exercises of ${element.exerciseTypeId}: ${element.exercises}"); + }); + } + } + + @override + Stream mapEventToState( + TestSetExecuteEvent event, + ) async* { + try { + if (event is TestSetExecuteLoad) { + yield TestSetExecuteLoading(); + initExercisePlan(); + if (exerciseTypeId != null) { + int step = 0; + if (exercisePlanDetails != null) { + exercisePlanDetails.forEach((element) { + if (element.exerciseTypeId == this.exerciseTypeId) { + scrollOffset = (step * 85).toDouble() + 10; + } + step++; + }); + } + } + yield TestSetExecuteReady(); + } else if (event is TestSetExecuteDeleteActive) { + print("Delete: ${exercisePlan.type} paralellTest: $paralellTest"); + if (exercisePlan != null && ExerciseAbility.mini_test_set.equalsStringTo(exercisePlan.type)) { + exercisePlan = null; + if (exercisePlanDetails != null) { + exercisePlanDetails.removeRange(0, exercisePlanDetails.length - 1); + } + await Cache().deleteActiveExercisePlan(); + } + } else if (event is TestSetExecuteExerciseFinished) { + yield TestSetExecuteLoading(); + exercisePlanDetails.forEach((element) { + if (element.exerciseTypeId == event.exerciseTypeId) { + element.repeats = event.quantity.toInt(); + element.weightEquation = event.unitQuantity.toString(); + if (element.exercises == null) { + element.exercises = List(); + } + final Exercise exercise = Exercise(); + exercise.customerId = Cache().userLoggedIn.customerId; + exercise.exerciseTypeId = event.exerciseTypeId; + exercise.quantity = event.quantity; + exercise.unit = element.exerciseType.unit; + exercise.unitQuantity = event.unitQuantity; + exercise.dateAdd = DateTime.now(); + element.exercises.add(exercise); + setPlanDetailState(element); + } + }); + Cache().saveActiveExercisePlan(exercisePlan, exercisePlanDetails); + if (this.isDone100Percent()) { + add(TestSetExecuteFinish()); + } else { + yield TestSetExecuteReady(); + } + } else if (event is TestSetExecuteNewExercise) { + yield TestSetExecuteLoading(); + if (exercisePlan == null) { + exercisePlan = ExercisePlan(Cache().userLoggedIn.name + " Custom Test", Cache().userLoggedIn.customerId); + exercisePlan.private = true; + exercisePlan.dateAdd = DateTime.now(); + ExercisePlan savedExercisePlan = await ExercisePlanApi().saveExercisePlan(exercisePlan); + exercisePlan = savedExercisePlan; + exercisePlanDetails = List(); + } + exercisePlan.type = ExerciseAbility.paralell_test.enumToString(); + + if (!this.existsInPlanDetails(event.exerciseTypeId)) { + ExercisePlanDetail exercisePlanDetail = ExercisePlanDetail(event.exerciseTypeId); + exercisePlanDetail.exercisePlanId = exercisePlan.exercisePlanId; + final ExerciseType exerciseType = Cache().getExerciseTypeById(event.exerciseTypeId); + exercisePlanDetail.serie = exerciseType.unitQuantityUnit == null ? 1 : 4; + exercisePlanDetail.exerciseType = exerciseType; + exercisePlanDetail.exerciseTypeId = event.exerciseTypeId; + ExercisePlanDetail savedDetail = await ExercisePlanApi().saveExercisePlanDetail(exercisePlanDetail); + exercisePlanDetail.exercisePlanDetailId = savedDetail.exercisePlanDetailId; + exercisePlanDetail.state = ExercisePlanDetailState.start; + exercisePlanDetail.exercises = List(); + exercisePlanDetails.add(exercisePlanDetail); + await Cache().saveActiveExercisePlan(exercisePlan, exercisePlanDetails); + paralellTest = true; + } + yield TestSetExecuteReady(); + } else if (event is TestSetExecuteDeleteExercise) { + yield TestSetExecuteLoading(); + ExercisePlanDetail deleteDetail; + exercisePlanDetails.forEach((element) { + if (element.exerciseTypeId == event.exerciseTypeId) { + deleteDetail = element; + } + }); + if (deleteDetail != null) { + exercisePlanDetails.remove(deleteDetail); + if (exercisePlanDetails.isEmpty) { + exercisePlan = null; + exercisePlanDetails = null; + Cache().deleteActiveExercisePlan(); + } + } + yield TestSetExecuteReady(); + } else if (event is TestSetExecuteFinish) { + Cache().deleteActiveExercisePlan(); + isDone100 = isDone100Percent(); + // Animation + // Home + yield TestSetExecuteFinished(); + } + } on Exception catch (e) { + yield TestSetExecuteError(message: e.toString()); + } + } + + bool hasBegun() { + if (exercisePlanDetails == null || + exercisePlanDetails.isEmpty || + exercisePlanDetails[0].exercises == null || + exercisePlanDetails[0].exercises.length == 0) { + return false; + } + + return true; + } + + void setPlanDetailState(ExercisePlanDetail exercisePlanDetail) { + if (exercisePlanDetail.exercises == null || exercisePlanDetail.exercises.length == 0) { + exercisePlanDetail.state = ExercisePlanDetailState.start; + } else { + int maxLength = 1; + if (exercisePlanDetail.exerciseType.unitQuantityUnit != null) { + maxLength = 4; + } + if (exercisePlanDetail.exercises.length >= maxLength) { + exercisePlanDetail.state = ExercisePlanDetailState.finished; + } else { + exercisePlanDetail.state = ExercisePlanDetailState.inProgress; + } + } + } + + ExercisePlanDetailState actualState(int exerciseTypeId) { + ExercisePlanDetailState state = ExercisePlanDetailState.start; + exercisePlanDetails.forEach((element) { + if (element.exerciseTypeId == exerciseTypeId) { + state = element.state; + } + }); + return state; + } + + bool existsInPlanDetails(int exerciseTypeId) { + bool found = false; + exercisePlanDetails.forEach((element) { + if (element.exerciseTypeId == exerciseTypeId) { + found = true; + } + }); + return found; + } + + ExercisePlanDetail actualExercisePlanDetail(int exerciseTypeId) { + ExercisePlanDetail found; + exercisePlanDetails.forEach((element) { + if (element.exerciseTypeId == exerciseTypeId) { + found = element; + } + }); + return found; + } + + bool isDone100Percent() { + bool done = true; + if (exercisePlanDetails == null || exercisePlanDetails.isEmpty) { + return false; + } + exercisePlanDetails.forEach((element) { + if (!element.state.equalsTo(ExercisePlanDetailState.finished)) { + done = false; + } + }); + return done; + } + + HashMap canAddNewExercise() { + HashMap ret = HashMap(); + if (exercisePlan != null && ExerciseAbility.mini_test_set.equalsStringTo(exercisePlan.type)) { + final String message = "You have an active Test Set!"; + final String message2 = "Do you want you to override it?"; + ret['message'] = message; + ret['message2'] = message2; + ret['canAdd'] = false; + } else { + ret['canAdd'] = true; + } + + return ret; + } + + int getActualWorkoutTreeId(int exerciseTypeId) { + final WorkoutMenuTree workoutTree = this.menuBloc.menuTreeRepository.getMenuItemByExerciseTypeId(exerciseTypeId); + if (workoutTree == null) { + return null; + } + return workoutTree.id; + } + + String getActualImageName(int exerciseTypeId) { + if (exerciseTypeId <= 0) { + return ""; + } + final WorkoutMenuTree workoutTree = this.menuBloc.menuTreeRepository.getMenuItemByExerciseTypeId(exerciseTypeId); + if (workoutTree == null) { + return ""; + } + + return workoutTree.imageName; + } + + bool isFirst() { + if (exercisePlanDetails == null) { + return true; + } + if (exercisePlanDetails.isEmpty) { + return true; + } + return (exercisePlanDetails[0].exercises == null || exercisePlanDetails[0].exercises.length == 0); + } + + bool existsActivePlan() { + final bool exists = exercisePlan != null && exercisePlanDetails.length > 0; + print("Exists active plan: $exists"); + return exists; + } + + bool isDone(ExercisePlanDetail exercisePlanDetail) { + return (exercisePlanDetail.state.equalsTo(ExercisePlanDetailState.finished)); + } + + ExercisePlanDetail getNext() { + ExercisePlanDetail nextExercisePlanDetail; + int minStep = 99; + for (final detail in this.exercisePlanDetails) { + if (!detail.state.equalsTo(ExercisePlanDetailState.finished)) { + if (detail.exercises == null) { + nextExercisePlanDetail = detail; + minStep = 1; + break; + } else { + final int step = detail.exercises.length; + if (step < minStep) { + nextExercisePlanDetail = detail; + minStep = step; + } + } + } + } + return nextExercisePlanDetail; + } + + double calculate1RM(double quantity, double unitQuantity) { + double weight = unitQuantity; + double repeat = quantity; + if (weight == 0 || repeat == 0) { + return 0; + } + + double rmWendler = weight * repeat * 0.0333 + weight; + double rmOconner = weight * (1 + repeat / 40); + double average = (rmWendler + rmOconner) / 2; + + return average; + } + + String repeatTimesText(ExercisePlanDetail exercisePlanDetail) { + String text = "maximum"; + if (!hasBegun() || exercisePlanDetail.exerciseType.unitQuantityUnit == null) { + return text; + } + int step = exercisePlanDetail.exercises.length; + print("repeatTimes step $step"); + if (step == 2) { + text = "12"; + } + return text; + } + + String getExerciseWeight(ExercisePlanDetail exercisePlanDetail) { + String text = "you are able to do 12-20 repeats with"; + if (!hasBegun() || exercisePlanDetail.exercises.length < 2) { + return text; + } + final double unitQuantity = exercisePlanDetail.exercises.last.unitQuantity; + final double quantity = exercisePlanDetail.exercises.last.quantity; + double oneRepMax = this.calculate1RM(quantity, unitQuantity); + text = (oneRepMax * 0.75).round().toStringAsFixed(0) + " " + exercisePlanDetail.exerciseType.unitQuantityUnit; + return text; + } +} diff --git a/lib/bloc/test_set_execute/test_set_execute_event.dart b/lib/bloc/test_set_execute/test_set_execute_event.dart new file mode 100644 index 0000000..fc588c1 --- /dev/null +++ b/lib/bloc/test_set_execute/test_set_execute_event.dart @@ -0,0 +1,54 @@ +part of 'test_set_execute_bloc.dart'; + +abstract class TestSetExecuteEvent extends Equatable { + const TestSetExecuteEvent(); + + @override + List get props => []; +} + +class TestSetExecuteLoad extends TestSetExecuteEvent { + const TestSetExecuteLoad(); +} + +class TestSetExecuteExecute extends TestSetExecuteEvent { + final int exerciseTypeId; + const TestSetExecuteExecute({this.exerciseTypeId}); + + @override + List get props => [exerciseTypeId]; +} + +class TestSetExecuteFinish extends TestSetExecuteEvent { + const TestSetExecuteFinish(); +} + +class TestSetExecuteExerciseFinished extends TestSetExecuteEvent { + final int exerciseTypeId; + final double quantity; + final double unitQuantity; + const TestSetExecuteExerciseFinished({this.exerciseTypeId, this.quantity, this.unitQuantity}); + + @override + List get props => [exerciseTypeId, quantity, unitQuantity]; +} + +class TestSetExecuteNewExercise extends TestSetExecuteEvent { + final int exerciseTypeId; + const TestSetExecuteNewExercise({this.exerciseTypeId}); + + @override + List get props => [exerciseTypeId]; +} + +class TestSetExecuteDeleteExercise extends TestSetExecuteEvent { + final int exerciseTypeId; + const TestSetExecuteDeleteExercise({this.exerciseTypeId}); + + @override + List get props => [exerciseTypeId]; +} + +class TestSetExecuteDeleteActive extends TestSetExecuteEvent { + const TestSetExecuteDeleteActive(); +} diff --git a/lib/bloc/test_set_execute/test_set_execute_state.dart b/lib/bloc/test_set_execute/test_set_execute_state.dart new file mode 100644 index 0000000..fce639b --- /dev/null +++ b/lib/bloc/test_set_execute/test_set_execute_state.dart @@ -0,0 +1,32 @@ +part of 'test_set_execute_bloc.dart'; + +abstract class TestSetExecuteState extends Equatable { + const TestSetExecuteState(); + + @override + List get props => []; +} + +class TestSetExecuteInitial extends TestSetExecuteState { + const TestSetExecuteInitial(); +} + +class TestSetExecuteLoading extends TestSetExecuteState { + const TestSetExecuteLoading(); +} + +class TestSetExecuteFinished extends TestSetExecuteState { + const TestSetExecuteFinished(); +} + +class TestSetExecuteReady extends TestSetExecuteState { + const TestSetExecuteReady(); +} + +class TestSetExecuteError extends TestSetExecuteState { + final String message; + const TestSetExecuteError({this.message}); + + @override + List get props => [message]; +} diff --git a/lib/bloc/test_set_new/test_set_new_bloc.dart b/lib/bloc/test_set_new/test_set_new_bloc.dart new file mode 100644 index 0000000..f3bc07f --- /dev/null +++ b/lib/bloc/test_set_new/test_set_new_bloc.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/util/enums.dart'; +import 'package:aitrainer_app/util/track.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'test_set_new_event.dart'; +part 'test_set_new_state.dart'; + +class TestSetNewBloc extends Bloc { + final ExerciseRepository exerciseRepository; + final ExerciseType exerciseType; + final int exercisePlanDetailId; + final TestSetExecuteBloc executeBloc; + + TestSetNewBloc({this.exerciseRepository, this.exerciseType, this.exercisePlanDetailId, this.executeBloc}) : super(TestSetNewInitial()) { + exerciseRepository.exerciseType = exerciseType; + quantity = 12; + unitQuantity = 30; + exerciseRepository.setQuantity(quantity); + exerciseRepository.setUnit(exerciseType.unit); + exerciseRepository.setUnitQuantity(unitQuantity); + exerciseRepository.exercise.exercisePlanDetailId = exercisePlanDetailId; + exerciseRepository.start = DateTime.now(); + exerciseRepository.setCustomer(Cache().userLoggedIn); + } + + double quantity; + double unitQuantity; + + @override + Stream mapEventToState( + TestSetNewEvent event, + ) async* { + try { + if (event is TestSetNewChangeQuantity) { + quantity = event.quantity; + exerciseRepository.setQuantity(quantity); + } else if (event is TestSetNewChangeQuantityUnit) { + unitQuantity = event.quantity; + exerciseRepository.setUnitQuantity(unitQuantity); + } else if (event is TestSetNewSubmit) { + yield TestSetNewLoading(); + exerciseRepository.end = DateTime.now(); + await exerciseRepository.addExercise(); + executeBloc.add( + TestSetExecuteExerciseFinished(exerciseTypeId: exerciseType.exerciseTypeId, quantity: quantity, unitQuantity: unitQuantity)); + Track().track(TrackingEvent.test_set_new, eventValue: exerciseType.name); + } + } on Exception catch (e) { + yield TestSetNewError(message: e.toString()); + } + } +} diff --git a/lib/bloc/test_set_new/test_set_new_event.dart b/lib/bloc/test_set_new/test_set_new_event.dart new file mode 100644 index 0000000..4390db6 --- /dev/null +++ b/lib/bloc/test_set_new/test_set_new_event.dart @@ -0,0 +1,32 @@ +part of 'test_set_new_bloc.dart'; + +abstract class TestSetNewEvent extends Equatable { + const TestSetNewEvent(); + + @override + List get props => []; +} + +class TestSetNewLoad extends TestSetNewEvent { + const TestSetNewLoad(); +} + +class TestSetNewChangeQuantity extends TestSetNewEvent { + final double quantity; + const TestSetNewChangeQuantity({this.quantity}); + + @override + List get props => [quantity]; +} + +class TestSetNewChangeQuantityUnit extends TestSetNewEvent { + final double quantity; + const TestSetNewChangeQuantityUnit({this.quantity}); + + @override + List get props => [quantity]; +} + +class TestSetNewSubmit extends TestSetNewEvent { + const TestSetNewSubmit(); +} diff --git a/lib/bloc/test_set_new/test_set_new_state.dart b/lib/bloc/test_set_new/test_set_new_state.dart new file mode 100644 index 0000000..ac0998b --- /dev/null +++ b/lib/bloc/test_set_new/test_set_new_state.dart @@ -0,0 +1,28 @@ +part of 'test_set_new_bloc.dart'; + +abstract class TestSetNewState extends Equatable { + const TestSetNewState(); + + @override + List get props => []; +} + +class TestSetNewInitial extends TestSetNewState { + const TestSetNewInitial(); +} + +class TestSetNewLoading extends TestSetNewState { + const TestSetNewLoading(); +} + +class TestSetNewReady extends TestSetNewState { + const TestSetNewReady(); +} + +class TestSetNewError extends TestSetNewState { + final String message; + const TestSetNewError({this.message}); + + @override + List get props => [message]; +} diff --git a/lib/library/dropdown_search.dart b/lib/library/dropdown_search.dart new file mode 100644 index 0000000..b15de21 --- /dev/null +++ b/lib/library/dropdown_search.dart @@ -0,0 +1,529 @@ +library dropdown_search; + +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import 'popup_menu.dart'; +import 'select_dialog.dart'; + +typedef Future> DropdownSearchOnFind(String text); +typedef String DropdownSearchItemAsString(T item); +typedef bool DropdownSearchFilterFn(T item, String filter); +typedef bool DropdownSearchCompareFn(T item, T selectedItem); +typedef Widget DropdownSearchBuilder(BuildContext context, T selectedItem, String itemAsString); +typedef Widget DropdownSearchPopupItemBuilder( + BuildContext context, + T item, + bool isSelected, +); +typedef bool DropdownSearchPopupItemEnabled(T item); +typedef Widget ErrorBuilder(BuildContext context, String searchEntry, dynamic exception); +typedef Widget EmptyBuilder(BuildContext context, String searchEntry); +typedef Widget LoadingBuilder(BuildContext context, String searchEntry); +typedef Widget IconButtonBuilder(BuildContext context); +typedef Future BeforeChange(T prevItem, T nextItem); + +enum Mode { DIALOG, BOTTOM_SHEET, MENU } + +class DropdownSearch extends StatefulWidget { + ///DropDownSearch label + final String label; + + ///DropDownSearch hint + final String hint; + + ///show/hide the search box + final bool showSearchBox; + + ///true if the filter on items is applied onlie (via API) + final bool isFilteredOnline; + + ///show/hide clear selected item + final bool showClearButton; + + ///offline items list + final List items; + + ///selected item + final T selectedItem; + + ///function that returns item from API + final DropdownSearchOnFind onFind; + + ///called when a new item is selected + final ValueChanged onChanged; + + ///to customize list of items UI + final DropdownSearchBuilder dropdownBuilder; + + ///to customize selected item + final DropdownSearchPopupItemBuilder popupItemBuilder; + + ///decoration for search box + final InputDecoration searchBoxDecoration; + + ///the title for dialog/menu/bottomSheet + final Color popupBackgroundColor; + + ///custom widget for the popup title + final Widget popupTitle; + + ///customize the fields the be shown + final DropdownSearchItemAsString itemAsString; + + /// custom filter function + final DropdownSearchFilterFn filterFn; + + ///enable/disable dropdownSearch + final bool enabled; + + ///MENU / DIALOG/ BOTTOM_SHEET + final Mode mode; + + ///the max height for dialog/bottomSheet/Menu + final double maxHeight; + + ///the max width for the dialog + final double dialogMaxWidth; + + ///select the selected item in the menu/dialog/bottomSheet of items + final bool showSelectedItem; + + ///function that compares two object with the same type to detected if it's the selected item or not + final DropdownSearchCompareFn compareFn; + + ///dropdownSearch input decoration + final InputDecoration dropdownSearchDecoration; + + ///custom layout for empty results + final EmptyBuilder emptyBuilder; + + ///custom layout for loading items + final LoadingBuilder loadingBuilder; + + ///custom layout for error + final ErrorBuilder errorBuilder; + + ///the search box will be focused if true + final bool autoFocusSearchBox; + + ///custom shape for the popup + final ShapeBorder popupShape; + + final AutovalidateMode autoValidateMode; + + /// An optional method to call with the final value when the form is saved via + final FormFieldSetter onSaved; + + /// An optional method that validates an input. Returns an error string to + /// display if the input is invalid, or null otherwise. + final FormFieldValidator validator; + + ///custom dropdown clear button icon widget + final Widget clearButton; + + ///custom clear button widget builder + final IconButtonBuilder clearButtonBuilder; + + ///custom dropdown icon button widget + final Widget dropDownButton; + + ///custom dropdown button widget builder + final IconButtonBuilder dropdownButtonBuilder; + + ///whether to manage the clear and dropdown icons via InputDecoration suffixIcon + final bool showAsSuffixIcons; + + ///If true, the dropdownBuilder will continue the uses of material behavior + ///This will be useful if you want to handle a custom UI only if the item !=null + final bool dropdownBuilderSupportsNullItem; + + ///defines if an item of the popup is enabled or not, if the item is disabled, + ///it cannot be clicked + final DropdownSearchPopupItemEnabled popupItemDisabled; + + ///set a custom color for the popup barrier + final Color popupBarrierColor; + + ///text controller to set default search word for example + final TextEditingController searchBoxController; + + ///called when popup is dismissed + final VoidCallback onPopupDismissed; + + /// callback executed before applying value change + ///delay before searching, change it to Duration(milliseconds: 0) + ///if you do not use online search + final Duration searchDelay; + + /// callback executed before applying value change + final BeforeChange onBeforeChange; + + DropdownSearch({ + Key key, + this.onSaved, + this.validator, + this.autoValidateMode = AutovalidateMode.disabled, + this.onChanged, + this.mode = Mode.DIALOG, + this.label, + this.hint, + this.isFilteredOnline = false, + this.popupTitle, + this.items, + this.selectedItem, + this.onFind, + this.dropdownBuilder, + this.popupItemBuilder, + this.showSearchBox = false, + this.showClearButton = false, + this.searchBoxDecoration, + this.popupBackgroundColor, + this.enabled = true, + this.maxHeight, + this.filterFn, + this.itemAsString, + this.showSelectedItem = false, + this.compareFn, + this.dropdownSearchDecoration, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, + this.autoFocusSearchBox = false, + this.dialogMaxWidth, + this.clearButton, + this.clearButtonBuilder, + this.dropDownButton, + this.dropdownButtonBuilder, + this.showAsSuffixIcons = false, + this.dropdownBuilderSupportsNullItem = false, + this.popupShape, + this.popupItemDisabled, + this.popupBarrierColor, + this.onPopupDismissed, + this.searchBoxController, + this.searchDelay, + this.onBeforeChange, + }) : assert(isFilteredOnline != null), + assert(dropdownBuilderSupportsNullItem != null), + assert(enabled != null), + assert(showSelectedItem != null), + assert(autoFocusSearchBox != null), + assert(showClearButton != null), + assert(showSearchBox != null), + assert(!showSelectedItem || T == String || compareFn != null), + super(key: key); + + @override + DropdownSearchState createState() => DropdownSearchState(); +} + +class DropdownSearchState extends State> { + final ValueNotifier _selectedItemNotifier = ValueNotifier(null); + final ValueNotifier _isFocused = ValueNotifier(false); + + @override + void initState() { + super.initState(); + _selectedItemNotifier.value = widget.selectedItem; + } + + @override + void didUpdateWidget(DropdownSearch oldWidget) { + final oldSelectedItem = oldWidget.selectedItem; + final newSelectedItem = widget.selectedItem; + if (oldSelectedItem != newSelectedItem) { + _selectedItemNotifier.value = newSelectedItem; + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _selectedItemNotifier, + builder: (context, T data, wt) { + return IgnorePointer( + ignoring: !widget.enabled, + child: GestureDetector( + onTap: () => _selectSearchMode(data), + child: _formField(data), + ), + ); + }, + ); + } + + Widget _defaultSelectItemWidget(T data) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: widget.dropdownBuilder != null + ? widget.dropdownBuilder( + context, + data, + _selectedItemAsString(data), + ) + : Text(_selectedItemAsString(data), style: Theme.of(context).textTheme.subtitle1), + ), + if (!widget.showAsSuffixIcons) _manageTrailingIcons(data), + ], + ); + } + + Widget _formField(T value) { + return FormField( + enabled: widget.enabled, + onSaved: widget.onSaved, + validator: widget.validator, + autovalidateMode: widget.autoValidateMode, + initialValue: widget.selectedItem, + builder: (FormFieldState state) { + if (state.value != value) { + WidgetsBinding.instance.addPostFrameCallback((_) { + state.didChange(value); + }); + } + return ValueListenableBuilder( + valueListenable: _isFocused, + builder: (context, bool isFocused, w) { + return InputDecorator( + isEmpty: value == null && (widget.dropdownBuilder == null || widget.dropdownBuilderSupportsNullItem), + isFocused: isFocused, + decoration: _manageDropdownDecoration(state, value), + child: _defaultSelectItemWidget(value), + ); + }); + }, + ); + } + + ///manage dropdownSearch field decoration + InputDecoration _manageDropdownDecoration(FormFieldState state, T data) { + return (widget.dropdownSearchDecoration ?? + InputDecoration(contentPadding: EdgeInsets.fromLTRB(12, 12, 0, 0), border: OutlineInputBorder())) + .applyDefaults(Theme.of(state.context).inputDecorationTheme) + .copyWith( + enabled: widget.enabled, + labelText: widget.label, + hintText: widget.hint, + suffixIcon: widget.showAsSuffixIcons ? _manageTrailingIcons(data) : null, + errorText: state.errorText); + } + + ///function that return the String value of an object + String _selectedItemAsString(T data) { + if (data == null) { + return ""; + } else if (widget.itemAsString != null) { + return widget.itemAsString(data); + } else { + return data.toString(); + } + } + + ///function that manage Trailing icons(close, dropDown) + Widget _manageTrailingIcons(T data) { + final clearButtonPressed = () => _handleOnChangeSelectedItem(null); + final dropdownButtonPressed = () => _selectSearchMode(data); + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (data != null && widget.showClearButton) + widget.clearButtonBuilder != null + ? GestureDetector( + onTap: clearButtonPressed, + child: widget.clearButtonBuilder(context), + ) + : IconButton( + icon: widget.clearButton ?? const Icon(Icons.clear, size: 24), + onPressed: clearButtonPressed, + ), + widget.dropdownButtonBuilder != null + ? GestureDetector( + onTap: dropdownButtonPressed, + child: widget.dropdownButtonBuilder(context), + ) + : IconButton( + icon: widget.dropDownButton ?? const Icon(Icons.arrow_drop_down, size: 24), + onPressed: dropdownButtonPressed, + ), + ], + ); + } + + ///open dialog + Future _openSelectDialog(T data) { + return showGeneralDialog( + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + transitionDuration: const Duration(milliseconds: 400), + barrierColor: widget.popupBarrierColor ?? const Color(0x80000000), + context: context, + pageBuilder: (context, animation, secondaryAnimation) { + return AlertDialog( + contentPadding: EdgeInsets.all(0), + shape: widget.popupShape, + backgroundColor: widget.popupBackgroundColor, + content: _selectDialogInstance(data), + ); + }, + ); + } + + ///open BottomSheet (Dialog mode) + Future _openBottomSheet(T data) { + return showModalBottomSheet( + barrierColor: widget.popupBarrierColor, + isScrollControlled: true, + backgroundColor: widget.popupBackgroundColor, + shape: widget.popupShape, + context: context, + builder: (context) { + return AnimatedPadding( + duration: Duration(milliseconds: 300), + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: _selectDialogInstance(data, defaultHeight: 350), + ); + }); + } + + ///openMenu + Future _openMenu(T data) { + // Here we get the render object of our physical button, later to get its size & position + final RenderBox popupButtonObject = context.findRenderObject(); + // Get the render object of the overlay used in `Navigator` / `MaterialApp`, i.e. screen size reference + final RenderBox overlay = Overlay.of(context).context.findRenderObject(); + // Calculate the show-up area for the dropdown using button's size & position based on the `overlay` used as the coordinate space. + final RelativeRect position = RelativeRect.fromSize( + Rect.fromPoints( + popupButtonObject.localToGlobal(popupButtonObject.size.bottomLeft(Offset.zero), ancestor: overlay), + popupButtonObject.localToGlobal(popupButtonObject.size.bottomRight(Offset.zero), ancestor: overlay), + ), + Size(overlay.size.width, overlay.size.height), + ); + return customShowMenu( + barrierColor: widget.popupBarrierColor, + shape: widget.popupShape, + color: widget.popupBackgroundColor, + context: context, + position: position, + elevation: 8, + items: [ + CustomPopupMenuItem( + enabled: false, + child: Container( + width: popupButtonObject.size.width, + child: _selectDialogInstance(data, defaultHeight: 224), + ), + ), + ]); + } + + SelectDialog _selectDialogInstance(T data, {double defaultHeight}) { + return SelectDialog( + popupTitle: widget.popupTitle, + maxHeight: widget.maxHeight ?? defaultHeight, + isFilteredOnline: widget.isFilteredOnline, + itemAsString: widget.itemAsString, + filterFn: widget.filterFn, + items: widget.items, + onFind: widget.onFind, + showSearchBox: widget.showSearchBox, + itemBuilder: widget.popupItemBuilder, + selectedValue: data, + searchBoxDecoration: widget.searchBoxDecoration, + onChanged: _handleOnChangeSelectedItem, + showSelectedItem: widget.showSelectedItem, + compareFn: widget.compareFn, + emptyBuilder: widget.emptyBuilder, + loadingBuilder: widget.loadingBuilder, + errorBuilder: widget.errorBuilder, + autoFocusSearchBox: widget.autoFocusSearchBox, + dialogMaxWidth: widget.dialogMaxWidth, + itemDisabled: widget.popupItemDisabled, + searchBoxController: widget.searchBoxController ?? TextEditingController(), + searchDelay: widget.searchDelay, + ); + } + + ///Function that manage focus listener + ///set true only if the widget already not focused to prevent unnecessary build + ///same thing for clear focus, + void _handleFocus(bool isFocused) { + if (isFocused && !_isFocused.value) { + FocusScope.of(context).unfocus(); + _isFocused.value = true; + } else if (!isFocused && _isFocused.value) _isFocused.value = false; + } + + ///handle on change value , if the validation is active , we validate the new selected item + void _handleOnChangeSelectedItem(T selectedItem) { + final changeItem = () { + _selectedItemNotifier.value = selectedItem; + if (widget.onChanged != null) widget.onChanged(selectedItem); + }; + + if (widget.onBeforeChange != null) { + widget.onBeforeChange(_selectedItemNotifier.value, selectedItem).then((value) { + if (value == true) { + changeItem(); + } + }); + } else { + changeItem(); + } + + _handleFocus(false); + } + + ///Function that return then UI based on searchMode + ///[data] selected item to be passed to the UI + ///If we close the popup , or maybe we just selected + ///another widget we should clear the focus + Future _selectSearchMode(T data) async { + _handleFocus(true); + T selectedItem; + if (widget.mode == Mode.MENU) { + selectedItem = await _openMenu(data); + } else if (widget.mode == Mode.BOTTOM_SHEET) { + selectedItem = await _openBottomSheet(data); + } else { + selectedItem = await _openSelectDialog(data); + } + _handleFocus(false); + widget.onPopupDismissed?.call(); + + return selectedItem; + } + + ///Public Function that return then UI based on searchMode + ///[data] selected item to be passed to the UI + ///If we close the popup , or maybe we just selected + ///another widget we should clear the focus + ///THIS USED FOR OPEN DROPDOWN_SEARCH PROGRAMMATICALLY, + ///otherwise you can you [_selectSearchMode] + Future openDropDownSearch() => _selectSearchMode(_selectedItemNotifier.value); + + ///Change selected Value; this function is public USED to change the selected + ///value PROGRAMMATICALLY, Otherwise you can use [_handleOnChangeSelectedItem] + void changeSelectedItem(T selectedItem) => _handleOnChangeSelectedItem(selectedItem); + + ///Change selected Value; this function is public USED to clear selected + ///value PROGRAMMATICALLY, Otherwise you can use [_handleOnChangeSelectedItem] + void clear() => _handleOnChangeSelectedItem(null); + + ///get selected value programmatically + T get getSelectedItem => _selectedItemNotifier.value; + + ///check if the dropdownSearch is focused + bool get isFocused => _isFocused.value; +} diff --git a/lib/library/gradient_bottom_navigation_bar.dart b/lib/library/gradient_bottom_navigation_bar.dart new file mode 100644 index 0000000..f069acd --- /dev/null +++ b/lib/library/gradient_bottom_navigation_bar.dart @@ -0,0 +1,703 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library gradient_bottom_navigation_bar; + +import 'dart:collection' show Queue; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:vector_math/vector_math_64.dart' show Vector3; + +const double _kActiveFontSize = 14.0; +const double _kInactiveFontSize = 12.0; +const double _kTopMargin = 6.0; +const double _kBottomMargin = 8.0; + +/// A material widget displayed at the bottom of an app for selecting among a +/// small number of views, typically between three and five. +/// +/// The bottom navigation bar consists of multiple items in the form of +/// text labels, icons, or both, laid out on top of a piece of material. It +/// provides quick navigation between the top-level views of an app. For larger +/// screens, side navigation may be a better fit. +/// +/// A bottom navigation bar is usually used in conjunction with a [Scaffold], +/// where it is provided as the [Scaffold.bottomNavigationBar] argument. +/// +/// The bottom navigation bar's [type] changes how its [items] are displayed. +/// If not specified it's automatically set to [BottomNavigationBarType.fixed] +/// when there are less than four items, [BottomNavigationBarType.shifting] +/// otherwise. +/// +/// * [BottomNavigationBarType.fixed], the default when there are less than +/// four [items]. The selected item is rendered with [fixedColor] if it's +/// non-null, otherwise the theme's [ThemeData.primaryColor] is used. The +/// navigation bar's background color is the default [Material] background +/// color, [ThemeData.canvasColor] (essentially opaque white). +/// * [BottomNavigationBarType.shifting], the default when there are four +/// or more [items]. All items are rendered in white and the navigation bar's +/// background color is the same as the +/// [BottomNavigationBarItem.backgroundColor] of the selected item. In this +/// case it's assumed that each item will have a different background color +/// and that background color will contrast well with white. +/// +/// ## Sample Code +/// +/// This example shows a [GradientBottomNavigationBar] as it is used within a [Scaffold] +/// widget. The [GradientBottomNavigationBar] has three [BottomNavigationBarItem] +/// widgets and the [currentIndex] is set to index 1. The color of the selected +/// item is set to a purple color. A function is called whenever any item is +/// tapped and the function helps display the appropriate [Text] in the body of +/// the [Scaffold]. +/// +/// ```dart +/// class MyHomePage extends StatefulWidget { +/// MyHomePage({Key key}) : super(key: key); +/// +/// @override +/// _MyHomePageState createState() => _MyHomePageState(); +/// } +/// +/// class _MyHomePageState extends State { +/// int _selectedIndex = 1; +/// final _widgetOptions = [ +/// Text('Index 0: Home'), +/// Text('Index 1: Business'), +/// Text('Index 2: School'), +/// ]; +/// +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// appBar: AppBar( +/// title: Text('BottomNavigationBar Sample'), +/// ), +/// body: Center( +/// child: _widgetOptions.elementAt(_selectedIndex), +/// ), +/// bottomNavigationBar: BottomNavigationBar( +/// items: [ +/// BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')), +/// BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')), +/// BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')), +/// ], +/// currentIndex: _selectedIndex, +/// fixedColor: Colors.deepPurple, +/// onTap: _onItemTapped, +/// ), +/// ); +/// } +/// +/// void _onItemTapped(int index) { +/// setState(() { +/// _selectedIndex = index; +/// }); +/// } +/// } +/// ``` +/// +/// See also: +/// +/// * [BottomNavigationBarItem] +/// * [Scaffold] +/// * +class GradientBottomNavigationBar extends StatefulWidget { + /// Creates a bottom navigation bar, typically used in a [Scaffold] where it + /// is provided as the [Scaffold.bottomNavigationBar] argument. + /// + /// The length of [items] must be at least two and each item's icon and title must be not null. + /// + /// It is required to specify a color for both [backgroundColorStart} and [backgroundColorEnd]. + /// + /// If [type] is null then [BottomNavigationBarType.fixed] is used when there + /// are two or three [items], [BottomNavigationBarType.shifting] otherwise. + /// + /// If [fixedColor] is null then the theme's primary color, + /// [ThemeData.primaryColor], is used. However if [GradientBottomNavigationBar.type] is + /// [BottomNavigationBarType.shifting] then [fixedColor] is ignored. + GradientBottomNavigationBar({ + Key key, + @required this.items, + this.onTap, + @required this.backgroundColorStart, + @required this.backgroundColorEnd, + this.currentIndex = 0, + BottomNavigationBarType type, + this.fixedColor, + this.iconSize = 24.0, + }) : assert(items != null), + assert(items.length >= 2), + assert(backgroundColorStart != null), + assert(backgroundColorEnd != null), + assert(0 <= currentIndex && currentIndex < items.length), + assert(iconSize != null), + type = type ?? (items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting), + super(key: key); + + /// The interactive items laid out within the bottom navigation bar where each item has an icon and title. + final List items; + + /// The callback that is called when a item is tapped. + /// + /// The widget creating the bottom navigation bar needs to keep track of the + /// current index and call `setState` to rebuild it with the newly provided + /// index. + final ValueChanged onTap; + + /// The index into [items] of the current active item. + final int currentIndex; + + /// Defines the layout and behavior of a [GradientBottomNavigationBar]. + /// + /// See documentation for [BottomNavigationBarType] for information on the meaning + /// of different types. + final BottomNavigationBarType type; + + /// Defines the start color shown in the [LinearGradient] + final Color backgroundColorStart; + + /// Defines the ending color shown in the [LinearGradient] + final Color backgroundColorEnd; + + /// The color of the selected item when bottom navigation bar is + /// [BottomNavigationBarType.fixed]. + /// + /// + /// If [fixedColor] is null then the theme's primary color, + /// [ThemeData.primaryColor], is used. However if [GradientBottomNavigationBar.type] is + /// [BottomNavigationBarType.shifting] then [fixedColor] is ignored. + final Color fixedColor; + + /// The size of all of the [BottomNavigationBarItem] icons. + /// + /// See [BottomNavigationBarItem.icon] for more information. + final double iconSize; + + @override + _GradientBottomNavigationBarState createState() => _GradientBottomNavigationBarState(); +} + +// This represents a single tile in the bottom navigation bar. It is intended +// to go into a flex container. +class _BottomNavigationTile extends StatelessWidget { + const _BottomNavigationTile( + this.type, + this.item, + this.animation, + this.iconSize, { + this.onTap, + this.colorTween, + this.flex, + this.selected = false, + this.indexLabel, + }) : assert(selected != null); + + final BottomNavigationBarType type; + final BottomNavigationBarItem item; + final Animation animation; + final double iconSize; + final VoidCallback onTap; + final ColorTween colorTween; + final double flex; + final bool selected; + final String indexLabel; + + Widget _buildIcon() { + double tweenStart; + Color iconColor; + switch (type) { + case BottomNavigationBarType.fixed: + tweenStart = 8.0; + iconColor = colorTween.evaluate(animation); + break; + case BottomNavigationBarType.shifting: + tweenStart = 16.0; + iconColor = Colors.white; + break; + } + return Align( + alignment: Alignment.topCenter, + heightFactor: 1.0, + child: Container( + margin: EdgeInsets.only( + top: Tween( + begin: tweenStart, + end: _kTopMargin, + ).evaluate(animation), + ), + child: IconTheme( + data: IconThemeData( + color: iconColor, + size: iconSize, + ), + child: selected ? item.activeIcon : item.icon, + ), + ), + ); + } + + Widget _buildFixedLabel() { + return Align( + alignment: Alignment.bottomCenter, + heightFactor: 1.0, + child: Container( + margin: const EdgeInsets.only(bottom: _kBottomMargin), + child: DefaultTextStyle.merge( + style: TextStyle( + fontSize: _kActiveFontSize, + color: colorTween.evaluate(animation), + ), + // The font size should grow here when active, but because of the way + // font rendering works, it doesn't grow smoothly if we just animate + // the font size, so we use a transform instead. + child: Transform( + transform: Matrix4.diagonal3( + Vector3.all( + Tween( + begin: _kInactiveFontSize / _kActiveFontSize, + end: 1.0, + ).evaluate(animation), + ), + ), + alignment: Alignment.bottomCenter, + child: item.title, + ), + ), + ), + ); + } + + Widget _buildShiftingLabel() { + return Align( + alignment: Alignment.bottomCenter, + heightFactor: 1.0, + child: Container( + margin: EdgeInsets.only( + bottom: Tween( + // In the spec, they just remove the label for inactive items and + // specify a 16dp bottom margin. We don't want to actually remove + // the label because we want to fade it in and out, so this modifies + // the bottom margin to take that into account. + begin: 2.0, + end: _kBottomMargin, + ).evaluate(animation), + ), + child: FadeTransition( + alwaysIncludeSemantics: true, + opacity: animation, + child: DefaultTextStyle.merge( + style: const TextStyle( + fontSize: _kActiveFontSize, + color: Colors.white, + ), + child: item.title, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + // In order to use the flex container to grow the tile during animation, we + // need to divide the changes in flex allotment into smaller pieces to + // produce smooth animation. We do this by multiplying the flex value + // (which is an integer) by a large number. + int size; + Widget label; + switch (type) { + case BottomNavigationBarType.fixed: + size = 1; + label = _buildFixedLabel(); + break; + case BottomNavigationBarType.shifting: + size = (flex * 1000.0).round(); + label = _buildShiftingLabel(); + break; + } + return Expanded( + flex: size, + child: Semantics( + container: true, + header: true, + selected: selected, + child: Stack( + children: [ + InkResponse( + onTap: onTap, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + _buildIcon(), + label, + ], + ), + ), + Semantics( + label: indexLabel, + ) + ], + ), + ), + ); + } +} + +class _GradientBottomNavigationBarState extends State with TickerProviderStateMixin { + List _controllers = []; + List _animations; + + // A queue of color splashes currently being animated. + final Queue<_Circle> _circles = Queue<_Circle>(); + + // Last splash circle's color, and the final color of the control after + // animation is complete. + Color _backgroundColor; + + static final Animatable _flexTween = Tween(begin: 1.0, end: 1.5); + + void _resetState() { + for (AnimationController controller in _controllers) controller.dispose(); + for (_Circle circle in _circles) circle.dispose(); + _circles.clear(); + + _controllers = List.generate(widget.items.length, (int index) { + return AnimationController( + duration: kThemeAnimationDuration, + vsync: this, + )..addListener(_rebuild); + }); + _animations = List.generate(widget.items.length, (int index) { + return CurvedAnimation( + parent: _controllers[index], + curve: Curves.fastOutSlowIn, + reverseCurve: Curves.fastOutSlowIn.flipped, + ); + }); + _controllers[widget.currentIndex].value = 1.0; + _backgroundColor = widget.items[widget.currentIndex].backgroundColor; + } + + @override + void initState() { + super.initState(); + _resetState(); + } + + void _rebuild() { + setState(() { + // Rebuilding when any of the controllers tick, i.e. when the items are + // animated. + }); + } + + @override + void dispose() { + for (AnimationController controller in _controllers) controller.dispose(); + for (_Circle circle in _circles) circle.dispose(); + super.dispose(); + } + + double _evaluateFlex(Animation animation) => _flexTween.evaluate(animation); + + void _pushCircle(int index) { + if (widget.items[index].backgroundColor != null) { + _circles.add( + _Circle( + state: this, + index: index, + color: widget.items[index].backgroundColor, + vsync: this, + )..controller.addStatusListener( + (AnimationStatus status) { + switch (status) { + case AnimationStatus.completed: + setState(() { + final _Circle circle = _circles.removeFirst(); + _backgroundColor = circle.color; + circle.dispose(); + }); + break; + case AnimationStatus.dismissed: + case AnimationStatus.forward: + case AnimationStatus.reverse: + break; + } + }, + ), + ); + } + } + + @override + void didUpdateWidget(GradientBottomNavigationBar oldWidget) { + super.didUpdateWidget(oldWidget); + + // No animated segue if the length of the items list changes. + if (widget.items.length != oldWidget.items.length) { + _resetState(); + return; + } + + if (widget.currentIndex != oldWidget.currentIndex) { + switch (widget.type) { + case BottomNavigationBarType.fixed: + break; + case BottomNavigationBarType.shifting: + _pushCircle(widget.currentIndex); + break; + } + _controllers[oldWidget.currentIndex].reverse(); + _controllers[widget.currentIndex].forward(); + } else { + if (_backgroundColor != widget.items[widget.currentIndex].backgroundColor) + _backgroundColor = widget.items[widget.currentIndex].backgroundColor; + } + } + + List _createTiles() { + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + assert(localizations != null); + final List children = []; + switch (widget.type) { + case BottomNavigationBarType.fixed: + final ThemeData themeData = Theme.of(context); + final TextTheme textTheme = themeData.textTheme; + Color themeColor; + switch (themeData.brightness) { + case Brightness.light: + themeColor = themeData.primaryColor; + break; + case Brightness.dark: + themeColor = themeData.accentColor; + break; + } + final ColorTween colorTween = ColorTween( + begin: textTheme.caption.color, + end: widget.fixedColor ?? themeColor, + ); + for (int i = 0; i < widget.items.length; i += 1) { + children.add( + _BottomNavigationTile( + widget.type, + widget.items[i], + _animations[i], + widget.iconSize, + onTap: () { + if (widget.onTap != null) widget.onTap(i); + }, + colorTween: colorTween, + selected: i == widget.currentIndex, + indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length), + ), + ); + } + break; + case BottomNavigationBarType.shifting: + for (int i = 0; i < widget.items.length; i += 1) { + children.add( + _BottomNavigationTile( + widget.type, + widget.items[i], + _animations[i], + widget.iconSize, + onTap: () { + if (widget.onTap != null) widget.onTap(i); + }, + flex: _evaluateFlex(_animations[i]), + selected: i == widget.currentIndex, + indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length), + ), + ); + } + break; + } + return children; + } + + Widget _createContainer(List tiles) { + return DefaultTextStyle.merge( + overflow: TextOverflow.ellipsis, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: tiles, + ), + ); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasDirectionality(context)); + assert(debugCheckHasMaterialLocalizations(context)); + + // Labels apply up to _bottomMargin padding. Remainder is media padding. + final double additionalBottomPadding = math.max(MediaQuery.of(context).padding.bottom - _kBottomMargin, 0.0); + + return Semantics( + container: true, + explicitChildNodes: true, + child: Stack( + children: [ + Positioned.fill( + child: Material( + // Casts shadow. + elevation: 8.0, + color: Color(0x00000000), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + widget.backgroundColorStart, + widget.backgroundColorEnd, + ], + begin: FractionalOffset(0.0, 0.0), + end: FractionalOffset(1.0, 0.0), + stops: [0.0, 1.0], + tileMode: TileMode.clamp), + ), + ), + ), + ), + ConstrainedBox( + constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding), + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint( + painter: _RadialPainter( + circles: _circles.toList(), + textDirection: Directionality.of(context), + ), + ), + ), + Material( + // Splashes. + type: MaterialType.transparency, + child: Padding( + padding: EdgeInsets.only(bottom: additionalBottomPadding), + child: MediaQuery.removePadding( + context: context, + removeBottom: true, + child: _createContainer(_createTiles()), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// Describes an animating color splash circle. +class _Circle { + _Circle({ + @required this.state, + @required this.index, + @required this.color, + @required TickerProvider vsync, + }) : assert(state != null), + assert(index != null), + assert(color != null) { + controller = AnimationController( + duration: kThemeAnimationDuration, + vsync: vsync, + ); + animation = CurvedAnimation( + parent: controller, + curve: Curves.fastOutSlowIn, + ); + controller.forward(); + } + + final _GradientBottomNavigationBarState state; + final int index; + final Color color; + AnimationController controller; + CurvedAnimation animation; + + double get horizontalLeadingOffset { + double weightSum(Iterable> animations) { + // We're adding flex values instead of animation values to produce correct + // ratios. + return animations.map(state._evaluateFlex).fold(0.0, (double sum, double value) => sum + value); + } + + final double allWeights = weightSum(state._animations); + // These weights sum to the start edge of the indexed item. + final double leadingWeights = weightSum(state._animations.sublist(0, index)); + + // Add half of its flex value in order to get to the center. + return (leadingWeights + state._evaluateFlex(state._animations[index]) / 2.0) / allWeights; + } + + void dispose() { + controller.dispose(); + } +} + +// Paints the animating color splash circles. +class _RadialPainter extends CustomPainter { + _RadialPainter({ + @required this.circles, + @required this.textDirection, + }) : assert(circles != null), + assert(textDirection != null); + + final List<_Circle> circles; + final TextDirection textDirection; + + // Computes the maximum radius attainable such that at least one of the + // bounding rectangle's corners touches the edge of the circle. Drawing a + // circle larger than this radius is not needed, since there is no perceivable + // difference within the cropped rectangle. + static double _maxRadius(Offset center, Size size) { + final double maxX = math.max(center.dx, size.width - center.dx); + final double maxY = math.max(center.dy, size.height - center.dy); + return math.sqrt(maxX * maxX + maxY * maxY); + } + + @override + bool shouldRepaint(_RadialPainter oldPainter) { + if (textDirection != oldPainter.textDirection) return true; + if (circles == oldPainter.circles) return false; + if (circles.length != oldPainter.circles.length) return true; + for (int i = 0; i < circles.length; i += 1) if (circles[i] != oldPainter.circles[i]) return true; + return false; + } + + @override + void paint(Canvas canvas, Size size) { + for (_Circle circle in circles) { + final Paint paint = Paint()..color = circle.color; + final Rect rect = Rect.fromLTWH(0.0, 0.0, size.width, size.height); + canvas.clipRect(rect); + double leftFraction; + switch (textDirection) { + case TextDirection.rtl: + leftFraction = 1.0 - circle.horizontalLeadingOffset; + break; + case TextDirection.ltr: + leftFraction = circle.horizontalLeadingOffset; + break; + } + final Offset center = Offset(leftFraction * size.width, size.height / 2.0); + final Tween radiusTween = Tween( + begin: 0.0, + end: _maxRadius(center, size), + ); + canvas.drawCircle( + center, + radiusTween.transform(circle.animation.value), + paint, + ); + } + } +} diff --git a/lib/library/popup_menu.dart b/lib/library/popup_menu.dart new file mode 100644 index 0000000..a969a4e --- /dev/null +++ b/lib/library/popup_menu.dart @@ -0,0 +1,599 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +const Duration _kMenuDuration = Duration(milliseconds: 300); +const double _kMenuCloseIntervalEnd = 2.0 / 3.0; +const double _kMenuHorizontalPadding = 0.0; +const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; +const double _kMenuVerticalPadding = 0.0; +const double _kMenuWidthStep = 1.0; +const double _kMenuScreenPadding = 0.0; + +// This widget only exists to enable _PopupMenuRoute to save the sizes of +// each menu item. The sizes are used by _PopupMenuRouteLayout to compute the +// y coordinate of the menu's origin so that the center of selected menu +// item lines up with the center of its PopupMenuButton. +class _MenuItem extends SingleChildRenderObjectWidget { + const _MenuItem({ + Key key, + @required this.onLayout, + Widget child, + }) : assert(onLayout != null), + super(key: key, child: child); + + final ValueChanged onLayout; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderMenuItem(onLayout); + } + + @override + void updateRenderObject(BuildContext context, covariant _RenderMenuItem renderObject) { + renderObject.onLayout = onLayout; + } +} + +class _RenderMenuItem extends RenderShiftedBox { + _RenderMenuItem(this.onLayout, [RenderBox child]) + : assert(onLayout != null), + super(child); + + ValueChanged onLayout; + + @override + void performLayout() { + if (child == null) { + size = Size.zero; + } else { + child.layout(constraints, parentUsesSize: true); + size = constraints.constrain(child.size); + } + final BoxParentData childParentData = child.parentData; + childParentData.offset = Offset.zero; + onLayout(size); + } +} + +/// An item in a material design popup menu. +/// +/// To show a popup menu, use the [customShowMenu] function. To create a button that +/// shows a popup menu, consider using [PopupMenuButton]. +/// +/// To show a checkmark next to a popup menu item, consider using +/// [CheckedPopupMenuItem]. +/// +/// Typically the [child] of a [CustomPopupMenuItem] is a [Text] widget. More +/// elaborate menus with icons can use a [ListTile]. By default, a +/// [CustomPopupMenuItem] is kMinInteractiveDimension pixels high. If you use a widget +/// with a different height, it must be specified in the [height] property. +/// +/// {@tool sample} +/// +/// Here, a [Text] widget is used with a popup menu item. The `WhyFarther` type +/// is an enum, not shown here. +/// +/// ```dart +/// const CustomPopupMenuItem( +/// value: WhyFarther.harder, +/// child: Text('Working a lot harder'), +/// ) +/// ``` +/// {@end-tool} +/// +/// See the example at [PopupMenuButton] for how this example could be used in a +/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to +/// keep the text of [CustomPopupMenuItem]s that use [Text] widgets in their [child] +/// slot aligned with the text of [CheckedPopupMenuItem]s or of [CustomPopupMenuItem] +/// that use a [ListTile] in their [child] slot. +/// +/// See also: +/// +/// * [PopupMenuDivider], which can be used to divide items from each other. +/// * [CheckedPopupMenuItem], a variant of [CustomPopupMenuItem] with a checkmark. +/// * [customShowMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +class CustomPopupMenuItem extends PopupMenuEntry { + /// Creates an item for a popup menu. + /// + /// By default, the item is [enabled]. + /// + /// The `enabled` and `height` arguments must not be null. + const CustomPopupMenuItem({ + Key key, + this.value, + this.enabled = true, + this.height = kMinInteractiveDimension, + this.textStyle, + @required this.child, + }) : assert(enabled != null), + assert(height != null), + super(key: key); + + /// The value that will be returned by [customShowMenu] if this entry is selected. + final T value; + + /// Whether the user is permitted to select this item. + /// + /// Defaults to true. If this is false, then the item will not react to + /// touches. + final bool enabled; + + /// The minimum height height of the menu item. + /// + /// Defaults to [kMinInteractiveDimension] pixels. + @override + final double height; + + /// The text style of the popup menu item. + /// + /// If this property is null, then [PopupMenuThemeData.textStyle] is used. + /// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subhead] is used. + final TextStyle textStyle; + + /// The widget below this widget in the tree. + /// + /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An + /// appropriate [DefaultTextStyle] is put in scope for the child. In either + /// case, the text should be short enough that it won't wrap. + final Widget child; + + @override + bool represents(T value) => value == this.value; + + @override + PopupMenuItemState> createState() => PopupMenuItemState>(); +} + +/// The [State] for [CustomPopupMenuItem] subclasses. +/// +/// By default this implements the basic styling and layout of Material Design +/// popup menu items. +/// +/// The [buildChild] method can be overridden to adjust exactly what gets placed +/// in the menu. By default it returns [CustomPopupMenuItem.child]. +/// +/// The [handleTap] method can be overridden to adjust exactly what happens when +/// the item is tapped. By default, it uses [Navigator.pop] to return the +/// [CustomPopupMenuItem.value] from the menu route. +/// +/// This class takes two type arguments. The second, `W`, is the exact type of +/// the [Widget] that is using this [State]. It must be a subclass of +/// [CustomPopupMenuItem]. The first, `T`, must match the type argument of that widget +/// class, and is the type of values returned from this menu. +class PopupMenuItemState> extends State { + /// The menu item contents. + /// + /// Used by the [build] method. + /// + /// By default, this returns [CustomPopupMenuItem.child]. Override this to put + /// something else in the menu entry. + @protected + Widget buildChild() => widget.child; + + /// The handler for when the user selects the menu item. + /// + /// Used by the [InkWell] inserted by the [build] method. + /// + /// By default, uses [Navigator.pop] to return the [CustomPopupMenuItem.value] from + /// the menu route. + @protected + void handleTap() { + Navigator.pop(context, widget.value); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subtitle1; + + if (!widget.enabled) style = style.copyWith(color: theme.disabledColor); + + Widget item = AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), + child: buildChild(), + ), + ); + + if (!widget.enabled) { + final bool isDark = theme.brightness == Brightness.dark; + item = IconTheme.merge( + data: IconThemeData(opacity: isDark ? 0.5 : 0.38), + child: item, + ); + } + + return InkWell( + onTap: widget.enabled ? handleTap : null, + canRequestFocus: widget.enabled, + child: item, + ); + } +} + +class _PopupMenu extends StatelessWidget { + const _PopupMenu({ + Key key, + this.route, + this.semanticLabel, + }) : super(key: key); + + final _PopupMenuRoute route; + final String semanticLabel; + + @override + Widget build(BuildContext context) { + final double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. + final List children = []; + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + + for (int i = 0; i < route.items.length; i += 1) { + final double start = (i + 1) * unit; + final double end = (start + 1.5 * unit).clamp(0.0, 1.0); + final CurvedAnimation opacity = CurvedAnimation( + parent: route.animation, + curve: Interval(start, end), + ); + Widget item = route.items[i]; + if (route.initialValue != null && route.items[i].represents(route.initialValue)) { + item = Container( + color: Theme.of(context).highlightColor, + child: item, + ); + } + children.add( + _MenuItem( + onLayout: (Size size) { + route.itemSizes[i] = size; + }, + child: FadeTransition( + opacity: opacity, + child: item, + ), + ), + ); + } + + final CurveTween opacity = CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); + final CurveTween width = CurveTween(curve: Interval(0.0, unit)); + final CurveTween height = CurveTween(curve: Interval(0.0, unit * route.items.length)); + + final Widget child = ConstrainedBox( + constraints: const BoxConstraints(minWidth: _kMenuMinWidth), + child: IntrinsicWidth( + stepWidth: _kMenuWidthStep, + child: Semantics( + scopesRoute: true, + namesRoute: true, + explicitChildNodes: true, + label: semanticLabel, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: _kMenuVerticalPadding), + child: ListBody(children: children), + ), + ), + ), + ); + + return AnimatedBuilder( + animation: route.animation, + builder: (BuildContext context, Widget child) { + return Opacity( + opacity: opacity.evaluate(route.animation), + child: Material( + shape: route.shape ?? popupMenuTheme.shape, + color: route.color ?? popupMenuTheme.color, + type: MaterialType.card, + elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0, + child: Align( + alignment: AlignmentDirectional.topEnd, + widthFactor: width.evaluate(route.animation), + heightFactor: height.evaluate(route.animation), + child: child, + ), + ), + ); + }, + child: child, + ); + } +} + +// Positioning of the menu on the screen. +class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { + _PopupMenuRouteLayout(this.position, this.itemSizes, this.selectedItemIndex, this.textDirection); + + // Rectangle of underlying button, relative to the overlay's dimensions. + final RelativeRect position; + + // The sizes of each item are computed when the menu is laid out, and before + // the route is laid out. + List itemSizes; + + // The index of the selected item, or null if PopupMenuButton.initialValue + // was not specified. + final int selectedItemIndex; + + // Whether to prefer going to the left or to the right. + final TextDirection textDirection; + + // We put the child wherever position specifies, so long as it will fit within + // the specified parent size padded (inset) by 8. If necessary, we adjust the + // child's position so that it fits. + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + // The menu can be at most the size of the overlay minus 8.0 pixels in each + // direction. + return BoxConstraints.loose(constraints.biggest - const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0)); + } + + @override + Offset getPositionForChild(Size size, Size childSize) { + // size: The size of the overlay. + // childSize: The size of the menu, when fully open, as determined by + // getConstraintsForChild. + + // Find the ideal vertical position. + double y = position.top; + if (selectedItemIndex != null && itemSizes != null) { + double selectedItemOffset = _kMenuVerticalPadding; + for (int index = 0; index < selectedItemIndex; index += 1) selectedItemOffset += itemSizes[index].height; + selectedItemOffset += itemSizes[selectedItemIndex].height / 2; + y = position.top + (size.height - position.top - position.bottom) / 2.0 - selectedItemOffset; + } + + // Find the ideal horizontal position. + double x; + if (position.left > position.right) { + // Menu button is closer to the right edge, so grow to the left, aligned to the right edge. + x = size.width - position.right - childSize.width; + } else if (position.left < position.right) { + // Menu button is closer to the left edge, so grow to the right, aligned to the left edge. + x = position.left; + } else { + // Menu button is equidistant from both edges, so grow in reading direction. + assert(textDirection != null); + switch (textDirection) { + case TextDirection.rtl: + x = size.width - position.right - childSize.width; + break; + case TextDirection.ltr: + x = position.left; + break; + } + } + + // Avoid going outside an area defined as the rectangle 8.0 pixels from the + // edge of the screen in every direction. + if (x < _kMenuScreenPadding) + x = _kMenuScreenPadding; + else if (x + childSize.width > size.width - _kMenuScreenPadding) x = size.width - childSize.width - _kMenuScreenPadding; + if (y < _kMenuScreenPadding) + y = _kMenuScreenPadding; + else if (y + childSize.height > size.height - _kMenuScreenPadding) y = size.height - childSize.height - _kMenuScreenPadding; + return Offset(x, y); + } + + @override + bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) { + // If called when the old and new itemSizes have been initialized then + // we expect them to have the same length because there's no practical + // way to change length of the items list once the menu has been shown. + assert(itemSizes.length == oldDelegate.itemSizes.length); + + return position != oldDelegate.position || + selectedItemIndex != oldDelegate.selectedItemIndex || + textDirection != oldDelegate.textDirection || + !listEquals(itemSizes, oldDelegate.itemSizes); + } +} + +class _PopupMenuRoute extends PopupRoute { + _PopupMenuRoute({ + this.position, + this.items, + this.initialValue, + this.elevation, + this.theme, + this.popupMenuTheme, + this.barrierLabel, + this.semanticLabel, + this.shape, + this.color, + this.showMenuContext, + this.captureInheritedThemes, + this.barrierColor, + }) : itemSizes = List(items.length); + + final RelativeRect position; + final List> items; + final List itemSizes; + final dynamic initialValue; + final double elevation; + final ThemeData theme; + final String semanticLabel; + final ShapeBorder shape; + final Color color; + final PopupMenuThemeData popupMenuTheme; + final BuildContext showMenuContext; + final bool captureInheritedThemes; + final Color barrierColor; + + @override + Animation createAnimation() { + return CurvedAnimation( + parent: super.createAnimation(), + curve: Curves.linear, + reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd), + ); + } + + @override + Duration get transitionDuration => _kMenuDuration; + + @override + bool get barrierDismissible => true; + + @override + final String barrierLabel; + + @override + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + int selectedItemIndex; + if (initialValue != null) { + for (int index = 0; selectedItemIndex == null && index < items.length; index += 1) { + if (items[index].represents(initialValue)) selectedItemIndex = index; + } + } + + Widget menu = _PopupMenu(route: this, semanticLabel: semanticLabel); + if (captureInheritedThemes) { + menu = InheritedTheme.captureAll(showMenuContext, menu); + } else { + // For the sake of backwards compatibility. An (unlikely) app that relied + // on having menus only inherit from the material Theme could set + // captureInheritedThemes to false and get the original behavior. + if (theme != null) menu = Theme(data: theme, child: menu); + } + + return MediaQuery.removePadding( + context: context, + removeTop: true, + removeBottom: true, + removeLeft: true, + removeRight: true, + child: Builder( + builder: (BuildContext context) { + return CustomSingleChildLayout( + delegate: _PopupMenuRouteLayout( + position, + itemSizes, + selectedItemIndex, + Directionality.of(context), + ), + child: menu, + ); + }, + ), + ); + } +} + +/// Show a popup menu that contains the `items` at `position`. +/// +/// `items` should be non-null and not empty. +/// +/// If `initialValue` is specified then the first item with a matching value +/// will be highlighted and the value of `position` gives the rectangle whose +/// vertical center will be aligned with the vertical center of the highlighted +/// item (when possible). +/// +/// If `initialValue` is not specified then the top of the menu will be aligned +/// with the top of the `position` rectangle. +/// +/// In both cases, the menu position will be adjusted if necessary to fit on the +/// screen. +/// +/// Horizontally, the menu is positioned so that it grows in the direction that +/// has the most room. For example, if the `position` describes a rectangle on +/// the left edge of the screen, then the left edge of the menu is aligned with +/// the left edge of the `position`, and the menu grows to the right. If both +/// edges of the `position` are equidistant from the opposite edge of the +/// screen, then the ambient [Directionality] is used as a tie-breaker, +/// preferring to grow in the reading direction. +/// +/// The positioning of the `initialValue` at the `position` is implemented by +/// iterating over the `items` to find the first whose +/// [CustomPopupMenuEntry.represents] method returns true for `initialValue`, and then +/// summing the values of [CustomPopupMenuEntry.height] for all the preceding widgets +/// in the list. +/// +/// The `elevation` argument specifies the z-coordinate at which to place the +/// menu. The elevation defaults to 8, the appropriate elevation for popup +/// menus. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the menu. It is only used when the method is called. Its corresponding +/// widget can be safely removed from the tree before the popup menu is closed. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// menu to the [Navigator] furthest from or nearest to the given `context`. It +/// is `false` by default. +/// +/// The `semanticLabel` argument is used by accessibility frameworks to +/// announce screen transitions when the menu is opened and closed. If this +/// label is not provided, it will default to +/// [MaterialLocalizations.popupMenuLabel]. +/// +/// See also: +/// +/// * [CustomPopupMenuItem], a popup menu entry for a single value. +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. +/// * [PopupMenuButton], which provides an [IconButton] that shows a menu by +/// calling this method automatically. +/// * [SemanticsConfiguration.namesRoute], for a description of edge triggered +/// semantics. +Future customShowMenu({ + @required BuildContext context, + @required RelativeRect position, + @required List> items, + T initialValue, + double elevation, + String semanticLabel, + Color barrierColor, + ShapeBorder shape, + Color color, + bool captureInheritedThemes = true, + bool useRootNavigator = false, +}) { + assert(context != null); + assert(position != null); + assert(useRootNavigator != null); + assert(items != null && items.isNotEmpty); + assert(captureInheritedThemes != null); + assert(debugCheckHasMaterialLocalizations(context)); + + String label = semanticLabel; + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + label = semanticLabel; + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel; + } + + return Navigator.of(context, rootNavigator: useRootNavigator).push( + _PopupMenuRoute( + position: position, + items: items, + initialValue: initialValue, + elevation: elevation, + semanticLabel: label, + theme: Theme.of(context), + popupMenuTheme: PopupMenuTheme.of(context), + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + barrierColor: barrierColor, + shape: shape, + color: color, + showMenuContext: context, + captureInheritedThemes: captureInheritedThemes, + ), + ); +} diff --git a/lib/library/select_dialog.dart b/lib/library/select_dialog.dart new file mode 100644 index 0000000..0f33172 --- /dev/null +++ b/lib/library/select_dialog.dart @@ -0,0 +1,407 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'dropdown_search.dart'; + +class SelectDialog extends StatefulWidget { + final T selectedValue; + final List items; + final bool showSearchBox; + final bool isFilteredOnline; + final ValueChanged onChanged; + final DropdownSearchOnFind onFind; + final DropdownSearchPopupItemBuilder itemBuilder; + final InputDecoration searchBoxDecoration; + final DropdownSearchItemAsString itemAsString; + final DropdownSearchFilterFn filterFn; + final String hintText; + final double maxHeight; + final double dialogMaxWidth; + final Widget popupTitle; + final bool showSelectedItem; + final DropdownSearchCompareFn compareFn; + final DropdownSearchPopupItemEnabled itemDisabled; + + ///custom layout for empty results + final EmptyBuilder emptyBuilder; + + ///custom layout for loading items + final LoadingBuilder loadingBuilder; + + ///custom layout for error + final ErrorBuilder errorBuilder; + + ///the search box will be focused if true + final bool autoFocusSearchBox; + + ///text controller to set default search word for example + final TextEditingController searchBoxController; + + ///delay before searching + final Duration searchDelay; + + const SelectDialog({ + Key key, + this.popupTitle, + this.items, + this.maxHeight, + this.showSearchBox = false, + this.isFilteredOnline = false, + this.onChanged, + this.selectedValue, + this.onFind, + this.itemBuilder, + this.searchBoxDecoration, + this.hintText, + this.itemAsString, + this.filterFn, + this.showSelectedItem = false, + this.compareFn, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, + this.autoFocusSearchBox = false, + this.dialogMaxWidth, + this.itemDisabled, + this.searchBoxController, + this.searchDelay, + }) : super(key: key); + + @override + _SelectDialogState createState() => _SelectDialogState(); +} + +class _SelectDialogState extends State> { + final FocusNode focusNode = new FocusNode(); + final StreamController> _itemsStream = StreamController(); + final ValueNotifier _loadingNotifier = ValueNotifier(false); + final List _items = List(); + Debouncer _debouncer; + + @override + void initState() { + super.initState(); + _debouncer = Debouncer(delay: widget.searchDelay); + + Future.delayed( + Duration.zero, + () => manageItemsByFilter(widget.searchBoxController?.text ?? '', isFistLoad: true), + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (widget.autoFocusSearchBox) FocusScope.of(context).requestFocus(focusNode); + } + + @override + void dispose() { + _itemsStream.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Size deviceSize = MediaQuery.of(context).size; + bool isTablet = deviceSize.width > deviceSize.height; + double maxHeight = deviceSize.height * (isTablet ? .8 : .6); + double maxWidth = deviceSize.width * (isTablet ? .7 : .9); + + return Container( + width: widget.dialogMaxWidth ?? maxWidth, + constraints: BoxConstraints(maxHeight: widget.maxHeight ?? maxHeight), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + _searchField(), + Expanded( + child: Stack( + children: [ + StreamBuilder>( + stream: _itemsStream.stream, + builder: (context, snapshot) { + if (snapshot.hasError) { + return _errorWidget(snapshot?.error); + } else if (!snapshot.hasData) { + return _loadingWidget(); + } else if (snapshot.data.isEmpty) { + if (widget.emptyBuilder != null) + return widget.emptyBuilder(context, widget.searchBoxController?.text); + else + return const Center( + child: const Text("No data found"), + ); + } + return ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.symmetric(vertical: 0), + itemCount: snapshot.data.length, + itemBuilder: (context, index) { + var item = snapshot.data[index]; + return _itemWidget(item); + }, + ); + }, + ), + _loadingWidget() + ], + ), + ), + ], + ), + ); + } + + void _showErrorDialog(dynamic error) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Error while getting online items"), + content: _errorWidget(error), + actions: [ + FlatButton( + child: new Text("OK"), + onPressed: () { + Navigator.of(context).pop(false); + }, + ) + ], + ); + }, + ); + } + + Widget _errorWidget(dynamic error) { + if (widget.errorBuilder != null) + return widget.errorBuilder(context, widget.searchBoxController?.text, error); + else + return Padding( + padding: EdgeInsets.all(8), + child: Text( + error?.toString() ?? 'Error', + ), + ); + } + + Widget _loadingWidget() { + return ValueListenableBuilder( + valueListenable: _loadingNotifier, + builder: (context, bool isLoading, wid) { + if (isLoading) { + if (widget.loadingBuilder != null) + return widget.loadingBuilder(context, widget.searchBoxController?.text); + else + return Padding( + padding: const EdgeInsets.all(24.0), + child: const Center( + child: const CircularProgressIndicator(), + ), + ); + } + return Container(); + }); + } + + void _onTextChanged(String filter) async { + manageItemsByFilter(filter); + } + + ///Function that filter item (online and offline) base on user filter + ///[filter] is the filter keyword + ///[isFirstLoad] true if it's the first time we load data from online, false other wises + void manageItemsByFilter(String filter, {bool isFistLoad = false}) async { + _loadingNotifier.value = true; + + String encoded(String item) { + String encodedItem = ""; + for (int i = 0; i < item.length; i++) { + var char = item[i]; + switch (char) { + case 'Á': + case 'á': + case 'ą': + case 'ä': + char = 'a'; + break; + case 'é': + case 'É': + char = 'e'; + break; + case 'ú': + case 'ű': + case 'ü': + case 'Ú': + case 'Ű': + case 'Ü': + char = 'u'; + break; + case 'ö': + case 'ő': + case 'ó': + case 'Ö': + case 'Ő': + case 'Ó': + char = 'o'; + break; + case 'í': + case 'Í': + char = 'i'; + break; + } + encodedItem += char; + } + return encodedItem; + } + + List applyFilter(String filter) { + return _items.where((i) { + if (widget.filterFn != null) { + return (widget.filterFn(i, filter)); + } else if (i.toString().toLowerCase().contains(filter.toLowerCase()) || + encoded(i.toString()).toLowerCase().contains(encoded(filter.toLowerCase()))) { + return true; + } else if (widget.itemAsString != null) { + bool found = (widget.itemAsString(i))?.toLowerCase()?.contains(filter.toLowerCase()) ?? false; + if (!found) { + found = (encoded(widget.itemAsString(i)))?.toLowerCase()?.contains(encoded(filter.toLowerCase())) ?? false; + } + + return found; + } + return false; + }).toList(); + } + + //load offline data for the first time + if (isFistLoad && widget.items != null) _items.addAll(widget.items); + + //manage offline items + if (widget.onFind != null && (widget.isFilteredOnline || isFistLoad)) { + try { + final List onlineItems = List(); + onlineItems.addAll(await widget.onFind(filter) ?? List()); + + //Remove all old data + _items.clear(); + //add offline items + if (widget.items != null) { + _items.addAll(widget.items); + //if filter online we filter only local list based on entred keyword (filter) + if (widget.isFilteredOnline == true) { + var filteredLocalList = applyFilter(filter); + _items.clear(); + _items.addAll(filteredLocalList); + } + } + //add new online items to list + _items.addAll(onlineItems); + + _addDataToStream(applyFilter(filter)); + } catch (e) { + _addErrorToStream(e); + //if offline items count > 0 , the error will be not visible for the user + //As solution we show it in dialog + if (widget.items != null && widget.items.isNotEmpty) { + _showErrorDialog(e); + _addDataToStream(applyFilter(filter)); + } + } + } else { + _addDataToStream(applyFilter(filter)); + } + + _loadingNotifier.value = false; + } + + void _addDataToStream(List data) { + if (_itemsStream.isClosed) return; + _itemsStream.add(data); + } + + void _addErrorToStream(Object error, [StackTrace stackTrace]) { + if (_itemsStream.isClosed) return; + _itemsStream.addError(error, stackTrace); + } + + Widget _itemWidget(T item) { + if (widget.itemBuilder != null) + return InkWell( + child: widget.itemBuilder( + context, + item, + _manageSelectedItemVisibility(item), + ), + onTap: widget.itemDisabled != null && (widget.itemDisabled(item) ?? false) == true + ? null + : () { + Navigator.pop(context, item); + if (widget.onChanged != null) widget.onChanged(item); + }, + ); + else + return ListTile( + title: Text( + widget.itemAsString != null ? (widget.itemAsString(item) ?? "") : item.toString(), + ), + selected: _manageSelectedItemVisibility(item), + onTap: widget.itemDisabled != null && (widget.itemDisabled(item) ?? false) == true + ? null + : () { + Navigator.pop(context, item); + if (widget.onChanged != null) widget.onChanged(item); + }, + ); + } + + /// selected item will be highlighted only when [widget.showSelectedItem] is true, + /// if our object is String [widget.compareFn] is not required , other wises it's required + bool _manageSelectedItemVisibility(T item) { + if (!widget.showSelectedItem) return false; + + if (T == String) { + return item == widget.selectedValue; + } else { + return widget.compareFn(item, widget.selectedValue); + } + } + + Widget _searchField() { + return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ + widget.popupTitle ?? const SizedBox.shrink(), + if (widget.showSearchBox) + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: widget.searchBoxController, + focusNode: focusNode, + onChanged: (f) => _debouncer(() { + _onTextChanged(f); + }), + decoration: widget.searchBoxDecoration ?? + InputDecoration( + hintText: widget.hintText, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + ), + ) + ]); + } +} + +class Debouncer { + final Duration delay; + Timer _timer; + + Debouncer({this.delay}); + + call(Function action) { + _timer?.cancel(); + _timer = Timer(delay ?? const Duration(milliseconds: 500), action); + } +} diff --git a/lib/main.dart b/lib/main.dart index 2f9ebba..4371ba6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; import 'package:aitrainer_app/push_notifications.dart'; import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:aitrainer_app/repository/workout_tree_repository.dart'; @@ -19,10 +20,8 @@ import 'package:aitrainer_app/view/exercise_execute_plan_add_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_custom_detail_add_page.dart'; -import 'package:aitrainer_app/view/exercise_type_description.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_body_page.dart'; import 'package:aitrainer_app/view/mydevelopment_muscle_page.dart'; import 'package:aitrainer_app/view/mydevelopment_page.dart'; @@ -32,7 +31,10 @@ import 'package:aitrainer_app/view/registration.dart'; import 'package:aitrainer_app/view/reset_password.dart'; import 'package:aitrainer_app/view/sales_page.dart'; import 'package:aitrainer_app/view/settings.dart'; +import 'package:aitrainer_app/view/test_set_control.dart'; import 'package:aitrainer_app/view/test_set_edit.dart'; +import 'package:aitrainer_app/view/test_set_execute.dart'; +import 'package:aitrainer_app/view/test_set_new.dart'; import 'package:aitrainer_app/widgets/home.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/observer.dart'; @@ -151,6 +153,9 @@ Future main() async { BlocProvider( create: (BuildContext context) => TimerBloc(), ), + BlocProvider( + create: (BuildContext context) => TestSetExecuteBloc(), + ), ], child: WorkoutTestApp(), )); @@ -211,10 +216,8 @@ class WorkoutTestApp extends StatelessWidget { 'login': (context) => LoginPage(), 'resetPassword': (context) => ResetPasswordPage(), 'registration': (context) => RegistrationPage(), - 'menu_page': (context) => MenuPage(), 'account': (context) => AccountPage(), 'settings': (context) => SettingsPage(), - 'exerciseTypeDescription': (context) => ExerciseTypeDescription(), 'myDevelopment': (context) => MyDevelopmentPage(), 'myExercisePlan': (context) => MyExercisePlanPage(), 'exerciseLogPage': (context) => ExerciseLogPage(), @@ -228,6 +231,9 @@ class WorkoutTestApp extends StatelessWidget { 'evaluationPage': (context) => EvaluationPage(), 'salesPage': (context) => SalesPage(), 'testSetEdit': (context) => TestSetEdit(), + 'testSetExecute': (context) => TestSetExecute(), + 'testSetNew': (context) => TestSetNew(), + 'testSetControl': (context) => TestSetControl(), }, initialRoute: 'home', title: 'WorkoutTest', diff --git a/lib/model/cache.dart b/lib/model/cache.dart index ded0a72..6d5d8aa 100644 --- a/lib/model/cache.dart +++ b/lib/model/cache.dart @@ -24,6 +24,7 @@ import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; import 'package:package_info/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:intl/intl.dart'; import 'customer_exercise_device.dart'; import 'exercise_device.dart'; @@ -66,6 +67,7 @@ class Cache with Logging { static final String loginTypeKey = 'login_type'; static final String timerDisplayKey = 'timer_display'; static final String activeExercisePlanKey = 'active_exercise_plan'; + static final String activeExercisePlanDateKey = 'active_exercise_plan_date'; static final String activeExercisePlanDetailsKey = 'active_exercise_details_plan'; static String baseUrl = 'http://aitrainer.info:8888/api/'; @@ -144,14 +146,27 @@ class Cache with Logging { this.activeExercisePlan = exercisePlan; this.activeExercisePlanDetails = exercisePlanDetails; String exercisePlanJson = JsonEncoder().convert(exercisePlan.toJson()); - String detailsJson = jsonEncode(exercisePlanDetails); + String detailsJson = jsonEncode(exercisePlanDetails.map((i) => i.toJsonWithExerciseList()).toList()).toString(); Future prefs = SharedPreferences.getInstance(); SharedPreferences sharedPreferences; sharedPreferences = await prefs; + final DateTime now = DateTime.now(); sharedPreferences.setString(Cache.activeExercisePlanKey, exercisePlanJson); sharedPreferences.setString(Cache.activeExercisePlanDetailsKey, detailsJson); + String savingDay = DateFormat("yyyy-MM-dd HH:mm:ss").format(now); + sharedPreferences.setString(Cache.activeExercisePlanDateKey, savingDay); + } + + Future deleteActiveExercisePlan() async { + Future prefs = SharedPreferences.getInstance(); + SharedPreferences sharedPreferences; + sharedPreferences = await prefs; + + sharedPreferences.remove(Cache.activeExercisePlanDateKey); + this.activeExercisePlan = null; + this.activeExercisePlanDetails = null; } Future getActiveExercisePlan() async { @@ -159,6 +174,25 @@ class Cache with Logging { SharedPreferences sharedPreferences; sharedPreferences = await prefs; + final savedPlanDateString = sharedPreferences.getString(Cache.activeExercisePlanDateKey); + if (savedPlanDateString == null) { + return; + } + + DateFormat format = DateFormat("yyyy-MM-dd HH:mm:ss"); + DateTime savedPlanDate; + try { + savedPlanDate = format.parse(savedPlanDateString); + } on Exception catch (e) { + return; + } + + final DateTime now = DateTime.now(); + final DateTime added = savedPlanDate.add(Duration(days: 1)); + if (added.isBefore(now)) { + return; + } + String exercisePlanJson = sharedPreferences.getString(Cache.activeExercisePlanKey); if (exercisePlanJson != null) { final Map map = JsonDecoder().convert(exercisePlanJson); @@ -167,8 +201,9 @@ class Cache with Logging { String detailsJson = sharedPreferences.getString(Cache.activeExercisePlanDetailsKey); if (detailsJson != null) { + print("Details $detailsJson"); Iterable json = jsonDecode(detailsJson); - this.activeExercisePlanDetails = json.map((details) => ExercisePlanDetail.fromJson(details)).toList(); + this.activeExercisePlanDetails = json.map((details) => ExercisePlanDetail.fromJsonWithExerciseList(details)).toList(); } } diff --git a/lib/model/exercise.dart b/lib/model/exercise.dart index 654776b..8f8b193 100644 --- a/lib/model/exercise.dart +++ b/lib/model/exercise.dart @@ -59,4 +59,9 @@ class Exercise { newExercise.exercisePlanDetailId = this.exercisePlanDetailId; return newExercise; } + + @override + String toString() { + return this.toJson().toString(); + } } diff --git a/lib/model/exercise_ability.dart b/lib/model/exercise_ability.dart index 6b8d444..1919109 100644 --- a/lib/model/exercise_ability.dart +++ b/lib/model/exercise_ability.dart @@ -1,6 +1,23 @@ -enum ExerciseAbility { oneRepMax, endurance, running, mini_test, none } +enum ExerciseAbility { oneRepMax, endurance, running, mini_test_set, paralell_test, none } extension ExerciseAbilityExt on ExerciseAbility { + String enumToString() => this.toString().split(".").last; bool equalsTo(ExerciseAbility ability) => this.toString() == ability.toString(); bool equalsStringTo(String ability) => this.toString() == ability; + String get description { + switch (this) { + case ExerciseAbility.endurance: + return "Endurance"; + case ExerciseAbility.oneRepMax: + return "One Rep Max"; + case ExerciseAbility.running: + return "Running"; + case ExerciseAbility.mini_test_set: + return "Compact Test"; + case ExerciseAbility.paralell_test: + return "Custom Test"; + default: + return "Compact Test"; + } + } } diff --git a/lib/model/exercise_plan_detail.dart b/lib/model/exercise_plan_detail.dart index 68642ec..ba8ffbe 100644 --- a/lib/model/exercise_plan_detail.dart +++ b/lib/model/exercise_plan_detail.dart @@ -1,4 +1,14 @@ -import 'exercise_type.dart'; +import 'dart:convert'; + +import 'package:aitrainer_app/model/exercise.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; + +enum ExercisePlanDetailState { start, inProgress, finished } + +extension ExericisePlanDetailStateExt on ExercisePlanDetailState { + bool equalsTo(ExercisePlanDetailState state) => this.toString() == state.toString(); + bool equalsStringTo(String state) => this.toString() == state; +} class ExercisePlanDetail { int exercisePlanDetailId; @@ -8,6 +18,10 @@ class ExercisePlanDetail { int repeats; String weightEquation; + List exercises; + bool finished; + ExercisePlanDetailState state = ExercisePlanDetailState.start; + ExerciseType exerciseType; String change; // 1: update -1:delete 0: new @@ -24,6 +38,28 @@ class ExercisePlanDetail { this.weightEquation = json['weightEquation']; } + ExercisePlanDetail.fromJsonWithExerciseList(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']; + try { + final String exercises = json['exercises']; + String jsonExercises = exercises.replaceAllMapped( + RegExp(r'([a-zA-Z]+|[0-9]{4}\-[0-9]{2}\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})'), (Match m) => "\"${m[0]}\""); + + jsonExercises = jsonExercises.replaceAll(r'\"null\"', 'null'); + + print("Exercises $jsonExercises"); + Iterable iterable = jsonDecode(jsonExercises); + this.exercises = iterable.map((exercise) => Exercise.fromJson(exercise)).toList(); + } on Exception catch (e) { + print("JsonDecode error " + e.toString()); + } + } + Map toJson() => { "exercisePlanId": exercisePlanId, "exerciseTypeId": exerciseTypeId, @@ -31,4 +67,14 @@ class ExercisePlanDetail { "repeats": repeats, "weightEquation": weightEquation }; + + Map toJsonWithExerciseList() => { + "exercisePlanDetailId": exercisePlanDetailId, + "exercisePlanId": exercisePlanId, + "exerciseTypeId": exerciseTypeId, + "serie": serie, + "repeats": repeats, + "weightEquation": weightEquation, + 'exercises': exercises.map((exercise) => exercise.toJson()).toList().toString(), + }; } diff --git a/lib/model/exercise_tree.dart b/lib/model/exercise_tree.dart index 0a9ba64..465d959 100644 --- a/lib/model/exercise_tree.dart +++ b/lib/model/exercise_tree.dart @@ -1,10 +1,23 @@ class ExerciseTree { + /// treeId int treeId; + + /// parentId int parentId; + + /// name String name; + + /// imageUrl String imageUrl; + + /// active bool active; + + /// nameTranslation String nameTranslation; + + /// sort int sort; ExerciseTree(); diff --git a/lib/model/exercise_type.dart b/lib/model/exercise_type.dart index 63c5e7d..9e1bd17 100644 --- a/lib/model/exercise_type.dart +++ b/lib/model/exercise_type.dart @@ -1,24 +1,51 @@ import 'package:aitrainer_app/model/exercise_ability.dart'; -import 'package:flutter/services.dart'; +import 'package:aitrainer_app/util/app_language.dart'; +import 'package:flutter/material.dart'; class ExerciseType { + ///exerciseTypeId int exerciseTypeId; - //int treeId; + + /// name String name; + + /// description String description; - BinaryCodec video; + + /// unit String unit; + + /// unitQuantity String unitQuantity; + + /// unitQuantityUnit String unitQuantityUnit; + + ///active bool active; + + /// base bool base; + + /// imageUrl String imageUrl = ""; + + /// nameTranslation String nameTranslation = ""; + + /// descriptionTranslation String descriptionTranslation = ""; + + /// devices[] List devices = List(); + + /// parents[] List parents = List(); + + /// alternatives [] List alternatives = List(); + /// ability ExerciseAbility ability; ExerciseType({this.name, this.description}); @@ -37,8 +64,8 @@ class ExerciseType { this.imageUrl = json['images'][0]['url']; } if (json['translations'].length > 0) { - this.nameTranslation = json['translations'][0]['name']; - this.descriptionTranslation = json['translations'][0]['description']; + this.nameTranslation = AppLanguage().appLocal == Locale('hu') ? json['translations'][0]['name'] : json['name']; + this.descriptionTranslation = AppLanguage().appLocal == Locale('hu') ? json['translations'][0]['description'] : json['description']; } if (json['devices'].length > 0) { @@ -86,11 +113,12 @@ class ExerciseType { return this.ability; } - bool isEndurance() { - return this.ability.equalsTo(ExerciseAbility.endurance); - } - bool is1RM() { return this.ability.equalsTo(ExerciseAbility.oneRepMax); } + + @override + String toString() { + return this.toJson().toString(); + } } diff --git a/lib/model/workout_menu_tree.dart b/lib/model/workout_menu_tree.dart index 8593df6..a468f5c 100644 --- a/lib/model/workout_menu_tree.dart +++ b/lib/model/workout_menu_tree.dart @@ -31,7 +31,6 @@ class WorkoutMenuTree { bool base; bool is1RM; - bool isEndurance; bool isRunning; List workoutTypes = List(); bool selected = false; @@ -42,24 +41,8 @@ class WorkoutMenuTree { String parentNameEnglish; int sort; - WorkoutMenuTree( - this.id, - this.parent, - this.name, - this.imageName, - this.color, - this.fontSize, - this.child, - this.exerciseTypeId, - this.exerciseType, - this.base, - this.is1RM, - this.isEndurance, - this.isRunning, - this.nameEnglish, - this.parentName, - this.parentNameEnglish, - this.sort); + WorkoutMenuTree(this.id, this.parent, this.name, this.imageName, this.color, this.fontSize, this.child, this.exerciseTypeId, + this.exerciseType, this.base, this.is1RM, this.isRunning, this.nameEnglish, this.parentName, this.parentNameEnglish, this.sort); Map toJson() { return { @@ -73,7 +56,6 @@ class WorkoutMenuTree { "exerciseTypeId": exerciseTypeId.toString(), "base": base.toString(), "is1RM": is1RM.toString(), - "isEndurance": isEndurance.toString(), "isRunning": isRunning.toString(), "sort": sort, }; diff --git a/lib/repository/exercise_repository.dart b/lib/repository/exercise_repository.dart index 1afc4e0..966a370 100644 --- a/lib/repository/exercise_repository.dart +++ b/lib/repository/exercise_repository.dart @@ -73,7 +73,7 @@ class ExerciseRepository { Exercise getExercise() => this.exercise; - Future addExercise() async { + Future addExercise() async { final Exercise modelExercise = this.exercise; modelExercise.customerId = this.customer.customerId; modelExercise.exerciseTypeId = this.exerciseType.exerciseTypeId; @@ -94,17 +94,18 @@ class ExerciseRepository { Cache().addExerciseTrainee(savedExercise); } -/* this.actualExerciseList.forEach((element) { - print("$index. actual: " + element.toJson().toString()); - }); */ + return savedExercise; + } + void initExercise() { this.createNew(); this.exerciseType = exerciseType; this.setUnit(exerciseType.unit); exercise.exerciseTypeId = this.exerciseType.exerciseTypeId; - this.setQuantity(quantity); - this.setUnitQuantity(modelExercise.unitQuantity); + this.setQuantity(12); + this.setUnitQuantity(30); this.exercise.exercisePlanDetailId = 0; + exercise.exerciseId = 0; this.start = DateTime.now(); } @@ -303,7 +304,7 @@ class ExerciseRepository { double quantity = exercise.quantity == null ? 0 : exercise.quantity; summary += delimiter + quantity.toStringAsFixed(0); ExerciseType exerciseType = Cache().getExerciseTypeById(exercise.exerciseTypeId); - //print("exerciseType " + (exerciseType == null ? "NULL" : exerciseType.name) + " ID " + exercise.exerciseTypeId.toString()); + print("exerciseType " + (exerciseType == null ? "NULL" : exerciseType.name) + " ID " + exercise.exerciseTypeId.toString()); if (exerciseType.unitQuantity == "1") { summary += "x" + exercise.unitQuantity.toStringAsFixed(0); } diff --git a/lib/repository/workout_tree_repository.dart b/lib/repository/workout_tree_repository.dart index 934dab9..07f5b94 100644 --- a/lib/repository/workout_tree_repository.dart +++ b/lib/repository/workout_tree_repository.dart @@ -11,23 +11,12 @@ import 'package:aitrainer_app/service/exercise_type_service.dart'; import 'package:aitrainer_app/service/logging.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 & ABS"; - static int coreNr = 6; - static String thigh = "Thigh"; - static int thighNr = 7; - static String calf = "Calf"; - static int calfNr = 8; +enum Antagonist { chest, biceps, triceps, back, shoulders, core, thigh, calf } + +extension AntagonistExt on Antagonist { + bool equalsTo(Antagonist type) => this.toString() == type.toString(); + bool equalsStringTo(String type) => this.toString() == type; + String enumToString() => this.toString().split(".").last; } class WorkoutTreeRepository with Logging { @@ -37,17 +26,6 @@ class WorkoutTreeRepository with Logging { WorkoutType workoutType; final List menuAsExercise = List(); - 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 { //if (Cache().getExerciseTree().length > 0 || Cache().getWorkoutMenuTree().length > 0) return; isEnglish = AppLanguage().appLocal == Locale('en'); @@ -66,20 +44,16 @@ class WorkoutTreeRepository with Logging { exerciseTypes = await ExerciseTypeApi().getExerciseTypes(); } + exerciseTree.sort((a, b) => a.sort.compareTo(b.sort)); + exerciseTree.forEach((treeItem) async { //log(" -- TreeItem " + treeItem.toJson().toString() + " active " + treeItem.active.toString()); if (treeItem.active == true) { String treeName = isEnglish ? treeItem.name : treeItem.nameTranslation; - //String assetImage = await _buildImage(treeItem.imageUrl); - bool is1RM = treeItem.name == 'One Rep Max' ? true : false; - if (is1RM == false && treeItem.parentId != 0) { - is1RM = isParent1RM(treeItem.parentId); - } - - bool isEndurance = treeItem.name == 'Endurance' ? true : false; - if (isEndurance == false && treeItem.parentId != 0) { - isEndurance = isParentEndurance(treeItem.parentId); + bool is1RM = treeItem.name.contains("Muscle") || treeItem.name.contains("Shape") ? true : false; + if (!is1RM) { + is1RM = this.isParent1RM(treeItem.parentId); } bool isRunning = treeItem.name == "Cardio" ? true : false; @@ -99,7 +73,6 @@ class WorkoutTreeRepository with Logging { null, false, is1RM, - isEndurance, isRunning, treeItem.name, parent != null ? parent.name : "", @@ -107,7 +80,7 @@ class WorkoutTreeRepository with Logging { treeItem.sort); menuItem = this.setWorkoutTypes(menuItem, treeItem); this.tree[treeItem.name + "_" + treeItem.parentId.toString()] = menuItem; - //log("WorkoutMenuTree item " + menuItem.toJson().toString()); + //log("WorkoutMenuTree item ${menuItem.toJson()}"); } }); @@ -120,11 +93,14 @@ class WorkoutTreeRepository with Logging { if (exerciseType.parents.isNotEmpty) { exerciseType.parents.forEach((parentId) { bool is1RM = this.isParent1RM(parentId); - bool isEndurance = this.isParentEndurance(parentId); - if (is1RM) exerciseType.setAbility(ExerciseAbility.oneRepMax); - if (isEndurance) exerciseType.setAbility(ExerciseAbility.endurance); + if (is1RM) { + exerciseType.setAbility(ExerciseAbility.oneRepMax); + } bool isRunning = this.isParentRunning(parentId); - if (isRunning) exerciseType.setAbility(ExerciseAbility.running); + if (isRunning) { + is1RM = false; + exerciseType.setAbility(ExerciseAbility.running); + } WorkoutMenuTree parent = getParentItem(parentId); WorkoutMenuTree menuItem = WorkoutMenuTree( exerciseType.exerciseTypeId, @@ -138,7 +114,6 @@ class WorkoutTreeRepository with Logging { exerciseType, exerciseType.base, is1RM, - isEndurance, isRunning, exerciseType.name, parent != null ? parent.name : "", @@ -146,13 +121,7 @@ class WorkoutTreeRepository with Logging { 0); this.tree[exerciseType.name] = menuItem; menuAsExercise.add(menuItem); - //log("WorkoutMenuTree item " + menuItem.toJson().toString()); - /* log("ExerciseType in Menu item " + - exerciseType.toJson().toString() + - " is1RM: " + - is1RM.toString() + - " isEndurance: " + - isEndurance.toString()); */ + //log("ExerciseType in Menu item ${exerciseType.toJson()} is1RM: $is1RM"); }); } else { //log("No Parents " + exerciseType.toJson().toString()); @@ -215,20 +184,6 @@ class WorkoutTreeRepository with Logging { return isTreeItem1RM; } - bool isParentEndurance(int treeId) { - bool isTreeItemEndurance = false; - - this.tree.forEach((key, value) { - WorkoutMenuTree treeItem = value as WorkoutMenuTree; - if (treeItem.id == treeId) { - isTreeItemEndurance = isTreeItemEndurance || treeItem.isEndurance; - //log(treeItem.id.toString() + " " + treeItem.name + " Endurance? " + treeItem.isEndurance.toString()); - } - }); - - return isTreeItemEndurance; - } - bool isChild(int parentId) { bool isChild = true; @@ -247,7 +202,7 @@ class WorkoutTreeRepository with Logging { this.getBranch(parentId).forEach((key, value) { WorkoutMenuTree workoutTree = value; isChild = isChild && workoutTree.child; - isGym = isGym && (workoutTree.is1RM || workoutTree.isEndurance); + isGym = isGym && (workoutTree.is1RM); }); return isChild && isGym; } @@ -308,7 +263,6 @@ class WorkoutTreeRepository with Logging { } List list = List(); - list.add(workoutMenuTree); alternatives.forEach((element) { final WorkoutMenuTree alternativeMenuItem = this.getMenuItemByExerciseTypeId(element.exerciseTypeId); list.add(alternativeMenuItem); @@ -335,12 +289,23 @@ class WorkoutTreeRepository with Logging { return list; } + String getAntagonistSort(String type) { + String found = ""; + for (int i = 0; i < Antagonist.values.length; i++) { + if (type.toLowerCase().contains((Antagonist.values[i]).enumToString())) { + found = (i + 1).toString(); + } + } + return found; + } + void sortByMuscleType() { sortedTree = SplayTreeMap>(); tree.forEach((key, value) { WorkoutMenuTree workoutTree = value as WorkoutMenuTree; - if (workoutTree.nameEnglish != 'One Rep Max' && workoutTree.is1RM && workoutTree.exerciseTypeId == 0) { - String treeName = _antagonist[workoutTree.nameEnglish].toString() + ". " + workoutTree.name; + if (!workoutTree.nameEnglish.contains('Muscle Build') && workoutTree.is1RM && workoutTree.exerciseTypeId == 0) { + String treeName = getAntagonistSort(workoutTree.nameEnglish) + ". " + workoutTree.name; + print("TreeName $treeName ${workoutTree.name}"); sortedTree[treeName] = this.getBranchList(workoutTree.id); } }); diff --git a/lib/service/exercise_tree_service.dart b/lib/service/exercise_tree_service.dart index 26045a0..1a13cd3 100644 --- a/lib/service/exercise_tree_service.dart +++ b/lib/service/exercise_tree_service.dart @@ -19,7 +19,6 @@ class ExerciseTreeApi with Logging { if (exerciseTree != null) { await Future.forEach(exerciseTree, (element) async { - //exerciseTree.forEach((element) async { element.imageUrl = await buildImage(element.imageUrl, element.treeId); }); log("ExerciseTree downloaded"); @@ -55,10 +54,11 @@ class ExerciseTreeApi with Logging { if (parent.exerciseTreeChildId == element.treeId) { if (index > 0) { ExerciseTree newElement = element.copy(parent.exerciseTreeParentId); + newElement.sort = parent.sort ?? 0; exerciseTree.add(newElement); } else { element.parentId = parent.exerciseTreeParentId; - element.sort = parent.sort; + element.sort = parent.sort ?? 0; exerciseTree[treeIndex].parentId = parent.exerciseTreeParentId; } index++; diff --git a/lib/service/package_service.dart b/lib/service/package_service.dart index 059ec5f..1f76553 100644 --- a/lib/service/package_service.dart +++ b/lib/service/package_service.dart @@ -66,6 +66,7 @@ class PackageApi { exerciseTree = this.getExerciseTreeParents(exerciseTree, exerciseTreeParents); if (exerciseTree != null) { await Future.forEach(exerciseTree, (element) async { + print("Tree ${element.toJson()}"); element.imageUrl = await ExerciseTreeApi().buildImage(element.imageUrl, element.treeId); }); Cache().setExerciseTree(exerciseTree); @@ -84,10 +85,13 @@ class PackageApi { if (parent.exerciseTreeChildId == element.treeId) { if (index > 0) { ExerciseTree newElement = element.copy(parent.exerciseTreeParentId); + newElement.sort = parent.sort; exerciseTree.add(newElement); } else { element.parentId = parent.exerciseTreeParentId; + element.sort = parent.sort; exerciseTree[treeIndex].parentId = parent.exerciseTreeParentId; + exerciseTree[treeIndex].sort = parent.sort; } index++; } diff --git a/lib/util/enums.dart b/lib/util/enums.dart index 6c122d2..d2c9457 100644 --- a/lib/util/enums.dart +++ b/lib/util/enums.dart @@ -21,6 +21,7 @@ enum TrackingEvent { purchase_request, purchase_successful, exercise_new, + exercise_new_paralell, result, exercise_log, exercise_log_open, @@ -40,7 +41,9 @@ enum TrackingEvent { exercise_device, customer_change, settings_lang, - settings_server + settings_server, + test_set_edit, + test_set_new, } T enumFromString(Iterable values, String value) { diff --git a/lib/view/exercise_control_page.dart b/lib/view/exercise_control_page.dart index 1eb134d..c94661c 100644 --- a/lib/view/exercise_control_page.dart +++ b/lib/view/exercise_control_page.dart @@ -36,15 +36,13 @@ class _ExerciseControlPage extends State with Trans { Widget build(BuildContext context) { LinkedHashMap arguments = ModalRoute.of(context).settings.arguments; final ExerciseRepository exerciseRepository = arguments['exerciseRepository']; - final double percent = arguments['percent']; final bool readonly = arguments['readonly']; setContext(context); // ignore: close_sinks TimerBloc timerBloc = BlocProvider.of(context); return BlocProvider( - create: (context) => ExerciseControlBloc( - exerciseRepository: exerciseRepository, percentToCalculate: percent, readonly: readonly, timerBloc: timerBloc) + create: (context) => ExerciseControlBloc(exerciseRepository: exerciseRepository, readonly: readonly, timerBloc: timerBloc) ..add(ExerciseControlLoad()), child: BlocConsumer(listener: (context, state) { if (state is ExerciseControlError) { diff --git a/lib/view/exercise_new_page.dart b/lib/view/exercise_new_page.dart index 5953360..33cdd15 100644 --- a/lib/view/exercise_new_page.dart +++ b/lib/view/exercise_new_page.dart @@ -2,8 +2,7 @@ import 'dart:collection'; import 'package:aitrainer_app/bloc/exercise_new/exercise_new_bloc.dart'; import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; -import 'package:aitrainer_app/library/custom_icon_icons.dart'; -import 'package:aitrainer_app/util/app_language.dart'; +import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/model/exercise_ability.dart'; import 'package:aitrainer_app/model/exercise_type.dart'; @@ -14,95 +13,20 @@ import 'package:aitrainer_app/util/trans.dart'; import 'package:aitrainer_app/widgets/app_bar.dart'; import 'package:aitrainer_app/widgets/bmi_widget.dart'; import 'package:aitrainer_app/widgets/bmr_widget.dart'; +import 'package:aitrainer_app/widgets/bottom_bar_multiple_exercises.dart'; +import 'package:aitrainer_app/widgets/exercise_save.dart'; import 'package:aitrainer_app/widgets/size_widget.dart'; -import 'package:aitrainer_app/widgets/time_picker.dart'; -import 'package:flutter/scheduler.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'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:modal_progress_hud/modal_progress_hud.dart'; -import 'package:stop_watch_timer/stop_watch_timer.dart'; -import 'package:wakelock/wakelock.dart'; class ExerciseNewPage extends StatefulWidget { _ExerciseNewPageState createState() => _ExerciseNewPageState(); } class _ExerciseNewPageState extends State with Trans, Logging { - final FocusNode _nodeText1 = FocusNode(); - final FocusNode _nodeText2 = FocusNode(); - final _controller1 = TextEditingController(); - final _controller2 = TextEditingController(); - - initState() { - super.initState(); - _controller1.text = "30"; - _nodeText1.addListener(() { - if (_nodeText1.hasFocus) { - _controller1.selection = TextSelection(baseOffset: 0, extentOffset: _controller1.text.length); - } - }); - - SchedulerBinding.instance.addPostFrameCallback((_) { - // ignore: close_sinks - final menuBloc = BlocProvider.of(context); - _controller2.text = menuBloc.ability.toString() == ExerciseAbility.oneRepMax.toString() ? "12" : "20"; - _nodeText2.addListener(() { - if (_nodeText2.hasFocus) { - _controller2.selection = TextSelection(baseOffset: 0, extentOffset: _controller2.text.length); - } - }); - }); - } - - KeyboardActionsConfig _buildConfig(BuildContext context) { - return KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.ALL, - keyboardBarColor: Colors.grey[200], - keyboardSeparatorColor: Colors.black26, - nextFocus: true, - actions: [ - KeyboardActionsItem(focusNode: _nodeText2, toolbarButtons: [ - (node) { - return GestureDetector( - onTap: () => node.unfocus(), - child: Container( - padding: EdgeInsets.all(8.0), - color: Colors.orange[500], - child: Text( - t("Done"), - style: TextStyle(color: Colors.white), - ), - ), - ); - } - ]), - KeyboardActionsItem( - focusNode: _nodeText1, - toolbarButtons: [ - //button 2 - (node) { - return GestureDetector( - onTap: () => node.unfocus(), - child: Container( - color: Colors.orange, - padding: EdgeInsets.all(8.0), - child: Text( - t("Done"), - style: TextStyle(color: Colors.white), - ), - ), - ); - } - ], - ), - ], - ); - } - @override Widget build(BuildContext context) { final ExerciseType exerciseType = ModalRoute.of(context).settings.arguments; @@ -122,12 +46,33 @@ class _ExerciseNewPageState extends State with Trans, Logging { if (state is ExerciseNewError) { Scaffold.of(context).showSnackBar( SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); + } else if (state is ExerciseNewSaved) { + final LinkedHashMap args = LinkedHashMap(); + // ignore: close_sinks + final TestSetExecuteBloc executeBloc = BlocProvider.of(context); + print("Execute paralell $exerciseType paralell: ${executeBloc.paralellTest}"); + if (executeBloc != null && executeBloc.existsActivePlan() == true) { + Navigator.of(context).pushNamed("testSetExecute"); + } else { + // ignore: close_sinks + final bloc = BlocProvider.of(context); + + if (bloc.exerciseRepository.exerciseType.unitQuantityUnit == null) { + args['exerciseRepository'] = bloc.exerciseRepository; + Navigator.of(context).pushNamed('evaluationPage', arguments: args); + } else if (menuBloc.ability.equalsTo(ExerciseAbility.oneRepMax)) { + args['exerciseRepository'] = bloc.exerciseRepository; + args['percent'] = 0.75; + args['readonly'] = false; + Navigator.of(context).pushNamed('exerciseControlPage', arguments: args); + } + } } }, builder: (context, state) { final exerciseBloc = BlocProvider.of(context); return ModalProgressHUD( - child: getExerciseWidget(exerciseBloc, exerciseType, menuBloc), + child: getExerciseSaveWidget(exerciseBloc, exerciseType, menuBloc), inAsyncCall: state is ExerciseNewLoading, opacity: 0.5, color: Colors.black54, @@ -137,21 +82,7 @@ class _ExerciseNewPageState extends State with Trans, Logging { )); } - Widget getExerciseWidget(ExerciseNewBloc exerciseBloc, ExerciseType exerciseType, MenuBloc menuBloc) { - exerciseBloc.exerciseRepository.setExerciseType(exerciseType); - final String exerciseName = AppLanguage().appLocal == Locale("en") - ? exerciseBloc.exerciseRepository.exerciseType.name - : exerciseBloc.exerciseRepository.exerciseType.nameTranslation; - - String exerciseDescription = AppLanguage().appLocal == Locale("en") - ? exerciseBloc.exerciseRepository.exerciseType.description - : exerciseBloc.exerciseRepository.exerciseType.descriptionTranslation; - if (exerciseDescription == null) { - exerciseDescription = ""; - } - - //log(exerciseBloc.exerciseRepository.exerciseType.name); - + Widget getExerciseSaveWidget(ExerciseNewBloc exerciseBloc, ExerciseType exerciseType, MenuBloc menuBloc) { if (exerciseBloc.exerciseRepository.exerciseType.name == "BMR") { return BMR(exerciseBloc: exerciseBloc); } @@ -162,288 +93,40 @@ class _ExerciseNewPageState extends State with Trans, Logging { return SizeWidget(exerciseBloc: exerciseBloc); } - final String exerciseTask = exerciseBloc.setExerciseTask(); - - return Form( - child: Scaffold( - resizeToAvoidBottomInset: true, + return Scaffold( appBar: AppBarNav(depth: 1), body: Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_black_background.jpg'), - fit: BoxFit.fill, - alignment: Alignment.center, - ), - ), - child: KeyboardActions( - config: _buildConfig(context), - child: Container( - padding: const EdgeInsets.only(top: 25, left: 55, right: 55), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - exerciseName, - style: GoogleFonts.archivoBlack( - fontWeight: FontWeight.bold, - fontSize: 24, - color: Colors.white, - shadows: [ - Shadow( - offset: Offset(5.0, 5.0), - blurRadius: 12.0, - color: Colors.black54, - ), - Shadow( - offset: Offset(-3.0, 3.0), - blurRadius: 12.0, - color: Colors.black54, - ), - ], - ), - overflow: TextOverflow.fade, - maxLines: 4, - softWrap: true, - textAlign: TextAlign.center, - ), - SizedBox( - height: 15, - ), - Text( - exerciseDescription, - style: GoogleFonts.inter(fontSize: 12, color: Colors.yellow[300]), - maxLines: 1, - overflow: TextOverflow.fade, - softWrap: true, - ), - InkWell( - child: Text( - t("More »"), - style: GoogleFonts.inter(fontSize: 12, color: Colors.blue[200]), - ), - onTap: () => { - Navigator.of(context).pushNamed('exerciseTypeDescription', arguments: exerciseBloc.exerciseRepository), - }, - ), - Divider( - color: Colors.transparent, - ), - Text( - t(exerciseTask), - style: GoogleFonts.inter( - fontSize: 14, - color: Colors.orange, - fontWeight: FontWeight.bold, - ), - maxLines: 3, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: true, - ), - Divider( - color: Colors.transparent, - ), - columnQuantityUnit(exerciseBloc), - Divider( - color: Colors.transparent, - ), - columnQuantity(exerciseBloc), - Divider( - color: Colors.transparent, - ), - exerciseBloc.exerciseRepository.exerciseType.unitQuantity == "1" - ? Text( - t("Step") + ": " + "1/4", - style: GoogleFonts.inter( - fontSize: 22, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - maxLines: 3, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: true, - ) - : Offstage(), - Divider( - color: Colors.transparent, - ), - Divider( - color: Colors.transparent, - ), - FlatButton( - onPressed: () => { - confirmationDialog(exerciseBloc, menuBloc), - }, - child: Stack( - alignment: Alignment.center, - children: [ - Image.asset('asset/icon/gomb_orange_c.png', width: 140, height: 60), - Text( - t("Save"), - style: TextStyle(fontSize: 16, color: Colors.white), - ), - ], - )), - ]), - ))), - ), - )); - } - - Column columnQuantityUnit(ExerciseNewBloc bloc) { - Column row = Column(); - if (bloc.exerciseRepository.exerciseType != null && bloc.exerciseRepository.exerciseType.unitQuantity == "1") { - row = Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - TextFormField( - focusNode: _nodeText1, - controller: _controller1, - decoration: InputDecoration( - contentPadding: EdgeInsets.only(left: 25, top: 5, bottom: 5), - labelText: t(bloc.exerciseRepository.exerciseType.unitQuantityUnit), - labelStyle: GoogleFonts.inter(fontSize: 20, color: Colors.yellow[50]), - fillColor: Colors.black38, - filled: true, - border: OutlineInputBorder( - gapPadding: 8.0, - borderRadius: BorderRadius.circular(12.0), - borderSide: BorderSide(color: Colors.white12, width: 0.4), - ), + padding: EdgeInsets.only(top: 10, left: 20, right: 20), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_black_background.jpg'), + fit: BoxFit.cover, + alignment: Alignment.center, ), - //initialValue: "30", - keyboardType: TextInputType.numberWithOptions(decimal: true), - textInputAction: TextInputAction.done, - style: GoogleFonts.archivoBlack(fontSize: 80, color: Colors.yellow[300]), - onChanged: (value) => {bloc.add(ExerciseNewQuantityUnitChange(quantity: double.parse(value)))}), - //] ), - ]); - } - return row; - } - - Column columnQuantity(ExerciseNewBloc bloc) { - if (bloc.exerciseRepository.exerciseType.unit == "second") { - return Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Padding( - padding: const EdgeInsets.only(bottom: 0), - child: StreamBuilder( - stream: bloc.stopWatchTimer.rawTime, - initialData: bloc.stopWatchTimer.rawTime.value, - builder: (context, snap) { - final value = snap.data; - final displayTime = StopWatchTimer.getDisplayTime(value, hours: false); - return Column(children: [ - Padding( - padding: const EdgeInsets.all(8), - child: Text( - displayTime, - style: const TextStyle(fontSize: 40, fontFamily: 'Helvetica', fontWeight: FontWeight.bold, color: Colors.white), - ), - ), - ]); - })), - Padding( - padding: const EdgeInsets.all(2), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: IconButton( - padding: const EdgeInsets.all(2), - color: Colors.white70, - //shape: const StadiumBorder(), - onPressed: () async { - bloc.stopWatchTimer.onExecute.add(StopWatchExecute.start); - Wakelock.enable(); // prevent sleep the phone - }, - icon: Icon(CustomIcon.play_1), - iconSize: 40, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: IconButton( - padding: const EdgeInsets.all(2), - iconSize: 40, - color: Colors.white70, - //shape: const StadiumBorder(), - onPressed: () async { - bloc.stopWatchTimer.onExecute.add(StopWatchExecute.stop); - Wakelock.disable(); - }, - icon: Icon(CustomIcon.stop), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: IconButton( - padding: const EdgeInsets.all(2), - iconSize: 40, - color: Colors.white70, - onPressed: () async { - bloc.stopWatchTimer.onExecute.add(StopWatchExecute.reset); - }, - icon: Icon(CustomIcon.creative_commons_zero), - ), - ), - ], - ), - ), - ], ), - ), - Divider(), - Divider(), - Text(t("Or type the time manually:"), style: GoogleFonts.inter(color: Colors.white)), - TimePickerWidget( - onChange: (value) => {print("timer"), bloc.add(ExerciseNewQuantityChange(quantity: value))}, - ) - ]); - } - Column row = Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - TextFormField( - focusNode: _nodeText2, - controller: _controller2, - decoration: InputDecoration( - contentPadding: EdgeInsets.only(left: 25, top: 5, bottom: 5), - labelText: t(bloc.exerciseRepository.exerciseType.unit), - labelStyle: GoogleFonts.inter(fontSize: 20, color: Colors.orange[50], decorationColor: Colors.black12), - fillColor: Colors.black38, - filled: true, - border: OutlineInputBorder( - gapPadding: 8.0, - borderRadius: BorderRadius.circular(12.0), - borderSide: BorderSide(color: Colors.black26, width: 0.4), - ), - ), - //initialValue: bloc.quantity.toStringAsFixed(0), - keyboardType: TextInputType.number, - textInputAction: TextInputAction.next, - style: GoogleFonts.archivoBlack(fontSize: 80, color: Colors.orange[200]), - onChanged: (value) => {bloc.add(ExerciseNewQuantityChange(quantity: double.parse(value)))}, + child: ExerciseSave( + exerciseName: exerciseBloc.exerciseRepository.exerciseType.nameTranslation, + exerciseDescription: exerciseBloc.exerciseRepository.exerciseType.descriptionTranslation, + exerciseTask: t("Please take a relative bigger weight and repeat 12-20 times"), + unit: exerciseBloc.exerciseRepository.exerciseType.unit, + unitQuantityUnit: exerciseBloc.exerciseRepository.exerciseType.unitQuantityUnit, + hasUnitQuantity: exerciseBloc.exerciseRepository.exerciseType.unitQuantityUnit != null, + onQuantityChanged: (value) { + exerciseBloc.add(ExerciseNewQuantityChange(quantity: double.parse(value))); + }, + onUnitQuantityChanged: (value) => exerciseBloc.add(ExerciseNewQuantityUnitChange(quantity: double.parse(value))), + onSubmit: () => confirmationDialog(exerciseBloc, menuBloc), + exerciseTypeId: exerciseType.exerciseTypeId, + )), + bottomNavigationBar: BottomBarMultipleExercises( + isSet: false, + exerciseTypeId: exerciseType.exerciseTypeId, ), - ]); - - return row; + ); } void confirmationDialog(ExerciseNewBloc bloc, MenuBloc menuBloc) { - LinkedHashMap args = LinkedHashMap(); - print("quantity: " + bloc.quantity.toString()); if (bloc.exerciseRepository.exercise.quantity == null) { - print("Repository quantity modify"); - //bloc.exerciseRepository.exercise.quantity = bloc.quantity; return; } @@ -458,6 +141,9 @@ class _ExerciseNewPageState extends State with Trans, Logging { : bloc.exerciseRepository.exercise.unitQuantity.toString(); } + // ignore: close_sinks + final TestSetExecuteBloc executeBloc = BlocProvider.of(context); + showCupertinoDialog( useRootNavigator: true, context: context, @@ -491,29 +177,12 @@ class _ExerciseNewPageState extends State with Trans, Logging { bloc.exerciseRepository.setCustomer(Cache().userLoggedIn), bloc.add(ExerciseNewSubmit()), Navigator.pop(context), - Navigator.pop(context), - log("Ability " + - menuBloc.ability.toString() + - " exerciseType 1rm " + - bloc.exerciseRepository.exerciseType.is1RM().toString()), - if (bloc.exerciseRepository.exerciseType.unitQuantityUnit == null) + if (executeBloc.existsActivePlan() == true) { - args['exerciseRepository'] = bloc.exerciseRepository, - Navigator.of(context).pushNamed('evaluationPage', arguments: args) - } - else if (menuBloc.ability.equalsTo(ExerciseAbility.oneRepMax)) - { - args['exerciseRepository'] = bloc.exerciseRepository, - args['percent'] = 0.75, - args['readonly'] = false, - Navigator.of(context).pushNamed('exerciseControlPage', arguments: args) - } - else if (menuBloc.ability.equalsTo(ExerciseAbility.endurance)) - { - args['exerciseRepository'] = bloc.exerciseRepository, - args['percent'] = 0.50, - args['readonly'] = false, - Navigator.of(context).pushNamed('exerciseControlPage', arguments: args) + executeBloc.add(TestSetExecuteExerciseFinished( + exerciseTypeId: bloc.exerciseRepository.exerciseType.exerciseTypeId, + quantity: bloc.exerciseRepository.exercise.quantity, + unitQuantity: bloc.exerciseRepository.exercise.unitQuantity)), } }, ) diff --git a/lib/view/exercise_plan_custom_page.dart b/lib/view/exercise_plan_custom_page.dart index d435fd1..1ac5615 100644 --- a/lib/view/exercise_plan_custom_page.dart +++ b/lib/view/exercise_plan_custom_page.dart @@ -137,7 +137,8 @@ class _ExercisePlanCustomPage extends State with Trans { List _getChildList(List listWorkoutTree, ExercisePlanBloc bloc) { List list = List(); listWorkoutTree.forEach((element) { - final String unitQuantityUnit = element.exerciseType.unitQuantityUnit != null ? element.exerciseType.unitQuantityUnit : ""; + final String unitQuantityUnit = + element.exerciseType != null && element.exerciseType.unitQuantityUnit != null ? element.exerciseType.unitQuantityUnit : ""; list.add(TreeViewChild( startExpanded: false, parent: Card( diff --git a/lib/view/settings.dart b/lib/view/settings.dart index fdb382d..24bafab 100644 --- a/lib/view/settings.dart +++ b/lib/view/settings.dart @@ -4,6 +4,8 @@ import 'package:aitrainer_app/library/custom_icon_icons.dart'; import 'package:aitrainer_app/util/app_language.dart'; import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/util/common.dart'; +import 'package:aitrainer_app/util/enums.dart'; +import 'package:aitrainer_app/util/track.dart'; import 'package:aitrainer_app/util/trans.dart'; import 'package:aitrainer_app/widgets/app_bar_min.dart'; import 'package:aitrainer_app/widgets/bottom_nav.dart'; @@ -71,6 +73,7 @@ class SettingsPage extends StatelessWidget with Trans { }).toList(), onChanged: (String lang) => { settingsBloc.add(SettingsChangeLanguage(language: lang)), + Track().track(TrackingEvent.settings_lang, eventValue: lang) })), getServer(settingsBloc), //getDevice(settingsBloc), diff --git a/lib/view/test_set_control.dart b/lib/view/test_set_control.dart new file mode 100644 index 0000000..36460f9 --- /dev/null +++ b/lib/view/test_set_control.dart @@ -0,0 +1,194 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/bloc/test_set_control/test_set_control_bloc.dart'; +import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; +import 'package:aitrainer_app/bloc/test_set_new/test_set_new_bloc.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; + +import 'package:aitrainer_app/util/trans.dart'; +import 'package:aitrainer_app/widgets/app_bar.dart'; +import 'package:aitrainer_app/widgets/bottom_bar_multiple_exercises.dart'; +import 'package:aitrainer_app/widgets/number_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:modal_progress_hud/modal_progress_hud.dart'; + +// ignore: must_be_immutable +class TestSetControl extends StatelessWidget with Trans { + @override + Widget build(BuildContext context) { + final HashMap args = ModalRoute.of(context).settings.arguments; + final ExerciseType exerciseType = args['exerciseType']; + final ExercisePlanDetail exercisePlanDetail = args['exercisePlanDetail']; + // ignore: close_sinks + TestSetExecuteBloc executeBloc = args['testSetExecuteBloc']; + + setContext(context); + return Scaffold( + appBar: AppBarNav(depth: 1), + body: Container( + height: double.infinity, + width: double.infinity, + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + image: DecorationImage( + image: Cache().userLoggedIn.sex == "m" + ? AssetImage("asset/image/WT_Results_for_men.jpg") + : AssetImage("asset/image/WT_Results_for_female.jpg"), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: BlocProvider( + create: (context) => + TestSetControlBloc(exercisePlanDetail: exercisePlanDetail, executeBloc: executeBloc, exerciseType: exerciseType), + child: BlocConsumer(listener: (context, state) { + if (state is TestSetControlError) { + Scaffold.of(context).showSnackBar( + SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); + } + }, builder: (context, state) { + final bloc = BlocProvider.of(context); + return ModalProgressHUD( + child: getExercisForm(bloc, exercisePlanDetail), + inAsyncCall: state is TestSetNewLoading, + opacity: 0.5, + color: Colors.black54, + progressIndicator: CircularProgressIndicator(), + ); + }), + )), + bottomNavigationBar: BottomBarMultipleExercises( + isSet: executeBloc.miniTestSet == true, + exerciseTypeId: exerciseType.exerciseTypeId, + ), + ); + } + + Widget getExercisForm(TestSetControlBloc bloc, ExercisePlanDetail exercisePlanDetail) { + return Container( + padding: const EdgeInsets.only(top: 10, left: 25, right: 25), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + children: [ + Text( + exercisePlanDetail.exerciseType.nameTranslation, + style: GoogleFonts.archivoBlack( + fontWeight: FontWeight.bold, + fontSize: 24, + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(2.0, 2.0), + blurRadius: 6.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + ), + overflow: TextOverflow.fade, + textAlign: TextAlign.center, + maxLines: 2, + softWrap: true, + ), + Divider( + color: Colors.transparent, + ), + Divider(), + numberPickForm(bloc), + ], + ))); + } + + Widget numberPickForm(TestSetControlBloc bloc) { + final String strTimes = bloc.step == 2 ? bloc.initQuantity.toStringAsFixed(0) : "maximum"; + + String title = (bloc.step + 1).toString() + "/4 " + t("Control Exercise:"); + + List listWidgets = [ + Text( + title, + style: GoogleFonts.inter(color: Colors.yellow[300], fontSize: 18, fontWeight: FontWeight.bold), + ), + GestureDetector( + onTap: () => {}, + child: RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.yellow[300], + ), + children: [ + TextSpan(text: t("Please repeat with ")), + TextSpan( + text: bloc.initUnitQuantity.toStringAsFixed(0) + " " + bloc.exercisePlanDetail.exerciseType.unitQuantityUnit, + style: GoogleFonts.inter( + decoration: TextDecoration.underline, + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.yellow[100], + ), + ), + TextSpan(text: t("hu_with") + " "), + TextSpan( + text: strTimes + " ", + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.yellow[100], + )), + TextSpan( + text: t( + "times!", + )), + ]), + )), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + NumberPickerWidget( + minValue: 0, + maxValue: 200, + initalValue: bloc.initQuantity.round(), + unit: t("reps"), + color: Colors.yellow[50], + onChange: (value) => {bloc.add(TestSetControlQuantityChange(quantity: value.toDouble()))}), + FlatButton( + padding: EdgeInsets.all(0), + textColor: Colors.white, + focusColor: Colors.blueAccent, + onPressed: () => { + bloc.add(TestSetControlSubmit()), + { + Navigator.of(context).pop(), + } + }, + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset('asset/icon/gomb_orange_c.png', width: 140, height: 60), + Text( + t("Save"), + style: TextStyle(fontSize: 16, color: Colors.white), + ), + ], + )), + ], + ), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: listWidgets, + ); + } +} diff --git a/lib/view/test_set_edit.dart b/lib/view/test_set_edit.dart index 61a4442..3dd0dd5 100644 --- a/lib/view/test_set_edit.dart +++ b/lib/view/test_set_edit.dart @@ -1,32 +1,36 @@ -import 'dart:convert'; +import 'dart:collection'; import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; import 'package:aitrainer_app/bloc/test_set_edit/test_set_edit_bloc.dart'; import 'package:aitrainer_app/library/custom_icon_icons.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; import 'package:aitrainer_app/model/workout_menu_tree.dart'; import 'package:aitrainer_app/util/trans.dart'; import 'package:aitrainer_app/widgets/app_bar.dart'; import 'package:aitrainer_app/widgets/dialog_common.dart'; +import 'package:aitrainer_app/widgets/menu_image.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:modal_progress_hud/modal_progress_hud.dart'; -import 'package:transparent_image/transparent_image.dart'; -import 'package:aitrainer_app/library/image_cache.dart' as wt; // ignore: must_be_immutable class TestSetEdit extends StatelessWidget with Trans { @override Widget build(BuildContext context) { - final String templateName = ModalRoute.of(context).settings.arguments; + HashMap args = ModalRoute.of(context).settings.arguments; + final String templateName = args['templateName']; + final String templateNameTranslation = args['templateNameTranslation']; // ignore: close_sinks final MenuBloc menuBloc = BlocProvider.of(context); TestSetEditBloc bloc; setContext(context); return Scaffold( - appBar: AppBarNav(depth: 1), + appBar: AppBarNav( + depth: 0, + ), body: Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( @@ -37,19 +41,23 @@ class TestSetEdit extends StatelessWidget with Trans { ), ), child: BlocProvider( - create: (context) => - TestSetEditBloc(templateName: templateName, workoutTreeRepository: menuBloc.menuTreeRepository, menuBloc: menuBloc), + create: (context) => TestSetEditBloc( + templateName: templateName, + templateNameTranslation: templateNameTranslation, + workoutTreeRepository: menuBloc.menuTreeRepository, + menuBloc: menuBloc), child: BlocConsumer(listener: (context, state) { if (state is TestSetEditError) { Scaffold.of(context).showSnackBar( SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); } else if (state is TestSetEditSaved) { - Navigator.of(context).pushNamed("home"); + Navigator.of(context).pop(); + Navigator.of(context).pushNamed("testSetExecute"); } }, builder: (context, state) { bloc = BlocProvider.of(context); return ModalProgressHUD( - child: getTestSetWidget(bloc, templateName), + child: getTestSetWidget(bloc, templateNameTranslation), inAsyncCall: state is TestSetEditLoading, opacity: 0.5, color: Colors.black54, @@ -63,7 +71,7 @@ class TestSetEdit extends StatelessWidget with Trans { builder: (BuildContext context) { return DialogCommon( title: "Start!", - descriptions: "GO", + descriptions: t("Enjoy the exercises, good luck with the testing!"), text: "OK", onTap: () => { Navigator.of(context).pop(), @@ -78,7 +86,7 @@ class TestSetEdit extends StatelessWidget with Trans { backgroundColor: Colors.orange[800], icon: Icon(CustomIcon.clock), label: Text( - "Start training", + t("Start training"), style: GoogleFonts.inter(fontWeight: FontWeight.bold, fontSize: 16), ), ), @@ -147,42 +155,70 @@ class TestSetEdit extends StatelessWidget with Trans { ])); } - List imageSliders(List alternatives, MenuBloc menuBloc) { + List imageSliders(List alternatives, MenuBloc menuBloc, WorkoutMenuTree workoutTree, TestSetEditBloc bloc) { final List list = List(); - alternatives.forEach((element) { - list.add(getImageStack(element, menuBloc)); - }); - list.add(Container( - padding: EdgeInsets.only(top: 25, bottom: 25), - child: ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: Container( - color: Colors.red[600], - child: Center( - child: Text( - "X", - style: GoogleFonts.archivoBlack( - color: Colors.white, - fontSize: 80, + if (bloc.exercisePlanDetails[workoutTree.exerciseTypeId] == null) { + list.add(Container( + padding: EdgeInsets.only(top: 25, bottom: 25), + child: ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: Container( + color: Colors.red[600], + child: Center( + child: Text( + "X", + style: GoogleFonts.archivoBlack( + color: Colors.white, + fontSize: 80, + ), ), - ), - ))))); + ))))); + list.add(getImageStack(workoutTree, menuBloc, bloc)); + } else { + ExerciseType exerciseType = bloc.exercisePlanDetails[workoutTree.exerciseTypeId]; + + final WorkoutMenuTree actualWorkoutTree = bloc.menuBloc.menuTreeRepository.getMenuItemByExerciseTypeId(exerciseType.exerciseTypeId); + list.add(getImageStack(actualWorkoutTree, menuBloc, bloc)); + } + + alternatives.forEach((element) { + list.add(getImageStack(element, menuBloc, bloc)); + }); return list; } - Stack getImageStack(WorkoutMenuTree element, MenuBloc menuBloc) { - print(element.toJson()); - return Stack(alignment: Alignment.bottomLeft, children: [ - _getButtonImage(element, menuBloc), - Container( - padding: EdgeInsets.only(left: 15, bottom: 15, right: 15), - child: Text( - element.name, - maxLines: 4, - style: GoogleFonts.archivoBlack(color: element.color, fontSize: 16, height: 1.1), + Stack getImageStack(WorkoutMenuTree element, MenuBloc menuBloc, TestSetEditBloc bloc) { + return Stack(alignment: Alignment.topRight, children: [ + Stack(alignment: Alignment.bottomLeft, children: [ + MenuImage(imageName: element.imageName, workoutTreeId: element.id), + Container( + padding: EdgeInsets.only(left: 15, bottom: 15, right: 15), + child: Text( + element.name, + maxLines: 4, + style: GoogleFonts.archivoBlack(color: element.color, fontSize: 16, height: 1.1), + ), ), - ), + ]), + Container( + width: 40, + height: 40, + child: ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: GestureDetector( + onTap: () => bloc.add(TestSetEditDeleteExerciseType(exerciseTypeId: element.exerciseTypeId)), + child: Container( + color: Colors.red[600], + child: Center( + child: Text( + "X", + style: GoogleFonts.archivoBlack( + color: Colors.white, + fontSize: 28, + ), + ), + ))))) ]); } @@ -204,54 +240,11 @@ class TestSetEdit extends StatelessWidget with Trans { onPageChanged: (index, reason) => bloc.add(TestSetEditChangeExerciseType(index: index, exerciseTypeId: element.exerciseTypeId)), enlargeStrategy: CenterPageEnlargeStrategy.scale), - items: imageSliders(alternativeMenuItems, bloc.menuBloc), + items: imageSliders(alternativeMenuItems, bloc.menuBloc, workoutTree, bloc), ); widgets.add(widget); } }); return widgets; } - - Widget _getButtonImage(WorkoutMenuTree workoutTree, MenuBloc menuBloc) { - if (workoutTree == null) { - return Offstage(); - } - String imageString = menuBloc.getImage(workoutTree.id, workoutTree.imageName); - Widget widget; - if (imageString != null) { - widget = ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: Container( - color: Colors.transparent, - child: FadeInImage( - fadeInDuration: Duration(milliseconds: 100), - image: MemoryImage(base64Decode(imageString)), - placeholder: MemoryImage(kTransparentImage), - ), - )); - } else { - if (workoutTree.imageName.contains("https")) { - if (!wt.ImageCache().existsImageInMap(workoutTree.id, workoutTree.imageName)) { - widget = ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: Container( - color: Colors.transparent, - child: FadeInImage( - fadeInDuration: Duration(milliseconds: 500), - image: NetworkImage(workoutTree.imageName), - placeholder: MemoryImage(kTransparentImage), - ), - )); - } - } else { - widget = ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: Container( - color: Colors.transparent, - child: Image.asset(workoutTree.imageName), - )); - } - } - return widget; - } } diff --git a/lib/view/test_set_execute.dart b/lib/view/test_set_execute.dart new file mode 100644 index 0000000..7c3a310 --- /dev/null +++ b/lib/view/test_set_execute.dart @@ -0,0 +1,473 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; +import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; +import 'package:aitrainer_app/library/custom_icon_icons.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/util/trans.dart'; +import 'package:aitrainer_app/widgets/app_bar.dart'; +import 'package:aitrainer_app/widgets/dialog_common.dart'; +import 'package:aitrainer_app/widgets/menu_image.dart'; +import 'package:aitrainer_app/widgets/victory_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:modal_progress_hud/modal_progress_hud.dart'; +import 'package:timeline_tile/timeline_tile.dart'; + +// ignore: must_be_immutable +class TestSetExecute extends StatelessWidget with Trans { + @override + Widget build(BuildContext context) { + // ignore: close_sinks + final MenuBloc menuBloc = BlocProvider.of(context); + // ignore: close_sinks + TestSetExecuteBloc executeBloc = BlocProvider.of(context); + executeBloc.menuBloc = menuBloc; + + executeBloc.add(TestSetExecuteLoad()); + setContext(context); + return Scaffold( + appBar: AppBarNav(depth: 1), + body: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_black_background.jpg'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: BlocConsumer(listener: (context, state) { + if (state is TestSetExecuteError) { + Scaffold.of(context).showSnackBar( + SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); + } else if (state is TestSetExecuteFinished) { + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return Victory( + victory: true, + ); + }); + } + }, builder: (context, state) { + executeBloc = BlocProvider.of(context); + return ModalProgressHUD( + child: getExercises(executeBloc, context), + inAsyncCall: state is TestSetExecuteLoading, + opacity: 0.5, + color: Colors.black54, + progressIndicator: CircularProgressIndicator(), + ); + }), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => executeExercise(executeBloc, executeBloc.getNext(), context), + backgroundColor: Colors.orange[800], + icon: Icon(CustomIcon.weight_hanging), + label: Text( + t("Next"), + style: GoogleFonts.inter(fontWeight: FontWeight.bold, fontSize: 16), + ), + ), + ); + } + + Widget getExercises(TestSetExecuteBloc bloc, BuildContext context) { + return CustomScrollView(slivers: [ + SliverList(delegate: SliverChildListDelegate(getTiles(bloc, context))), + ]); + } + + List getTiles(TestSetExecuteBloc bloc, BuildContext context) { + List tiles = List(); + tiles.add(getStartTile(bloc)); + tiles.addAll(getExerciseTiles(bloc, context)); + tiles.add(getEndTile()); + /* if (bloc.isDone100Percent()) { + tiles.add(Victory( + victory: true, + )); + } */ + return tiles; + } + + Widget getStartTile(TestSetExecuteBloc bloc) { + return TimelineTile( + alignment: TimelineAlign.manual, + lineXY: 0.1, + isFirst: true, + afterLineStyle: const LineStyle( + color: Colors.orange, + thickness: 6, + ), + indicatorStyle: IndicatorStyle( + width: 40, + color: Colors.orange, + padding: const EdgeInsets.all(8), + iconStyle: IconStyle( + color: Colors.white, + iconData: Icons.insert_emoticon, + ), + ), + endChild: Container( + padding: EdgeInsets.only(top: 30), + constraints: const BoxConstraints( + minHeight: 120, + ), + color: Colors.transparent, + child: RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + children: [ + TextSpan( + text: bloc.isFirst() ? t("Start") : t("Continue"), + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.yellow[400], + shadows: [ + Shadow( + offset: Offset(5.0, 5.0), + blurRadius: 12.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + )), + TextSpan( + text: t(" your ") + t(bloc.testType), + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.yellow[400], + shadows: [ + Shadow( + offset: Offset(5.0, 5.0), + blurRadius: 12.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + )), + TextSpan( + text: "\n", + style: GoogleFonts.inter( + fontSize: 16, + color: Colors.white, + )), + TextSpan( + text: bloc.testName == null ? "" : bloc.testName, + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + )), + TextSpan( + text: t("\nyour plan is available for 24 hours"), + style: GoogleFonts.inter( + fontSize: 16, + color: Colors.white, + )) + ])), + ), + ); + } + + Widget getEndTile() { + return Container( + color: Colors.transparent, + child: TimelineTile( + alignment: TimelineAlign.manual, + lineXY: 0.1, + isLast: true, + beforeLineStyle: const LineStyle( + color: Colors.orange, + thickness: 6, + ), + indicatorStyle: IndicatorStyle( + width: 40, + color: Colors.orange, + padding: const EdgeInsets.all(8), + iconStyle: IconStyle( + color: Colors.white, + iconData: Icons.thumb_up, + ), + ), + endChild: Container( + padding: EdgeInsets.only(top: 50), + constraints: const BoxConstraints( + minHeight: 120, + ), + color: Colors.transparent, + child: RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + children: [ + TextSpan( + text: "Finish!", + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.yellow[400], + shadows: [ + Shadow( + offset: Offset(5.0, 5.0), + blurRadius: 12.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + )), + ])), + ), + ), + ); + } + + List getExerciseTiles(TestSetExecuteBloc bloc, BuildContext context) { + List tiles = List(); + if (bloc.exercisePlanDetails != null) { + bloc.exercisePlanDetails.forEach((element) { + if (element != null && element.exerciseTypeId != null) { + tiles.add(GestureDetector( + onDoubleTap: () => print("Execute ${element.exerciseType.nameTranslation}"), + onTap: () => executeExercise(bloc, element, context), + child: ExerciseTile( + bloc: bloc, + exercisePlanDetail: element, + ))); + } + }); + } + + return tiles; + } + + void executeExercise(TestSetExecuteBloc bloc, ExercisePlanDetail exercisePlanDetail, BuildContext context) { + ExercisePlanDetail next = bloc.getNext(); + print("Detail: $next"); + if (next != null) { + final HashMap args = HashMap(); + args['exerciseType'] = exercisePlanDetail.exerciseType; + args['exercisePlanDetailId'] = exercisePlanDetail.exercisePlanDetailId; + args['testSetExecuteBloc'] = bloc; + String title = ""; + String description = ""; + String description2 = ""; + if (next.exerciseTypeId != exercisePlanDetail.exerciseTypeId) { + title = t("Stop!"); + description = t("Please continue with the next exercise in the queue:") + next.exerciseType.nameTranslation; + description2 = t("Or, you can redifine this exercise queue in the Compact Test menu"); + } else { + if (exercisePlanDetail.state.equalsTo(ExercisePlanDetailState.inProgress)) { + final HashMap args = HashMap(); + args['exerciseType'] = exercisePlanDetail.exerciseType; + args['exercisePlanDetail'] = exercisePlanDetail; + args['testSetExecuteBloc'] = bloc; + Navigator.of(context).pushNamed('testSetControl', arguments: args); + } else { + Navigator.of(context).pushNamed('testSetNew', arguments: args); + } + return; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return DialogCommon( + title: title, + descriptions: description, + description2: description2, + text: "OK", + onTap: () => {Navigator.of(context).pop()}, + onCancel: () => {Navigator.of(context).pop()}, + ); + }); + } else { + Navigator.of(context).pushNamed('home'); + } + } +} + +// ignore: must_be_immutable +class ExerciseTile extends StatelessWidget with Trans { + final TestSetExecuteBloc bloc; + final ExercisePlanDetail exercisePlanDetail; + + ExerciseTile({this.bloc, this.exercisePlanDetail}); + + Widget getIndicator(ExercisePlanDetailState state) { + ExercisePlanDetail next = bloc.getNext(); + bool actual = false; + if (next != null) { + if (next.exerciseTypeId == exercisePlanDetail.exerciseTypeId) { + actual = true; + } + } + if (state.equalsTo(ExercisePlanDetailState.inProgress)) { + return ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: Container( + color: actual ? Colors.green : Colors.orange, + child: Icon( + CustomIcon.calendar_2, + size: 28, + color: Colors.white, + ))); + } else if (state.equalsTo(ExercisePlanDetailState.finished)) { + return ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: Container( + color: Colors.white, + child: Icon( + CustomIcon.ok_circled, + size: 40, + color: Colors.green, + ))); + } else { + return Image.asset( + "asset/image/pict_reps_volumen_db.png", + ); + } + } + + @override + Widget build(BuildContext context) { + final ExercisePlanDetailState state = exercisePlanDetail.state; + final bool done = state.equalsTo(ExercisePlanDetailState.finished); + final String countSerie = exercisePlanDetail.exercises == null ? "1" : (exercisePlanDetail.exercises.length).toString(); + final String serie = exercisePlanDetail.exerciseType.unitQuantityUnit == null ? "/1" : "/4"; + setContext(context); + return Container( + color: Colors.transparent, + child: TimelineTile( + alignment: TimelineAlign.manual, + lineXY: 0.1, + beforeLineStyle: const LineStyle( + color: Color(0xffb4f500), + thickness: 6, + ), + afterLineStyle: const LineStyle( + color: Color(0xffb4f500), + thickness: 6, + ), + indicatorStyle: IndicatorStyle( + width: 40, + height: 40, + indicator: getIndicator(state), + ), + endChild: Container( + padding: EdgeInsets.only(left: 10), + child: Row(children: [ + Container( + width: 120, + height: 80, + child: MenuImage( + imageName: bloc.getActualImageName(exercisePlanDetail.exerciseType.exerciseTypeId), + workoutTreeId: bloc.getActualWorkoutTreeId(exercisePlanDetail.exerciseType.exerciseTypeId), + )), + SizedBox( + width: 10, + ), + Expanded( + child: RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.bold, + color: done ? Colors.grey[400] : Colors.white, + ), + children: [ + TextSpan( + text: exercisePlanDetail.exerciseType.nameTranslation, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.bold, + color: done ? Colors.grey[400] : Colors.orange[500], + shadows: [ + Shadow( + offset: Offset(5.0, 5.0), + blurRadius: 12.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + )), + exercisePlanDetail.exerciseType.unitQuantityUnit != null + ? TextSpan( + text: "\n", + ) + : TextSpan(), + exercisePlanDetail.exerciseType.unitQuantityUnit != null + ? TextSpan( + text: t(exercisePlanDetail.exerciseType.unitQuantityUnit) + ": ", + style: GoogleFonts.inter( + fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)) + : TextSpan(), + exercisePlanDetail.exerciseType.unitQuantityUnit != null + ? TextSpan( + text: t(bloc.getExerciseWeight(exercisePlanDetail)), + style: GoogleFonts.inter( + fontSize: 12, + )) + : TextSpan(), + TextSpan( + text: "\n", + ), + TextSpan( + text: t(exercisePlanDetail.exerciseType.unit) + ": ", + style: GoogleFonts.inter( + fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)), + TextSpan( + text: bloc.repeatTimesText(exercisePlanDetail), + style: GoogleFonts.inter( + fontSize: 12, + )), + TextSpan( + text: "\n", + ), + TextSpan( + text: t("Set") + ": ", + style: GoogleFonts.inter( + fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)), + TextSpan( + text: countSerie + serie, + style: GoogleFonts.inter( + fontSize: 12, + )), + ]), + )), + ]), + ), + ), + ); + } +} diff --git a/lib/view/test_set_new.dart b/lib/view/test_set_new.dart new file mode 100644 index 0000000..e652dcc --- /dev/null +++ b/lib/view/test_set_new.dart @@ -0,0 +1,95 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; +import 'package:aitrainer_app/bloc/test_set_new/test_set_new_bloc.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/model/exercise_type.dart'; +import 'package:aitrainer_app/repository/exercise_repository.dart'; +import 'package:aitrainer_app/util/trans.dart'; +import 'package:aitrainer_app/widgets/app_bar.dart'; +import 'package:aitrainer_app/widgets/bottom_bar_multiple_exercises.dart'; +import 'package:aitrainer_app/widgets/exercise_save.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:modal_progress_hud/modal_progress_hud.dart'; + +// ignore: must_be_immutable +class TestSetNew extends StatelessWidget with Trans { + @override + Widget build(BuildContext context) { + final HashMap args = ModalRoute.of(context).settings.arguments; + final ExerciseType exerciseType = args['exerciseType']; + final int exercisePlanDetailId = args['exercisePlanDetailId']; + // ignore: close_sinks + final TestSetExecuteBloc executeBloc = args['testSetExecuteBloc']; + TestSetNewBloc bloc; + setContext(context); + return Scaffold( + appBar: AppBarNav(depth: 1), + body: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_black_background.jpg'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: BlocProvider( + create: (context) => TestSetNewBloc( + exerciseRepository: ExerciseRepository(), + exerciseType: exerciseType, + exercisePlanDetailId: exercisePlanDetailId, + executeBloc: executeBloc), + child: BlocConsumer(listener: (context, state) { + if (state is TestSetNewError) { + Scaffold.of(context).showSnackBar( + SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); + } else if (state is TestSetNewReady) { + print("Actual state: ${executeBloc.actualState(exerciseType.exerciseTypeId).toString()}"); + if (executeBloc.actualState(exerciseType.exerciseTypeId).equalsTo(ExercisePlanDetailState.inProgress)) { + HashMap args = HashMap(); + final ExercisePlanDetail actualExercisePlanDetail = executeBloc.actualExercisePlanDetail(exerciseType.exerciseTypeId); + args['exerciseType'] = exerciseType; + args['exercisePlanDetail'] = actualExercisePlanDetail; + args['testSetExecuteBloc'] = executeBloc; + Navigator.of(context).pushNamed("testSetControl", arguments: args); + } + } + }, builder: (context, state) { + bloc = BlocProvider.of(context); + return ModalProgressHUD( + child: getExercises(bloc), + inAsyncCall: state is TestSetNewLoading, + opacity: 0.5, + color: Colors.black54, + progressIndicator: CircularProgressIndicator(), + ); + }), + )), + bottomNavigationBar: BottomBarMultipleExercises( + isSet: executeBloc.miniTestSet == true, + exerciseTypeId: exerciseType.exerciseTypeId, + ), + ); + } + + Widget getExercises(TestSetNewBloc bloc) { + return ExerciseSave( + exerciseName: bloc.exerciseType.nameTranslation, + exerciseDescription: bloc.exerciseType.descriptionTranslation, + exerciseTask: t("Please take a relative bigger weight and repeat 12-20 times"), + unit: bloc.exerciseType.unit, + unitQuantityUnit: bloc.exerciseType.unitQuantityUnit, + hasUnitQuantity: bloc.exerciseType.unitQuantityUnit != null, + onQuantityChanged: (value) { + bloc.add(TestSetNewChangeQuantity(quantity: double.parse(value))); + }, + onUnitQuantityChanged: (value) => bloc.add(TestSetNewChangeQuantityUnit(quantity: double.parse(value))), + exerciseTypeId: bloc.exerciseType.exerciseTypeId, + onSubmit: () { + Navigator.of(context).pop(); + bloc.add(TestSetNewSubmit()); + }); + } +} diff --git a/lib/widgets/bmr_widget.dart b/lib/widgets/bmr_widget.dart index 5ecb64e..38acc1e 100644 --- a/lib/widgets/bmr_widget.dart +++ b/lib/widgets/bmr_widget.dart @@ -1,4 +1,5 @@ import 'package:aitrainer_app/bloc/exercise_new/exercise_new_bloc.dart'; +import 'package:aitrainer_app/library/dropdown_search.dart'; import 'package:aitrainer_app/util/app_localization.dart'; import 'package:aitrainer_app/model/fitness_state.dart'; import 'package:aitrainer_app/util/trans.dart'; @@ -8,7 +9,6 @@ import 'package:flutter/widgets.dart'; import 'package:google_fonts/google_fonts.dart'; import 'app_bar.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; -import 'package:dropdown_search/dropdown_search.dart'; // ignore: must_be_immutable class BMR extends StatefulWidget { diff --git a/lib/widgets/bottom_bar_multiple_exercises.dart b/lib/widgets/bottom_bar_multiple_exercises.dart new file mode 100644 index 0000000..65300f5 --- /dev/null +++ b/lib/widgets/bottom_bar_multiple_exercises.dart @@ -0,0 +1,292 @@ +import 'dart:collection'; + +import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; +import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; +import 'package:aitrainer_app/model/exercise_plan_detail.dart'; +import 'package:aitrainer_app/model/workout_menu_tree.dart'; +import 'package:aitrainer_app/util/enums.dart'; +import 'package:aitrainer_app/util/track.dart'; +import 'package:aitrainer_app/util/trans.dart'; +import 'package:aitrainer_app/widgets/dialog_widget.dart'; +import 'package:aitrainer_app/widgets/menu_search_bar.dart'; +import 'package:aitrainer_app/widgets/victory_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:modal_progress_hud/modal_progress_hud.dart'; +import 'package:flutter/cupertino.dart'; + +// ignore: must_be_immutable +class BottomBarMultipleExercises extends StatefulWidget { + bool isSet = false; + final List details; + int exerciseTypeId; + + BottomBarMultipleExercises({this.details, this.isSet, this.exerciseTypeId}); + @override + _BottomBarMultipleExercisesState createState() => _BottomBarMultipleExercisesState(); +} + +class _BottomBarMultipleExercisesState extends State with Trans { + final Color bgrColor = Color(0xffb4f500); + final Color bgrColorEnd = Colors.blue; + final Color active = Colors.black; + final Color inactive = Colors.black26; + ScrollController _controller; + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.addPostFrameCallback((_) { + _controller = ScrollController(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + setContext(context); + // ignore: close_sinks + final MenuBloc menuBloc = BlocProvider.of(context); + // ignore: close_sinks + final TestSetExecuteBloc bloc = BlocProvider.of(context); + bloc.menuBloc = menuBloc; + bloc.setExerciseTypeId(widget.exerciseTypeId); + bloc.add(TestSetExecuteLoad()); + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + stops: [0.1, 0.99], + colors: [ + bgrColor, + bgrColorEnd, + ], + ), + ), + height: 90, + child: BlocConsumer(listener: (context, state) { + if (state is TestSetExecuteError) { + Scaffold.of(context) + .showSnackBar(SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); + } + }, builder: (context, state) { + if (state is TestSetExecuteReady && bloc.exercisePlanDetails != null) { + print("BottomBarMulti offset ${bloc.scrollOffset}"); + /* if (bloc.scrollOffset > 0 && _controller != null) { + _controller.animateTo(bloc.scrollOffset, duration: Duration(milliseconds: 300), curve: Curves.easeIn); + } */ + } + return ModalProgressHUD( + child: getWidget(bloc), + inAsyncCall: state is TestSetExecuteLoading, + opacity: 0.5, + color: Colors.black54, + progressIndicator: CircularProgressIndicator(), + ); + }), + ); + } + + Widget getWidget(TestSetExecuteBloc bloc) { + return SingleChildScrollView( + controller: _controller, + padding: EdgeInsets.only(left: 10, right: 10, bottom: 5), + scrollDirection: Axis.horizontal, + child: Row( + children: getChildren(bloc), + ), + ); + } + + List getChildren(TestSetExecuteBloc bloc) { + final List list = List(); + if (!widget.isSet && !bloc.hasBegun()) { + list.add(GestureDetector( + onTap: () => newExercise(bloc), + child: ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: Container( + padding: EdgeInsets.only(top: 5), + height: 60, + color: Colors.transparent, + child: Image.asset("asset/image/add_test.png"), + )))); + list.add( + SizedBox( + width: 10, + ), + ); + if (bloc.isDone100) { + Victory(); + } + } + + //if (bloc.miniTestSet && widget.isSet || bloc.paralellTest && !widget.isSet) { + if (bloc.exercisePlanDetails != null) { + bloc.exercisePlanDetails.forEach((element) { + final bool highlighted = element.exerciseTypeId == widget.exerciseTypeId; + list.add(getImageStack(element.exerciseType.nameTranslation, bloc, element.exerciseTypeId, + bloc.getActualImageName(element.exerciseType.exerciseTypeId), + highlighted: highlighted)); + }); + // } + } + + return list; + } + + void newExercise(TestSetExecuteBloc bloc) { + HashMap ret = bloc.canAddNewExercise(); + if (ret['canAdd'] == false) { + showCupertinoDialog( + useRootNavigator: true, + context: context, + builder: (_) => CupertinoAlertDialog( + insetAnimationDuration: Duration(milliseconds: 500), + insetAnimationCurve: Curves.elasticInOut, + title: ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: Container( + color: Colors.transparent, + child: Text( + t("You are about to add a new parallel test"), + style: GoogleFonts.inter(color: Colors.grey[900]), + ))), + content: ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: Container( + color: Colors.transparent, + child: Column(children: [ + Divider(), + Text( + t(ret['message']), + style: GoogleFonts.inter(color: Colors.grey[800]), + ), + Divider( + color: Colors.transparent, + ), + Text( + ret['message2'], + style: GoogleFonts.inter(color: Colors.grey[800]), + ), + ]))), + actions: [ + FlatButton( + child: Text(t("No")), + onPressed: () => Navigator.pop(context), + ), + FlatButton( + child: Text(t("Yes")), + onPressed: () => { + Navigator.pop(context), + addNewExercise(bloc), + }, + ) + ], + )); + } else { + addNewExercise(bloc); + } + } + + void addNewExercise(TestSetExecuteBloc bloc) { + WorkoutMenuTree foundMenuItem; + showDialog( + context: context, + builder: (BuildContext context) { + return DialogWidget( + onTap: () => { + if (foundMenuItem != null) + { + bloc.add(TestSetExecuteDeleteActive()), + bloc.add(TestSetExecuteNewExercise(exerciseTypeId: widget.exerciseTypeId)), + bloc.add(TestSetExecuteNewExercise(exerciseTypeId: foundMenuItem.exerciseTypeId)), + Track().track(TrackingEvent.exercise_new_paralell, eventValue: foundMenuItem.exerciseType.name), + }, + Navigator.pop(context), + }, + title: t("Please select an exercise"), + description: t("Add this exercise to execute it paralell"), + widget: MenuSearchBar( + showIcon: false, + onFind: (workoutMenuTree) => foundMenuItem = workoutMenuTree, + listItems: bloc.menuBloc.menuTreeRepository.menuAsExercise, + ), + ); + }); + } + + List imageSliders(TestSetExecuteBloc bloc) { + List list = List(); + final Widget widget = ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: GestureDetector( + onTap: () => newExercise(bloc), + child: Container( + color: Colors.transparent, + child: Image.asset("asset/image/add_test.png"), + ))); + list.add(widget); + return list; + } + + Widget getImageStack(String imageName, TestSetExecuteBloc bloc, int exerciseTypeId, String image, {highlighted = false}) { + return Container( + width: 120, + child: Stack(alignment: Alignment.bottomLeft, fit: StackFit.loose, children: [ + Container( + padding: EdgeInsets.only(left: 0, top: 10), + child: Stack(alignment: Alignment.topRight, children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: Container( + decoration: highlighted + ? BoxDecoration( + border: Border.all( + color: Colors.white, + width: 3, // + ), + ) + : BoxDecoration( + border: Border.all( + color: Colors.transparent, + width: 1, // + ), + ), + height: 60, + //color: Colors.transparent, + child: Image.asset(image), + )), + !widget.isSet && !bloc.hasBegun() + ? Positioned( + top: -8, + child: GestureDetector( + onTap: () => { + print("Delete: $imageName"), + bloc.add(TestSetExecuteDeleteExercise(exerciseTypeId: exerciseTypeId)), + }, + child: Text( + "X", + style: GoogleFonts.archivoBlack(color: Colors.orange[900], fontSize: 20), + ))) + : Offstage(), + ])), + Container( + padding: EdgeInsets.only(left: 10, bottom: 5, right: 5), + child: Text( + imageName, + maxLines: 2, + style: GoogleFonts.archivoBlack(color: Color(0xffb4f500), fontSize: 10), + ), + ), + ])); + } +} diff --git a/lib/widgets/bottom_nav.dart b/lib/widgets/bottom_nav.dart index 9321fb0..a533d68 100644 --- a/lib/widgets/bottom_nav.dart +++ b/lib/widgets/bottom_nav.dart @@ -1,3 +1,4 @@ +import 'package:aitrainer_app/library/gradient_bottom_navigation_bar.dart'; import 'package:aitrainer_app/util/app_localization.dart'; import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/service/logging.dart'; @@ -6,7 +7,6 @@ import 'package:aitrainer_app/util/enums.dart'; import 'package:aitrainer_app/util/track.dart'; import 'package:aitrainer_app/util/trans.dart'; import 'package:flutter/material.dart'; -import 'package:gradient_bottom_navigation_bar/gradient_bottom_navigation_bar.dart'; // ignore: must_be_immutable class BottomNavigator extends StatefulWidget { diff --git a/lib/widgets/dialog_common.dart b/lib/widgets/dialog_common.dart index 12f0077..c72a6ad 100644 --- a/lib/widgets/dialog_common.dart +++ b/lib/widgets/dialog_common.dart @@ -65,8 +65,9 @@ class _DialogPremiumState extends State with Trans { children: [ Text( widget.title + " ", + textAlign: TextAlign.center, style: GoogleFonts.archivoBlack( - fontSize: 18, + fontSize: 20, color: Colors.yellow[400], shadows: [ Shadow( @@ -90,7 +91,8 @@ class _DialogPremiumState extends State with Trans { Text( widget.descriptions, style: GoogleFonts.inter( - fontSize: 14, + fontSize: 16, + fontWeight: FontWeight.bold, color: Colors.white, shadows: [ Shadow( @@ -113,7 +115,7 @@ class _DialogPremiumState extends State with Trans { Text( widget.description2, style: GoogleFonts.inter( - fontSize: 14, + fontSize: 16, color: Colors.white, shadows: [ Shadow( @@ -136,7 +138,7 @@ class _DialogPremiumState extends State with Trans { Text( widget.description3, style: GoogleFonts.inter( - fontSize: 14, + fontSize: 16, color: Colors.white, shadows: [ Shadow( diff --git a/lib/widgets/dialog_widget.dart b/lib/widgets/dialog_widget.dart new file mode 100644 index 0000000..d02f6f6 --- /dev/null +++ b/lib/widgets/dialog_widget.dart @@ -0,0 +1,154 @@ +import 'package:aitrainer_app/util/trans.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +// ignore: must_be_immutable +class DialogWidget extends StatefulWidget { + final String title, description; + final Widget widget; + final VoidCallback onTap; + final VoidCallback onCancel; + + DialogWidget({Key key, this.title, this.description, this.widget, this.onTap, this.onCancel}) : super(key: key); + + @override + _DialogPremiumState createState() { + return _DialogPremiumState(); + } +} + +class _DialogPremiumState extends State with Trans { + @override + Widget build(BuildContext context) { + setContext(context); + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(31), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: contentBox(context), + ); + } + + contentBox(context) { + return Stack(alignment: AlignmentDirectional.topStart, children: [ + Stack( + children: [ + Container( + padding: EdgeInsets.only(left: 20, top: 24, right: 20, bottom: 30), + margin: EdgeInsets.only(top: 30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [BoxShadow(color: Colors.black, offset: Offset(0, 10), blurRadius: 10)], + image: DecorationImage( + image: AssetImage('asset/image/WT_black_G_background.jpg'), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 5, + ), + Stack( + alignment: AlignmentDirectional.topEnd, + children: [ + Text( + widget.title, + textAlign: TextAlign.center, + style: GoogleFonts.archivoBlack( + fontSize: 20, + color: Colors.yellow[400], + shadows: [ + Shadow( + offset: Offset(5.0, 5.0), + blurRadius: 12.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + ), + ), + ], + ), + SizedBox( + height: 35, + ), + Text( + widget.description, + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(5.0, 5.0), + blurRadius: 12.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + ), + textAlign: TextAlign.center, + ), + SizedBox( + height: 15, + ), + widget.widget, + SizedBox( + height: 52, + ), + Align( + alignment: Alignment.center, + child: GestureDetector( + onTap: widget.onTap ?? widget.onTap, + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset('asset/icon/gomb_orange_c.png', width: 100, height: 45), + Text( + t("OK"), + style: TextStyle(fontSize: 16, color: Colors.white), + ), + ], + ))), + ], + ), + ), + ], + ), + GestureDetector( + onTap: () { + if (widget.onCancel == null) { + Navigator.of(context).pop(); + } else { + widget.onCancel(); + } + }, + child: CircleAvatar( + backgroundColor: Colors.transparent, + radius: 28, + child: Text( + "X", + style: GoogleFonts.archivoBlack(fontSize: 32, color: Colors.white54), + ), + )), + ]); + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/lib/widgets/exercise_save.dart b/lib/widgets/exercise_save.dart new file mode 100644 index 0000000..944d366 --- /dev/null +++ b/lib/widgets/exercise_save.dart @@ -0,0 +1,394 @@ +import 'package:aitrainer_app/library/custom_icon_icons.dart'; +import 'package:aitrainer_app/util/trans.dart'; +import 'package:aitrainer_app/widgets/time_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:keyboard_actions/keyboard_actions_config.dart'; +import 'package:stop_watch_timer/stop_watch_timer.dart'; +import 'package:wakelock/wakelock.dart'; + +import 'dialog_html.dart'; + +// ignore: must_be_immutable +class ExerciseSave extends StatefulWidget { + final ValueChanged onQuantityChanged; + final ValueChanged onUnitQuantityChanged; + final VoidCallback onSubmit; + final bool hasUnitQuantity; + final String unitQuantityUnit; + final String unit; + final String exerciseName; + final String exerciseDescription; + final String exerciseTask; + final int exerciseTypeId; + + ExerciseSave( + {this.onQuantityChanged, + this.onUnitQuantityChanged, + this.onSubmit, + this.hasUnitQuantity, + this.unitQuantityUnit, + this.unit, + this.exerciseName, + this.exerciseDescription, + this.exerciseTask, + this.exerciseTypeId}); + @override + _ExerciseSaveState createState() => _ExerciseSaveState(); +} + +class _ExerciseSaveState extends State with Trans { + final FocusNode _nodeText1 = FocusNode(); + final FocusNode _nodeText2 = FocusNode(); + final _controller1 = TextEditingController(); + final _controller2 = TextEditingController(); + final StopWatchTimer stopWatchTimer = StopWatchTimer( + isLapHours: false, + ); + + @override + Widget build(BuildContext context) { + setContext(context); + return getExerciseWidget(); + } + + //@override + initState() { + super.initState(); + _controller1.text = "30"; + _controller2.text = "12"; + _nodeText1.addListener(() { + if (_nodeText1.hasFocus) { + _controller1.selection = TextSelection(baseOffset: 0, extentOffset: _controller1.text.length); + } + }); + _nodeText2.addListener(() { + if (_nodeText2.hasFocus) { + _controller2.selection = TextSelection(baseOffset: 0, extentOffset: _controller2.text.length); + } + }); + if (widget.unit == "second") { + stopWatchTimer.rawTime.listen((value) => widget.onQuantityChanged((value / 1000).toString())); + } + } + + @override + dispose() { + _controller1.dispose(); + stopWatchTimer.dispose(); + super.dispose(); + } + + KeyboardActionsConfig _buildConfig(BuildContext context) { + return KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.ALL, + keyboardBarColor: Colors.grey[200], + keyboardSeparatorColor: Colors.black26, + nextFocus: true, + actions: [ + KeyboardActionsItem(focusNode: _nodeText2, toolbarButtons: [ + (node) { + return GestureDetector( + onTap: () => node.unfocus(), + child: Container( + padding: EdgeInsets.all(8.0), + color: Colors.orange[500], + child: Text( + t("Done"), + style: TextStyle(color: Colors.white), + ), + ), + ); + } + ]), + KeyboardActionsItem( + focusNode: _nodeText1, + toolbarButtons: [ + //button 2 + (node) { + return GestureDetector( + onTap: () => node.unfocus(), + child: Container( + color: Colors.orange, + padding: EdgeInsets.all(8.0), + child: Text( + t("Done"), + style: TextStyle(color: Colors.white), + ), + ), + ); + } + ], + ), + ], + ); + } + + Widget getExerciseWidget() { + return KeyboardActions( + config: _buildConfig(context), + child: Container( + padding: const EdgeInsets.only(top: 10, left: 55, right: 55), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.exerciseName, + style: GoogleFonts.archivoBlack( + fontWeight: FontWeight.bold, + fontSize: 24, + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(5.0, 5.0), + blurRadius: 12.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + ), + overflow: TextOverflow.fade, + maxLines: 4, + softWrap: true, + textAlign: TextAlign.center, + ), + SizedBox( + height: 15, + ), + Text( + widget.exerciseDescription, + style: GoogleFonts.inter(fontSize: 12, color: Colors.yellow[300]), + maxLines: 1, + overflow: TextOverflow.fade, + softWrap: true, + ), + InkWell( + child: Text( + t("More »"), + style: GoogleFonts.inter(fontSize: 12, color: Colors.blue[200]), + ), + onTap: () => { + showDialog( + context: context, + builder: (BuildContext context) { + return DialogHTML(title: widget.exerciseName, htmlData: '

' + widget.exerciseDescription + '

'); + }) + }, + ), + Divider( + color: Colors.transparent, + ), + widget.hasUnitQuantity + ? Text( + t(widget.exerciseTask), + style: GoogleFonts.inter( + fontSize: 14, + color: Colors.orange, + fontWeight: FontWeight.bold, + ), + maxLines: 3, + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: true, + ) + : Offstage(), + Divider( + color: Colors.transparent, + ), + columnQuantityUnit(), + Divider( + color: Colors.transparent, + ), + columnQuantity(), + Divider( + color: Colors.transparent, + ), + widget.hasUnitQuantity + ? Text( + t("Step") + ": " + "1/4", + style: GoogleFonts.inter( + fontSize: 22, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + maxLines: 3, + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: true, + ) + : Offstage(), + Divider( + color: Colors.transparent, + ), + Divider( + color: Colors.transparent, + ), + FlatButton( + onPressed: () { + widget.onSubmit(); + /* showDialog( + context: context, + builder: (BuildContext context) { + return Victory( + victory: true, + ); + }); */ + }, + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset('asset/icon/gomb_orange_c.png', width: 140, height: 60), + Text( + t("Save"), + style: TextStyle(fontSize: 16, color: Colors.white), + ), + ], + )), + ]), + ))); + } + + Column columnQuantityUnit() { + Column row = Column(); + if (widget.hasUnitQuantity) { + row = Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ + TextFormField( + focusNode: _nodeText1, + controller: _controller1, + decoration: InputDecoration( + contentPadding: EdgeInsets.only(left: 25, top: 5, bottom: 5), + labelText: t(widget.unitQuantityUnit), + labelStyle: GoogleFonts.inter(fontSize: 20, color: Colors.yellow[50]), + fillColor: Colors.black38, + filled: true, + border: OutlineInputBorder( + gapPadding: 8.0, + borderRadius: BorderRadius.circular(12.0), + borderSide: BorderSide(color: Colors.white12, width: 0.4), + ), + ), + keyboardType: TextInputType.numberWithOptions(decimal: true), + textInputAction: TextInputAction.done, + style: GoogleFonts.archivoBlack(fontSize: 80, color: Colors.yellow[300]), + onChanged: (value) => widget.onUnitQuantityChanged(value)), + ]); + } + return row; + } + + Column columnQuantity() { + if (widget.unit == "second") { + return Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0), + child: StreamBuilder( + stream: stopWatchTimer.rawTime, + initialData: stopWatchTimer.rawTime.value, + builder: (context, snap) { + final value = snap.data; + final displayTime = StopWatchTimer.getDisplayTime(value, hours: false); + return Column(children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Text( + displayTime, + style: const TextStyle(fontSize: 40, fontFamily: 'Helvetica', fontWeight: FontWeight.bold, color: Colors.white), + ), + ), + ]); + })), + Padding( + padding: const EdgeInsets.all(2), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: IconButton( + padding: const EdgeInsets.all(2), + color: Colors.white70, + onPressed: () async { + stopWatchTimer.onExecute.add(StopWatchExecute.start); + Wakelock.enable(); // prevent sleep the phone + }, + icon: Icon(CustomIcon.play_1), + iconSize: 40, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: IconButton( + padding: const EdgeInsets.all(2), + iconSize: 40, + color: Colors.white70, + onPressed: () async { + stopWatchTimer.onExecute.add(StopWatchExecute.stop); + Wakelock.disable(); + }, + icon: Icon(CustomIcon.stop), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: IconButton( + padding: const EdgeInsets.all(2), + iconSize: 40, + color: Colors.white70, + onPressed: () async { + stopWatchTimer.onExecute.add(StopWatchExecute.reset); + }, + icon: Icon(CustomIcon.creative_commons_zero), + ), + ), + ], + ), + ), + ], + ), + ), + Divider(), + Divider(), + Text(t("Or type the time manually:"), style: GoogleFonts.inter(color: Colors.white)), + TimePickerWidget(onChange: (value) => widget.onQuantityChanged((value).toString())) + ]); + } + Column row = Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ + TextFormField( + focusNode: _nodeText2, + controller: _controller2, + decoration: InputDecoration( + contentPadding: EdgeInsets.only(left: 25, top: 5, bottom: 5), + labelText: t(widget.unit), + labelStyle: GoogleFonts.inter(fontSize: 20, color: Colors.orange[50], decorationColor: Colors.black12), + fillColor: Colors.black38, + filled: true, + border: OutlineInputBorder( + gapPadding: 8.0, + borderRadius: BorderRadius.circular(12.0), + borderSide: BorderSide(color: Colors.black26, width: 0.4), + ), + ), + keyboardType: TextInputType.number, + textInputAction: TextInputAction.next, + style: GoogleFonts.archivoBlack(fontSize: 80, color: Colors.orange[200]), + onChanged: (value) { + widget.onQuantityChanged(value); + }, + ), + ]); + + return row; + } +} diff --git a/lib/widgets/menu_image.dart b/lib/widgets/menu_image.dart new file mode 100644 index 0000000..cffe8c4 --- /dev/null +++ b/lib/widgets/menu_image.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:aitrainer_app/library/image_cache.dart' as wt; +import 'package:transparent_image/transparent_image.dart'; + +class MenuImage extends StatelessWidget { + final int workoutTreeId; + final String imageName; + const MenuImage({this.workoutTreeId, this.imageName}); + + @override + Widget build(BuildContext context) { + if (workoutTreeId == null) { + return Offstage(); + } + String imageString = this.getImage(workoutTreeId, imageName); + Widget widget; + if (imageString != null) { + widget = ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: Container( + color: Colors.transparent, + child: FadeInImage( + fadeInDuration: Duration(milliseconds: 100), + image: MemoryImage(base64Decode(imageString)), + placeholder: MemoryImage(kTransparentImage), + ), + )); + } else { + if (imageName.contains("https")) { + if (!wt.ImageCache().existsImageInMap(workoutTreeId, imageName)) { + widget = ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: Container( + color: Colors.transparent, + child: FadeInImage( + fadeInDuration: Duration(milliseconds: 500), + image: NetworkImage(imageName), + placeholder: MemoryImage(kTransparentImage), + ), + )); + } + } else { + widget = ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: Container( + color: Colors.transparent, + child: Image.asset(imageName), + )); + } + } + if (widget == null) { + return Offstage(); + } + return widget; + } + + String getImage(int id, String name) { + String imageString; + if (name.contains("http")) { + imageString = wt.ImageCache().getImageString(id, name); + } + return imageString; + } +} diff --git a/lib/widgets/menu_page_widget.dart b/lib/widgets/menu_page_widget.dart index 8b1886e..7fbc2ed 100644 --- a/lib/widgets/menu_page_widget.dart +++ b/lib/widgets/menu_page_widget.dart @@ -1,10 +1,11 @@ -import 'dart:convert'; +import 'dart:collection'; import 'dart:ui'; import 'package:aitrainer_app/model/exercise_ability.dart'; import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; import 'package:aitrainer_app/library/custom_icon_icons.dart'; import 'package:aitrainer_app/util/enums.dart'; import 'package:aitrainer_app/util/track.dart'; +import 'package:aitrainer_app/widgets/menu_image.dart'; import 'package:aitrainer_app/widgets/menu_search_bar.dart'; import 'package:aitrainer_app/util/app_language.dart'; import 'package:aitrainer_app/util/app_localization.dart'; @@ -21,8 +22,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:transparent_image/transparent_image.dart'; -import 'package:aitrainer_app/library/image_cache.dart' as wt; import 'dialog_html.dart'; @@ -144,8 +143,15 @@ class _MenuPageWidgetState extends State with Trans, Logging { padding: EdgeInsets.only(left: 0.0, bottom: 0), onPressed: () => menuClick(workoutTree, menuBloc, context), ), + /* Container( + padding: EdgeInsets.only(left: 5, bottom: 5, right: 5), + height: 80, + child: Container( + color: Colors.black.withOpacity(0.2), + ), + ), */ Container( - padding: EdgeInsets.only(left: 15, bottom: 15, right: 15), + padding: EdgeInsets.only(left: 15, bottom: 8, right: 15), child: GestureDetector( onTap: () => menuClick(workoutTree, menuBloc, context), child: Text( @@ -285,7 +291,6 @@ class _MenuPageWidgetState extends State with Trans, Logging { MenuSearchBar( listItems: menuBloc.menuTreeRepository.menuAsExercise, onFind: (value) { - print("onFind: ${value.toJson()}"); if (Cache().userLoggedIn == null) { Scaffold.of(context).showSnackBar(SnackBar( backgroundColor: Colors.orange, @@ -303,11 +308,12 @@ class _MenuPageWidgetState extends State with Trans, Logging { context: context, builder: (BuildContext context) { return DialogCommon( - title: t("You have an acive Compact Test"), - descriptions: t("Press OK to continue!"), + title: t("You have an active Test Set!"), + descriptions: t("Press OK to continue"), text: "OK", onTap: () => { Navigator.of(context).pop(), + Navigator.of(context).pushNamed("testSetExecute"), }, onCancel: () => { Navigator.of(context).pop(), @@ -334,18 +340,22 @@ class _MenuPageWidgetState extends State with Trans, Logging { } void menuClick(WorkoutMenuTree workoutTree, MenuBloc menuBloc, BuildContext context) { - if (workoutTree.child == false) { - if (ExerciseAbility.mini_test.equalsTo(menuBloc.ability) && workoutTree.parent != 0) { - Navigator.of(context).pushNamed('testSetEdit', arguments: workoutTree.nameEnglish); - } - menuBloc.add(MenuTreeDown(item: workoutTree, parent: workoutTree.id)); + if (Cache().userLoggedIn == null) { + Scaffold.of(context).showSnackBar(SnackBar( + backgroundColor: Colors.orange, + content: Text(AppLocalizations.of(context).translate('Please log in'), style: TextStyle(color: Colors.white)))); } else { - menuBloc.add(MenuClickExercise(exerciseTypeId: workoutTree.id)); - if (Cache().userLoggedIn == null) { - Scaffold.of(context).showSnackBar(SnackBar( - backgroundColor: Colors.orange, - content: Text(AppLocalizations.of(context).translate('Please log in'), style: TextStyle(color: Colors.white)))); + if (workoutTree.child == false) { + if (ExerciseAbility.mini_test_set.equalsTo(menuBloc.ability) && workoutTree.parent != 0) { + HashMap args = HashMap(); + args['templateName'] = workoutTree.nameEnglish; + args['templateNameTranslation'] = workoutTree.name; + Navigator.of(context).pushNamed('testSetEdit', arguments: args); + } + menuBloc.add(MenuTreeDown(item: workoutTree, parent: workoutTree.id)); } else { + menuBloc.add(MenuClickExercise(exerciseTypeId: workoutTree.id)); + if (workoutTree.exerciseType.name == "Custom" && Cache().userLoggedIn.admin == 1) { Navigator.of(context).pushNamed('exerciseCustomPage', arguments: workoutTree.exerciseType); } else { @@ -365,60 +375,11 @@ class _MenuPageWidgetState extends State with Trans, Logging { return returnCode; } - Widget _getButtonImage(WorkoutMenuTree workoutTree, double cWidth, double cHeight) { - //print("_getButtonImage " + workoutTree.imageName); - String imageString = menuBloc.getImage(workoutTree.id, workoutTree.imageName); - Widget widget; - if (imageString != null) { - print(" -- get Image from MEMORY " + workoutTree.imageName); - widget = ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: Container( - color: Colors.transparent, - child: FadeInImage( - fadeInDuration: Duration(milliseconds: 100), - image: MemoryImage(base64Decode(imageString)), - placeholder: MemoryImage(kTransparentImage), - ), - )); - } else { - if (workoutTree.imageName.contains("https")) { - if (!wt.ImageCache().existsImageInMap(workoutTree.id, workoutTree.imageName)) { - print(" -- get Image from network " + workoutTree.imageName); - widget = ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: Container( - color: Colors.transparent, - child: FadeInImage( - fadeInDuration: Duration(milliseconds: 500), - image: NetworkImage(workoutTree.imageName), - placeholder: MemoryImage(kTransparentImage), - ), - )); - } - } else { - //print(" -- get Image from asset " + workoutTree.imageName); - widget = ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: Container( - color: Colors.transparent, - child: Image.asset(workoutTree.imageName), - /* FadeInImage( - fadeInDuration: Duration(milliseconds: 50), - image: AssetImage(workoutTree.imageName), - placeholder: MemoryImage(kTransparentImage), - ), */ - )); - } - } - return widget; - } - Widget badgedIcon(WorkoutMenuTree workoutMenuTree, double cWidth, double cHeight) { String badgeKey = workoutMenuTree.nameEnglish; bool show = Cache().getBadges()[badgeKey] != null; int counter = Cache().getBadges()[badgeKey] != null ? Cache().getBadges()[badgeKey] : 0; - Widget buttonImage = _getButtonImage(workoutMenuTree, cWidth, cHeight); + Widget buttonImage = MenuImage(imageName: workoutMenuTree.imageName, workoutTreeId: workoutMenuTree.id); return Badge( padding: EdgeInsets.all(8), position: BadgePosition.topEnd(top: 3, end: 3), @@ -441,6 +402,7 @@ class _MenuPageWidgetState extends State with Trans, Logging { @override void dispose() { + scrollController.dispose(); super.dispose(); } } diff --git a/lib/widgets/menu_search_bar.dart b/lib/widgets/menu_search_bar.dart index a5e9205..a1dd584 100644 --- a/lib/widgets/menu_search_bar.dart +++ b/lib/widgets/menu_search_bar.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:aitrainer_app/model/workout_menu_tree.dart'; import 'package:aitrainer_app/util/trans.dart'; -import 'package:dropdown_search/dropdown_search.dart'; +import 'package:aitrainer_app/library/dropdown_search.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -23,26 +23,31 @@ class SearchBarStream { } } +// ignore: must_be_immutable class MenuSearchBar extends StatelessWidget { final List listItems; final Function(WorkoutMenuTree) onFind; - - const MenuSearchBar({@required this.listItems, this.onFind}); + bool showIcon; + MenuSearchBar({@required this.listItems, this.onFind, this.showIcon = true}); @override Widget build(BuildContext context) { return AnimatedSearch( listItems: listItems, onFind: onFind, + showIcon: showIcon, ); } } +// ignore: must_be_immutable class AnimatedSearch extends StatefulWidget { final List listItems; final Function(WorkoutMenuTree) onFind; + bool showIcon = true; + + AnimatedSearch({this.listItems, this.onFind, this.showIcon}); - AnimatedSearch({this.listItems, this.onFind}); @override _AnimatedSearch createState() => _AnimatedSearch(); } @@ -81,7 +86,7 @@ class _AnimatedSearch extends State { alignment: Alignment.center, children: [ AnimateExpansion( - animate: !isSearching, + animate: widget.showIcon ? !isSearching : false, axisAlignment: 1.0, child: IconButton( onPressed: () => { @@ -98,7 +103,7 @@ class _AnimatedSearch extends State { ), )), AnimateExpansion( - animate: isSearching, + animate: widget.showIcon ? isSearching : true, axisAlignment: -1.0, child: Search( listItems: widget.listItems, diff --git a/lib/widgets/time_picker.dart b/lib/widgets/time_picker.dart index eefeb42..01663b8 100644 --- a/lib/widgets/time_picker.dart +++ b/lib/widgets/time_picker.dart @@ -25,7 +25,6 @@ class _TimePickerWidgetState extends State with Trans { currentTimeInMin = x.toDouble(); } seconds = currentTimeInMin * 60 + currentTimeInSec + currentTimeInDec / 100; - //print("sec" + seconds.toStringAsFixed(2)); setState(() {}); widget.onChange(seconds); }, diff --git a/lib/widgets/victory_widget.dart b/lib/widgets/victory_widget.dart new file mode 100644 index 0000000..92a17de --- /dev/null +++ b/lib/widgets/victory_widget.dart @@ -0,0 +1,110 @@ +import 'package:confetti/confetti.dart'; +import 'package:ezanimation/ezanimation.dart'; +import 'package:flutter/material.dart'; +import 'dart:math'; + +import 'package:flutter/scheduler.dart'; + +class VictoryConfetti extends StatefulWidget { + @override + _VictoryConfettiState createState() => _VictoryConfettiState(); +} + +class _VictoryConfettiState extends State { + ConfettiController _controllerBottomCenter; + + @override + void initState() { + _controllerBottomCenter = ConfettiController(duration: const Duration(seconds: 2)); + SchedulerBinding.instance.addPostFrameCallback((_) { + Future.delayed(Duration(milliseconds: 500)).then((value) => _controllerBottomCenter.play()); + }); + super.initState(); + } + + @override + void dispose() { + _controllerBottomCenter.dispose(); + super.dispose(); + } + + Path drawStar(Size size) { + // Method to convert degree to radians + double degToRad(double deg) => deg * (pi / 180.0); + + const numberOfPoints = 5; + final halfWidth = size.width / 2; + final externalRadius = halfWidth; + final internalRadius = halfWidth / 2.5; + final degreesPerStep = degToRad(360 / numberOfPoints); + final halfDegreesPerStep = degreesPerStep / 2; + final path = Path(); + final fullAngle = degToRad(360); + path.moveTo(size.width, halfWidth); + + for (double step = 0; step < fullAngle; step += degreesPerStep) { + path.lineTo(halfWidth + externalRadius * cos(step), halfWidth + externalRadius * sin(step)); + path.lineTo(halfWidth + internalRadius * cos(step + halfDegreesPerStep), halfWidth + internalRadius * sin(step + halfDegreesPerStep)); + } + path.close(); + return path; + } + + @override + Widget build(BuildContext context) { + return Container( + child: Align( + alignment: Alignment.bottomCenter, + child: ConfettiWidget( + confettiController: _controllerBottomCenter, + blastDirectionality: BlastDirectionality.explosive, // don't specify a direction, blast randomly + numberOfParticles: 20, + colors: const [Colors.green, Colors.blue, Colors.pink, Colors.orange, Colors.purple], // manually specify the colors to be used + createParticlePath: drawStar, // define a custom shape/path. + ), + ), + ); + } +} + +class Victory extends StatefulWidget { + final bool victory; + const Victory({this.victory}); + @override + _VictoryState createState() => _VictoryState(); +} + +class _VictoryState extends State { + final EzAnimation animation = EzAnimation(1.0, 200.0, Duration(seconds: 3), reverseCurve: null); + + @override + void initState() { + animation.start(); + animation.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() {}); + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + child: AnimatedBuilder( + animation: animation, + builder: (context, snapshot) { + return Center( + child: Container( + width: animation.value, + height: animation.value, + child: Row(children: [ + VictoryConfetti(), + widget.victory ? Image.asset("asset/image/WT_cup_victory400.png") : Offstage(), + ]), + ), + ); + })); + } +} diff --git a/pubspec.lock b/pubspec.lock index dbecfce..e35c843 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -211,6 +211,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0-nullsafety.3" + confetti: + dependency: "direct main" + description: + name: confetti + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.5" convert: dependency: transitive description: @@ -267,13 +274,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.3" - dropdown_search: - dependency: "direct main" - description: - name: dropdown_search - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.9" equatable: dependency: "direct main" description: @@ -546,13 +546,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.2" - gradient_bottom_navigation_bar: - dependency: "direct main" - description: - name: gradient_bottom_navigation_bar - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0+4" graphs: dependency: transitive description: @@ -896,6 +889,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + random_color: + dependency: transitive + description: + name: random_color + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3413689..9a42498 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,6 @@ dependencies: spider_chart: ^0.1.5 rainbow_color: ^0.1.1 percent_indicator: ^2.1.8 - gradient_bottom_navigation_bar: ^1.0.0+4 fl_chart: ^0.12.0 infinite_listview: ^1.0.1+1 toggle_switch: ^0.1.8 @@ -52,10 +51,10 @@ dependencies: network_image_to_byte: ^0.0.1 package_info: ^0.4.3+4 liquid_progress_indicator: ^0.3.2 - dropdown_search: ^0.4.9 audioplayer: ^0.8.1 ezanimation: ^0.4.1 - + confetti: ^0.5.5 + firebase_core: ^0.5.0 firebase_analytics: ^6.2.0 @@ -148,6 +147,7 @@ flutter: - asset/image/WT_Results_for_female.jpg - asset/image/WT_Results_for_men.jpg - asset/image/WT_results_background.jpg + - asset/image/WT_cup_victory400.png - asset/image/button_fb.png - asset/image/button_apple.png @@ -291,6 +291,7 @@ flutter: - asset/menu/leg_curls.jpg - asset/menu/leg_extension.jpg - asset/menu/legpress.jpg + - asset/menu/lower_body_test.jpg - asset/menu/lunges_with_dumbbells.jpg - asset/menu/lunges.jpg - asset/menu/lying_alternating_leg_raises.jpg @@ -302,6 +303,7 @@ flutter: - asset/menu/lying_triceps_extension.jpg - asset/menu/machine_shoulder_press.jpg - asset/menu/machine_test.jpg + - asset/menu/no_equipment_test.jpg - asset/menu/oblique_crunch.jpg - asset/menu/olympic_squat.jpg - asset/menu/one_arm_row.jpg @@ -353,16 +355,18 @@ flutter: - asset/menu/straight-arm_rope_pull-down.jpg - asset/menu/t_bar_rows.jpg - asset/menu/test_center.jpg + - asset/menu/test_on_machines.jpg - asset/menu/thigh_adductor.jpg - asset/menu/triceps_extension_on_cable_with_rope.jpg - asset/menu/triceps_kickback.jpg - asset/menu/triceps_pushdown.jpg - asset/menu/twisted_crunches.jpg - asset/menu/under_body.jpg + - asset/menu/upper_body_test.jpg - asset/menu/upper_body.jpg - asset/menu/v_ups.jpg - asset/menu/wall_sit.jpg - - asset/menu/weight_test.jpg + - asset/menu/weight_free_test.jpg - asset/menu/weighted_bench_dip.jpg - asset/menu/wide_grip_behind_the_neck_pull_ups.jpg - asset/menu/wide_grip_front_lat_pulldown.jpg