WT1.1.10 compact test, paralell test

This commit is contained in:
bossanyit 2021-03-25 21:54:18 +01:00
parent 2e0d7fc37d
commit f724737ea3
65 changed files with 5250 additions and 749 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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 = (

View File

@ -13,7 +13,6 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
final TimerBloc timerBloc;
final ExerciseRepository exerciseRepository;
final bool readonly;
final double percentToCalculate;
int step = 1;
double initialRM;
@ -29,18 +28,18 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
double scrollOffset = 0;
@override
ExerciseControlBloc({this.exerciseRepository, this.readonly, this.percentToCalculate, @required this.timerBloc})
: super(ExerciseControlInitial()) {
ExerciseControlBloc({this.exerciseRepository, this.readonly, @required this.timerBloc}) : super(ExerciseControlInitial()) {
print("Exercise ${exerciseRepository.exercise.toJson()}");
oneRepQuantity = exerciseRepository.exercise.quantity;
oneRepUnitQuantity = exerciseRepository.exercise.unitQuantity;
initialRM = this.calculate1RM(percent75: false);
unitQuantity = this.calculate1RM(percent75: true).roundToDouble();
quantity = percentToCalculate == 0.75 ? 12 : 35;
quantity = 12;
origQuantity = quantity;
exerciseRepository.setUnitQuantity(unitQuantity);
exerciseRepository.setQuantity(quantity);
print("init quantity: " + quantity.toString());
timerBloc.add(TimerStart(duration: 300));
}
@ -49,7 +48,6 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
try {
if (event is ExerciseControlLoad) {
yield ExerciseControlLoading();
print("init quantity: " + quantity.toString());
step = 1;
yield ExerciseControlReady();
} else if (event is ExerciseControlQuantityChange) {
@ -83,9 +81,8 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
}
exerciseRepository.end = DateTime.now();
await exerciseRepository.addExercise();
exerciseRepository.initExercise();
exerciseRepository.setQuantity(quantity);
exerciseRepository.exercise.exerciseId = null;
step <= 3 ? timerBloc.add(TimerStart(duration: 300)) : timerBloc.add(TimerEnd());
}
yield ExerciseControlReady();
@ -110,7 +107,7 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
print("Weight: $weight repeat: $repeat, $rmWendler, Oconner: $rmOconner");
double average = (rmWendler + rmOconner) / 2;
return percent75 ? average * this.percentToCalculate : average;
return percent75 ? average * 0.75 : average;
}
int calculateQuantityByUnitQuantity() {

View File

@ -74,6 +74,7 @@ class ExerciseExecutePlanAddBloc extends Bloc<ExerciseExecutePlanAddEvent, Exerc
exerciseRepository.exercise.unit = workoutTree.exerciseType.unit;
workoutTree.executed = true;
await exerciseRepository.addExercise();
exerciseRepository.initExercise();
Track().track(TrackingEvent.my_exercise_plan_execute_save);
step++;
scrollOffset = step * 200.0;

View File

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:aitrainer_app/bloc/menu/menu_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';
import 'package:aitrainer_app/model/fitness_state.dart';
import 'package:aitrainer_app/repository/customer_repository.dart';
@ -62,6 +61,7 @@ class ExerciseNewBloc extends Bloc<ExerciseNewEvent, ExerciseNewState> 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<ExerciseNewEvent, ExerciseNewState> 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<ExerciseNewEvent, ExerciseNewState> 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();

View File

@ -20,6 +20,10 @@ class ExerciseNewReady extends ExerciseNewState {
const ExerciseNewReady();
}
class ExerciseNewSaved extends ExerciseNewState {
const ExerciseNewSaved();
}
class ExerciseNewError extends ExerciseNewState {
final String message;
const ExerciseNewError({this.message});

View File

@ -166,7 +166,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> 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<MenuEvent, MenuState> 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<void> getImages(LinkedHashMap<String, WorkoutMenuTree> branch) async {
@ -231,7 +231,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> with Trans, Logging {
}
}
});
return filtered;
}
}

View File

@ -37,7 +37,6 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> 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<SettingsEvent, SettingsState> 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();

View File

@ -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<TestSetControlEvent, TestSetControlState> {
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<TestSetControlState> 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());
}
}
}

View File

@ -0,0 +1,32 @@
part of 'test_set_control_bloc.dart';
abstract class TestSetControlEvent extends Equatable {
const TestSetControlEvent();
@override
List<Object> get props => [];
}
class TestSetControlLoad extends TestSetControlEvent {
const TestSetControlLoad();
}
class TestSetControlQuantityChange extends TestSetControlEvent {
final double quantity;
const TestSetControlQuantityChange({this.quantity});
@override
List<Object> get props => [quantity];
}
class TestSetControlUnitQuantityChange extends TestSetControlEvent {
final double quantity;
const TestSetControlUnitQuantityChange({this.quantity});
@override
List<Object> get props => [quantity];
}
class TestSetControlSubmit extends TestSetControlEvent {
const TestSetControlSubmit();
}

View File

@ -0,0 +1,28 @@
part of 'test_set_control_bloc.dart';
abstract class TestSetControlState extends Equatable {
const TestSetControlState();
@override
List<Object> 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<Object> get props => [message];
}

View File

@ -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<TestSetEditEvent, TestSetEditState> {
final String templateName;
final String templateNameTranslation;
final WorkoutTreeRepository workoutTreeRepository;
final MenuBloc menuBloc;
final List<ExerciseType> _exerciseTypes = List();
final List<ExerciseType> _actualExerciseTypes = List();
final HashMap<int, ExerciseType> _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<TestSetEditEvent, TestSetEditState> {
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<TestSetEditEvent, TestSetEditState> {
Stream<TestSetEditState> mapEventToState(TestSetEditEvent event) async* {
try {
if (event is TestSetEditChangeExerciseType) {
yield TestSetEditLoading();
final List<ExerciseType> 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 {
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<TestSetEditEvent, TestSetEditState> {
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<TestSetEditEvent, TestSetEditState> {
}
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);
});
}
}

View File

@ -20,6 +20,14 @@ class TestSetEditChangeExerciseType extends TestSetEditEvent {
List<Object> get props => [index, exerciseTypeId];
}
class TestSetEditDeleteExerciseType extends TestSetEditEvent {
final int exerciseTypeId;
const TestSetEditDeleteExerciseType({this.exerciseTypeId});
@override
List<Object> get props => [exerciseTypeId];
}
class TestSetEditSkipExerciseType extends TestSetEditEvent {
final int exerciseTypeId;
const TestSetEditSkipExerciseType({this.exerciseTypeId});

View File

@ -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<TestSetExecuteEvent, TestSetExecuteState> {
// 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<ExercisePlanDetail> 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<TestSetExecuteState> 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;
}
}

View File

@ -0,0 +1,54 @@
part of 'test_set_execute_bloc.dart';
abstract class TestSetExecuteEvent extends Equatable {
const TestSetExecuteEvent();
@override
List<Object> get props => [];
}
class TestSetExecuteLoad extends TestSetExecuteEvent {
const TestSetExecuteLoad();
}
class TestSetExecuteExecute extends TestSetExecuteEvent {
final int exerciseTypeId;
const TestSetExecuteExecute({this.exerciseTypeId});
@override
List<Object> 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<Object> get props => [exerciseTypeId, quantity, unitQuantity];
}
class TestSetExecuteNewExercise extends TestSetExecuteEvent {
final int exerciseTypeId;
const TestSetExecuteNewExercise({this.exerciseTypeId});
@override
List<Object> get props => [exerciseTypeId];
}
class TestSetExecuteDeleteExercise extends TestSetExecuteEvent {
final int exerciseTypeId;
const TestSetExecuteDeleteExercise({this.exerciseTypeId});
@override
List<Object> get props => [exerciseTypeId];
}
class TestSetExecuteDeleteActive extends TestSetExecuteEvent {
const TestSetExecuteDeleteActive();
}

View File

@ -0,0 +1,32 @@
part of 'test_set_execute_bloc.dart';
abstract class TestSetExecuteState extends Equatable {
const TestSetExecuteState();
@override
List<Object> 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<Object> get props => [message];
}

View File

@ -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<TestSetNewEvent, TestSetNewState> {
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<TestSetNewState> 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());
}
}
}

View File

@ -0,0 +1,32 @@
part of 'test_set_new_bloc.dart';
abstract class TestSetNewEvent extends Equatable {
const TestSetNewEvent();
@override
List<Object> get props => [];
}
class TestSetNewLoad extends TestSetNewEvent {
const TestSetNewLoad();
}
class TestSetNewChangeQuantity extends TestSetNewEvent {
final double quantity;
const TestSetNewChangeQuantity({this.quantity});
@override
List<Object> get props => [quantity];
}
class TestSetNewChangeQuantityUnit extends TestSetNewEvent {
final double quantity;
const TestSetNewChangeQuantityUnit({this.quantity});
@override
List<Object> get props => [quantity];
}
class TestSetNewSubmit extends TestSetNewEvent {
const TestSetNewSubmit();
}

View File

@ -0,0 +1,28 @@
part of 'test_set_new_bloc.dart';
abstract class TestSetNewState extends Equatable {
const TestSetNewState();
@override
List<Object> 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<Object> get props => [message];
}

View File

@ -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<List<T>> DropdownSearchOnFind<T>(String text);
typedef String DropdownSearchItemAsString<T>(T item);
typedef bool DropdownSearchFilterFn<T>(T item, String filter);
typedef bool DropdownSearchCompareFn<T>(T item, T selectedItem);
typedef Widget DropdownSearchBuilder<T>(BuildContext context, T selectedItem, String itemAsString);
typedef Widget DropdownSearchPopupItemBuilder<T>(
BuildContext context,
T item,
bool isSelected,
);
typedef bool DropdownSearchPopupItemEnabled<T>(T item);
typedef Widget ErrorBuilder<T>(BuildContext context, String searchEntry, dynamic exception);
typedef Widget EmptyBuilder<T>(BuildContext context, String searchEntry);
typedef Widget LoadingBuilder<T>(BuildContext context, String searchEntry);
typedef Widget IconButtonBuilder(BuildContext context);
typedef Future<bool> BeforeChange<T>(T prevItem, T nextItem);
enum Mode { DIALOG, BOTTOM_SHEET, MENU }
class DropdownSearch<T> 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<T> items;
///selected item
final T selectedItem;
///function that returns item from API
final DropdownSearchOnFind<T> onFind;
///called when a new item is selected
final ValueChanged<T> onChanged;
///to customize list of items UI
final DropdownSearchBuilder<T> dropdownBuilder;
///to customize selected item
final DropdownSearchPopupItemBuilder<T> 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<T> itemAsString;
/// custom filter function
final DropdownSearchFilterFn<T> 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<T> 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<T> onSaved;
/// An optional method that validates an input. Returns an error string to
/// display if the input is invalid, or null otherwise.
final FormFieldValidator<T> 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<T> 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<T> 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<T> createState() => DropdownSearchState<T>();
}
class DropdownSearchState<T> extends State<DropdownSearch<T>> {
final ValueNotifier<T> _selectedItemNotifier = ValueNotifier(null);
final ValueNotifier<bool> _isFocused = ValueNotifier(false);
@override
void initState() {
super.initState();
_selectedItemNotifier.value = widget.selectedItem;
}
@override
void didUpdateWidget(DropdownSearch<T> 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<T>(
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: <Widget>[
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<T> 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: <Widget>[
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<T> _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<T> _openBottomSheet(T data) {
return showModalBottomSheet<T>(
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<T> _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<T>(
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<T> _selectDialogInstance(T data, {double defaultHeight}) {
return SelectDialog<T>(
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<T> _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<T> 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;
}

View File

@ -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<MyHomePage> {
/// 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>[
/// 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]
/// * <https://material.google.com/components/bottom-navigation.html>
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<BottomNavigationBarItem> 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<int> 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<double> 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<double>(
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<double>(
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<double>(
// 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: <Widget>[
InkResponse(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildIcon(),
label,
],
),
),
Semantics(
label: indexLabel,
)
],
),
),
);
}
}
class _GradientBottomNavigationBarState extends State<GradientBottomNavigationBar> with TickerProviderStateMixin {
List<AnimationController> _controllers = <AnimationController>[];
List<CurvedAnimation> _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<double> _flexTween = Tween<double>(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<AnimationController>.generate(widget.items.length, (int index) {
return AnimationController(
duration: kThemeAnimationDuration,
vsync: this,
)..addListener(_rebuild);
});
_animations = List<CurvedAnimation>.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<double> 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<Widget> _createTiles() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
assert(localizations != null);
final List<Widget> children = <Widget>[];
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<Widget> 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: <Widget>[
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: <Widget>[
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<Animation<double>> animations) {
// We're adding flex values instead of animation values to produce correct
// ratios.
return animations.map<double>(state._evaluateFlex).fold<double>(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<double> radiusTween = Tween<double>(
begin: 0.0,
end: _maxRadius(center, size),
);
canvas.drawCircle(
center,
radiusTween.transform(circle.animation.value),
paint,
);
}
}
}

599
lib/library/popup_menu.dart Normal file
View File

@ -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<Size> 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<Size> 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<WhyFarther>(
/// 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<T> extends PopupMenuEntry<T> {
/// 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<T, CustomPopupMenuItem<T>> createState() => PopupMenuItemState<T, CustomPopupMenuItem<T>>();
}
/// 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<T, W extends CustomPopupMenuItem<T>> extends State<W> {
/// 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<T>(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<T> extends StatelessWidget {
const _PopupMenu({
Key key,
this.route,
this.semanticLabel,
}) : super(key: key);
final _PopupMenuRoute<T> 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<Widget> children = <Widget>[];
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<Size> 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<T> extends PopupRoute<T> {
_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<Size>(items.length);
final RelativeRect position;
final List<PopupMenuEntry<T>> items;
final List<Size> 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<double> 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<double> animation, Animation<double> 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<T>(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<T> customShowMenu<T>({
@required BuildContext context,
@required RelativeRect position,
@required List<PopupMenuEntry<T>> 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<T>(
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,
),
);
}

View File

@ -0,0 +1,407 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'dropdown_search.dart';
class SelectDialog<T> extends StatefulWidget {
final T selectedValue;
final List<T> items;
final bool showSearchBox;
final bool isFilteredOnline;
final ValueChanged<T> onChanged;
final DropdownSearchOnFind<T> onFind;
final DropdownSearchPopupItemBuilder<T> itemBuilder;
final InputDecoration searchBoxDecoration;
final DropdownSearchItemAsString<T> itemAsString;
final DropdownSearchFilterFn<T> filterFn;
final String hintText;
final double maxHeight;
final double dialogMaxWidth;
final Widget popupTitle;
final bool showSelectedItem;
final DropdownSearchCompareFn<T> compareFn;
final DropdownSearchPopupItemEnabled<T> 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<T> createState() => _SelectDialogState<T>();
}
class _SelectDialogState<T> extends State<SelectDialog<T>> {
final FocusNode focusNode = new FocusNode();
final StreamController<List<T>> _itemsStream = StreamController();
final ValueNotifier<bool> _loadingNotifier = ValueNotifier(false);
final List<T> _items = List<T>();
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: <Widget>[
_searchField(),
Expanded(
child: Stack(
children: <Widget>[
StreamBuilder<List<T>>(
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: <Widget>[
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<T> 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<T> 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<T> 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>[
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);
}
}

View File

@ -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<Null> main() async {
BlocProvider<TimerBloc>(
create: (BuildContext context) => TimerBloc(),
),
BlocProvider<TestSetExecuteBloc>(
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',

View File

@ -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<SharedPreferences> 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<void> deleteActiveExercisePlan() async {
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
SharedPreferences sharedPreferences;
sharedPreferences = await prefs;
sharedPreferences.remove(Cache.activeExercisePlanDateKey);
this.activeExercisePlan = null;
this.activeExercisePlanDetails = null;
}
Future<void> 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<String, dynamic> 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();
}
}

View File

@ -59,4 +59,9 @@ class Exercise {
newExercise.exercisePlanDetailId = this.exercisePlanDetailId;
return newExercise;
}
@override
String toString() {
return this.toJson().toString();
}
}

View File

@ -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";
}
}
}

View File

@ -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<String, dynamic> toJson() => {
"exercisePlanId": exercisePlanId,
"exerciseTypeId": exerciseTypeId,
@ -31,4 +67,14 @@ class ExercisePlanDetail {
"repeats": repeats,
"weightEquation": weightEquation
};
Map<String, dynamic> toJsonWithExerciseList() => {
"exercisePlanDetailId": exercisePlanDetailId,
"exercisePlanId": exercisePlanId,
"exerciseTypeId": exerciseTypeId,
"serie": serie,
"repeats": repeats,
"weightEquation": weightEquation,
'exercises': exercises.map((exercise) => exercise.toJson()).toList().toString(),
};
}

View File

@ -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();

View File

@ -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<int> devices = List();
/// parents[]
List<int> parents = List();
/// alternatives []
List<int> 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();
}
}

View File

@ -31,7 +31,6 @@ class WorkoutMenuTree {
bool base;
bool is1RM;
bool isEndurance;
bool isRunning;
List<WorkoutType> 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<String, dynamic> 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,
};

View File

@ -73,7 +73,7 @@ class ExerciseRepository {
Exercise getExercise() => this.exercise;
Future<void> addExercise() async {
Future<Exercise> 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);
}

View File

@ -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<WorkoutMenuTree> menuAsExercise = List();
final Map<String, int> _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<void> 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<WorkoutMenuTree> 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<String, List<WorkoutMenuTree>>();
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);
}
});

View File

@ -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++;

View File

@ -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++;
}

View File

@ -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<T>(Iterable<T> values, String value) {

View File

@ -36,15 +36,13 @@ class _ExerciseControlPage extends State<ExerciseControlPage> 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<TimerBloc>(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<ExerciseControlBloc, ExerciseControlState>(listener: (context, state) {
if (state is ExerciseControlError) {

View File

@ -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<ExerciseNewPage> 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<MenuBloc>(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<ExerciseNewPage> 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<TestSetExecuteBloc>(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<ExerciseNewBloc>(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<ExerciseNewBloc>(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<ExerciseNewPage> 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<ExerciseNewPage> 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,
padding: EdgeInsets.only(top: 10, left: 20, right: 20),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('asset/image/WT_black_background.jpg'),
fit: BoxFit.fill,
fit: BoxFit.cover,
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: <Widget>[
Text(
exerciseName,
style: GoogleFonts.archivoBlack(
fontWeight: FontWeight.bold,
fontSize: 24,
color: Colors.white,
shadows: <Shadow>[
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),
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)));
},
),
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),
),
],
onUnitQuantityChanged: (value) => exerciseBloc.add(ExerciseNewQuantityUnitChange(quantity: double.parse(value))),
onSubmit: () => confirmationDialog(exerciseBloc, menuBloc),
exerciseTypeId: exerciseType.exerciseTypeId,
)),
]),
))),
bottomNavigationBar: BottomBarMultipleExercises(
isSet: false,
exerciseTypeId: exerciseType.exerciseTypeId,
),
));
}
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),
),
),
//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<int>(
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: <Widget>[
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: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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)))},
),
]);
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<ExerciseNewPage> with Trans, Logging {
: bloc.exerciseRepository.exercise.unitQuantity.toString();
}
// ignore: close_sinks
final TestSetExecuteBloc executeBloc = BlocProvider.of<TestSetExecuteBloc>(context);
showCupertinoDialog(
useRootNavigator: true,
context: context,
@ -491,29 +177,12 @@ class _ExerciseNewPageState extends State<ExerciseNewPage> 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)),
}
},
)

View File

@ -137,7 +137,8 @@ class _ExercisePlanCustomPage extends State<ExercisePlanCustomPage> with Trans {
List<Widget> _getChildList(List<WorkoutMenuTree> listWorkoutTree, ExercisePlanBloc bloc) {
List<Widget> 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(

View File

@ -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),

View File

@ -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<TestSetControlBloc, TestSetControlState>(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<TestSetControlBloc>(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>[
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<Widget> 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,
);
}
}

View File

@ -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<MenuBloc>(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<TestSetEditBloc, TestSetEditState>(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<TestSetEditBloc>(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,11 +155,9 @@ class TestSetEdit extends StatelessWidget with Trans {
]));
}
List<Widget> imageSliders(List<WorkoutMenuTree> alternatives, MenuBloc menuBloc) {
List<Widget> imageSliders(List<WorkoutMenuTree> alternatives, MenuBloc menuBloc, WorkoutMenuTree workoutTree, TestSetEditBloc bloc) {
final List<Widget> list = List();
alternatives.forEach((element) {
list.add(getImageStack(element, menuBloc));
});
if (bloc.exercisePlanDetails[workoutTree.exerciseTypeId] == null) {
list.add(Container(
padding: EdgeInsets.only(top: 25, bottom: 25),
child: ClipRRect(
@ -167,14 +173,25 @@ class TestSetEdit extends StatelessWidget with Trans {
),
),
)))));
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),
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(
@ -183,6 +200,25 @@ class TestSetEdit extends StatelessWidget with Trans {
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;
}
}

View File

@ -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<MenuBloc>(context);
// ignore: close_sinks
TestSetExecuteBloc executeBloc = BlocProvider.of<TestSetExecuteBloc>(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<TestSetExecuteBloc, TestSetExecuteState>(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<TestSetExecuteBloc>(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<Widget> getTiles(TestSetExecuteBloc bloc, BuildContext context) {
List<Widget> 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>[
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>[
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>[
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<Widget> getExerciseTiles(TestSetExecuteBloc bloc, BuildContext context) {
List<Widget> 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>[
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,
)),
]),
)),
]),
),
),
);
}
}

View File

@ -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<TestSetNewBloc, TestSetNewState>(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<TestSetNewBloc>(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());
});
}
}

View File

@ -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 {

View File

@ -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<ExercisePlanDetail> details;
int exerciseTypeId;
BottomBarMultipleExercises({this.details, this.isSet, this.exerciseTypeId});
@override
_BottomBarMultipleExercisesState createState() => _BottomBarMultipleExercisesState();
}
class _BottomBarMultipleExercisesState extends State<BottomBarMultipleExercises> 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<MenuBloc>(context);
// ignore: close_sinks
final TestSetExecuteBloc bloc = BlocProvider.of<TestSetExecuteBloc>(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<TestSetExecuteBloc, TestSetExecuteState>(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<Widget> getChildren(TestSetExecuteBloc bloc) {
final List<Widget> 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<Widget> imageSliders(TestSetExecuteBloc bloc) {
List<Widget> 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),
),
),
]));
}
}

View File

@ -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 {

View File

@ -65,8 +65,9 @@ class _DialogPremiumState extends State<DialogCommon> with Trans {
children: [
Text(
widget.title + " ",
textAlign: TextAlign.center,
style: GoogleFonts.archivoBlack(
fontSize: 18,
fontSize: 20,
color: Colors.yellow[400],
shadows: <Shadow>[
Shadow(
@ -90,7 +91,8 @@ class _DialogPremiumState extends State<DialogCommon> with Trans {
Text(
widget.descriptions,
style: GoogleFonts.inter(
fontSize: 14,
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
shadows: <Shadow>[
Shadow(
@ -113,7 +115,7 @@ class _DialogPremiumState extends State<DialogCommon> with Trans {
Text(
widget.description2,
style: GoogleFonts.inter(
fontSize: 14,
fontSize: 16,
color: Colors.white,
shadows: <Shadow>[
Shadow(
@ -136,7 +138,7 @@ class _DialogPremiumState extends State<DialogCommon> with Trans {
Text(
widget.description3,
style: GoogleFonts.inter(
fontSize: 14,
fontSize: 16,
color: Colors.white,
shadows: <Shadow>[
Shadow(

View File

@ -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<DialogWidget> 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: <Widget>[
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: <Widget>[
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>[
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>[
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();
}
}

View File

@ -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<dynamic> onQuantityChanged;
final ValueChanged<dynamic> 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<ExerciseSave> 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: <Widget>[
Text(
widget.exerciseName,
style: GoogleFonts.archivoBlack(
fontWeight: FontWeight.bold,
fontSize: 24,
color: Colors.white,
shadows: <Shadow>[
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: '<p>' + widget.exerciseDescription + '</p>');
})
},
),
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<int>(
stream: stopWatchTimer.rawTime,
initialData: stopWatchTimer.rawTime.value,
builder: (context, snap) {
final value = snap.data;
final displayTime = StopWatchTimer.getDisplayTime(value, hours: false);
return Column(children: <Widget>[
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: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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;
}
}

View File

@ -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;
}
}

View File

@ -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<MenuPageWidget> 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<MenuPageWidget> 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<MenuPageWidget> 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<MenuPageWidget> 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));
} 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))));
} else {
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<MenuPageWidget> 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<MenuPageWidget> with Trans, Logging {
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
}

View File

@ -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<AnimatedSearch> {
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<AnimatedSearch> {
),
)),
AnimateExpansion(
animate: isSearching,
animate: widget.showIcon ? isSearching : true,
axisAlignment: -1.0,
child: Search(
listItems: widget.listItems,

View File

@ -25,7 +25,6 @@ class _TimePickerWidgetState extends State<TimePickerWidget> with Trans {
currentTimeInMin = x.toDouble();
}
seconds = currentTimeInMin * 60 + currentTimeInSec + currentTimeInDec / 100;
//print("sec" + seconds.toStringAsFixed(2));
setState(() {});
widget.onChange(seconds);
},

View File

@ -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<VictoryConfetti> {
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<Victory> {
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(),
]),
),
);
}));
}
}

View File

@ -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:

View File

@ -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,9 +51,9 @@ 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
@ -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