diff --git a/asset/image/Gain_muscle.jpg b/asset/image/Gain_muscle.jpg index c3d2e04..69fe5a3 100644 Binary files a/asset/image/Gain_muscle.jpg and b/asset/image/Gain_muscle.jpg differ diff --git a/asset/image/WT_weight_loss.jpg b/asset/image/WT_weight_loss.jpg deleted file mode 100644 index b22aaba..0000000 Binary files a/asset/image/WT_weight_loss.jpg and /dev/null differ diff --git a/asset/image/endurance.jpg b/asset/image/endurance.jpg new file mode 100644 index 0000000..472c113 Binary files /dev/null and b/asset/image/endurance.jpg differ diff --git a/asset/image/explosiveness.jpg b/asset/image/explosiveness.jpg new file mode 100644 index 0000000..e891257 Binary files /dev/null and b/asset/image/explosiveness.jpg differ diff --git a/asset/image/flexibility.jpg b/asset/image/flexibility.jpg new file mode 100644 index 0000000..bc9013f Binary files /dev/null and b/asset/image/flexibility.jpg differ diff --git a/asset/image/gain_strength.jpg b/asset/image/gain_strength.jpg new file mode 100644 index 0000000..f0bbb9b Binary files /dev/null and b/asset/image/gain_strength.jpg differ diff --git a/asset/image/muscle_endurance.jpg b/asset/image/muscle_endurance.jpg new file mode 100644 index 0000000..6dfb17a Binary files /dev/null and b/asset/image/muscle_endurance.jpg differ diff --git a/asset/image/shape_forming.jpg b/asset/image/shape_forming.jpg new file mode 100644 index 0000000..065e6bb Binary files /dev/null and b/asset/image/shape_forming.jpg differ diff --git a/asset/image/weight_loss.jpg b/asset/image/weight_loss.jpg new file mode 100644 index 0000000..296de4c Binary files /dev/null and b/asset/image/weight_loss.jpg differ diff --git a/i18n/en.json b/i18n/en.json index 685de90..e549413 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -51,8 +51,7 @@ "Aerobic": "Aerobic", "Anaerobic": "Anaerobic", "Cooper": "Cooper", - "Strength": "Strength", - "Endurance": "Strength Endurance", + "Strength": "Strength", "Pushups": "Pushups", "Timed Pushups": "Timed Pushups", "Core": "Core", @@ -116,7 +115,7 @@ "Next": "Next", "Select a gender": "Select a gender", - "Set Your Goals": "Set Your Goals", + "Set Your Primary Goal": "Set Your Primary Goal", "Gain Muscle": "Gain Muscle", "Loose Weight": "Loose Weight", "Your Fitness State": "Your Fitness State", @@ -353,10 +352,6 @@ "Activity":"Activity", "Body Type":"Body Type", - "Goal":"Goal", - "gain_muscle": "Gain Muscle", - "weight_loss":"Weight Loss", - "Set your goal":"Set your goal", "Set your fitness level":"Set your fitness level", "Set your body type":"Set your body type", "These equipments and devices are available":"These equipments and devices are available", @@ -441,6 +436,41 @@ "Result":"Result", "Name too short":"Name too short", "No, bring me there":"No, bring me there", - "You are about to add a new parallel test":"You are about to add a new parallel test" + "You are about to add a new parallel test":"You are about to add a new parallel test", + "Your Primary Sport":"Your Primary Sport", + "and":"and", + "Sport":"Sport", + + "Goal":"Goal", + + "gain_muscle": "Gain Muscle", + "gain_strength": "Gain Strength", + "weight_loss":"Weight Loss", + "muscle_endurance": "Muscle Endurance", + "flexibility":"Flexibility", + "explosiveness":"Explosiveness", + "shape_forming":"Shape Forming", + "endurance": "Endurance", + + "Set your primary goal":"Set your primary goal", + + "Endurance": "Endurance", + "Muscle Endurance": "Muscle Endurance", + "Flexibility":"Flexibility", + "Explosiveness":"Explosiveness", + "Shape Forming":"Shape Forming", + "Loss Weight":"Loss Weight", + + "Skip":"Skip", + "Exception: Network error, try again later!":"Network error, try again later!", + "Exception: Exception: Network Error, please try again later": "Exception: Network Error, please try again later", + + "Warning":"Warning", + "No Registration":"No Registration", + "You will skip the registration process.":"You will skip the registration process.", + "Please take a short tour in the app":"Please take a short tour in the app", + "No Login":"No Login", + "You will skip the login.":"You will skip the login.", + "The app functionalitity will be restricted, but please take a tour!":"The app functionalitity will be restricted, but please take a tour!" } \ No newline at end of file diff --git a/i18n/hu.json b/i18n/hu.json index cb12778..52f8310 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -58,7 +58,7 @@ "Anaerobic": "Anaerob", "Cooper": "Cooper teszt", "Strength": "Erő", - "Endurance": "Erő állóképesség", + "Endurance": "Állóképesség", "Pushups": "Fekvőtámasz", "Timed Pushups": "Fekvőtámasz időre", "Core": "Core (plank)", @@ -120,7 +120,7 @@ "Next": "Tovább", "Select a gender": "Válaszd ki a nemet", - "Set Your Goals": "Mi a célod?", + "Set Your Primary Goal": "Mi az elsődleges célod?", "Gain Muscle": "Izomépítés", "Loose Weight": "Fogyás", "Your Fitness State": "Milyen a fizikai állapotod?", @@ -436,5 +436,32 @@ "Result":"Értékelés", "Name too short":"A név túl rövid", "No, bring me there":"Nem, vigyél oda", - "You are about to add a new parallel test":"Egy új gyakorlatot készülsz párhuzamosan végrehajtani" + "You are about to add a new parallel test":"Egy új gyakorlatot készülsz párhuzamosan végrehajtani", + "Your Primary Sport":"Elsődleges sportág", + "and":"és", + "Sport":"sport", + + "gain_strength": "Erőnövelés", + "muscle_endurance": "Erő állóképesség", + "flexibility":"Rugalmasság", + "explosiveness":"Robbanékonyság", + "shape_forming":"Alakformálás", + "endurance": "Állóképesség", + + "Muscle Endurance": "Erő állóképesség", + "Flexibility":"Rugalmasság", + "Explosiveness":"Robbanékonyság", + "Shape Forming":"Alakformálás", + "Loss Weight":"Fogyás", + + "Skip":"Kihagyom", + "Exception: Network error, try again later!":"Hálózati hiba, próbálkozz később!", + "Exception: Exception: Network Error, please try again later": "Hálózati hiba, próbálkozz később!", + "Warning":"Figyelmeztetés", + "No Registration":"Regisztráció kimaradt", + "You will skip the registration process.":"Átugrod a regisztrációs folyamatot.", + "Please take a short tour in the app":"Kérlek tégy egy rövid túrát az applikációban", + "No Login":"Bejelentkezés kimaradt", + "You will skip the login.":"Átugrod a bejelentkezést.", + "The app functionalitity will be restricted, but please take a tour!":"Az applikációt korlátozottan tudod így használni, de tégy egy túrát!" } \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b3e970d..47f524b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -91,6 +91,8 @@ PODS: - Flutter - Flurry-iOS-SDK/FlurrySDK (11.2.0) - Flutter (1.0.0) + - flutter_app_badger (0.0.1): + - Flutter - flutter_facebook_auth (2.0.0): - FBSDKCoreKit (~> 9.1.0) - FBSDKLoginKit (~> 9.1.0) @@ -99,6 +101,9 @@ PODS: - Flutter - flutter_secure_storage (3.3.1): - Flutter + - flutter_uxcam (1.3.2): + - Flutter + - UXCam (~> 3.3.3) - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) @@ -153,6 +158,8 @@ PODS: - nanopb/encode (2.30906.0) - package_info (0.0.1): - Flutter + - package_info_plus (0.4.5): + - Flutter - path_provider (0.0.1): - Flutter - PromisesObjC (1.2.12) @@ -164,13 +171,18 @@ PODS: - PurchasesCoreSwift (3.10.7) - PurchasesHybridCommon (1.6.2): - Purchases (= 3.10.7) - - shared_preferences (0.0.1): + - Sentry (6.2.1): + - Sentry/Core (= 6.2.1) + - Sentry/Core (6.2.1) + - sentry_flutter (0.0.1): - Flutter - - smartlook (0.0.5): + - Sentry (~> 6.2.1) + - shared_preferences (0.0.1): - Flutter - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) + - UXCam (3.3.4) - video_player (0.0.1): - Flutter - wakelock (0.0.1): @@ -187,16 +199,19 @@ DEPENDENCIES: - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - flurry (from `.symlinks/plugins/flurry/ios`) - Flutter (from `Flutter`) + - flutter_app_badger (from `.symlinks/plugins/flutter_app_badger/ios`) - flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - flutter_uxcam (from `.symlinks/plugins/flutter_uxcam/ios`) - google_sign_in (from `.symlinks/plugins/google_sign_in/ios`) - modal_progress_hud_nsn (from `.symlinks/plugins/modal_progress_hud_nsn/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`) + - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - - smartlook (from `.symlinks/plugins/smartlook/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - video_player (from `.symlinks/plugins/video_player/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) @@ -228,6 +243,8 @@ SPEC REPOS: - Purchases - PurchasesCoreSwift - PurchasesHybridCommon + - Sentry + - UXCam EXTERNAL SOURCES: apple_sign_in: @@ -246,26 +263,32 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flurry/ios" Flutter: :path: Flutter + flutter_app_badger: + :path: ".symlinks/plugins/flutter_app_badger/ios" flutter_facebook_auth: :path: ".symlinks/plugins/flutter_facebook_auth/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + flutter_uxcam: + :path: ".symlinks/plugins/flutter_uxcam/ios" google_sign_in: :path: ".symlinks/plugins/google_sign_in/ios" modal_progress_hud_nsn: :path: ".symlinks/plugins/modal_progress_hud_nsn/ios" package_info: :path: ".symlinks/plugins/package_info/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" purchases_flutter: :path: ".symlinks/plugins/purchases_flutter/ios" + sentry_flutter: + :path: ".symlinks/plugins/sentry_flutter/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" - smartlook: - :path: ".symlinks/plugins/smartlook/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" video_player: @@ -296,9 +319,11 @@ SPEC CHECKSUMS: flurry: 15b01f664ab1367c62b50291541ea7f78ca85aad Flurry-iOS-SDK: 6636d30c30f12010e7c7c71d84b443416a168efc Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c + flutter_app_badger: 65de4d6f0c34a891df49e6cfb8a1c0496426fa68 flutter_facebook_auth: 4b170c07b7fce791497093fcc3f134fb215f3f07 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec + flutter_uxcam: 87dd981feb200bc81a2f062edb5ca2d16dae595a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a google_sign_in: 6bd214b9c154f881422f5fe27b66aaa7bbd580cc GoogleAppMeasurement: 8d3c0aeede16ab7764144b5a4ca8e1d4323841b7 @@ -310,15 +335,18 @@ SPEC CHECKSUMS: modal_progress_hud_nsn: f6fb744cd060653d66ed8f325360ef3650eb2fde nanopb: 1bf24dd71191072e120b83dd02d08f3da0d65e53 package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 + package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97 Purchases: b8b8fb6e856ac8166e217f6e014df894d821dda1 purchases_flutter: 0130970b895c903e4e0aad793dd3a4c1b70bb434 PurchasesCoreSwift: 8ae0f08e020f0bc97c1befa4e38a0dbc8e9732e0 PurchasesHybridCommon: 5f5c1c245b12fc5e8760af7d11cb10f888109a9b + Sentry: 9b922b396b0e0bca8516a10e36b0ea3ebea5faf7 + sentry_flutter: 5b3c6d717db5b7482504a313c831b318297d4d37 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d - smartlook: bbc5c73a85c752a31dabf100c8930838c646342e sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + UXCam: c2c00873595ab89be227f197213dc3679ff88ae5 video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e wakelock: b0843b2479edbf6504d8d262c2959446f35373aa webview_flutter: 9f491a9b5a66f2573946a389b2677987b0ff8c0b diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index ebf5959..7116abd 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 = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -405,7 +405,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.1.13; + MARKETING_VERSION = 1.1.14; 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 = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -548,7 +548,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.1.13; + MARKETING_VERSION = 1.1.14; 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 = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = SFJJBDCU6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -583,7 +583,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.1.13; + MARKETING_VERSION = 1.1.14; PRODUCT_BUNDLE_IDENTIFIER = com.aitrainer.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index da4d36f..86c6d65 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -54,8 +54,6 @@ UIBackgroundModes - audio - fetch remote-notification UILaunchStoryboardName diff --git a/lib/bloc/customer_change/customer_change_bloc.dart b/lib/bloc/customer_change/customer_change_bloc.dart index 2771b36..4ec65a7 100644 --- a/lib/bloc/customer_change/customer_change_bloc.dart +++ b/lib/bloc/customer_change/customer_change_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/sport.dart'; import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:aitrainer_app/util/common.dart'; import 'package:bloc/bloc.dart'; @@ -8,8 +9,6 @@ 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'; @@ -30,24 +29,7 @@ class CustomerChangeBloc extends Bloc 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; - } - } - + selectedSport = customerRepository.getSport(); print("selected: $selectedFitnessItem sport: $selectedSport " + customerRepository.customer!.fitnessLevel.toString()); } @@ -63,13 +45,16 @@ class CustomerChangeBloc extends Bloc yield CustomerChangeLoading(); yield CustomerDataChanged(); } else if (event is CustomerGoalChange) { + yield CustomerChangeLoading(); customerRepository.setGoal(event.goal); yield CustomerDataChanged(); } else if (event is CustomerChangePasswordObscure) { + yield CustomerChangeLoading(); visiblePassword = !visiblePassword; yield CustomerDataChanged(); } else if (event is CustomerFitnessChange) { - //customerRepository.setFitnessLevel(event.fitness); + yield CustomerChangeLoading(); + customerRepository.setFitnessLevel(event.fitness); selectedFitnessItem = event.fitness; yield CustomerDataChanged(); } else if (event is CustomerBirthYearChange) { @@ -108,22 +93,17 @@ class CustomerChangeBloc extends Bloc } 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!); - } + customerRepository.setFitnessLevel(selectedFitnessItem!); } + if (selectedSport != null) { + customerRepository.customer!.sportId = selectedSport!.sportId; + } + await customerRepository.saveCustomer(); Cache().initBadges(); yield CustomerSaveSuccess(); diff --git a/lib/bloc/development_by_muscle/development_by_muscle_bloc.dart b/lib/bloc/development_by_muscle/development_by_muscle_bloc.dart index d718565..24bafd2 100644 --- a/lib/bloc/development_by_muscle/development_by_muscle_bloc.dart +++ b/lib/bloc/development_by_muscle/development_by_muscle_bloc.dart @@ -369,7 +369,7 @@ class DevelopmentByMuscleBloc extends Bloc { try { if (event is ExerciseLogLoad) { yield ExerciseLogLoading(); - await Cache().setExerciseLogSeen(); + await Cache().setActivityDonePrefs(ActivityDone.isExerciseLogSeen); Track().track(TrackingEvent.exercise_log_open); yield ExerciseLogReady(); } else if (event is ExerciseLogDelete) { diff --git a/lib/bloc/exercise_new/exercise_new_bloc.dart b/lib/bloc/exercise_new/exercise_new_bloc.dart index 423510a..e1e78ad 100644 --- a/lib/bloc/exercise_new/exercise_new_bloc.dart +++ b/lib/bloc/exercise_new/exercise_new_bloc.dart @@ -158,6 +158,9 @@ class ExerciseNewBloc extends Bloc with Logg } else if (event is ExerciseNewBMIAnimate) { yield ExerciseNewLoading(); yield ExerciseNewReady(); + } else if (event is ExerciseNewAddError) { + yield ExerciseNewLoading(); + yield ExerciseNewError(message: event.message); } } on Exception catch (e) { yield ExerciseNewError(message: e.toString()); diff --git a/lib/bloc/exercise_new/exercise_new_event.dart b/lib/bloc/exercise_new/exercise_new_event.dart index e701619..77f30fa 100644 --- a/lib/bloc/exercise_new/exercise_new_event.dart +++ b/lib/bloc/exercise_new/exercise_new_event.dart @@ -73,7 +73,7 @@ class ExerciseNewSaveWeight extends ExerciseNewEvent { class ExerciseNewBMIAnimate extends ExerciseNewEvent { final dynamic value; - const ExerciseNewBMIAnimate({this.value}); + const ExerciseNewBMIAnimate({required this.value}); @override List get props => [value]; @@ -82,3 +82,10 @@ class ExerciseNewBMIAnimate extends ExerciseNewEvent { class ExerciseNewSubmit extends ExerciseNewEvent { const ExerciseNewSubmit(); } + +class ExerciseNewAddError extends ExerciseNewEvent { + final String message; + const ExerciseNewAddError({required this.message}); + @override + List get props => [message]; +} diff --git a/lib/bloc/login/login_bloc.dart b/lib/bloc/login/login_bloc.dart index 4a93a77..6604332 100644 --- a/lib/bloc/login/login_bloc.dart +++ b/lib/bloc/login/login_bloc.dart @@ -22,6 +22,7 @@ class LoginBloc extends Bloc with Trans { final BuildContext context; final bool isRegistration; bool dataPolicyAllowed = false; + bool emailSubscription = false; bool obscure = true; LoginBloc({required this.accountBloc, required this.userRepository, required this.context, required this.isRegistration}) @@ -81,6 +82,7 @@ class LoginBloc extends Bloc with Trans { } else { await userRepository.addUser(); accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!)); + customerRepository.customer!.emailSubscription = emailSubscription == true ? 1 : 0; await saveCustomer(); Track().track(TrackingEvent.registration, eventValue: "email"); Cache().setLoginType(LoginType.email); @@ -94,6 +96,7 @@ class LoginBloc extends Bloc with Trans { Cache().setLoginType(LoginType.fb); await userRepository.addUserFB(); accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!)); + customerRepository.customer!.emailSubscription = emailSubscription == true ? 1 : 0; await saveCustomer(); Track().track(TrackingEvent.registration, eventValue: "FB"); yield LoginSuccess(); @@ -105,6 +108,7 @@ class LoginBloc extends Bloc with Trans { Cache().setLoginType(LoginType.google); await userRepository.addUserGoogle(); accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!)); + customerRepository.customer!.emailSubscription = emailSubscription == true ? 1 : 0; await saveCustomer(); Track().track(TrackingEvent.registration, eventValue: "Google"); yield LoginSuccess(); @@ -116,6 +120,7 @@ class LoginBloc extends Bloc with Trans { Cache().setLoginType(LoginType.apple); await userRepository.addUserApple(); accountBloc.add(AccountLogInFinished(customer: Cache().userLoggedIn!)); + customerRepository.customer!.emailSubscription = emailSubscription == true ? 1 : 0; await saveCustomer(); Track().track(TrackingEvent.registration, eventValue: "Apple"); @@ -124,10 +129,18 @@ class LoginBloc extends Bloc with Trans { yield LoginLoading(); this.dataPolicyAllowed = !dataPolicyAllowed; yield LoginReady(); + } else if (event is EmailSubscriptionClicked) { + yield LoginLoading(); + this.emailSubscription = !emailSubscription; + yield LoginReady(); } else if (event is LoginPasswordChangeObscure) { yield LoginLoading(); this.obscure = !this.obscure; yield LoginReady(); + } else if (event is LoginSkip) { + yield LoginLoading(); + Cache().startPage = "home"; + yield LoginSkipped(); } } on Exception catch (e) { yield LoginError(message: e.toString()); diff --git a/lib/bloc/login/login_event.dart b/lib/bloc/login/login_event.dart index 1e4a008..5bc0053 100644 --- a/lib/bloc/login/login_event.dart +++ b/lib/bloc/login/login_event.dart @@ -43,11 +43,20 @@ class LoginApple extends LoginEvent { const LoginApple(); } +class LoginSkip extends LoginEvent { + const LoginSkip(); +} + class DataProtectionClicked extends LoginEvent { final bool marked; const DataProtectionClicked({required this.marked}); } +class EmailSubscriptionClicked extends LoginEvent { + final bool marked; + const EmailSubscriptionClicked({required this.marked}); +} + class RegistrationSubmit extends LoginEvent { const RegistrationSubmit(); } diff --git a/lib/bloc/login/login_state.dart b/lib/bloc/login/login_state.dart index 8918cdf..ee78c6c 100644 --- a/lib/bloc/login/login_state.dart +++ b/lib/bloc/login/login_state.dart @@ -23,6 +23,10 @@ class LoginSuccess extends LoginState { const LoginSuccess(); } +class LoginSkipped extends LoginState { + const LoginSkipped(); +} + class LoginError extends LoginState { final String message; const LoginError({required this.message}); diff --git a/lib/bloc/menu/menu_bloc.dart b/lib/bloc/menu/menu_bloc.dart index f82e7fb..bb4bba2 100644 --- a/lib/bloc/menu/menu_bloc.dart +++ b/lib/bloc/menu/menu_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:collection'; +import 'package:aitrainer_app/bloc/tutorial/tutorial_bloc.dart'; import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/model/exercise_ability.dart'; import 'package:aitrainer_app/model/workout_menu_tree.dart'; @@ -26,11 +27,6 @@ class MenuBloc extends Bloc with Trans, Logging { WorkoutMenuTree? workoutItem; final List listFilterDevice = []; - String infoTitle = ""; - String infoText = ""; - String infoText2 = ""; - String infoText3 = ""; - String infoLink = ""; int missingParent = 0; String? missingTreeName = ""; @@ -42,48 +38,6 @@ class MenuBloc extends Bloc with Trans, Logging { parent = 0; } - void setMenuInfo() { - double percent = Cache().getPercentExercises(); - if (percent == -1) { - exerciseRepository.getBaseExerciseFinishedPercent(); - percent = Cache().getPercentExercises(); - } - - percent = percent * 100; - //log("Percent " + percent.toString()); - if (percent == -1 || percent == 0) { - infoTitle = t("Greetings!"); - infoText = t("The purpose is to measure you physical condition") + - " " + - t("The suggested order of the exercises: chest - biceps - triceps - back - shoulders - core - tigh - calf."); - infoText2 = t("I suggest begin your tests with a"); - infoText3 = t("Go to the menu Strength - One Rep Max - Chest, and select your favourite exercise."); - infoLink = t("Bring me there"); - } else if (percent > 0 && percent < 20) { - infoTitle = t("Nice! This is a good start"); - } else if (percent > 20 && percent < 40) { - infoTitle = t("Go on!") + " " + t("You are on track"); - } else if (percent > 60 && percent < 80) { - infoTitle = t("Persistence!") + " " + t("Not so much left"); - } else if (percent > 80 && percent < 100) { - infoTitle = t("Almost!") + " " + t("You have only 1-2 exercise left to finish!"); - } else { - infoTitle = t("Congratulation!"); - infoText2 = t("You have achieved to first 100% test-round!"); - infoText3 = t("Now you unlocked: Development By Muscles and the Suggested Trainings Plan"); - } - - menuTreeRepository.sortByMuscleType(); - missingTreeName = exerciseRepository.nextMissingBaseExercise(menuTreeRepository.sortedTree); - //log("Missing " + missingTreeName); - if (missingTreeName != null) { - if (percent > 0) { - infoText = t("Please continue your tests with a") + " '" + missingTreeName! + "' " + t("exercise!"); - infoLink = t("Bring me there"); - } - } - } - void setContext(BuildContext context) { this.context = context; } @@ -118,7 +72,6 @@ class MenuBloc extends Bloc with Trans, Logging { final LinkedHashMap branch = menuTreeRepository.getBranch(event.parent); await getImages(branch); - //await Future.delayed(Duration(seconds: 2)); yield MenuReady(); } else if (event is MenuTreeUp) { yield MenuLoading(); diff --git a/lib/bloc/settings/settings_bloc.dart b/lib/bloc/settings/settings_bloc.dart index 63100d1..b687f52 100644 --- a/lib/bloc/settings/settings_bloc.dart +++ b/lib/bloc/settings/settings_bloc.dart @@ -61,6 +61,11 @@ class SettingsBloc extends Bloc with Logging { } Cache().initBadges(); yield SettingsReady(); + } else if (event is SettingsActivateTutorial) { + yield SettingsLoading(); + Cache().activitiesDone[event.activity.toStr()] = false; + print(" ----------------- Setting ${event.activity} to false"); + yield SettingsReady(); } } diff --git a/lib/bloc/settings/settings_event.dart b/lib/bloc/settings/settings_event.dart index bbfb0fe..1612858 100644 --- a/lib/bloc/settings/settings_event.dart +++ b/lib/bloc/settings/settings_event.dart @@ -31,3 +31,11 @@ class SettingsSetHardware extends SettingsEvent { @override List get props => [this.hasHardware]; } + +class SettingsActivateTutorial extends SettingsEvent { + final ActivityDone activity; + const SettingsActivateTutorial({required this.activity}); + + @override + List get props => [this.activity]; +} diff --git a/lib/bloc/test_set_edit/test_set_edit_bloc.dart b/lib/bloc/test_set_edit/test_set_edit_bloc.dart index 8eee1d1..f74ee6c 100644 --- a/lib/bloc/test_set_edit/test_set_edit_bloc.dart +++ b/lib/bloc/test_set_edit/test_set_edit_bloc.dart @@ -113,6 +113,8 @@ class TestSetEditBloc extends Bloc { Cache().saveActiveExercisePlan(exercisePlan, details); Track().track(TrackingEvent.test_set_edit, eventValue: templateName); yield TestSetEditSaved(); + } else if (event is TestSetEditAddError) { + yield TestSetEditError(message: event.message); } } on Exception catch (e) { yield TestSetEditError(message: e.toString()); diff --git a/lib/bloc/test_set_edit/test_set_edit_event.dart b/lib/bloc/test_set_edit/test_set_edit_event.dart index b845cdd..13a4ff7 100644 --- a/lib/bloc/test_set_edit/test_set_edit_event.dart +++ b/lib/bloc/test_set_edit/test_set_edit_event.dart @@ -47,3 +47,10 @@ class TestSetEditSkipExerciseType extends TestSetEditEvent { class TestSetEditSubmit extends TestSetEditEvent { const TestSetEditSubmit(); } + +class TestSetEditAddError extends TestSetEditEvent { + final String message; + const TestSetEditAddError({required this.message}); + @override + List get props => [message]; +} diff --git a/lib/bloc/tutorial/tutorial_bloc.dart b/lib/bloc/tutorial/tutorial_bloc.dart new file mode 100644 index 0000000..f7bc352 --- /dev/null +++ b/lib/bloc/tutorial/tutorial_bloc.dart @@ -0,0 +1,214 @@ +import 'dart:async'; +import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/tutorial.dart'; +import 'package:aitrainer_app/model/tutorial_step.dart'; +import 'package:aitrainer_app/service/logging.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'tutorial_event.dart'; +part 'tutorial_state.dart'; + +class TutorialBloc extends Bloc with Logging { + late String tutorialName; + bool isActive = false; + bool canActivate = false; + + Tutorial? tutorial; + String? actualText; + String? actualCheck; + + List checks = []; + + double calculatedHeight = 0; + + TutorialStepAction? action; + double? top; + double left = 20; + bool showCheckText = true; + int parent = 0; + + int step = 0; + + MenuBloc? menuBloc; + + TutorialBloc({required this.tutorialName}) : super(TutorialInitial()); + + init() { + if (Cache().tutorials != null) { + print("Actual step: $step"); + final List tutorials = Cache().tutorials!; + tutorials.forEach((element) { + final String realTutorialName = tutorialName.replaceFirst("tutorial", "").toLowerCase(); + if (element.name.toLowerCase() == realTutorialName) { + tutorial = element; + + setNextStepData(step); + isActive = true; + } + }); + } + } + + void setNextStepData(int step) { + if (step == -1) { + isActive = false; + return; + } + if (tutorial != null && tutorial!.steps != null) { + actualText = tutorial!.steps![step].tutorialTextTranslation; + print("Step: $step, text: $actualText"); + this.actualCheck = tutorial!.steps![step].checkText; + this.checks = []; + + if (this.actualCheck != null) { + this.checks = this.actualCheck!.split("|"); + } else { + this.checks.add("Next"); + } + + if (this.tutorial!.steps![step].action != null) { + this.action = this.tutorial!.steps![step].action; + this.top = action!.top.toDouble(); + this.left = action!.left == -1 ? 20 : action!.left.toDouble(); + this.showCheckText = action!.showCheckText == true; + this.parent = action!.parent; + } + + calculateHeight(); + } + /* else { + this.add(TutorialFinished()); + } */ + } + + bool activateTutorial() { + if (!canActivate) { + print("Tutorial canActivate false"); + return false; + } + ActivityDone? activityDone = ActivityDone.tutorialBasic.searchByString(tutorialName); + + if (activityDone == null) { + return false; + } + bool? isActivityDone = Cache().activitiesDone[activityDone.toStr()]; + log("isActivityDone? $isActivityDone"); + if (isActivityDone == null || isActivityDone == true) { + return false; + } + log("Running tutorial $activityDone"); + init(); + return true; + } + + void calculateHeight() { + int count = getElementCount('

'); + count += getElementCount('
'); + double lines = (actualText!.length / 32 + count); + if (lines < 5) { + lines = lines + 2; + } + calculatedHeight = lines * 15; + print("Calculated Height: $calculatedHeight length: ${actualText!.length} lines: $lines count: $count"); + } + + int getElementCount(String elem) { + int pos = 0; + int count = 0; + bool out = false; + while (out == false) { + pos = actualText!.indexOf(elem, pos); + if (pos == -1) { + out = true; + } else { + count++; + pos++; + } + } + return count; + } + + int getNextStep(int step) { + int next = 0; + if (action == null) { + return step + 1; + } + /* bool found = false; + if (tutorial != null && tutorial!.steps != null) { + for (var nextStep in tutorial!.steps!) { + print("step $step parent: ${nextStep.action!.parent}"); + if (step + 1 == nextStep.step) { + next = nextStep.step!; + found = true; + break; + } + } + } */ + step++; + next = step; + print("Next step:! $next"); + if (step >= tutorial!.steps!.length) { + next = -1; + this.add(TutorialFinished()); + } + return next; + } + + bool checkAction(String checkText) { + final bool nextStep = checkText == actualCheck; + if (nextStep) { + this.add(TutorialNext(text: actualCheck!)); + } + return nextStep; + } + + @override + Stream mapEventToState(TutorialEvent event) async* { + if (event is TutorialLoad) { + init(); + if (menuBloc != null) { + menuBloc!.add(MenuCreate()); + } + } else if (event is TutorialNext) { + if (tutorial != null) { + yield TutorialLoading(); + + step = this.getNextStep(step); + if (step == -1) { + print("Tutorial $tutorialName finished!"); + return; + } + print("Step: $step"); + setNextStepData(step); + + print("Menu rebuild $menuBloc"); + if (menuBloc != null) { + menuBloc!.add(MenuCreate()); + } + yield TutorialReady(); + } + } else if (event is TutorialWrongAction) { + yield TutorialLoading(); + actualText = tutorial!.steps![step].errorTextTranslation!; + yield TutorialReady(); + } else if (event is TutorialStart) { + yield TutorialLoading(); + isActive = true; + canActivate = true; + step = 0; + yield TutorialReady(); + } else if (event is TutorialFinished) { + yield TutorialLoading(); + isActive = false; + canActivate = false; + ActivityDone? activityDone = ActivityDone.tutorialBasic.searchByString(tutorialName); + print("activity Finished: $activityDone"); + if (activityDone != null) { + await Cache().setActivityDonePrefs(activityDone); + } + yield TutorialReady(); + } + } +} diff --git a/lib/bloc/tutorial/tutorial_event.dart b/lib/bloc/tutorial/tutorial_event.dart new file mode 100644 index 0000000..640e158 --- /dev/null +++ b/lib/bloc/tutorial/tutorial_event.dart @@ -0,0 +1,29 @@ +part of 'tutorial_bloc.dart'; + +abstract class TutorialEvent extends Equatable { + const TutorialEvent(); + + @override + List get props => []; +} + +class TutorialLoad extends TutorialEvent { + const TutorialLoad(); +} + +class TutorialStart extends TutorialEvent { + const TutorialStart(); +} + +class TutorialNext extends TutorialEvent { + final String text; + const TutorialNext({required this.text}); +} + +class TutorialFinished extends TutorialEvent { + const TutorialFinished(); +} + +class TutorialWrongAction extends TutorialEvent { + const TutorialWrongAction(); +} diff --git a/lib/bloc/tutorial/tutorial_state.dart b/lib/bloc/tutorial/tutorial_state.dart new file mode 100644 index 0000000..ca67e21 --- /dev/null +++ b/lib/bloc/tutorial/tutorial_state.dart @@ -0,0 +1,28 @@ +part of 'tutorial_bloc.dart'; + +abstract class TutorialState extends Equatable { + const TutorialState(); + + @override + List get props => []; +} + +class TutorialInitial extends TutorialState { + const TutorialInitial(); +} + +class TutorialLoading extends TutorialState { + const TutorialLoading(); +} + +class TutorialReady extends TutorialState { + const TutorialReady(); +} + +class TutorialError extends TutorialState { + final String message; + const TutorialError({required this.message}); + + @override + List get props => [message]; +} diff --git a/lib/library/fade_in.dart b/lib/library/fade_in.dart deleted file mode 100644 index 12d9e0f..0000000 --- a/lib/library/fade_in.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/widgets.dart'; - -class FadeIn extends StatefulWidget { - /// Fade-in controller - final FadeInController controller; - - /// Child widget to fade-in - final Widget child; - - /// Duration of fade-in. Defaults to 250ms - final Duration duration; - - /// Fade-in curve. Defaults to [Curves.easeIn] - final Curve curve; - - const FadeIn({ - Key? key, - required this.controller, - required this.child, - this.duration = const Duration(milliseconds: 250), - this.curve = Curves.easeIn, - }) : super(key: key); - - @override - _FadeInState createState() => _FadeInState(); -} - -enum FadeInAction { - fadeIn, - fadeOut, -} - -/// Fade-in controller which dispatches fade-in/fade-out actions -class FadeInController { - final _streamController = StreamController(); - - /// Automatically starts the initial fade-in. Defaults to true - final bool autoStart; - - FadeInController({this.autoStart = true}); - - void dispose() => _streamController.close(); - - /// Fades-in child - void fadeIn() => run(FadeInAction.fadeIn); - - /// Fades-out child - void fadeOut() => run(FadeInAction.fadeOut); - - /// Dispatches a [FadeInAction] - void run(FadeInAction action) => _streamController.add(action); - - /// Stream of [FadeInAction]s dispatched by this controller - Stream get stream => _streamController.stream; -} - -class _FadeInState extends State with TickerProviderStateMixin { - late AnimationController _controller; - late StreamSubscription _listener; - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - vsync: this, - duration: widget.duration, - ); - - _setupCurve(); - - if (widget.controller.autoStart != false) { - fadeIn(); - } - - _listener = widget.controller.stream.listen(_onAction); - } - - void _setupCurve() { - final curve = CurvedAnimation(parent: _controller, curve: widget.curve); - - Tween( - begin: 0.0, - end: 1.0, - ).animate(curve); - } - - void _onAction(FadeInAction action) { - switch (action) { - case FadeInAction.fadeIn: - fadeIn(); - break; - case FadeInAction.fadeOut: - fadeOut(); - break; - } - } - - @override - void didUpdateWidget(FadeIn oldWidget) { - if (oldWidget.controller != widget.controller) { - _listener = widget.controller.stream.listen(_onAction); - } - - if (oldWidget.duration != widget.duration) { - _controller.duration = widget.duration; - } - - if (oldWidget.curve != widget.curve) { - _setupCurve(); - } - - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - _controller.dispose(); - _listener.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: _controller, - child: widget.child, - ); - } - - /// Fades-in child - void fadeIn() => _controller.forward(); - - /// Fades-out child - void fadeOut() => _controller.reverse(); -} diff --git a/lib/library/liquid_progress_indicator/liquid_linear_progress_indicator.dart b/lib/library/liquid_progress_indicator/liquid_linear_progress_indicator.dart index 9895af5..72e7442 100644 --- a/lib/library/liquid_progress_indicator/liquid_linear_progress_indicator.dart +++ b/lib/library/liquid_progress_indicator/liquid_linear_progress_indicator.dart @@ -59,9 +59,9 @@ class _LiquidLinearProgressIndicatorState extends State[ diff --git a/lib/library/super_tooltip.dart b/lib/library/super_tooltip.dart new file mode 100644 index 0000000..d9b8f61 --- /dev/null +++ b/lib/library/super_tooltip.dart @@ -0,0 +1,973 @@ +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +enum TooltipDirection { up, down, left, right } +enum ShowCloseButton { inside, outside, none } +enum ClipAreaShape { oval, rectangle } + +typedef OutSideTapHandler = void Function(); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +/// Super flexible Tooltip class that allows you to show any content +/// inside a Tooltip in the overlay of the screen. +/// +class SuperTooltip { + /// Allows to accedd the closebutton for UI Testing + static Key closeButtonKey = const Key("CloseButtonKey"); + + /// Signals if the Tooltip is visible at the moment + bool isOpen = false; + + /// + /// The content of the Tooltip + final Widget content; + + /// + /// The direcion in which the tooltip should open + TooltipDirection popupDirection; + + /// + /// optional handler that gets called when the Tooltip is closed + final OutSideTapHandler? onClose; + + /// + /// [minWidth], [minHeight], [maxWidth], [maxHeight] optional size constraints. + /// If a constraint is not set the size will ajust to the content + double? minWidth, minHeight, maxWidth, maxHeight; + + /// + /// The minium padding from the Tooltip to the screen limits + final double minimumOutSidePadding; + + /// + /// If [snapsFarAwayVertically== true] the bigger free space above or below the target will be + /// covered completely by the ToolTip. All other dimension or position constraints get overwritten + final bool snapsFarAwayVertically; + + /// + /// If [snapsFarAwayHorizontally== true] the bigger free space left or right of the target will be + /// covered completely by the ToolTip. All other dimension or position constraints get overwritten + final bool snapsFarAwayHorizontally; + + /// [top], [right], [bottom], [left] position the Tooltip absolute relative to the whole screen + double? top, right, bottom, left; + + /// + /// A Tooltip can have none, an inside or an outside close icon + final ShowCloseButton showCloseButton; + + /// + /// [hasShadow] defines if the tooltip should have a shadow + final bool hasShadow; + + /// + /// The shadow color. + final Color shadowColor; + + /// + /// The shadow blur radius. + final double shadowBlurRadius; + + /// + /// The shadow spread radius. + final double shadowSpreadRadius; + + /// + /// the stroke width of the border + final double borderWidth; + + /// + /// The corder radii of the border + final double borderRadius; + + /// + /// The color of the border + final Color borderColor; + + /// + /// The color of the close icon + final Color closeButtonColor; + + /// + /// The size of the close button + final double closeButtonSize; + + /// + /// The icon for the close button + final IconData closeButtonIcon; + + /// + /// The length of the Arrow + final double arrowLength; + + /// + /// The width of the arrow at its base + final double arrowBaseWidth; + + /// + /// The distance of the tip of the arrow's tip to the center of the target + final double arrowTipDistance; + + /// + /// The backgroundcolor of the Tooltip + final Color backgroundColor; + + /// The color of the rest of the overlay surrounding the Tooltip. + /// typically a translucent color. + final Color outsideBackgroundColor; + + /// + /// By default touching the surrounding of the Tooltip closes the tooltip. + /// you can define a rectangle area where the background is completely transparent + /// and the widgets below react to touch + final Rect? touchThrougArea; + + /// + /// The shape of the [touchThrougArea]. + final ClipAreaShape touchThroughAreaShape; + + /// + /// If [touchThroughAreaShape] is [ClipAreaShape.rectangle] you can define a border radius + final double touchThroughAreaCornerRadius; + + /// + /// Let's you pass a key to the Tooltips cotainer for UI Testing + final Key? tooltipContainerKey; + + /// + /// Allow the tooltip to be dismissed tapping outside + final bool dismissOnTapOutside; + + /// + /// Enable background overlay + final bool containsBackgroundOverlay; + + final bool custom; + + Offset? _targetCenter; + OverlayEntry? _backGroundOverlay; + OverlayEntry? _ballonOverlay; + + SuperTooltip({ + this.tooltipContainerKey, + required this.content, // The contents of the tooltip. + required this.popupDirection, + this.onClose, + this.minWidth, + this.minHeight, + this.maxWidth, + this.maxHeight, + this.top, + this.right, + this.bottom, + this.left, + this.minimumOutSidePadding = 20.0, + this.showCloseButton = ShowCloseButton.none, + this.snapsFarAwayVertically = false, + this.snapsFarAwayHorizontally = false, + this.hasShadow = true, + this.shadowColor = Colors.black54, + this.shadowBlurRadius = 10.0, + this.shadowSpreadRadius = 5.0, + this.borderWidth = 2.0, + this.borderRadius = 10.0, + this.borderColor = Colors.black, + this.closeButtonIcon = Icons.close, + this.closeButtonColor = Colors.black, + this.closeButtonSize = 30.0, + this.arrowLength = 20.0, + this.arrowBaseWidth = 20.0, + this.arrowTipDistance = 2.0, + this.backgroundColor = Colors.white, + this.outsideBackgroundColor = const Color.fromARGB(50, 255, 255, 255), + this.touchThroughAreaShape = ClipAreaShape.oval, + this.touchThroughAreaCornerRadius = 5.0, + this.touchThrougArea, + this.dismissOnTapOutside = true, + this.containsBackgroundOverlay = true, + this.custom = false, + }) : assert((maxWidth ?? double.infinity) >= (minWidth ?? 0.0)), + assert((maxHeight ?? double.infinity) >= (minHeight ?? 0.0)); + + /// + /// Removes the Tooltip from the overlay + void close() { + if (onClose != null) { + onClose!(); + } + + _ballonOverlay!.remove(); + _backGroundOverlay?.remove(); + isOpen = false; + } + + void rebuild() { + _ballonOverlay!.remove(); + _backGroundOverlay?.remove(); + isOpen = false; + } + + /// + /// Displays the tooltip + /// The center of [targetContext] is used as target of the arrow + void show(BuildContext targetContext) { + final renderBox = targetContext.findRenderObject() as RenderBox; + final overlay = Overlay.of(targetContext)!.context.findRenderObject() as RenderBox?; + + _targetCenter = renderBox.localToGlobal(renderBox.size.center(Offset.zero), ancestor: overlay); + + // Create the background below the popup including the clipArea. + if (containsBackgroundOverlay) { + _backGroundOverlay = OverlayEntry( + builder: (context) => _AnimationWrapper( + builder: (context, opacity) => AnimatedOpacity( + opacity: opacity, + duration: const Duration(milliseconds: 600), + child: GestureDetector( + onTap: () { + if (dismissOnTapOutside) { + close(); + } + }, + child: Container( + decoration: ShapeDecoration( + shape: _ShapeOverlay( + touchThrougArea, touchThroughAreaShape, touchThroughAreaCornerRadius, outsideBackgroundColor))), + ), + ), + )); + } + + /// Handling snap far away feature. + if (snapsFarAwayVertically) { + maxHeight = null; + left = 0.0; + right = 0.0; + if (_targetCenter!.dy > overlay!.size.center(Offset.zero).dy) { + popupDirection = TooltipDirection.up; + top = 0.0; + } else { + popupDirection = TooltipDirection.down; + bottom = 0.0; + } + } // Only one of of them is possible, and vertical has higher priority. + else if (snapsFarAwayHorizontally) { + maxWidth = null; + top = 0.0; + bottom = 0.0; + if (_targetCenter!.dx < overlay!.size.center(Offset.zero).dx) { + popupDirection = TooltipDirection.right; + right = 0.0; + } else { + popupDirection = TooltipDirection.left; + left = 0.0; + } + } + + _ballonOverlay = OverlayEntry( + builder: (context) => _AnimationWrapper( + builder: (context, opacity) => AnimatedOpacity( + duration: Duration( + milliseconds: 300, + ), + opacity: opacity, + child: Center( + child: CustomSingleChildLayout( + delegate: _PopupBallonLayoutDelegate( + popupDirection: popupDirection, + targetCenter: _targetCenter, + minWidth: minWidth, + maxWidth: maxWidth, + minHeight: minHeight, + maxHeight: maxHeight, + outSidePadding: minimumOutSidePadding, + top: top, + bottom: bottom, + left: left, + right: right, + ), + child: Stack( + fit: StackFit.passthrough, + children: [_buildPopUp(), _buildCloseButton()], + ))), + ), + )); + + var overlays = []; + + if (containsBackgroundOverlay) { + overlays.add(_backGroundOverlay!); + } + overlays.add(_ballonOverlay!); + + Overlay.of(targetContext)!.insertAll(overlays); + isOpen = true; + } + + Widget _buildPopUp() { + return Positioned( + child: Container( + key: tooltipContainerKey, + decoration: ShapeDecoration( + color: backgroundColor, + shadows: hasShadow ? [BoxShadow(color: shadowColor, blurRadius: shadowBlurRadius, spreadRadius: shadowSpreadRadius)] : null, + shape: !custom + ? _BubbleShape(popupDirection, _targetCenter, borderRadius, arrowBaseWidth, arrowTipDistance, borderColor, borderWidth, + left, top, right, bottom) + : RoundedRectangleBorder( + side: BorderSide(color: borderColor, width: borderWidth), + borderRadius: BorderRadius.all(Radius.circular(borderRadius)))), + margin: _getBallonContainerMargin(), + child: content, + ), + ); + } + + Widget _buildCloseButton() { + const internalClickAreaPadding = 2.0; + + // + if (showCloseButton == ShowCloseButton.none) { + return new SizedBox(); + } + + // --- + + double right; + double top; + + switch (popupDirection) { + // + // LEFT: ------------------------------------- + case TooltipDirection.left: + right = arrowLength + arrowTipDistance + 3.0; + if (showCloseButton == ShowCloseButton.inside) { + top = 2.0; + } else if (showCloseButton == ShowCloseButton.outside) { + top = 0.0; + } else + throw AssertionError(showCloseButton); + break; + + // RIGHT/UP: --------------------------------- + case TooltipDirection.right: + case TooltipDirection.up: + right = 5.0; + if (showCloseButton == ShowCloseButton.inside) { + top = 2.0; + } else if (showCloseButton == ShowCloseButton.outside) { + top = 0.0; + } else + throw AssertionError(showCloseButton); + break; + + // DOWN: ------------------------------------- + case TooltipDirection.down: + // If this value gets negative the Shadow gets clipped. The problem occurs is arrowlength + arrowTipDistance + // is smaller than _outSideCloseButtonPadding which would mean arrowLength would need to be increased if the button is ouside. + right = 2.0; + if (showCloseButton == ShowCloseButton.inside) { + top = arrowLength + arrowTipDistance + 2.0; + } else if (showCloseButton == ShowCloseButton.outside) { + top = 0.0; + } else + throw AssertionError(showCloseButton); + break; + + // --------------------------------------------- + + default: + throw AssertionError(popupDirection); + } + + // --- + + return Positioned( + right: right, + top: top, + child: GestureDetector( + onTap: close, + child: Padding( + padding: const EdgeInsets.all(internalClickAreaPadding), + child: Icon( + closeButtonIcon, + size: closeButtonSize, + color: closeButtonColor, + ), + ), + )); + } + + EdgeInsets _getBallonContainerMargin() { + var top = (showCloseButton == ShowCloseButton.outside) ? closeButtonSize + 5 : 0.0; + + switch (popupDirection) { + // + case TooltipDirection.down: + return EdgeInsets.only( + top: arrowTipDistance + arrowLength, + ); + + case TooltipDirection.up: + return EdgeInsets.only(bottom: arrowTipDistance + arrowLength, top: top); + + case TooltipDirection.left: + return EdgeInsets.only(right: arrowTipDistance + arrowLength, top: top); + + case TooltipDirection.right: + return EdgeInsets.only(left: arrowTipDistance + arrowLength, top: top); + + default: + throw AssertionError(popupDirection); + } + } +} + +class _CustomBallonLayoutDelegate extends SingleChildLayoutDelegate { + final TooltipDirection? _popupDirection; + final Offset? _targetCenter; + final double? _minWidth; + final double? _maxWidth; + final double? _minHeight; + final double? _maxHeight; + final double _top; + final double? _bottom; + final double _left; + final double? _right; + final double? _outSidePadding; + + _CustomBallonLayoutDelegate({ + TooltipDirection? popupDirection, + Offset? targetCenter, + double? minWidth, + double? maxWidth, + double? minHeight, + double? maxHeight, + double? outSidePadding, + required double top, + double? bottom, + required double left, + double? right, + }) : _targetCenter = targetCenter, + _popupDirection = popupDirection, + _minWidth = minWidth, + _maxWidth = maxWidth, + _minHeight = minHeight, + _maxHeight = maxHeight, + _top = top, + _bottom = bottom, + _left = left, + _right = right, + _outSidePadding = outSidePadding; + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + return super.getConstraintsForChild(constraints); + } + + @override + Offset getPositionForChild(Size size, Size childSize) { + print(" ------ getPositionFroChild: $_top - $_left"); + //we place the widget at the cnter, by dividing the width and height by 2 to get the center + return Offset(_left, _top); + } + + @override + bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) { + return false; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +class _PopupBallonLayoutDelegate extends SingleChildLayoutDelegate { + final TooltipDirection? _popupDirection; + final Offset? _targetCenter; + final double? _minWidth; + final double? _maxWidth; + final double? _minHeight; + final double? _maxHeight; + final double? _top; + final double? _bottom; + final double? _left; + final double? _right; + final double? _outSidePadding; + + _PopupBallonLayoutDelegate({ + TooltipDirection? popupDirection, + Offset? targetCenter, + double? minWidth, + double? maxWidth, + double? minHeight, + double? maxHeight, + double? outSidePadding, + double? top, + double? bottom, + double? left, + double? right, + }) : _targetCenter = targetCenter, + _popupDirection = popupDirection, + _minWidth = minWidth, + _maxWidth = maxWidth, + _minHeight = minHeight, + _maxHeight = maxHeight, + _top = top, + _bottom = bottom, + _left = left, + _right = right, + _outSidePadding = outSidePadding; + + @override + Offset getPositionForChild(Size size, Size childSize) { + double? calcLeftMostXtoTarget() { + double? leftMostXtoTarget; + if (_left != null) { + leftMostXtoTarget = _left; + } else if (_right != null) { + leftMostXtoTarget = max( + size.topLeft(Offset.zero).dx + _outSidePadding!, size.topRight(Offset.zero).dx - _outSidePadding! - childSize.width - _right!); + } else { + leftMostXtoTarget = max(_outSidePadding!, + min(_targetCenter!.dx - childSize.width / 2, size.topRight(Offset.zero).dx - _outSidePadding! - childSize.width)); + } + return leftMostXtoTarget; + } + + double? calcTopMostYtoTarget() { + double? topmostYtoTarget; + if (_top != null) { + topmostYtoTarget = _top!; + } else if (_bottom != null) { + topmostYtoTarget = max(size.topLeft(Offset.zero).dy + _outSidePadding!, + size.bottomRight(Offset.zero).dy - _outSidePadding! - childSize.height - _bottom!); + } else { + topmostYtoTarget = max(_outSidePadding!, + min(_targetCenter!.dy - childSize.height / 2, size.bottomRight(Offset.zero).dy - _outSidePadding! - childSize.height)); + } + return topmostYtoTarget; + } + + switch (_popupDirection) { + // + case TooltipDirection.down: + return new Offset(calcLeftMostXtoTarget()!, _targetCenter!.dy); + + case TooltipDirection.up: + var top = _top ?? _targetCenter!.dy - childSize.height; + return new Offset(calcLeftMostXtoTarget()!, top); + + case TooltipDirection.left: + var left = _left ?? _targetCenter!.dx - childSize.width; + return new Offset(left, calcTopMostYtoTarget()!); + + case TooltipDirection.right: + return new Offset( + _targetCenter!.dx, + calcTopMostYtoTarget()!, + ); + + default: + throw AssertionError(_popupDirection); + } + } + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + // print("ParentConstraints: $constraints"); + + var calcMinWidth = _minWidth ?? 0.0; + var calcMaxWidth = _maxWidth ?? double.infinity; + var calcMinHeight = _minHeight ?? 0.0; + var calcMaxHeight = _maxHeight ?? double.infinity; + + void calcMinMaxWidth() { + if (_left != null && _right != null) { + calcMaxWidth = constraints.maxWidth - (_left! + _right!); + } else if ((_left != null && _right == null) || (_left == null && _right != null)) { + // make sure that the sum of left, right + maxwidth isn't bigger than the screen width. + var sideDelta = (_left ?? 0.0) + (_right ?? 0.0) + _outSidePadding!; + if (calcMaxWidth > constraints.maxWidth - sideDelta) { + calcMaxWidth = constraints.maxWidth - sideDelta; + } + } else { + if (calcMaxWidth > constraints.maxWidth - 2 * _outSidePadding!) { + calcMaxWidth = constraints.maxWidth - 2 * _outSidePadding!; + } + } + } + + void calcMinMaxHeight() { + if (_top != null && _bottom != null) { + calcMaxHeight = constraints.maxHeight - (_top! + _bottom!); + } else if ((_top != null && _bottom == null) || (_top == null && _bottom != null)) { + // make sure that the sum of top, bottom + maxHeight isn't bigger than the screen Height. + var sideDelta = (_top ?? 0.0) + (_bottom ?? 0.0) + _outSidePadding!; + if (calcMaxHeight > constraints.maxHeight - sideDelta) { + calcMaxHeight = constraints.maxHeight - sideDelta; + } + } else { + if (calcMaxHeight > constraints.maxHeight - 2 * _outSidePadding!) { + calcMaxHeight = constraints.maxHeight - 2 * _outSidePadding!; + } + } + } + + switch (_popupDirection) { + // + case TooltipDirection.down: + calcMinMaxWidth(); + if (_bottom != null) { + calcMinHeight = calcMaxHeight = constraints.maxHeight - _bottom! - _targetCenter!.dy; + } else { + calcMaxHeight = min((_maxHeight ?? constraints.maxHeight), constraints.maxHeight - _targetCenter!.dy) - _outSidePadding!; + } + break; + + case TooltipDirection.up: + calcMinMaxWidth(); + + if (_top != null) { + calcMinHeight = calcMaxHeight = _targetCenter!.dy - _top!; + } else { + calcMaxHeight = min((_maxHeight ?? constraints.maxHeight), _targetCenter!.dy) - _outSidePadding!; + } + break; + + case TooltipDirection.right: + calcMinMaxHeight(); + if (_right != null) { + calcMinWidth = calcMaxWidth = constraints.maxWidth - _right! - _targetCenter!.dx; + } else { + calcMaxWidth = min((_maxWidth ?? constraints.maxWidth), constraints.maxWidth - _targetCenter!.dx) - _outSidePadding!; + } + break; + + case TooltipDirection.left: + calcMinMaxHeight(); + if (_left != null) { + calcMinWidth = calcMaxWidth = _targetCenter!.dx - _left!; + } else { + calcMaxWidth = min((_maxWidth ?? constraints.maxWidth), _targetCenter!.dx) - _outSidePadding!; + } + break; + + default: + throw AssertionError(_popupDirection); + } + + var childConstraints = new BoxConstraints( + minWidth: calcMinWidth > calcMaxWidth ? calcMaxWidth : calcMinWidth, + maxWidth: calcMaxWidth, + minHeight: calcMinHeight > calcMaxHeight ? calcMaxHeight : calcMinHeight, + maxHeight: calcMaxHeight); + + // print("Child constraints: $childConstraints"); + + return childConstraints; + } + + @override + bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) { + return false; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +class _BubbleShape extends ShapeBorder { + final Offset? targetCenter; + final double arrowBaseWidth; + final double arrowTipDistance; + final double borderRadius; + final Color borderColor; + final double borderWidth; + final double? left, top, right, bottom; + final TooltipDirection popupDirection; + + _BubbleShape(this.popupDirection, this.targetCenter, this.borderRadius, this.arrowBaseWidth, this.arrowTipDistance, this.borderColor, + this.borderWidth, this.left, this.top, this.right, this.bottom); + + @override + EdgeInsetsGeometry get dimensions => new EdgeInsets.all(10.0); + + @override + Path getInnerPath(Rect rect, {TextDirection? textDirection}) { + return new Path() + ..fillType = PathFillType.evenOdd + ..addPath(getOuterPath(rect), Offset.zero); + } + + @override + Path getOuterPath(Rect rect, {TextDirection? textDirection}) { + // + late double topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius; + + Path _getLeftTopPath(Rect rect) { + return new Path() + ..moveTo(rect.left, rect.bottom - bottomLeftRadius) + ..lineTo(rect.left, rect.top + topLeftRadius) + ..arcToPoint(Offset(rect.left + topLeftRadius, rect.top), radius: new Radius.circular(topLeftRadius)) + ..lineTo(rect.right - topRightRadius, rect.top) + ..arcToPoint(Offset(rect.right, rect.top + topRightRadius), radius: new Radius.circular(topRightRadius), clockwise: true); + } + + Path _getBottomRightPath(Rect rect) { + return new Path() + ..moveTo(rect.left + bottomLeftRadius, rect.bottom) + ..lineTo(rect.right - bottomRightRadius, rect.bottom) + ..arcToPoint(Offset(rect.right, rect.bottom - bottomRightRadius), radius: new Radius.circular(bottomRightRadius), clockwise: false) + ..lineTo(rect.right, rect.top + topRightRadius) + ..arcToPoint(Offset(rect.right - topRightRadius, rect.top), radius: new Radius.circular(topRightRadius), clockwise: false); + } + + topLeftRadius = (left == 0 || top == 0) ? 0.0 : borderRadius; + topRightRadius = (right == 0 || top == 0) ? 0.0 : borderRadius; + bottomLeftRadius = (left == 0 || bottom == 0) ? 0.0 : borderRadius; + bottomRightRadius = (right == 0 || bottom == 0) ? 0.0 : borderRadius; + + switch (popupDirection) { + // + + case TooltipDirection.down: + return _getBottomRightPath(rect) + ..lineTo(min(max(targetCenter!.dx + arrowBaseWidth / 2, rect.left + borderRadius + arrowBaseWidth), rect.right - topRightRadius), + rect.top) + ..lineTo(targetCenter!.dx, targetCenter!.dy + arrowTipDistance) // up to arrow tip \ + ..lineTo(max(min(targetCenter!.dx - arrowBaseWidth / 2, rect.right - topLeftRadius - arrowBaseWidth), rect.left + topLeftRadius), + rect.top) // down / + + ..lineTo(rect.left + topLeftRadius, rect.top) + ..arcToPoint(Offset(rect.left, rect.top + topLeftRadius), radius: new Radius.circular(topLeftRadius), clockwise: false) + ..lineTo(rect.left, rect.bottom - bottomLeftRadius) + ..arcToPoint(Offset(rect.left + bottomLeftRadius, rect.bottom), radius: new Radius.circular(bottomLeftRadius), clockwise: false); + + case TooltipDirection.up: + return _getLeftTopPath(rect) + ..lineTo(rect.right, rect.bottom - bottomRightRadius) + ..arcToPoint(Offset(rect.right - bottomRightRadius, rect.bottom), radius: new Radius.circular(bottomRightRadius), clockwise: true) + ..lineTo( + min(max(targetCenter!.dx + arrowBaseWidth / 2, rect.left + bottomLeftRadius + arrowBaseWidth), + rect.right - bottomRightRadius), + rect.bottom) + + // up to arrow tip \ + ..lineTo(targetCenter!.dx, targetCenter!.dy - arrowTipDistance) + + // down / + ..lineTo( + max(min(targetCenter!.dx - arrowBaseWidth / 2, rect.right - bottomRightRadius - arrowBaseWidth), + rect.left + bottomLeftRadius), + rect.bottom) + ..lineTo(rect.left + bottomLeftRadius, rect.bottom) + ..arcToPoint(Offset(rect.left, rect.bottom - bottomLeftRadius), radius: new Radius.circular(bottomLeftRadius), clockwise: true) + ..lineTo(rect.left, rect.top + topLeftRadius) + ..arcToPoint(Offset(rect.left + topLeftRadius, rect.top), radius: new Radius.circular(topLeftRadius), clockwise: true); + + case TooltipDirection.left: + return _getLeftTopPath(rect) + ..lineTo(rect.right, + max(min(targetCenter!.dy - arrowBaseWidth / 2, rect.bottom - bottomRightRadius - arrowBaseWidth), rect.top + topRightRadius)) + ..lineTo(targetCenter!.dx - arrowTipDistance, targetCenter!.dy) // right to arrow tip \ + // left / + ..lineTo(rect.right, min(targetCenter!.dy + arrowBaseWidth / 2, rect.bottom - bottomRightRadius)) + ..lineTo(rect.right, rect.bottom - borderRadius) + ..arcToPoint(Offset(rect.right - bottomRightRadius, rect.bottom), radius: new Radius.circular(bottomRightRadius), clockwise: true) + ..lineTo(rect.left + bottomLeftRadius, rect.bottom) + ..arcToPoint(Offset(rect.left, rect.bottom - bottomLeftRadius), radius: new Radius.circular(bottomLeftRadius), clockwise: true); + + case TooltipDirection.right: + return _getBottomRightPath(rect) + ..lineTo(rect.left + topLeftRadius, rect.top) + ..arcToPoint(Offset(rect.left, rect.top + topLeftRadius), radius: new Radius.circular(topLeftRadius), clockwise: false) + ..lineTo(rect.left, + max(min(targetCenter!.dy - arrowBaseWidth / 2, rect.bottom - bottomLeftRadius - arrowBaseWidth), rect.top + topLeftRadius)) + + //left to arrow tip / + ..lineTo(targetCenter!.dx + arrowTipDistance, targetCenter!.dy) + + // right \ + ..lineTo(rect.left, min(targetCenter!.dy + arrowBaseWidth / 2, rect.bottom - bottomLeftRadius)) + ..lineTo(rect.left, rect.bottom - bottomLeftRadius) + ..arcToPoint(Offset(rect.left + bottomLeftRadius, rect.bottom), radius: new Radius.circular(bottomLeftRadius), clockwise: false); + + default: + throw AssertionError(popupDirection); + } + } + + @override + void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { + var paint = new Paint() + ..color = borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth; + + canvas.drawPath(getOuterPath(rect), paint); + paint = new Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth; + + if (right == 0.0) { + if (top == 0.0 && bottom == 0.0) { + canvas.drawPath( + new Path() + ..moveTo(rect.right, rect.top) + ..lineTo(rect.right, rect.bottom), + paint); + } else { + canvas.drawPath( + new Path() + ..moveTo(rect.right, rect.top + borderWidth / 2) + ..lineTo(rect.right, rect.bottom - borderWidth / 2), + paint); + } + } + if (left == 0.0) { + if (top == 0.0 && bottom == 0.0) { + canvas.drawPath( + new Path() + ..moveTo(rect.left, rect.top) + ..lineTo(rect.left, rect.bottom), + paint); + } else { + canvas.drawPath( + new Path() + ..moveTo(rect.left, rect.top + borderWidth / 2) + ..lineTo(rect.left, rect.bottom - borderWidth / 2), + paint); + } + } + if (top == 0.0) { + if (left == 0.0 && right == 0.0) { + canvas.drawPath( + new Path() + ..moveTo(rect.right, rect.top) + ..lineTo(rect.left, rect.top), + paint); + } else { + canvas.drawPath( + new Path() + ..moveTo(rect.right - borderWidth / 2, rect.top) + ..lineTo(rect.left + borderWidth / 2, rect.top), + paint); + } + } + if (bottom == 0.0) { + if (left == 0.0 && right == 0.0) { + canvas.drawPath( + new Path() + ..moveTo(rect.right, rect.bottom) + ..lineTo(rect.left, rect.bottom), + paint); + } else { + canvas.drawPath( + new Path() + ..moveTo(rect.right - borderWidth / 2, rect.bottom) + ..lineTo(rect.left + borderWidth / 2, rect.bottom), + paint); + } + } + } + + @override + ShapeBorder scale(double t) { + return new _BubbleShape( + popupDirection, targetCenter, borderRadius, arrowBaseWidth, arrowTipDistance, borderColor, borderWidth, left, top, right, bottom); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +class _ShapeOverlay extends ShapeBorder { + final Rect? clipRect; + final Color outsideBackgroundColor; + final ClipAreaShape clipAreaShape; + final double clipAreaCornerRadius; + + _ShapeOverlay(this.clipRect, this.clipAreaShape, this.clipAreaCornerRadius, this.outsideBackgroundColor); + + @override + EdgeInsetsGeometry get dimensions => new EdgeInsets.all(10.0); + + @override + Path getInnerPath(Rect rect, {TextDirection? textDirection}) { + return new Path()..addOval(clipRect!); + } + + @override + Path getOuterPath(Rect rect, {TextDirection? textDirection}) { + var outer = new Path()..addRect(rect); + + if (clipRect == null) { + return outer; + } + Path exclusion; + if (clipAreaShape == ClipAreaShape.oval) { + exclusion = new Path()..addOval(clipRect!); + } else { + exclusion = new Path() + ..moveTo(clipRect!.left + clipAreaCornerRadius, clipRect!.top) + ..lineTo(clipRect!.right - clipAreaCornerRadius, clipRect!.top) + ..arcToPoint(Offset(clipRect!.right, clipRect!.top + clipAreaCornerRadius), radius: new Radius.circular(clipAreaCornerRadius)) + ..lineTo(clipRect!.right, clipRect!.bottom - clipAreaCornerRadius) + ..arcToPoint(Offset(clipRect!.right - clipAreaCornerRadius, clipRect!.bottom), radius: new Radius.circular(clipAreaCornerRadius)) + ..lineTo(clipRect!.left + clipAreaCornerRadius, clipRect!.bottom) + ..arcToPoint(Offset(clipRect!.left, clipRect!.bottom - clipAreaCornerRadius), radius: new Radius.circular(clipAreaCornerRadius)) + ..lineTo(clipRect!.left, clipRect!.top + clipAreaCornerRadius) + ..arcToPoint(Offset(clipRect!.left + clipAreaCornerRadius, clipRect!.top), radius: new Radius.circular(clipAreaCornerRadius)) + ..close(); + } + + return Path.combine(ui.PathOperation.difference, outer, exclusion); + } + + @override + void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { + canvas.drawPath(getOuterPath(rect), new Paint()..color = outsideBackgroundColor); + } + + @override + ShapeBorder scale(double t) { + return new _ShapeOverlay(clipRect, clipAreaShape, clipAreaCornerRadius, outsideBackgroundColor); + } +} + +typedef FadeBuilder = Widget Function(BuildContext, double); + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +class _AnimationWrapper extends StatefulWidget { + final FadeBuilder? builder; + + _AnimationWrapper({this.builder}); + + @override + _AnimationWrapperState createState() => new _AnimationWrapperState(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +class _AnimationWrapperState extends State<_AnimationWrapper> { + double opacity = 0.0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance!.addPostFrameCallback((_) { + if (mounted) { + setState(() { + opacity = 1.0; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return widget.builder!(context, opacity); + } +} diff --git a/lib/main.dart b/lib/main.dart index d6fcc8a..0230544 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; +import 'package:aitrainer_app/bloc/tutorial/tutorial_bloc.dart'; import 'package:aitrainer_app/push_notifications.dart'; import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:aitrainer_app/repository/workout_tree_repository.dart'; @@ -46,9 +47,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:aitrainer_app/util/app_localization.dart'; +import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:sentry/sentry.dart'; -import 'package:smartlook/smartlook.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'bloc/account/account_bloc.dart'; import 'bloc/body_development/body_development_bloc.dart'; import 'bloc/development_by_muscle/development_by_muscle_bloc.dart'; @@ -60,7 +61,7 @@ import 'bloc/settings/settings_bloc.dart'; import 'bloc/timer/timer_bloc.dart'; import 'model/cache.dart'; -const dsn = 'https://5fac40cbfcfb4c15aa80c7a8638d7310@o418565.ingest.sentry.io/5322520'; +const dsn = 'https://0f635b7225564abc9089f8106f25eb5c@sentry.aitrainer.app/1'; /// Whether the VM is running in debug mode. /// @@ -88,6 +89,9 @@ Future _reportError(dynamic error, dynamic stackTrace) async { print('Reporting to Sentry.io...'); final String customerId = Cache().userLoggedIn != null ? Cache().userLoggedIn!.customerId.toString() : "0"; + Sentry.configureScope( + (scope) => scope.user = SentryUser(id: customerId), + ); final String platform = Platform.isAndroid ? "Android" : "iOS"; final String version = Cache().packageInfo != null ? Cache().packageInfo!.version + "+" + Cache().packageInfo!.buildNumber : ""; final sentryId = @@ -121,9 +125,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( + await SentryFlutter.init( (options) { options.dsn = dsn; + options.release = Cache().packageInfo != null ? Cache().packageInfo!.version + "+" + Cache().packageInfo!.buildNumber : ""; + options.enableAutoSessionTracking = true; }, ); final WorkoutTreeRepository menuTreeRepository = WorkoutTreeRepository(); @@ -164,6 +170,7 @@ Future main() async { BlocProvider( create: (BuildContext context) => TestSetExecuteBloc(), ), + BlocProvider(create: (BuildContext context) => TutorialBloc(tutorialName: ActivityDone.tutorialBasic.toStr())), ], child: WorkoutTestApp(), )); @@ -176,14 +183,18 @@ Future initFlurry() async { if (!isInDebugMode) { await Flurry.initialize(androidKey: "JNYCTCWBT34FM3J8TV36", iosKey: "3QBG7BSMGPDH24S8TRQP", enableLog: true); - SetupOptions options = (new SetupOptionsBuilder('682883e5cd71a46160c4f6ed070530ee593f49c6') + /* SetupOptions options = (new SetupOptionsBuilder('682883e5cd71a46160c4f6ed070530ee593f49c6') ..Fps = 2 ..StartNewSession = true) .build(); Smartlook.setupAndStartRecording(options); Smartlook.enableCrashlytics(true); - Smartlook.setEventTrackingMode(EventTrackingMode.FULL_TRACKING); + Smartlook.setEventTrackingMode(EventTrackingMode.FULL_TRACKING); */ + + FlutterUxcam.optIntoSchematicRecordings(); +// FlutterUxcam.optIntoVideoRecording(); + FlutterUxcam.startWithKey("wvdstyoml4tiwfd"); } } @@ -193,6 +204,7 @@ class WorkoutTestApp extends StatelessWidget { Widget build(BuildContext context) { SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); final FirebaseAnalytics analytics = FirebaseAnalytics(); + //facebookAppEvents.setAdvertiserTracking(enabled: true); initFlurry(); PushNotificationsManager().init(); diff --git a/lib/model/cache.dart b/lib/model/cache.dart index ee029f5..c9e7661 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/customer_activity.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'; @@ -13,6 +14,8 @@ import 'package:aitrainer_app/model/product.dart'; import 'package:aitrainer_app/model/product_test.dart'; import 'package:aitrainer_app/model/property.dart'; import 'package:aitrainer_app/model/purchase.dart'; +import 'package:aitrainer_app/model/sport.dart'; +import 'package:aitrainer_app/model/tutorial.dart'; import 'package:aitrainer_app/model/workout_menu_tree.dart'; import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:aitrainer_app/service/firebase_api.dart'; @@ -24,11 +27,11 @@ import 'package:aitrainer_app/util/env.dart'; import 'package:aitrainer_app/util/track.dart'; import 'package:flurry/flurry.dart'; import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; +import 'package:flutter_uxcam/flutter_uxcam.dart'; import 'package:package_info/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:aitrainer_app/model/exercise_type.dart'; import 'package:intl/intl.dart'; -import 'package:smartlook/smartlook.dart'; import 'customer_exercise_device.dart'; import 'exercise_device.dart'; @@ -55,6 +58,24 @@ enum SharePrefsChange { - is_logged_in */ +enum ActivityDone { tutorialBasic, tutorialDevelopment, isExerciseLogSeen, isMuscleDevelopmentSeen } + +extension ActivityDoneExt on ActivityDone { + String toStr() => this.toString().split(".").last; + bool equalsTo(ActivityDone value) => this.toString() == value.toString(); + bool equalsStringTo(String value) => this.toStr() == value; + + ActivityDone? searchByString(String activityString) { + ActivityDone? activity; + ActivityDone.values.forEach((element) { + if (element.equalsStringTo(activityString)) { + activity = element; + } + }); + return activity; + } +} + class Cache with Logging { static final Cache _singleton = Cache._internal(); @@ -73,8 +94,6 @@ class Cache with Logging { static final String activeExercisePlanKey = "active_exercise_plan"; static final String activeExercisePlanDateKey = "active_exercise_plan_date"; static final String activeExercisePlanDetailsKey = "active_exercise_details_plan"; - static final String exerciseLogSeenKey = "exercise_log_seen"; - static final String muscleDevelopmentSeenKey = "muscle_development_seen_key"; static String baseUrl = 'http://aitrainer.info:8888/api/'; static final String mediaUrl = 'https://aitrainer.info:4343/media/'; @@ -99,6 +118,7 @@ class Cache with Logging { List? _exercises; ExercisePlan? _myExercisePlan; List? _properties; + List? _sports; List? _products; List _purchases = []; List? _productTests; @@ -109,6 +129,8 @@ class Cache with Logging { List? _devices; List? _customerDevices; + List? _customerActivities; + List? _tutorials; LinkedHashMap _myExercisesPlanDetails = LinkedHashMap(); @@ -126,8 +148,8 @@ class Cache with Logging { late String testEnvironment; bool liveServer = true; bool? hasHardware = false; - bool? isExerciseLogSeen = false; - bool? isMuscleDevelopmentSeen = false; + + HashMap activitiesDone = HashMap(); factory Cache() { return _singleton; @@ -140,6 +162,10 @@ class Cache with Logging { baseUrl = 'http://aitrainer.app:8899/api/'; liveServer = false; } + + ActivityDone.values.forEach((element) { + activitiesDone[element.toStr()] = false; + }); } void setTestBaseUrl() { @@ -461,9 +487,11 @@ class Cache with Logging { } void setProperties(List properties) => this._properties = properties; - List? getProperties() => _properties; + List? getSports() => _sports; + void setSports(List sports) => this._sports = sports; + void setDevices(List devices) => this._devices = devices; List? getDevices() => this._devices; @@ -559,11 +587,11 @@ class Cache with Logging { setBadge("account", true); } if (this._exercises != null && this._exercises!.isNotEmpty) { - if (!isExerciseLogSeen!) { + if (!activitiesDone[ActivityDone.isExerciseLogSeen.toStr()]!) { setBadge("exerciseLog", true); setBadge("development", true); } - if (!isMuscleDevelopmentSeen!) { + if (!activitiesDone[ActivityDone.isMuscleDevelopmentSeen.toStr()]!) { setBadge("muscleDevelopment", true); setBadge("development", true); } @@ -589,14 +617,16 @@ class Cache with Logging { if (!isInDebugMode) { Flurry.setUserId(customerId.toString()); - Smartlook.setUserIdentifier(customerId.toString()); + //Smartlook.setUserIdentifier(customerId.toString()); + FlutterUxcam.setUserProperty("username", customerId.toString()); Track().track(TrackingEvent.enter); } - await setLoginTypeFromPrefs(); - await getActiveExercisePlan(); - await isExerciseLogSeenPrefs(); - await isMuscleDevelopmentSeenPrefs(); + await Future.forEach(ActivityDone.values, (element) async { + ActivityDone activity = element as ActivityDone; + await isActivityDonePrefs(activity); + }); + Cache().startPage = "home"; } @@ -609,41 +639,29 @@ class Cache with Logging { List get exercisePlanTemplates => this._exercisePlanTemplates; setExercisePlanTemplates(value) => this._exercisePlanTemplates = value; - setExerciseLogSeen() async { + isActivityDonePrefs(ActivityDone activity) async { Future prefs = SharedPreferences.getInstance(); SharedPreferences sharedPreferences = await prefs; - isExerciseLogSeen = true; - sharedPreferences.setBool(Cache.exerciseLogSeenKey, true); - } - - Future isExerciseLogSeenPrefs() async { - Future prefs = SharedPreferences.getInstance(); - SharedPreferences sharedPreferences = await prefs; - isExerciseLogSeen = sharedPreferences.getBool(Cache.exerciseLogSeenKey); - if (isExerciseLogSeen == null) { - isExerciseLogSeen = false; + if (sharedPreferences.getBool(activity.toStr()) != null) { + activitiesDone[activity.toStr()] = sharedPreferences.getBool(activity.toStr())!; } - //print("ExerciseLogSeen $isExerciseLogSeen"); - return isExerciseLogSeen!; + + return activitiesDone[activity.toStr()]!; } - setMuscleDevelopmentSeen() async { + setActivityDonePrefs(ActivityDone activity) async { Future prefs = SharedPreferences.getInstance(); SharedPreferences sharedPreferences = await prefs; - isMuscleDevelopmentSeen = true; - sharedPreferences.setBool(Cache.muscleDevelopmentSeenKey, true); - } - - Future isMuscleDevelopmentSeenPrefs() async { - Future prefs = SharedPreferences.getInstance(); - SharedPreferences sharedPreferences = await prefs; - isMuscleDevelopmentSeen = sharedPreferences.getBool(Cache.muscleDevelopmentSeenKey); - if (isMuscleDevelopmentSeen == null) { - isMuscleDevelopmentSeen = false; - } - return isMuscleDevelopmentSeen!; + activitiesDone[activity.toStr()] = true; + sharedPreferences.setBool(activity.toStr(), true); } List? get evaluations => this._evaluations; set evaluations(List? value) => this._evaluations = value; + + List? get customerActivities => this._customerActivities; + setCustomerActivities(List? value) => this._customerActivities = value; + + List? get tutorials => this._tutorials; + setTutorials(List? value) => this._tutorials = value; } diff --git a/lib/model/customer.dart b/lib/model/customer.dart index 521f05f..edd4a33 100644 --- a/lib/model/customer.dart +++ b/lib/model/customer.dart @@ -22,6 +22,8 @@ class Customer { String? firebaseUid; DateTime? dateAdd; DateTime? dateChange; + int? emailSubscription; + int? sportId; LinkedHashMap properties = LinkedHashMap(); @@ -65,6 +67,10 @@ class Customer { this.trainer = json['trainer']; this.firebaseUid = json['firebaseUid']; + this.dataPolicyAllowed = json['dataPolicyAllowed']; + this.emailSubscription = json['emailSubscription']; + this.sportId = json['sportId']; + this.dateAdd = json['dateAdd'] == null ? DateTime.parse("0000-00-00") : DateTime.parse(json['dateAdd']); this.dateChange = json['dateChange'] == null ? DateTime.parse("0000-00-00") : DateTime.parse(json['dateChange']); } @@ -86,6 +92,8 @@ class Customer { "dataPolicyAllowed": dataPolicyAllowed, "dateAdd": DateFormat('yyyy-MM-dd HH:mm:ss').format(this.dateAdd!), "dateChange": DateFormat('yyyy-MM-dd HH:mm:ss').format(this.dateChange!), + "emailSubscription": this.emailSubscription, + "sportId": this.sportId }; double getProperty(String propertyName) { diff --git a/lib/model/customer_activity.dart b/lib/model/customer_activity.dart new file mode 100644 index 0000000..5960f80 --- /dev/null +++ b/lib/model/customer_activity.dart @@ -0,0 +1,30 @@ +import 'package:intl/intl.dart'; + +class CustomerActivity { + late int activityId; + late int customerId; + late String type; + late DateTime? dateAdd; + bool? skipped; + + CustomerActivity.fromJson(Map json) { + activityId = json['activityId']; + customerId = json['custoemrId']; + type = json['type']; + skipped = json['skipped']; + this.dateAdd = DateTime.parse(json['dateAdd']); + } + + Map toJson() => { + 'activityId': this.activityId, + 'customerId': this.customerId, + 'type': this.type, + 'skipped': this.skipped, + "dateAdd": DateFormat('yyyy-MM-dd HH:mm:ss').format(this.dateAdd!), + }; + + @override + String toString() { + return this.toJson().toString(); + } +} diff --git a/lib/model/fitness_state.dart b/lib/model/fitness_state.dart index 798b987..e24c109 100644 --- a/lib/model/fitness_state.dart +++ b/lib/model/fitness_state.dart @@ -1,23 +1,3 @@ -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/sport.dart b/lib/model/sport.dart new file mode 100644 index 0000000..fc41fd2 --- /dev/null +++ b/lib/model/sport.dart @@ -0,0 +1,22 @@ +class Sport { + late int sportId; + late String name; + late String sportNameTranslation; + + Sport.fromJson(Map json) { + this.sportId = json['sportId']; + this.name = json['name']; + this.sportNameTranslation = + json['translations'] != null && (json['translations']).length > 0 ? json['translations'][0]['sportName'] : this.name; + } + + Map toJson() => { + "sportId": sportId, + "name": name, + }; + + @override + String toString() { + return this.toJson().toString(); + } +} diff --git a/lib/model/tutorial.dart b/lib/model/tutorial.dart new file mode 100644 index 0000000..2a2204e --- /dev/null +++ b/lib/model/tutorial.dart @@ -0,0 +1,24 @@ +import 'package:aitrainer_app/model/tutorial_step.dart'; + +enum TutorialEnum { basic, development, training } + +class Tutorial { + late int tutorialId; + late String name; + + List? steps; + + Tutorial.fromJson(Map json) { + this.tutorialId = json['tutorialId']; + this.name = json['name']; + + if (json['steps'] != null && json['steps'].length > 0) { + steps = json['steps'].map((step) => TutorialStep.fromJson(step)).toList(); + } + } + + Map toJson() => {'tutorialId': this.tutorialId, 'name': this.name, 'steps': steps.toString()}; + + @override + String toString() => this.toJson().toString(); +} diff --git a/lib/model/tutorial_step.dart b/lib/model/tutorial_step.dart new file mode 100644 index 0000000..7ec14f5 --- /dev/null +++ b/lib/model/tutorial_step.dart @@ -0,0 +1,99 @@ +import 'dart:ui'; +import 'dart:convert'; + +import 'package:aitrainer_app/util/app_language.dart'; + +enum TutorialEnum { basic, development, training } + +class TutorialStepAction { + late String direction; + late int top; + late int left; + late bool showBubble; + late int bubbleX; + late int bubbleY; + late int bubbleWidth; + late int bubbleHeight; + late bool showCheckText; + late int parent; + + TutorialStepAction.fromJson(Map json) { + this.direction = json['direction']; + this.top = json['top']; + this.left = json['left']; + this.showBubble = json['show_bubble']; + this.bubbleX = json['bubble_x']; + this.bubbleY = json['bubble_y']; + this.bubbleWidth = json['bubble_width']; + this.bubbleHeight = json['bubble_height']; + this.showCheckText = json['show_check_text']; + this.parent = json['parent']; + } + + Map toJson() => { + "direction": this.direction, + "top": this.top, + "left": this.left, + "showBubble": this.showBubble, + "bubbleX": this.bubbleX, + "bubbleY": this.bubbleY, + "bubbleWidth": this.bubbleWidth, + "bubbleHeight": this.bubbleHeight, + "showCheckText": this.showCheckText, + "parent": this.parent, + }; + + @override + String toString() => this.toJson().toString(); +} + +class TutorialStep { + int? tutorialStepId; + int? tutorialId; + int? step; + String? tutorialText; + String? direction; + String? checkText; + String? condition; + String? branch; + int? parentId; + TutorialStepAction? action; + + String? tutorialTextTranslation; + String? errorTextTranslation; + + TutorialStep.fromJson(Map json) { + this.tutorialStepId = json['tutorialStepId']; + this.tutorialId = json['tutorialId']; + this.step = json['step']; + this.tutorialText = json['tutorialText']; + this.checkText = json['checkText']; + this.condition = json['condition']; + if (this.condition != null) { + this.condition = condition!.replaceAll(r'\\', "replace"); + print("Json condition $condition"); + this.action = TutorialStepAction.fromJson(jsonDecode(condition!)); + } + + if (json['translations'] != null && json['translations'].length > 0) { + this.tutorialTextTranslation = + AppLanguage().appLocal == Locale('hu') ? json['translations'][0]['tutorialText'] : json['tutorialText']; + this.errorTextTranslation = AppLanguage().appLocal == Locale('hu') ? json['translations'][0]['errorText'] : json['errorText']; + } + } + + Map toJson() => { + "tutorialStepId": this.tutorialStepId, + "tutorialId": this.tutorialId, + "step": this.step, + "tutorialText": this.tutorialText, + "checkText": this.checkText, + "tutorialTextTranslation": this.tutorialTextTranslation, + "errorTextTranslation": this.errorTextTranslation, + "condition": this.condition, + "action": this.action != null ? this.action!.toJson() : "" + }; + + @override + String toString() => this.toJson().toString(); +} diff --git a/lib/repository/customer_repository.dart b/lib/repository/customer_repository.dart index ce1ed93..bd71ee4 100644 --- a/lib/repository/customer_repository.dart +++ b/lib/repository/customer_repository.dart @@ -6,6 +6,7 @@ import 'package:aitrainer_app/model/customer_property.dart'; import 'package:aitrainer_app/model/product_test.dart'; import 'package:aitrainer_app/model/property.dart'; import 'package:aitrainer_app/model/purchase.dart'; +import 'package:aitrainer_app/model/sport.dart'; import 'package:aitrainer_app/repository/property_repository.dart'; import 'package:aitrainer_app/service/customer_service.dart'; import 'package:aitrainer_app/service/logging.dart'; @@ -90,6 +91,36 @@ class CustomerRepository with Logging { return this.customer!.goal; } + String? getSportString() { + if (this.customer == null) throw Exception("Initialize the customer object"); + String? sport; + List? sports = Cache().getSports(); + if (sports != null) { + for (Sport sportObject in sports) { + if (sportObject.sportId == this.customer!.sportId) { + sport = sportObject.name; + break; + } + } + } + return sport; + } + + Sport? getSport() { + if (this.customer == null) throw Exception("Initialize the customer object"); + Sport? sport; + List? sports = Cache().getSports(); + if (sports != null) { + for (Sport sportObject in sports) { + if (sportObject.sportId == this.customer!.sportId) { + sport = sportObject; + break; + } + } + } + return sport; + } + String? get fitnessLevel { if (this.customer == null) throw Exception("Initialize the customer object"); return this.customer!.fitnessLevel; @@ -189,6 +220,19 @@ class CustomerRepository with Logging { this.customer!.goal = goal; } + setSportString(String selectedSport) { + if (this.customer == null) throw Exception("Initialize the customer object"); + List? sports = Cache().getSports(); + if (sports != null) { + for (Sport sportObject in sports) { + if (sportObject.name == selectedSport) { + this.customer!.sportId = sportObject.sportId; + break; + } + } + } + } + setBodyType(String bodyType) { if (this.customer == null) throw Exception("Initialize the customer object"); this.customer!.bodyType = bodyType; diff --git a/lib/repository/exercise_repository.dart b/lib/repository/exercise_repository.dart index e066cfd..44387c6 100644 --- a/lib/repository/exercise_repository.dart +++ b/lib/repository/exercise_repository.dart @@ -74,6 +74,9 @@ class ExerciseRepository { Exercise? getExercise() => this.exercise; Future addExercise() async { + if (this.customer == null) { + throw Exception("Please log in"); + } final Exercise modelExercise = this.exercise!; modelExercise.customerId = this.customer!.customerId; modelExercise.exerciseTypeId = this.exerciseType!.exerciseTypeId; diff --git a/lib/service/api.dart b/lib/service/api.dart index 7a949ec..b1f5b88 100644 --- a/lib/service/api.dart +++ b/lib/service/api.dart @@ -59,16 +59,17 @@ class APIClient with Common, Logging { final responseCode = response.statusCode; if (responseCode != 200) { trace("authentication response: $responseCode"); - return { + throw Exception("Network error, try again later!"); + /* return { "error": "Authentication error, total failure", - }; + }; */ } final responseJson = json.decode(response.body); return responseJson; } catch (exception) { print(exception.toString()); - return {"error": "Network error, try again later " + exception.toString()}; + throw Exception("Network error, try again later!"); } } diff --git a/lib/service/package_service.dart b/lib/service/package_service.dart index 07fefe1..d08ce1a 100644 --- a/lib/service/package_service.dart +++ b/lib/service/package_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/model/customer.dart'; +import 'package:aitrainer_app/model/customer_activity.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'; @@ -15,10 +16,12 @@ import 'package:aitrainer_app/model/product.dart'; import 'package:aitrainer_app/model/product_test.dart'; import 'package:aitrainer_app/model/property.dart'; import 'package:aitrainer_app/model/purchase.dart'; +import 'package:aitrainer_app/model/tutorial.dart'; import 'package:aitrainer_app/service/api.dart'; import 'package:aitrainer_app/service/exercise_type_service.dart'; import 'package:aitrainer_app/util/not_found_exception.dart'; +import '../model/sport.dart'; import 'customer_service.dart'; import 'exercise_tree_service.dart'; @@ -63,6 +66,14 @@ class PackageApi { } else if (headRecord[0] == "Evaluation") { final List evaluations = json.map((evaluation) => Evaluation.fromJson(evaluation)).toList(); Cache().evaluations = evaluations; + } else if (headRecord[0] == "Sport") { + final List sports = json.map((sport) => Sport.fromJson(sport)).toList(); + Cache().setSports(sports); + } else if (headRecord[0] == "Tutorial") { + final Iterable json = jsonDecode(headRecord[1]); + final List tutorials = json.map((tutorial) => Tutorial.fromJson(tutorial)).toList(); + print("Tutorial: $tutorials"); + Cache().setTutorials(tutorials); } }); @@ -145,6 +156,10 @@ class PackageApi { return item; }).toList(); // ToDo */ + } else if (headRecord[0] == "CustomerActivity") { + final Iterable json = jsonDecode(headRecord[1]); + final List customerActivities = json.map((activity) => CustomerActivity.fromJson(activity)).toList(); + Cache().setCustomerActivities(customerActivities); } }); } on NotFoundException catch (_) { diff --git a/lib/service/property_service.dart b/lib/service/property_service.dart index 7742a6d..a5f4a4b 100644 --- a/lib/service/property_service.dart +++ b/lib/service/property_service.dart @@ -1,9 +1,10 @@ import 'dart:convert'; import 'package:aitrainer_app/model/cache.dart'; -import 'package:aitrainer_app/model/property.dart'; import 'package:aitrainer_app/service/api.dart'; +import '../model/property.dart'; + class PropertyApi { final APIClient _client = new APIClient(); diff --git a/lib/service/sport_service.dart b/lib/service/sport_service.dart new file mode 100644 index 0000000..a725ff6 --- /dev/null +++ b/lib/service/sport_service.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; + +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/service/api.dart'; + +import '../model/sport.dart'; + +class SportApi { + final APIClient _client = new APIClient(); + + Future> getSports() async { + final body = await _client.get("sports/", ""); + final Iterable json = jsonDecode(body); + final List sports = json.map((sport) => Sport.fromJson(sport)).toList(); + Cache().setSports(sports); + return sports; + } +} diff --git a/lib/util/session.dart b/lib/util/session.dart index a514f22..da96042 100644 --- a/lib/util/session.dart +++ b/lib/util/session.dart @@ -8,6 +8,7 @@ import 'package:aitrainer_app/service/package_service.dart'; import 'package:aitrainer_app/util/purchases.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:package_info/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:aitrainer_app/model/cache.dart'; @@ -31,6 +32,10 @@ class Session with Logging { Cache().getHardware(_sharedPreferences); await _fetchToken(_sharedPreferences); await RevenueCatPurchases().initPlatform(); + bool res = await FlutterAppBadger.isAppBadgeSupported(); + if (res == true) { + FlutterAppBadger.removeBadge(); + } } } diff --git a/lib/util/track.dart b/lib/util/track.dart index 5105f0d..969058f 100644 --- a/lib/util/track.dart +++ b/lib/util/track.dart @@ -5,7 +5,7 @@ import 'package:aitrainer_app/service/tracking_service.dart'; import 'package:aitrainer_app/util/enums.dart'; import 'package:aitrainer_app/model/tracking.dart' as model; import 'package:flurry/flurry.dart'; -import 'package:smartlook/smartlook.dart'; +import 'package:flutter_uxcam/flutter_uxcam.dart'; class Track with Logging { static final Track _singleton = Track._internal(); @@ -19,7 +19,8 @@ class Track with Logging { void track(TrackingEvent event, {String eventValue = ""}) { if (!isInDebugMode) { Flurry.logEvent(event.toString()); - Smartlook.setGlobalEventProperty(event.toString(), eventValue, false); +// Smartlook.setGlobalEventProperty(event.toString(), eventValue, false); + FlutterUxcam.logEventWithProperties(event.enumToString(), {"value": eventValue}); model.Tracking tracking = model.Tracking(); tracking.customerId = Cache().userLoggedIn == null ? 0 : Cache().userLoggedIn!.customerId!; tracking.event = event.enumToString(); diff --git a/lib/view/account.dart b/lib/view/account.dart index da7a2b0..bdf5169 100644 --- a/lib/view/account.dart +++ b/lib/view/account.dart @@ -107,7 +107,7 @@ class AccountPage extends StatelessWidget with Trans { ), ListTile( leading: Common.badgedIcon(Colors.grey, Icons.perm_contact_cal, "FitnessLevel"), //Icon(Icons.perm_contact_cal), - subtitle: Text(t("Activity")), + subtitle: Text(t("Activity") + " " + t("and") + " " + t("Sport")), title: TextButton( child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(fitnessLevel, style: TextStyle(color: Colors.blue)), diff --git a/lib/view/customer_fitness_page.dart b/lib/view/customer_fitness_page.dart index b626c7d..21cde39 100644 --- a/lib/view/customer_fitness_page.dart +++ b/lib/view/customer_fitness_page.dart @@ -1,6 +1,8 @@ import 'dart:collection'; import 'package:aitrainer_app/bloc/customer_change/customer_change_bloc.dart'; +import 'package:aitrainer_app/model/cache.dart'; +import 'package:aitrainer_app/model/sport.dart'; import 'package:aitrainer_app/util/app_localization.dart'; import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:aitrainer_app/model/fitness_state.dart'; @@ -82,10 +84,9 @@ class _CustomerFitnessPageState extends State with Trans { Text( t("Your Fitness State"), textAlign: TextAlign.center, - style: TextStyle( + style: GoogleFonts.archivoBlack( color: Colors.orange, - fontSize: 42, - fontFamily: 'Arial', + fontSize: 30, fontWeight: FontWeight.w900, ), ) @@ -218,7 +219,15 @@ class _CustomerFitnessPageState extends State with Trans { }), }), Divider(), - selected == FitnessState.professional ? getSport(changeBloc) : Offstage(), + Text( + t("Your Primary Sport") + ":", + textAlign: TextAlign.center, + style: GoogleFonts.archivoBlack( + color: Colors.orange, + fontSize: 20, + ), + ), + getSport(changeBloc), Divider(), ElevatedButton( style: ElevatedButton.styleFrom( @@ -253,7 +262,7 @@ class _CustomerFitnessPageState extends State with Trans { } Widget getSport(CustomerChangeBloc bloc) { - Sport? selected = bloc.getSelectedSport; + Sport? selected = bloc.selectedSport; return Container( padding: EdgeInsets.only(left: 65, right: 65), child: DropdownSearch( @@ -270,10 +279,16 @@ class _CustomerFitnessPageState extends State with Trans { ), ), mode: Mode.MENU, - compareFn: (Sport i, Sport s) => i.equalsTo(s), + compareFn: (Sport? i, Sport? s) { + if (i == null || s == null) { + return false; + } else { + return i.sportId == s.sportId; + } + }, showSelectedItem: true, selectedItem: selected, - itemAsString: (data) => t(data.toStr()), + itemAsString: (data) => t(data.sportNameTranslation), onChanged: (data) { bloc.add(CustomerSportChange(sport: data)); }, @@ -281,7 +296,7 @@ class _CustomerFitnessPageState extends State with Trans { popupItemBuilder: _customMenuBuilder, popupBarrierColor: Colors.white10, //popupBackgroundColor: Colors.yellow, - items: Sport.values, + items: Cache().getSports(), dropDownButton: Icon( Icons.arrow_drop_down, color: Colors.indigo, @@ -291,7 +306,6 @@ class _CustomerFitnessPageState extends State with Trans { } Widget _customMenuBuilder(BuildContext context, Sport sport, bool isSelected) { - //bool selected = bloc.getSelectedSport; return Container( decoration: !isSelected ? BoxDecoration(color: Colors.grey[300]) @@ -303,11 +317,11 @@ class _CustomerFitnessPageState extends State with Trans { child: ListTile( selected: isSelected, title: Text( - t(sport.toStr()), + t(sport.sportNameTranslation), style: GoogleFonts.archivoBlack(fontSize: 20, color: Colors.blue[600]), ), subtitle: Text( - t(sport.description(sport)), + t(sport.name), style: GoogleFonts.inter(fontSize: 12, color: Colors.blue[600]), ), ), @@ -327,11 +341,11 @@ class _CustomerFitnessPageState extends State with Trans { : ListTile( contentPadding: EdgeInsets.all(0), title: Text( - t(item.toStr()), + t(item.sportNameTranslation), style: GoogleFonts.archivoBlack(fontSize: 20, color: Colors.blue[600]), ), subtitle: Text( - t(item.description(item)), + t(item.name), 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 b4e0c22..f021867 100644 --- a/lib/view/customer_goal_page.dart +++ b/lib/view/customer_goal_page.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'package:aitrainer_app/bloc/customer_change/customer_change_bloc.dart'; -import 'package:aitrainer_app/util/app_localization.dart'; +import 'package:aitrainer_app/library/custom_icon_icons.dart'; import 'package:aitrainer_app/repository/customer_repository.dart'; import 'package:aitrainer_app/util/trans.dart'; import 'package:aitrainer_app/widgets/app_bar_min.dart'; @@ -11,9 +11,45 @@ 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"; - static String weight = "weight_loss"; +enum Goals { gain_muscle, weight_loss, endurance, muscle_endurance, flexibility, gain_strength, explosiveness, shape_forming } + +extension GoalsExt on Goals { + String toStr() => this.toString().split(".").last; + bool equalsTo(Goals goal) => this.toString() == goal.toString(); + bool equalsStringTo(String goal) => this.toStr() == goal; + + String description(Goals goal) { + switch (goal) { + case Goals.endurance: + return "Endurance"; + case Goals.weight_loss: + return "Loss Weight"; + case Goals.gain_muscle: + return "Gain Muscle"; + case Goals.gain_strength: + return "Gain Strength"; + case Goals.muscle_endurance: + return "Muscle Endurance"; + case Goals.flexibility: + return "Flexibility"; + case Goals.explosiveness: + return "Explosiveness"; + case Goals.shape_forming: + return "Shape Forming"; + default: + return "Gain Muscle"; + } + } + + Goals getGoal(Goals goal) { + Goals selected = Goals.gain_muscle; + Goals.values.forEach((element) { + if (goal.equalsTo(element)) { + selected = element; + } + }); + return selected; + } } // ignore: must_be_immutable @@ -25,6 +61,7 @@ class CustomerGoalPage extends StatefulWidget { class _CustomerGoalPage extends State with Trans { String? selected; bool fulldata = false; + late CustomerChangeBloc changeBloc; @override Widget build(BuildContext context) { @@ -47,107 +84,135 @@ class _CustomerGoalPage extends State with Trans { } return Scaffold( - appBar: _bar, - body: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('asset/image/WT_light_background.jpg'), - fit: BoxFit.cover, - alignment: Alignment.center, - ), + appBar: _bar, + body: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('asset/image/WT_light_background.jpg'), + fit: BoxFit.cover, + alignment: Alignment.center, ), - height: double.infinity, - width: double.infinity, - child: BlocProvider( - create: (context) => CustomerChangeBloc(customerRepository: customerRepository), - child: Builder(builder: (context) { - CustomerChangeBloc changeBloc = BlocProvider.of(context); + ), + height: double.infinity, + width: double.infinity, + child: BlocProvider( + create: (context) => CustomerChangeBloc(customerRepository: customerRepository), + child: Builder(builder: (context) { + changeBloc = BlocProvider.of(context); - return SingleChildScrollView( - child: Center( - child: Column( - children: [ - Divider(), - Wrap(alignment: WrapAlignment.center, children: [ - Text( - AppLocalizations.of(context)!.translate("Set Your Goals"), - style: GoogleFonts.archivoBlack( - color: Colors.orange, - fontSize: 42, - fontWeight: FontWeight.w900, - ), + return SingleChildScrollView( + child: Center( + child: Column( + children: [ + Divider(), + Wrap(alignment: WrapAlignment.center, children: [ + Text( + t("Set Your Primary Goal"), + maxLines: 2, + style: GoogleFonts.archivoBlack( + color: Colors.orange, + fontSize: 30, + shadows: [ + Shadow( + offset: Offset(2.0, 2.0), + blurRadius: 3.0, + color: Colors.black87, + ), + ], ), - ]), - Divider(), - Stack(alignment: Alignment.bottomLeft, children: [ - TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.all(0.0), - shape: getShape(changeBloc, GoalsItem.muscle), - ), - child: Image.asset( - "asset/image/Gain_muscle.jpg", - height: 180, - ), - onPressed: () => { - setState(() { - selected = GoalsItem.muscle; - changeBloc.add(CustomerGoalChange(goal: GoalsItem.muscle)); - }), - }), - InkWell( - child: Text( - AppLocalizations.of(context)!.translate("Gain Muscle"), - style: TextStyle(color: Colors.white, fontSize: 32, fontFamily: 'Arial', fontWeight: FontWeight.w900), - ), - highlightColor: Colors.white, - ) - ]), - Divider(), - Stack(alignment: Alignment.bottomLeft, children: [ - TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.all(0.0), - shape: getShape(changeBloc, GoalsItem.weight), - ), - child: Image.asset( - "asset/image/WT_weight_loss.jpg", - height: 180, - ), - onPressed: () => { - setState(() { - selected = GoalsItem.muscle; - changeBloc.add(CustomerGoalChange(goal: GoalsItem.weight)); - }), - }), - InkWell( - child: Text( - AppLocalizations.of(context)!.translate("Loose Weight"), - style: TextStyle(color: Colors.white, fontSize: 32, fontFamily: 'Arial', fontWeight: FontWeight.w900), - ), - highlightColor: Colors.white, - ) - ]), - Divider(), - ElevatedButton( - style: ElevatedButton.styleFrom( - onPrimary: Colors.white, - primary: Colors.orange, - ), - child: Text(fulldata ? t("Save") : t("Next")), - onPressed: () => { - //changingViewModel.saveCustomer(), - changeBloc.add(CustomerSave()), - Navigator.of(context).pop(), - if (!fulldata) {Navigator.of(context).pushNamed("customerFitnessPage", arguments: changeBloc.customerRepository)} - }, - ) - ], - ), - )); - }), + ), + ]), + Divider(), + getItem(changeBloc, Goals.gain_muscle), + Divider(), + getItem(changeBloc, Goals.weight_loss), + Divider(), + getItem(changeBloc, Goals.shape_forming), + Divider(), + getItem(changeBloc, Goals.endurance), + Divider(), + getItem(changeBloc, Goals.gain_strength), + Divider(), + getItem(changeBloc, Goals.muscle_endurance), + Divider(), + getItem(changeBloc, Goals.flexibility), + Divider(), + getItem(changeBloc, Goals.explosiveness), + Divider(), + /* ElevatedButton( + style: ElevatedButton.styleFrom( + onPrimary: Colors.white, + primary: Colors.orange, + ), + child: Text(fulldata ? t("Save") : t("Next")), + onPressed: () => { + //changingViewModel.saveCustomer(), + changeBloc.add(CustomerSave()), + Navigator.of(context).pop(), + if (!fulldata) {Navigator.of(context).pushNamed("customerFitnessPage", arguments: changeBloc.customerRepository)} + }, + ) */ + ], + ), + )); + }), + ), + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => { + //changingViewModel.saveCustomer(), + changeBloc.add(CustomerSave()), + Navigator.of(context).pop(), + if (!fulldata) {Navigator.of(context).pushNamed("customerFitnessPage", arguments: changeBloc.customerRepository)} + }, + backgroundColor: Colors.orange[800], + icon: Icon( + CustomIcon.save, + size: 20, + ), + label: Text( + fulldata ? t("Save") : t("Next"), + style: GoogleFonts.inter(fontWeight: FontWeight.bold, fontSize: 12), + ), + ), + ); + } + + Widget getItem(CustomerChangeBloc changeBloc, Goals goal) { + return Stack(alignment: Alignment.bottomLeft, children: [ + TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.all(0.0), + shape: getShape(changeBloc, goal.toStr()), ), - )); + child: Image.asset( + "asset/image/" + goal.toStr() + ".jpg", + height: 180, + ), + onPressed: () => { + setState(() { + selected = goal.toStr(); + changeBloc.add(CustomerGoalChange(goal: goal.toStr())); + }), + }), + Container( + padding: EdgeInsets.only(bottom: 5, left: 10), + child: Text( + t(goal.description(goal)), + style: GoogleFonts.archivoBlack( + color: Colors.yellow[300], + fontSize: 28, + shadows: [ + Shadow( + offset: Offset(2.0, 2.0), + blurRadius: 5.0, + color: Colors.black87, + ), + ], + ), + ), + ) + ]); } dynamic getShape(CustomerChangeBloc customerBloc, String goal) { diff --git a/lib/view/customer_modify_page.dart b/lib/view/customer_modify_page.dart index 45643fe..8adbda7 100644 --- a/lib/view/customer_modify_page.dart +++ b/lib/view/customer_modify_page.dart @@ -126,37 +126,6 @@ class CustomerModifyPage extends StatelessWidget with Trans { Divider( color: Colors.transparent, ), - /* Cache().getLoginType() == LoginType.email - ? TextFormField( - key: LibraryKeys.loginPasswordField, - obscureText: true, - decoration: InputDecoration( - labelStyle: TextStyle(fontSize: 14), - contentPadding: EdgeInsets.only(left: 15, top: 15, bottom: 15), - suffixIcon: IconButton( - onPressed: () => {customerBloc.add(CustomerChangePasswordObscure())}, - icon: Icon(Icons.remove_red_eye), - ), - labelText: t('Password (Leave empty if no change)'), - fillColor: Colors.white24, - filled: true, - border: OutlineInputBorder( - gapPadding: 1.0, - borderRadius: BorderRadius.circular(12.0), - borderSide: BorderSide(color: Colors.green[50]!, width: 0.4), - ), - ), - initialValue: customerBloc.customerRepository.customer!.password, - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (val) { - String? validator = customerBloc.passwordValidation(val); - return validator == null ? null : t(validator); - }, - keyboardType: TextInputType.visiblePassword, - style: new TextStyle(fontSize: 16, color: Colors.indigo), - onChanged: (value) => {customerBloc.add(CustomerPasswordChange(password: value))}) - ) - : Offstage(), */ Divider( color: Colors.transparent, ), diff --git a/lib/view/evaluation_page.dart b/lib/view/evaluation_page.dart index f498b36..4821960 100644 --- a/lib/view/evaluation_page.dart +++ b/lib/view/evaluation_page.dart @@ -1,6 +1,8 @@ import 'dart:collection'; import 'dart:ui'; +import 'package:aitrainer_app/bloc/tutorial/tutorial_bloc.dart'; import 'package:aitrainer_app/util/enums.dart'; +import 'package:aitrainer_app/widgets/tutorial_widget.dart'; import 'package:intl/intl.dart'; import 'package:aitrainer_app/bloc/result/result_bloc.dart'; import 'package:aitrainer_app/util/app_language.dart'; @@ -56,6 +58,12 @@ class EvaluationPage extends StatelessWidget with Trans { imageUrl = 'asset/image/WT_Results_for_runners.jpg'; } + final TutorialBloc tutorialBloc = BlocProvider.of(context); + print("Evaluation page tutorial isActive? ${tutorialBloc.isActive}"); + if (tutorialBloc.isActive == false) { + TutorialWidget().close(); + } + setContext(context); return Scaffold( appBar: AppBarMin( diff --git a/lib/view/exercise_control_page.dart b/lib/view/exercise_control_page.dart index 5de4389..d4d8706 100644 --- a/lib/view/exercise_control_page.dart +++ b/lib/view/exercise_control_page.dart @@ -229,6 +229,9 @@ class _ExerciseControlPage extends State with Trans { numberPickForm(exerciseBloc, 2), Divider(), numberPickForm(exerciseBloc, 3), + SizedBox( + height: 80, + ) ]), )), TimerWidget( diff --git a/lib/view/exercise_new_page.dart b/lib/view/exercise_new_page.dart index cafb2d5..f1350ff 100644 --- a/lib/view/exercise_new_page.dart +++ b/lib/view/exercise_new_page.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:aitrainer_app/bloc/exercise_new/exercise_new_bloc.dart'; import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; import 'package:aitrainer_app/bloc/test_set_execute/test_set_execute_bloc.dart'; +import 'package:aitrainer_app/bloc/tutorial/tutorial_bloc.dart'; import 'package:aitrainer_app/library/custom_icon_icons.dart'; import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/model/exercise_ability.dart'; @@ -15,6 +16,7 @@ import 'package:aitrainer_app/widgets/app_bar.dart'; import 'package:aitrainer_app/widgets/bmi_widget.dart'; import 'package:aitrainer_app/widgets/bmr_widget.dart'; import 'package:aitrainer_app/widgets/bottom_bar_multiple_exercises.dart'; +import 'package:aitrainer_app/widgets/dialog_common.dart'; import 'package:aitrainer_app/widgets/exercise_save.dart'; import 'package:aitrainer_app/widgets/size_widget.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -45,8 +47,20 @@ class _ExerciseNewPageState extends State with Trans, Logging { child: BlocConsumer( listener: (context, state) { if (state is ExerciseNewError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); + showDialog( + context: context, + builder: (BuildContext context) { + return DialogCommon( + warning: true, + title: t("Warning"), + descriptions: t(state.message), + text: "OK", + onTap: () => Navigator.of(context).pushNamed("login"), + onCancel: () => { + Navigator.of(context).pop(), + }, + ); + }); } else if (state is ExerciseNewSaved) { final LinkedHashMap args = LinkedHashMap(); // ignore: close_sinks @@ -84,13 +98,25 @@ class _ExerciseNewPageState extends State with Trans, Logging { Widget getExerciseSaveWidget(ExerciseNewBloc exerciseBloc, ExerciseType exerciseType, MenuBloc menuBloc) { if (exerciseBloc.exerciseRepository.exerciseType!.name == "BMR") { - return BMR(exerciseBloc: exerciseBloc); + if (Cache().userLoggedIn == null) { + exerciseBloc.add(ExerciseNewAddError(message: "Please log in")); + } else { + return BMR(exerciseBloc: exerciseBloc); + } } if (exerciseBloc.exerciseRepository.exerciseType!.name == "BMI") { - return BMI(exerciseBloc: exerciseBloc); + if (Cache().userLoggedIn == null) { + exerciseBloc.add(ExerciseNewAddError(message: "Please log in")); + } else { + return BMI(exerciseBloc: exerciseBloc); + } } if (exerciseBloc.exerciseRepository.exerciseType!.name == "Sizes") { - return SizeWidget(exerciseBloc: exerciseBloc); + if (Cache().userLoggedIn == null) { + exerciseBloc.add(ExerciseNewAddError(message: "Please log in")); + } else { + return SizeWidget(exerciseBloc: exerciseBloc); + } } return Scaffold( @@ -143,6 +169,20 @@ class _ExerciseNewPageState extends State with Trans, Logging { // ignore: close_sinks final TestSetExecuteBloc? executeBloc = BlocProvider.of(context); + final TutorialBloc tutorialBloc = BlocProvider.of(context); + if (tutorialBloc.isActive) { + final String checkText = "Save"; + if (!tutorialBloc.checkAction(checkText)) { + return; + } + if (Cache().userLoggedIn != null) { + saveAll(bloc); + return; + } else { + Navigator.of(context).pushNamed("registration"); + } + } + if (executeBloc != null && executeBloc.existsActivePlan() == true) { confirmationOverride(bloc); } else { @@ -203,14 +243,19 @@ class _ExerciseNewPageState extends State with Trans, Logging { TextButton( child: Text(t("Yes")), onPressed: () { - saveAll(bloc); - if (executeBloc.existsActivePlan() == true) { - executeBloc.add(TestSetExecuteExerciseFinished( - exerciseTypeId: bloc.exerciseRepository.exerciseType!.exerciseTypeId, - quantity: bloc.exerciseRepository.exercise!.quantity!, - unitQuantity: bloc.exerciseRepository.exercise!.unitQuantity!)); + if (Cache().userLoggedIn == null) { + Navigator.pop(context); + bloc.add(ExerciseNewAddError(message: "Please log in")); + } else { + saveAll(bloc); + if (executeBloc.existsActivePlan() == true) { + executeBloc.add(TestSetExecuteExerciseFinished( + exerciseTypeId: bloc.exerciseRepository.exerciseType!.exerciseTypeId, + quantity: bloc.exerciseRepository.exercise!.quantity!, + unitQuantity: bloc.exerciseRepository.exercise!.unitQuantity!)); + } + Navigator.pop(context); } - Navigator.pop(context); }, ) ], diff --git a/lib/view/login.dart b/lib/view/login.dart index 608e509..ee6397e 100644 --- a/lib/view/login.dart +++ b/lib/view/login.dart @@ -5,6 +5,7 @@ import 'package:aitrainer_app/bloc/login/login_bloc.dart'; import 'package:aitrainer_app/repository/user_repository.dart'; import 'package:aitrainer_app/util/trans.dart'; import 'package:aitrainer_app/widgets/app_bar_min.dart'; +import 'package:aitrainer_app/widgets/dialog_common.dart'; import 'package:aitrainer_app/widgets/dialog_long.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; @@ -33,6 +34,21 @@ class LoginPage extends StatelessWidget with Trans { SnackBar(backgroundColor: Colors.orange, content: Text(t(state.message), style: TextStyle(color: Colors.white)))); } else if (state is LoginSuccess) { Navigator.of(context).pushNamed('home'); + } else if (state is LoginSkipped) { + showDialog( + context: context, + builder: (BuildContext context) { + return DialogCommon( + title: t("No Login"), + descriptions: t("You will skip the login."), + description2: t("The app functionalitity will be restricted, but please take a tour!"), + text: "OK", + onTap: () => {Navigator.of(context).pushNamed('home')}, + onCancel: () => { + Navigator.of(context).pop(), + }, + ); + }); } }, builder: (context, state) { final loginBloc = BlocProvider.of(context); @@ -69,8 +85,17 @@ class LoginPage extends StatelessWidget with Trans { key: _scaffoldKey, child: Container( padding: const EdgeInsets.only(left: 20, right: 20), - child: ListView(shrinkWrap: false, padding: EdgeInsets.only(top: 150.0), children: [ - ListTile(title: Text(t("Login"), style: GoogleFonts.inter(fontSize: 24))), + child: ListView(shrinkWrap: false, padding: EdgeInsets.only(top: 10.0), children: [ + GestureDetector( + onTap: () => loginBloc.add(LoginSkip()), + child: Text( + t("Skip"), + textAlign: TextAlign.right, + style: GoogleFonts.inter(color: Colors.black, decoration: TextDecoration.underline), + )), + SizedBox( + height: 140, + ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -102,8 +127,9 @@ class LoginPage extends StatelessWidget with Trans { ], ), Divider(), - ListTile(title: Text(t("OR"), style: GoogleFonts.inter())), - Divider(), + SizedBox( + height: 50, + ), TextFormField( key: LibraryKeys.loginEmailField, decoration: InputDecoration( @@ -178,22 +204,31 @@ class LoginPage extends StatelessWidget with Trans { //Image.asset('asset/icon/gomb_zold_b-1.png', width: 100, height: 100), onPressed: () => {loginBloc.add(LoginSubmit())}), ]), - Divider( - color: Colors.transparent, + SizedBox( + height: 50, ), Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ InkWell( - child: Text(t('SignUpLink')), + child: Text( + t('SignUpLink'), + style: GoogleFonts.inter(color: Colors.black, decoration: TextDecoration.underline), + ), onTap: () => Navigator.of(context).pushNamed('registration'), ), Spacer(flex: 2), InkWell( - child: Text(t('I forgot the password')), + child: Text( + t('I forgot the password'), + style: GoogleFonts.inter(color: Colors.black, decoration: TextDecoration.underline), + ), onTap: () => Navigator.of(context).pushNamed('resetPassword'), ), Spacer(flex: 2), InkWell( - child: Text(t('Privacy')), + child: Text( + t('Privacy'), + style: GoogleFonts.inter(color: Colors.black, decoration: TextDecoration.underline), + ), onTap: () => { showDialog( context: context, diff --git a/lib/view/registration.dart b/lib/view/registration.dart index 24590f9..5c3d66b 100644 --- a/lib/view/registration.dart +++ b/lib/view/registration.dart @@ -33,7 +33,6 @@ class RegistrationPage extends StatelessWidget with Trans { ScaffoldMessenger.of(context).showSnackBar( SnackBar(backgroundColor: Colors.orange, content: Text(t(state.message), style: TextStyle(color: Colors.white)))); } else if (state is LoginSuccess) { - //Navigator.of(context).pushNamed('customerModifyPage'); showDialog( context: context, builder: (BuildContext context) { @@ -48,6 +47,21 @@ class RegistrationPage extends StatelessWidget with Trans { }, ); }); + } else if (state is LoginSkipped) { + showDialog( + context: context, + builder: (BuildContext context) { + return DialogCommon( + title: t("No Registration"), + descriptions: t("You will skip the registration process."), + description2: t("Please take a short tour in the app"), + text: "OK", + onTap: () => {Navigator.of(context).pushNamed('home')}, + onCancel: () => { + Navigator.of(context).pop(), + }, + ); + }); } }, builder: (context, state) { final loginBloc = BlocProvider.of(context); @@ -84,7 +98,17 @@ class RegistrationPage extends StatelessWidget with Trans { key: _scaffoldKey, child: Container( padding: const EdgeInsets.only(left: 20, right: 20), - child: ListView(shrinkWrap: false, padding: EdgeInsets.only(top: 150.0), children: [ + child: ListView(shrinkWrap: false, padding: EdgeInsets.only(top: 10.0), children: [ + GestureDetector( + onTap: () => loginBloc.add(LoginSkip()), + child: Text( + t("Skip"), + textAlign: TextAlign.right, + style: GoogleFonts.inter(color: Colors.black, decoration: TextDecoration.underline), + )), + SizedBox( + height: 120, + ), ListTile(title: Text(t("SignUp"), style: GoogleFonts.inter())), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -116,7 +140,10 @@ class RegistrationPage extends StatelessWidget with Trans { : Offstage(), ], ), - ListTile(title: Text(t("OR"), style: GoogleFonts.inter())), + //ListTile(title: Text(t("OR"), style: GoogleFonts.inter())), + Divider( + color: Colors.transparent, + ), TextFormField( key: LibraryKeys.loginEmailField, decoration: InputDecoration( @@ -176,6 +203,7 @@ class RegistrationPage extends StatelessWidget with Trans { color: Colors.transparent, ), getDataProtection(loginBloc), + getEmailSubscription(loginBloc), Divider( color: Colors.transparent, ), @@ -203,12 +231,18 @@ class RegistrationPage extends StatelessWidget with Trans { ), Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ InkWell( - child: Text(t('Login')), + child: Text( + t('Login'), + style: GoogleFonts.inter(decoration: TextDecoration.underline), + ), onTap: () => Navigator.of(context).pushNamed('login'), ), Spacer(flex: 2), InkWell( - child: Text(t('Privacy')), + child: Text( + t('Privacy'), + style: GoogleFonts.inter(decoration: TextDecoration.underline), + ), onTap: () => { showDialog( context: context, @@ -234,4 +268,18 @@ class RegistrationPage extends StatelessWidget with Trans { controlAffinity: ListTileControlAffinity.leading, // <-- leading Checkbox ); } + + Widget getEmailSubscription(LoginBloc loginBloc) { + return CheckboxListTile( + title: Text(t("Email notifications")), + subtitle: Text(t("We may ask you about your opinion, send events in email")), + dense: true, + value: loginBloc.emailSubscription, + activeColor: Colors.indigo, + onChanged: (value) { + loginBloc.add(DataProtectionClicked(marked: value!)); + }, + controlAffinity: ListTileControlAffinity.leading, // <-- leading Checkbox + ); + } } diff --git a/lib/view/settings.dart b/lib/view/settings.dart index c5cf213..7518021 100644 --- a/lib/view/settings.dart +++ b/lib/view/settings.dart @@ -1,5 +1,6 @@ import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; import 'package:aitrainer_app/bloc/settings/settings_bloc.dart'; +import 'package:aitrainer_app/bloc/tutorial/tutorial_bloc.dart'; import 'package:aitrainer_app/library/custom_icon_icons.dart'; import 'package:aitrainer_app/util/app_language.dart'; import 'package:aitrainer_app/model/cache.dart'; @@ -43,6 +44,7 @@ class SettingsPage extends StatelessWidget with Trans { SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); } else if (state is SettingsReady) { menuBloc.add(MenuRecreateTree()); + Navigator.of(context).pushNamed("home"); } }, builder: (context, state) { return ModalProgressHUD( @@ -76,6 +78,7 @@ class SettingsPage extends StatelessWidget with Trans { Track().track(TrackingEvent.settings_lang, eventValue: lang) })), getServer(settingsBloc), + getTuturialBasic(settingsBloc), //getDevice(settingsBloc), ]); } @@ -131,4 +134,27 @@ class SettingsPage extends StatelessWidget with Trans { ), ); } + + ListTile getTuturialBasic(SettingsBloc settingsBloc) { + final TutorialBloc tutorialBloc = BlocProvider.of(context); + return ListTile( + leading: Icon(CustomIcon.question_circle), + subtitle: Text("Activating the basic tutorial"), + title: ToggleSwitch( + minWidth: 120.0, + minHeight: 30.0, + fontSize: 14.0, + initialLabelIndex: 0, + activeBgColor: Colors.indigo, + activeFgColor: Colors.white, + inactiveBgColor: Colors.white60, + inactiveFgColor: Colors.grey[900], + labels: [t('Basic Tutorial'), t('Activate')], + onToggle: (index) { + settingsBloc.add(SettingsActivateTutorial(activity: ActivityDone.tutorialBasic)); + tutorialBloc.add(TutorialStart()); + }, + ), + ); + } } diff --git a/lib/view/test_set_edit.dart b/lib/view/test_set_edit.dart index 70ab97f..3801f75 100644 --- a/lib/view/test_set_edit.dart +++ b/lib/view/test_set_edit.dart @@ -26,7 +26,7 @@ class TestSetEdit extends StatelessWidget with Trans { final String templateNameTranslation = args['templateNameTranslation']; // ignore: close_sinks final MenuBloc menuBloc = BlocProvider.of(context); - late TestSetEditBloc? bloc; + late TestSetEditBloc bloc; final bool activeExercisePlan = Cache().activeExercisePlan != null; setContext(context); @@ -51,8 +51,20 @@ class TestSetEdit extends StatelessWidget with Trans { menuBloc: menuBloc), child: BlocConsumer(listener: (context, state) { if (state is TestSetEditError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white)))); + showDialog( + context: context, + builder: (BuildContext context) { + return DialogCommon( + warning: true, + title: t("Warning"), + descriptions: t(state.message), + text: "OK", + onTap: () => Navigator.of(context).pushNamed("login"), + onCancel: () => { + Navigator.of(context).pop(), + }, + ); + }); } else if (state is TestSetEditSaved) { Navigator.of(context).pop(); Navigator.of(context).pushNamed("testSetExecute"); @@ -60,7 +72,7 @@ class TestSetEdit extends StatelessWidget with Trans { }, builder: (context, state) { bloc = BlocProvider.of(context); return ModalProgressHUD( - child: getTestSetWidget(bloc!, templateNameTranslation), + child: getTestSetWidget(bloc, templateNameTranslation), inAsyncCall: state is TestSetEditLoading, opacity: 0.5, color: Colors.black54, @@ -69,36 +81,40 @@ class TestSetEdit extends StatelessWidget with Trans { }))), floatingActionButton: FloatingActionButton.extended( onPressed: () { - if (activeExercisePlan) { - showCupertinoDialog( - useRootNavigator: true, - context: context, - builder: (_) => CupertinoAlertDialog( - title: Text(t("You have an active Test Set!") + "\n" + Cache().activeExercisePlan!.name), - content: Column(children: [ - Divider(), - Text(t("Do you want to override it?"), style: GoogleFonts.inter(color: Colors.black, fontSize: 16)), - ]), - actions: [ - TextButton( - child: Text(t("No, bring me there"), textAlign: TextAlign.center), - onPressed: () => { - Navigator.pop(context), - Navigator.pop(context), - Navigator.of(context).pushNamed("testSetExecute"), - }, - ), - TextButton( - child: Text(t("Yes")), - onPressed: () { - Navigator.pop(context); - startTrainingDialog(bloc); - }, - ) - ], - )); + if (Cache().userLoggedIn == null) { + bloc.add(TestSetEditAddError(message: "Please log in")); } else { - startTrainingDialog(bloc); + if (activeExercisePlan) { + showCupertinoDialog( + useRootNavigator: true, + context: context, + builder: (_) => CupertinoAlertDialog( + title: Text(t("You have an active Test Set!") + "\n" + Cache().activeExercisePlan!.name), + content: Column(children: [ + Divider(), + Text(t("Do you want to override it?"), style: GoogleFonts.inter(color: Colors.black, fontSize: 16)), + ]), + actions: [ + TextButton( + child: Text(t("No, bring me there"), textAlign: TextAlign.center), + onPressed: () => { + Navigator.pop(context), + Navigator.pop(context), + Navigator.of(context).pushNamed("testSetExecute"), + }, + ), + TextButton( + child: Text(t("Yes")), + onPressed: () { + Navigator.pop(context); + startTrainingDialog(bloc); + }, + ) + ], + )); + } else { + startTrainingDialog(bloc); + } } }, backgroundColor: Colors.orange[800], @@ -249,7 +265,9 @@ class TestSetEdit extends StatelessWidget with Trans { child: ClipRRect( borderRadius: BorderRadius.circular(24.0), child: GestureDetector( - onTap: () => bloc.add(TestSetEditAddExerciseType(indexKey: index)), + onTap: () { + bloc.add(TestSetEditAddExerciseType(indexKey: index)); + }, child: Container( color: Colors.yellow[700], child: Center( diff --git a/lib/widgets/dialog_common.dart b/lib/widgets/dialog_common.dart index 4d3423f..e6db5df 100644 --- a/lib/widgets/dialog_common.dart +++ b/lib/widgets/dialog_common.dart @@ -9,6 +9,7 @@ class DialogCommon extends StatefulWidget { final VoidCallback? onCancel; String? description2, description3; final Image? img; + final bool warning; DialogCommon( {Key? key, @@ -19,7 +20,8 @@ class DialogCommon extends StatefulWidget { required this.text, this.img, required this.onTap, - required this.onCancel}) + required this.onCancel, + this.warning = false}) : super(key: key) { description2 = description2 ?? ""; description3 = description3 ?? ""; @@ -72,11 +74,11 @@ class _DialogPremiumState extends State with Trans { alignment: AlignmentDirectional.topEnd, children: [ Text( - widget.title + " ", + widget.title, textAlign: TextAlign.center, style: GoogleFonts.archivoBlack( fontSize: 20, - color: Colors.yellow[400], + color: widget.warning ? Colors.red[800] : Colors.yellow[400], shadows: [ Shadow( offset: Offset(5.0, 5.0), @@ -173,7 +175,9 @@ class _DialogPremiumState extends State with Trans { child: Stack( alignment: Alignment.center, children: [ - Image.asset('asset/icon/gomb_orange_c.png', width: 100, height: 45), + widget.warning + ? Image.asset('asset/icon/gomb_pink_b.png', width: 100, height: 45) + : Image.asset('asset/icon/gomb_orange_c.png', width: 100, height: 45), Text( t("OK"), style: TextStyle(fontSize: 16, color: Colors.white), diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 22dce82..775d588 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -2,9 +2,11 @@ import 'package:aitrainer_app/bloc/session/session_bloc.dart'; import 'package:aitrainer_app/bloc/settings/settings_bloc.dart'; import 'package:aitrainer_app/model/cache.dart'; import 'package:aitrainer_app/service/logging.dart'; +import 'package:aitrainer_app/util/trans.dart'; import 'package:aitrainer_app/view/login.dart'; import 'package:aitrainer_app/view/menu_page.dart'; import 'package:aitrainer_app/view/registration.dart'; +import 'package:aitrainer_app/widgets/dialog_common.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -22,7 +24,7 @@ class AitrainerHome extends StatefulWidget { } } -class _HomePageState extends State with Logging { +class _HomePageState extends State with Logging, Trans { GlobalKey _scaffoldKey = new GlobalKey(); @override @@ -50,16 +52,26 @@ class _HomePageState extends State with Logging { @override Widget build(BuildContext context) { + setContext(context); return Scaffold( key: _scaffoldKey, body: BlocConsumer(listener: (context, state) { if (state is SessionFailure) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - state.message, - ), - backgroundColor: Colors.orange, - )); + showDialog( + context: context, + builder: (BuildContext context) { + return DialogCommon( + title: t("Error"), + descriptions: t(state.message), + text: "OK", + onTap: () => { + Navigator.of(context).pop(), + }, + onCancel: () => { + Navigator.of(context).pop(), + }, + ); + }); } }, builder: (context, state) { if (state is SessionInitial) { @@ -78,8 +90,8 @@ class _HomePageState extends State with Logging { } } else { log("home: unknown state"); - //return MenuPage(parent: 0); - return LoginPage(); + return MenuPage(parent: 0); + //return LoginPage(); } }), ); diff --git a/lib/widgets/menu_page_widget.dart b/lib/widgets/menu_page_widget.dart index 266c10e..813378a 100644 --- a/lib/widgets/menu_page_widget.dart +++ b/lib/widgets/menu_page_widget.dart @@ -1,8 +1,8 @@ import 'dart:collection'; import 'dart:ui'; +import 'package:aitrainer_app/bloc/tutorial/tutorial_bloc.dart'; import 'package:aitrainer_app/model/exercise_ability.dart'; import 'package:aitrainer_app/bloc/menu/menu_bloc.dart'; -import 'package:aitrainer_app/library/custom_icon_icons.dart'; import 'package:aitrainer_app/util/enums.dart'; import 'package:aitrainer_app/util/track.dart'; import 'package:aitrainer_app/widgets/menu_image.dart'; @@ -14,6 +14,7 @@ import 'package:aitrainer_app/model/workout_menu_tree.dart'; import 'package:aitrainer_app/service/logging.dart'; import 'package:aitrainer_app/util/trans.dart'; import 'package:aitrainer_app/widgets/dialog_common.dart'; +import 'package:aitrainer_app/widgets/tutorial_widget.dart'; import 'package:badges/badges.dart'; import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/cupertino.dart'; @@ -39,6 +40,7 @@ class _MenuPageWidgetState extends State with Trans, Logging { final double baseWidth = 312; final double baseHeight = 675.2; late MenuBloc menuBloc; + late TutorialBloc tutorialBloc; final scrollController = ScrollController(); final bool activeExercisePlan = Cache().activeExercisePlan != null; final EzAnimation animation = EzAnimation(35.0, 10.0, Duration(seconds: 2), reverseCurve: Curves.linear); @@ -52,29 +54,52 @@ class _MenuPageWidgetState extends State with Trans, Logging { animation.reverse(); } }); - animation.addListener(() { - //setState(() {}); - }); + animation.addListener(() {}); } /// We require the initializers to run after the loading screen is rendered SchedulerBinding.instance!.addPostFrameCallback((_) { menuBloc.add(MenuCreate()); + //runDelayedEvent(); }); super.initState(); } + Future runDelayedEvent() async { + print("runDelayedEvent start"); + bool isFirst = false; + await Future.delayed(Duration(milliseconds: 600), () async { + if (tutorialBloc.isActive == false) { + print("Activate tutorial"); + tutorialBloc.canActivate = true; + tutorialBloc.isActive = true; + tutorialBloc.menuBloc = menuBloc; + tutorialBloc.add(TutorialLoad()); + tutorialBloc.init(); + isFirst = true; + } + }); + final bool canActivate = tutorialBloc.activateTutorial(); + if (canActivate) { + if (!isFirst) { + TutorialWidget().tip(context); + } + } + } + @override bool didUpdateWidget(MenuPageWidget oldWidget) { super.didUpdateWidget(oldWidget); scrollController.animateTo(5, duration: Duration(milliseconds: 300), curve: Curves.easeIn); + runDelayedEvent(); return true; } @override Widget build(BuildContext context) { menuBloc = BlocProvider.of(context); + tutorialBloc = BlocProvider.of(context); setContext(context); double cWidth = MediaQuery.of(context).size.width; double cHeight = MediaQuery.of(context).size.height; @@ -83,11 +108,12 @@ class _MenuPageWidgetState extends State with Trans, Logging { widget.parent = 0; } - return CustomScrollView( - // Must add scrollController to sliver root - controller: scrollController, - scrollDirection: Axis.vertical, - slivers: buildMenuColumn(widget.parent!, context, menuBloc, cWidth, cHeight)); + return Stack(children: [ + CustomScrollView( + controller: scrollController, + scrollDirection: Axis.vertical, + slivers: buildMenuColumn(widget.parent!, context, menuBloc, cWidth, cHeight)), + ]); } List buildMenuColumn(int parent, BuildContext context, MenuBloc menuBloc, double cWidth, double cHeight) { @@ -151,19 +177,12 @@ class _MenuPageWidgetState extends State with Trans, Logging { child: Stack(alignment: Alignment.bottomLeft, children: [ TextButton( child: badgedIcon(workoutTree, cWidth, cHeight), - onPressed: () => menuClick(workoutTree, menuBloc, context), + onPressed: () => menuClick(workoutTree, menuBloc), ), - /* Container( - padding: EdgeInsets.only(left: 5, bottom: 5, right: 5), - height: 80, - child: Container( - color: Colors.black.withOpacity(0.2), - ), - ), */ Container( padding: EdgeInsets.only(left: 15, bottom: 8, right: 15), child: GestureDetector( - onTap: () => menuClick(workoutTree, menuBloc, context), + onTap: () => menuClick(workoutTree, menuBloc), child: Text( workoutTree.name, maxLines: 4, @@ -174,19 +193,6 @@ class _MenuPageWidgetState extends State with Trans, Logging { ]))))); }); } - /* LiveSliverList sliverList = LiveSliverList( - // And attach root sliver scrollController to widgets - controller: scrollController, - - itemCount: _columnChildren.length, - reAnimateOnVisibility: false, - showItemDuration: Duration(milliseconds: 100), - itemBuilder: (BuildContext context, int index, Animation animation) => FadeTransition( - opacity: animation, - child: _columnChildren[index], - ), - */ -//delegate: SliverChildListDelegate(_columnChildren), SliverList sliverList = SliverList( delegate: SliverChildListDelegate(_columnChildren), @@ -273,7 +279,6 @@ class _MenuPageWidgetState extends State with Trans, Logging { SliverAppBar getInfoWidget(BuildContext context, MenuBloc menuBloc) { menuBloc.setContext(context); - menuBloc.setMenuInfo(); SliverAppBar sliverAppBar = SliverAppBar( automaticallyImplyLeading: false, @@ -286,28 +291,6 @@ class _MenuPageWidgetState extends State with Trans, Logging { : SizedBox( width: 10, ), - GestureDetector( - onTap: () => { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return DialogCommon( - title: menuBloc.infoTitle, - descriptions: menuBloc.infoText, - description2: menuBloc.infoText2, - description3: menuBloc.infoText3, - text: "OK", - onTap: () => {Navigator.of(context).pop()}, - onCancel: () => {Navigator.of(context).pop()}, - ); - }) - }, - child: Icon( - CustomIcon.question_circle, - color: Colors.orange[400], - size: 40, - )), MenuSearchBar( listItems: menuBloc.menuTreeRepository.menuAsExercise, onFind: (value) { @@ -365,28 +348,28 @@ class _MenuPageWidgetState extends State with Trans, Logging { return sliverAppBar; } - void menuClick(WorkoutMenuTree workoutTree, MenuBloc menuBloc, BuildContext context) { - if (Cache().userLoggedIn == null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - backgroundColor: Colors.orange, - content: Text(AppLocalizations.of(context)!.translate('Please log in'), style: TextStyle(color: Colors.white)))); + void menuClick(WorkoutMenuTree workoutTree, MenuBloc menuBloc) { + if (tutorialBloc.isActive) { + final String checkText = workoutTree.nameEnglish; + if (!tutorialBloc.checkAction(checkText)) { + return; + } + } + if (workoutTree.child == false) { + if (menuBloc.ability != null && ExerciseAbility.mini_test_set.equalsTo(menuBloc.ability!) && workoutTree.parent != 0) { + HashMap args = HashMap(); + args['templateName'] = workoutTree.nameEnglish; + args['templateNameTranslation'] = workoutTree.name; + Navigator.of(context).pushNamed('testSetEdit', arguments: args); + } + menuBloc.add(MenuTreeDown(item: workoutTree, parent: workoutTree.id)); } else { - if (workoutTree.child == false) { - if (menuBloc.ability != null && ExerciseAbility.mini_test_set.equalsTo(menuBloc.ability!) && workoutTree.parent != 0) { - HashMap args = HashMap(); - args['templateName'] = workoutTree.nameEnglish; - args['templateNameTranslation'] = workoutTree.name; - Navigator.of(context).pushNamed('testSetEdit', arguments: args); - } - menuBloc.add(MenuTreeDown(item: workoutTree, parent: workoutTree.id)); - } else { - menuBloc.add(MenuClickExercise(exerciseTypeId: workoutTree.id)); + menuBloc.add(MenuClickExercise(exerciseTypeId: workoutTree.id)); - if (workoutTree.exerciseType!.name == "Custom" && Cache().userLoggedIn!.admin == 1) { - Navigator.of(context).pushNamed('exerciseCustomPage', arguments: workoutTree.exerciseType); - } else { - Navigator.of(context).pushNamed('exerciseNewPage', arguments: workoutTree.exerciseType); - } + if (workoutTree.exerciseType!.name == "Custom" && Cache().userLoggedIn!.admin == 1) { + Navigator.of(context).pushNamed('exerciseCustomPage', arguments: workoutTree.exerciseType); + } else { + Navigator.of(context).pushNamed('exerciseNewPage', arguments: workoutTree.exerciseType); } } } diff --git a/lib/widgets/tutorial_widget.dart b/lib/widgets/tutorial_widget.dart new file mode 100644 index 0000000..e0c8441 --- /dev/null +++ b/lib/widgets/tutorial_widget.dart @@ -0,0 +1,152 @@ +import 'package:aitrainer_app/bloc/tutorial/tutorial_bloc.dart'; +import 'package:aitrainer_app/service/logging.dart'; +import 'package:aitrainer_app/util/trans.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/style.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:aitrainer_app/library/super_tooltip.dart'; + +class TutorialWidget with Trans, Logging { + static final TutorialWidget _singleton = TutorialWidget._internal(); + SuperTooltip? tooltip; + TutorialWidget._internal(); + factory TutorialWidget() { + return _singleton; + } + + void close() { + if (tooltip != null && tooltip!.isOpen) { + tooltip!.close(); + } + } + + void tip(BuildContext context) { + setContext(context); + var renderBox = context.findRenderObject() as RenderBox; + final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox?; + + var targetGlobalCenter = renderBox.localToGlobal(renderBox.size.center(Offset.zero), ancestor: overlay); + + final TutorialBloc bloc = BlocProvider.of(context); + + if (tooltip != null && tooltip!.isOpen) { + tooltip!.rebuild(); + } + + Rect? area; + if (bloc.action != null) { + area = bloc.action!.showBubble == true + ? Rect.fromLTWH(targetGlobalCenter.dx - bloc.action!.bubbleX, targetGlobalCenter.dy - bloc.action!.bubbleY, + bloc.action!.bubbleWidth.toDouble(), bloc.action!.bubbleHeight.toDouble()) + : null; + } + + final double mediaSize = MediaQuery.of(context).size.width; + + double fontSize = 14; + if (mediaSize > 400) { + fontSize = 15; + } else if (mediaSize < 375) { + fontSize = 13; + } + tooltip = SuperTooltip( + top: bloc.top, + left: bloc.left, + backgroundColor: Colors.black.withOpacity(0.7), + popupDirection: bloc.action == null || bloc.action!.direction == "up" ? TooltipDirection.up : TooltipDirection.down, + maxWidth: 390, + minWidth: 300, + minHeight: 100, + maxHeight: 300, + borderColor: Colors.orange, + borderWidth: 1.0, + minimumOutSidePadding: 20, + snapsFarAwayVertically: false, + showCloseButton: ShowCloseButton.inside, + closeButtonColor: Colors.grey, + dismissOnTapOutside: false, + outsideBackgroundColor: Colors.black.withOpacity(0.6), + hasShadow: true, + touchThrougArea: area, + onClose: () => bloc.add(TutorialFinished()), + custom: true, + touchThroughAreaShape: ClipAreaShape.oval, + content: new Material( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Stack(alignment: Alignment.bottomRight, children: [ + SingleChildScrollView( + child: Html( + data: bloc.actualText! + "


", + //Optional parameters: + style: { + "p": Style( + color: Colors.white, + fontSize: FontSize(fontSize), + padding: const EdgeInsets.all(4), + ), + "li": Style( + color: Colors.white, + fontSize: FontSize(fontSize), + padding: const EdgeInsets.only(left: 4), + ), + "h2": Style( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: FontSize.larger, + //padding: const EdgeInsets.all(4), + ), + "h1": Style( + color: Colors.yellow[400], + fontWeight: FontWeight.bold, + fontSize: FontSize.larger, + alignment: Alignment.center, + padding: const EdgeInsets.all(4), + ), + }, + )), + bloc.showCheckText + ? bloc.checks.length > 1 + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.transparent, + ), + onPressed: () => {bloc.add(TutorialNext(text: bloc.checks[0]))}, + child: Text("« " + t(bloc.checks[0]), + style: GoogleFonts.archivoBlack(color: Colors.orange[400]!, fontSize: fontSize)), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.transparent, + ), + onPressed: () => {bloc.add(TutorialNext(text: bloc.checks[1]))}, + child: Text(t(bloc.checks[1]) + " »", + style: GoogleFonts.archivoBlack(color: Colors.orange[400]!, fontSize: fontSize)), + ), + ], + ) + : ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.transparent, + ), + onPressed: () => { + //tooltip!.rebuild(context), + bloc.add(TutorialNext(text: bloc.checks[0])), + }, + child: Text(t(bloc.checks[0]) + " »", + style: GoogleFonts.archivoBlack(color: Colors.orange[400]!, fontSize: fontSize)), + ) + : Offstage(), + ]), + )), + ); + + tooltip!.show(context); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index ba3742b..24c2cd8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.1.13+67 +version: 1.1.14+68 environment: sdk: ">=2.12.0 <3.0.0" @@ -27,7 +27,7 @@ dependencies: cupertino_icons: ^1.0.0 google_fonts: ^2.0.0 devicelocale: ^0.4.1 - sentry: ^5.0.0 + sentry_flutter: ^5.0.0 flutter_bloc: ^7.0.0 equatable: ^2.0.0 @@ -49,13 +49,15 @@ dependencies: purchases_flutter: ^3.2.1 package_info: ^2.0.0 ezanimation: ^0.5.0 + flutter_fadein: ^2.0.0 confetti: ^0.6.0-nullsafety crypto: ^3.0.0 carousel_slider: ^4.0.0-nullsafety.0 #dropdown_search: ^0.5.0 convex_bottom_bar: ^3.0.0 - - + flutter_app_badger: ^1.2.0 + #super_tooltip: ^1.0.1 + firebase_core: ^1.0.3 firebase_analytics: ^8.0.0-dev.2 firebase_messaging: ^9.1.1 @@ -65,10 +67,11 @@ dependencies: google_sign_in: ^5.0.1 apple_sign_in: ^0.1.0 - smartlook: ^1.0.7 + #smartlook: ^1.0.7 flurry: ^0.0.4 + flutter_uxcam: ^1.3.2 - #animated_widgets: ^1.0.6 + animated_widgets: ^1.0.6 mockito: ^5.0.3 sqflite: ^2.0.0+3 @@ -139,8 +142,6 @@ flutter: - asset/image/WT_sales_background.jpg - asset/image/WT_sales_background_3x5.jpg - asset/image/WT_menu_dark.jpg - - asset/image/Gain_muscle.jpg - - asset/image/WT_weight_loss.jpg - asset/image/WT_welcome.jpg - asset/image/WT_Results_for_runners.jpg - asset/image/WT_Results_for_female.jpg @@ -156,14 +157,22 @@ flutter: - asset/image/testemfejl400x400.jpg - asset/image/izomcsop400400.jpg - asset/image/edzesnaplom400400.jpg + - asset/image/endurance.jpg - asset/image/exercise_plan_stars.jpg - asset/image/exercise_plan_special.jpg - asset/image/exercise_plan_execute.jpg - asset/image/exercise_plan_custom.jpg - asset/image/exercise_plan_suggested.jpg + - asset/image/explosiveness.jpg + - asset/image/flexibility.jpg - asset/image/predictions.jpg - asset/image/man_sizes.png + - asset/image/gain_muscle.jpg + - asset/image/gain_strength.jpg + - asset/image/muscle_endurance.jpg + - asset/image/shape_forming.jpg - asset/image/woman_sizes.png + - asset/image/weight_loss.jpg - asset/image/merleg.png - asset/image/BMI_graph_C.png - asset/image/BMI_mutato.png