diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8ef85e5..2ef35ac 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,8 @@ 7.3.0) - - firebase_analytics (8.0.0-dev.0): + - firebase_analytics (8.0.0-dev.2): - Firebase/Analytics (= 7.3.0) - firebase_core - Flutter - - firebase_auth (1.0.1): + - firebase_auth (1.0.3): - Firebase/Auth (= 7.3.0) - firebase_core - Flutter - - firebase_core (1.0.2): + - firebase_core (1.0.3): - Firebase/CoreOnly (= 7.3.0) - Flutter - - firebase_messaging (9.1.0): + - firebase_messaging (9.1.1): - Firebase/Messaging (= 7.3.0) - firebase_core - Flutter @@ -86,6 +86,10 @@ PODS: - GoogleUtilities/Environment (~> 7.0) - GoogleUtilities/Reachability (~> 7.0) - GoogleUtilities/UserDefaults (~> 7.0) + - flurry (0.0.4): + - Flurry-iOS-SDK/FlurrySDK + - Flutter + - Flurry-iOS-SDK/FlurrySDK (11.2.0) - Flutter (1.0.0) - flutter_facebook_auth (2.0.0): - FBSDKCoreKit (~> 9.1.0) @@ -181,6 +185,7 @@ DEPENDENCIES: - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) + - flurry (from `.symlinks/plugins/flurry/ios`) - Flutter (from `Flutter`) - flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) @@ -210,6 +215,7 @@ SPEC REPOS: - FirebaseInstallations - FirebaseInstanceID - FirebaseMessaging + - Flurry-iOS-SDK - FMDB - GoogleAppMeasurement - GoogleDataTransport @@ -236,6 +242,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: :path: ".symlinks/plugins/firebase_messaging/ios" + flurry: + :path: ".symlinks/plugins/flurry/ios" Flutter: :path: Flutter flutter_facebook_auth: @@ -274,10 +282,10 @@ SPEC CHECKSUMS: FBSDKCoreKit: a00fe2efd780c195a5e09201bf51c56106245b40 FBSDKLoginKit: d98498c598ec09de657385a9349a1f21119b7f86 Firebase: 26223c695fe322633274198cb19dca8cb7e54416 - firebase_analytics: c9b8ddc8e864e45cd70761c5d972bd11c83574ab - firebase_auth: 9f6491ea8e44570323361ae713a2ae3175b3f21a - firebase_core: e6cbb0d1f7091edfcae31559e58224bfc1e455dc - firebase_messaging: 9c746d6c52bb05764e73bbe745d0d698e5afb695 + firebase_analytics: 4c032e04324c47ee6dd7a28bfff831e0ac0d09b6 + firebase_auth: c2c27e1081671b02f90981e70dad54722198491f + firebase_core: b5d81dfd4fb2d6f700e67de34d9a633ae325c4e9 + firebase_messaging: 7547c99f31466371f9cfcb733d5a1bf32a819872 FirebaseAnalytics: 2580c2d62535ae7b644143d48941fcc239ea897a FirebaseAuth: c224a0cf1afa0949bd5c7bfcf154b4f5ce8ddef2 FirebaseCore: 4d3c72622ce0e2106aaa07bb4b2935ba2c370972 @@ -285,6 +293,8 @@ SPEC CHECKSUMS: FirebaseInstallations: 5e777e6640fa060405cc7632447b6c5ca5af4742 FirebaseInstanceID: 53140c03b9f6136f890d7901399f85a4c90ab2d0 FirebaseMessaging: 68d1bcb14880189558a8ae57167abe0b7e417232 + flurry: 15b01f664ab1367c62b50291541ea7f78ca85aad + Flurry-iOS-SDK: 6636d30c30f12010e7c7c71d84b443416a168efc Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c flutter_facebook_auth: 4b170c07b7fce791497093fcc3f134fb215f3f07 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 @@ -307,7 +317,7 @@ SPEC CHECKSUMS: PurchasesCoreSwift: 31c2a3d7394432abbe64d46f0933835de0b33033 PurchasesHybridCommon: 013c8072b73e752a206779747e88c068fbf999ec shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d - smartlook: bda0b1561935a02ef0fea5448258d5ac75027859 + smartlook: bbc5c73a85c752a31dabf100c8930838c646342e sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e wakelock: b0843b2479edbf6504d8d262c2959446f35373aa diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8ffd538..26f1ac7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -405,7 +405,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; PRODUCT_BUNDLE_IDENTIFIER = com.aitrainer.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -531,7 +531,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -548,7 +548,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; PRODUCT_BUNDLE_IDENTIFIER = com.aitrainer.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -566,7 +566,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -583,7 +583,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; PRODUCT_BUNDLE_IDENTIFIER = com.aitrainer.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/lib/bloc/account/account_bloc.dart b/lib/bloc/account/account_bloc.dart index b4d8920..9b4f89b 100644 --- a/lib/bloc/account/account_bloc.dart +++ b/lib/bloc/account/account_bloc.dart @@ -74,7 +74,7 @@ class AccountBloc extends Bloc { yield AccountLoggedIn(); } else if (event is AccountLogout) { await Cache().logout(); - //customerRepository.customer = null; + customerRepository.customer = null; customerRepository.emptyTrainees(); loggedIn = false; yield AccountLoggedOut(); diff --git a/lib/bloc/customer_change/customer_change_bloc.dart b/lib/bloc/customer_change/customer_change_bloc.dart index f9b73d6..99f0963 100644 --- a/lib/bloc/customer_change/customer_change_bloc.dart +++ b/lib/bloc/customer_change/customer_change_bloc.dart @@ -9,6 +9,8 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; +import '../../model/fitness_state.dart'; + part 'customer_change_event.dart'; part 'customer_change_state.dart'; @@ -19,14 +21,40 @@ class CustomerChangeBloc extends Bloc double weight = 60; double height = 170; CustomerChangeBloc({required this.customerRepository}) : super(CustomerChangeInitial()) { - year = this.customerRepository.customer.birthYear; + if (this.customerRepository.customer == null) { + this.customerRepository.createNew(); + } + year = this.customerRepository.customer!.birthYear; if (year == null || year == 0) { year = 1990; } weight = this.customerRepository.getWeight() == 0 ? 60 : this.customerRepository.getWeight(); height = this.customerRepository.getHeight() == 0 ? 170 : this.customerRepository.getHeight(); + + print("fitnesslevel " + customerRepository.customer!.fitnessLevel.toString()); + if (customerRepository.customer!.fitnessLevel != null) { + if (!FitnessItem().elements.contains(customerRepository.customer!.fitnessLevel)) { + Sport.values.forEach((element) { + print(" .. ${element.toStr()}"); + if (element.equalsStringTo(customerRepository.customer!.fitnessLevel!)) { + selectedSport = element; + selectedFitnessItem = FitnessState.professional; + } + }); + if (selectedSport == null) { + selectedFitnessItem = customerRepository.customer!.fitnessLevel; + } + } else { + selectedFitnessItem = customerRepository.customer!.fitnessLevel; + } + } + + print("selected: $selectedFitnessItem sport: $selectedSport " + customerRepository.customer!.fitnessLevel.toString()); } + Sport? selectedSport; + String? selectedFitnessItem; + @override Stream mapEventToState( CustomerChangeEvent event, @@ -42,7 +70,8 @@ class CustomerChangeBloc extends Bloc visiblePassword = !visiblePassword; yield CustomerDataChanged(); } else if (event is CustomerFitnessChange) { - customerRepository.setFitnessLevel(event.fitness); + //customerRepository.setFitnessLevel(event.fitness); + selectedFitnessItem = event.fitness; yield CustomerDataChanged(); } else if (event is CustomerBirthYearChange) { yield CustomerChangeLoading(); @@ -77,9 +106,25 @@ class CustomerChangeBloc extends Bloc } else if (event is CustomerGenderChange) { customerRepository.setSex(event.gender == 0 ? "m" : "w"); yield CustomerDataChanged(); + } else if (event is CustomerSportChange) { + yield CustomerChangeLoading(); + selectedSport = event.sport; + //customerRepository.setFitnessLevel(event.sport.toStr()); + yield CustomerDataChanged(); } else if (event is CustomerSave) { yield CustomerSaving(); if (validation()) { + if (selectedFitnessItem != null) { + if (selectedFitnessItem == FitnessState.professional) { + if (selectedSport != null) { + customerRepository.setFitnessLevel(selectedSport!.toStr()); + } else { + customerRepository.setFitnessLevel(FitnessState.professional); + } + } else { + customerRepository.setFitnessLevel(selectedFitnessItem!); + } + } await customerRepository.saveCustomer(); Cache().initBadges(); yield CustomerSaveSuccess(); @@ -123,4 +168,7 @@ class CustomerChangeBloc extends Bloc } return null; } + + Sport? get getSelectedSport => this.selectedSport; + set setSelectedSport(Sport? selectedSport) => this.selectedSport = selectedSport; } diff --git a/lib/bloc/customer_change/customer_change_event.dart b/lib/bloc/customer_change/customer_change_event.dart index 0a0a2a2..da7da66 100644 --- a/lib/bloc/customer_change/customer_change_event.dart +++ b/lib/bloc/customer_change/customer_change_event.dart @@ -95,6 +95,14 @@ class CustomerPasswordChange extends CustomerChangeEvent { List get props => [password]; } +class CustomerSportChange extends CustomerChangeEvent { + final Sport sport; + const CustomerSportChange({required this.sport}); + + @override + List get props => [sport]; +} + class CustomerChangePasswordObscure extends CustomerChangeEvent { const CustomerChangePasswordObscure(); } diff --git a/lib/bloc/exercise_execute_plan_add/exercise_execute_plan_add_bloc.dart b/lib/bloc/exercise_execute_plan_add/exercise_execute_plan_add_bloc.dart index cf48547..4e04269 100644 --- a/lib/bloc/exercise_execute_plan_add/exercise_execute_plan_add_bloc.dart +++ b/lib/bloc/exercise_execute_plan_add/exercise_execute_plan_add_bloc.dart @@ -26,8 +26,8 @@ class ExerciseExecutePlanAddBloc extends Bloc with Logg exerciseRepository.start = DateTime.now(); if (Cache().userLoggedIn != null) { customerRepository.customer = Cache().userLoggedIn!; - weight = customerRepository.customer.getProperty("Weight"); - height = customerRepository.customer.getProperty("Height"); - birthYear = customerRepository.customer.birthYear!; - fitnessLevel = customerRepository.customer.fitnessLevel!; + weight = customerRepository.customer!.getProperty("Weight"); + height = customerRepository.customer!.getProperty("Height"); + birthYear = customerRepository.customer!.birthYear!; + fitnessLevel = customerRepository.customer!.fitnessLevel!; } if (exerciseType.unit == "second") { stopWatchTimer.rawTime.listen((value) => {timerValue = value, this.setQuantity((value / 1000).toDouble())}); @@ -183,22 +183,22 @@ class ExerciseNewBloc extends Bloc with Logg int year = int.parse(DateFormat(DateFormat.YEAR).format(date)); - if (customerRepository.customer.sex == "m") { + if (customerRepository.customer!.sex == "m") { //66.47 + ( 13.75 × tömeg kg-ban ) + ( 5.003 × magasság cm-ben ) − ( 6.755 × életkor évben kifejezve ) - bmr = 66.47 + (13.75 * weight) + (5.003 * height) - (6.755 * (year - customerRepository.customer.birthYear!)); + bmr = 66.47 + (13.75 * weight) + (5.003 * height) - (6.755 * (year - customerRepository.customer!.birthYear!)); } else { //BMR = 655.1 + ( 9.563 × ömeg kg-ban ) + ( 1.85 × magasság cm-ben) − ( 4.676 × életkor évben kifejezve ) - bmr = 655.1 + (9.563 * weight) + (1.85 * height) - (4.676 * (year - customerRepository.customer.birthYear!)); + bmr = 655.1 + (9.563 * weight) + (1.85 * height) - (4.676 * (year - customerRepository.customer!.birthYear!)); } bmrEnergy = bmr; - if (customerRepository.customer.fitnessLevel == FitnessState.beginner) { + if (customerRepository.customer!.fitnessLevel == FitnessState.beginner) { bmr *= 1.2; - } else if (customerRepository.customer.fitnessLevel == FitnessState.intermediate) { + } else if (customerRepository.customer!.fitnessLevel == FitnessState.intermediate) { bmr *= 1.375; - } else if (customerRepository.customer.fitnessLevel == FitnessState.advanced) { + } else if (customerRepository.customer!.fitnessLevel == FitnessState.advanced) { bmr *= 1.55; - } else if (customerRepository.customer.fitnessLevel == FitnessState.professional) { + } else if (customerRepository.customer!.fitnessLevel == FitnessState.professional) { bmr *= 1.9; } return bmr; diff --git a/lib/bloc/login/login_bloc.dart b/lib/bloc/login/login_bloc.dart index a40ea5d..4a93a77 100644 --- a/lib/bloc/login/login_bloc.dart +++ b/lib/bloc/login/login_bloc.dart @@ -75,12 +75,17 @@ class LoginBloc extends Bloc with Trans { if (!this.dataPolicyAllowed) { throw Exception("Please accept our data policy"); } - await userRepository.addUser(); - accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!)); - await saveCustomer(); - Track().track(TrackingEvent.registration, eventValue: "email"); - Cache().setLoginType(LoginType.email); - yield LoginSuccess(); + final String? validationError = validate(); + if (validationError != null) { + yield LoginError(message: validationError); + } else { + await userRepository.addUser(); + accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!)); + await saveCustomer(); + Track().track(TrackingEvent.registration, eventValue: "email"); + Cache().setLoginType(LoginType.email); + yield LoginSuccess(); + } } else if (event is RegistrationFB) { yield LoginLoading(); if (!this.dataPolicyAllowed) { @@ -131,20 +136,36 @@ class LoginBloc extends Bloc with Trans { Future saveCustomer() async { customerRepository.customer = Cache().userLoggedIn!; - customerRepository.customer.dataPolicyAllowed = 1; + customerRepository.customer!.dataPolicyAllowed = 1; await customerRepository.saveCustomer(); } - String? emailValidation(String email) { + String? emailValidation(String? email) { String? message = Common.emailValidation(email); return message; } - String? passwordValidation(String value) { + String? passwordValidation(String? value) { String? message = Common.passwordValidation(value); if (message != null) { message = t(message); } return message; } + + String? validate() { + String? error; + + error = emailValidation(userRepository.user.email); + if (error != null) { + return error; + } + + error = passwordValidation(userRepository.user.password); + if (error != null) { + return error; + } + + return error; + } } diff --git a/lib/bloc/menu/menu_bloc.dart b/lib/bloc/menu/menu_bloc.dart index 8af724f..f82e7fb 100644 --- a/lib/bloc/menu/menu_bloc.dart +++ b/lib/bloc/menu/menu_bloc.dart @@ -98,7 +98,9 @@ class MenuBloc extends Bloc with Trans, Logging { //await menuTreeRepository.createTree(); //menuTreeRepository.getBranch(this.parent); //setMenuInfo(); - exerciseDeviceRepository.setDevices(Cache().getDevices()!); + if (Cache().getDevices() != null) { + exerciseDeviceRepository.setDevices(Cache().getDevices()!); + } yield MenuReady(); } else if (event is MenuRecreateTree) { yield MenuLoading(); diff --git a/lib/bloc/password_reset/password_reset_bloc.dart b/lib/bloc/password_reset/password_reset_bloc.dart index c0659ee..3af2af6 100644 --- a/lib/bloc/password_reset/password_reset_bloc.dart +++ b/lib/bloc/password_reset/password_reset_bloc.dart @@ -25,7 +25,7 @@ class PasswordResetBloc extends Bloc { } else if (event is PasswordResetSubmit) { yield PasswordResetLoading(); await userRepository.resetPassword(); - yield PasswordResetReady(); + yield PasswordResetFinished(); } } on Exception catch (e) { yield PasswordResetError(message: e.toString()); diff --git a/lib/bloc/password_reset/password_reset_state.dart b/lib/bloc/password_reset/password_reset_state.dart index f323b23..9d3ce24 100644 --- a/lib/bloc/password_reset/password_reset_state.dart +++ b/lib/bloc/password_reset/password_reset_state.dart @@ -26,3 +26,7 @@ class PasswordResetError extends PasswordResetState { class PasswordResetReady extends PasswordResetState { const PasswordResetReady(); } + +class PasswordResetFinished extends PasswordResetState { + const PasswordResetFinished(); +} diff --git a/lib/bloc/result/result_bloc.dart b/lib/bloc/result/result_bloc.dart index 5dd1837..4d46856 100644 --- a/lib/bloc/result/result_bloc.dart +++ b/lib/bloc/result/result_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aitrainer_app/model/result.dart'; +import 'package:aitrainer_app/repository/evaluation_repository.dart'; import 'package:aitrainer_app/repository/exercise_repository.dart'; import 'package:aitrainer_app/repository/exercise_result_repository.dart'; import 'package:aitrainer_app/service/logging.dart'; @@ -17,6 +18,7 @@ part 'result_state.dart'; class ResultBloc extends Bloc with Logging, Trans { final ExerciseResultRepository resultRepository; final ExerciseRepository exerciseRepository; + final EvaluationRepository evaluationRepository = EvaluationRepository(); final BuildContext context; //List _healthDataList = []; DateTime? startTime; @@ -53,7 +55,7 @@ class ResultBloc extends Bloc with Logging, Trans { //await _fetchHealthData(); _matchExerciseData(); - await resultRepository.saveExerciseResults(); + //await resultRepository.saveExerciseResults(); yield ResultReady(); } } on Exception catch (ex) { diff --git a/lib/bloc/test_set_control/test_set_control_bloc.dart b/lib/bloc/test_set_control/test_set_control_bloc.dart index 3cc8a2b..c888bdf 100644 --- a/lib/bloc/test_set_control/test_set_control_bloc.dart +++ b/lib/bloc/test_set_control/test_set_control_bloc.dart @@ -31,7 +31,7 @@ class TestSetControlBloc extends Bloc } void initBloc() { - oneRepMax = executeBloc.calculate1RM(exercisePlanDetail.exercises!.last.unitQuantity, exercisePlanDetail.exercises!.last.quantity); + oneRepMax = executeBloc.calculate1RM(exercisePlanDetail.exercises!.last.unitQuantity!, exercisePlanDetail.exercises!.last.quantity!); initQuantity = 12; quantity = initQuantity; initUnitQuantity = oneRepMax * 0.75; diff --git a/lib/bloc/test_set_execute/test_set_execute_bloc.dart b/lib/bloc/test_set_execute/test_set_execute_bloc.dart index a6960e7..a1b68f9 100644 --- a/lib/bloc/test_set_execute/test_set_execute_bloc.dart +++ b/lib/bloc/test_set_execute/test_set_execute_bloc.dart @@ -136,7 +136,9 @@ class TestSetExecuteBloc extends Bloc if (!this.existsInPlanDetails(event.exerciseTypeId)) { ExercisePlanDetail exercisePlanDetail = ExercisePlanDetail(event.exerciseTypeId); - exercisePlanDetail.exercisePlanId = exercisePlan!.exercisePlanId!; + if (exercisePlan!.exercisePlanId != null) { + exercisePlanDetail.exercisePlanId = exercisePlan!.exercisePlanId!; + } final ExerciseType exerciseType = Cache().getExerciseTypeById(event.exerciseTypeId)!; exercisePlanDetail.serie = exerciseType.unitQuantityUnit == null ? 1 : 4; @@ -316,8 +318,9 @@ class TestSetExecuteBloc extends Bloc ExercisePlanDetail? getNext() { ExercisePlanDetail? nextExercisePlanDetail; + int minStep = 99; - if (this.exercisePlanDetails == null) { + if (this.exercisePlanDetails != null) { for (final detail in this.exercisePlanDetails!) { if (!detail.state.equalsTo(ExercisePlanDetailState.finished)) { if (detail.exercises == null) { @@ -369,9 +372,9 @@ class TestSetExecuteBloc extends Bloc 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); + final double? unitQuantity = exercisePlanDetail.exercises!.last.unitQuantity!; + final double? quantity = exercisePlanDetail.exercises!.last.quantity!; + double oneRepMax = this.calculate1RM(quantity!, unitQuantity!); text = (oneRepMax * 0.75).round().toStringAsFixed(0) + " " + exercisePlanDetail.exerciseType!.unitQuantityUnit!; return text; } diff --git a/lib/library/dropdown_search.dart b/lib/library/dropdown_search.dart new file mode 100644 index 0000000..9c5f4bc --- /dev/null +++ b/lib/library/dropdown_search.dart @@ -0,0 +1,541 @@ +library dropdown_search; + +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import 'popup_menu.dart'; +import 'select_dialog.dart'; + +typedef Future> DropdownSearchOnFind(String text); +typedef String DropdownSearchItemAsString(T item); +typedef bool DropdownSearchFilterFn(T item, String filter); +typedef bool DropdownSearchCompareFn(T item, T selectedItem); +typedef Widget DropdownSearchBuilder(BuildContext context, T selectedItem, String itemAsString); +typedef Widget DropdownSearchPopupItemBuilder( + BuildContext context, + T item, + bool isSelected, +); +typedef bool DropdownSearchPopupItemEnabled(T item); +typedef Widget ErrorBuilder(BuildContext context, String? searchEntry, dynamic exception); +typedef Widget EmptyBuilder(BuildContext context, String? searchEntry); +typedef Widget LoadingBuilder(BuildContext context, String? searchEntry); +typedef Widget IconButtonBuilder(BuildContext context); +typedef Future BeforeChange(T prevItem, T nextItem); + +typedef Widget FavoriteItemsBuilder(BuildContext context, T item); + +///[items] are the original item from [items] or/and [onFind] +typedef List FavoriteItems(List items); + +enum Mode { DIALOG, BOTTOM_SHEET, MENU } + +class DropdownSearch extends StatefulWidget { + ///DropDownSearch label + final String? label; + + ///DropDownSearch hint + final String? hint; + + ///show/hide the search box + final bool showSearchBox; + + ///true if the filter on items is applied onlie (via API) + final bool isFilteredOnline; + + ///show/hide clear selected item + final bool showClearButton; + + ///offline items list + final List? items; + + ///selected item + final T? selectedItem; + + ///function that returns item from API + final DropdownSearchOnFind? onFind; + + ///called when a new item is selected + final ValueChanged? onChanged; + + ///to customize list of items UI + final DropdownSearchBuilder? dropdownBuilder; + + ///to customize selected item + final DropdownSearchPopupItemBuilder? popupItemBuilder; + + ///decoration for search box + final InputDecoration? searchBoxDecoration; + + ///the title for dialog/menu/bottomSheet + final Color? popupBackgroundColor; + + ///custom widget for the popup title + final Widget? popupTitle; + + ///customize the fields the be shown + final DropdownSearchItemAsString? itemAsString; + + /// custom filter function + final DropdownSearchFilterFn? filterFn; + + ///enable/disable dropdownSearch + final bool enabled; + + ///MENU / DIALOG/ BOTTOM_SHEET + final Mode mode; + + ///the max height for dialog/bottomSheet/Menu + final double? maxHeight; + + ///the max width for the dialog + final double? dialogMaxWidth; + + ///select the selected item in the menu/dialog/bottomSheet of items + final bool showSelectedItem; + + ///function that compares two object with the same type to detected if it's the selected item or not + final DropdownSearchCompareFn? compareFn; + + ///dropdownSearch input decoration + final InputDecoration? dropdownSearchDecoration; + + ///custom layout for empty results + final EmptyBuilder? emptyBuilder; + + ///custom layout for loading items + final LoadingBuilder? loadingBuilder; + + ///custom layout for error + final ErrorBuilder? errorBuilder; + + ///the search box will be focused if true + final bool autoFocusSearchBox; + + ///custom shape for the popup + final ShapeBorder? popupShape; + + final AutovalidateMode autoValidateMode; + + /// An optional method to call with the final value when the form is saved via + final FormFieldSetter? onSaved; + + /// An optional method that validates an input. Returns an error string to + /// display if the input is invalid, or null otherwise. + final FormFieldValidator? validator; + + ///custom dropdown clear button icon widget + final Widget? clearButton; + + ///custom clear button widget builder + final IconButtonBuilder? clearButtonBuilder; + + ///custom dropdown icon button widget + final Widget? dropDownButton; + + ///custom dropdown button widget builder + final IconButtonBuilder? dropdownButtonBuilder; + + ///whether to manage the clear and dropdown icons via InputDecoration suffixIcon + final bool showAsSuffixIcons; + + ///If true, the dropdownBuilder will continue the uses of material behavior + ///This will be useful if you want to handle a custom UI only if the item !=null + final bool dropdownBuilderSupportsNullItem; + + ///defines if an item of the popup is enabled or not, if the item is disabled, + ///it cannot be clicked + final DropdownSearchPopupItemEnabled? popupItemDisabled; + + ///set a custom color for the popup barrier + final Color? popupBarrierColor; + + ///text controller to set default search word for example + final TextEditingController? searchBoxController; + + ///called when popup is dismissed + final VoidCallback? onPopupDismissed; + + /// callback executed before applying value change + ///delay before searching, change it to Duration(milliseconds: 0) + ///if you do not use online search + final Duration? searchDelay; + + /// callback executed before applying value change + final BeforeChange? onBeforeChange; + + ///show or hide favorites items + final bool showFavoriteItems; + + ///to customize favorites chips + final FavoriteItemsBuilder? favoriteItemBuilder; + + ///favorites items list + final FavoriteItems? favoriteItems; + + ///favorite items alignment + final MainAxisAlignment? favoriteItemsAlignment; + + 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, + this.favoriteItemBuilder, + this.favoriteItems, + this.showFavoriteItems = false, + this.favoriteItemsAlignment = MainAxisAlignment.start, + }) : assert(!showSelectedItem || T == String || compareFn != null), + super(key: key); + + @override + DropdownSearchState createState() => DropdownSearchState(); +} + +class DropdownSearchState extends State> { + final ValueNotifier _selectedItemNotifier = ValueNotifier(null); + final ValueNotifier _isFocused = ValueNotifier(false); + + @override + void initState() { + super.initState(); + _selectedItemNotifier.value = widget.selectedItem; + } + + @override + void didUpdateWidget(DropdownSearch oldWidget) { + final oldSelectedItem = oldWidget.selectedItem; + final newSelectedItem = widget.selectedItem; + if (oldSelectedItem != newSelectedItem) { + _selectedItemNotifier.value = newSelectedItem; + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _selectedItemNotifier, + builder: (context, T? data, wt) { + return IgnorePointer( + ignoring: !widget.enabled, + child: GestureDetector( + onTap: () => _selectSearchMode(data), + child: _formField(data), + ), + ); + }, + ); + } + + Widget _defaultSelectItemWidget(T? data) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: widget.dropdownBuilder != null + ? widget.dropdownBuilder!( + context, + data, + _selectedItemAsString(data), + ) + : Text(_selectedItemAsString(data), style: Theme.of(context).textTheme.subtitle1), + ), + if (!widget.showAsSuffixIcons) _manageTrailingIcons(data), + ], + ); + } + + Widget _formField(T? value) { + return FormField( + enabled: widget.enabled, + onSaved: widget.onSaved, + validator: widget.validator, + autovalidateMode: widget.autoValidateMode, + initialValue: widget.selectedItem, + builder: (FormFieldState state) { + if (state.value != value) { + WidgetsBinding.instance!.addPostFrameCallback((_) { + state.didChange(value); + }); + } + return ValueListenableBuilder( + valueListenable: _isFocused, + builder: (context, bool isFocused, w) { + return InputDecorator( + isEmpty: value == null && (widget.dropdownBuilder == null || widget.dropdownBuilderSupportsNullItem), + isFocused: isFocused, + decoration: _manageDropdownDecoration(state, value), + child: _defaultSelectItemWidget(value), + ); + }); + }, + ); + } + + ///manage dropdownSearch field decoration + InputDecoration _manageDropdownDecoration(FormFieldState state, T? data) { + return (widget.dropdownSearchDecoration ?? + InputDecoration(contentPadding: EdgeInsets.fromLTRB(12, 12, 0, 0), border: OutlineInputBorder())) + .applyDefaults(Theme.of(state.context).inputDecorationTheme) + .copyWith( + enabled: widget.enabled, + labelText: widget.label, + hintText: widget.hint, + suffixIcon: widget.showAsSuffixIcons ? _manageTrailingIcons(data) : null, + errorText: state.errorText); + } + + ///function that return the String value of an object + String _selectedItemAsString(T? data) { + if (data == null) { + return ""; + } else if (widget.itemAsString != null) { + return widget.itemAsString!(data); + } else { + return data.toString(); + } + } + + ///function that manage Trailing icons(close, dropDown) + Widget _manageTrailingIcons(T? data) { + final clearButtonPressed = () => _handleOnChangeSelectedItem(null); + final dropdownButtonPressed = () => _selectSearchMode(data); + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (data != null && widget.showClearButton == true) + widget.clearButtonBuilder != null + ? GestureDetector( + onTap: clearButtonPressed, + child: widget.clearButtonBuilder!(context), + ) + : IconButton( + icon: widget.clearButton ?? const Icon(Icons.clear, size: 24), + onPressed: clearButtonPressed, + ), + widget.dropdownButtonBuilder != null + ? GestureDetector( + onTap: dropdownButtonPressed, + child: widget.dropdownButtonBuilder!(context), + ) + : IconButton( + icon: widget.dropDownButton ?? const Icon(Icons.arrow_drop_down, size: 24), + onPressed: dropdownButtonPressed, + ), + ], + ); + } + + ///open dialog + Future _openSelectDialog(T? data) { + return showGeneralDialog( + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + transitionDuration: const Duration(milliseconds: 400), + barrierColor: widget.popupBarrierColor ?? const Color(0x80000000), + context: context, + pageBuilder: (context, animation, secondaryAnimation) { + return AlertDialog( + contentPadding: EdgeInsets.all(0), + shape: widget.popupShape, + backgroundColor: widget.popupBackgroundColor, + content: _selectDialogInstance(data), + ); + }, + ); + } + + ///open BottomSheet (Dialog mode) + Future _openBottomSheet(T? data) { + return showModalBottomSheet( + barrierColor: widget.popupBarrierColor, + backgroundColor: widget.popupBackgroundColor, + isScrollControlled: true, + shape: widget.popupShape, + context: context, + builder: (ctx) { + return _selectDialogInstance(data, defaultHeight: 350); + }); + } + + ///openMenu + Future _openMenu(T? data) { + // Here we get the render object of our physical button, later to get its size & position + final RenderBox popupButtonObject = context.findRenderObject() as RenderBox; + // Get the render object of the overlay used in `Navigator` / `MaterialApp`, i.e. screen size reference + final RenderBox overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox; + // Calculate the show-up area for the dropdown using button's size & position based on the `overlay` used as the coordinate space. + final RelativeRect position = RelativeRect.fromSize( + Rect.fromPoints( + popupButtonObject.localToGlobal(popupButtonObject.size.bottomLeft(Offset.zero), ancestor: overlay), + popupButtonObject.localToGlobal(popupButtonObject.size.bottomRight(Offset.zero), ancestor: overlay), + ), + Size(overlay.size.width, overlay.size.height), + ); + return customShowMenu( + barrierColor: widget.popupBarrierColor, + shape: widget.popupShape, + color: widget.popupBackgroundColor, + context: context, + position: position, + elevation: 8, + items: [ + CustomPopupMenuItem( + enabled: false, + child: Container( + width: popupButtonObject.size.width, + child: _selectDialogInstance(data, defaultHeight: 224), + ), + ), + ]); + } + + SelectDialog _selectDialogInstance(T? data, {double? defaultHeight}) { + return SelectDialog( + popupTitle: widget.popupTitle, + maxHeight: widget.maxHeight ?? defaultHeight, + isFilteredOnline: widget.isFilteredOnline, + itemAsString: widget.itemAsString, + filterFn: widget.filterFn, + items: widget.items, + onFind: widget.onFind, + showSearchBox: widget.showSearchBox, + itemBuilder: widget.popupItemBuilder, + selectedValue: data, + searchBoxDecoration: widget.searchBoxDecoration, + onChanged: _handleOnChangeSelectedItem, + showSelectedItem: widget.showSelectedItem, + compareFn: widget.compareFn, + emptyBuilder: widget.emptyBuilder, + loadingBuilder: widget.loadingBuilder, + errorBuilder: widget.errorBuilder, + autoFocusSearchBox: widget.autoFocusSearchBox, + dialogMaxWidth: widget.dialogMaxWidth, + itemDisabled: widget.popupItemDisabled, + searchBoxController: widget.searchBoxController ?? TextEditingController(), + searchDelay: widget.searchDelay, + showFavoriteItems: widget.showFavoriteItems, + favoriteItems: widget.favoriteItems, + favoriteItemBuilder: widget.favoriteItemBuilder, + favoriteItemsAlignment: widget.favoriteItemsAlignment, + ); + } + + ///Function that manage focus listener + ///set true only if the widget already not focused to prevent unnecessary build + ///same thing for clear focus, + void _handleFocus(bool isFocused) { + if (isFocused && !_isFocused.value) { + FocusScope.of(context).unfocus(); + _isFocused.value = true; + } else if (!isFocused && _isFocused.value) _isFocused.value = false; + } + + ///handle on change value , if the validation is active , we validate the new selected item + void _handleOnChangeSelectedItem(T? selectedItem) { + final changeItem = () { + _selectedItemNotifier.value = selectedItem; + if (widget.onChanged != null) widget.onChanged!(selectedItem); + }; + + if (widget.onBeforeChange != null) { + widget.onBeforeChange!(_selectedItemNotifier.value, selectedItem).then((value) { + if (value == true) { + changeItem(); + } + }); + } else { + changeItem(); + } + + _handleFocus(false); + } + + ///Function that return then UI based on searchMode + ///[data] selected item to be passed to the UI + ///If we close the popup , or maybe we just selected + ///another widget we should clear the focus + Future _selectSearchMode(T? data) async { + _handleFocus(true); + T? selectedItem; + if (widget.mode == Mode.MENU) { + selectedItem = await _openMenu(data); + } else if (widget.mode == Mode.BOTTOM_SHEET) { + selectedItem = await _openBottomSheet(data); + } else { + selectedItem = await _openSelectDialog(data); + } + _handleFocus(false); + widget.onPopupDismissed?.call(); + + return selectedItem; + } + + ///Public Function that return then UI based on searchMode + ///[data] selected item to be passed to the UI + ///If we close the popup , or maybe we just selected + ///another widget we should clear the focus + ///THIS USED FOR OPEN DROPDOWN_SEARCH PROGRAMMATICALLY, + ///otherwise you can you [_selectSearchMode] + Future openDropDownSearch() => _selectSearchMode(_selectedItemNotifier.value); + + ///Change selected Value; this function is public USED to change the selected + ///value PROGRAMMATICALLY, Otherwise you can use [_handleOnChangeSelectedItem] + void changeSelectedItem(T selectedItem) => _handleOnChangeSelectedItem(selectedItem); + + ///Change selected Value; this function is public USED to clear selected + ///value PROGRAMMATICALLY, Otherwise you can use [_handleOnChangeSelectedItem] + void clear() => _handleOnChangeSelectedItem(null); + + ///get selected value programmatically + T? get getSelectedItem => _selectedItemNotifier.value; + + ///check if the dropdownSearch is focused + bool get isFocused => _isFocused.value; +} diff --git a/lib/library/flurry.dart b/lib/library/flurry.dart deleted file mode 100644 index 4d9f17b..0000000 --- a/lib/library/flurry.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; - -class Flurry { - static const MethodChannel _channel = const MethodChannel('flurry'); - - static Future get platformVersion async { - final String version = await _channel.invokeMethod('getPlatformVersion'); - return version; - } - - static Future initialize({String androidKey = "", String iosKey = "", bool enableLog = true}) async { - Map args = {}; - args.putIfAbsent("api_key_android", () => androidKey); - args.putIfAbsent("api_key_ios", () => iosKey); - args.putIfAbsent("is_log_enabled", () => enableLog); - - await _channel.invokeMethod('initialize', args); - - return null; - } - - static Future logEvent(String message) async { - Map args = {}; - args.putIfAbsent("message", () => message); - - await _channel.invokeMethod('logEvent', args); - return null; - } - - static Future setUserId(String userId) async { - Map args = {}; - args.putIfAbsent("userId", () => userId); - - await _channel.invokeMethod('userId', args); - return null; - } -} diff --git a/lib/library/popup_menu.dart b/lib/library/popup_menu.dart new file mode 100644 index 0000000..f36c046 --- /dev/null +++ b/lib/library/popup_menu.dart @@ -0,0 +1,589 @@ +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, + }) : super(key: key, child: child); + + final ValueChanged onLayout; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderMenuItem(onLayout); + } + + @override + void updateRenderObject(BuildContext context, covariant _RenderMenuItem renderObject) { + renderObject.onLayout = onLayout; + } +} + +class _RenderMenuItem extends RenderShiftedBox { + _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child); + + ValueChanged onLayout; + + @override + void performLayout() { + if (child == null) { + size = Size.zero; + } else { + child!.layout(constraints, parentUsesSize: true); + size = constraints.constrain(child!.size); + } + final BoxParentData childParentData = child!.parentData as BoxParentData; + childParentData.offset = Offset.zero; + onLayout(size); + } +} + +/// An item in a material design popup menu. +/// +/// To show a popup menu, use the [customShowMenu] function. To create a button that +/// shows a popup menu, consider using [PopupMenuButton]. +/// +/// To show a checkmark next to a popup menu item, consider using +/// [CheckedPopupMenuItem]. +/// +/// Typically the [child] of a [CustomPopupMenuItem] is a [Text] widget. More +/// elaborate menus with icons can use a [ListTile]. By default, a +/// [CustomPopupMenuItem] is kMinInteractiveDimension pixels high. If you use a widget +/// with a different height, it must be specified in the [height] property. +/// +/// {@tool sample} +/// +/// Here, a [Text] widget is used with a popup menu item. The `WhyFarther` type +/// is an enum, not shown here. +/// +/// ```dart +/// const CustomPopupMenuItem( +/// value: WhyFarther.harder, +/// child: Text('Working a lot harder'), +/// ) +/// ``` +/// {@end-tool} +/// +/// See the example at [PopupMenuButton] for how this example could be used in a +/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to +/// keep the text of [CustomPopupMenuItem]s that use [Text] widgets in their [child] +/// slot aligned with the text of [CheckedPopupMenuItem]s or of [CustomPopupMenuItem] +/// that use a [ListTile] in their [child] slot. +/// +/// See also: +/// +/// * [PopupMenuDivider], which can be used to divide items from each other. +/// * [CheckedPopupMenuItem], a variant of [CustomPopupMenuItem] with a checkmark. +/// * [customShowMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +class CustomPopupMenuItem extends PopupMenuEntry { + /// Creates an item for a popup menu. + /// + /// By default, the item is [enabled]. + /// + /// The `enabled` and `height` arguments must not be null. + const CustomPopupMenuItem({ + Key? key, + this.value, + this.enabled = true, + this.height = kMinInteractiveDimension, + this.textStyle, + required this.child, + }) : super(key: key); + + /// The value that will be returned by [customShowMenu] if this entry is selected. + final T? value; + + /// Whether the user is permitted to select this item. + /// + /// Defaults to true. If this is false, then the item will not react to + /// touches. + final bool enabled; + + /// The minimum height height of the menu item. + /// + /// Defaults to [kMinInteractiveDimension] pixels. + @override + final double height; + + /// The text style of the popup menu item. + /// + /// If this property is null, then [PopupMenuThemeData.textStyle] is used. + /// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subhead] is used. + final TextStyle? textStyle; + + /// The widget below this widget in the tree. + /// + /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An + /// appropriate [DefaultTextStyle] is put in scope for the child. In either + /// case, the text should be short enough that it won't wrap. + final Widget child; + + @override + bool represents(T? value) => value == this.value; + + @override + PopupMenuItemState> createState() => PopupMenuItemState>(); +} + +/// The [State] for [CustomPopupMenuItem] subclasses. +/// +/// By default this implements the basic styling and layout of Material Design +/// popup menu items. +/// +/// The [buildChild] method can be overridden to adjust exactly what gets placed +/// in the menu. By default it returns [CustomPopupMenuItem.child]. +/// +/// The [handleTap] method can be overridden to adjust exactly what happens when +/// the item is tapped. By default, it uses [Navigator.pop] to return the +/// [CustomPopupMenuItem.value] from the menu route. +/// +/// This class takes two type arguments. The second, `W`, is the exact type of +/// the [Widget] that is using this [State]. It must be a subclass of +/// [CustomPopupMenuItem]. The first, `T`, must match the type argument of that widget +/// class, and is the type of values returned from this menu. +class PopupMenuItemState> extends State { + /// The menu item contents. + /// + /// Used by the [build] method. + /// + /// By default, this returns [CustomPopupMenuItem.child]. Override this to put + /// something else in the menu entry. + @protected + Widget buildChild() => widget.child; + + /// The handler for when the user selects the menu item. + /// + /// Used by the [InkWell] inserted by the [build] method. + /// + /// By default, uses [Navigator.pop] to return the [CustomPopupMenuItem.value] from + /// the menu route. + @protected + void handleTap() { + Navigator.pop(context, widget.value); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + TextStyle? style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subtitle1; + + if (!widget.enabled) style = style!.copyWith(color: theme.disabledColor); + + Widget item = AnimatedDefaultTextStyle( + style: style!, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), + child: buildChild(), + ), + ); + + if (!widget.enabled) { + final bool isDark = theme.brightness == Brightness.dark; + item = IconTheme.merge( + data: IconThemeData(opacity: isDark ? 0.5 : 0.38), + child: item, + ); + } + + return InkWell( + onTap: widget.enabled ? handleTap : null, + canRequestFocus: widget.enabled, + child: item, + ); + } +} + +class _PopupMenu extends StatelessWidget { + const _PopupMenu({ + Key? key, + this.route, + this.semanticLabel, + }) : super(key: key); + + final _PopupMenuRoute? route; + final String? semanticLabel; + + @override + Widget build(BuildContext context) { + final double unit = 1.0 / (route!.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. + final List children = []; + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + + for (int i = 0; i < route!.items.length; i += 1) { + final double start = (i + 1) * unit; + final double end = (start + 1.5 * unit).clamp(0.0, 1.0); + final CurvedAnimation opacity = CurvedAnimation( + parent: route!.animation!, + curve: Interval(start, end), + ); + Widget item = route!.items[i]; + if (route!.initialValue != null && route!.items[i].represents(route!.initialValue)) { + item = Container( + color: Theme.of(context).highlightColor, + child: item, + ); + } + children.add( + _MenuItem( + onLayout: (Size size) { + route!.itemSizes[i] = size; + }, + child: FadeTransition( + opacity: opacity, + child: item, + ), + ), + ); + } + + final CurveTween opacity = CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); + final CurveTween width = CurveTween(curve: Interval(0.0, unit)); + final CurveTween height = CurveTween(curve: Interval(0.0, unit * route!.items.length)); + + final Widget child = ConstrainedBox( + constraints: const BoxConstraints(minWidth: _kMenuMinWidth), + child: IntrinsicWidth( + stepWidth: _kMenuWidthStep, + child: Semantics( + scopesRoute: true, + namesRoute: true, + explicitChildNodes: true, + label: semanticLabel, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: _kMenuVerticalPadding), + child: ListBody(children: children), + ), + ), + ), + ); + + return AnimatedBuilder( + animation: route!.animation!, + builder: (BuildContext context, Widget? child) { + return Opacity( + opacity: opacity.evaluate(route!.animation!), + child: Material( + shape: route!.shape ?? popupMenuTheme.shape, + color: route!.color ?? popupMenuTheme.color, + type: MaterialType.card, + elevation: route!.elevation ?? popupMenuTheme.elevation ?? 8.0, + child: Align( + alignment: AlignmentDirectional.topEnd, + widthFactor: width.evaluate(route!.animation!), + heightFactor: height.evaluate(route!.animation!), + child: child, + ), + ), + ); + }, + child: child, + ); + } +} + +// Positioning of the menu on the screen. +class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { + _PopupMenuRouteLayout(this.position, this.itemSizes, this.selectedItemIndex, this.textDirection); + + // Rectangle of underlying button, relative to the overlay's dimensions. + final RelativeRect? position; + + // The sizes of each item are computed when the menu is laid out, and before + // the route is laid out. + List itemSizes; + + // The index of the selected item, or null if PopupMenuButton.initialValue + // was not specified. + final int? selectedItemIndex; + + // Whether to prefer going to the left or to the right. + final TextDirection textDirection; + + // We put the child wherever position specifies, so long as it will fit within + // the specified parent size padded (inset) by 8. If necessary, we adjust the + // child's position so that it fits. + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + // The menu can be at most the size of the overlay minus 8.0 pixels in each + // direction. + return BoxConstraints.loose(constraints.biggest - const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0) as Size); + } + + @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) { + 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. + late 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. + switch (textDirection) { + case TextDirection.rtl: + x = size.width - position!.right - childSize.width; + break; + case TextDirection.ltr: + x = position!.left; + break; + } + } + + // Avoid going outside an area defined as the rectangle 8.0 pixels from the + // edge of the screen in every direction. + if (x < _kMenuScreenPadding) + x = _kMenuScreenPadding; + else if (x + childSize.width > size.width - _kMenuScreenPadding) x = size.width - childSize.width - _kMenuScreenPadding; + if (y < _kMenuScreenPadding) + y = _kMenuScreenPadding; + else if (y + childSize.height > size.height - _kMenuScreenPadding) y = size.height - childSize.height - _kMenuScreenPadding; + return Offset(x, y); + } + + @override + bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) { + // If called when the old and new itemSizes have been initialized then + // we expect them to have the same length because there's no practical + // way to change length of the items list once the menu has been shown. + assert(itemSizes.length == oldDelegate.itemSizes.length); + + return position != oldDelegate.position || + selectedItemIndex != oldDelegate.selectedItemIndex || + textDirection != oldDelegate.textDirection || + !listEquals(itemSizes, oldDelegate.itemSizes); + } +} + +class _PopupMenuRoute extends PopupRoute { + _PopupMenuRoute({ + this.position, + required 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.filled(items.length, null, growable: false); + + final RelativeRect? position; + final List> items; + final List itemSizes; + final dynamic initialValue; + final double? elevation; + final ThemeData? theme; + final String? semanticLabel; + final ShapeBorder? shape; + final Color? color; + final PopupMenuThemeData? popupMenuTheme; + final BuildContext? showMenuContext; + final bool? captureInheritedThemes; + final Color? barrierColor; + + @override + Animation createAnimation() { + return CurvedAnimation( + parent: super.createAnimation(), + curve: Curves.linear, + reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd), + ); + } + + @override + Duration get transitionDuration => _kMenuDuration; + + @override + bool get barrierDismissible => true; + + @override + final String? barrierLabel; + + @override + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + int? selectedItemIndex; + if (initialValue != null) { + for (int index = 0; selectedItemIndex == null && index < items.length; index += 1) { + if (items[index].represents(initialValue)) selectedItemIndex = index; + } + } + + Widget menu = _PopupMenu(route: this, semanticLabel: semanticLabel); + if (captureInheritedThemes!) { + menu = InheritedTheme.captureAll(showMenuContext!, menu); + } else { + // For the sake of backwards compatibility. An (unlikely) app that relied + // on having menus only inherit from the material Theme could set + // captureInheritedThemes to false and get the original behavior. + if (theme != null) menu = Theme(data: theme!, child: menu); + } + + return MediaQuery.removePadding( + context: context, + removeTop: true, + removeBottom: true, + removeLeft: true, + removeRight: true, + child: Builder( + builder: (BuildContext context) { + return CustomSingleChildLayout( + delegate: _PopupMenuRouteLayout( + position, + itemSizes, + selectedItemIndex, + Directionality.of(context), + ), + child: menu, + ); + }, + ), + ); + } +} + +/// Show a popup menu that contains the `items` at `position`. +/// +/// `items` should be non-null and not empty. +/// +/// If `initialValue` is specified then the first item with a matching value +/// will be highlighted and the value of `position` gives the rectangle whose +/// vertical center will be aligned with the vertical center of the highlighted +/// item (when possible). +/// +/// If `initialValue` is not specified then the top of the menu will be aligned +/// with the top of the `position` rectangle. +/// +/// In both cases, the menu position will be adjusted if necessary to fit on the +/// screen. +/// +/// Horizontally, the menu is positioned so that it grows in the direction that +/// has the most room. For example, if the `position` describes a rectangle on +/// the left edge of the screen, then the left edge of the menu is aligned with +/// the left edge of the `position`, and the menu grows to the right. If both +/// edges of the `position` are equidistant from the opposite edge of the +/// screen, then the ambient [Directionality] is used as a tie-breaker, +/// preferring to grow in the reading direction. +/// +/// The positioning of the `initialValue` at the `position` is implemented by +/// iterating over the `items` to find the first whose +/// [CustomPopupMenuEntry.represents] method returns true for `initialValue`, and then +/// summing the values of [CustomPopupMenuEntry.height] for all the preceding widgets +/// in the list. +/// +/// The `elevation` argument specifies the z-coordinate at which to place the +/// menu. The elevation defaults to 8, the appropriate elevation for popup +/// menus. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the menu. It is only used when the method is called. Its corresponding +/// widget can be safely removed from the tree before the popup menu is closed. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// menu to the [Navigator] furthest from or nearest to the given `context`. It +/// is `false` by default. +/// +/// The `semanticLabel` argument is used by accessibility frameworks to +/// announce screen transitions when the menu is opened and closed. If this +/// label is not provided, it will default to +/// [MaterialLocalizations.popupMenuLabel]. +/// +/// See also: +/// +/// * [CustomPopupMenuItem], a popup menu entry for a single value. +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. +/// * [PopupMenuButton], which provides an [IconButton] that shows a menu by +/// calling this method automatically. +/// * [SemanticsConfiguration.namesRoute], for a description of edge triggered +/// semantics. +Future customShowMenu({ + required BuildContext context, + required RelativeRect position, + required List> items, + T? initialValue, + double? elevation, + String? semanticLabel, + Color? barrierColor, + ShapeBorder? shape, + Color? color, + bool captureInheritedThemes = true, + bool useRootNavigator = false, +}) { + assert(items.isNotEmpty); + assert(debugCheckHasMaterialLocalizations(context)); + + String? label = semanticLabel; + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + label = semanticLabel; + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + label = semanticLabel ?? MaterialLocalizations.of(context).popupMenuLabel; + } + + return Navigator.of(context, rootNavigator: useRootNavigator).push( + _PopupMenuRoute( + position: position, + items: items, + initialValue: initialValue, + elevation: elevation, + semanticLabel: label, + theme: Theme.of(context), + popupMenuTheme: PopupMenuTheme.of(context), + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + barrierColor: barrierColor, + shape: shape, + color: color, + showMenuContext: context, + captureInheritedThemes: captureInheritedThemes, + ), + ); +} diff --git a/lib/library/select_dialog.dart b/lib/library/select_dialog.dart new file mode 100644 index 0000000..45ccf36 --- /dev/null +++ b/lib/library/select_dialog.dart @@ -0,0 +1,489 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'dropdown_search.dart'; + +class SelectDialog extends StatefulWidget { + final T? selectedValue; + final List? items; + final bool showSearchBox; + final bool isFilteredOnline; + final ValueChanged? onChanged; + final DropdownSearchOnFind? onFind; + final DropdownSearchPopupItemBuilder? itemBuilder; + final InputDecoration? searchBoxDecoration; + final DropdownSearchItemAsString? itemAsString; + final DropdownSearchFilterFn? filterFn; + final String? hintText; + final double? maxHeight; + final double? dialogMaxWidth; + final Widget? popupTitle; + final bool showSelectedItem; + final DropdownSearchCompareFn? compareFn; + final DropdownSearchPopupItemEnabled? itemDisabled; + + ///custom layout for empty results + final EmptyBuilder? emptyBuilder; + + ///custom layout for loading items + final LoadingBuilder? loadingBuilder; + + ///custom layout for error + final ErrorBuilder? errorBuilder; + + ///the search box will be focused if true + final bool autoFocusSearchBox; + + ///text controller to set default search word for example + final TextEditingController? searchBoxController; + + ///delay before searching + final Duration? searchDelay; + + ///show or hide favorites items + final bool showFavoriteItems; + + ///build favorites chips + final FavoriteItemsBuilder? favoriteItemBuilder; + + ///favorite items alignment + final MainAxisAlignment? favoriteItemsAlignment; + + ///favorites item + final FavoriteItems? favoriteItems; + + 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, + this.favoriteItemBuilder, + this.favoriteItems, + this.showFavoriteItems = false, + this.favoriteItemsAlignment = MainAxisAlignment.start, + }) : super(key: key); + + @override + _SelectDialogState createState() => _SelectDialogState(); +} + +class _SelectDialogState extends State> { + final FocusNode focusNode = new FocusNode(); + final StreamController> _itemsStream = StreamController>.broadcast(); + final ValueNotifier _loadingNotifier = ValueNotifier(false); + final List _items = []; + late Debouncer _debouncer; + + @override + void initState() { + super.initState(); + _debouncer = Debouncer(delay: widget.searchDelay); + + Future.delayed( + Duration.zero, + () => manageItemsByFilter(widget.searchBoxController?.text ?? '', isFistLoad: true), + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (widget.autoFocusSearchBox) FocusScope.of(context).requestFocus(focusNode); + } + + @override + void dispose() { + _itemsStream.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Size deviceSize = MediaQuery.of(context).size; + bool isTablet = deviceSize.width > deviceSize.height; + double maxHeight = deviceSize.height * (isTablet ? .8 : .6); + double maxWidth = deviceSize.width * (isTablet ? .7 : .9); + + return Container( + width: widget.dialogMaxWidth ?? maxWidth, + constraints: BoxConstraints(maxHeight: widget.maxHeight ?? maxHeight), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + _searchField(), + if (widget.showFavoriteItems == true) _favoriteItemsWidget(), + Expanded( + child: Stack( + children: [ + StreamBuilder>( + stream: _itemsStream.stream, + builder: (context, snapshot) { + if (snapshot.hasError) { + return _errorWidget(snapshot.error); + } else if (!snapshot.hasData) { + return _loadingWidget(); + } else if (snapshot.data!.isEmpty) { + if (widget.emptyBuilder != null) + return widget.emptyBuilder!(context, widget.searchBoxController?.text); + else + return const Center( + child: const Text("No data found"), + ); + } + return ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.symmetric(vertical: 0), + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + var item = snapshot.data![index]; + return _itemWidget(item); + }, + ); + }, + ), + _loadingWidget() + ], + ), + ), + ], + ), + ); + } + + void _showErrorDialog(dynamic error) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Error while getting online items"), + content: _errorWidget(error), + actions: [ + TextButton( + child: new Text("OK"), + onPressed: () { + Navigator.of(context).pop(false); + }, + ) + ], + ); + }, + ); + } + + Widget _errorWidget(dynamic error) { + if (widget.errorBuilder != null) + return widget.errorBuilder!(context, widget.searchBoxController?.text, error); + else + return Padding( + padding: EdgeInsets.all(8), + child: Text( + error?.toString() ?? 'Error', + ), + ); + } + + Widget _loadingWidget() { + return ValueListenableBuilder( + valueListenable: _loadingNotifier, + builder: (context, bool isLoading, wid) { + if (isLoading) { + if (widget.loadingBuilder != null) + return widget.loadingBuilder!(context, widget.searchBoxController?.text); + else + return Padding( + padding: const EdgeInsets.all(24.0), + child: const Center( + child: const CircularProgressIndicator(), + ), + ); + } + return Container(); + }); + } + + void _onTextChanged(String filter) async { + manageItemsByFilter(filter); + } + + ///Function that filter item (online and offline) base on user filter + ///[filter] is the filter keyword + ///[isFirstLoad] true if it's the first time we load data from online, false other wises + void manageItemsByFilter(String filter, {bool isFistLoad = false}) async { + _loadingNotifier.value = true; + + String encoded(String item) { + String encodedItem = ""; + for (int i = 0; i < item.length; i++) { + var char = item[i]; + switch (char) { + case 'Á': + case 'á': + case 'ą': + case 'ä': + char = 'a'; + break; + case 'é': + case 'É': + char = 'e'; + break; + case 'ú': + case 'ű': + case 'ü': + case 'Ú': + case 'Ű': + case 'Ü': + char = 'u'; + break; + case 'ö': + case 'ő': + case 'ó': + case 'Ö': + case 'Ő': + case 'Ó': + char = 'o'; + break; + case 'í': + case 'Í': + char = 'i'; + break; + } + encodedItem += char; + } + return encodedItem; + } + + List applyFilter(String filter) { + return _items.where((i) { + if (widget.filterFn != null) + return (widget.filterFn!(i, filter)); + else if (i.toString().toLowerCase().contains(filter.toLowerCase()) || + encoded(i.toString()).toLowerCase().contains(encoded(filter.toLowerCase()))) { + return true; + } else if (widget.itemAsString != null) { + bool found = (widget.itemAsString!(i)).toLowerCase().contains(filter.toLowerCase()); + if (!found) { + found = (encoded(widget.itemAsString!(i))).toLowerCase().contains(encoded(filter.toLowerCase())); + } + return found; + } + return false; + }).toList(); + } + + //load offline data for the first time + if (isFistLoad && widget.items != null) _items.addAll(widget.items!); + + //manage offline items + if (widget.onFind != null && (widget.isFilteredOnline || isFistLoad)) { + try { + final List onlineItems = []; + onlineItems.addAll(await widget.onFind!(filter)); + + //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 entered keyword (filter) + if (widget.isFilteredOnline == true) { + var filteredLocalList = applyFilter(filter); + _items.clear(); + _items.addAll(filteredLocalList); + } + } + //add new online items to list + _items.addAll(onlineItems); + + //don't filter data , they are already filtred online and local data are already filtered + if (widget.isFilteredOnline == true) + _addDataToStream(_items); + else + _addDataToStream(applyFilter(filter)); + } catch (e) { + _addErrorToStream(e); + //if offline items count > 0 , the error will be not visible for the user + //As solution we show it in dialog + if (widget.items != null && widget.items!.isNotEmpty) { + _showErrorDialog(e); + _addDataToStream(applyFilter(filter)); + } + } + } else { + _addDataToStream(applyFilter(filter)); + } + + _loadingNotifier.value = false; + } + + void _addDataToStream(List data) { + if (_itemsStream.isClosed) return; + _itemsStream.add(data); + } + + void _addErrorToStream(Object error, [StackTrace? stackTrace]) { + if (_itemsStream.isClosed) return; + _itemsStream.addError(error, stackTrace); + } + + Widget _itemWidget(T? item) { + if (widget.itemBuilder != null) + return InkWell( + child: widget.itemBuilder!( + context, + item, + _manageSelectedItemVisibility(item), + ), + onTap: widget.itemDisabled != null && (widget.itemDisabled!(item)) == true ? null : () => _handleSelectItem(item), + ); + else + return ListTile( + title: Text(_selectedItemAsString(item)), + selected: _manageSelectedItemVisibility(item), + onTap: widget.itemDisabled != null && (widget.itemDisabled!(item)) == true ? null : () => _handleSelectItem(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 (item is String?) { + return item == widget.selectedValue; + } else { + return widget.compareFn!(item, widget.selectedValue); + } + } + + Widget _searchField() { + return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ + widget.popupTitle ?? const SizedBox.shrink(), + if (widget.showSearchBox) + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: widget.searchBoxController, + focusNode: focusNode, + onChanged: (f) => _debouncer(() { + _onTextChanged(f); + }), + decoration: widget.searchBoxDecoration ?? + InputDecoration( + hintText: widget.hintText, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + ), + ) + ]); + } + + Widget _favoriteItemsWidget() { + return StreamBuilder>( + stream: _itemsStream.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + return _buildFavoriteItems(widget.favoriteItems!(snapshot.data!)); + } else { + return Container(); + } + }); + } + + Widget _buildFavoriteItems(List? favoriteItems) { + if (favoriteItems != null) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8), + child: LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: widget.favoriteItemsAlignment ?? MainAxisAlignment.start, + children: favoriteItems + .map( + (f) => GestureDetector( + onTap: () => _handleSelectItem(f), + child: Container( + margin: EdgeInsets.only(right: 4), + child: widget.favoriteItemBuilder != null + ? widget.favoriteItemBuilder!(context, f) + : _favoriteItemDefaultWidget(f), + ), + ), + ) + .toList()), + ), + ); + }), + ); + } else { + return Container(); + } + } + + void _handleSelectItem(T? selectedItem) { + Navigator.pop(context, selectedItem); + if (widget.onChanged != null) widget.onChanged!(selectedItem); + } + + Widget _favoriteItemDefaultWidget(T? item) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: Theme.of(context).primaryColorLight), + child: Text( + _selectedItemAsString(item), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.subtitle1, + ), + ); + } + + ///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(); + } + } +} + +class Debouncer { + final Duration? delay; + Timer? _timer; + + Debouncer({this.delay}); + + call(Function action) { + _timer?.cancel(); + _timer = Timer(delay ?? const Duration(milliseconds: 500), action as void Function()); + } +} diff --git a/lib/main.dart b/lib/main.dart index 46a13b1..d68b962 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,7 @@ import 'package:aitrainer_app/view/customer_fitness_page.dart'; import 'package:aitrainer_app/view/customer_goal_page.dart'; import 'package:aitrainer_app/view/customer_modify_page.dart'; import 'package:aitrainer_app/view/customer_welcome_page.dart'; -import 'package:aitrainer_app/view/evaluation.dart'; +import 'package:aitrainer_app/view/evaluation_page.dart'; import 'package:aitrainer_app/view/exercise_control_page.dart'; import 'package:aitrainer_app/view/exercise_execute_page.dart'; import 'package:aitrainer_app/view/exercise_execute_plan_add_page.dart'; @@ -37,7 +37,7 @@ import 'package:aitrainer_app/view/test_set_new.dart'; import 'package:aitrainer_app/widgets/home.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/observer.dart'; -import 'package:aitrainer_app/library/flurry.dart'; +import 'package:flurry/flurry.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -118,6 +118,11 @@ Future main() async { // - https://api.dartlang.org/stable/1.24.2/dart-async/Zone-class.html // - https://www.dartlang.org/articles/libraries/zones runZonedGuarded>(() async { + await Sentry.init( + (options) { + options.dsn = dsn; + }, + ); final WorkoutTreeRepository menuTreeRepository = WorkoutTreeRepository(); WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/model/cache.dart b/lib/model/cache.dart index a9a90fa..1d9f775 100644 --- a/lib/model/cache.dart +++ b/lib/model/cache.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'dart:convert'; import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/model/evaluation.dart'; import 'package:aitrainer_app/model/exercise_plan.dart'; import 'package:aitrainer_app/model/exercise_plan_detail.dart'; import 'package:aitrainer_app/model/exercise_plan_template.dart'; @@ -21,7 +22,7 @@ import 'package:aitrainer_app/main.dart'; import 'package:aitrainer_app/util/enums.dart'; import 'package:aitrainer_app/util/env.dart'; import 'package:aitrainer_app/util/track.dart'; -import 'package:aitrainer_app/library/flurry.dart'; +import 'package:flurry/flurry.dart'; import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; import 'package:package_info/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -84,8 +85,8 @@ class Cache with Logging { AccessToken? accessTokenFacebook; Customer? userLoggedIn; String? firebaseUid; - late LoginType loginType; - late PackageInfo packageInfo; + LoginType? loginType; + PackageInfo? packageInfo; bool hasPurchased = false; @@ -93,6 +94,7 @@ class Cache with Logging { List? _exerciseTypes; List? _exerciseTree; + List? _evaluations; List? _exercises; ExercisePlan? _myExercisePlan; @@ -204,7 +206,6 @@ class Cache with Logging { String? detailsJson = sharedPreferences.getString(Cache.activeExercisePlanDetailsKey); if (detailsJson != null) { - print("Details $detailsJson"); Iterable json = jsonDecode(detailsJson); this.activeExercisePlanDetails = json.map((details) => ExercisePlanDetail.fromJsonWithExerciseList(details)).toList(); } @@ -641,4 +642,7 @@ class Cache with Logging { } return isMuscleDevelopmentSeen!; } + + List? get evaluations => this._evaluations; + set evaluations(List? value) => this._evaluations = value; } diff --git a/lib/model/evaluation.dart b/lib/model/evaluation.dart new file mode 100644 index 0000000..b4e4bc9 --- /dev/null +++ b/lib/model/evaluation.dart @@ -0,0 +1,28 @@ +import 'package:aitrainer_app/model/evaluation_attribute.dart'; + +class Evaluation { + late int evaluationId; + late String name; + late int exerciseTypeId; + late String unit; + late List attributes; + + Evaluation.fromJson(Map json) { + evaluationId = json['evaluationId']; + name = json['name']; + exerciseTypeId = json['exerciseTypeId']; + unit = json['unit']; + this.attributes = json['attributes'].map((attr) => EvaluationAttribute.fromJson(attr)).toList(); + } + + @override + String toString() { + Map json = { + 'evaluationId': this.evaluationId, + 'name': this.name, + 'exerciseTypeId': this.exerciseTypeId, + 'unit': this.unit + }; + return json.toString(); + } +} diff --git a/lib/model/evaluation_attribute.dart b/lib/model/evaluation_attribute.dart new file mode 100644 index 0000000..31250aa --- /dev/null +++ b/lib/model/evaluation_attribute.dart @@ -0,0 +1,41 @@ +class EvaluationAttribute { + late int evaluationAttrId; + late int evaluationId; + late String name; + late String sex; + late int ageMin; + late int ageMax; + late double valueMin; + late double valueMax; + late String evaluationText; + String? suggestion; + + EvaluationAttribute.fromJson(Map json) { + evaluationAttrId = json['evaluationAttrId']; + evaluationId = json['evaluationId']; + name = json['name']; + sex = json['sex']; + ageMin = json['ageMin']; + ageMax = json['ageMax']; + valueMin = json['valueMin']; + valueMax = json['valueMax']; + evaluationText = json['evaluation_text']; + suggestion = json['suggestion']; + } + + @override + String toString() { + Map json = { + 'evaluationAttrId': this.evaluationAttrId, + 'evaluationId': this.evaluationId, + 'name': this.name, + 'sex': this.sex, + 'ageMin': this.ageMin, + 'ageMax': this.ageMax, + 'valueMin': this.valueMin, + 'valueMax': this.valueMax, + 'evaluation_text': this.evaluationText, + }; + return json.toString(); + } +} diff --git a/lib/model/exercise_plan.dart b/lib/model/exercise_plan.dart index f74dd23..1763c18 100644 --- a/lib/model/exercise_plan.dart +++ b/lib/model/exercise_plan.dart @@ -61,4 +61,10 @@ class ExercisePlan { }; } } + + @override + String toString() { + Map json = toJson(); + return json.toString(); + } } diff --git a/lib/model/exercise_plan_detail.dart b/lib/model/exercise_plan_detail.dart index e502098..6f208d0 100644 --- a/lib/model/exercise_plan_detail.dart +++ b/lib/model/exercise_plan_detail.dart @@ -12,13 +12,16 @@ extension ExericisePlanDetailStateExt on ExercisePlanDetailState { class ExercisePlanDetail { int? exercisePlanDetailId; - late int exercisePlanId; + int? exercisePlanId; late int exerciseTypeId; int? serie; int? repeats; String? weightEquation; - List? exercises; + /// List + List? exercises; + + /// bool finished bool? finished; ExercisePlanDetailState state = ExercisePlanDetailState.start; @@ -53,7 +56,7 @@ class ExercisePlanDetail { jsonExercises = jsonExercises.replaceAll(r'\"null\"', 'null'); - print("Exercises $jsonExercises"); + //print("Exercises $jsonExercises"); Iterable iterable = jsonDecode(jsonExercises); this.exercises = iterable.map((exercise) => Exercise.fromJson(exercise)).toList(); } on Exception catch (e) { @@ -62,7 +65,7 @@ class ExercisePlanDetail { } Map toJson() => { - "exercisePlanId": exercisePlanId, + "exercisePlanId": exercisePlanId == null ? 0 : exercisePlanId, "exerciseTypeId": exerciseTypeId, "serie": serie, "repeats": repeats, @@ -76,6 +79,12 @@ class ExercisePlanDetail { "serie": serie, "repeats": repeats, "weightEquation": weightEquation, - 'exercises': exercises!.map((exercise) => exercise.toJson()).toList().toString(), + 'exercises': exercises == null ? [].toString() : exercises!.map((exercise) => exercise.toJson()).toList().toString(), }; + + @override + String toString() { + Map json = toJsonWithExerciseList(); + return json.toString(); + } } diff --git a/lib/model/fitness_state.dart b/lib/model/fitness_state.dart index e24c109..798b987 100644 --- a/lib/model/fitness_state.dart +++ b/lib/model/fitness_state.dart @@ -1,3 +1,23 @@ +enum Sport { football, fitness, footgolf } + +extension SportExt on Sport { + String toStr() => this.toString().split(".").last; + bool equalsTo(Sport sport) => this.toString() == sport.toString(); + bool equalsStringTo(String sport) => this.toStr() == sport; + + String description(Sport sport) { + if (Sport.football.equalsTo(sport)) { + return "Football"; + } else if (Sport.fitness.equalsTo(sport)) { + return "Fitness / Body Building"; + } else if (Sport.footgolf.equalsTo(sport)) { + return "Footgolf"; + } else { + return "Sport"; + } + } +} + class FitnessState { late final String value; late final String stateText; diff --git a/lib/model/tracking.dart b/lib/model/tracking.dart index 5fe9d23..377453a 100644 --- a/lib/model/tracking.dart +++ b/lib/model/tracking.dart @@ -7,7 +7,7 @@ class Tracking { late int customerId; late DateTime dateAdd; late String event; - late String eventValue; + String? eventValue; late String area; late String platform; late String version; @@ -19,6 +19,6 @@ class Tracking { "eventValue": eventValue, "area": Platform.localeName, "platform": Platform.isAndroid ? "Android" : "iOS", - "version": Cache().packageInfo.version + "+" + Cache().packageInfo.buildNumber + "version": Cache().packageInfo != null ? Cache().packageInfo!.version + "+" + Cache().packageInfo!.buildNumber : "" }; } diff --git a/lib/repository/customer_repository.dart b/lib/repository/customer_repository.dart index 33affc6..219690a 100644 --- a/lib/repository/customer_repository.dart +++ b/lib/repository/customer_repository.dart @@ -21,8 +21,8 @@ class GenderItem { } class CustomerRepository with Logging { - late Customer customer; - late Customer? _trainee; + Customer? customer; + Customer? _trainee; List? _trainees; List? _allProperties; final PropertyRepository propertyRepository = PropertyRepository(); @@ -68,51 +68,61 @@ class CustomerRepository with Logging { } String? get name { - return this.customer.name != null ? this.customer.name : ""; + return this.customer != null && this.customer!.name != null ? this.customer!.name : ""; } String? get firstName { - return this.customer.firstname != null ? this.customer.firstname : ""; + return this.customer != null && this.customer!.firstname != null ? this.customer!.firstname : ""; } String get sex { - return this.customer.sex == "m" ? "Man" : "Woman"; + if (this.customer == null) throw Exception("Initialize the customer object"); + return this.customer!.sex == "m" ? "Man" : "Woman"; } int? get birthYear { - return this.customer.birthYear; + if (this.customer == null) throw Exception("Initialize the customer object"); + return this.customer!.birthYear; } String? get goal { - return this.customer.goal; + if (this.customer == null) throw Exception("Initialize the customer object"); + return this.customer!.goal; } String? get fitnessLevel { - return this.customer.fitnessLevel; + if (this.customer == null) throw Exception("Initialize the customer object"); + return this.customer!.fitnessLevel; } String? get bodyType { - return this.customer.bodyType; + if (this.customer == null) throw Exception("Initialize the customer object"); + return this.customer!.bodyType; } setName(String name) { - this.customer.name = name; + if (this.customer == null) return; + this.customer!.name = name; } setFirstName(String firstName) { - this.customer.firstname = firstName; + if (this.customer == null) return; + this.customer!.firstname = firstName; } setPassword(String password) { - this.customer.password = password; + if (this.customer == null) return; + this.customer!.password = password; } setEmail(String email) { - this.customer.email = email; + if (this.customer == null) return; + this.customer!.email = email; } setSex(String sex) { - this.customer.sex = sex; + if (this.customer == null) return; + this.customer!.sex = sex; } setWeight(int weight) { @@ -126,19 +136,20 @@ class CustomerRepository with Logging { } setCustomerProperty(String propertyName, double value, {id = 0}) { - if (this.customer.properties[propertyName] == null) { - this.customer.properties[propertyName] = CustomerProperty( + if (this.customer == null) throw Exception("Initialize the customer object"); + if (this.customer!.properties[propertyName] == null) { + this.customer!.properties[propertyName] = CustomerProperty( propertyId: propertyRepository.getPropertyByName("Height")!.propertyId, - customerId: this.customer.customerId!, + customerId: this.customer!.customerId!, propertyValue: value, dateAdd: DateTime.now()); } else { - this.customer.properties[propertyName]!.propertyValue = value; + this.customer!.properties[propertyName]!.propertyValue = value; } - this.customer.properties[propertyName]!.dateAdd = DateTime.now(); - this.customer.properties[propertyName]!.newData = true; + this.customer!.properties[propertyName]!.dateAdd = DateTime.now(); + this.customer!.properties[propertyName]!.newData = true; if (id > 0) { - this.customer.properties[propertyName]!.customerPropertyId = id; + this.customer!.properties[propertyName]!.customerPropertyId = id; } } @@ -151,31 +162,36 @@ class CustomerRepository with Logging { } double getCustomerPropertyValue(String propertyName) { - if (this.customer.properties[propertyName] == null) { + if (this.customer == null || this.customer!.properties[propertyName] == null) { return 0.0; } else { - return this.customer.properties[propertyName]!.propertyValue; + return this.customer!.properties[propertyName]!.propertyValue; } } CustomerProperty? getCustomerProperty(String propertyName) { - return this.customer.properties[propertyName]; + if (this.customer == null) throw Exception("Initialize the customer object"); + return this.customer!.properties[propertyName]; } setBirthYear(int birthYear) { - this.customer.birthYear = birthYear; + if (this.customer == null) throw Exception("Initialize the customer object"); + this.customer!.birthYear = birthYear; } setFitnessLevel(String level) { - this.customer.fitnessLevel = level; + if (this.customer == null) throw Exception("Initialize the customer object"); + this.customer!.fitnessLevel = level; } setGoal(String goal) { - this.customer.goal = goal; + if (this.customer == null) throw Exception("Initialize the customer object"); + this.customer!.goal = goal; } setBodyType(String bodyType) { - this.customer.bodyType = bodyType; + if (this.customer == null) throw Exception("Initialize the customer object"); + this.customer!.bodyType = bodyType; } createNew() { @@ -183,7 +199,8 @@ class CustomerRepository with Logging { } Customer getCustomer() { - return this.customer; + if (this.customer == null) throw Exception("Initialize the customer object"); + return this.customer!; } void setCustomer(Customer customer) { @@ -191,12 +208,14 @@ class CustomerRepository with Logging { } Future addCustomer() async { - final Customer modelCustomer = customer; + if (this.customer == null) throw Exception("Initialize the customer object"); + final Customer modelCustomer = customer!; await CustomerApi().addCustomer(modelCustomer); } Future saveCustomer() async { - final Customer modelCustomer = customer; + if (this.customer == null) throw Exception("Initialize the customer object"); + final Customer modelCustomer = customer!; await CustomerApi().saveCustomer(modelCustomer); await this.saveProperties(modelCustomer.properties); } @@ -317,6 +336,7 @@ class CustomerRepository with Logging { } void addSizes(String sex) { + if (this.customer == null) throw Exception("Initialize the customer object"); List? properties = Cache().getProperties(); if (properties == null) { return; @@ -328,62 +348,62 @@ class CustomerRepository with Logging { if (element.propertyName == "Shoulder") { element.top = (122 * distortionHeight).toInt(); element.left = (130 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Shoulder"); + element.value = this.customer!.getProperty("Shoulder"); manSizes.add(element); } else if (element.propertyName == "Neck") { element.top = (68 * distortionHeight).toInt(); element.left = (130 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Neck"); + element.value = this.customer!.getProperty("Neck"); manSizes.add(element); } else if (element.propertyName == "Biceps") { element.top = (178 * distortionHeight).toInt(); element.left = (208 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Biceps"); + element.value = this.customer!.getProperty("Biceps"); manSizes.add(element); } else if (element.propertyName == "Chest") { element.top = (154 * distortionHeight).toInt(); element.left = (130 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Chest"); + element.value = this.customer!.getProperty("Chest"); manSizes.add(element); } else if (element.propertyName == "Belly") { element.top = (244 * distortionHeight).toInt(); element.left = (130 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Belly"); + element.value = this.customer!.getProperty("Belly"); manSizes.add(element); } else if (element.propertyName == "Hip") { element.top = (308 * distortionHeight).toInt(); element.left = (130 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Hip"); + element.value = this.customer!.getProperty("Hip"); manSizes.add(element); } else if (element.propertyName == "Thigh Top") { element.top = (332 * distortionHeight).toInt(); element.left = (165 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Thigh Top"); + element.value = this.customer!.getProperty("Thigh Top"); manSizes.add(element); } else if (element.propertyName == "Thigh Middle") { element.top = (382 * distortionHeight).toInt(); element.left = (100 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Thigh Middle"); + element.value = this.customer!.getProperty("Thigh Middle"); manSizes.add(element); } else if (element.propertyName == "Knee") { element.top = (464 * distortionHeight).toInt(); element.left = (97 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Knee"); + element.value = this.customer!.getProperty("Knee"); manSizes.add(element); } else if (element.propertyName == "Calf") { element.top = (520 * distortionHeight).toInt(); element.left = (97 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Calf"); + element.value = this.customer!.getProperty("Calf"); manSizes.add(element); } else if (element.propertyName == "Ankle") { element.top = (620 * distortionHeight).toInt(); element.left = (150 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Ankle"); + element.value = this.customer!.getProperty("Ankle"); manSizes.add(element); } else if (element.propertyName == "Weight") { element.top = (402 * distortionHeight).toInt(); element.left = (240 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Weight"); + element.value = this.customer!.getProperty("Weight"); manSizes.add(element); } }); @@ -392,62 +412,62 @@ class CustomerRepository with Logging { if (element.propertyName == "Shoulder") { element.top = (122 * distortionHeight).toInt(); element.left = (151 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Shoulder"); + element.value = this.customer!.getProperty("Shoulder"); manSizes.add(element); } else if (element.propertyName == "Neck") { element.top = (78 * distortionHeight).toInt(); element.left = (151 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Neck"); + element.value = this.customer!.getProperty("Neck"); manSizes.add(element); } else if (element.propertyName == "Biceps") { element.top = (178 * distortionHeight).toInt(); element.left = (212 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Biceps"); + element.value = this.customer!.getProperty("Biceps"); manSizes.add(element); } else if (element.propertyName == "Chest") { element.top = (154 * distortionHeight).toInt(); element.left = (151 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Chest"); + element.value = this.customer!.getProperty("Chest"); manSizes.add(element); } else if (element.propertyName == "Belly") { element.top = (230 * distortionHeight).toInt(); element.left = (151 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Belly"); + element.value = this.customer!.getProperty("Belly"); manSizes.add(element); } else if (element.propertyName == "Hip") { element.top = (294 * distortionHeight).toInt(); element.left = (151 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Hip"); + element.value = this.customer!.getProperty("Hip"); manSizes.add(element); } else if (element.propertyName == "Thigh Top") { element.top = (335 * distortionHeight).toInt(); element.left = (185 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Thigh Top"); + element.value = this.customer!.getProperty("Thigh Top"); manSizes.add(element); } else if (element.propertyName == "Thigh Middle") { element.top = (377 * distortionHeight).toInt(); element.left = (125 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Thigh Middle"); + element.value = this.customer!.getProperty("Thigh Middle"); manSizes.add(element); } else if (element.propertyName == "Knee") { element.top = (468 * distortionHeight).toInt(); element.left = (129 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Knee"); + element.value = this.customer!.getProperty("Knee"); manSizes.add(element); } else if (element.propertyName == "Calf") { element.top = (525 * distortionHeight).toInt(); element.left = (129 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Calf"); + element.value = this.customer!.getProperty("Calf"); manSizes.add(element); } else if (element.propertyName == "Ankle") { element.top = (620 * distortionHeight).toInt(); element.left = (162 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Ankle"); + element.value = this.customer!.getProperty("Ankle"); manSizes.add(element); } else if (element.propertyName == "Weight") { element.top = (402 * distortionHeight).toInt(); element.left = (240 * distortionWidth).toInt(); - element.value = this.customer.getProperty("Weight"); + element.value = this.customer!.getProperty("Weight"); manSizes.add(element); } }); diff --git a/lib/repository/evaluation_repository.dart b/lib/repository/evaluation_repository.dart new file mode 100644 index 0000000..2c85301 --- /dev/null +++ b/lib/repository/evaluation_repository.dart @@ -0,0 +1,77 @@ +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/model/evaluation.dart'; +import 'package:aitrainer_app/model/evaluation_attribute.dart'; +import 'package:aitrainer_app/util/enums.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class EvaluationRepository { + List? evaluations; + + EvaluationRepository() { + evaluations = Cache().evaluations; + } + + String getEvaluationTextByExerciseType(int exerciseTypeId, double value) { + String? eval; + + Evaluation? evaluation = this.getEvaluationByExerciseTypeId(exerciseTypeId); + Customer? customer = Cache().userLoggedIn; + + if (evaluation != null && customer != null) { + int? age = customer.age; + if (age != null) { + for (var attribute in evaluation.attributes) { + EvaluationAttribute attr = attribute as EvaluationAttribute; + if (age >= attr.ageMin && age <= attr.ageMax && value >= attr.valueMin && value <= attr.valueMax) { + eval = attr.evaluationText; + break; + } + } + } + } + + if (eval == null) { + eval = EvaluationText.fair.toStr(); + } + return eval; + } + + Evaluation? getEvaluationByExerciseTypeId(int exerciseTypeId) { + Evaluation? eval; + if (evaluations != null) { + for (var evaluation in evaluations!) { + if (evaluation.exerciseTypeId == exerciseTypeId) { + eval = evaluation; + break; + } + } + } + return eval; + } + + Color getEvaluationColor(String eval) { + Color color = Colors.yellow[100]!; + + if (EvaluationText.very_poor.equalsStringTo(eval)) { + return Colors.red[800]!; + } else if (EvaluationText.poor.equalsStringTo(eval)) { + return Colors.red[400]!; + } else if (EvaluationText.below_average.equalsStringTo(eval)) { + return Colors.orange[600]!; + } else if (EvaluationText.average.equalsStringTo(eval)) { + return Colors.yellow[600]!; + } else if (EvaluationText.above_average.equalsStringTo(eval)) { + return Colors.greenAccent; + } else if (EvaluationText.good.equalsStringTo(eval)) { + return Colors.green[400]!; + } else if (EvaluationText.excellent.equalsStringTo(eval)) { + return Colors.green[600]!; + } else if (EvaluationText.elite.equalsStringTo(eval)) { + return Colors.green[800]!; + } + + return color; + } +} diff --git a/lib/repository/exercise_device_repository.dart b/lib/repository/exercise_device_repository.dart index e14fa5b..a82d400 100644 --- a/lib/repository/exercise_device_repository.dart +++ b/lib/repository/exercise_device_repository.dart @@ -37,6 +37,7 @@ class ExerciseDeviceRepository { } List getGymDevices() { + if (Cache().getDevices() == null) return []; final List gymDevices = []; if (_devices.isEmpty) { _devices = Cache().getDevices()!; diff --git a/lib/repository/exercise_plan_repository.dart b/lib/repository/exercise_plan_repository.dart index dd3d05e..b0d4092 100644 --- a/lib/repository/exercise_plan_repository.dart +++ b/lib/repository/exercise_plan_repository.dart @@ -27,8 +27,13 @@ class ExercisePlanRepository { ExercisePlan? getExercisePlan() => exercisePlan; void addDetailToPlan() { + if (exercisePlan == null) { + this.createNewPlan(); + } if (exercisePlan != null && actualPlanDetail != null) { - actualPlanDetail!.exercisePlanId = exercisePlan!.exercisePlanId!; + if (exercisePlan!.exercisePlanId != null) { + actualPlanDetail!.exercisePlanId = exercisePlan!.exercisePlanId!; + } exercisePlanDetails[actualPlanDetail!.exerciseTypeId] = actualPlanDetail!; Cache().addToMyExercisePlanDetails(actualPlanDetail!); } @@ -85,24 +90,14 @@ class ExercisePlanRepository { } void removeExerciseTypeFromPlanByExerciseTypeId(int exerciseTypeId) { + if (exercisePlanDetails[exerciseTypeId] == null) return; exercisePlanDetails[exerciseTypeId]!.change = ModelChange.delete; Cache().deleteMyExercisePlanDetailByExerciseTypeId(exerciseTypeId); } Future saveExercisePlan() async { if (exercisePlan == null) { - if (Cache().userLoggedIn == null) { - throw Exception("please log in"); - } - - String exercisePlanName; - if (this.customerId == Cache().userLoggedIn!.customerId) { - exercisePlanName = Cache().userLoggedIn!.name! + " private"; - } else { - exercisePlanName = Cache().getTrainee()!.name! + " " + Cache().getTrainee()!.firstname! + " private"; - } - - exercisePlan = ExercisePlan(exercisePlanName, this.customerId); + this.createNewPlan(); } if (newPlan) { exercisePlan!.dateAdd = DateTime.now(); @@ -139,6 +134,22 @@ class ExercisePlanRepository { } } + void createNewPlan() { + if (Cache().userLoggedIn == null) { + throw Exception("please log in"); + } + + String exercisePlanName; + if (this.customerId == Cache().userLoggedIn!.customerId) { + exercisePlanName = Cache().userLoggedIn!.name! + " private"; + } else { + exercisePlanName = Cache().getTrainee()!.name! + " " + Cache().getTrainee()!.firstname! + " private"; + } + + exercisePlan = ExercisePlan(exercisePlanName, this.customerId); + newPlan = true; + } + Future getLastExercisePlan() async { if (customerId == 0) { return null; @@ -150,7 +161,12 @@ class ExercisePlanRepository { } exercisePlan = await ExercisePlanApi().getLastExercisePlan(customerId); + newPlan = (exercisePlan == null); + if (exercisePlan == null) { + this.createNewPlan(); + } + ; Cache().setMyExercisePlan(exercisePlan!); return exercisePlan; } @@ -170,16 +186,19 @@ class ExercisePlanRepository { exercisePlanDetails = listCache; return; } else { - list = await ExercisePlanApi().getExercisePlanDetail(exercisePlan!.exercisePlanId!); + if (exercisePlan!.exercisePlanId != null) { + list = await ExercisePlanApi().getExercisePlanDetail(exercisePlan!.exercisePlanId!); + } } exercisePlanDetails = LinkedHashMap(); - - list.forEach((element) { - newPlan = false; - ExercisePlanDetail detail = element; - exercisePlanDetails[detail.exerciseTypeId] = detail; - }); + if (list.isNotEmpty) { + list.forEach((element) { + newPlan = false; + ExercisePlanDetail detail = element; + exercisePlanDetails[detail.exerciseTypeId] = detail; + }); + } Cache().setMyExercisePlanDetails(exercisePlanDetails); return; diff --git a/lib/repository/exercise_repository.dart b/lib/repository/exercise_repository.dart index 09af0d0..e066cfd 100644 --- a/lib/repository/exercise_repository.dart +++ b/lib/repository/exercise_repository.dart @@ -270,16 +270,167 @@ class ExerciseRepository { void getSameExercise(int exerciseTypeId, String day) { this.actualExerciseList = []; if (exerciseList != null) { + int index = 0; for (int i = 0; i < this.exerciseList!.length; i++) { Exercise exercise = exerciseList![i]; - String exerciseDate = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(exercise.dateAdd!); - if (exerciseTypeId == exercise.exerciseTypeId && exerciseDate == day) { + final String exerciseDate = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(exercise.dateAdd!); + if (exerciseTypeId == exercise.exerciseTypeId && exerciseDate == day && index < 4) { this.actualExerciseList!.add(exercise); + index++; } } } } + double calculate1RM(Exercise exercise) { + double weight = exercise.unitQuantity!; + double repeat = exercise.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; + } + + double getBest1RMPercent(Exercise exercise) { + double result = 0; + if (this.exerciseList == null || this.exerciseList!.isEmpty) { + this.exerciseList = Cache().getExercises(); + } + + final int exerciseTypeId = exercise.exerciseTypeId!; + double toCompare = this.calculate1RM(exercise); + + final String today = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(DateTime.now()); + List oldExercises = []; + this.exerciseList!.forEach((exercise) { + final String exerciseDate = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(exercise.dateAdd!); + if (exercise.exerciseTypeId == exerciseTypeId && exerciseDate.compareTo(today) < 0) { + oldExercises.add(exercise); + } + }); + + if (oldExercises.isNotEmpty) { + oldExercises.sort((a, b) { + double sumA = 0; + double sumB = 0; + if (a.unitQuantity != null && b.unitQuantity != null) { + sumA = a.quantity! * a.unitQuantity!; + sumB = b.quantity! * b.unitQuantity!; + } else { + sumA = a.quantity!; + sumB = b.quantity!; + } + return sumA >= sumB ? 1 : -1; + }); + + double withCompare = this.calculate1RM(oldExercises.last); + + result = toCompare >= withCompare ? (1 - toCompare / withCompare) * 100 : (1 - toCompare / withCompare) * -100; + } + return result; + } + + double getLast1RMPercent(Exercise exercise) { + double result = 0; + if (this.exerciseList == null || this.exerciseList!.isEmpty) { + this.exerciseList = Cache().getExercises(); + } + + final int exerciseTypeId = exercise.exerciseTypeId!; + double toCompare = this.calculate1RM(exercise); + + final String today = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(exercise.dateAdd!); + List oldExercises = []; + this.exerciseList!.forEach((exercise) { + final String exerciseDate = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(exercise.dateAdd!); + if (exercise.exerciseTypeId == exerciseTypeId && exerciseDate.compareTo(today) < 0) { + oldExercises.add(exercise); + } + }); + + if (oldExercises.isNotEmpty) { + double withCompare = this.calculate1RM(oldExercises.first); + result = toCompare >= withCompare ? (toCompare / withCompare) * 100 : (1 - toCompare / withCompare) * -100; + } + return result; + } + + double getBestExercisePercent(Exercise exercise) { + double result = 0; + if (this.exerciseList == null || this.exerciseList!.isEmpty) { + this.exerciseList = Cache().getExercises(); + } + + final int exerciseTypeId = exercise.exerciseTypeId!; + double toCompare = exercise.unitQuantity != null ? exercise.quantity! * exercise.unitQuantity! : exercise.quantity!; + + final String today = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(DateTime.now()); + List oldExercises = []; + this.exerciseList!.forEach((exercise) { + final String exerciseDate = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(exercise.dateAdd!); + if (exercise.exerciseTypeId == exerciseTypeId && exerciseDate.compareTo(today) < 0) { + oldExercises.add(exercise); + } + }); + + if (oldExercises.isNotEmpty) { + oldExercises.sort((a, b) { + double sumA = 0; + double sumB = 0; + if (a.unitQuantity != null && b.unitQuantity != null) { + sumA = a.quantity! * a.unitQuantity!; + sumB = b.quantity! * b.unitQuantity!; + } else { + sumA = a.quantity!; + sumB = b.quantity!; + } + return sumA >= sumB ? 1 : -1; + }); + + double withCompare = oldExercises.last.unitQuantity != null + ? oldExercises.last.quantity! * oldExercises.last.unitQuantity! + : oldExercises.last.quantity!; + + result = toCompare >= withCompare ? (1 - toCompare / withCompare) * 100 : (1 - toCompare / withCompare) * -100; + print("Last Best: ${oldExercises.last} - result: $result"); + } + return result; + } + + double getLastExercisePercent(Exercise exercise) { + double result = 0; + if (this.exerciseList == null || this.exerciseList!.isEmpty) { + this.exerciseList = Cache().getExercises(); + } + + final int exerciseTypeId = exercise.exerciseTypeId!; + double toCompare = exercise.unitQuantity != null ? exercise.quantity! * exercise.unitQuantity! : exercise.quantity!; + + final String today = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(exercise.dateAdd!); + List oldExercises = []; + this.exerciseList!.forEach((exercise) { + final String exerciseDate = DateFormat("yyyy-MM-dd", AppLanguage().appLocal.toString()).format(exercise.dateAdd!); + if (exercise.exerciseTypeId == exerciseTypeId && exerciseDate.compareTo(today) < 0) { + oldExercises.add(exercise); + } + }); + + if (oldExercises.isNotEmpty) { + double withCompare = oldExercises.first.unitQuantity != null + ? oldExercises.first.quantity! * oldExercises.first.unitQuantity! + : oldExercises.first.quantity!; + + result = toCompare >= withCompare ? (toCompare / withCompare) * 100 : (1 - toCompare / withCompare) * -100; + print("Last Last: ${oldExercises.first} vs. $exercise - - result: $result"); + } + return result; + } + void sortByDate() { if (exerciseList == null || exerciseList!.isEmpty) { return; diff --git a/lib/repository/property_repository.dart b/lib/repository/property_repository.dart index 2c23594..e5f23fb 100644 --- a/lib/repository/property_repository.dart +++ b/lib/repository/property_repository.dart @@ -3,7 +3,7 @@ import 'package:aitrainer_app/model/property.dart'; import 'package:aitrainer_app/service/property_service.dart'; class PropertyRepository { - late List? _properties; + List? _properties; Future?> getDBProperties() async { this._properties = await PropertyApi().getProperties(); diff --git a/lib/repository/workout_tree_repository.dart b/lib/repository/workout_tree_repository.dart index a7cbab5..a0d2a41 100644 --- a/lib/repository/workout_tree_repository.dart +++ b/lib/repository/workout_tree_repository.dart @@ -112,7 +112,9 @@ class WorkoutTreeRepository with Logging { parent != null ? parent.nameEnglish : "", 0); this.tree[exerciseType.name] = menuItem; - menuAsExercise.add(menuItem); + if (isRunning || is1RM) { + menuAsExercise.add(menuItem); + } //log("ExerciseType in Menu item ${exerciseType.toJson()} is1RM: $is1RM"); }); } else { diff --git a/lib/service/api.dart b/lib/service/api.dart index d94b765..7a949ec 100644 --- a/lib/service/api.dart +++ b/lib/service/api.dart @@ -8,6 +8,7 @@ import 'package:aitrainer_app/model/cache.dart'; class APIClient with Common, Logging { Future get(String endPoint, String param) async { final url = Cache.getBaseUrl() + endPoint + param; + trace("-------- API get " + url); String authToken = Cache().getAuthToken(); if (authToken.length == 0) { @@ -57,6 +58,7 @@ class APIClient with Common, Logging { final response = await http.post(uri, headers: {'Authorization': '1', 'Content-Type': 'application/json'}, body: body); final responseCode = response.statusCode; if (responseCode != 200) { + trace("authentication response: $responseCode"); return { "error": "Authentication error, total failure", }; @@ -65,6 +67,7 @@ class APIClient with Common, Logging { final responseJson = json.decode(response.body); return responseJson; } catch (exception) { + print(exception.toString()); return {"error": "Network error, try again later " + exception.toString()}; } } diff --git a/lib/service/exercise_plan_service.dart b/lib/service/exercise_plan_service.dart index 7b9d678..181cfac 100644 --- a/lib/service/exercise_plan_service.dart +++ b/lib/service/exercise_plan_service.dart @@ -91,9 +91,10 @@ class ExercisePlanApi with Logging { final String responseBody = await _client.get("exercise_plan/last/" + customerId.toString(), body); exercisePlan = ExercisePlan.fromJson(jsonDecode(responseBody)); } on Exception catch (e) { + print(e.toString()); if (e is NotFoundException) { log("ExercisePlan not found for " + customerId.toString()); - return exercisePlan; + return null; } else { throw new Exception(e.toString()); } @@ -104,7 +105,7 @@ class ExercisePlanApi with Logging { Future> getExercisePlanDetail(int exercisePlanId) async { String body = ""; log(" ===== get exercisePlanDetail $exercisePlanId"); - List listExercisePlanDetail; + List listExercisePlanDetail = []; try { final String responseBody = await _client.get("exercise_plan_detail/" + exercisePlanId.toString(), body); log("response body:" + responseBody); diff --git a/lib/service/firebase_api.dart b/lib/service/firebase_api.dart index 3e3e225..b224a3d 100644 --- a/lib/service/firebase_api.dart +++ b/lib/service/firebase_api.dart @@ -109,7 +109,7 @@ class FirebaseApi with logging.Logging { // Sign in the user with Firebase. If the nonce we generated earlier does // not match the nonce in `appleCredential.identityToken`, sign in will fail. UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(oauthCredential); - + Cache().firebaseUid = userCredential.user!.uid; log("userCredential: " + userCredential.toString()); log("Apple Credentials: " + @@ -181,7 +181,8 @@ class FirebaseApi with logging.Logging { idToken: googleAuth.idToken, ); - await FirebaseAuth.instance.signInWithCredential(credential); + UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(credential); + Cache().firebaseUid = userCredential.user!.uid; log("GoogleUser: " + googleUser.toString()); userData['email'] = googleUser.email; @@ -230,8 +231,9 @@ class FirebaseApi with logging.Logging { Map userData; // by default the login method has the next permissions ['email','public_profile'] - AccessToken? accessToken = await FacebookAuth.instance.accessToken; - if (accessToken != null) { + final LoginResult result = await FacebookAuth.instance.login(); + if (result.status == LoginStatus.success) { + final AccessToken accessToken = result.accessToken!; log(accessToken.toJson().toString()); Cache().accessTokenFacebook = accessToken; // get the user data @@ -249,8 +251,9 @@ class FirebaseApi with logging.Logging { Map userData; // by default the login method has the next permissions ['email','public_profile'] - AccessToken? accessToken = await FacebookAuth.instance.accessToken; - if (accessToken != null) { + final LoginResult result = await FacebookAuth.instance.login(); + if (result.status == LoginStatus.success) { + final AccessToken accessToken = result.accessToken!; Cache().accessTokenFacebook = accessToken; // get the user data userData = await FacebookAuth.instance.getUserData(); diff --git a/lib/service/package_service.dart b/lib/service/package_service.dart index 3864fb8..07fefe1 100644 --- a/lib/service/package_service.dart +++ b/lib/service/package_service.dart @@ -4,6 +4,7 @@ import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/model/customer.dart'; import 'package:aitrainer_app/model/customer_exercise_device.dart'; import 'package:aitrainer_app/model/customer_property.dart'; +import 'package:aitrainer_app/model/evaluation.dart'; import 'package:aitrainer_app/model/exercise.dart'; import 'package:aitrainer_app/model/exercise_device.dart'; import 'package:aitrainer_app/model/exercise_plan_template.dart'; @@ -59,6 +60,9 @@ class PackageApi { Cache().setExercisePlanTemplates(exercisePlanTemplates); } else if (headRecord[0] == "ExerciseTreeParents") { exerciseTreeParents = json.map((exerciseTreeParent) => ExerciseTreeParents.fromJson(exerciseTreeParent)).toList(); + } else if (headRecord[0] == "Evaluation") { + final List evaluations = json.map((evaluation) => Evaluation.fromJson(evaluation)).toList(); + Cache().evaluations = evaluations; } }); diff --git a/lib/util/common.dart b/lib/util/common.dart index 8b3d17b..ef08ff6 100644 --- a/lib/util/common.dart +++ b/lib/util/common.dart @@ -98,17 +98,22 @@ mixin Common { return datePart; } - static String? emailValidation(String email) { + static String? emailValidation(String? email) { + final String error = "Please type an email address"; + if (email == null) { + return error; + } bool emailValid = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(email); - return emailValid ? null : "Please type an email address"; + return emailValid ? null : error; } static String? passwordValidation(String? value) { + final String error = "Password too short"; if (value == null || value.length == 0) { - return null; + return error; } bool valid = 8 < value.length; - return valid ? null : "Password too short"; + return valid ? null : error; } static Widget badgedIcon(Color color, IconData icon, String badgeKey) { diff --git a/lib/util/enums.dart b/lib/util/enums.dart index f3d16bc..e7576ba 100644 --- a/lib/util/enums.dart +++ b/lib/util/enums.dart @@ -73,3 +73,11 @@ extension SizesExt on SizesEnum { bool equalsTo(SizesEnum event) => this.toString() == event.toString(); bool equalsStringTo(String event) => this.toString() == event; } + +enum EvaluationText { very_poor, poor, fair, below_average, average, above_average, good, excellent, elite } + +extension EvaluationTextExt on EvaluationText { + String toStr() => this.toString().split(".").last; + bool equalsTo(EvaluationText eval) => this.toString() == eval.toString(); + bool equalsStringTo(String eval) => this.toStr() == eval; +} diff --git a/lib/util/track.dart b/lib/util/track.dart index bf40084..3bfae7b 100644 --- a/lib/util/track.dart +++ b/lib/util/track.dart @@ -3,8 +3,8 @@ import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/service/logging.dart'; import 'package:aitrainer_app/service/tracking_service.dart'; import 'package:aitrainer_app/util/enums.dart'; -import 'package:aitrainer_app/library/flurry.dart'; import 'package:aitrainer_app/model/tracking.dart' as model; +import 'package:flurry/flurry.dart'; import 'package:smartlook/smartlook.dart'; class Track with Logging { diff --git a/lib/util/trans.dart b/lib/util/trans.dart index ef4e07d..618c473 100644 --- a/lib/util/trans.dart +++ b/lib/util/trans.dart @@ -8,6 +8,9 @@ mixin Trans { } String t(String text) { + if (context == null) { + throw Exception("Translation: initialize the context"); + } return AppLocalizations.of(context)!.translate(text); } } diff --git a/lib/view/account.dart b/lib/view/account.dart index 2f0660c..da7a2b0 100644 --- a/lib/view/account.dart +++ b/lib/view/account.dart @@ -52,9 +52,11 @@ class AccountPage extends StatelessWidget with Trans { customerName = accountBloc.customerRepository.firstName! + " " + accountBloc.customerRepository.name!; customerName = customerName.length < 3 ? t("Personal data") : customerName; - goal = accountBloc.customerRepository.customer.goal != null ? t(accountBloc.customerRepository.customer.goal!) : goal; - fitnessLevel = accountBloc.customerRepository.customer.fitnessLevel != null - ? t(capitalize(accountBloc.customerRepository.customer.fitnessLevel!)) + goal = accountBloc.customerRepository.customer != null && accountBloc.customerRepository.customer!.goal != null + ? t(accountBloc.customerRepository.customer!.goal!) + : goal; + fitnessLevel = accountBloc.customerRepository.customer != null && accountBloc.customerRepository.customer!.fitnessLevel != null + ? t(capitalize(accountBloc.customerRepository.customer!.fitnessLevel!)) : fitnessLevel; bodyType = accountBloc.getAccurateBodyType(); @@ -166,8 +168,6 @@ class AccountPage extends StatelessWidget with Trans { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(t("Available Devices"), style: TextStyle(color: Colors.orange)), Icon(Icons.arrow_forward_ios)]), - //textColor: Colors.orange, - //color: Colors.white, onPressed: () => { if (Cache().userLoggedIn != null) { @@ -185,7 +185,7 @@ class AccountPage extends StatelessWidget with Trans { String text = "Logout"; Color buttonColor = Colors.orange; - if (accountBloc.customerRepository.customer.email == null) { + if (accountBloc.customerRepository.customer == null || accountBloc.customerRepository.customer!.email == null) { text = "Login"; buttonColor = Colors.blue; } @@ -220,7 +220,7 @@ class AccountPage extends StatelessWidget with Trans { } Widget getMyTrainees(BuildContext context, AccountBloc accountBloc) { - if (accountBloc.customerRepository.customer.trainer == 0) { + if (accountBloc.customerRepository.customer == null || accountBloc.customerRepository.customer!.trainer == 0) { return ListTile( title: Container(), ); diff --git a/lib/view/customer_fitness_page.dart b/lib/view/customer_fitness_page.dart index 5dc05a9..7725c60 100644 --- a/lib/view/customer_fitness_page.dart +++ b/lib/view/customer_fitness_page.dart @@ -11,6 +11,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../bloc/customer_change/customer_change_bloc.dart'; +import '../library/dropdown_search.dart'; // ignore: must_be_immutable class CustomerFitnessPage extends StatefulWidget { @@ -22,10 +26,8 @@ class CustomerFitnessPage extends StatefulWidget { } } -// dropbox for professional sport - class _CustomerFitnessPageState extends State with Trans { - late String selected; + String? selected; bool fulldata = false; @override @@ -41,8 +43,6 @@ class _CustomerFitnessPageState extends State with Trans { customerRepository = ModalRoute.of(context)!.settings.arguments as CustomerRepository; } - selected = customerRepository.customer.fitnessLevel!; - PreferredSizeWidget _bar = AppBarMin( back: true, ); @@ -56,6 +56,10 @@ class _CustomerFitnessPageState extends State with Trans { child: Builder(builder: (context) { // ignore: close_sinks CustomerChangeBloc changeBloc = BlocProvider.of(context); + selected = changeBloc.selectedFitnessItem; + if (selected == null) { + selected = FitnessState.beginner; + } return SingleChildScrollView( scrollDirection: Axis.vertical, child: Container( @@ -85,7 +89,7 @@ class _CustomerFitnessPageState extends State with Trans { TextButton( style: TextButton.styleFrom( padding: EdgeInsets.all(10.0), - shape: getShape(customerRepository, FitnessState.beginner), + shape: getShape(changeBloc, FitnessState.beginner), ), child: Container( width: cWidth, @@ -103,14 +107,14 @@ class _CustomerFitnessPageState extends State with Trans { onPressed: () => { setState(() { selected = FitnessState.beginner; - changeBloc.add(CustomerFitnessChange(fitness: selected)); + changeBloc.add(CustomerFitnessChange(fitness: selected!)); }), }), Divider(), TextButton( style: TextButton.styleFrom( padding: EdgeInsets.all(10.0), - shape: getShape(customerRepository, FitnessState.intermediate), + shape: getShape(changeBloc, FitnessState.intermediate), ), child: Container( width: cWidth, @@ -136,7 +140,7 @@ class _CustomerFitnessPageState extends State with Trans { onPressed: () => { setState(() { selected = FitnessState.intermediate; - changeBloc.add(CustomerFitnessChange(fitness: selected)); + changeBloc.add(CustomerFitnessChange(fitness: selected!)); print(selected); }), }), @@ -144,7 +148,7 @@ class _CustomerFitnessPageState extends State with Trans { TextButton( style: TextButton.styleFrom( padding: EdgeInsets.all(10.0), - shape: getShape(customerRepository, FitnessState.advanced), + shape: getShape(changeBloc, FitnessState.advanced), ), child: Container( width: cWidth, @@ -170,7 +174,7 @@ class _CustomerFitnessPageState extends State with Trans { onPressed: () => { setState(() { selected = FitnessState.advanced; - changeBloc.add(CustomerFitnessChange(fitness: selected)); + changeBloc.add(CustomerFitnessChange(fitness: selected!)); print(selected); }), }), @@ -178,7 +182,7 @@ class _CustomerFitnessPageState extends State with Trans { TextButton( style: TextButton.styleFrom( padding: EdgeInsets.all(10.0), - shape: getShape(customerRepository, FitnessState.professional), + shape: getShape(changeBloc, FitnessState.professional), ), child: Container( width: cWidth, @@ -204,11 +208,13 @@ class _CustomerFitnessPageState extends State with Trans { onPressed: () => { setState(() { selected = FitnessState.professional; - changeBloc.add(CustomerFitnessChange(fitness: selected)); + changeBloc.add(CustomerFitnessChange(fitness: selected!)); print(selected); }), }), Divider(), + selected == FitnessState.professional ? getSport(changeBloc) : Offstage(), + Divider(), ElevatedButton( style: ElevatedButton.styleFrom( onPrimary: Colors.white, @@ -228,8 +234,8 @@ class _CustomerFitnessPageState extends State with Trans { }))); } - dynamic getShape(CustomerRepository customerRepository, String fitnessLevel) { - String selected = customerRepository.fitnessLevel!; + dynamic getShape(CustomerChangeBloc changeBloc, String fitnessLevel) { + String? selected = changeBloc.selectedFitnessItem; dynamic returnCode = (selected == fitnessLevel) ? RoundedRectangleBorder( side: BorderSide(width: 4, color: Colors.orange), @@ -240,4 +246,90 @@ class _CustomerFitnessPageState extends State with Trans { //return return returnCode; } + + Widget getSport(CustomerChangeBloc bloc) { + Sport? selected = bloc.getSelectedSport; + return Container( + padding: EdgeInsets.only(left: 65, right: 65), + child: DropdownSearch( + dropdownSearchDecoration: InputDecoration( + contentPadding: EdgeInsets.only(left: 15, top: 5, bottom: 5), + labelText: t("Sport"), + labelStyle: GoogleFonts.inter(fontSize: 16, color: Colors.indigo), + //fillColor: Colors.black38, + filled: false, + border: OutlineInputBorder( + gapPadding: 2.0, + borderRadius: BorderRadius.circular(12.0), + borderSide: BorderSide(color: Colors.blue, width: 0.4), + ), + ), + mode: Mode.MENU, + compareFn: (Sport i, Sport s) => i.equalsTo(s), + showSelectedItem: true, + selectedItem: selected, + itemAsString: (data) => t(data.toStr()), + onChanged: (data) { + bloc.add(CustomerSportChange(sport: data)); + }, + dropdownBuilder: _customDropDownItem, + popupItemBuilder: _customMenuBuilder, + popupBarrierColor: Colors.white10, + //popupBackgroundColor: Colors.yellow, + items: Sport.values, + dropDownButton: Icon( + Icons.arrow_drop_down, + color: Colors.indigo, + ), + )); + //items: FitnessItem().toList())); + } + + Widget _customMenuBuilder(BuildContext context, Sport sport, bool isSelected) { + //bool selected = bloc.getSelectedSport; + return Container( + decoration: !isSelected + ? BoxDecoration(color: Colors.grey[300]) + : BoxDecoration( + border: Border.all(color: Colors.blue), + borderRadius: BorderRadius.circular(12), + color: Colors.grey[100], + ), + child: ListTile( + selected: isSelected, + title: Text( + t(sport.toStr()), + style: GoogleFonts.archivoBlack(fontSize: 20, color: Colors.blue[600]), + ), + subtitle: Text( + t(sport.description(sport)), + style: GoogleFonts.inter(fontSize: 12, color: Colors.blue[600]), + ), + ), + ); + } + + Widget _customDropDownItem(BuildContext context, Sport? item, String itemDesignation) { + return Container( + child: (item == null) + ? ListTile( + contentPadding: EdgeInsets.all(0), + title: Text( + t("No item selected"), + style: GoogleFonts.inter(fontSize: 14, color: Colors.blue[600]), + ), + ) + : ListTile( + contentPadding: EdgeInsets.all(0), + title: Text( + t(item.toStr()), + style: GoogleFonts.archivoBlack(fontSize: 20, color: Colors.blue[600]), + ), + subtitle: Text( + t(item.description(item)), + style: GoogleFonts.inter(fontSize: 12, color: Colors.blue[600]), + ), + ), + ); + } } diff --git a/lib/view/customer_goal_page.dart b/lib/view/customer_goal_page.dart index b6d50b0..245ac94 100644 --- a/lib/view/customer_goal_page.dart +++ b/lib/view/customer_goal_page.dart @@ -9,6 +9,7 @@ import 'package:aitrainer_app/widgets/app_bar_progress.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; class GoalsItem { static String muscle = "gain_muscle"; @@ -70,7 +71,8 @@ class _CustomerGoalPage extends State with Trans { InkWell( child: Text( AppLocalizations.of(context)!.translate("Set Your Goals"), - style: TextStyle(color: Colors.orange, fontSize: 50, fontFamily: 'Arial', fontWeight: FontWeight.w900), + style: + GoogleFonts.archivoBlack(color: Colors.orange, fontSize: 30, fontWeight: FontWeight.w900), ), highlightColor: Colors.white, ), @@ -146,6 +148,7 @@ class _CustomerGoalPage extends State with Trans { } dynamic getShape(CustomerChangeBloc customerBloc, String goal) { + if (customerBloc.customerRepository.goal == null) return null; String selectedGoal = customerBloc.customerRepository.goal!; dynamic returnCode = (selectedGoal == goal) ? RoundedRectangleBorder( diff --git a/lib/view/customer_modify_page.dart b/lib/view/customer_modify_page.dart index dd43ea1..93efdb3 100644 --- a/lib/view/customer_modify_page.dart +++ b/lib/view/customer_modify_page.dart @@ -26,12 +26,12 @@ class CustomerModifyPage extends StatelessWidget with Trans { @override Widget build(BuildContext context) { + setContext(context); dynamic arguments = ModalRoute.of(context)!.settings.arguments; if (arguments is HashMap && arguments['personal_data'] != null) { fulldata = arguments['personal_data']; } - setContext(context); // ignore: close_sinks final accountBloc = BlocProvider.of(context); @@ -88,7 +88,6 @@ class CustomerModifyPage extends StatelessWidget with Trans { Widget loadForm(CustomerChangeBloc customerBloc) { return Form( key: _scaffoldKey, - autovalidateMode: AutovalidateMode.always, child: SingleChildScrollView( scrollDirection: Axis.vertical, padding: EdgeInsets.only(top: 40, left: 25, right: 25, bottom: 250), @@ -96,7 +95,7 @@ class CustomerModifyPage extends StatelessWidget with Trans { alignment: Alignment.center, child: Column( children: [ - Text(t("Edit Profile"), style: GoogleFonts.inter(color: Colors.indigo, fontSize: 16), textAlign: TextAlign.center), + Text(t("Edit Profile"), style: GoogleFonts.archivoBlack(color: Colors.indigo, fontSize: 20), textAlign: TextAlign.center), Divider( color: Colors.transparent, ), @@ -114,7 +113,7 @@ class CustomerModifyPage extends StatelessWidget with Trans { borderSide: BorderSide(color: Colors.green[50]!, width: 0.4), ), ), - initialValue: customerBloc.customerRepository.customer.email, + initialValue: customerBloc.customerRepository.customer!.email, autovalidateMode: AutovalidateMode.always, validator: (val) { String? validator = customerBloc.emailValidation(val!); @@ -147,8 +146,8 @@ class CustomerModifyPage extends StatelessWidget with Trans { borderSide: BorderSide(color: Colors.green[50]!, width: 0.4), ), ), - initialValue: customerBloc.customerRepository.customer.password, - autovalidateMode: AutovalidateMode.always, + initialValue: customerBloc.customerRepository.customer!.password, + autovalidateMode: AutovalidateMode.onUserInteraction, validator: (val) { String? validator = customerBloc.passwordValidation(val!); return validator == null ? null : t(validator); @@ -173,8 +172,8 @@ class CustomerModifyPage extends StatelessWidget with Trans { borderSide: BorderSide(color: Colors.green[50]!, width: 0.4), ), ), - initialValue: customerBloc.customerRepository.customer.name, - autovalidateMode: AutovalidateMode.always, + initialValue: customerBloc.customerRepository.customer!.name, + autovalidateMode: AutovalidateMode.onUserInteraction, validator: (val) { return customerBloc.nameValidation(val); }, @@ -198,8 +197,8 @@ class CustomerModifyPage extends StatelessWidget with Trans { borderSide: BorderSide(color: Colors.green[50]!, width: 0.4), ), ), - initialValue: customerBloc.customerRepository.customer.firstname, - autovalidateMode: AutovalidateMode.always, + initialValue: customerBloc.customerRepository.customer!.firstname, + autovalidateMode: AutovalidateMode.onUserInteraction, validator: (val) { return customerBloc.nameValidation(val); }, @@ -210,65 +209,92 @@ class CustomerModifyPage extends StatelessWidget with Trans { Divider( color: Colors.transparent, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 4, - child: Text(t("Birth Year"), style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18)), - ), - NumberPickerWidget( - minValue: 1930, - maxValue: 2100, - initalValue: customerBloc.year!.toInt(), - unit: " ", - color: Colors.indigo, - onChange: (value) => {customerBloc.add(CustomerBirthYearChange(year: value.toInt()))}), - SizedBox(width: 80), - ], + Container( + padding: EdgeInsets.only(left: 15), + decoration: BoxDecoration( + color: Colors.white24, + border: Border.all(color: Colors.black, width: 0.4), + borderRadius: BorderRadius.all(Radius.circular(12))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 2, + child: Text(t("Birth Year"), + style: GoogleFonts.inter( + fontWeight: FontWeight.normal, + fontSize: 18, + ))), + NumberPickerWidget( + minValue: 1930, + maxValue: DateTime.now().year, + initalValue: customerBloc.year!.toInt(), + unit: " ", + color: Colors.indigo, + onChange: (value) => {customerBloc.add(CustomerBirthYearChange(year: value.toInt()))}), + SizedBox(width: 30), + ], + )), + Divider( + color: Colors.transparent, ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - flex: 4, - child: Text(t("Weight"), style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18)), - ), - NumberPickerWidget( - minValue: 0, - maxValue: 200, - initalValue: customerBloc.weight.toInt(), - unit: " ", - color: Colors.indigo, - onChange: (value) => {customerBloc.add(CustomerWeightChange(weight: value.toInt()))}), - SizedBox(width: 80), - ], + Container( + padding: EdgeInsets.only(left: 15), + decoration: BoxDecoration( + color: Colors.white24, + border: Border.all(color: Colors.black, width: 0.4), + borderRadius: BorderRadius.all(Radius.circular(12))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + flex: 4, + child: Text(t("Weight"), style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18)), + ), + NumberPickerWidget( + minValue: 0, + maxValue: 200, + initalValue: customerBloc.weight.toInt(), + unit: " ", + color: Colors.indigo, + onChange: (value) => {customerBloc.add(CustomerWeightChange(weight: value.toInt()))}), + SizedBox(width: 30), + ], + )), + Divider( + color: Colors.transparent, ), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - flex: 4, - child: Text(t("Height"), style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18)), - ), - NumberPickerWidget( - minValue: 0, - maxValue: 230, - initalValue: customerBloc.height.toInt(), - unit: " ", - color: Colors.indigo[300]!, - onChange: (value) => {customerBloc.add(CustomerHeightChange(height: value.toInt()))}), - SizedBox(width: 80), - ], + Container( + padding: EdgeInsets.only(left: 15), + decoration: BoxDecoration( + color: Colors.white24, + border: Border.all(color: Colors.black, width: 0.4), + borderRadius: BorderRadius.all(Radius.circular(12))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + flex: 4, + child: Text(t("Height"), style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18)), + ), + NumberPickerWidget( + minValue: 0, + maxValue: 230, + initalValue: customerBloc.height.toInt(), + unit: " ", + color: Colors.indigo[300]!, + onChange: (value) => {customerBloc.add(CustomerHeightChange(height: value.toInt()))}), + SizedBox(width: 30), + ], + )), + Divider( + color: Colors.transparent, ), - Divider(), ToggleSwitch( minWidth: 80.0, minHeight: 50.0, fontSize: 14.0, - initialLabelIndex: customerBloc.customerRepository.customer.sex == "m" ? 0 : 1, + initialLabelIndex: customerBloc.customerRepository.customer!.sex == "m" ? 0 : 1, activeBgColor: Colors.indigo, activeFgColor: Colors.white, inactiveBgColor: Colors.white30, diff --git a/lib/view/evaluation.dart b/lib/view/evaluation_page.dart similarity index 70% rename from lib/view/evaluation.dart rename to lib/view/evaluation_page.dart index c799307..f498b36 100644 --- a/lib/view/evaluation.dart +++ b/lib/view/evaluation_page.dart @@ -1,5 +1,6 @@ import 'dart:collection'; import 'dart:ui'; +import 'package:aitrainer_app/util/enums.dart'; import 'package:intl/intl.dart'; import 'package:aitrainer_app/bloc/result/result_bloc.dart'; import 'package:aitrainer_app/util/app_language.dart'; @@ -281,6 +282,7 @@ class EvaluationPage extends StatelessWidget with Trans { } Widget getSuggestionWidget(ResultBloc resultBloc, String title, String picture, String repeats, double percent, String restTime) { + double _opacity = 1; String unitQuantityUnit = resultBloc.exerciseRepository.exerciseType!.unitQuantityUnit == null ? "" : resultBloc.exerciseRepository.exerciseType!.unitQuantityUnit!; @@ -325,7 +327,7 @@ class EvaluationPage extends StatelessWidget with Trans { softWrap: true, style: GoogleFonts.archivoBlack( fontSize: 18, - color: Colors.orange, + color: Colors.orange.withOpacity(_opacity), shadows: [ Shadow( offset: Offset(5.0, 5.0), @@ -398,7 +400,7 @@ class EvaluationPage extends StatelessWidget with Trans { Widget getSummary(ResultBloc bloc) { int index = 0; - List resultList = []; + List resultList = []; bloc.exerciseRepository.actualExerciseList!.forEach((actual) { //final String unit = t(bloc.exerciseRepository.exerciseType.unit); @@ -421,36 +423,248 @@ class EvaluationPage extends StatelessWidget with Trans { exerciseElement = t("3rd Control") + ": "; } index++; - resultList.add( - Text(exerciseElement + exerciseRepeats + exerciseUnitQuantity + " " + t(unit), - textAlign: TextAlign.center, - maxLines: 2, - softWrap: true, + resultList.add(RichText( + text: TextSpan( style: GoogleFonts.inter( - fontSize: 18, + fontSize: 20, color: Colors.white, - shadows: [ - Shadow( - offset: Offset(5.0, 5.0), - blurRadius: 12.0, - color: Colors.black54, + ), + children: [ + TextSpan(text: exerciseElement), + TextSpan( + text: exerciseRepeats + exerciseUnitQuantity + " " + t(unit), + style: GoogleFonts.archivoBlack( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.yellow[300]!, ), - Shadow( - offset: Offset(-3.0, 3.0), - blurRadius: 12.0, - color: Colors.black54, - ), - ], - )), - ); + ), + ]), + )); }); + + return Column(children: resultList); + } + + Widget getEvaluationWidget(ResultBloc bloc) { + List resultList = []; + print("Act ${bloc.exerciseRepository.actualExerciseList}"); + if (bloc.exerciseRepository.actualExerciseList == null || bloc.exerciseRepository.actualExerciseList!.isEmpty) { + return Offstage(); + } + final int exerciseTypeId = bloc.exerciseRepository.actualExerciseList![0].exerciseTypeId!; + final double quantity = bloc.exerciseRepository.actualExerciseList![0].quantity!; + String eval = bloc.evaluationRepository.getEvaluationTextByExerciseType(exerciseTypeId, quantity); + Color color = bloc.evaluationRepository.getEvaluationColor(eval); + double compareBest = bloc.exerciseRepository.getBestExercisePercent(bloc.exerciseRepository.actualExerciseList![0]); + double compareLast = bloc.exerciseRepository.getLastExercisePercent(bloc.exerciseRepository.actualExerciseList![0]); + bool has1RM = bloc.exerciseRepository.actualExerciseList![0].unitQuantity != null; + double? bestCompared1RM; + double? lastCompared1RM; + if (has1RM) { + lastCompared1RM = bloc.exerciseRepository.getLast1RMPercent(bloc.exerciseRepository.actualExerciseList![0]); + bestCompared1RM = bloc.exerciseRepository.getBest1RMPercent(bloc.exerciseRepository.actualExerciseList![0]); + } + + if (!EvaluationText.fair.equalsStringTo(eval)) { + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + children: [ + TextSpan(text: t("Your result is: ")), + TextSpan( + text: eval, + style: GoogleFonts.archivoBlack( + fontWeight: FontWeight.bold, + color: color, + ), + ), + ]), + )); + + resultList.add(Divider(color: Colors.transparent)); + } + + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 20, + color: Colors.white, + ), + children: [ + TextSpan(text: t("Compared with...")), + ]), + )); + + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 20, + color: Colors.white, + ), + children: [ + TextSpan(text: t("your best")), + TextSpan(text: " "), + TextSpan(text: has1RM ? t("volumen") : t("exercise")), + TextSpan(text: ": "), + TextSpan( + text: compareBest.toStringAsFixed(1) + "%", + style: GoogleFonts.archivoBlack( + fontWeight: FontWeight.bold, + color: compareBest >= 0 ? Colors.green : Colors.red[600], + ), + ), + ]), + )); + + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 20, + color: Colors.white, + ), + children: [ + TextSpan(text: t("your last")), + TextSpan(text: " "), + TextSpan(text: has1RM ? t("volumen") : t("exercise")), + TextSpan(text: ": "), + TextSpan( + text: compareLast.toStringAsFixed(1) + "%", + style: GoogleFonts.archivoBlack( + fontWeight: FontWeight.bold, + color: compareLast >= 0 ? Colors.green : Colors.red[600], + ), + ), + ]), + )); + resultList.add(Divider(color: Colors.transparent)); + + if (has1RM) { + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 20, + color: Colors.white, + ), + children: [ + TextSpan(text: t("best")), + TextSpan(text: " "), + TextSpan(text: t("1RM")), + TextSpan(text: ": "), + TextSpan( + text: bestCompared1RM!.toStringAsFixed(1) + "%", + style: GoogleFonts.archivoBlack( + fontWeight: FontWeight.bold, + color: bestCompared1RM >= 0 ? Colors.green : Colors.red[600], + ), + ), + ]), + )); + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 20, + color: Colors.white, + ), + children: [ + TextSpan(text: t("last")), + TextSpan(text: " "), + TextSpan(text: t("1RM")), + TextSpan(text: ": "), + TextSpan( + text: lastCompared1RM!.toStringAsFixed(1) + "%", + style: GoogleFonts.archivoBlack( + fontWeight: FontWeight.bold, + color: lastCompared1RM >= 0 ? Colors.green : Colors.red[600], + ), + ), + ]), + )); + } + + if (!Cache().hasPurchased) { + resultList.add(Divider()); + resultList.add(Divider()); + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.normal, + color: Colors.white, + ), + children: [ + TextSpan( + text: t('How can serve you this result?'), + style: GoogleFonts.inter( + color: Colors.white, + ), + ), + ]), + )); + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.normal, + color: Colors.white, + ), + children: [ + TextSpan( + text: t('Get the Fastlane to your'), + style: GoogleFonts.inter( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ]), + )); + resultList.add(RichText( + text: TextSpan( + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.normal, + color: Colors.white, + ), + children: [ + TextSpan( + text: t('Development'), + style: GoogleFonts.inter( + fontWeight: FontWeight.bold, + color: Colors.yellow[300], + ), + ) + ]))); + + resultList.add(TextButton( + onPressed: () => {Navigator.of(context).pushNamed("salesPage")}, + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset('asset/icon/gomb_orange_a.png', width: 140, height: 60), + Text( + t("Go"), + style: GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + )); + } + return Column(children: resultList); } Widget getResultSummary(ResultBloc resultBloc) { return SliverList( delegate: SliverChildListDelegate( - [Divider(color: Colors.transparent), getSummary(resultBloc)], + [Divider(color: Colors.transparent), getSummary(resultBloc), Divider(color: Colors.transparent), getEvaluationWidget(resultBloc)], ), ); } diff --git a/lib/view/exercise_execute_plan_add_page.dart b/lib/view/exercise_execute_plan_add_page.dart index fe2d46e..70bdfe3 100644 --- a/lib/view/exercise_execute_plan_add_page.dart +++ b/lib/view/exercise_execute_plan_add_page.dart @@ -45,7 +45,8 @@ class _ExerciseExecuteAddPage extends State with Tra exercisePlanRepository: planBloc.exercisePlanRepository, customerId: customerId, workoutTree: workoutTree, - planBloc: planBloc), + planBloc: planBloc) + ..add(ExerciseExecutePlanAddLoad()), child: BlocConsumer(listener: (context, state) { if (state is ExerciseExecutePlanAddError) { ScaffoldMessenger.of(context).showSnackBar( @@ -54,7 +55,7 @@ class _ExerciseExecuteAddPage extends State with Tra }, builder: (context, state) { // ignore: close_sinks final exerciseBloc = BlocProvider.of(context); - if (state is ExerciseExecutePlanAddReady) { + if (state is ExerciseExecutePlanAddReady && _controller.hasClients) { _controller.animateTo(exerciseBloc.scrollOffset, duration: Duration(milliseconds: 300), curve: Curves.easeIn); } return ModalProgressHUD( @@ -67,7 +68,10 @@ class _ExerciseExecuteAddPage extends State with Tra })); } - Form getControlForm(ExerciseExecutePlanAddBloc exerciseBloc) { + Widget getControlForm(ExerciseExecutePlanAddBloc exerciseBloc) { + if (exerciseBloc.exerciseRepository.exerciseType == null || exerciseBloc.quantity == null) { + return Offstage(); + } String exerciseName = AppLanguage().appLocal == Locale("en") ? exerciseBloc.exerciseRepository.exerciseType!.name : exerciseBloc.exerciseRepository.exerciseType!.nameTranslation; @@ -186,7 +190,7 @@ class _ExerciseExecuteAddPage extends State with Tra minValue: 0, maxValue: 1000, fontSize: 16, - initalValue: exerciseBloc.unitQuantity.toInt(), + initalValue: exerciseBloc.unitQuantity!.toInt(), unit: t(exerciseBloc.exerciseRepository.exerciseType!.unitQuantityUnit!), color: Colors.yellow[50]!, onChange: (value) => {exerciseBloc.add(ExerciseExecutePlanAddChangeUnitQuantity(quantity: value.toDouble()))}), @@ -194,7 +198,7 @@ class _ExerciseExecuteAddPage extends State with Tra minValue: 0, maxValue: 200, fontSize: 16, - initalValue: exerciseBloc.quantity.toInt(), + initalValue: exerciseBloc.quantity!.toInt(), unit: t(exerciseBloc.exerciseRepository.exerciseType!.unit), //t("repeat"), color: Colors.yellow[50]!, onChange: (value) => {exerciseBloc.add(ExerciseExecutePlanAddChangeQuantity(quantity: value.toDouble()))}), diff --git a/lib/view/exercise_log_page.dart b/lib/view/exercise_log_page.dart index 288fede..dae5e7b 100644 --- a/lib/view/exercise_log_page.dart +++ b/lib/view/exercise_log_page.dart @@ -1,6 +1,5 @@ import 'dart:collection'; import 'package:aitrainer_app/bloc/exercise_log/exercise_log_bloc.dart'; -import 'package:aitrainer_app/library/custom_icon_icons.dart'; import 'package:aitrainer_app/widgets/app_bar.dart'; import 'package:aitrainer_app/widgets/bottom_nav.dart'; import 'package:aitrainer_app/widgets/dialog_premium.dart'; @@ -196,13 +195,12 @@ class _ExerciseLogPage extends State with Trans, Common { exercise.summary == null ? "" : exercise.summary!, style: TextStyle(fontSize: 12, color: Colors.blue[800]), )), - IconButton( - iconSize: 36, - icon: Icon(CustomIcon.heart_1, color: Colors.orange[800]), - onPressed: () { - evaluation(exerciseRepository, exercise); - }, - ), + GestureDetector( + onTap: () => evaluation(exerciseRepository, exercise), + child: Image.asset( + "asset/image/kupa.png", + width: 35, + )), IconButton( icon: Icon(Icons.delete, color: Colors.black12), onPressed: () { diff --git a/lib/view/exercise_new_page.dart b/lib/view/exercise_new_page.dart index e142322..cafb2d5 100644 --- a/lib/view/exercise_new_page.dart +++ b/lib/view/exercise_new_page.dart @@ -107,7 +107,9 @@ class _ExerciseNewPageState extends State with Trans, Logging { child: ExerciseSave( exerciseName: exerciseBloc.exerciseRepository.exerciseType!.nameTranslation, exerciseDescription: exerciseBloc.exerciseRepository.exerciseType!.descriptionTranslation, - exerciseTask: t("Please take a relative bigger weight and repeat 12-20 times"), + exerciseTask: exerciseBloc.exerciseRepository.exerciseType!.unitQuantityUnit != null + ? t("Please take a relative bigger weight and repeat 12-20 times and do your best! MAXIMIZE it!") + : t("Please repeat as much times as you can! MAXIMIZE it!"), unit: exerciseBloc.exerciseRepository.exerciseType!.unit, unitQuantityUnit: exerciseBloc.exerciseRepository.exerciseType!.unitQuantityUnit, hasUnitQuantity: exerciseBloc.exerciseRepository.exerciseType!.unitQuantityUnit != null, @@ -121,10 +123,13 @@ class _ExerciseNewPageState extends State with Trans, Logging { floatingActionButton: FloatingActionButton.extended( onPressed: () => save(exerciseBloc, menuBloc), backgroundColor: Colors.orange[800], - icon: Icon(CustomIcon.save), + icon: Icon( + CustomIcon.save, + size: 20, + ), label: Text( t("Save"), - style: GoogleFonts.inter(fontWeight: FontWeight.bold, fontSize: 16), + style: GoogleFonts.inter(fontWeight: FontWeight.bold, fontSize: 12), ), ), bottomNavigationBar: BottomBarMultipleExercises( @@ -164,11 +169,15 @@ class _ExerciseNewPageState extends State with Trans, Logging { // ignore: close_sinks final TestSetExecuteBloc executeBloc = BlocProvider.of(context); + final question = bloc.exerciseRepository.exercise!.quantity! == 12.0 + ? "Did you try the MAXIMUM what you can do? Are you sure we save the exercise with ONLY 12 repeats?" + : "Do you save this exercise with these parameters?"; + showCupertinoDialog( useRootNavigator: true, context: context, builder: (_) => CupertinoAlertDialog( - title: Text(t("Do you save this exercise with these parameters?")), + title: Text(t(question)), content: Column(children: [ Divider(), Text( diff --git a/lib/view/login.dart b/lib/view/login.dart index 50dd671..608e509 100644 --- a/lib/view/login.dart +++ b/lib/view/login.dart @@ -120,7 +120,7 @@ class LoginPage extends StatelessWidget with Trans { initialValue: loginBloc.userRepository.user.email, autovalidateMode: AutovalidateMode.onUserInteraction, validator: (val) { - String? validationText = loginBloc.emailValidation(val!); + String? validationText = loginBloc.emailValidation(val); return validationText == null ? null : t(validationText); }, onChanged: (value) => loginBloc.add(LoginEmailChange(email: value)), @@ -150,9 +150,9 @@ class LoginPage extends StatelessWidget with Trans { ), ), initialValue: loginBloc.userRepository.user.password, - autovalidateMode: AutovalidateMode.always, + autovalidateMode: AutovalidateMode.onUserInteraction, validator: (val) { - String? validationText = loginBloc.passwordValidation(val!); + String? validationText = loginBloc.passwordValidation(val); return validationText == null ? null : t(validationText); }, onChanged: (value) => loginBloc.add(LoginPasswordChange(password: value)), diff --git a/lib/view/registration.dart b/lib/view/registration.dart index 60bf2f3..24590f9 100644 --- a/lib/view/registration.dart +++ b/lib/view/registration.dart @@ -163,7 +163,7 @@ class RegistrationPage extends StatelessWidget with Trans { ), ), initialValue: loginBloc.userRepository.user.password, - autovalidateMode: AutovalidateMode.always, + autovalidateMode: AutovalidateMode.onUserInteraction, validator: (val) { final String? validator = loginBloc.passwordValidation(val!); return validator == null ? null : t(validator); diff --git a/lib/view/reset_password.dart b/lib/view/reset_password.dart index 440ce9b..114565b 100644 --- a/lib/view/reset_password.dart +++ b/lib/view/reset_password.dart @@ -31,7 +31,7 @@ class ResetPasswordPage extends StatelessWidget with Trans { if (state is PasswordResetError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(backgroundColor: Colors.orange, content: Text(t(state.message), style: TextStyle(color: Colors.white)))); - } else if (state is PasswordResetReady) { + } else if (state is PasswordResetFinished) { Navigator.of(context).pop(); } }, diff --git a/lib/view/sales_page.dart b/lib/view/sales_page.dart index 5814c1c..65c4680 100644 --- a/lib/view/sales_page.dart +++ b/lib/view/sales_page.dart @@ -150,7 +150,7 @@ class SalesPage extends StatelessWidget with Trans, Logging { bloc.product2Display.forEach((element) { final String title = element.sort == 3 ? t("Montly") : t("Annual"); final String desc4 = element.sort == 1 ? "" : t("Predictions with Artificial Intelligence"); - late String badge; + String? badge; if (element.sort == 2) { badge = t("14% discount"); } else if (element.sort == 1) { diff --git a/lib/view/test_set_execute.dart b/lib/view/test_set_execute.dart index b900866..a137812 100644 --- a/lib/view/test_set_execute.dart +++ b/lib/view/test_set_execute.dart @@ -3,12 +3,16 @@ 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/library/fade_in.dart'; +import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/model/exercise_plan_detail.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/dialog_common.dart'; import 'package:aitrainer_app/widgets/menu_image.dart'; import 'package:aitrainer_app/widgets/victory_widget.dart'; +import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -64,7 +68,9 @@ class TestSetExecute extends StatelessWidget with Trans { }), ), floatingActionButton: FloatingActionButton.extended( - onPressed: () => executeExercise(executeBloc, executeBloc.getNext()!, context), + onPressed: () => executeBloc.getNext() != null + ? executeExercise(executeBloc, executeBloc.getNext()!, context) + : Navigator.of(context).pushNamed('home'), backgroundColor: Colors.orange[800], icon: Icon(CustomIcon.weight_hanging), label: Text( @@ -86,11 +92,6 @@ class TestSetExecute extends StatelessWidget with Trans { tiles.add(getStartTile(bloc)); tiles.addAll(getExerciseTiles(bloc, context)); tiles.add(getEndTile()); - /* if (bloc.isDone100Percent()) { - tiles.add(Victory( - victory: true, - )); - } */ return tiles; } @@ -254,7 +255,9 @@ class TestSetExecute extends StatelessWidget with Trans { //if (element != null && element.exerciseTypeId != null) { tiles.add(GestureDetector( onDoubleTap: () => print("Execute ${element.exerciseType!.nameTranslation}"), - onTap: () => executeExercise(bloc, element, context), + onTap: () => element.state.equalsTo(ExercisePlanDetailState.finished) + ? evaluation(bloc, element) + : executeExercise(bloc, element, context), child: ExerciseTile( bloc: bloc, exercisePlanDetail: element, @@ -266,12 +269,27 @@ class TestSetExecute extends StatelessWidget with Trans { return tiles; } + void evaluation(TestSetExecuteBloc bloc, ExercisePlanDetail planDetail) { + if (planDetail.exercises != null && planDetail.exercises!.isNotEmpty) { + ExerciseRepository exerciseRepository = ExerciseRepository(); + exerciseRepository.start = planDetail.exercises![0].dateAdd; + exerciseRepository.exerciseList = Cache().getExercises(); + exerciseRepository.actualExerciseList = planDetail.exercises!; + print("actualEx ${exerciseRepository.actualExerciseList}"); + LinkedHashMap args = LinkedHashMap(); + args['exerciseRepository'] = exerciseRepository; + args['exercise'] = planDetail.exercises![0]; + args['past'] = true; + Navigator.of(context).pushNamed('evaluationPage', arguments: args); + } + } + void executeExercise(TestSetExecuteBloc bloc, ExercisePlanDetail exercisePlanDetail, BuildContext context) { ExercisePlanDetail? next = bloc.getNext(); if (next != null) { final HashMap args = HashMap(); - args['exerciseType'] = exercisePlanDetail.exerciseType; + args['exerciseType'] = next.exerciseType; args['exercisePlanDetailId'] = exercisePlanDetail.exercisePlanDetailId; args['testSetExecuteBloc'] = bloc; String title = ""; @@ -314,17 +332,43 @@ class TestSetExecute extends StatelessWidget with Trans { } // ignore: must_be_immutable -class ExerciseTile extends StatelessWidget with Trans { +class ExerciseTile extends StatefulWidget { final TestSetExecuteBloc bloc; final ExercisePlanDetail exercisePlanDetail; ExerciseTile({required this.bloc, required this.exercisePlanDetail}); + @override + _ExerciseTileState createState() => _ExerciseTileState(); +} + +class _ExerciseTileState extends State with Trans { + final EzAnimation animation = EzAnimation(1.0, 30.0, Duration(seconds: 3), reverseCurve: Curves.easeIn); + + @override + void initState() { + animation.start(); + animation.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() {}); + } + }); + + super.initState(); + } + + @override + bool didUpdateWidget(ExerciseTile oldWidget) { + super.didUpdateWidget(oldWidget); + animation.start(); + return true; + } + Widget getIndicator(ExercisePlanDetailState state) { - ExercisePlanDetail? next = bloc.getNext(); + ExercisePlanDetail? next = widget.bloc.getNext(); bool actual = false; if (next != null) { - if (next.exerciseTypeId == exercisePlanDetail.exerciseTypeId) { + if (next.exerciseTypeId == widget.exercisePlanDetail.exerciseTypeId) { actual = true; } } @@ -357,10 +401,11 @@ class ExerciseTile extends StatelessWidget with Trans { @override Widget build(BuildContext context) { - final ExercisePlanDetailState state = exercisePlanDetail.state; + final ExercisePlanDetailState state = widget.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"; + final String countSerie = widget.exercisePlanDetail.exercises == null ? "1" : (widget.exercisePlanDetail.exercises!.length).toString(); + final String serie = widget.exercisePlanDetail.exerciseType!.unitQuantityUnit == null ? "/1" : "/4"; + setContext(context); return Container( color: Colors.transparent, @@ -387,8 +432,8 @@ class ExerciseTile extends StatelessWidget with Trans { width: 120, height: 80, child: MenuImage( - imageName: bloc.getActualImageName(exercisePlanDetail.exerciseType!.exerciseTypeId), - workoutTreeId: bloc.getActualWorkoutTreeId(exercisePlanDetail.exerciseType!.exerciseTypeId)!, + imageName: widget.bloc.getActualImageName(widget.exercisePlanDetail.exerciseType!.exerciseTypeId), + workoutTreeId: widget.bloc.getActualWorkoutTreeId(widget.exercisePlanDetail.exerciseType!.exerciseTypeId)!, )), SizedBox( width: 10, @@ -403,7 +448,7 @@ class ExerciseTile extends StatelessWidget with Trans { ), children: [ TextSpan( - text: exercisePlanDetail.exerciseType!.nameTranslation, + text: widget.exercisePlanDetail.exerciseType!.nameTranslation, style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.bold, @@ -421,20 +466,20 @@ class ExerciseTile extends StatelessWidget with Trans { ), ], )), - exercisePlanDetail.exerciseType!.unitQuantityUnit != null + widget.exercisePlanDetail.exerciseType!.unitQuantityUnit != null ? TextSpan( text: "\n", ) : TextSpan(), - exercisePlanDetail.exerciseType!.unitQuantityUnit != null + widget.exercisePlanDetail.exerciseType!.unitQuantityUnit != null ? TextSpan( - text: t(exercisePlanDetail.exerciseType!.unitQuantityUnit!) + ": ", + text: t(widget.exercisePlanDetail.exerciseType!.unitQuantityUnit!) + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)) : TextSpan(), - exercisePlanDetail.exerciseType!.unitQuantityUnit != null + widget.exercisePlanDetail.exerciseType!.unitQuantityUnit != null ? TextSpan( - text: t(bloc.getExerciseWeight(exercisePlanDetail)), + text: t(widget.bloc.getExerciseWeight(widget.exercisePlanDetail)), style: GoogleFonts.inter( fontSize: 12, )) @@ -443,11 +488,11 @@ class ExerciseTile extends StatelessWidget with Trans { text: "\n", ), TextSpan( - text: t(exercisePlanDetail.exerciseType!.unit) + ": ", + text: t(widget.exercisePlanDetail.exerciseType!.unit) + ": ", style: GoogleFonts.inter( fontSize: 12, color: done ? Colors.grey[100] : Colors.yellow[400], fontWeight: FontWeight.bold)), TextSpan( - text: bloc.repeatTimesText(exercisePlanDetail), + text: widget.bloc.repeatTimesText(widget.exercisePlanDetail), style: GoogleFonts.inter( fontSize: 12, )), @@ -465,6 +510,19 @@ class ExerciseTile extends StatelessWidget with Trans { )), ]), )), + done + ? AnimatedBuilder( + animation: animation, + builder: (context, snapshot) { + return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Image.asset( + "asset/image/kupa.png", + width: animation.value, + ), + Text("Result", style: GoogleFonts.inter(fontSize: 10, color: Colors.white)), + ]); + }) + : Offstage() ]), ), ), diff --git a/lib/view/test_set_new.dart b/lib/view/test_set_new.dart index 5c47114..01a5ea1 100644 --- a/lib/view/test_set_new.dart +++ b/lib/view/test_set_new.dart @@ -92,9 +92,11 @@ class TestSetNew extends StatelessWidget with Trans { return ExerciseSave( exerciseName: bloc.exerciseType.nameTranslation, exerciseDescription: bloc.exerciseType.descriptionTranslation, - exerciseTask: t("Please take a relative bigger weight and repeat 12-20 times"), + exerciseTask: bloc.exerciseType.unitQuantityUnit != null + ? t("Please take a relative bigger weight and repeat 12-20 times and do your best! MAXIMIZE it!") + : t("Please repeat as much times as you can! MAXIMIZE it!"), unit: bloc.exerciseType.unit, - unitQuantityUnit: bloc.exerciseType.unitQuantityUnit!, + unitQuantityUnit: bloc.exerciseType.unitQuantityUnit, hasUnitQuantity: bloc.exerciseType.unitQuantityUnit != null, onQuantityChanged: (value) { bloc.add(TestSetNewChangeQuantity(quantity: double.parse(value))); diff --git a/lib/widgets/bmi_widget.dart b/lib/widgets/bmi_widget.dart index 533e321..7d5c68f 100644 --- a/lib/widgets/bmi_widget.dart +++ b/lib/widgets/bmi_widget.dart @@ -241,7 +241,7 @@ class _BMIState extends State with Trans { } Widget getHeightInput() { - if (widget.exerciseBloc.customerRepository.customer.birthYear! < 2003) { + if (widget.exerciseBloc.customerRepository.customer!.birthYear! < 2003) { return Flexible( child: TextFormField( focusNode: _nodeText2, diff --git a/lib/widgets/bmr_widget.dart b/lib/widgets/bmr_widget.dart index b5bdc16..be6ca0b 100644 --- a/lib/widgets/bmr_widget.dart +++ b/lib/widgets/bmr_widget.dart @@ -3,7 +3,7 @@ import 'package:aitrainer_app/bloc/exercise_new/exercise_new_bloc.dart'; import 'package:aitrainer_app/util/app_localization.dart'; import 'package:aitrainer_app/model/fitness_state.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/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -196,7 +196,7 @@ class _BMRState extends State with Trans { } Widget getHeightInput() { - if (widget.exerciseBloc.customerRepository.customer.birthYear! < 2003) { + if (widget.exerciseBloc.customerRepository.customer!.birthYear! < 2003) { return Flexible( child: TextFormField( focusNode: _nodeText2, diff --git a/lib/widgets/bottom_bar_multiple_exercises.dart b/lib/widgets/bottom_bar_multiple_exercises.dart index 4b4188e..7195845 100644 --- a/lib/widgets/bottom_bar_multiple_exercises.dart +++ b/lib/widgets/bottom_bar_multiple_exercises.dart @@ -71,7 +71,7 @@ class _BottomBarMultipleExercisesState extends State ], ), ), - height: 90, + height: 70, child: BlocConsumer(listener: (context, state) { if (state is TestSetExecuteError) { ScaffoldMessenger.of(context) diff --git a/lib/widgets/bottom_nav.dart b/lib/widgets/bottom_nav.dart index 2618466..a639439 100644 --- a/lib/widgets/bottom_nav.dart +++ b/lib/widgets/bottom_nav.dart @@ -1,10 +1,12 @@ -import 'package:aitrainer_app/library/gradient_bottom_navigation_bar.dart'; +import 'dart:ui'; + import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/service/logging.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:convex_bottom_bar/convex_bottom_bar.dart'; import 'package:flutter/material.dart'; // ignore: must_be_immutable @@ -34,91 +36,93 @@ class _NawDrawerWidget extends State with Trans, Logging { Widget build(BuildContext context) { final Color bgrColor = Color(0xffb4f500); final Color bgrColorEnd = Colors.blue; - final Color active = Colors.black; - final Color inactive = Colors.black26; + final Color active = Colors.white; + final Color inactive = Colors.black38; setContext(context); - return GradientBottomNavigationBar( - currentIndex: widget.bottomNavIndex, // this will be set when a new tab is tapped - backgroundColorStart: bgrColorEnd, - backgroundColorEnd: bgrColor, - fixedColor: active, - items: [ - BottomNavigationBarItem( - backgroundColor: bgrColor, - icon: Common.badgedIcon(inactive, Icons.home, "home"), - activeIcon: Common.badgedIcon(active, Icons.home, "home"), - title: Text(t("Home"), style: TextStyle(fontSize: 12))), - BottomNavigationBarItem( - backgroundColor: bgrColor, - icon: Common.badgedIcon(inactive, Icons.trending_up, "development"), - activeIcon: Common.badgedIcon(active, Icons.trending_up, "development"), - title: Text( - t("My Development"), - style: TextStyle(fontSize: 12), - ), - ), - BottomNavigationBarItem( - backgroundColor: bgrColor, - icon: Icon(Icons.featured_play_list, color: inactive), - activeIcon: Icon( - Icons.featured_play_list, - color: active, - ), - title: Text( - t("My Training Plan"), - style: TextStyle(fontSize: 12), - ), - ), - BottomNavigationBarItem( - backgroundColor: bgrColor, - icon: Common.badgedIcon(inactive, Icons.person, "account"), - activeIcon: Common.badgedIcon(active, Icons.person, "account"), - title: Text( - t("Account"), - style: TextStyle(fontSize: 12), - )), - BottomNavigationBarItem( - backgroundColor: bgrColor, - icon: Icon(Icons.settings, color: inactive), - activeIcon: Icon(Icons.settings, color: active), - title: Text(t("Settings"), style: TextStyle(fontSize: 12))), - ], - onTap: (index) { - setState(() { - widget.bottomNavIndex = index; - switch (index) { - case 0: - Navigator.of(context).pop(); - Track().track(TrackingEvent.home); - Navigator.of(context).pushNamed('home'); + return StyleProvider( + style: Style(), + child: ConvexAppBar( + initialActiveIndex: widget.bottomNavIndex, + curve: Curves.easeIn, + style: TabStyle.react, + color: inactive, + height: 55, + gradient: LinearGradient(colors: [bgrColorEnd, bgrColor], stops: [0.1, .75]), + items: [ + TabItem( + isIconBlend: false, + title: t("Home"), + icon: Common.badgedIcon(inactive, Icons.home, "home"), + activeIcon: Common.badgedIcon(active, Icons.home, "home"), + ), + TabItem( + title: t("Growth"), + icon: Common.badgedIcon(inactive, Icons.trending_up, "development"), + activeIcon: Common.badgedIcon(active, Icons.trending_up, "development"), + ), + TabItem( + title: t("Training"), + icon: Icon(Icons.featured_play_list, color: inactive), + activeIcon: Icon(Icons.featured_play_list, color: active)), + TabItem( + title: t("Account"), + icon: Common.badgedIcon(inactive, Icons.person, "account"), + activeIcon: Common.badgedIcon(active, Icons.person, "account"), + ), + TabItem(title: t("Settings"), icon: Icon(Icons.settings, color: inactive), activeIcon: Icon(Icons.settings, color: active)), + ], + onTap: (index) { + setState(() { + widget.bottomNavIndex = index; + switch (index) { + case 0: + Navigator.of(context).pop(); + Track().track(TrackingEvent.home); + Navigator.of(context).pushNamed('home'); - break; - case 1: - Navigator.of(context).pop(); - Track().track(TrackingEvent.my_development); - Navigator.of(context).pushNamed('myDevelopment'); - break; - case 2: - Navigator.of(context).pop(); - Track().track(TrackingEvent.my_exerciseplan); - Navigator.of(context).pushNamed('myExercisePlan'); + break; + case 1: + Navigator.of(context).pop(); + Track().track(TrackingEvent.my_development); + Navigator.of(context).pushNamed('myDevelopment'); + break; + case 2: + Navigator.of(context).pop(); + Track().track(TrackingEvent.my_exerciseplan); + Navigator.of(context).pushNamed('myExercisePlan'); - break; - case 3: - Navigator.of(context).pop(); - Track().track(TrackingEvent.account); - Navigator.of(context).pushNamed('account'); + break; + case 3: + Navigator.of(context).pop(); + Track().track(TrackingEvent.account); + Navigator.of(context).pushNamed('account'); - break; - case 4: - Navigator.of(context).pop(); - Track().track(TrackingEvent.settings); - Navigator.of(context).pushNamed('settings'); + break; + case 4: + Navigator.of(context).pop(); + Track().track(TrackingEvent.settings); + Navigator.of(context).pushNamed('settings'); - break; - } - }); - }); + break; + } + }); + })); + } +} + +class Style extends StyleHook { + @override + double get activeIconSize => 50; + + @override + double get activeIconMargin => 10; + + @override + double get iconSize => 20; + + @override + TextStyle textStyle(Color color) { + return TextStyle(fontSize: 12, color: color); } } diff --git a/lib/widgets/dialog_common.dart b/lib/widgets/dialog_common.dart index 11aee68..4d3423f 100644 --- a/lib/widgets/dialog_common.dart +++ b/lib/widgets/dialog_common.dart @@ -42,7 +42,7 @@ class _DialogPremiumState extends State with Trans { ), elevation: 0, backgroundColor: Colors.transparent, - child: contentBox(context), + child: SingleChildScrollView(padding: EdgeInsets.only(bottom: 30), child: contentBox(context)), ); } diff --git a/lib/widgets/exercise_save.dart b/lib/widgets/exercise_save.dart index 1a357fb..707a371 100644 --- a/lib/widgets/exercise_save.dart +++ b/lib/widgets/exercise_save.dart @@ -129,109 +129,105 @@ class _ExerciseSaveState extends State with Trans { return KeyboardActions( config: _buildConfig(context), child: Container( - padding: const EdgeInsets.only(top: 10, left: 55, right: 55), child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - widget.exerciseName, - style: GoogleFonts.archivoBlack( - fontWeight: FontWeight.bold, - fontSize: 24, - color: Colors.white, - shadows: [ - Shadow( - offset: Offset(5.0, 5.0), - blurRadius: 12.0, - color: Colors.black54, - ), - Shadow( - offset: Offset(-3.0, 3.0), - blurRadius: 12.0, - color: Colors.black54, - ), - ], - ), - overflow: TextOverflow.fade, - maxLines: 4, - softWrap: true, - textAlign: TextAlign.center, + scrollDirection: Axis.vertical, + child: Column(mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ + Text( + widget.exerciseName, + style: GoogleFonts.archivoBlack( + fontWeight: FontWeight.bold, + fontSize: 24, + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(5.0, 5.0), + blurRadius: 12.0, + color: Colors.black54, + ), + Shadow( + offset: Offset(-3.0, 3.0), + blurRadius: 12.0, + color: Colors.black54, + ), + ], + ), + overflow: TextOverflow.fade, + maxLines: 4, + softWrap: true, + textAlign: TextAlign.center, + ), + SizedBox( + height: 15, + ), + Padding( + padding: const EdgeInsets.only(top: 10, left: 55, right: 55), + child: Text( + widget.exerciseDescription, + style: GoogleFonts.inter(fontSize: 12, color: Colors.yellow[300]), + maxLines: 1, + overflow: TextOverflow.fade, + softWrap: true, + )), + InkWell( + child: Text( + t("More »"), + style: GoogleFonts.inter(fontSize: 12, color: Colors.blue[200]), + ), + onTap: () => { + showDialog( + context: context, + builder: (BuildContext context) { + return DialogHTML(title: widget.exerciseName, htmlData: '

' + widget.exerciseDescription + '

'); + }) + }, + ), + Divider( + color: Colors.transparent, + ), + 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, + ), + 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, ), - SizedBox( - height: 15, - ), - Text( - widget.exerciseDescription, - style: GoogleFonts.inter(fontSize: 12, color: Colors.yellow[300]), - maxLines: 1, - overflow: TextOverflow.fade, - softWrap: true, - ), - InkWell( - child: Text( - t("More »"), - style: GoogleFonts.inter(fontSize: 12, color: Colors.blue[200]), - ), - onTap: () => { - showDialog( - context: context, - builder: (BuildContext context) { - return DialogHTML(title: widget.exerciseName, htmlData: '

' + widget.exerciseDescription + '

'); - }) - }, - ), - Divider( - color: Colors.transparent, - ), - widget.hasUnitQuantity - ? Text( - t(widget.exerciseTask), - style: GoogleFonts.inter( - fontSize: 14, - color: Colors.orange, - fontWeight: FontWeight.bold, - ), - maxLines: 3, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: true, - ) - : Offstage(), - Divider( - color: Colors.transparent, - ), - columnQuantityUnit(), - Divider( - color: Colors.transparent, - ), - columnQuantity(), - Divider( - color: Colors.transparent, - ), - widget.hasUnitQuantity - ? Text( - t("Step") + ": " + "1/4", - style: GoogleFonts.inter( - fontSize: 22, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - maxLines: 3, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: true, - ) - : Offstage(), - Divider( - color: Colors.transparent, - ), - Divider( - color: Colors.transparent, - ), - /* TextButton( + maxLines: 3, + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: true, + ) + : Offstage(), + Divider( + color: Colors.transparent, + ), + Divider( + color: Colors.transparent, + ), + /* TextButton( onPressed: () { widget.onSubmit(); /* showDialog( @@ -252,43 +248,46 @@ class _ExerciseSaveState extends State with Trans { ), ], )), */ - ]), - ))); + ]), + ))); } - Column columnQuantityUnit() { - Column row = Column(); + Widget columnQuantityUnit() { + Widget row = Padding(padding: const EdgeInsets.only(top: 10, left: 55, right: 55), child: 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)), - ]); + row = Padding( + padding: const EdgeInsets.only(top: 10, left: 55, right: 55), + child: 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() { + Widget columnQuantity() { if (widget.unit == "second") { return Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Padding( - padding: const EdgeInsets.only(bottom: 0), + padding: const EdgeInsets.only(top: 10, left: 55, right: 55), + //padding: const EdgeInsets.only(bottom: 0), child: StreamBuilder( stream: stopWatchTimer.rawTime, initialData: stopWatchTimer.rawTime.valueWrapper?.value, @@ -300,13 +299,14 @@ class _ExerciseSaveState extends State with Trans { padding: const EdgeInsets.all(8), child: Text( displayTime, - style: const TextStyle(fontSize: 40, fontFamily: 'Helvetica', fontWeight: FontWeight.bold, color: Colors.white), + style: const TextStyle(fontSize: 35, fontFamily: 'Helvetica', fontWeight: FontWeight.bold, color: Colors.white), ), ), ]); })), Padding( - padding: const EdgeInsets.all(2), + //padding: const EdgeInsets.all(2), + padding: const EdgeInsets.only(top: 10, left: 25, right: 25), child: Column( children: [ Padding( @@ -315,7 +315,7 @@ class _ExerciseSaveState extends State with Trans { mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.symmetric(horizontal: 5), child: IconButton( padding: const EdgeInsets.all(2), color: Colors.white70, @@ -328,7 +328,7 @@ class _ExerciseSaveState extends State with Trans { ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.symmetric(horizontal: 5), child: IconButton( padding: const EdgeInsets.all(2), iconSize: 40, @@ -341,7 +341,7 @@ class _ExerciseSaveState extends State with Trans { ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.symmetric(horizontal: 5), child: IconButton( padding: const EdgeInsets.all(2), iconSize: 40, @@ -364,30 +364,32 @@ class _ExerciseSaveState extends State with Trans { 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), + Widget row = Container( + padding: const EdgeInsets.only(top: 10, left: 55, right: 55), + child: 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); + }, ), - ), - keyboardType: TextInputType.number, - textInputAction: TextInputAction.next, - style: GoogleFonts.archivoBlack(fontSize: 80, color: Colors.orange[200]), - onChanged: (value) { - widget.onQuantityChanged(value); - }, - ), - ]); + ])); return row; } diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index d0d47ca..22dce82 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -36,7 +36,7 @@ class _HomePageState extends State with Logging { } Future runDelayedEvent() async { - await Future.delayed(Duration(seconds: 2), () async { + await Future.delayed(Duration(milliseconds: 500), () async { // ignore: close_sinks SessionBloc sessionBloc = BlocProvider.of(context); if (sessionBloc.state != SessionReady()) { diff --git a/lib/widgets/menu_page_widget.dart b/lib/widgets/menu_page_widget.dart index df23b78..8829ffd 100644 --- a/lib/widgets/menu_page_widget.dart +++ b/lib/widgets/menu_page_widget.dart @@ -68,7 +68,7 @@ class _MenuPageWidgetState extends State with Trans, Logging { @override bool didUpdateWidget(MenuPageWidget oldWidget) { super.didUpdateWidget(oldWidget); - scrollController.animateTo(40, duration: Duration(milliseconds: 300), curve: Curves.easeIn); + scrollController.animateTo(5, duration: Duration(milliseconds: 300), curve: Curves.easeIn); return true; } @@ -234,17 +234,18 @@ class _MenuPageWidgetState extends State with Trans, Logging { SliverGrid getFilterElements(MenuBloc menuBloc) { List list = []; + int index = 0; menuBloc.exerciseDeviceRepository.getGymDevices().forEach((element) { String deviceName = AppLanguage().appLocal == Locale('en') ? element.name : element.nameTranslation; ChoiceChip chip = ChoiceChip( - labelPadding: EdgeInsets.only(right: 5), + labelPadding: EdgeInsets.only(right: 3), avatar: Icon( Icons.remove_circle_outline, color: Colors.orange, - size: 18, + size: 10, ), label: Text(deviceName), - labelStyle: TextStyle(fontSize: 9, color: Colors.black), + labelStyle: TextStyle(fontSize: 9, color: Colors.indigo), selectedColor: Colors.white, selected: menuBloc.selectedDevice(element.exerciseDeviceId), backgroundColor: Colors.blue[100], @@ -252,6 +253,11 @@ class _MenuPageWidgetState extends State with Trans, Logging { onSelected: (value) => menuBloc.add(MenuFilterExerciseType(deviceId: element.exerciseDeviceId)), ); list.add(chip); + if (index == 4) { + list.add(Divider()); + } + + index++; }); SliverGrid sliverList = SliverGrid( diff --git a/lib/widgets/menu_search_bar.dart b/lib/widgets/menu_search_bar.dart index 5e7efb2..fe98635 100644 --- a/lib/widgets/menu_search_bar.dart +++ b/lib/widgets/menu_search_bar.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:aitrainer_app/model/workout_menu_tree.dart'; import 'package:aitrainer_app/util/trans.dart'; -import 'package:dropdown_search/dropdown_search.dart'; +import 'package:aitrainer_app/library/dropdown_search.dart'; //import 'package:aitrainer_app/library/dropdown_search.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; diff --git a/lib/widgets/number_picker.dart b/lib/widgets/number_picker.dart index 7487775..12e1840 100644 --- a/lib/widgets/number_picker.dart +++ b/lib/widgets/number_picker.dart @@ -35,19 +35,23 @@ class _NumberPickerWidgetState extends State with Trans { void initState() { super.initState(); _scrollController = FixedExtentScrollController(initialItem: widget.initalValue); + _scrollController.animateToItem(widget.initalValue, duration: Duration(milliseconds: 100), curve: Curves.easeIn); } @override void didUpdateWidget(NumberPickerWidget oldWidget) { super.didUpdateWidget(oldWidget); - _scrollController.animateToItem(widget.initalValue, duration: Duration(milliseconds: 100), curve: Curves.easeIn); } Widget durationPicker({bool inSeconds = false, bool inHundredths = false}) { double value = 0; return CupertinoPicker( scrollController: _scrollController, + useMagnifier: true, + magnification: 1.2, + diameterRatio: 0.85, backgroundColor: Colors.transparent, + selectionOverlay: Container(), onSelectedItemChanged: (x) { currentData = x.toDouble(); value = x.toDouble(); @@ -57,7 +61,7 @@ class _NumberPickerWidgetState extends State with Trans { }, children: List.generate( widget.maxValue, (index) => Text('$index ' + widget.unit, style: TextStyle(color: widget.color, fontSize: widget.fontSize))), - itemExtent: 40, + itemExtent: 30, ); } @@ -66,32 +70,30 @@ class _NumberPickerWidgetState extends State with Trans { Widget build(BuildContext context) { setContext(context); return Container( - //color: Colors.white24, + color: Colors.transparent, width: MediaQuery.of(context).size.width * .40, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 5.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - color: Colors.transparent, - width: MediaQuery.of(context).size.width * .35, - child: Center( - child: Container( - color: Colors.transparent, - width: MediaQuery.of(context).size.width * 0.95, - height: MediaQuery.of(context).size.height * 0.25, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded(child: durationPicker()), - ], - )), - ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Colors.transparent, + width: MediaQuery.of(context).size.width * .35, + child: Center( + child: Container( + color: Colors.transparent, + width: MediaQuery.of(context).size.width * 0.95, + //height: MediaQuery.of(context).size.height * 0.25, + height: 100, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded(child: durationPicker()), + ], + )), ), - ], - ), + ), + ], ), ); } diff --git a/pubspec.lock b/pubspec.lock index d0704ae..63995c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,14 +77,14 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.6.3" + version: "2.0.0" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.4.5" + version: "0.4.7" build_daemon: dependency: transitive description: @@ -98,21 +98,21 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "2.0.0" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.11.5" + version: "1.12.2" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "6.1.10" + version: "6.1.12" built_collection: dependency: transitive description: @@ -154,7 +154,7 @@ packages: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" chewie: dependency: transitive description: @@ -211,6 +211,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + convex_bottom_bar: + dependency: "direct main" + description: + name: convex_bottom_bar + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" coverage: dependency: transitive description: @@ -252,7 +259,7 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.14" + version: "2.0.0" devicelocale: dependency: "direct main" description: @@ -260,13 +267,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.1" - dropdown_search: - dependency: "direct main" - description: - name: dropdown_search - url: "https://pub.dartlang.org" - source: hosted - version: "0.5.0" equatable: dependency: "direct main" description: @@ -315,7 +315,7 @@ packages: name: firebase_analytics url: "https://pub.dartlang.org" source: hosted - version: "8.0.0-dev.0" + version: "8.0.0-dev.2" firebase_analytics_platform_interface: dependency: transitive description: @@ -336,28 +336,28 @@ packages: name: firebase_auth url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.2" firebase_auth_web: dependency: transitive description: name: firebase_auth_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.5" firebase_core: dependency: "direct main" description: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" firebase_core_platform_interface: dependency: transitive description: @@ -378,21 +378,21 @@ packages: name: firebase_messaging url: "https://pub.dartlang.org" source: hosted - version: "9.1.0" + version: "9.1.1" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" fixnum: dependency: transitive description: @@ -407,6 +407,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.30.0" + flurry: + dependency: "direct main" + description: + name: flurry + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.7" flutter: dependency: "direct main" description: flutter @@ -425,21 +432,21 @@ packages: name: flutter_facebook_auth url: "https://pub.dartlang.org" source: hosted - version: "3.3.1+1" + version: "3.3.2+2" flutter_facebook_auth_platform_interface: dependency: transitive description: name: flutter_facebook_auth_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.1" flutter_facebook_auth_web: dependency: transitive description: name: flutter_facebook_auth_web url: "https://pub.dartlang.org" source: hosted - version: "2.4.0+2" + version: "2.4.1+1" flutter_html: dependency: "direct main" description: @@ -545,7 +552,7 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "1.0.0" html: dependency: transitive description: @@ -615,7 +622,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "4.0.1" keyboard_actions: dependency: "direct main" description: @@ -636,7 +643,7 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.1" matcher: dependency: transitive description: @@ -832,7 +839,7 @@ packages: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.8" + version: "1.0.0" purchases_flutter: dependency: "direct main" description: @@ -881,7 +888,7 @@ packages: name: sentry url: "https://pub.dartlang.org" source: hosted - version: "4.1.0-nullsafety.1" + version: "5.0.0" shared_preferences: dependency: "direct dev" description: @@ -963,14 +970,14 @@ packages: name: smartlook url: "https://pub.dartlang.org" source: hosted - version: "1.0.6" + version: "1.0.7" source_gen: dependency: transitive description: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.10+3" + version: "1.0.0" source_map_stack_trace: dependency: transitive description: @@ -1040,7 +1047,7 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" string_scanner: dependency: transitive description: @@ -1208,7 +1215,7 @@ packages: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "0.7.4" + version: "1.0.0" webview_flutter: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index de05cb5..c8f0a9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: cupertino_icons: ^1.0.0 google_fonts: ^2.0.0 devicelocale: ^0.4.1 - sentry: ^4.1.0-nullsafety.1 + sentry: ^5.0.0 flutter_bloc: ^7.0.0 equatable: ^2.0.0 @@ -54,25 +54,28 @@ dependencies: confetti: ^0.6.0-nullsafety crypto: ^3.0.0 carousel_slider: ^4.0.0-nullsafety.0 - dropdown_search: ^0.5.0 + #dropdown_search: ^0.5.0 + convex_bottom_bar: ^3.0.0 - firebase_core: ^1.0.2 - firebase_analytics: ^8.0.0-dev.0 - firebase_messaging: ^9.1.0 + firebase_core: ^1.0.3 + firebase_analytics: ^8.0.0-dev.2 + firebase_messaging: ^9.1.1 flutter_local_notifications: ^5.0.0 - firebase_auth: ^1.0.1 - flutter_facebook_auth: ^3.3.1+1 + firebase_auth: ^1.0.3 + flutter_facebook_auth: ^3.3.2 google_sign_in: ^5.0.1 apple_sign_in: ^0.1.0 - smartlook: ^1.0.6 + smartlook: ^1.0.7 + flurry: ^0.0.4 #animated_widgets: ^1.0.6 mockito: ^5.0.3 sqflite: ^2.0.0+3 flutter_secure_storage: ^4.1.0 + #social_share: ^2.1.1 flutter_localizations: sdk: flutter @@ -167,6 +170,7 @@ flutter: - asset/image/BMI_graph_C.png - asset/image/BMI_mutato.png - asset/image/haken.png + - asset/image/kupa.png - asset/image/pict_calorie.png - asset/image/pict_development_by_bodypart_percent.png @@ -226,6 +230,8 @@ flutter: - asset/menu/3.bcs1.jpg - asset/menu/300m.jpg - asset/menu/400m.jpg + - asset/menu/FG_1_test.jpg + - asset/menu/FG_1_training.jpg - asset/menu/alternate_dumbbell_presses.jpg - asset/menu/alternate_standing_shoulder_press.jpg - asset/menu/arnold_press.jpg