WT1.1.10 compact test, paralell test
BIN
asset/image/WT_cup_victory400.png
Normal file
After Width: | Height: | Size: 291 KiB |
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
BIN
asset/menu/lower_body_test.jpg
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
asset/menu/no_equipment_test.jpg
Normal file
After Width: | Height: | Size: 143 KiB |
BIN
asset/menu/test_on_machines.jpg
Normal file
After Width: | Height: | Size: 134 KiB |
BIN
asset/menu/upper_body_test.jpg
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
asset/menu/weight_free_test.jpg
Normal file
After Width: | Height: | Size: 142 KiB |
18
i18n/en.json
@ -391,6 +391,22 @@
|
|||||||
"Change the weight to":"Change the weight to",
|
"Change the weight to":"Change the weight to",
|
||||||
|
|
||||||
"Search Exercises...":"Search Exercises...",
|
"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"
|
||||||
|
|
||||||
}
|
}
|
19
i18n/hu.json
@ -387,6 +387,23 @@
|
|||||||
"Change the weight to":"Súly változtatása",
|
"Change the weight to":"Súly változtatása",
|
||||||
|
|
||||||
"Search Exercises...":"Gyakorlat keresése...",
|
"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"
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -388,7 +388,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
DEVELOPMENT_TEAM = SFJJBDCU6Z;
|
DEVELOPMENT_TEAM = SFJJBDCU6Z;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@ -531,7 +531,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
DEVELOPMENT_TEAM = SFJJBDCU6Z;
|
DEVELOPMENT_TEAM = SFJJBDCU6Z;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@ -566,7 +566,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
DEVELOPMENT_TEAM = SFJJBDCU6Z;
|
DEVELOPMENT_TEAM = SFJJBDCU6Z;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
@ -13,7 +13,6 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
|
|||||||
final TimerBloc timerBloc;
|
final TimerBloc timerBloc;
|
||||||
final ExerciseRepository exerciseRepository;
|
final ExerciseRepository exerciseRepository;
|
||||||
final bool readonly;
|
final bool readonly;
|
||||||
final double percentToCalculate;
|
|
||||||
int step = 1;
|
int step = 1;
|
||||||
|
|
||||||
double initialRM;
|
double initialRM;
|
||||||
@ -29,18 +28,18 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
|
|||||||
double scrollOffset = 0;
|
double scrollOffset = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ExerciseControlBloc({this.exerciseRepository, this.readonly, this.percentToCalculate, @required this.timerBloc})
|
ExerciseControlBloc({this.exerciseRepository, this.readonly, @required this.timerBloc}) : super(ExerciseControlInitial()) {
|
||||||
: super(ExerciseControlInitial()) {
|
print("Exercise ${exerciseRepository.exercise.toJson()}");
|
||||||
oneRepQuantity = exerciseRepository.exercise.quantity;
|
oneRepQuantity = exerciseRepository.exercise.quantity;
|
||||||
oneRepUnitQuantity = exerciseRepository.exercise.unitQuantity;
|
oneRepUnitQuantity = exerciseRepository.exercise.unitQuantity;
|
||||||
initialRM = this.calculate1RM(percent75: false);
|
initialRM = this.calculate1RM(percent75: false);
|
||||||
unitQuantity = this.calculate1RM(percent75: true).roundToDouble();
|
unitQuantity = this.calculate1RM(percent75: true).roundToDouble();
|
||||||
quantity = percentToCalculate == 0.75 ? 12 : 35;
|
quantity = 12;
|
||||||
origQuantity = quantity;
|
origQuantity = quantity;
|
||||||
|
|
||||||
exerciseRepository.setUnitQuantity(unitQuantity);
|
exerciseRepository.setUnitQuantity(unitQuantity);
|
||||||
exerciseRepository.setQuantity(quantity);
|
exerciseRepository.setQuantity(quantity);
|
||||||
print("init quantity: " + quantity.toString());
|
|
||||||
timerBloc.add(TimerStart(duration: 300));
|
timerBloc.add(TimerStart(duration: 300));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +48,6 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
|
|||||||
try {
|
try {
|
||||||
if (event is ExerciseControlLoad) {
|
if (event is ExerciseControlLoad) {
|
||||||
yield ExerciseControlLoading();
|
yield ExerciseControlLoading();
|
||||||
print("init quantity: " + quantity.toString());
|
|
||||||
step = 1;
|
step = 1;
|
||||||
yield ExerciseControlReady();
|
yield ExerciseControlReady();
|
||||||
} else if (event is ExerciseControlQuantityChange) {
|
} else if (event is ExerciseControlQuantityChange) {
|
||||||
@ -83,9 +81,8 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
|
|||||||
}
|
}
|
||||||
exerciseRepository.end = DateTime.now();
|
exerciseRepository.end = DateTime.now();
|
||||||
await exerciseRepository.addExercise();
|
await exerciseRepository.addExercise();
|
||||||
|
exerciseRepository.initExercise();
|
||||||
|
|
||||||
exerciseRepository.setQuantity(quantity);
|
|
||||||
exerciseRepository.exercise.exerciseId = null;
|
|
||||||
step <= 3 ? timerBloc.add(TimerStart(duration: 300)) : timerBloc.add(TimerEnd());
|
step <= 3 ? timerBloc.add(TimerStart(duration: 300)) : timerBloc.add(TimerEnd());
|
||||||
}
|
}
|
||||||
yield ExerciseControlReady();
|
yield ExerciseControlReady();
|
||||||
@ -110,7 +107,7 @@ class ExerciseControlBloc extends Bloc<ExerciseControlEvent, ExerciseControlStat
|
|||||||
print("Weight: $weight repeat: $repeat, $rmWendler, Oconner: $rmOconner");
|
print("Weight: $weight repeat: $repeat, $rmWendler, Oconner: $rmOconner");
|
||||||
double average = (rmWendler + rmOconner) / 2;
|
double average = (rmWendler + rmOconner) / 2;
|
||||||
|
|
||||||
return percent75 ? average * this.percentToCalculate : average;
|
return percent75 ? average * 0.75 : average;
|
||||||
}
|
}
|
||||||
|
|
||||||
int calculateQuantityByUnitQuantity() {
|
int calculateQuantityByUnitQuantity() {
|
||||||
|
@ -74,6 +74,7 @@ class ExerciseExecutePlanAddBloc extends Bloc<ExerciseExecutePlanAddEvent, Exerc
|
|||||||
exerciseRepository.exercise.unit = workoutTree.exerciseType.unit;
|
exerciseRepository.exercise.unit = workoutTree.exerciseType.unit;
|
||||||
workoutTree.executed = true;
|
workoutTree.executed = true;
|
||||||
await exerciseRepository.addExercise();
|
await exerciseRepository.addExercise();
|
||||||
|
exerciseRepository.initExercise();
|
||||||
Track().track(TrackingEvent.my_exercise_plan_execute_save);
|
Track().track(TrackingEvent.my_exercise_plan_execute_save);
|
||||||
step++;
|
step++;
|
||||||
scrollOffset = step * 200.0;
|
scrollOffset = step * 200.0;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:aitrainer_app/bloc/menu/menu_bloc.dart';
|
import 'package:aitrainer_app/bloc/menu/menu_bloc.dart';
|
||||||
import 'package:aitrainer_app/model/cache.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/exercise_type.dart';
|
||||||
import 'package:aitrainer_app/model/fitness_state.dart';
|
import 'package:aitrainer_app/model/fitness_state.dart';
|
||||||
import 'package:aitrainer_app/repository/customer_repository.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.exerciseType = exerciseType;
|
||||||
exerciseRepository.setUnit(exerciseType.unit);
|
exerciseRepository.setUnit(exerciseType.unit);
|
||||||
exerciseRepository.setUnitQuantity(unitQuantity);
|
exerciseRepository.setUnitQuantity(unitQuantity);
|
||||||
|
exerciseRepository.setQuantity(quantity);
|
||||||
exerciseRepository.exercise.exercisePlanDetailId = 0;
|
exerciseRepository.exercise.exercisePlanDetailId = 0;
|
||||||
exerciseRepository.start = DateTime.now();
|
exerciseRepository.start = DateTime.now();
|
||||||
if (Cache().userLoggedIn != null) {
|
if (Cache().userLoggedIn != null) {
|
||||||
@ -74,30 +74,6 @@ class ExerciseNewBloc extends Bloc<ExerciseNewEvent, ExerciseNewState> with Logg
|
|||||||
if (exerciseType.unit == "second") {
|
if (exerciseType.unit == "second") {
|
||||||
stopWatchTimer.rawTime.listen((value) => {timerValue = value, this.setQuantity((value / 1000).toDouble())});
|
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) {
|
void setQuantity(double quantity) {
|
||||||
@ -171,10 +147,11 @@ class ExerciseNewBloc extends Bloc<ExerciseNewEvent, ExerciseNewState> with Logg
|
|||||||
yield ExerciseNewLoading();
|
yield ExerciseNewLoading();
|
||||||
exerciseRepository.end = DateTime.now();
|
exerciseRepository.end = DateTime.now();
|
||||||
await exerciseRepository.addExercise();
|
await exerciseRepository.addExercise();
|
||||||
|
exerciseRepository.initExercise();
|
||||||
menuBloc.add(MenuTreeDown(parent: 0));
|
menuBloc.add(MenuTreeDown(parent: 0));
|
||||||
Cache().initBadges();
|
Cache().initBadges();
|
||||||
Track().track(TrackingEvent.exercise_new, eventValue: exerciseRepository.exerciseType.name);
|
Track().track(TrackingEvent.exercise_new, eventValue: exerciseRepository.exerciseType.name);
|
||||||
yield ExerciseNewReady();
|
yield ExerciseNewSaved();
|
||||||
} else if (event is ExerciseNewBMIAnimate) {
|
} else if (event is ExerciseNewBMIAnimate) {
|
||||||
yield ExerciseNewLoading();
|
yield ExerciseNewLoading();
|
||||||
yield ExerciseNewReady();
|
yield ExerciseNewReady();
|
||||||
|
@ -20,6 +20,10 @@ class ExerciseNewReady extends ExerciseNewState {
|
|||||||
const ExerciseNewReady();
|
const ExerciseNewReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ExerciseNewSaved extends ExerciseNewState {
|
||||||
|
const ExerciseNewSaved();
|
||||||
|
}
|
||||||
|
|
||||||
class ExerciseNewError extends ExerciseNewState {
|
class ExerciseNewError extends ExerciseNewState {
|
||||||
final String message;
|
final String message;
|
||||||
const ExerciseNewError({this.message});
|
const ExerciseNewError({this.message});
|
||||||
|
@ -166,7 +166,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> with Trans, Logging {
|
|||||||
|
|
||||||
void setAbility(String name) {
|
void setAbility(String name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "One Rep Max":
|
case "Muscle Build / Shape Toning":
|
||||||
ability = ExerciseAbility.oneRepMax;
|
ability = ExerciseAbility.oneRepMax;
|
||||||
break;
|
break;
|
||||||
case "Endurance":
|
case "Endurance":
|
||||||
@ -176,7 +176,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> with Trans, Logging {
|
|||||||
ability = ExerciseAbility.running;
|
ability = ExerciseAbility.running;
|
||||||
break;
|
break;
|
||||||
case "Test Center":
|
case "Test Center":
|
||||||
ability = ExerciseAbility.mini_test;
|
ability = ExerciseAbility.mini_test_set;
|
||||||
break;
|
break;
|
||||||
case "My Body":
|
case "My Body":
|
||||||
ability = ExerciseAbility.none;
|
ability = ExerciseAbility.none;
|
||||||
@ -231,7 +231,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> with Trans, Logging {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,6 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> with Logging {
|
|||||||
if (event is SettingsChangeLanguage) {
|
if (event is SettingsChangeLanguage) {
|
||||||
yield SettingsLoading();
|
yield SettingsLoading();
|
||||||
await _changeLang(event.language);
|
await _changeLang(event.language);
|
||||||
Track().track(TrackingEvent.settings_lang);
|
|
||||||
yield SettingsReady(_locale);
|
yield SettingsReady(_locale);
|
||||||
} else if (event is SettingsGetLanguage) {
|
} else if (event is SettingsGetLanguage) {
|
||||||
await AppLanguage().fetchLocale();
|
await AppLanguage().fetchLocale();
|
||||||
@ -47,7 +46,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> with Logging {
|
|||||||
yield SettingsLoading();
|
yield SettingsLoading();
|
||||||
final bool live = event.live;
|
final bool live = event.live;
|
||||||
Cache().setServer(live);
|
Cache().setServer(live);
|
||||||
Track().track(TrackingEvent.settings_server);
|
Track().track(TrackingEvent.settings_server, eventValue: live.toString());
|
||||||
yield SettingsReady(_locale);
|
yield SettingsReady(_locale);
|
||||||
} else if (event is SettingsSetHardware) {
|
} else if (event is SettingsSetHardware) {
|
||||||
yield SettingsLoading();
|
yield SettingsLoading();
|
||||||
|
69
lib/bloc/test_set_control/test_set_control_bloc.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
lib/bloc/test_set_control/test_set_control_event.dart
Normal 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();
|
||||||
|
}
|
28
lib/bloc/test_set_control/test_set_control_state.dart
Normal 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];
|
||||||
|
}
|
@ -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/model/exercise_type.dart';
|
||||||
import 'package:aitrainer_app/repository/workout_tree_repository.dart';
|
import 'package:aitrainer_app/repository/workout_tree_repository.dart';
|
||||||
import 'package:aitrainer_app/service/exercise_plan_service.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:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
@ -18,12 +20,15 @@ part 'test_set_edit_state.dart';
|
|||||||
|
|
||||||
class TestSetEditBloc extends Bloc<TestSetEditEvent, TestSetEditState> {
|
class TestSetEditBloc extends Bloc<TestSetEditEvent, TestSetEditState> {
|
||||||
final String templateName;
|
final String templateName;
|
||||||
|
final String templateNameTranslation;
|
||||||
final WorkoutTreeRepository workoutTreeRepository;
|
final WorkoutTreeRepository workoutTreeRepository;
|
||||||
final MenuBloc menuBloc;
|
final MenuBloc menuBloc;
|
||||||
final List<ExerciseType> _exerciseTypes = List();
|
final List<ExerciseType> _exerciseTypes = List();
|
||||||
|
final List<ExerciseType> _actualExerciseTypes = List();
|
||||||
final HashMap<int, ExerciseType> _exercisePlanDetails = HashMap();
|
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) {
|
if (Cache().exercisePlanTemplates.isNotEmpty) {
|
||||||
Cache().exercisePlanTemplates.forEach((element) {
|
Cache().exercisePlanTemplates.forEach((element) {
|
||||||
final ExercisePlanTemplate template = element as ExercisePlanTemplate;
|
final ExercisePlanTemplate template = element as ExercisePlanTemplate;
|
||||||
@ -31,6 +36,7 @@ class TestSetEditBloc extends Bloc<TestSetEditEvent, TestSetEditState> {
|
|||||||
template.exerciseTypes.forEach((id) {
|
template.exerciseTypes.forEach((id) {
|
||||||
final ExerciseType exerciseType = Cache().getExerciseTypeById(id);
|
final ExerciseType exerciseType = Cache().getExerciseTypeById(id);
|
||||||
_exerciseTypes.add(exerciseType);
|
_exerciseTypes.add(exerciseType);
|
||||||
|
_actualExerciseTypes.add(exerciseType);
|
||||||
_exercisePlanDetails[exerciseType.exerciseTypeId] = exerciseType;
|
_exercisePlanDetails[exerciseType.exerciseTypeId] = exerciseType;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -42,22 +48,36 @@ class TestSetEditBloc extends Bloc<TestSetEditEvent, TestSetEditState> {
|
|||||||
Stream<TestSetEditState> mapEventToState(TestSetEditEvent event) async* {
|
Stream<TestSetEditState> mapEventToState(TestSetEditEvent event) async* {
|
||||||
try {
|
try {
|
||||||
if (event is TestSetEditChangeExerciseType) {
|
if (event is TestSetEditChangeExerciseType) {
|
||||||
|
yield TestSetEditLoading();
|
||||||
final List<ExerciseType> alternatives = workoutTreeRepository.getExerciseTypeAlternatives(event.exerciseTypeId);
|
final List<ExerciseType> alternatives = workoutTreeRepository.getExerciseTypeAlternatives(event.exerciseTypeId);
|
||||||
final ExerciseType exerciseType = Cache().getExerciseTypeById(event.exerciseTypeId);
|
final ExerciseType exerciseType = Cache().getExerciseTypeById(event.exerciseTypeId);
|
||||||
if (event.index > alternatives.length) {
|
|
||||||
/// skip
|
if (_exercisePlanDetails[event.exerciseTypeId] == null) {
|
||||||
_exercisePlanDetails[exerciseType.exerciseTypeId] = null;
|
/// it was skipped
|
||||||
} else if (event.index == 0) {
|
_exercisePlanDetails[exerciseType.exerciseTypeId] = exerciseType;
|
||||||
|
} else {
|
||||||
|
if (event.index == 0) {
|
||||||
_exercisePlanDetails[exerciseType.exerciseTypeId] = exerciseType;
|
_exercisePlanDetails[exerciseType.exerciseTypeId] = exerciseType;
|
||||||
} else {
|
} else {
|
||||||
final changedExerciseType = alternatives[event.index - 1];
|
final changedExerciseType = alternatives[event.index - 1];
|
||||||
_exercisePlanDetails[exerciseType.exerciseTypeId] = changedExerciseType;
|
_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) {
|
} else if (event is TestSetEditSubmit) {
|
||||||
yield TestSetEditLoading();
|
yield TestSetEditLoading();
|
||||||
ExercisePlan exercisePlan = ExercisePlan(templateName, Cache().userLoggedIn.customerId);
|
ExercisePlan exercisePlan = ExercisePlan(templateNameTranslation, Cache().userLoggedIn.customerId);
|
||||||
exercisePlan.private = true;
|
exercisePlan.private = true;
|
||||||
exercisePlan.type = ExerciseAbility.mini_test.toString();
|
exercisePlan.type = ExerciseAbility.mini_test_set.toString();
|
||||||
exercisePlan.dateAdd = DateTime.now();
|
exercisePlan.dateAdd = DateTime.now();
|
||||||
ExercisePlan savedExercisePlan = await ExercisePlanApi().saveExercisePlan(exercisePlan);
|
ExercisePlan savedExercisePlan = await ExercisePlanApi().saveExercisePlan(exercisePlan);
|
||||||
|
|
||||||
@ -67,11 +87,15 @@ class TestSetEditBloc extends Bloc<TestSetEditEvent, TestSetEditState> {
|
|||||||
ExercisePlanDetail exercisePlanDetail = ExercisePlanDetail(entry.value.exerciseTypeId);
|
ExercisePlanDetail exercisePlanDetail = ExercisePlanDetail(entry.value.exerciseTypeId);
|
||||||
exercisePlanDetail.exercisePlanId = savedExercisePlan.exercisePlanId;
|
exercisePlanDetail.exercisePlanId = savedExercisePlan.exercisePlanId;
|
||||||
exercisePlanDetail.serie = 1;
|
exercisePlanDetail.serie = 1;
|
||||||
|
|
||||||
ExercisePlanDetail savedDetail = await ExercisePlanApi().saveExercisePlanDetail(exercisePlanDetail);
|
ExercisePlanDetail savedDetail = await ExercisePlanApi().saveExercisePlanDetail(exercisePlanDetail);
|
||||||
details.add(savedDetail);
|
exercisePlanDetail.exercisePlanDetailId = savedDetail.exercisePlanDetailId;
|
||||||
|
exercisePlanDetail.exercises = List();
|
||||||
|
details.add(exercisePlanDetail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Cache().saveActiveExercisePlan(exercisePlan, details);
|
Cache().saveActiveExercisePlan(exercisePlan, details);
|
||||||
|
Track().track(TrackingEvent.test_set_edit, eventValue: templateName);
|
||||||
yield TestSetEditSaved();
|
yield TestSetEditSaved();
|
||||||
}
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
@ -80,4 +104,14 @@ class TestSetEditBloc extends Bloc<TestSetEditEvent, TestSetEditState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List get exerciseTypes => this._exerciseTypes;
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,14 @@ class TestSetEditChangeExerciseType extends TestSetEditEvent {
|
|||||||
List<Object> get props => [index, exerciseTypeId];
|
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 {
|
class TestSetEditSkipExerciseType extends TestSetEditEvent {
|
||||||
final int exerciseTypeId;
|
final int exerciseTypeId;
|
||||||
const TestSetEditSkipExerciseType({this.exerciseTypeId});
|
const TestSetEditSkipExerciseType({this.exerciseTypeId});
|
||||||
|
358
lib/bloc/test_set_execute/test_set_execute_bloc.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
54
lib/bloc/test_set_execute/test_set_execute_event.dart
Normal 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();
|
||||||
|
}
|
32
lib/bloc/test_set_execute/test_set_execute_state.dart
Normal 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];
|
||||||
|
}
|
59
lib/bloc/test_set_new/test_set_new_bloc.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
lib/bloc/test_set_new/test_set_new_event.dart
Normal 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();
|
||||||
|
}
|
28
lib/bloc/test_set_new/test_set_new_state.dart
Normal 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];
|
||||||
|
}
|
529
lib/library/dropdown_search.dart
Normal 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;
|
||||||
|
}
|
703
lib/library/gradient_bottom_navigation_bar.dart
Normal 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
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
407
lib/library/select_dialog.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
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/push_notifications.dart';
|
||||||
import 'package:aitrainer_app/repository/customer_repository.dart';
|
import 'package:aitrainer_app/repository/customer_repository.dart';
|
||||||
import 'package:aitrainer_app/repository/workout_tree_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_log_page.dart';
|
||||||
import 'package:aitrainer_app/view/exercise_plan_custom_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_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/login.dart';
|
||||||
import 'package:aitrainer_app/view/exercise_new_page.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_body_page.dart';
|
||||||
import 'package:aitrainer_app/view/mydevelopment_muscle_page.dart';
|
import 'package:aitrainer_app/view/mydevelopment_muscle_page.dart';
|
||||||
import 'package:aitrainer_app/view/mydevelopment_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/reset_password.dart';
|
||||||
import 'package:aitrainer_app/view/sales_page.dart';
|
import 'package:aitrainer_app/view/sales_page.dart';
|
||||||
import 'package:aitrainer_app/view/settings.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_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:aitrainer_app/widgets/home.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:firebase_analytics/observer.dart';
|
import 'package:firebase_analytics/observer.dart';
|
||||||
@ -151,6 +153,9 @@ Future<Null> main() async {
|
|||||||
BlocProvider<TimerBloc>(
|
BlocProvider<TimerBloc>(
|
||||||
create: (BuildContext context) => TimerBloc(),
|
create: (BuildContext context) => TimerBloc(),
|
||||||
),
|
),
|
||||||
|
BlocProvider<TestSetExecuteBloc>(
|
||||||
|
create: (BuildContext context) => TestSetExecuteBloc(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: WorkoutTestApp(),
|
child: WorkoutTestApp(),
|
||||||
));
|
));
|
||||||
@ -211,10 +216,8 @@ class WorkoutTestApp extends StatelessWidget {
|
|||||||
'login': (context) => LoginPage(),
|
'login': (context) => LoginPage(),
|
||||||
'resetPassword': (context) => ResetPasswordPage(),
|
'resetPassword': (context) => ResetPasswordPage(),
|
||||||
'registration': (context) => RegistrationPage(),
|
'registration': (context) => RegistrationPage(),
|
||||||
'menu_page': (context) => MenuPage(),
|
|
||||||
'account': (context) => AccountPage(),
|
'account': (context) => AccountPage(),
|
||||||
'settings': (context) => SettingsPage(),
|
'settings': (context) => SettingsPage(),
|
||||||
'exerciseTypeDescription': (context) => ExerciseTypeDescription(),
|
|
||||||
'myDevelopment': (context) => MyDevelopmentPage(),
|
'myDevelopment': (context) => MyDevelopmentPage(),
|
||||||
'myExercisePlan': (context) => MyExercisePlanPage(),
|
'myExercisePlan': (context) => MyExercisePlanPage(),
|
||||||
'exerciseLogPage': (context) => ExerciseLogPage(),
|
'exerciseLogPage': (context) => ExerciseLogPage(),
|
||||||
@ -228,6 +231,9 @@ class WorkoutTestApp extends StatelessWidget {
|
|||||||
'evaluationPage': (context) => EvaluationPage(),
|
'evaluationPage': (context) => EvaluationPage(),
|
||||||
'salesPage': (context) => SalesPage(),
|
'salesPage': (context) => SalesPage(),
|
||||||
'testSetEdit': (context) => TestSetEdit(),
|
'testSetEdit': (context) => TestSetEdit(),
|
||||||
|
'testSetExecute': (context) => TestSetExecute(),
|
||||||
|
'testSetNew': (context) => TestSetNew(),
|
||||||
|
'testSetControl': (context) => TestSetControl(),
|
||||||
},
|
},
|
||||||
initialRoute: 'home',
|
initialRoute: 'home',
|
||||||
title: 'WorkoutTest',
|
title: 'WorkoutTest',
|
||||||
|
@ -24,6 +24,7 @@ import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
|
|||||||
import 'package:package_info/package_info.dart';
|
import 'package:package_info/package_info.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:aitrainer_app/model/exercise_type.dart';
|
import 'package:aitrainer_app/model/exercise_type.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'customer_exercise_device.dart';
|
import 'customer_exercise_device.dart';
|
||||||
import 'exercise_device.dart';
|
import 'exercise_device.dart';
|
||||||
@ -66,6 +67,7 @@ class Cache with Logging {
|
|||||||
static final String loginTypeKey = 'login_type';
|
static final String loginTypeKey = 'login_type';
|
||||||
static final String timerDisplayKey = 'timer_display';
|
static final String timerDisplayKey = 'timer_display';
|
||||||
static final String activeExercisePlanKey = 'active_exercise_plan';
|
static final String activeExercisePlanKey = 'active_exercise_plan';
|
||||||
|
static final String activeExercisePlanDateKey = 'active_exercise_plan_date';
|
||||||
static final String activeExercisePlanDetailsKey = 'active_exercise_details_plan';
|
static final String activeExercisePlanDetailsKey = 'active_exercise_details_plan';
|
||||||
|
|
||||||
static String baseUrl = 'http://aitrainer.info:8888/api/';
|
static String baseUrl = 'http://aitrainer.info:8888/api/';
|
||||||
@ -144,14 +146,27 @@ class Cache with Logging {
|
|||||||
this.activeExercisePlan = exercisePlan;
|
this.activeExercisePlan = exercisePlan;
|
||||||
this.activeExercisePlanDetails = exercisePlanDetails;
|
this.activeExercisePlanDetails = exercisePlanDetails;
|
||||||
String exercisePlanJson = JsonEncoder().convert(exercisePlan.toJson());
|
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();
|
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
|
||||||
SharedPreferences sharedPreferences;
|
SharedPreferences sharedPreferences;
|
||||||
sharedPreferences = await prefs;
|
sharedPreferences = await prefs;
|
||||||
|
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
sharedPreferences.setString(Cache.activeExercisePlanKey, exercisePlanJson);
|
sharedPreferences.setString(Cache.activeExercisePlanKey, exercisePlanJson);
|
||||||
sharedPreferences.setString(Cache.activeExercisePlanDetailsKey, detailsJson);
|
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 {
|
Future<void> getActiveExercisePlan() async {
|
||||||
@ -159,6 +174,25 @@ class Cache with Logging {
|
|||||||
SharedPreferences sharedPreferences;
|
SharedPreferences sharedPreferences;
|
||||||
sharedPreferences = await prefs;
|
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);
|
String exercisePlanJson = sharedPreferences.getString(Cache.activeExercisePlanKey);
|
||||||
if (exercisePlanJson != null) {
|
if (exercisePlanJson != null) {
|
||||||
final Map<String, dynamic> map = JsonDecoder().convert(exercisePlanJson);
|
final Map<String, dynamic> map = JsonDecoder().convert(exercisePlanJson);
|
||||||
@ -167,8 +201,9 @@ class Cache with Logging {
|
|||||||
|
|
||||||
String detailsJson = sharedPreferences.getString(Cache.activeExercisePlanDetailsKey);
|
String detailsJson = sharedPreferences.getString(Cache.activeExercisePlanDetailsKey);
|
||||||
if (detailsJson != null) {
|
if (detailsJson != null) {
|
||||||
|
print("Details $detailsJson");
|
||||||
Iterable json = jsonDecode(detailsJson);
|
Iterable json = jsonDecode(detailsJson);
|
||||||
this.activeExercisePlanDetails = json.map((details) => ExercisePlanDetail.fromJson(details)).toList();
|
this.activeExercisePlanDetails = json.map((details) => ExercisePlanDetail.fromJsonWithExerciseList(details)).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,4 +59,9 @@ class Exercise {
|
|||||||
newExercise.exercisePlanDetailId = this.exercisePlanDetailId;
|
newExercise.exercisePlanDetailId = this.exercisePlanDetailId;
|
||||||
return newExercise;
|
return newExercise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return this.toJson().toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
extension ExerciseAbilityExt on ExerciseAbility {
|
||||||
|
String enumToString() => this.toString().split(".").last;
|
||||||
bool equalsTo(ExerciseAbility ability) => this.toString() == ability.toString();
|
bool equalsTo(ExerciseAbility ability) => this.toString() == ability.toString();
|
||||||
bool equalsStringTo(String ability) => this.toString() == ability;
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
class ExercisePlanDetail {
|
||||||
int exercisePlanDetailId;
|
int exercisePlanDetailId;
|
||||||
@ -8,6 +18,10 @@ class ExercisePlanDetail {
|
|||||||
int repeats;
|
int repeats;
|
||||||
String weightEquation;
|
String weightEquation;
|
||||||
|
|
||||||
|
List exercises;
|
||||||
|
bool finished;
|
||||||
|
ExercisePlanDetailState state = ExercisePlanDetailState.start;
|
||||||
|
|
||||||
ExerciseType exerciseType;
|
ExerciseType exerciseType;
|
||||||
String change; // 1: update -1:delete 0: new
|
String change; // 1: update -1:delete 0: new
|
||||||
|
|
||||||
@ -24,6 +38,28 @@ class ExercisePlanDetail {
|
|||||||
this.weightEquation = json['weightEquation'];
|
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() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"exercisePlanId": exercisePlanId,
|
"exercisePlanId": exercisePlanId,
|
||||||
"exerciseTypeId": exerciseTypeId,
|
"exerciseTypeId": exerciseTypeId,
|
||||||
@ -31,4 +67,14 @@ class ExercisePlanDetail {
|
|||||||
"repeats": repeats,
|
"repeats": repeats,
|
||||||
"weightEquation": weightEquation
|
"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(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
class ExerciseTree {
|
class ExerciseTree {
|
||||||
|
/// treeId
|
||||||
int treeId;
|
int treeId;
|
||||||
|
|
||||||
|
/// parentId
|
||||||
int parentId;
|
int parentId;
|
||||||
|
|
||||||
|
/// name
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
|
/// imageUrl
|
||||||
String imageUrl;
|
String imageUrl;
|
||||||
|
|
||||||
|
/// active
|
||||||
bool active;
|
bool active;
|
||||||
|
|
||||||
|
/// nameTranslation
|
||||||
String nameTranslation;
|
String nameTranslation;
|
||||||
|
|
||||||
|
/// sort
|
||||||
int sort;
|
int sort;
|
||||||
|
|
||||||
ExerciseTree();
|
ExerciseTree();
|
||||||
|
@ -1,24 +1,51 @@
|
|||||||
import 'package:aitrainer_app/model/exercise_ability.dart';
|
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 {
|
class ExerciseType {
|
||||||
|
///exerciseTypeId
|
||||||
int exerciseTypeId;
|
int exerciseTypeId;
|
||||||
//int treeId;
|
|
||||||
|
/// name
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
|
/// description
|
||||||
String description;
|
String description;
|
||||||
BinaryCodec video;
|
|
||||||
|
/// unit
|
||||||
String unit;
|
String unit;
|
||||||
|
|
||||||
|
/// unitQuantity
|
||||||
String unitQuantity;
|
String unitQuantity;
|
||||||
|
|
||||||
|
/// unitQuantityUnit
|
||||||
String unitQuantityUnit;
|
String unitQuantityUnit;
|
||||||
|
|
||||||
|
///active
|
||||||
bool active;
|
bool active;
|
||||||
|
|
||||||
|
/// base
|
||||||
bool base;
|
bool base;
|
||||||
|
|
||||||
|
/// imageUrl
|
||||||
String imageUrl = "";
|
String imageUrl = "";
|
||||||
|
|
||||||
|
/// nameTranslation
|
||||||
String nameTranslation = "";
|
String nameTranslation = "";
|
||||||
|
|
||||||
|
/// descriptionTranslation
|
||||||
String descriptionTranslation = "";
|
String descriptionTranslation = "";
|
||||||
|
|
||||||
|
/// devices[]
|
||||||
List<int> devices = List();
|
List<int> devices = List();
|
||||||
|
|
||||||
|
/// parents[]
|
||||||
List<int> parents = List();
|
List<int> parents = List();
|
||||||
|
|
||||||
|
/// alternatives []
|
||||||
List<int> alternatives = List();
|
List<int> alternatives = List();
|
||||||
|
|
||||||
|
/// ability
|
||||||
ExerciseAbility ability;
|
ExerciseAbility ability;
|
||||||
|
|
||||||
ExerciseType({this.name, this.description});
|
ExerciseType({this.name, this.description});
|
||||||
@ -37,8 +64,8 @@ class ExerciseType {
|
|||||||
this.imageUrl = json['images'][0]['url'];
|
this.imageUrl = json['images'][0]['url'];
|
||||||
}
|
}
|
||||||
if (json['translations'].length > 0) {
|
if (json['translations'].length > 0) {
|
||||||
this.nameTranslation = json['translations'][0]['name'];
|
this.nameTranslation = AppLanguage().appLocal == Locale('hu') ? json['translations'][0]['name'] : json['name'];
|
||||||
this.descriptionTranslation = json['translations'][0]['description'];
|
this.descriptionTranslation = AppLanguage().appLocal == Locale('hu') ? json['translations'][0]['description'] : json['description'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json['devices'].length > 0) {
|
if (json['devices'].length > 0) {
|
||||||
@ -86,11 +113,12 @@ class ExerciseType {
|
|||||||
return this.ability;
|
return this.ability;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isEndurance() {
|
|
||||||
return this.ability.equalsTo(ExerciseAbility.endurance);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is1RM() {
|
bool is1RM() {
|
||||||
return this.ability.equalsTo(ExerciseAbility.oneRepMax);
|
return this.ability.equalsTo(ExerciseAbility.oneRepMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return this.toJson().toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ class WorkoutMenuTree {
|
|||||||
bool base;
|
bool base;
|
||||||
|
|
||||||
bool is1RM;
|
bool is1RM;
|
||||||
bool isEndurance;
|
|
||||||
bool isRunning;
|
bool isRunning;
|
||||||
List<WorkoutType> workoutTypes = List();
|
List<WorkoutType> workoutTypes = List();
|
||||||
bool selected = false;
|
bool selected = false;
|
||||||
@ -42,24 +41,8 @@ class WorkoutMenuTree {
|
|||||||
String parentNameEnglish;
|
String parentNameEnglish;
|
||||||
int sort;
|
int sort;
|
||||||
|
|
||||||
WorkoutMenuTree(
|
WorkoutMenuTree(this.id, this.parent, this.name, this.imageName, this.color, this.fontSize, this.child, this.exerciseTypeId,
|
||||||
this.id,
|
this.exerciseType, this.base, this.is1RM, this.isRunning, this.nameEnglish, this.parentName, this.parentNameEnglish, this.sort);
|
||||||
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);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@ -73,7 +56,6 @@ class WorkoutMenuTree {
|
|||||||
"exerciseTypeId": exerciseTypeId.toString(),
|
"exerciseTypeId": exerciseTypeId.toString(),
|
||||||
"base": base.toString(),
|
"base": base.toString(),
|
||||||
"is1RM": is1RM.toString(),
|
"is1RM": is1RM.toString(),
|
||||||
"isEndurance": isEndurance.toString(),
|
|
||||||
"isRunning": isRunning.toString(),
|
"isRunning": isRunning.toString(),
|
||||||
"sort": sort,
|
"sort": sort,
|
||||||
};
|
};
|
||||||
|
@ -73,7 +73,7 @@ class ExerciseRepository {
|
|||||||
|
|
||||||
Exercise getExercise() => this.exercise;
|
Exercise getExercise() => this.exercise;
|
||||||
|
|
||||||
Future<void> addExercise() async {
|
Future<Exercise> addExercise() async {
|
||||||
final Exercise modelExercise = this.exercise;
|
final Exercise modelExercise = this.exercise;
|
||||||
modelExercise.customerId = this.customer.customerId;
|
modelExercise.customerId = this.customer.customerId;
|
||||||
modelExercise.exerciseTypeId = this.exerciseType.exerciseTypeId;
|
modelExercise.exerciseTypeId = this.exerciseType.exerciseTypeId;
|
||||||
@ -94,17 +94,18 @@ class ExerciseRepository {
|
|||||||
Cache().addExerciseTrainee(savedExercise);
|
Cache().addExerciseTrainee(savedExercise);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* this.actualExerciseList.forEach((element) {
|
return savedExercise;
|
||||||
print("$index. actual: " + element.toJson().toString());
|
}
|
||||||
}); */
|
|
||||||
|
|
||||||
|
void initExercise() {
|
||||||
this.createNew();
|
this.createNew();
|
||||||
this.exerciseType = exerciseType;
|
this.exerciseType = exerciseType;
|
||||||
this.setUnit(exerciseType.unit);
|
this.setUnit(exerciseType.unit);
|
||||||
exercise.exerciseTypeId = this.exerciseType.exerciseTypeId;
|
exercise.exerciseTypeId = this.exerciseType.exerciseTypeId;
|
||||||
this.setQuantity(quantity);
|
this.setQuantity(12);
|
||||||
this.setUnitQuantity(modelExercise.unitQuantity);
|
this.setUnitQuantity(30);
|
||||||
this.exercise.exercisePlanDetailId = 0;
|
this.exercise.exercisePlanDetailId = 0;
|
||||||
|
exercise.exerciseId = 0;
|
||||||
this.start = DateTime.now();
|
this.start = DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,7 +304,7 @@ class ExerciseRepository {
|
|||||||
double quantity = exercise.quantity == null ? 0 : exercise.quantity;
|
double quantity = exercise.quantity == null ? 0 : exercise.quantity;
|
||||||
summary += delimiter + quantity.toStringAsFixed(0);
|
summary += delimiter + quantity.toStringAsFixed(0);
|
||||||
ExerciseType exerciseType = Cache().getExerciseTypeById(exercise.exerciseTypeId);
|
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") {
|
if (exerciseType.unitQuantity == "1") {
|
||||||
summary += "x" + exercise.unitQuantity.toStringAsFixed(0);
|
summary += "x" + exercise.unitQuantity.toStringAsFixed(0);
|
||||||
}
|
}
|
||||||
|
@ -11,23 +11,12 @@ import 'package:aitrainer_app/service/exercise_type_service.dart';
|
|||||||
import 'package:aitrainer_app/service/logging.dart';
|
import 'package:aitrainer_app/service/logging.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Antagonist {
|
enum Antagonist { chest, biceps, triceps, back, shoulders, core, thigh, calf }
|
||||||
static String chest = "Chest";
|
|
||||||
static int chestNr = 1;
|
extension AntagonistExt on Antagonist {
|
||||||
static String biceps = "Biceps";
|
bool equalsTo(Antagonist type) => this.toString() == type.toString();
|
||||||
static int bicepsNr = 2;
|
bool equalsStringTo(String type) => this.toString() == type;
|
||||||
static String triceps = "Triceps";
|
String enumToString() => this.toString().split(".").last;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class WorkoutTreeRepository with Logging {
|
class WorkoutTreeRepository with Logging {
|
||||||
@ -37,17 +26,6 @@ class WorkoutTreeRepository with Logging {
|
|||||||
WorkoutType workoutType;
|
WorkoutType workoutType;
|
||||||
final List<WorkoutMenuTree> menuAsExercise = List();
|
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 {
|
Future<void> createTree() async {
|
||||||
//if (Cache().getExerciseTree().length > 0 || Cache().getWorkoutMenuTree().length > 0) return;
|
//if (Cache().getExerciseTree().length > 0 || Cache().getWorkoutMenuTree().length > 0) return;
|
||||||
isEnglish = AppLanguage().appLocal == Locale('en');
|
isEnglish = AppLanguage().appLocal == Locale('en');
|
||||||
@ -66,20 +44,16 @@ class WorkoutTreeRepository with Logging {
|
|||||||
exerciseTypes = await ExerciseTypeApi().getExerciseTypes();
|
exerciseTypes = await ExerciseTypeApi().getExerciseTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exerciseTree.sort((a, b) => a.sort.compareTo(b.sort));
|
||||||
|
|
||||||
exerciseTree.forEach((treeItem) async {
|
exerciseTree.forEach((treeItem) async {
|
||||||
//log(" -- TreeItem " + treeItem.toJson().toString() + " active " + treeItem.active.toString());
|
//log(" -- TreeItem " + treeItem.toJson().toString() + " active " + treeItem.active.toString());
|
||||||
if (treeItem.active == true) {
|
if (treeItem.active == true) {
|
||||||
String treeName = isEnglish ? treeItem.name : treeItem.nameTranslation;
|
String treeName = isEnglish ? treeItem.name : treeItem.nameTranslation;
|
||||||
//String assetImage = await _buildImage(treeItem.imageUrl);
|
|
||||||
|
|
||||||
bool is1RM = treeItem.name == 'One Rep Max' ? true : false;
|
bool is1RM = treeItem.name.contains("Muscle") || treeItem.name.contains("Shape") ? true : false;
|
||||||
if (is1RM == false && treeItem.parentId != 0) {
|
if (!is1RM) {
|
||||||
is1RM = isParent1RM(treeItem.parentId);
|
is1RM = this.isParent1RM(treeItem.parentId);
|
||||||
}
|
|
||||||
|
|
||||||
bool isEndurance = treeItem.name == 'Endurance' ? true : false;
|
|
||||||
if (isEndurance == false && treeItem.parentId != 0) {
|
|
||||||
isEndurance = isParentEndurance(treeItem.parentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isRunning = treeItem.name == "Cardio" ? true : false;
|
bool isRunning = treeItem.name == "Cardio" ? true : false;
|
||||||
@ -99,7 +73,6 @@ class WorkoutTreeRepository with Logging {
|
|||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
is1RM,
|
is1RM,
|
||||||
isEndurance,
|
|
||||||
isRunning,
|
isRunning,
|
||||||
treeItem.name,
|
treeItem.name,
|
||||||
parent != null ? parent.name : "",
|
parent != null ? parent.name : "",
|
||||||
@ -107,7 +80,7 @@ class WorkoutTreeRepository with Logging {
|
|||||||
treeItem.sort);
|
treeItem.sort);
|
||||||
menuItem = this.setWorkoutTypes(menuItem, treeItem);
|
menuItem = this.setWorkoutTypes(menuItem, treeItem);
|
||||||
this.tree[treeItem.name + "_" + treeItem.parentId.toString()] = menuItem;
|
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) {
|
if (exerciseType.parents.isNotEmpty) {
|
||||||
exerciseType.parents.forEach((parentId) {
|
exerciseType.parents.forEach((parentId) {
|
||||||
bool is1RM = this.isParent1RM(parentId);
|
bool is1RM = this.isParent1RM(parentId);
|
||||||
bool isEndurance = this.isParentEndurance(parentId);
|
if (is1RM) {
|
||||||
if (is1RM) exerciseType.setAbility(ExerciseAbility.oneRepMax);
|
exerciseType.setAbility(ExerciseAbility.oneRepMax);
|
||||||
if (isEndurance) exerciseType.setAbility(ExerciseAbility.endurance);
|
}
|
||||||
bool isRunning = this.isParentRunning(parentId);
|
bool isRunning = this.isParentRunning(parentId);
|
||||||
if (isRunning) exerciseType.setAbility(ExerciseAbility.running);
|
if (isRunning) {
|
||||||
|
is1RM = false;
|
||||||
|
exerciseType.setAbility(ExerciseAbility.running);
|
||||||
|
}
|
||||||
WorkoutMenuTree parent = getParentItem(parentId);
|
WorkoutMenuTree parent = getParentItem(parentId);
|
||||||
WorkoutMenuTree menuItem = WorkoutMenuTree(
|
WorkoutMenuTree menuItem = WorkoutMenuTree(
|
||||||
exerciseType.exerciseTypeId,
|
exerciseType.exerciseTypeId,
|
||||||
@ -138,7 +114,6 @@ class WorkoutTreeRepository with Logging {
|
|||||||
exerciseType,
|
exerciseType,
|
||||||
exerciseType.base,
|
exerciseType.base,
|
||||||
is1RM,
|
is1RM,
|
||||||
isEndurance,
|
|
||||||
isRunning,
|
isRunning,
|
||||||
exerciseType.name,
|
exerciseType.name,
|
||||||
parent != null ? parent.name : "",
|
parent != null ? parent.name : "",
|
||||||
@ -146,13 +121,7 @@ class WorkoutTreeRepository with Logging {
|
|||||||
0);
|
0);
|
||||||
this.tree[exerciseType.name] = menuItem;
|
this.tree[exerciseType.name] = menuItem;
|
||||||
menuAsExercise.add(menuItem);
|
menuAsExercise.add(menuItem);
|
||||||
//log("WorkoutMenuTree item " + menuItem.toJson().toString());
|
//log("ExerciseType in Menu item ${exerciseType.toJson()} is1RM: $is1RM");
|
||||||
/* log("ExerciseType in Menu item " +
|
|
||||||
exerciseType.toJson().toString() +
|
|
||||||
" is1RM: " +
|
|
||||||
is1RM.toString() +
|
|
||||||
" isEndurance: " +
|
|
||||||
isEndurance.toString()); */
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
//log("No Parents " + exerciseType.toJson().toString());
|
//log("No Parents " + exerciseType.toJson().toString());
|
||||||
@ -215,20 +184,6 @@ class WorkoutTreeRepository with Logging {
|
|||||||
return isTreeItem1RM;
|
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(int parentId) {
|
||||||
bool isChild = true;
|
bool isChild = true;
|
||||||
|
|
||||||
@ -247,7 +202,7 @@ class WorkoutTreeRepository with Logging {
|
|||||||
this.getBranch(parentId).forEach((key, value) {
|
this.getBranch(parentId).forEach((key, value) {
|
||||||
WorkoutMenuTree workoutTree = value;
|
WorkoutMenuTree workoutTree = value;
|
||||||
isChild = isChild && workoutTree.child;
|
isChild = isChild && workoutTree.child;
|
||||||
isGym = isGym && (workoutTree.is1RM || workoutTree.isEndurance);
|
isGym = isGym && (workoutTree.is1RM);
|
||||||
});
|
});
|
||||||
return isChild && isGym;
|
return isChild && isGym;
|
||||||
}
|
}
|
||||||
@ -308,7 +263,6 @@ class WorkoutTreeRepository with Logging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<WorkoutMenuTree> list = List();
|
List<WorkoutMenuTree> list = List();
|
||||||
list.add(workoutMenuTree);
|
|
||||||
alternatives.forEach((element) {
|
alternatives.forEach((element) {
|
||||||
final WorkoutMenuTree alternativeMenuItem = this.getMenuItemByExerciseTypeId(element.exerciseTypeId);
|
final WorkoutMenuTree alternativeMenuItem = this.getMenuItemByExerciseTypeId(element.exerciseTypeId);
|
||||||
list.add(alternativeMenuItem);
|
list.add(alternativeMenuItem);
|
||||||
@ -335,12 +289,23 @@ class WorkoutTreeRepository with Logging {
|
|||||||
return list;
|
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() {
|
void sortByMuscleType() {
|
||||||
sortedTree = SplayTreeMap<String, List<WorkoutMenuTree>>();
|
sortedTree = SplayTreeMap<String, List<WorkoutMenuTree>>();
|
||||||
tree.forEach((key, value) {
|
tree.forEach((key, value) {
|
||||||
WorkoutMenuTree workoutTree = value as WorkoutMenuTree;
|
WorkoutMenuTree workoutTree = value as WorkoutMenuTree;
|
||||||
if (workoutTree.nameEnglish != 'One Rep Max' && workoutTree.is1RM && workoutTree.exerciseTypeId == 0) {
|
if (!workoutTree.nameEnglish.contains('Muscle Build') && workoutTree.is1RM && workoutTree.exerciseTypeId == 0) {
|
||||||
String treeName = _antagonist[workoutTree.nameEnglish].toString() + ". " + workoutTree.name;
|
String treeName = getAntagonistSort(workoutTree.nameEnglish) + ". " + workoutTree.name;
|
||||||
|
print("TreeName $treeName ${workoutTree.name}");
|
||||||
sortedTree[treeName] = this.getBranchList(workoutTree.id);
|
sortedTree[treeName] = this.getBranchList(workoutTree.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -19,7 +19,6 @@ class ExerciseTreeApi with Logging {
|
|||||||
|
|
||||||
if (exerciseTree != null) {
|
if (exerciseTree != null) {
|
||||||
await Future.forEach(exerciseTree, (element) async {
|
await Future.forEach(exerciseTree, (element) async {
|
||||||
//exerciseTree.forEach((element) async {
|
|
||||||
element.imageUrl = await buildImage(element.imageUrl, element.treeId);
|
element.imageUrl = await buildImage(element.imageUrl, element.treeId);
|
||||||
});
|
});
|
||||||
log("ExerciseTree downloaded");
|
log("ExerciseTree downloaded");
|
||||||
@ -55,10 +54,11 @@ class ExerciseTreeApi with Logging {
|
|||||||
if (parent.exerciseTreeChildId == element.treeId) {
|
if (parent.exerciseTreeChildId == element.treeId) {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
ExerciseTree newElement = element.copy(parent.exerciseTreeParentId);
|
ExerciseTree newElement = element.copy(parent.exerciseTreeParentId);
|
||||||
|
newElement.sort = parent.sort ?? 0;
|
||||||
exerciseTree.add(newElement);
|
exerciseTree.add(newElement);
|
||||||
} else {
|
} else {
|
||||||
element.parentId = parent.exerciseTreeParentId;
|
element.parentId = parent.exerciseTreeParentId;
|
||||||
element.sort = parent.sort;
|
element.sort = parent.sort ?? 0;
|
||||||
exerciseTree[treeIndex].parentId = parent.exerciseTreeParentId;
|
exerciseTree[treeIndex].parentId = parent.exerciseTreeParentId;
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
|
@ -66,6 +66,7 @@ class PackageApi {
|
|||||||
exerciseTree = this.getExerciseTreeParents(exerciseTree, exerciseTreeParents);
|
exerciseTree = this.getExerciseTreeParents(exerciseTree, exerciseTreeParents);
|
||||||
if (exerciseTree != null) {
|
if (exerciseTree != null) {
|
||||||
await Future.forEach(exerciseTree, (element) async {
|
await Future.forEach(exerciseTree, (element) async {
|
||||||
|
print("Tree ${element.toJson()}");
|
||||||
element.imageUrl = await ExerciseTreeApi().buildImage(element.imageUrl, element.treeId);
|
element.imageUrl = await ExerciseTreeApi().buildImage(element.imageUrl, element.treeId);
|
||||||
});
|
});
|
||||||
Cache().setExerciseTree(exerciseTree);
|
Cache().setExerciseTree(exerciseTree);
|
||||||
@ -84,10 +85,13 @@ class PackageApi {
|
|||||||
if (parent.exerciseTreeChildId == element.treeId) {
|
if (parent.exerciseTreeChildId == element.treeId) {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
ExerciseTree newElement = element.copy(parent.exerciseTreeParentId);
|
ExerciseTree newElement = element.copy(parent.exerciseTreeParentId);
|
||||||
|
newElement.sort = parent.sort;
|
||||||
exerciseTree.add(newElement);
|
exerciseTree.add(newElement);
|
||||||
} else {
|
} else {
|
||||||
element.parentId = parent.exerciseTreeParentId;
|
element.parentId = parent.exerciseTreeParentId;
|
||||||
|
element.sort = parent.sort;
|
||||||
exerciseTree[treeIndex].parentId = parent.exerciseTreeParentId;
|
exerciseTree[treeIndex].parentId = parent.exerciseTreeParentId;
|
||||||
|
exerciseTree[treeIndex].sort = parent.sort;
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ enum TrackingEvent {
|
|||||||
purchase_request,
|
purchase_request,
|
||||||
purchase_successful,
|
purchase_successful,
|
||||||
exercise_new,
|
exercise_new,
|
||||||
|
exercise_new_paralell,
|
||||||
result,
|
result,
|
||||||
exercise_log,
|
exercise_log,
|
||||||
exercise_log_open,
|
exercise_log_open,
|
||||||
@ -40,7 +41,9 @@ enum TrackingEvent {
|
|||||||
exercise_device,
|
exercise_device,
|
||||||
customer_change,
|
customer_change,
|
||||||
settings_lang,
|
settings_lang,
|
||||||
settings_server
|
settings_server,
|
||||||
|
test_set_edit,
|
||||||
|
test_set_new,
|
||||||
}
|
}
|
||||||
|
|
||||||
T enumFromString<T>(Iterable<T> values, String value) {
|
T enumFromString<T>(Iterable<T> values, String value) {
|
||||||
|
@ -36,15 +36,13 @@ class _ExerciseControlPage extends State<ExerciseControlPage> with Trans {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
LinkedHashMap arguments = ModalRoute.of(context).settings.arguments;
|
LinkedHashMap arguments = ModalRoute.of(context).settings.arguments;
|
||||||
final ExerciseRepository exerciseRepository = arguments['exerciseRepository'];
|
final ExerciseRepository exerciseRepository = arguments['exerciseRepository'];
|
||||||
final double percent = arguments['percent'];
|
|
||||||
final bool readonly = arguments['readonly'];
|
final bool readonly = arguments['readonly'];
|
||||||
setContext(context);
|
setContext(context);
|
||||||
// ignore: close_sinks
|
// ignore: close_sinks
|
||||||
TimerBloc timerBloc = BlocProvider.of<TimerBloc>(context);
|
TimerBloc timerBloc = BlocProvider.of<TimerBloc>(context);
|
||||||
|
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => ExerciseControlBloc(
|
create: (context) => ExerciseControlBloc(exerciseRepository: exerciseRepository, readonly: readonly, timerBloc: timerBloc)
|
||||||
exerciseRepository: exerciseRepository, percentToCalculate: percent, readonly: readonly, timerBloc: timerBloc)
|
|
||||||
..add(ExerciseControlLoad()),
|
..add(ExerciseControlLoad()),
|
||||||
child: BlocConsumer<ExerciseControlBloc, ExerciseControlState>(listener: (context, state) {
|
child: BlocConsumer<ExerciseControlBloc, ExerciseControlState>(listener: (context, state) {
|
||||||
if (state is ExerciseControlError) {
|
if (state is ExerciseControlError) {
|
||||||
|
@ -2,8 +2,7 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:aitrainer_app/bloc/exercise_new/exercise_new_bloc.dart';
|
import 'package:aitrainer_app/bloc/exercise_new/exercise_new_bloc.dart';
|
||||||
import 'package:aitrainer_app/bloc/menu/menu_bloc.dart';
|
import 'package:aitrainer_app/bloc/menu/menu_bloc.dart';
|
||||||
import 'package:aitrainer_app/library/custom_icon_icons.dart';
|
import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart';
|
||||||
import 'package:aitrainer_app/util/app_language.dart';
|
|
||||||
import 'package:aitrainer_app/model/cache.dart';
|
import 'package:aitrainer_app/model/cache.dart';
|
||||||
import 'package:aitrainer_app/model/exercise_ability.dart';
|
import 'package:aitrainer_app/model/exercise_ability.dart';
|
||||||
import 'package:aitrainer_app/model/exercise_type.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/app_bar.dart';
|
||||||
import 'package:aitrainer_app/widgets/bmi_widget.dart';
|
import 'package:aitrainer_app/widgets/bmi_widget.dart';
|
||||||
import 'package:aitrainer_app/widgets/bmr_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/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_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_bloc/flutter_form_bloc.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: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 {
|
class ExerciseNewPage extends StatefulWidget {
|
||||||
_ExerciseNewPageState createState() => _ExerciseNewPageState();
|
_ExerciseNewPageState createState() => _ExerciseNewPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ExerciseNewPageState extends State<ExerciseNewPage> with Trans, Logging {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ExerciseType exerciseType = ModalRoute.of(context).settings.arguments;
|
final ExerciseType exerciseType = ModalRoute.of(context).settings.arguments;
|
||||||
@ -122,12 +46,33 @@ class _ExerciseNewPageState extends State<ExerciseNewPage> with Trans, Logging {
|
|||||||
if (state is ExerciseNewError) {
|
if (state is ExerciseNewError) {
|
||||||
Scaffold.of(context).showSnackBar(
|
Scaffold.of(context).showSnackBar(
|
||||||
SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white))));
|
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) {
|
builder: (context, state) {
|
||||||
final exerciseBloc = BlocProvider.of<ExerciseNewBloc>(context);
|
final exerciseBloc = BlocProvider.of<ExerciseNewBloc>(context);
|
||||||
return ModalProgressHUD(
|
return ModalProgressHUD(
|
||||||
child: getExerciseWidget(exerciseBloc, exerciseType, menuBloc),
|
child: getExerciseSaveWidget(exerciseBloc, exerciseType, menuBloc),
|
||||||
inAsyncCall: state is ExerciseNewLoading,
|
inAsyncCall: state is ExerciseNewLoading,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
@ -137,21 +82,7 @@ class _ExerciseNewPageState extends State<ExerciseNewPage> with Trans, Logging {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getExerciseWidget(ExerciseNewBloc exerciseBloc, ExerciseType exerciseType, MenuBloc menuBloc) {
|
Widget getExerciseSaveWidget(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);
|
|
||||||
|
|
||||||
if (exerciseBloc.exerciseRepository.exerciseType.name == "BMR") {
|
if (exerciseBloc.exerciseRepository.exerciseType.name == "BMR") {
|
||||||
return BMR(exerciseBloc: exerciseBloc);
|
return BMR(exerciseBloc: exerciseBloc);
|
||||||
}
|
}
|
||||||
@ -162,288 +93,40 @@ class _ExerciseNewPageState extends State<ExerciseNewPage> with Trans, Logging {
|
|||||||
return SizeWidget(exerciseBloc: exerciseBloc);
|
return SizeWidget(exerciseBloc: exerciseBloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String exerciseTask = exerciseBloc.setExerciseTask();
|
return Scaffold(
|
||||||
|
|
||||||
return Form(
|
|
||||||
child: Scaffold(
|
|
||||||
resizeToAvoidBottomInset: true,
|
|
||||||
appBar: AppBarNav(depth: 1),
|
appBar: AppBarNav(depth: 1),
|
||||||
body: Container(
|
body: Container(
|
||||||
width: MediaQuery.of(context).size.width,
|
padding: EdgeInsets.only(top: 10, left: 20, right: 20),
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: AssetImage('asset/image/WT_black_background.jpg'),
|
image: AssetImage('asset/image/WT_black_background.jpg'),
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.cover,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: KeyboardActions(
|
child: ExerciseSave(
|
||||||
config: _buildConfig(context),
|
exerciseName: exerciseBloc.exerciseRepository.exerciseType.nameTranslation,
|
||||||
child: Container(
|
exerciseDescription: exerciseBloc.exerciseRepository.exerciseType.descriptionTranslation,
|
||||||
padding: const EdgeInsets.only(top: 25, left: 55, right: 55),
|
exerciseTask: t("Please take a relative bigger weight and repeat 12-20 times"),
|
||||||
child: SingleChildScrollView(
|
unit: exerciseBloc.exerciseRepository.exerciseType.unit,
|
||||||
scrollDirection: Axis.vertical,
|
unitQuantityUnit: exerciseBloc.exerciseRepository.exerciseType.unitQuantityUnit,
|
||||||
child: Column(
|
hasUnitQuantity: exerciseBloc.exerciseRepository.exerciseType.unitQuantityUnit != null,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
onQuantityChanged: (value) {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
exerciseBloc.add(ExerciseNewQuantityChange(quantity: double.parse(value)));
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
),
|
onUnitQuantityChanged: (value) => exerciseBloc.add(ExerciseNewQuantityUnitChange(quantity: double.parse(value))),
|
||||||
Divider(
|
onSubmit: () => confirmationDialog(exerciseBloc, menuBloc),
|
||||||
color: Colors.transparent,
|
exerciseTypeId: exerciseType.exerciseTypeId,
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
)),
|
||||||
]),
|
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) {
|
void confirmationDialog(ExerciseNewBloc bloc, MenuBloc menuBloc) {
|
||||||
LinkedHashMap args = LinkedHashMap();
|
|
||||||
print("quantity: " + bloc.quantity.toString());
|
|
||||||
if (bloc.exerciseRepository.exercise.quantity == null) {
|
if (bloc.exerciseRepository.exercise.quantity == null) {
|
||||||
print("Repository quantity modify");
|
|
||||||
//bloc.exerciseRepository.exercise.quantity = bloc.quantity;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,6 +141,9 @@ class _ExerciseNewPageState extends State<ExerciseNewPage> with Trans, Logging {
|
|||||||
: bloc.exerciseRepository.exercise.unitQuantity.toString();
|
: bloc.exerciseRepository.exercise.unitQuantity.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: close_sinks
|
||||||
|
final TestSetExecuteBloc executeBloc = BlocProvider.of<TestSetExecuteBloc>(context);
|
||||||
|
|
||||||
showCupertinoDialog(
|
showCupertinoDialog(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
context: context,
|
context: context,
|
||||||
@ -491,29 +177,12 @@ class _ExerciseNewPageState extends State<ExerciseNewPage> with Trans, Logging {
|
|||||||
bloc.exerciseRepository.setCustomer(Cache().userLoggedIn),
|
bloc.exerciseRepository.setCustomer(Cache().userLoggedIn),
|
||||||
bloc.add(ExerciseNewSubmit()),
|
bloc.add(ExerciseNewSubmit()),
|
||||||
Navigator.pop(context),
|
Navigator.pop(context),
|
||||||
Navigator.pop(context),
|
if (executeBloc.existsActivePlan() == true)
|
||||||
log("Ability " +
|
|
||||||
menuBloc.ability.toString() +
|
|
||||||
" exerciseType 1rm " +
|
|
||||||
bloc.exerciseRepository.exerciseType.is1RM().toString()),
|
|
||||||
if (bloc.exerciseRepository.exerciseType.unitQuantityUnit == null)
|
|
||||||
{
|
{
|
||||||
args['exerciseRepository'] = bloc.exerciseRepository,
|
executeBloc.add(TestSetExecuteExerciseFinished(
|
||||||
Navigator.of(context).pushNamed('evaluationPage', arguments: args)
|
exerciseTypeId: bloc.exerciseRepository.exerciseType.exerciseTypeId,
|
||||||
}
|
quantity: bloc.exerciseRepository.exercise.quantity,
|
||||||
else if (menuBloc.ability.equalsTo(ExerciseAbility.oneRepMax))
|
unitQuantity: bloc.exerciseRepository.exercise.unitQuantity)),
|
||||||
{
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -137,7 +137,8 @@ class _ExercisePlanCustomPage extends State<ExercisePlanCustomPage> with Trans {
|
|||||||
List<Widget> _getChildList(List<WorkoutMenuTree> listWorkoutTree, ExercisePlanBloc bloc) {
|
List<Widget> _getChildList(List<WorkoutMenuTree> listWorkoutTree, ExercisePlanBloc bloc) {
|
||||||
List<Widget> list = List();
|
List<Widget> list = List();
|
||||||
listWorkoutTree.forEach((element) {
|
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(
|
list.add(TreeViewChild(
|
||||||
startExpanded: false,
|
startExpanded: false,
|
||||||
parent: Card(
|
parent: Card(
|
||||||
|
@ -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/util/app_language.dart';
|
||||||
import 'package:aitrainer_app/model/cache.dart';
|
import 'package:aitrainer_app/model/cache.dart';
|
||||||
import 'package:aitrainer_app/util/common.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/util/trans.dart';
|
||||||
import 'package:aitrainer_app/widgets/app_bar_min.dart';
|
import 'package:aitrainer_app/widgets/app_bar_min.dart';
|
||||||
import 'package:aitrainer_app/widgets/bottom_nav.dart';
|
import 'package:aitrainer_app/widgets/bottom_nav.dart';
|
||||||
@ -71,6 +73,7 @@ class SettingsPage extends StatelessWidget with Trans {
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (String lang) => {
|
onChanged: (String lang) => {
|
||||||
settingsBloc.add(SettingsChangeLanguage(language: lang)),
|
settingsBloc.add(SettingsChangeLanguage(language: lang)),
|
||||||
|
Track().track(TrackingEvent.settings_lang, eventValue: lang)
|
||||||
})),
|
})),
|
||||||
getServer(settingsBloc),
|
getServer(settingsBloc),
|
||||||
//getDevice(settingsBloc),
|
//getDevice(settingsBloc),
|
||||||
|
194
lib/view/test_set_control.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +1,36 @@
|
|||||||
import 'dart:convert';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aitrainer_app/bloc/menu/menu_bloc.dart';
|
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/bloc/test_set_edit/test_set_edit_bloc.dart';
|
||||||
import 'package:aitrainer_app/library/custom_icon_icons.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/model/workout_menu_tree.dart';
|
||||||
import 'package:aitrainer_app/util/trans.dart';
|
import 'package:aitrainer_app/util/trans.dart';
|
||||||
import 'package:aitrainer_app/widgets/app_bar.dart';
|
import 'package:aitrainer_app/widgets/app_bar.dart';
|
||||||
import 'package:aitrainer_app/widgets/dialog_common.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:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:modal_progress_hud/modal_progress_hud.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
|
// ignore: must_be_immutable
|
||||||
class TestSetEdit extends StatelessWidget with Trans {
|
class TestSetEdit extends StatelessWidget with Trans {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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
|
// ignore: close_sinks
|
||||||
final MenuBloc menuBloc = BlocProvider.of<MenuBloc>(context);
|
final MenuBloc menuBloc = BlocProvider.of<MenuBloc>(context);
|
||||||
TestSetEditBloc bloc;
|
TestSetEditBloc bloc;
|
||||||
|
|
||||||
setContext(context);
|
setContext(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBarNav(depth: 1),
|
appBar: AppBarNav(
|
||||||
|
depth: 0,
|
||||||
|
),
|
||||||
body: Container(
|
body: Container(
|
||||||
padding: EdgeInsets.all(20),
|
padding: EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -37,19 +41,23 @@ class TestSetEdit extends StatelessWidget with Trans {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
create: (context) =>
|
create: (context) => TestSetEditBloc(
|
||||||
TestSetEditBloc(templateName: templateName, workoutTreeRepository: menuBloc.menuTreeRepository, menuBloc: menuBloc),
|
templateName: templateName,
|
||||||
|
templateNameTranslation: templateNameTranslation,
|
||||||
|
workoutTreeRepository: menuBloc.menuTreeRepository,
|
||||||
|
menuBloc: menuBloc),
|
||||||
child: BlocConsumer<TestSetEditBloc, TestSetEditState>(listener: (context, state) {
|
child: BlocConsumer<TestSetEditBloc, TestSetEditState>(listener: (context, state) {
|
||||||
if (state is TestSetEditError) {
|
if (state is TestSetEditError) {
|
||||||
Scaffold.of(context).showSnackBar(
|
Scaffold.of(context).showSnackBar(
|
||||||
SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white))));
|
SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white))));
|
||||||
} else if (state is TestSetEditSaved) {
|
} else if (state is TestSetEditSaved) {
|
||||||
Navigator.of(context).pushNamed("home");
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushNamed("testSetExecute");
|
||||||
}
|
}
|
||||||
}, builder: (context, state) {
|
}, builder: (context, state) {
|
||||||
bloc = BlocProvider.of<TestSetEditBloc>(context);
|
bloc = BlocProvider.of<TestSetEditBloc>(context);
|
||||||
return ModalProgressHUD(
|
return ModalProgressHUD(
|
||||||
child: getTestSetWidget(bloc, templateName),
|
child: getTestSetWidget(bloc, templateNameTranslation),
|
||||||
inAsyncCall: state is TestSetEditLoading,
|
inAsyncCall: state is TestSetEditLoading,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
@ -63,7 +71,7 @@ class TestSetEdit extends StatelessWidget with Trans {
|
|||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return DialogCommon(
|
return DialogCommon(
|
||||||
title: "Start!",
|
title: "Start!",
|
||||||
descriptions: "GO",
|
descriptions: t("Enjoy the exercises, good luck with the testing!"),
|
||||||
text: "OK",
|
text: "OK",
|
||||||
onTap: () => {
|
onTap: () => {
|
||||||
Navigator.of(context).pop(),
|
Navigator.of(context).pop(),
|
||||||
@ -78,7 +86,7 @@ class TestSetEdit extends StatelessWidget with Trans {
|
|||||||
backgroundColor: Colors.orange[800],
|
backgroundColor: Colors.orange[800],
|
||||||
icon: Icon(CustomIcon.clock),
|
icon: Icon(CustomIcon.clock),
|
||||||
label: Text(
|
label: Text(
|
||||||
"Start training",
|
t("Start training"),
|
||||||
style: GoogleFonts.inter(fontWeight: FontWeight.bold, fontSize: 16),
|
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();
|
final List<Widget> list = List();
|
||||||
alternatives.forEach((element) {
|
if (bloc.exercisePlanDetails[workoutTree.exerciseTypeId] == null) {
|
||||||
list.add(getImageStack(element, menuBloc));
|
|
||||||
});
|
|
||||||
list.add(Container(
|
list.add(Container(
|
||||||
padding: EdgeInsets.only(top: 25, bottom: 25),
|
padding: EdgeInsets.only(top: 25, bottom: 25),
|
||||||
child: ClipRRect(
|
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;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stack getImageStack(WorkoutMenuTree element, MenuBloc menuBloc) {
|
Stack getImageStack(WorkoutMenuTree element, MenuBloc menuBloc, TestSetEditBloc bloc) {
|
||||||
print(element.toJson());
|
return Stack(alignment: Alignment.topRight, children: [
|
||||||
return Stack(alignment: Alignment.bottomLeft, children: [
|
Stack(alignment: Alignment.bottomLeft, children: [
|
||||||
_getButtonImage(element, menuBloc),
|
MenuImage(imageName: element.imageName, workoutTreeId: element.id),
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.only(left: 15, bottom: 15, right: 15),
|
padding: EdgeInsets.only(left: 15, bottom: 15, right: 15),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -183,6 +200,25 @@ class TestSetEdit extends StatelessWidget with Trans {
|
|||||||
style: GoogleFonts.archivoBlack(color: element.color, fontSize: 16, height: 1.1),
|
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) =>
|
onPageChanged: (index, reason) =>
|
||||||
bloc.add(TestSetEditChangeExerciseType(index: index, exerciseTypeId: element.exerciseTypeId)),
|
bloc.add(TestSetEditChangeExerciseType(index: index, exerciseTypeId: element.exerciseTypeId)),
|
||||||
enlargeStrategy: CenterPageEnlargeStrategy.scale),
|
enlargeStrategy: CenterPageEnlargeStrategy.scale),
|
||||||
items: imageSliders(alternativeMenuItems, bloc.menuBloc),
|
items: imageSliders(alternativeMenuItems, bloc.menuBloc, workoutTree, bloc),
|
||||||
);
|
);
|
||||||
widgets.add(widget);
|
widgets.add(widget);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return widgets;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
473
lib/view/test_set_execute.dart
Normal 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,
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
95
lib/view/test_set_new.dart
Normal 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:aitrainer_app/bloc/exercise_new/exercise_new_bloc.dart';
|
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/util/app_localization.dart';
|
||||||
import 'package:aitrainer_app/model/fitness_state.dart';
|
import 'package:aitrainer_app/model/fitness_state.dart';
|
||||||
import 'package:aitrainer_app/util/trans.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 'package:google_fonts/google_fonts.dart';
|
||||||
import 'app_bar.dart';
|
import 'app_bar.dart';
|
||||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||||
import 'package:dropdown_search/dropdown_search.dart';
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class BMR extends StatefulWidget {
|
class BMR extends StatefulWidget {
|
||||||
|
292
lib/widgets/bottom_bar_multiple_exercises.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
@ -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/util/app_localization.dart';
|
||||||
import 'package:aitrainer_app/model/cache.dart';
|
import 'package:aitrainer_app/model/cache.dart';
|
||||||
import 'package:aitrainer_app/service/logging.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/track.dart';
|
||||||
import 'package:aitrainer_app/util/trans.dart';
|
import 'package:aitrainer_app/util/trans.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gradient_bottom_navigation_bar/gradient_bottom_navigation_bar.dart';
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class BottomNavigator extends StatefulWidget {
|
class BottomNavigator extends StatefulWidget {
|
||||||
|
@ -65,8 +65,9 @@ class _DialogPremiumState extends State<DialogCommon> with Trans {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.title + " ",
|
widget.title + " ",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: GoogleFonts.archivoBlack(
|
style: GoogleFonts.archivoBlack(
|
||||||
fontSize: 18,
|
fontSize: 20,
|
||||||
color: Colors.yellow[400],
|
color: Colors.yellow[400],
|
||||||
shadows: <Shadow>[
|
shadows: <Shadow>[
|
||||||
Shadow(
|
Shadow(
|
||||||
@ -90,7 +91,8 @@ class _DialogPremiumState extends State<DialogCommon> with Trans {
|
|||||||
Text(
|
Text(
|
||||||
widget.descriptions,
|
widget.descriptions,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: <Shadow>[
|
shadows: <Shadow>[
|
||||||
Shadow(
|
Shadow(
|
||||||
@ -113,7 +115,7 @@ class _DialogPremiumState extends State<DialogCommon> with Trans {
|
|||||||
Text(
|
Text(
|
||||||
widget.description2,
|
widget.description2,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: <Shadow>[
|
shadows: <Shadow>[
|
||||||
Shadow(
|
Shadow(
|
||||||
@ -136,7 +138,7 @@ class _DialogPremiumState extends State<DialogCommon> with Trans {
|
|||||||
Text(
|
Text(
|
||||||
widget.description3,
|
widget.description3,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: <Shadow>[
|
shadows: <Shadow>[
|
||||||
Shadow(
|
Shadow(
|
||||||
|
154
lib/widgets/dialog_widget.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
394
lib/widgets/exercise_save.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
66
lib/widgets/menu_image.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import 'dart:convert';
|
import 'dart:collection';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:aitrainer_app/model/exercise_ability.dart';
|
import 'package:aitrainer_app/model/exercise_ability.dart';
|
||||||
import 'package:aitrainer_app/bloc/menu/menu_bloc.dart';
|
import 'package:aitrainer_app/bloc/menu/menu_bloc.dart';
|
||||||
import 'package:aitrainer_app/library/custom_icon_icons.dart';
|
import 'package:aitrainer_app/library/custom_icon_icons.dart';
|
||||||
import 'package:aitrainer_app/util/enums.dart';
|
import 'package:aitrainer_app/util/enums.dart';
|
||||||
import 'package:aitrainer_app/util/track.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/widgets/menu_search_bar.dart';
|
||||||
import 'package:aitrainer_app/util/app_language.dart';
|
import 'package:aitrainer_app/util/app_language.dart';
|
||||||
import 'package:aitrainer_app/util/app_localization.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/scheduler.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:google_fonts/google_fonts.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';
|
import 'dialog_html.dart';
|
||||||
|
|
||||||
@ -144,8 +143,15 @@ class _MenuPageWidgetState extends State<MenuPageWidget> with Trans, Logging {
|
|||||||
padding: EdgeInsets.only(left: 0.0, bottom: 0),
|
padding: EdgeInsets.only(left: 0.0, bottom: 0),
|
||||||
onPressed: () => menuClick(workoutTree, menuBloc, context),
|
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(
|
Container(
|
||||||
padding: EdgeInsets.only(left: 15, bottom: 15, right: 15),
|
padding: EdgeInsets.only(left: 15, bottom: 8, right: 15),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => menuClick(workoutTree, menuBloc, context),
|
onTap: () => menuClick(workoutTree, menuBloc, context),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -285,7 +291,6 @@ class _MenuPageWidgetState extends State<MenuPageWidget> with Trans, Logging {
|
|||||||
MenuSearchBar(
|
MenuSearchBar(
|
||||||
listItems: menuBloc.menuTreeRepository.menuAsExercise,
|
listItems: menuBloc.menuTreeRepository.menuAsExercise,
|
||||||
onFind: (value) {
|
onFind: (value) {
|
||||||
print("onFind: ${value.toJson()}");
|
|
||||||
if (Cache().userLoggedIn == null) {
|
if (Cache().userLoggedIn == null) {
|
||||||
Scaffold.of(context).showSnackBar(SnackBar(
|
Scaffold.of(context).showSnackBar(SnackBar(
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
@ -303,11 +308,12 @@ class _MenuPageWidgetState extends State<MenuPageWidget> with Trans, Logging {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return DialogCommon(
|
return DialogCommon(
|
||||||
title: t("You have an acive Compact Test"),
|
title: t("You have an active Test Set!"),
|
||||||
descriptions: t("Press OK to continue!"),
|
descriptions: t("Press OK to continue"),
|
||||||
text: "OK",
|
text: "OK",
|
||||||
onTap: () => {
|
onTap: () => {
|
||||||
Navigator.of(context).pop(),
|
Navigator.of(context).pop(),
|
||||||
|
Navigator.of(context).pushNamed("testSetExecute"),
|
||||||
},
|
},
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
Navigator.of(context).pop(),
|
Navigator.of(context).pop(),
|
||||||
@ -334,18 +340,22 @@ class _MenuPageWidgetState extends State<MenuPageWidget> with Trans, Logging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void menuClick(WorkoutMenuTree workoutTree, MenuBloc menuBloc, BuildContext context) {
|
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) {
|
if (Cache().userLoggedIn == null) {
|
||||||
Scaffold.of(context).showSnackBar(SnackBar(
|
Scaffold.of(context).showSnackBar(SnackBar(
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
content: Text(AppLocalizations.of(context).translate('Please log in'), style: TextStyle(color: Colors.white))));
|
content: Text(AppLocalizations.of(context).translate('Please log in'), style: TextStyle(color: Colors.white))));
|
||||||
} else {
|
} 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) {
|
if (workoutTree.exerciseType.name == "Custom" && Cache().userLoggedIn.admin == 1) {
|
||||||
Navigator.of(context).pushNamed('exerciseCustomPage', arguments: workoutTree.exerciseType);
|
Navigator.of(context).pushNamed('exerciseCustomPage', arguments: workoutTree.exerciseType);
|
||||||
} else {
|
} else {
|
||||||
@ -365,60 +375,11 @@ class _MenuPageWidgetState extends State<MenuPageWidget> with Trans, Logging {
|
|||||||
return returnCode;
|
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) {
|
Widget badgedIcon(WorkoutMenuTree workoutMenuTree, double cWidth, double cHeight) {
|
||||||
String badgeKey = workoutMenuTree.nameEnglish;
|
String badgeKey = workoutMenuTree.nameEnglish;
|
||||||
bool show = Cache().getBadges()[badgeKey] != null;
|
bool show = Cache().getBadges()[badgeKey] != null;
|
||||||
int counter = Cache().getBadges()[badgeKey] != null ? Cache().getBadges()[badgeKey] : 0;
|
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(
|
return Badge(
|
||||||
padding: EdgeInsets.all(8),
|
padding: EdgeInsets.all(8),
|
||||||
position: BadgePosition.topEnd(top: 3, end: 3),
|
position: BadgePosition.topEnd(top: 3, end: 3),
|
||||||
@ -441,6 +402,7 @@ class _MenuPageWidgetState extends State<MenuPageWidget> with Trans, Logging {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:aitrainer_app/model/workout_menu_tree.dart';
|
import 'package:aitrainer_app/model/workout_menu_tree.dart';
|
||||||
import 'package:aitrainer_app/util/trans.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:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
@ -23,26 +23,31 @@ class SearchBarStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
class MenuSearchBar extends StatelessWidget {
|
class MenuSearchBar extends StatelessWidget {
|
||||||
final List listItems;
|
final List listItems;
|
||||||
final Function(WorkoutMenuTree) onFind;
|
final Function(WorkoutMenuTree) onFind;
|
||||||
|
bool showIcon;
|
||||||
const MenuSearchBar({@required this.listItems, this.onFind});
|
MenuSearchBar({@required this.listItems, this.onFind, this.showIcon = true});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedSearch(
|
return AnimatedSearch(
|
||||||
listItems: listItems,
|
listItems: listItems,
|
||||||
onFind: onFind,
|
onFind: onFind,
|
||||||
|
showIcon: showIcon,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
class AnimatedSearch extends StatefulWidget {
|
class AnimatedSearch extends StatefulWidget {
|
||||||
final List listItems;
|
final List listItems;
|
||||||
final Function(WorkoutMenuTree) onFind;
|
final Function(WorkoutMenuTree) onFind;
|
||||||
|
bool showIcon = true;
|
||||||
|
|
||||||
|
AnimatedSearch({this.listItems, this.onFind, this.showIcon});
|
||||||
|
|
||||||
AnimatedSearch({this.listItems, this.onFind});
|
|
||||||
@override
|
@override
|
||||||
_AnimatedSearch createState() => _AnimatedSearch();
|
_AnimatedSearch createState() => _AnimatedSearch();
|
||||||
}
|
}
|
||||||
@ -81,7 +86,7 @@ class _AnimatedSearch extends State<AnimatedSearch> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
AnimateExpansion(
|
AnimateExpansion(
|
||||||
animate: !isSearching,
|
animate: widget.showIcon ? !isSearching : false,
|
||||||
axisAlignment: 1.0,
|
axisAlignment: 1.0,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () => {
|
onPressed: () => {
|
||||||
@ -98,7 +103,7 @@ class _AnimatedSearch extends State<AnimatedSearch> {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
AnimateExpansion(
|
AnimateExpansion(
|
||||||
animate: isSearching,
|
animate: widget.showIcon ? isSearching : true,
|
||||||
axisAlignment: -1.0,
|
axisAlignment: -1.0,
|
||||||
child: Search(
|
child: Search(
|
||||||
listItems: widget.listItems,
|
listItems: widget.listItems,
|
||||||
|
@ -25,7 +25,6 @@ class _TimePickerWidgetState extends State<TimePickerWidget> with Trans {
|
|||||||
currentTimeInMin = x.toDouble();
|
currentTimeInMin = x.toDouble();
|
||||||
}
|
}
|
||||||
seconds = currentTimeInMin * 60 + currentTimeInSec + currentTimeInDec / 100;
|
seconds = currentTimeInMin * 60 + currentTimeInSec + currentTimeInDec / 100;
|
||||||
//print("sec" + seconds.toStringAsFixed(2));
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
widget.onChange(seconds);
|
widget.onChange(seconds);
|
||||||
},
|
},
|
||||||
|
110
lib/widgets/victory_widget.dart
Normal 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(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
28
pubspec.lock
@ -211,6 +211,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0-nullsafety.3"
|
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:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -267,13 +274,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3"
|
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:
|
equatable:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -546,13 +546,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.2"
|
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:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -896,6 +889,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.3"
|
||||||
|
random_color:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: random_color
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
10
pubspec.yaml
@ -35,7 +35,6 @@ dependencies:
|
|||||||
spider_chart: ^0.1.5
|
spider_chart: ^0.1.5
|
||||||
rainbow_color: ^0.1.1
|
rainbow_color: ^0.1.1
|
||||||
percent_indicator: ^2.1.8
|
percent_indicator: ^2.1.8
|
||||||
gradient_bottom_navigation_bar: ^1.0.0+4
|
|
||||||
fl_chart: ^0.12.0
|
fl_chart: ^0.12.0
|
||||||
infinite_listview: ^1.0.1+1
|
infinite_listview: ^1.0.1+1
|
||||||
toggle_switch: ^0.1.8
|
toggle_switch: ^0.1.8
|
||||||
@ -52,9 +51,9 @@ dependencies:
|
|||||||
network_image_to_byte: ^0.0.1
|
network_image_to_byte: ^0.0.1
|
||||||
package_info: ^0.4.3+4
|
package_info: ^0.4.3+4
|
||||||
liquid_progress_indicator: ^0.3.2
|
liquid_progress_indicator: ^0.3.2
|
||||||
dropdown_search: ^0.4.9
|
|
||||||
audioplayer: ^0.8.1
|
audioplayer: ^0.8.1
|
||||||
ezanimation: ^0.4.1
|
ezanimation: ^0.4.1
|
||||||
|
confetti: ^0.5.5
|
||||||
|
|
||||||
|
|
||||||
firebase_core: ^0.5.0
|
firebase_core: ^0.5.0
|
||||||
@ -148,6 +147,7 @@ flutter:
|
|||||||
- asset/image/WT_Results_for_female.jpg
|
- asset/image/WT_Results_for_female.jpg
|
||||||
- asset/image/WT_Results_for_men.jpg
|
- asset/image/WT_Results_for_men.jpg
|
||||||
- asset/image/WT_results_background.jpg
|
- asset/image/WT_results_background.jpg
|
||||||
|
- asset/image/WT_cup_victory400.png
|
||||||
|
|
||||||
- asset/image/button_fb.png
|
- asset/image/button_fb.png
|
||||||
- asset/image/button_apple.png
|
- asset/image/button_apple.png
|
||||||
@ -291,6 +291,7 @@ flutter:
|
|||||||
- asset/menu/leg_curls.jpg
|
- asset/menu/leg_curls.jpg
|
||||||
- asset/menu/leg_extension.jpg
|
- asset/menu/leg_extension.jpg
|
||||||
- asset/menu/legpress.jpg
|
- asset/menu/legpress.jpg
|
||||||
|
- asset/menu/lower_body_test.jpg
|
||||||
- asset/menu/lunges_with_dumbbells.jpg
|
- asset/menu/lunges_with_dumbbells.jpg
|
||||||
- asset/menu/lunges.jpg
|
- asset/menu/lunges.jpg
|
||||||
- asset/menu/lying_alternating_leg_raises.jpg
|
- asset/menu/lying_alternating_leg_raises.jpg
|
||||||
@ -302,6 +303,7 @@ flutter:
|
|||||||
- asset/menu/lying_triceps_extension.jpg
|
- asset/menu/lying_triceps_extension.jpg
|
||||||
- asset/menu/machine_shoulder_press.jpg
|
- asset/menu/machine_shoulder_press.jpg
|
||||||
- asset/menu/machine_test.jpg
|
- asset/menu/machine_test.jpg
|
||||||
|
- asset/menu/no_equipment_test.jpg
|
||||||
- asset/menu/oblique_crunch.jpg
|
- asset/menu/oblique_crunch.jpg
|
||||||
- asset/menu/olympic_squat.jpg
|
- asset/menu/olympic_squat.jpg
|
||||||
- asset/menu/one_arm_row.jpg
|
- asset/menu/one_arm_row.jpg
|
||||||
@ -353,16 +355,18 @@ flutter:
|
|||||||
- asset/menu/straight-arm_rope_pull-down.jpg
|
- asset/menu/straight-arm_rope_pull-down.jpg
|
||||||
- asset/menu/t_bar_rows.jpg
|
- asset/menu/t_bar_rows.jpg
|
||||||
- asset/menu/test_center.jpg
|
- asset/menu/test_center.jpg
|
||||||
|
- asset/menu/test_on_machines.jpg
|
||||||
- asset/menu/thigh_adductor.jpg
|
- asset/menu/thigh_adductor.jpg
|
||||||
- asset/menu/triceps_extension_on_cable_with_rope.jpg
|
- asset/menu/triceps_extension_on_cable_with_rope.jpg
|
||||||
- asset/menu/triceps_kickback.jpg
|
- asset/menu/triceps_kickback.jpg
|
||||||
- asset/menu/triceps_pushdown.jpg
|
- asset/menu/triceps_pushdown.jpg
|
||||||
- asset/menu/twisted_crunches.jpg
|
- asset/menu/twisted_crunches.jpg
|
||||||
- asset/menu/under_body.jpg
|
- asset/menu/under_body.jpg
|
||||||
|
- asset/menu/upper_body_test.jpg
|
||||||
- asset/menu/upper_body.jpg
|
- asset/menu/upper_body.jpg
|
||||||
- asset/menu/v_ups.jpg
|
- asset/menu/v_ups.jpg
|
||||||
- asset/menu/wall_sit.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/weighted_bench_dip.jpg
|
||||||
- asset/menu/wide_grip_behind_the_neck_pull_ups.jpg
|
- asset/menu/wide_grip_behind_the_neck_pull_ups.jpg
|
||||||
- asset/menu/wide_grip_front_lat_pulldown.jpg
|
- asset/menu/wide_grip_front_lat_pulldown.jpg
|
||||||
|